Svelte Ui Admin後臺管理系統|svelte3+svelteUI中後臺前端解決方案

2022-09-17 06:01:43

基於svelte3.x+svelteKit+svelte-ui網頁後臺管理系統SvelteAdmin

Svelte-Ui-Admin 基於svelte3.x+svelteKit+vite3+echarts搭配使用Svelte UI元件庫開發的一套輕量級前端中後臺管理系統解決方案。Svelte Ui Admin遵循Svelte Ui元件設計和開發規範,高顏值的元件讓整體風格細膩統一。

+layout.svelte整體分為頂部欄+左側選單+右側主體內容三大板塊。

<div class="svadmin__container" style="--themeSkin: {$skin}">
    <div class="svadmin__wrapper-layout flexbox flex-col">
        <div class="sv__layout-header">
            <Header />
        </div>
        
        <div class="sv__layout-body flex1 flexbox">
            <!-- //側邊欄 -->
            {#if rootRouteEnable}
            <div class="sv__bd-sidebar">
                <SideMenu routes={mainRoutes} {activeRoute} />
            </div>
            {/if}

            {#if (rootRouteEnable && route != '/') || !rootRouteEnable}
            <div class="sv__bd-menus" class:collapsed={collapsed&&!rootRouteEnable}>
                <RouteMenu
                    routes={getAllRoutes}
                    {activeRoute}
                    {activeRootRoute}
                    {rootRouteEnable}
                    {collapsed}
                />
            </div>
            {/if}

            <div class="sv__bd-main flex1 flexbox flex-col">
                <!-- 麵包屑導航 -->
                <BreadCrumb routes={getAllRoutes} {activeRoute} {activeRootRoute} />
                
                <!-- 主內容區 -->
                <Scrollbar autohide gap={2}>
                    <div class="sv__main-wrapper">
                        <slot />
                    </div>
                </Scrollbar>
            </div>
        </div>
    </div>
</div>

+error.svelte錯誤頁處理。

<script>
    import { page } from '$app/stores'
    import { goto } from '$app/navigation'
    import { Button } from '$lib/svelte-ui'

    function goHome() {
        goto('/home/index')
    }
</script>

<svelte:head>
    <title>{$page.status} Error!</title>
</svelte:head>

<div class="svadmin__pageErr flexbox flex-col flex-alignc flex-justifyc">
    <div class="svadmin__pageErr-img"><i class="sv-icon-round_close_fill_light"></i></div>
    <div class="svadmin__pageErr-content">
        <div class="c-red fs-18">┗| {$page.status} |┛  Page Error~~</div>
        <div class="c-999 mt-10 mb-10">{$page.error.message}</div>
        <Button size="small" on:click={goHome}>Go Home</Button>
    </div>
</div>

自定義路由選單Menu

如上圖:使用了svelte-ui元件庫中的Menu元件來動態生成選單。

<Menu
    class="svadmin__menu-list"
    data={getNewRoutes}
    active={activeRoute}
    trigger="click"
    collapse={collapsed}
    backgroundHover="#e2f2ff"
>
    {#each getNewRoutes as route}
        <svelte:component this={routeMenuItem} item={route} {activeRootRoute} {rootRouteEnable} />
    {/each}
</Menu>
<!-- 二級選單 -->
<script>
    import { goto } from '$app/navigation'
    import { _ } from 'svelte-i18n'

    import { Menu, MenuItem, MenuSub } from '$lib/svelte-ui'
    import routeMenuItem from './routeMenuItem.svelte'

    import utils from '@/utils'
    import { getCurrentRootRoute, hasChildrenRoute } from '@/utils/routes'

    export let item = []
    // 根選單
    export let activeRootRoute = ''
    // 是否開啟一級路由選單
    export let rootRouteEnable = true

    function changeRoute(path) {
        if(utils.isExternal(path)) {
            window.open(path, '_blank')
        }else {
            goto(path)
        }
    }
</script>

{#if !item.meta.hidden}
    {#if activeRootRoute !== getCurrentRootRoute(item) && rootRouteEnable === true}
    <div></div>
    {:else}
        {#if item.children && Array.isArray(item.children) && hasChildrenRoute(item.children)}
            <MenuSub key={item.key}>
                <span slot="icon"><i class={item.meta.icon}></i></span>
                <div slot="title">{$_(`menu.${item.meta.title}`)}</div>

                {#each item.children || [] as route2}
                <svelte:component this={routeMenuItem} item={route2} {activeRootRoute} {rootRouteEnable} />
                {/each}
            </MenuSub>
        {:else}
            <MenuItem key={item.key} title={$_(`menu.${item.meta.title}`)} on:click={changeRoute(item.path)}></MenuItem>
        {/if}
    {/if}
{/if}
/**
 * 路由選單Layout.js
 */
export function load() {
    return {
        mainRoutes: [
            // 主頁模組
            {
                key: 'home', // 標識Menu元件匹配路徑
                path: '/home', // 跳轉路由
                redirect: '/home/index', // 重定向路由
                meta: {
                    auth: true, // 是否驗證狀態
                    icon: 'sv-icon-homefill', // 路由圖示
                    title: 'layouts__main-menu__home', // 路由標題
                    hidden: false, //是否隱藏選單項
                },
                children: [
                    // 首頁
                    {
                        key: 'home_index',
                        path: 'index',
                        meta: {
                            auth: true,
                            icon: 'sv-icon-home',
                            title: 'layouts__main-menu__home_index'
                        }
                    },
                    // 工作臺
                    {
                        key: 'home_workplace',
                        path: 'workplace',
                        meta: {
                            auth: true,
                            icon: 'sv-icon-dashboard',
                            title: 'layouts__main-menu__home_dashboard'
                        }
                    },
                    // 自定義麵包屑
                    {
                        key: 'home_breadcrumb',
                        path: 'breadcrumb',
                        meta: {
                            auth: true,
                            icon: 'sv-icon-breadcrumb',
                            title: 'layouts__main-menu__home_breadcrumb',
                            // 自定義麵包屑
                            breadcrumb: [
                                {
                                    meta: {title: 'layouts__main-menu__home_breadcrumb'},
                                    path: '/home/breadcrumb',
                                },
                                {
                                    meta: {title: 'layouts__main-menu__home'},
                                    path: '/home',
                                },
                                {
                                    meta: {title: 'layouts__main-menu__home_breadcrumb-links'},
                                }
                            ]
                        }
                    },
                    // 外部連結
                    {
                        key: 'https://svelte.dev/',
                        path: 'https://svelte.dev/',
                        meta: {
                            icon: 'sv-icon-openlink',
                            title: 'layouts__main-menu__home_apidocs',
                            rootRoute: '/home'
                        }
                    }
                ]
            },
        
            // 元件模組
            {
                key: 'component',
                path: '/component',
                redirect: '/component/table/all',
                meta: {
                    auth: true, //是否驗證狀態
                    icon: 'sv-icon-apps-fill',
                    title: 'layouts__main-menu__component',
                    hidden: false, //是否隱藏選單項
                },
                children: [
                    {
                        key: 'component_table',
                        path: 'table',
                        redirect: '/component/table/all',
                        meta: {
                            auth: true,
                            icon: 'sv-icon-table',
                            title: 'layouts__main-menu__component_table',
                        },
                        children: [
                            {
                                key: 'component_table_all',
                                path: 'all',
                                meta: {
                                    title: 'layouts__main-menu__component_table-all'
                                }
                            },
                            {
                                key: 'component_table_custom',
                                path: 'custom',
                                meta: {
                                    title: 'layouts__main-menu__component_table-custom'
                                }
                            },
                            {
                                key: 'component_table_search',
                                path: 'search',
                                redirect: '/component/table/search/list',
                                meta: {
                                    title: 'layouts__main-menu__component_table-search',
                                },
                                children: [
                                    {
                                        key: 'component_table_search_list',
                                        path: 'list',
                                        meta: {
                                            title: 'layouts__main-menu__component_table-search-list'
                                        }
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        key: 'component_list',
                        path: 'list',
                        meta: {
                            icon: 'sv-icon-sort',
                            title: 'layouts__main-menu__component_list',
                        }
                    },
                    {
                        key: 'component_form',
                        path: 'form',
                        redirect: '/component/form/all',
                        meta: {
                            auth: true,
                            icon: 'sv-icon-forms',
                            title: 'layouts__main-menu__component_form',
                        },
                        children: [
                            {
                                key: 'component_form_all',
                                path: 'all',
                                meta: {
                                    title: 'layouts__main-menu__component_form-all',
                                }
                            },
                            {
                                key: 'component_form_custom',
                                path: 'custom',
                                meta: {
                                    title: 'layouts__main-menu__component_form-custom',
                                }
                            }
                        ]
                    },
                    {
                        key: 'component_editor',
                        path: 'editor',
                        meta: {
                            icon: 'sv-icon-editor',
                            title: 'layouts__main-menu__component_editor',
                        }
                    }
                ]
            },
        
            // 設定模組
            {
                key: 'setting',
                path: '/setting',
                redirect: '/setting/mine',
                meta: {
                    icon: 'sv-icon-setting',
                    title: 'layouts__main-menu__setting',
                    hidden: false,
                },
                children: [
                    ...
                ]
            },
        
            // 許可權驗證模組
            {
                key: 'permission',
                path: '/permission',
                redirect: '/permission/all',
                meta: {
                    auth: true,
                    icon: 'sv-icon-secret',
                    title: 'layouts__main-menu__permission',
                    hidden: false,
                },
                children: [
                    ...
                ]
            },
        
            // 錯誤頁面模組
            {
                key: 'error',
                path: '/error',
                redirect: '/error/403',
                meta: {
                    icon: 'sv-icon-roundclosefill',
                    title: 'layouts__main-menu__error',
                    hidden: false,
                },
                children: [
                    ...
                ]
            },
        ]
    }
}

npm i svelte-i18n -D

/**
 * 國際化語言設定
 * @author YXY
 */

import { addMessages, init, getLocaleFromNavigator } from 'svelte-i18n'
import { browser } from '$app/env'
import Storage from '@/utils/storage'

// 引入語言設定
import cn from '@/locale/zh-CN'
import tw from '@/locale/zh-TW'
import en from '@/locale/en-US'

export const langKey = 'lang'
export const langVal = 'cn'

addMessages('cn', cn)
addMessages('tw', tw)
addMessages('en', en)

const lang = getLang()
console.log('當前國際化:', lang)
init({
    fallbackLocale: lang,
    initialLocale: getLocaleFromNavigator()
})
setHtmlLang(lang)

/* 獲取語言 */
export function getLang() {
    const lang = Storage.get(langKey)
    return lang || langVal
}

/* 持久化儲存 */
export function setLang(lang, reload = false) {
    if(lang != getLang()) {
        Storage.set(langKey, lang || '')
        setHtmlLang(lang)

        // 過載頁面
        if(reload) {
            window.location.reload()
        }
    }
}

svelte動態圖表Hooks

由於專案中多個地方使用了Echarts圖表元件,於是單獨抽離了一個hooks檔案來初始化echarts元件。

針對自適應圖表,則使用了 "element-resize-detector": "^1.2.4" 來實時監聽DOM尺寸改變。

/**
 * @title    動態圖表Hooks
 * @author    YXY
*/
import * as echarts from 'echarts'
import elementResizeDetector from "element-resize-detector"
import utils from '@/utils'

export const useCharts = async(node, options) => {
    let chartInstance
    let chartNode = null
    let erd = elementResizeDetector()

    const resizeFn = utils.debounce(() => {
        chartInstance.resize()
    }, 100)

    if(node) {
        chartInstance = echarts.init(node)
        chartInstance.setOption(options)
        chartNode = chartInstance
    }
    erd.listenTo(node, resizeFn)
}

通過如下方式即可快速呼叫圖表hooks。

<script>
    import { useCharts } from '@/hooks.js'
    
    function useBarChart(node) {
        useCharts(node, {
            ...
        })
    }

    function useStackChart(node) {
        useCharts(node, {
            ...
        })
    }
</script>

<div class="card-charts" use:useBarChart></div>
<div class="card-charts" use:useStackChart></div>

專案中還有一大亮點就是Table表格元件,支援固定表頭/列,單選及多選,邊框/隔行換色,支援動態slot插槽等功能。

<script>
/**
 * 表格元件
 * @author YXY
 * Q:282310962 WX:xy190310
 */

let tableNode
let curpage = 1
let limit = 10
let selectRows = []
let tableData = Mock.mock({
    total: 100,
    page: 1,
    pagesize: 5,
    'list|10': [
        {
            id: '@integer(1, 1000)',
            author: '@cname()',
            title: '@ctitle(10, 15)',
            image: `https://cdn2.thecatapi.com/images/@integer(200, 300).jpg`,
            summary: '@ctitle(20, 70)',
            'role|1': ['admin', 'test', 'dev'],
            topmost: '@boolean()',
            progress: '@integer(30, 90)',
            date: '@datetime()'
        }
    ]
})
let tableColumns = [
    {type: 'selection', align: 'center', width: 60, fixed: true}, // 多選
    // {type: 'index', align: 'center', width: 60}, // 索引序號
    {prop: 'id', label: 'id', align: 'center', width: 60}, // 索引序號
    {prop: 'author', label: '作者', align: 'center', width: 100},
    {slot: 'title', label: '標題', align: 'left', width: 250},
    {slot: 'image', label: '影象', align: 'center', width: 120},
    {slot: 'summary', label: '詳細內容', align: 'left', width: 450},
    {slot: 'role', label: '角色', align: 'center', width: 100},
    {slot: 'topmost', label: '置頂', align: 'center', width: 100},
    {slot: 'progress', label: '熱度', align: 'center', width: 150},
    {prop: 'date', label: '釋出時間', align: 'left', width: 200}, // 時間
    {slot: 'btns', label: '操作', align: 'center', width: 200, fixed: 'right'}, // 操作
]
// 獲取選中行資料
function getSelectionRow() {
    svLayer({
        title: '資訊',
        content: JSON.stringify(selectRows),
        xclose: true,
        area: '640px'
    })
}
// 選中第3行
function updateRow() {
    tableNode.setCurrent(2)
}
// 取消選擇
function cancelSelection() {
    tableNode.setCurrent()
}
// 當前頁
function handleChangePage(e) {
    console.log('當前頁:', e.detail)
    curpage = Number(e.detail)
}
// 頁碼
function handleChangeSize(e) {
    console.log('每頁:', e.detail)
    limit = Number(e.detail)
}
// 點選行
function handleSelectionChange(e) {
    console.log('selection change選中行資料>>:', e.detail)
    selectRows = e.detail
}
</script>

<Table
    dataSource={tableData.list}
    columns={tableColumns}
    stripe={isStripe}
    border={isBorder}
    size={tableSizeCmd}
    highlightCurrentRow
    let:row
    let:col
    let:index
    on:selectionChange={handleSelectionChange}
    on:headerClick={handleHeaderClick}
    bind:this={tableNode}
    style="height: 500px; margin-bottom: 15px;"
>
    {#if col.slot == 'title'}
        <Link href="https://svelte.dev/" target="_blank" isUnderline color="#06f">{row.title}</Link>
    {:else if col.slot == 'image'}
        <img src={row.image} style="height: 50px; width: 50px;" alt="" />
    {:else if col.slot == 'summary'}
        <Tooltip content={row.summary} placement="top-start">
            <div class="clamp2">{row.summary}</div>
        </Tooltip>
    {:else if col.slot == 'role'}
        {#if row.role == 'admin'}
            <Tag type="success" effect="dark" size="mini">{row.role}</Tag>
        {:else if row.role == 'test'}
            <Tag type="primary" size="mini">{row.role}</Tag>
        {:else}
            <Tag type="warning" effect="dark" size="mini">{row.role}</Tag>
        {/if}
    {:else if col.slot == 'topmost'}
        <Switch checked={row.topmost} activeColor="#ff4079" size="small" />
    {:else if col.slot == 'progress'}
        <Progress percent={row.progress} color="#ffa222" showtext="false" strokeWidth={10} style="width: 100px;" />
    {:else if col.slot == 'btns'}
        <Link type="primary" icon="sv-icon-attention_light" gap="5" on:click={handleTableView}>檢視</Link>
        <Link type="success" icon="sv-icon-edit" gap="5" on:click={handleTableEdit(row)}>編輯</Link>
        <Link type="danger" icon="sv-icon-delete" gap="5" on:click={handleTableDel(row)}>刪除</Link>
    {/if}
</Table>
<Pagination
    layout="total, sizes, prev, pager, next, jumper"
    currentPage={curpage}
    pageSize={limit}
    pageSizes={[10, 50, 100]}
    total="500"
    size="mini"
    position="center"
    on:changePage={handleChangePage}
    on:changeSize={handleChangeSize}
/>

Ok,基於svelte+svelteUI開發後臺管理系統就分享到這裡,希望對大家有所幫助~~

最後附上一個svelte.js網頁聊天範例專案

https://www.cnblogs.com/xiaoyan2017/p/16272097.html