wip: layout / theme / settings / components / i18n / config

main
NoahLan 2 years ago
parent c117a90750
commit e16214e2c6

@ -14,6 +14,10 @@ VITE_UPLOAD_URL=/api/upload
# API接口前缀 # API接口前缀
VITE_API_URL_PREFIX= VITE_API_URL_PREFIX=
# 本地ICON前缀
VITE_ICON_LOCAL_PREFIX=nl
# UNOCSS ICON前缀
VITE_ICON_UNOCSS_PREFIX=i
# 是否开启请求代理 # 是否开启请求代理
VITE_HTTP_PROXY=false VITE_HTTP_PROXY=false
# 是否开启打包文件大小结果分析 # 是否开启打包文件大小结果分析

@ -18,7 +18,7 @@
"vmodel", "vmodel",
"vueuse" "vueuse"
], ],
"i18n-ally.sourceLanguage": "zh-CN", "i18n-ally.sourceLanguage": "zh-chs",
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": "locales", "i18n-ally.localesPaths": "locales",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": true,

@ -1,3 +1,32 @@
sys:
exception:
backLogin: 返回登录页
backHome: 返回首页
subTitle403: 抱歉,您无权访问此页面。
subTitle404: 抱歉,您访问的页面不存在。
subTitle500: 抱歉,服务器错误。
noDataTitle: 当前页无数据。
networkErrorTitle: 网络错误
networkErrorSubTitle: 抱歉,您的网络连接已断开,请检查网络!
common:
okText: 确认
closeText: 关闭
cancelText: 取消
loadingText: 加载中...
saveText: 保存
delText: 删除
resetText: 重置
searchText: 搜索
queryText: 查询
#
inputText: 请输入
chooseText: 请选择
#
redo: 刷新
back: 返回
#
light: 明亮
dark: 暗黑
button: button:
about: 关于 about: 关于
back: 返回 back: 返回
@ -5,6 +34,9 @@ button:
home: 首页 home: 首页
toggle_dark: 切换深色模式 toggle_dark: 切换深色模式
toggle_langs: 切换语言 toggle_langs: 切换语言
copy: 复制
reset: 重置
reload: 刷新
not-found: 未找到页面 not-found: 未找到页面
menu: menu:
home: 首页 home: 首页
@ -14,8 +46,20 @@ menu:
dialog: dialog:
errTitle: 错误提示 errTitle: 错误提示
layout: layout:
header:
tooltipExitFull: 退出全屏
tooltipEntryFull: 全屏模式
tooltipSettings: 设置
tooltipLanguage: 切换语言
footer: footer:
onlineDocument: 在线文档 onlineDocument: 在线文档
multipleTab:
reload: 重新加载
close: 关闭标签页
closeLeft: 关闭左侧标签页
closeRight: 关闭右侧标签页
closeOther: 关闭其它标签页
closeAll: 关闭所有标签页
setting: setting:
# content mode # content mode
contentModeFull: 流式 contentModeFull: 流式
@ -43,8 +87,6 @@ layout:
operatingContent: 复制成功,请到 src/settings/projectSetting.ts 中修改配置! operatingContent: 复制成功,请到 src/settings/projectSetting.ts 中修改配置!
resetSuccess: 重置成功! resetSuccess: 重置成功!
copyBtn: 拷贝
resetBtn: 重置配置
clearBtn: 清空缓存并返回登录页 clearBtn: 清空缓存并返回登录页
drawerTitle: 项目配置 drawerTitle: 项目配置
@ -59,9 +101,14 @@ layout:
splitMenu: 分割菜单 splitMenu: 分割菜单
closeMixSidebarOnChange: 切换页面关闭菜单 closeMixSidebarOnChange: 切换页面关闭菜单
sysTheme: 系统主题 themeEditor: 主题配置
primaryColor: 主题色
successColor: 成功色
warningColor: 警告色
errorColor: 错误色
infoColor: 信息色
headerTheme: 顶栏主题 headerTheme: 顶栏主题
sidebarTheme: 菜单主题 sidebarDark: 侧边栏深色
menuDrag: 侧边菜单拖拽 menuDrag: 侧边菜单拖拽
menuSearch: 菜单搜索 menuSearch: 菜单搜索

@ -4,13 +4,18 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"packageManager": "pnpm@8.5.1", "packageManager": "pnpm@8.5.1",
"description": "description",
"author": {
"name": "NorthLan"
},
"engines": { "engines": {
"node": ">=16.15.1", "node": ">=16.15.1",
"pnpm": ">=8.1.0" "pnpm": ">=8.1.0"
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev": "vite --port 3333", "predev": "esno build/generate/index.ts && pnpm run icon:dev",
"dev": "vite --port 8088",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"preview-https": "serve dist", "preview-https": "serve dist",
@ -20,13 +25,14 @@
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"up": "taze major -I", "up": "taze major -I",
"postinstall": "npx simple-git-hooks && turbo run stub", "postinstall": "npx simple-git-hooks && turbo run stub",
"sizecheck": "npx vite-bundle-visualizer" "sizecheck": "npx vite-bundle-visualizer",
"icon:dev": "esno build/icon dev",
"icon:prod": "esno build/icon prod"
}, },
"dependencies": { "dependencies": {
"@unhead/vue": "^1.7.4", "@unhead/vue": "^1.7.4",
"@unocss/reset": "^0.55.7", "@unocss/reset": "^0.55.7",
"@vueuse/core": "^10.5.0", "@vueuse/core": "^10.5.0",
"@vueuse/head": "^2.0.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.3.6", "vue": "^3.3.6",
@ -35,30 +41,37 @@
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/colors": "^7.0.0",
"@antfu/eslint-config": "1.0.0-beta.28", "@antfu/eslint-config": "1.0.0-beta.28",
"@iconify-json/ant-design": "^1.1.10", "@iconify/json": "^2.2.141",
"@iconify-json/carbon": "^1.1.21", "@iconify/tools": "^3.0.6",
"@iconify-json/emojione": "^1.1.7", "@iconify/types": "^2.0.0",
"@iconify-json/gridicons": "^1.1.11", "@iconify/utils": "^2.1.11",
"@iconify-json/ion": "^1.1.12", "@iconify/vue": "^4.1.1",
"@intlify/unplugin-vue-i18n": "^1.4.0", "@intlify/unplugin-vue-i18n": "^1.4.0",
"@types/markdown-it-link-attributes": "^3.0.3", "@types/markdown-it-link-attributes": "^3.0.3",
"@types/nprogress": "^0.2.2", "@types/nprogress": "^0.2.2",
"@types/qs": "^6.9.9", "@types/qs": "^6.9.9",
"@types/sortablejs": "^1.15.4",
"@unocss/eslint-config": "^0.57.0", "@unocss/eslint-config": "^0.57.0",
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
"@vue-macros/reactivity-transform": "^0.3.23", "@vue-macros/reactivity-transform": "^0.3.23",
"@vue-macros/short-vmodel": "^1.3.0", "@vue-macros/short-vmodel": "^1.3.0",
"@vue-macros/volar": "^0.14.3", "@vue-macros/volar": "^0.14.3",
"@vue/test-utils": "^2.4.1", "@vue/test-utils": "^2.4.1",
"chalk": "^5.3.0",
"critters": "^0.0.20", "critters": "^0.0.20",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.3.2", "cypress": "^13.3.2",
"cypress-vite": "^1.4.2", "cypress-vite": "^1.4.2",
"easy-fns-ts": "^1.2.3",
"eslint": "^8.52.0", "eslint": "^8.52.0",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^2.15.1",
"esno": "^4.0.0",
"fast-glob": "^3.3.2",
"https-localhost": "^4.7.1", "https-localhost": "^4.7.1",
"less": "^4.2.0", "less": "^4.2.0",
"less-loader": "^11.1.3",
"lint-staged": "^14.0.1", "lint-staged": "^14.0.1",
"markdown-it-link-attributes": "^4.0.1", "markdown-it-link-attributes": "^4.0.1",
"markdown-it-shiki": "^0.9.0", "markdown-it-shiki": "^0.9.0",
@ -71,6 +84,7 @@
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"taze": "^0.11.4", "taze": "^0.11.4",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"typescript-json-schema": "^0.62.0",
"unocss": "^0.55.7", "unocss": "^0.55.7",
"unplugin-auto-import": "^0.16.6", "unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.2", "unplugin-vue-components": "^0.25.2",
@ -78,10 +92,12 @@
"unplugin-vue-markdown": "^0.24.3", "unplugin-vue-markdown": "^0.24.3",
"vite": "^4.5.0", "vite": "^4.5.0",
"vite-bundle-visualizer": "^0.10.0", "vite-bundle-visualizer": "^0.10.0",
"vite-plugin-banner": "^0.7.1",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-inspect": "^0.7.40", "vite-plugin-inspect": "^0.7.40",
"vite-plugin-pages": "^0.31.0", "vite-plugin-pages": "^0.31.0",
"vite-plugin-pwa": "^0.16.5", "vite-plugin-pwa": "^0.16.5",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-component-preview": "^1.1.6", "vite-plugin-vue-component-preview": "^1.1.6",
"vite-plugin-vue-devtools": "1.0.0-rc.5", "vite-plugin-vue-devtools": "1.0.0-rc.5",
"vite-plugin-vue-layouts": "file:lib/vite-plugin-vue-layouts", "vite-plugin-vue-layouts": "file:lib/vite-plugin-vue-layouts",

File diff suppressed because it is too large Load Diff

@ -2,29 +2,30 @@
import type { GlobalThemeOverrides } from 'naive-ui' import type { GlobalThemeOverrides } from 'naive-ui'
import { darkTheme, dateZhCN, zhCN } from 'naive-ui' import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
const { isDark, themeColors } = useAppTheme()
// https://github.com/vueuse/head // https://github.com/vueuse/head
// you can use this to manipulate the document head in any components, // https://unhead.unjs.io/
// they will be rendered correctly in the html results with vite-ssg useHead({
// useHead({ title: `${import.meta.env.VITE_APP_TITLE}`,
// title: `${import.meta.env.VITE_TITLE}`, meta: [
// meta: [ { name: 'description', content: `${import.meta.env.VITE_APP_DESC}` },
// { name: 'description', content: `${import.meta.env.VITE_DESCRIPTION}` }, {
// { name: 'theme-color',
// name: 'theme-color', content: () => isDark.value ? '#00aba9' : '#ffffff',
// content: () => isDark.value ? '#00aba9' : '#ffffff', },
// }, ],
// ], link: [
// link: [ {
// { rel: 'icon',
// rel: 'icon', type: 'image/svg+xml',
// type: 'image/svg+xml', href: () => isDark.value ? '/favicon-dark.svg' : '/favicon.svg',
// href: () => preferredDark.value ? '/favicon-dark.svg' : '/favicon.svg', },
// }, ],
// ], })
// })
const theme = computed(() => { const theme = computed(() => {
return isDark.value ? darkTheme : null return unref(isDark) ? darkTheme : null
}) })
// const i18n = useI18n() // const i18n = useI18n()
@ -32,12 +33,9 @@ const theme = computed(() => {
const themeOverridesRef = computed((): GlobalThemeOverrides => { const themeOverridesRef = computed((): GlobalThemeOverrides => {
return { return {
// common: { common: {
// primaryColor: '#0052D9', ...unref(themeColors),
// primaryColorHover: '#366ef4', },
// primaryColorPressed: '#003cab',
// primaryColorSuppl: '#003ccc',
// },
} }
}) })
</script> </script>

@ -0,0 +1,29 @@
<script setup lang="ts">
const loadingClasses = [
'left-0 top-0',
'left-0 bottom-0 animate-delay-500',
'right-0 top-0 animate-delay-1000',
'right-0 bottom-0 animate-delay-1500',
]
const title = computed(() => import.meta.env.VITE_APP_TITLE)
</script>
<template>
<div class="fixed-center flex-col">
<LIcon icon="i-logo" size="240" />
<div class="my-36px h-25px w-56px">
<div class="relative h-full animate-spin">
<div
v-for="(item, index) in loadingClasses"
:key="index"
class="absolute h-16px w-16px animate-pulse rounded-8px bg-primary"
:class="item"
/>
</div>
</div>
<h2 class="text-28px font-500 text-#646464">
{{ title }}
</h2>
</div>
</template>

@ -27,10 +27,12 @@ declare global {
const createEventHook: typeof import('@vueuse/core')['createEventHook'] const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createMediaPrefersColorSchemeListen: typeof import('./composables/web/theme')['createMediaPrefersColorSchemeListen']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createThemeColorListen: typeof import('./composables/web/theme')['createThemeColorListen']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef'] const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
@ -43,12 +45,14 @@ declare global {
const effectScope: typeof import('vue')['effectScope'] const effectScope: typeof import('vue')['effectScope']
const envD: typeof import('./types/env.d')['default'] const envD: typeof import('./types/env.d')['default']
const extendRef: typeof import('@vueuse/core')['extendRef'] const extendRef: typeof import('@vueuse/core')['extendRef']
const getActiveHead: typeof import('@unhead/vue')['getActiveHead']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const getRoutes: typeof import('./composables/router/routes')['getRoutes'] const getRoutes: typeof import('./composables/router/routes')['getRoutes']
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
const injectHead: typeof import('@unhead/vue')['injectHead']
const injectLocal: typeof import('@vueuse/core')['injectLocal'] const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDark: typeof import('./composables/dark')['isDark'] const isDark: typeof import('./composables/dark')['isDark']
const isDefined: typeof import('@vueuse/core')['isDefined'] const isDefined: typeof import('@vueuse/core')['isDefined']
@ -104,6 +108,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent'] const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const routeLink: typeof import('vue-router/auto')['routeLink']
const setGlobalOptions: typeof import('./composables/request')['setGlobalOptions'] const setGlobalOptions: typeof import('./composables/request')['setGlobalOptions']
const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowReadonly: typeof import('vue')['shallowReadonly']
@ -132,6 +137,7 @@ declare global {
const useAnimate: typeof import('@vueuse/core')['useAnimate'] const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAppConfig: typeof import('./composables/config/app-config')['useAppConfig'] const useAppConfig: typeof import('./composables/config/app-config')['useAppConfig']
const useAppConfigStore: typeof import('./stores/app-config')['useAppConfigStore'] const useAppConfigStore: typeof import('./stores/app-config')['useAppConfigStore']
const useAppTheme: typeof import('./composables/web/theme')['useAppTheme']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
@ -199,9 +205,12 @@ declare global {
const useGamepad: typeof import('@vueuse/core')['useGamepad'] const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGo: typeof import('./composables/page')['useGo'] const useGo: typeof import('./composables/page')['useGo']
const useHead: typeof import('@vueuse/head')['useHead'] const useHead: typeof import('@unhead/vue')['useHead']
const useHeadSafe: typeof import('@unhead/vue')['useHeadSafe']
const useHeaderSetting: typeof import('./composables/setting/header-setting')['useHeaderSetting'] const useHeaderSetting: typeof import('./composables/setting/header-setting')['useHeaderSetting']
const useI18n: typeof import('./composables/i18n')['useI18n'] const useI18n: typeof import('./composables/locale/i18n')['useI18n']
const useIcon: typeof import('./composables/icon')['useIcon']
const useIconRender: typeof import('./composables/icon')['useIconRender']
const useIdle: typeof import('@vueuse/core')['useIdle'] const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage'] const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
@ -215,6 +224,8 @@ declare global {
const useLoadMore: typeof import('./composables/request')['useLoadMore'] const useLoadMore: typeof import('./composables/request')['useLoadMore']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar'] const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLocale: typeof import('./composables/locale/locale')['useLocale']
const useLocaleStore: typeof import('./stores/i18n')['useLocaleStore']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
@ -255,6 +266,7 @@ declare global {
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious'] const usePrevious: typeof import('@vueuse/core')['usePrevious']
const usePromise: typeof import('./composables/promise')['usePromise']
const useRafFn: typeof import('@vueuse/core')['useRafFn'] const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRedo: typeof import('./composables/page')['useRedo'] const useRedo: typeof import('./composables/page')['useRedo']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
@ -263,13 +275,17 @@ declare global {
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRootSetting: typeof import('./composables/setting/root-setting')['useRootSetting'] const useRootSetting: typeof import('./composables/setting/root-setting')['useRootSetting']
const useRoute: typeof import('vue-router')['useRoute'] const useRoute: typeof import('vue-router')['useRoute']
const useRouteStore: typeof import('./stores/routes')['useRouteStore']
const useRouter: typeof import('vue-router')['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll'] const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta'] const useSeoMeta: typeof import('@unhead/vue')['useSeoMeta']
const useServerHead: typeof import('@unhead/vue')['useServerHead']
const useServerHeadSafe: typeof import('@unhead/vue')['useServerHeadSafe']
const useServerSeoMeta: typeof import('@unhead/vue')['useServerSeoMeta']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare'] const useShare: typeof import('@vueuse/core')['useShare']
const useSiteConfigStore: typeof import('./stores/site')['useSiteConfigStore'] const useSiteConfigStore: typeof import('./stores/site')['useSiteConfigStore']
@ -289,6 +305,7 @@ declare global {
const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThemeStore: typeof import('./stores/theme')['useThemeStore']
const useThrottle: typeof import('@vueuse/core')['useThrottle'] const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
@ -366,10 +383,12 @@ declare module 'vue' {
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']> readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']> readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']> readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createMediaPrefersColorSchemeListen: UnwrapRef<typeof import('./composables/web/theme')['createMediaPrefersColorSchemeListen']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']> readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']> readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']> readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']> readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createThemeColorListen: UnwrapRef<typeof import('./composables/web/theme')['createThemeColorListen']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']> readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']> readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']> readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
@ -379,14 +398,14 @@ declare module 'vue' {
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']> readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActiveHead: UnwrapRef<typeof import('@unhead/vue')['getActiveHead']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']> readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getRoutes: UnwrapRef<typeof import('./composables/router/routes')['getRoutes']>
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']> readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectHead: UnwrapRef<typeof import('@unhead/vue')['injectHead']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']> readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']> readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@ -420,7 +439,6 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']> readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']> readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
@ -454,7 +472,6 @@ declare module 'vue' {
readonly toRef: UnwrapRef<typeof import('vue')['toRef']> readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']> readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']> readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']> readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@ -468,6 +485,7 @@ declare module 'vue' {
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']> readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']> readonly useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']>
readonly useAppConfigStore: UnwrapRef<typeof import('./stores/app-config')['useAppConfigStore']> readonly useAppConfigStore: UnwrapRef<typeof import('./stores/app-config')['useAppConfigStore']>
readonly useAppTheme: UnwrapRef<typeof import('./composables/web/theme')['useAppTheme']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']> readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']> readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']> readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@ -535,9 +553,11 @@ declare module 'vue' {
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGo: UnwrapRef<typeof import('./composables/page')['useGo']> readonly useGo: UnwrapRef<typeof import('./composables/page')['useGo']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']> readonly useHead: UnwrapRef<typeof import('@unhead/vue')['useHead']>
readonly useHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useHeadSafe']>
readonly useHeaderSetting: UnwrapRef<typeof import('./composables/setting/header-setting')['useHeaderSetting']> readonly useHeaderSetting: UnwrapRef<typeof import('./composables/setting/header-setting')['useHeaderSetting']>
readonly useI18n: UnwrapRef<typeof import('./composables/i18n')['useI18n']> readonly useI18n: UnwrapRef<typeof import('./composables/locale/i18n')['useI18n']>
readonly useIcon: UnwrapRef<typeof import('./composables/icon')['useIcon']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@ -551,6 +571,8 @@ declare module 'vue' {
readonly useLoadMore: UnwrapRef<typeof import('./composables/request')['useLoadMore']> readonly useLoadMore: UnwrapRef<typeof import('./composables/request')['useLoadMore']>
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']> readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']> readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLocale: UnwrapRef<typeof import('./composables/locale/locale')['useLocale']>
readonly useLocaleStore: UnwrapRef<typeof import('./stores/i18n')['useLocaleStore']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']> readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']> readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']> readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
@ -559,7 +581,6 @@ declare module 'vue' {
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']> readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']> readonly useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']>
readonly useMenuSetting: UnwrapRef<typeof import('./composables/setting/menu-setting')['useMenuSetting']> readonly useMenuSetting: UnwrapRef<typeof import('./composables/setting/menu-setting')['useMenuSetting']>
readonly useMenuStore: UnwrapRef<typeof import('./stores/menu')['useMenuStore']>
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']> readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']> readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']> readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
@ -590,6 +611,7 @@ declare module 'vue' {
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']> readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']> readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']> readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly usePromise: UnwrapRef<typeof import('./composables/promise')['usePromise']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']> readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']> readonly useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']> readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
@ -598,13 +620,17 @@ declare module 'vue' {
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']> readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']> readonly useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']> readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouteStore: UnwrapRef<typeof import('./stores/routes')['useRouteStore']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']> readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']> readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']> readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']> readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']> readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']> readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']> readonly useSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useSeoMeta']>
readonly useServerHead: UnwrapRef<typeof import('@unhead/vue')['useServerHead']>
readonly useServerHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useServerHeadSafe']>
readonly useServerSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useServerSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']> readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']> readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSiteConfigStore: UnwrapRef<typeof import('./stores/site')['useSiteConfigStore']> readonly useSiteConfigStore: UnwrapRef<typeof import('./stores/site')['useSiteConfigStore']>
@ -624,6 +650,7 @@ declare module 'vue' {
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']> readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']> readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']> readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThemeStore: UnwrapRef<typeof import('./stores/theme')['useThemeStore']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']> readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']> readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']> readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
@ -695,10 +722,12 @@ declare module '@vue/runtime-core' {
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']> readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']> readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']> readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createMediaPrefersColorSchemeListen: UnwrapRef<typeof import('./composables/web/theme')['createMediaPrefersColorSchemeListen']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']> readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']> readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']> readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']> readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createThemeColorListen: UnwrapRef<typeof import('./composables/web/theme')['createThemeColorListen']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']> readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']> readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']> readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
@ -708,14 +737,14 @@ declare module '@vue/runtime-core' {
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']> readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActiveHead: UnwrapRef<typeof import('@unhead/vue')['getActiveHead']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']> readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getRoutes: UnwrapRef<typeof import('./composables/router/routes')['getRoutes']>
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']> readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectHead: UnwrapRef<typeof import('@unhead/vue')['injectHead']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']> readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']> readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@ -749,7 +778,6 @@ declare module '@vue/runtime-core' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']> readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']> readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
@ -783,7 +811,6 @@ declare module '@vue/runtime-core' {
readonly toRef: UnwrapRef<typeof import('vue')['toRef']> readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']> readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']> readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']> readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']> readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@ -797,6 +824,7 @@ declare module '@vue/runtime-core' {
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']> readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']> readonly useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']>
readonly useAppConfigStore: UnwrapRef<typeof import('./stores/app-config')['useAppConfigStore']> readonly useAppConfigStore: UnwrapRef<typeof import('./stores/app-config')['useAppConfigStore']>
readonly useAppTheme: UnwrapRef<typeof import('./composables/web/theme')['useAppTheme']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']> readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']> readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']> readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@ -864,9 +892,11 @@ declare module '@vue/runtime-core' {
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']> readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']> readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGo: UnwrapRef<typeof import('./composables/page')['useGo']> readonly useGo: UnwrapRef<typeof import('./composables/page')['useGo']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']> readonly useHead: UnwrapRef<typeof import('@unhead/vue')['useHead']>
readonly useHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useHeadSafe']>
readonly useHeaderSetting: UnwrapRef<typeof import('./composables/setting/header-setting')['useHeaderSetting']> readonly useHeaderSetting: UnwrapRef<typeof import('./composables/setting/header-setting')['useHeaderSetting']>
readonly useI18n: UnwrapRef<typeof import('./composables/i18n')['useI18n']> readonly useI18n: UnwrapRef<typeof import('./composables/locale/i18n')['useI18n']>
readonly useIcon: UnwrapRef<typeof import('./composables/icon')['useIcon']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']> readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']> readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']> readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@ -880,6 +910,8 @@ declare module '@vue/runtime-core' {
readonly useLoadMore: UnwrapRef<typeof import('./composables/request')['useLoadMore']> readonly useLoadMore: UnwrapRef<typeof import('./composables/request')['useLoadMore']>
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']> readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']> readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLocale: UnwrapRef<typeof import('./composables/locale/locale')['useLocale']>
readonly useLocaleStore: UnwrapRef<typeof import('./stores/i18n')['useLocaleStore']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']> readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']> readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']> readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
@ -888,7 +920,6 @@ declare module '@vue/runtime-core' {
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']> readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']> readonly useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']>
readonly useMenuSetting: UnwrapRef<typeof import('./composables/setting/menu-setting')['useMenuSetting']> readonly useMenuSetting: UnwrapRef<typeof import('./composables/setting/menu-setting')['useMenuSetting']>
readonly useMenuStore: UnwrapRef<typeof import('./stores/menu')['useMenuStore']>
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']> readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']> readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']> readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
@ -919,6 +950,7 @@ declare module '@vue/runtime-core' {
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']> readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']> readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']> readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly usePromise: UnwrapRef<typeof import('./composables/promise')['usePromise']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']> readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']> readonly useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']> readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
@ -927,13 +959,17 @@ declare module '@vue/runtime-core' {
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']> readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']> readonly useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']> readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouteStore: UnwrapRef<typeof import('./stores/routes')['useRouteStore']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']> readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']> readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']> readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']> readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']> readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']> readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']> readonly useSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useSeoMeta']>
readonly useServerHead: UnwrapRef<typeof import('@unhead/vue')['useServerHead']>
readonly useServerHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useServerHeadSafe']>
readonly useServerSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useServerSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']> readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']> readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSiteConfigStore: UnwrapRef<typeof import('./stores/site')['useSiteConfigStore']> readonly useSiteConfigStore: UnwrapRef<typeof import('./stores/site')['useSiteConfigStore']>
@ -953,6 +989,7 @@ declare module '@vue/runtime-core' {
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']> readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']> readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']> readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThemeStore: UnwrapRef<typeof import('./stores/theme')['useThemeStore']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']> readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']> readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']> readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>

@ -7,26 +7,32 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AuthBg: typeof import('./components/auth/bg.vue')['default'] LAuth: typeof import('./components/LAuth/index.ts')['default']
AuthLayout: typeof import('./components/auth/layout.vue')['default'] LAuthBg: typeof import('./components/LAuth/bg.vue')['default']
FormBg: typeof import('./components/form/bg.vue')['default'] LAuthLayout: typeof import('./components/LAuth/layout.vue')['default']
LIcon: typeof import('./components/LIcon/index.ts')['default']
LIconSrcIcon: typeof import('./components/LIcon/src/icon.vue')['default']
LLocalePicker: typeof import('./components/LLocalePicker/index.vue')['default']
NAffix: typeof import('naive-ui')['NAffix'] NAffix: typeof import('naive-ui')['NAffix']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb'] NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem'] NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider'] NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer'] NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent'] NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown'] NDropdown: typeof import('naive-ui')['NDropdown']
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm'] NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle'] NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
NH5: typeof import('naive-ui')['NH5']
NH6: typeof import('naive-ui')['NH6'] NH6: typeof import('naive-ui')['NH6']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput'] NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup'] NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber'] NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout'] NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent'] NLayoutContent: typeof import('naive-ui')['NLayoutContent']
@ -38,10 +44,13 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopover: typeof import('naive-ui')['NPopover'] NPopover: typeof import('naive-ui')['NPopover']
NResult: typeof import('naive-ui')['NResult']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect'] NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane'] NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs'] NTabs: typeof import('naive-ui')['NTabs']
NText: typeof import('naive-ui')['NText'] NText: typeof import('naive-ui')['NText']

@ -12,7 +12,7 @@ const desc = computed(() => import.meta.env.VITE_APP_DESC)
<template> <template>
<div flex="~ col" h-screen w-screen bg="zinc-50 dark:zinc-900"> <div flex="~ col" h-screen w-screen bg="zinc-50 dark:zinc-900">
<AuthBg /> <LAuthBg />
<!-- 头部 --> <!-- 头部 -->
<div <div
bg="white dark:zinc-800" flex="~ col" bg="white dark:zinc-800" flex="~ col"
@ -27,7 +27,7 @@ const desc = computed(() => import.meta.env.VITE_APP_DESC)
</div> </div>
</RouterLink> </RouterLink>
<div flex items-center gap-3> <div flex items-center gap-3>
<i class="i-nl-logo" h-18 w-18 select-none /> <LIcon icon="i-logo" size="4.5rem" />
<div> <div>
<div whitespace-nowrap font-extrabold tracking-widest text="4xl gray-700 dark:gray-100"> <div whitespace-nowrap font-extrabold tracking-widest text="4xl gray-700 dark:gray-100">
{{ title }} {{ title }}

@ -0,0 +1,64 @@
<script setup lang="ts">
import { localeList } from '~/modules/i18n/config'
defineOptions({
name: 'LocalPicker',
})
const props = defineProps({
//
showText: { type: Boolean, default: true },
//
reload: { type: Boolean, default: false },
})
let selectedKeys = $ref<string[]>([])
const { getLocale, changeLocale } = useLocale()
const getLocaleText = computed(() => {
const key = selectedKeys[0]
if (!key) {
return ''
}
return localeList.find(item => item.event === key)?.text
})
const { renderIcon } = useIcon()
const getLocaleList = computed(() => localeList.map(item => ({
label: item.text,
key: item.event,
icon: () => renderIcon({ icon: item.icon }),
})))
watchEffect(() => {
selectedKeys = [unref(getLocale)]
})
async function toggleLocale(lang: string) {
await changeLocale(lang)
selectedKeys = [lang]
props.reload && location.reload()
}
function handleMenuEvent(menuLocale: string) {
if (unref(getLocale) === menuLocale) {
return
}
toggleLocale(menuLocale)
}
</script>
<template>
<div class="px-3!">
<NDropdown
trigger="click"
:options="getLocaleList"
@select="handleMenuEvent"
>
<div class="flex cursor-pointer items-center">
<LIcon icon="carbon:translate" class="hover:cursor-pointer" />
<span v-if="showText" class="ml-1!">{{ getLocaleText }}</span>
</div>
</NDropdown>
</div>
</template>

@ -1,3 +0,0 @@
<template>
<div>222</div>
</template>

@ -1,6 +1,6 @@
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { HandlerSettingEnum, ThemeEnum } from '~/constants' import { HandlerSettingEnum, NavBarModeEnum } from '~/constants'
import { _omit } from '~/utils' import { _merge, _omit } from '~/utils'
export function useAppConfig() { export function useAppConfig() {
const configStore = useAppConfigStore() const configStore = useAppConfigStore()
@ -14,7 +14,19 @@ export function useAppConfig() {
} = appConfigOptions } = appConfigOptions
function setAppConfig(configs: DeepPartial<DefineAppConfigOptions>) { function setAppConfig(configs: DeepPartial<DefineAppConfigOptions>) {
configStore.setAppConfig(configs) configStore.$patch((state) => {
_merge(state, preDealConfig(configs))
})
}
function preDealConfig(configs: DeepPartial<DefineAppConfigOptions>) {
if (
configs.navBarMode
&& configs.navBarMode === NavBarModeEnum.MIX_SIDEBAR
) {
configs.logo = { show: true, visible: true }
}
return configs
} }
function toggleOpenSettingDrawer() { function toggleOpenSettingDrawer() {
@ -82,12 +94,11 @@ function handlerResults(
value: any, value: any,
configOptions: DefineAppConfigOptions, configOptions: DefineAppConfigOptions,
): DeepPartial<DefineAppConfigOptions> { ): DeepPartial<DefineAppConfigOptions> {
const { themeColor, theme, sidebar, header } = configOptions const { themeColor, sidebar, header } = configOptions
switch (event) { switch (event) {
case HandlerSettingEnum.CHANGE_LAYOUT: case HandlerSettingEnum.CHANGE_LAYOUT:
// eslint-disable-next-line no-case-declarations {
const { mode, type, split } = value const { mode, type, split } = value
// eslint-disable-next-line no-case-declarations
const splitOpt = split === undefined ? { split } : {} const splitOpt = split === undefined ? { split } : {}
return { return {
navBarMode: type, navBarMode: type,
@ -97,19 +108,17 @@ function handlerResults(
}, },
sidebar: { collapsed: false }, sidebar: { collapsed: false },
} }
}
// TODO remove it
case HandlerSettingEnum.CHANGE_THEME_COLOR: case HandlerSettingEnum.CHANGE_THEME_COLOR:
if (unref(themeColor) === value) if (unref(themeColor) === value)
return {} return {}
// changeTheme(value); // changeTheme(value);
return { themeColor: value } return { themeColor: value }
// TODO remove it
case HandlerSettingEnum.CHANGE_THEME: case HandlerSettingEnum.CHANGE_THEME:
if (unref(theme) === value) return { theme: value }
return {}
return { theme: value ? ThemeEnum.DARK : ThemeEnum.LIGHT }
case HandlerSettingEnum.MENU_HAS_DRAG: case HandlerSettingEnum.MENU_HAS_DRAG:
return { menu: { canDrag: value } } return { menu: { canDrag: value } }
@ -137,8 +146,9 @@ function handlerResults(
case HandlerSettingEnum.MENU_THEME: case HandlerSettingEnum.MENU_THEME:
// updateSidebarBgColor(value); // updateSidebarBgColor(value);
if (unref(sidebar).bgColor === value) if (unref(sidebar).bgColor === value) {
return {} return {}
}
return { sidebar: { bgColor: value } } return { sidebar: { bgColor: value } }
case HandlerSettingEnum.MENU_SPLIT: case HandlerSettingEnum.MENU_SPLIT:
@ -219,8 +229,9 @@ function handlerResults(
// ============header================== // ============header==================
case HandlerSettingEnum.HEADER_THEME: case HandlerSettingEnum.HEADER_THEME:
// updateHeaderBgColor(value); // updateHeaderBgColor(value);
if (unref(header).bgColor === value) if (unref(header).bgColor === value) {
return {} return {}
}
return { header: { bgColor: value } } return { header: { bgColor: value } }
case HandlerSettingEnum.HEADER_SEARCH: case HandlerSettingEnum.HEADER_SEARCH:

@ -1,9 +0,0 @@
// these APIs are auto-imported from @vueuse/core
export const isDark = useDark({
selector: 'html',
attribute: 'theme-mode',
valueDark: 'dark',
valueLight: '',
})
export const toggleDark = useToggle(isDark)
export const preferredDark = usePreferredDark()

@ -1,4 +1,13 @@
import { i18n } from '~/modules/i18n' import { i18n } from '~/modules/i18n/index'
export interface I18nGlobalTranslation {
(key: string): string
(key: string, locale: string): string
(key: string, locale: string, list: unknown[]): string
(key: string, locale: string, named: Record<string, unknown>): string
(key: string, list: unknown[]): string
(key: string, named: Record<string, unknown>): string
}
type I18nTranslationRestParameters = [string, any] type I18nTranslationRestParameters = [string, any]
@ -22,13 +31,14 @@ export function useI18n(namespace?: string) {
return normalFn return normalFn
} }
const { t, ...other } = i18n.global const { t, ...other } = i18n.global
const tFn = (key: string, ...arg: any[]) => { const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
if (!key) { if (!key) {
return '' return ''
} }
if (!key.includes('.') && !namespace) { if (!key.includes('.') && !namespace) {
return key return key
} }
// @ts-expect-error no-err
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters)) return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
} }
return { return {

@ -0,0 +1,26 @@
import { storeToRefs } from 'pinia'
import { i18n, loadLanguageAsync } from '~/modules/i18n'
export function useLocale() {
const localeStore = useLocaleStore()
const { setLocale } = localeStore
const { getLocale } = storeToRefs(localeStore)
const changeLocale = async (locale: string) => {
const currentLocale = unref(i18n.global.locale) as string
if (locale === currentLocale) {
return locale
}
await loadLanguageAsync(locale)
setLocale(locale)
return locale
}
return {
getLocale,
changeLocale,
}
}

@ -0,0 +1,166 @@
import type { ShallowRef, UnwrapRef } from 'vue'
import { containsProp, isEqual } from '~/utils'
export interface UsePromiseOption {
/**
*
* default: true
*/
immediate?: boolean
/**
*
* default: false
*/
redo?: boolean
/**
* redo = true, . ms
* default: 0(ms)
*/
debounce?: number
/**
* loading()
* default: false
*/
ignoreLoading?: boolean
/**
*
* default: false
*/
throwOnFailed?: boolean
}
export interface UsePromiseReturnType<T> {
data: ShallowRef<T | null>
loading: Ref<UnwrapRef<boolean>>
handleFn: () => Promise<T | null>
error: ShallowRef<unknown>
finished: Ref<UnwrapRef<boolean>>
}
function isUsePromiseOption(obj: object): obj is UsePromiseOption {
return containsProp(
obj,
'immediate',
'redo',
'debounce',
'ignoreLoading',
'throwOnFailed',
)
}
export function usePromise<T = any>(
fn: (...args: any[]) => Promise<T>,
): UsePromiseReturnType<T>
export function usePromise<T = any>(
fn: (...args: any[]) => Promise<T>,
opt: UsePromiseOption,
): UsePromiseReturnType<T>
export function usePromise<T = any>(
fn: (...args: any[]) => Promise<T>,
fnArgs: unknown,
opt?: UsePromiseOption,
): UsePromiseReturnType<T>
export function usePromise<T = any>(
fn: (...args: any[]) => Promise<T>,
...args: any[]
): UsePromiseReturnType<T> {
const data = shallowRef<T | null>(null)
const loading = ref(false)
const finished = ref(false)
const error = shallowRef<unknown>(null)
// fn params
const fnArgs = ref<unknown>()
// config
let config: UsePromiseOption = {
immediate: true,
redo: false,
debounce: 0,
ignoreLoading: false,
throwOnFailed: false,
}
function handleFn(): Promise<T | null> {
return new Promise((resolve, reject) => {
const { ignoreLoading, throwOnFailed } = config
if (!ignoreLoading && loading.value) {
return
}
loading.value = true
finished.value = false
fn(undefined, unref(fnArgs))
.then((res) => {
data.value = res
error.value = null
return resolve(res)
})
.catch((e) => {
data.value = null
error.value = e
if (throwOnFailed) {
return reject(e)
}
return resolve(null)
})
.finally(() => {
loading.value = false
finished.value = true
})
})
}
// 存在不在vue3 setup中执行.
const scoped = effectScope()
scoped.run(() => {
if (args.length > 0) {
if (isUsePromiseOption(args[0])) {
config = { ...config, ...args[0] }
}
else { fnArgs.value = args[0] }
}
if (args.length > 1) {
if (isUsePromiseOption(args[1])) {
config = { ...config, ...args[1] }
}
}
const { debounce, immediate, redo } = config
const debounceFn = useDebounceFn(() => {
return handleFn()
}, debounce)
if (immediate) {
debounceFn().then()
}
if (redo) {
watch(
fnArgs,
(newArgs, oldArgs) => {
if (!isEqual(newArgs, oldArgs)) {
debounceFn().then()
}
},
{ deep: true },
)
}
})
tryOnBeforeUnmount(() => {
scoped.stop()
})
return {
data,
loading,
finished,
error,
handleFn,
}
}

@ -1,66 +1,54 @@
import type { RouteRecordRaw } from 'vue-router' import { storeToRefs } from 'pinia'
import { filterTree, getAllParentPath, hideFilter } from '~/utils'
export function useMenu() { export function useMenu() {
const menuStore = useMenuStore() const routeStore = useRouteStore()
const userStore = useUserStore() const { initMenu, menuListRef } = storeToRefs(routeStore)
const { resolve } = useRouter()
/** 判断给定的route中是否具有给定perms列表的权限 */ const getMenuList = async () => {
function hasPermission(route: RouteRecordRaw, perms: any[] = []) { if (!unref(initMenu)) {
if (!route.meta?.perm) { await routeStore.buildMenu()
return true
} }
const menuList = unref(menuListRef)
// 递归寻找子节点perm return filterTree(menuList, hideFilter)
if (route.meta?.perm === true && route.children?.length) {
return filterAsyncRoutes(route.children, perms).length
}
// 否则直接通过perm进行判断
return perms.includes(
Array.isArray(route.meta?.perm)
? route.meta.perm[0]?.perm
: route.meta?.perm,
)
} }
// 过滤掉所有perm不匹配的路由 const getCurrentParentPath = async (currentPath: string) => {
function filterAsyncRoutes(routes: RouteRecordRaw[], perms: any[]) { const menus = await getMenuList()
return routes.reduce((rs: RouteRecordRaw[], route) => { const allParentPath = await getAllParentPath(menus, currentPath)
if (hasPermission(route, perms)) { return allParentPath?.[0]
rs.push({
...route,
children: route.children ? filterAsyncRoutes(route.children, perms) : [],
})
}
return rs
}, [])
} }
async function generateRoutes() { const getShallowMenus = async () => {
const perms = userStore.userInfo?.perms ?? ['role', 'role/post'] const menus = await getMenuList()
menuStore.menuList = filterAsyncRoutes(getRoutes(), perms) const shallowMenuList = menus.map(item => ({
...item,
children: undefined,
}))
// if (isRoleMode()) {
// const routes = router.getRoutes()
// return shallowMenuList.filter(basicFilter(routes))
// }
return shallowMenuList
} }
function getMenuList(routes: RouteRecordRaw[]) { const getChildrenMenus = async (parentPath: string) => {
return routes.reduce((rs: RouteRecordRaw[], route) => { const menus = await getMenuList()
if (!route.meta?.hidden && !route.meta?.hideInMenu) { const parent = menus.find(item => item.path === parentPath)
rs.push({ if (!parent || !parent.children || !!parent?.meta?.hideChildrenInMenu) {
...route, return [] as Menu[]
path: resolve(<string>route.redirect || route).path, }
children: route.children ? getMenuList(route.children) : [], // if (isRoleMode()) {
}) // const routes = router.getRoutes()
} // return filterTree(parent.children, basicFilter(routes))
return rs // }
}, []).sort((a, b) => { return parent.children
return (a.meta?.order || 999) - (b.meta?.order || 999)
})
} }
const menuList = computed(() => getMenuList(menuStore.menuList))
return { return {
menuList, getMenuList,
generateRoutes, getShallowMenus,
getChildrenMenus,
getCurrentParentPath,
} }
} }

@ -1,21 +0,0 @@
import type { RouteRecordRaw } from 'vue-router'
import routes from '~pages'
/**
* hidden
*/
function filterHiddenRoutes(routes: RouteRecordRaw[]) {
return routes.filter((i) => {
if (i.children) {
i.children = filterHiddenRoutes(i.children)
}
return !i.meta?.hidden
})
}
/**
*
*/
export function getRoutes() {
return filterHiddenRoutes(routes)
}

@ -1,4 +1,6 @@
import { import {
MenuModeEnum,
NavBarModeEnum,
SIDE_BAR_MINI_WIDTH, SIDE_BAR_MINI_WIDTH,
SIDE_BAR_SHOW_TIT_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH,
TriggerEnum, TriggerEnum,
@ -8,59 +10,66 @@ const mixSideHasChildren = ref(false)
export function useMenuSetting() { export function useMenuSetting() {
const { getFullContent: fullContent } = useFullContent() const { getFullContent: fullContent } = useFullContent()
const configStore = useAppConfigStore() const appConfig = useAppConfig()
const { getShowLogo } = useRootSetting() const { getShowLogo } = useRootSetting()
const getCollapsed = computed(() => configStore.sidebar.collapsed) const getCollapsed = computed(() => appConfig.sidebar.value.collapsed)
const getMenuType = computed(() => configStore.navBarMode) const getMenuType = computed(() => unref(appConfig.navBarMode))
const getMenuMode = computed(() => configStore.menu.mode) const getMenuMode = computed(() => appConfig.menu.value.mode)
const getMenuFixed = computed(() => configStore.sidebar.fixed) const getMenuFixed = computed(() => appConfig.sidebar.value.fixed)
const getShowMenu = computed(() => configStore.menu.show) const getShowMenu = computed(() => appConfig.menu.value.show)
const getMenuHidden = computed(() => !configStore.sidebar.visible) const getMenuHidden = computed(() => !appConfig.sidebar.value.visible)
const getMenuWidth = computed(() => configStore.sidebar.width) const getMenuWidth = computed(() => appConfig.sidebar.value.width)
const getTrigger = computed(() => configStore.sidebar.trigger) const getTrigger = computed(() => appConfig.sidebar.value.trigger)
const getMenuTheme = computed(() => configStore.sidebar.theme) const getMenuTheme = computed(() => appConfig.sidebar.value.theme)
const getSplit = computed(() => configStore.menu.split) const getSplit = computed(() => appConfig.menu.value.split)
const getMenuBgColor = computed(() => configStore.sidebar.bgColor) const getMenuBgColor = computed(() => appConfig.sidebar.value.bgColor)
const getMixSideTrigger = computed(() => configStore.menu.mixSideTrigger) const getMixSideTrigger = computed(() => appConfig.menu.value.mixSideTrigger)
const getShowSidebar = computed(() => { const getShowSidebar = computed(() => {
return ( return (
unref(getSplit) unref(getSplit)
|| (unref(getShowMenu) || (unref(getShowMenu)
&& !unref(configStore.isHorizontal) && !unref(appConfig.isHorizontal)
&& !unref(fullContent)) && !unref(fullContent))
) )
}) })
const getCanDrag = computed(() => configStore.menu.canDrag) const getCanDrag = computed(() => appConfig.menu.value.canDrag)
const getAccordion = computed(() => configStore.menu.accordion) const getAccordion = computed(() => appConfig.menu.value.accordion)
const getMixSideFixed = computed(() => configStore.menu.mixSideFixed) const getMixSideFixed = computed(() => appConfig.menu.value.mixSideFixed)
const getTopMenuAlign = computed(() => configStore.menu.topMenuAlign) const getTopMenuAlign = computed(() => appConfig.menu.value.topMenuAlign)
const getCloseMixSidebarOnChange = computed(() => configStore.closeMixSidebarOnChange) const getCloseMixSidebarOnChange = computed(() => appConfig.closeMixSidebarOnChange.value)
const getIsSidebarType = computed(() => configStore.isSidebar) const getIsSidebarType = computed(() => unref(getMenuType) === NavBarModeEnum.SIDEBAR)
const getIsTopMenu = computed(() => configStore.isTopMenu) const getIsTopMenu = computed(() => unref(getMenuType) === NavBarModeEnum.TOP_MENU)
const getMenuShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType)) const getMenuShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType))
const getCollapsedShowTitle = computed(() => configStore.isCollapsedShowTitle) const getCollapsedShowTitle = computed(() => appConfig.menu.value.collapsedShowTitle)
const getShowTopMenu = computed(() => unref(configStore.isHorizontal) || unref(getSplit)) const getShowTopMenu = computed(() => unref(getMenuMode) === MenuModeEnum.HORIZONTAL || unref(getSplit))
const getShowHeaderTrigger = computed(() => { const getShowHeaderTrigger = computed(() => {
if ( if (
configStore.isTopMenu unref(getMenuType) === NavBarModeEnum.TOP_MENU
|| !unref(getShowMenu) || !unref(getShowMenu)
|| unref(getMenuHidden) || unref(getMenuHidden)
) ) {
return false return false
}
return unref(getTrigger) === TriggerEnum.HEADER return unref(getTrigger) === TriggerEnum.HEADER
}) })
const getShowCenterTrigger = computed(() => unref(getTrigger) === TriggerEnum.CENTER) const getShowCenterTrigger = computed(() => unref(getTrigger) === TriggerEnum.CENTER)
const getShowFooterTrigger = computed(() => unref(getTrigger) === TriggerEnum.FOOTER) const getShowFooterTrigger = computed(() => unref(getTrigger) === TriggerEnum.FOOTER)
const getIsHorizontal = computed(() => configStore.isHorizontal) const getIsHorizontal = computed(() => unref(getMenuMode) === MenuModeEnum.HORIZONTAL)
const getIsMixSidebar = computed(() => configStore.isMixSidebar) const getIsMixSidebar = computed(() => unref(getMenuType) === NavBarModeEnum.MIX_SIDEBAR)
const getIsMixMode = computed(() => configStore.isMixMode) const getIsMixMode = computed(() => {
return (
unref(getMenuMode) === MenuModeEnum.INLINE
&& unref(getMenuType) === NavBarModeEnum.MIX
)
})
const getMiniWidthNumber = computed(() => { const getMiniWidthNumber = computed(() => {
const { collapsedShowTitle } = configStore.menu const { collapsedShowTitle } = appConfig.menu.value
return collapsedShowTitle return collapsedShowTitle
? SIDE_BAR_SHOW_TIT_MINI_WIDTH ? SIDE_BAR_SHOW_TIT_MINI_WIDTH
: SIDE_BAR_MINI_WIDTH : SIDE_BAR_MINI_WIDTH
@ -93,14 +102,18 @@ export function useMenuSetting() {
return `calc(100% - ${unref(width)}px)` return `calc(100% - ${unref(width)}px)`
}) })
const getIsFixed = computed(() => {
return unref(getMixSideFixed) && unref(mixSideHasChildren)
})
function setMenuSetting(menuSetting: Partial<MenuSetting>): void { function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
configStore.setMenu(menuSetting) appConfig.setAppConfig({ menu: menuSetting })
} }
function setSidebarSetting( function setSidebarSetting(
sidebarSetting: Partial<SidebarConfigOptions>, sidebarSetting: Partial<SidebarConfigOptions>,
): void { ): void {
configStore.setSidebar(sidebarSetting) appConfig.setAppConfig({ sidebar: sidebarSetting })
} }
function setSiderWidth(width: number) { function setSiderWidth(width: number) {
@ -148,6 +161,7 @@ export function useMenuSetting() {
getCloseMixSidebarOnChange, getCloseMixSidebarOnChange,
getMixSideTrigger, getMixSideTrigger,
getMixSideFixed, getMixSideFixed,
getIsFixed,
mixSideHasChildren, mixSideHasChildren,
getMenuShowLogo, getMenuShowLogo,
} }

@ -0,0 +1,343 @@
import { storeToRefs } from 'pinia'
import { ThemeEnum } from '~/constants'
import type { MaybeElementRef } from '~/utils'
import { darken, generateColors, lighten, pickTextColorBasedOnBgColor, setCssVar, toggleClass } from '~/utils'
const HEADER_HEIGHT = '--header-height'
const HEADER_BG_COLOR_VAR = '--header-background-color'
const HEADER_TEXT_COLOR_VAR = '--header-text-color'
const HEADER_ACTION_HOVER_BG_COLOR_VAR = '--header-action-hover-bg-color'
const ASIDE_WIDTH = '--aside-width'
const ASIDE_DARK_BG_COLOR = '--aside-background-color'
const ASIDE_TEXT_COLOR_VAR = '--aside-text-color'
const TRIGGER_BG_COLOR_VAR = '--trigger-background-color'
const TAB_BAR_HEIGHT = '--tab-bar-height'
const FOOTER_HEIGHT = '--footer-height'
const LIGHT_TEXT_COLOR = 'rgba(0,0,0,.85)'
const DARK_TEXT_COLOR = '#fff'
export function createMediaPrefersColorSchemeListen() {
const { setAppConfig } = useAppConfig()
// 监听系统主题变更
useEventListener(
window.matchMedia('(prefers-color-scheme: dark)'),
'change',
(e: MediaQueryListEvent) => setAppConfig({ theme: e.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT }),
)
}
function toggleGrayMode(val: boolean) {
toggleClass(val, 'gray-mode', document.documentElement)
}
function toggleColorWeak(val: boolean) {
toggleClass(val, 'color-weak', document.documentElement)
}
export function createGridLayoutListen(el: MaybeElementRef | null) {
const {
isTopMenu,
sidebar,
header,
footer,
tabTar,
getCollapsedShowTitle,
menu,
isMixSidebar,
} = useAppConfig()
const asideWidth = useCssVar(ASIDE_WIDTH, el, {
initialValue: `${unref(sidebar).width}px`,
})
const headerHeight = useCssVar(HEADER_HEIGHT, el, {
initialValue: `${unref(header).height}px`,
})
const tabBarHeight = useCssVar(TAB_BAR_HEIGHT, el, {
initialValue: `${unref(tabTar).height}px`,
})
const footerHeight = useCssVar(FOOTER_HEIGHT, el, {
initialValue: `${unref(footer).height}px`,
})
watchEffect(() => {
const getAsideWidth = () => {
if (unref(isTopMenu) || !unref(sidebar).visible)
return 0
if (unref(getCollapsedShowTitle)) {
return unref(menu).mixSideFixed && unref(isMixSidebar)
? unref(sidebar).mixSidebarWidth + unref(menu).subMenuWidth
: unref(sidebar).mixSidebarWidth
}
if (unref(sidebar).collapsed) {
return unref(menu).mixSideFixed && unref(isMixSidebar)
? unref(sidebar).collapsedWidth + unref(menu).subMenuWidth
: unref(sidebar).collapsedWidth
}
return unref(sidebar).width
}
const getHeaderHeight = () => {
if (!unref(header).visible)
return 0
return unref(header).height
}
const getTabBarHeight = () => {
if (!unref(tabTar).visible)
return 0
return unref(tabTar).height
}
const getFooterHeight = () => {
if (!unref(footer).visible)
return 0
return unref(footer).height
}
asideWidth.value = `${getAsideWidth()}px`
headerHeight.value = `${getHeaderHeight()}px`
tabBarHeight.value = `${getTabBarHeight()}px`
footerHeight.value = `${getFooterHeight()}px`
})
}
export function createThemeColorListen() {
const {
sidebar,
header,
grayMode,
colorWeak,
setAppConfig,
} = useAppConfig()
const headerBgColor = useCssVar(HEADER_BG_COLOR_VAR, null, {
initialValue: `${unref(header).bgColor}px`,
})
const headerTextColor = useCssVar(
HEADER_TEXT_COLOR_VAR,
null,
{
initialValue: LIGHT_TEXT_COLOR,
},
)
const headerActionHoverBgColor = useCssVar(
HEADER_ACTION_HOVER_BG_COLOR_VAR,
null,
)
const sidebarBgColor = useCssVar(
ASIDE_DARK_BG_COLOR,
null,
{
initialValue: `${unref(sidebar).bgColor}px`,
},
)
const asideTextColor = useCssVar(
ASIDE_TEXT_COLOR_VAR,
null,
{
initialValue: LIGHT_TEXT_COLOR,
},
)
const triggerBackgroundColor = useCssVar(
TRIGGER_BG_COLOR_VAR,
null,
)
watchEffect(() => {
headerBgColor.value = unref(header).bgColor
headerTextColor.value = pickTextColorBasedOnBgColor(
unref(header).bgColor,
LIGHT_TEXT_COLOR,
DARK_TEXT_COLOR,
)
if (['#fff', '#ffffff'].includes(unref(header).bgColor.toLowerCase())) {
headerActionHoverBgColor.value = darken(unref(header).bgColor, 6)
setAppConfig({ header: { theme: ThemeEnum.LIGHT } })
}
else {
headerActionHoverBgColor.value = lighten(unref(header).bgColor, 6)
setAppConfig({ header: { theme: ThemeEnum.DARK } })
}
sidebarBgColor.value = unref(sidebar).bgColor
asideTextColor.value = pickTextColorBasedOnBgColor(
unref(sidebar).bgColor,
LIGHT_TEXT_COLOR,
DARK_TEXT_COLOR,
)
if (['#fff', '#ffffff'].includes(unref(sidebar).bgColor.toLowerCase())) {
triggerBackgroundColor.value = darken(unref(sidebar).bgColor, 6)
setAppConfig({ sidebar: { theme: ThemeEnum.LIGHT } })
}
else {
triggerBackgroundColor.value = lighten(unref(sidebar).bgColor, 6)
setAppConfig({ sidebar: { theme: ThemeEnum.DARK } })
}
toggleGrayMode(unref(grayMode))
toggleColorWeak(unref(colorWeak))
// toggleClass(
// ThemeEnum.DARK === unref(theme),
// ThemeEnum.DARK,
// document.documentElement,
// )
})
}
export function useAppTheme() {
const themeStore = useThemeStore()
const { setThemeConfig, setSidebarTheme, setHeaderTheme } = themeStore
const { themeConfig: getThemeConfig, theme, sidebar, header } = storeToRefs(themeStore)
const darkRef = useDark({
selector: 'html',
attribute: 'theme-mode',
valueDark: 'dark',
valueLight: '',
})
const toggleDark = useToggle(darkRef)
const isDark = computed(() => unref(darkRef) && unref(theme) === ThemeEnum.DARK)
const toggleTheme = (dark: boolean) => {
theme.value = dark ? ThemeEnum.DARK : ThemeEnum.LIGHT
toggleDark(dark)
}
// sidebar
const isSidebarDark = computed(() => (unref(theme) === ThemeEnum.DARK || unref(sidebar) === ThemeEnum.DARK))
const toggleSidebarTheme = (dark: boolean) => {
setSidebarTheme(dark ? ThemeEnum.DARK : ThemeEnum.LIGHT)
}
const isHeaderDark = computed(() => (unref(theme) === ThemeEnum.DARK || unref(header) === ThemeEnum.DARK))
const toggleHeaderTheme = (dark: boolean) => {
setHeaderTheme(dark ? ThemeEnum.DARK : ThemeEnum.LIGHT)
}
const primaryColor = computed(() => {
return getThemeConfig.value.primaryColor
})
const infoColor = computed(() => {
return getThemeConfig.value.infoColor
})
const successColor = computed(() => {
return getThemeConfig.value.successColor
})
const warningColor = computed(() => {
return getThemeConfig.value.warningColor
})
const errorColor = computed(() => {
return getThemeConfig.value.errorColor
})
const themeColors = computed(() => {
let colors: ThemeColors = {}
const themeConfig = getThemeConfig.value
if (themeConfig.primaryColor) {
const primaryColorList = generateColors(themeConfig.primaryColor)
colors = {
...colors,
...{
primaryColor: primaryColorList[5],
primaryColorHover: primaryColorList[4],
primaryColorPressed: primaryColorList[4],
primaryColorSuppl: primaryColorList[6],
},
}
}
if (themeConfig.infoColor) {
const infoColorList = generateColors(themeConfig.infoColor)
colors = {
...colors,
...{
infoColor: infoColorList[5],
infoColorHover: infoColorList[4],
infoColorPressed: infoColorList[4],
infoColorSuppl: infoColorList[6],
},
}
}
if (themeConfig.successColor) {
const successColorList = generateColors(themeConfig.successColor)
colors = {
...colors,
...{
successColor: successColorList[5],
successColorHover: successColorList[4],
successColorPressed: successColorList[4],
successColorSuppl: successColorList[6],
},
}
}
if (themeConfig.warningColor) {
const warningColorList = generateColors(themeConfig.warningColor)
colors = {
...colors,
...{
warningColor: warningColorList[5],
warningColorHover: warningColorList[4],
warningColorPressed: warningColorList[4],
warningColorSuppl: warningColorList[6],
},
}
}
if (themeConfig.errorColor) {
const errorColorList = generateColors(themeConfig.errorColor)
colors = {
...colors,
...{
errorColor: errorColorList[5],
errorColorHover: errorColorList[4],
errorColorPressed: errorColorList[4],
errorColorSuppl: errorColorList[6],
},
}
}
return colors
})
watch(
themeColors,
(val) => {
val.primaryColor && setCssVar('--primary-color', val.primaryColor)
val.successColor && setCssVar('--success-color', val.successColor)
val.errorColor && setCssVar('--error-color', val.errorColor)
val.warningColor && setCssVar('--warning-color', val.warningColor)
},
{ deep: true },
)
return {
isDark,
isSidebarDark,
isHeaderDark,
toggleTheme,
primaryColor,
infoColor,
successColor,
warningColor,
errorColor,
themeColors,
setThemeConfig,
toggleSidebarTheme,
toggleHeaderTheme,
}
}

@ -23,6 +23,16 @@ export enum NavBarModeEnum {
TOP_MENU = 'top-menu', TOP_MENU = 'top-menu',
} }
/**
*
*/
export enum PermissionModeEnum {
// 角色权限模式
ROLE = 'role',
// 权限代码模式
PERM = 'perm',
}
// Session过期处理方式 // Session过期处理方式
export enum SessionTimeoutProcessingEnum { export enum SessionTimeoutProcessingEnum {
ROUTE_JUMP, // 路由跳转 ROUTE_JUMP, // 路由跳转
@ -54,3 +64,17 @@ export enum ErrorTypeEnum {
AJAX = 'ajax', AJAX = 'ajax',
PROMISE = 'promise', PROMISE = 'promise',
} }
// 异常类型
export enum ExceptionEnum {
// 无权访问
PAGE_NOT_ACCESS = 403,
// 页面未找到
PAGE_NOT_FOUND = 404,
// 服务器错误
ERROR = 500,
// 网络错误
NET_WORK_ERROR = 10000,
// 页面无数据
PAGE_NOT_DATA = 10100,
}

@ -40,6 +40,19 @@ export const SIDE_BAR_BG_COLOR_LIST: string[] = [
'#383f45', '#383f45',
] ]
export enum ThemeChangeEnum {
THEME_CHANGE,
THEME_PRIMARY_COLOR_CHANGE,
THEME_INFO_COLOR_CHANGE,
THEME_SUCCESS_COLOR_CHANGE,
THEME_WARNING_COLOR_CHANGE,
THEME_ERROR_COLOR_CHANGE,
THEME_HEADER_BG_COLOR_CHANGE,
THEME_SIDEBAR_BG_COLOR_CHANGE,
}
// 设置事件Enum // 设置事件Enum
export enum HandlerSettingEnum { export enum HandlerSettingEnum {
CHANGE_LAYOUT, CHANGE_LAYOUT,

@ -1,18 +0,0 @@
<script setup lang="ts">
const router = useRouter()
const { t } = useI18n()
</script>
<template>
<main p="x4 y10" text="center teal-700 dark:gray-200">
<div text-4xl>
<div i-carbon-warning inline-block />
</div>
<RouterView />
<div>
<button btn text-sm m="3 t8" @click="router.back()">
{{ t('button.back') }}
</button>
</div>
</main>
</template>

@ -1,33 +1,45 @@
<!-- eslint-disable unused-imports/no-unused-vars -->
<script setup lang="ts"> <script setup lang="ts">
import { NIcon } from 'naive-ui'
import type { RouteLocationMatched } from 'vue-router' import type { RouteLocationMatched } from 'vue-router'
import { REDIRECT_NAME } from '~/constants' import { REDIRECT_NAME } from '~/constants'
import { filterTree, isString } from '~/utils' import { filterTree, getAllParentPath, isString } from '~/utils'
const { header } = useAppConfig() const { header } = useAppConfig()
const { currentRoute } = useRouter() const { currentRoute } = useRouter()
const { t } = useI18n() const { t } = useI18n()
const go = useGo() const go = useGo()
const routes = $ref<RouteLocationMatched[]>([]) let routes = $ref<RouteLocationMatched[]>([])
const { renderIcon } = useIcon()
// const { menuList } = useMenu() const { getMenuList } = useMenu()
watchEffect(async () => { watchEffect(async () => {
if (currentRoute.value.name === REDIRECT_NAME) { if (currentRoute.value.name === REDIRECT_NAME) {
console.log('233') return
} }
// const menus = unref(menuList) const menus = await getMenuList()
// const { matched: routeMatched } = unref(currentRoute) const { matched: routeMatched } = unref(currentRoute)
const cur = routeMatched?.[routeMatched.length - 1]
let path = currentRoute.value.path
if (cur && cur?.meta?.currentActiveMenu) {
path = cur.meta.currentActiveMenu as string
}
// const cur = routeMatched?.[routeMatched.length - 1] const parent = getAllParentPath(menus, path)
// let path = currentRoute.value.path const filterMenus = menus.filter(item => item.path === parent[0])
// if(cur && cur?.meta?.currentActiveMenu) { const matched = getMatched(filterMenus, parent) as any
// path = cur.meta.currentActiveMenu as string if (!matched || matched.length === 0) {
// } return
// const parent = }
const breadcrumbList = filterItem(matched)
if (currentRoute.value.meta?.currentActiveMenu) {
breadcrumbList.push({
...currentRoute.value,
name: currentRoute.value.name,
} as unknown as RouteLocationMatched)
}
routes = breadcrumbList
}) })
function getMatched(menus: Menu[], parent: string[]) { function getMatched(menus: Menu[], parent: string[]) {
@ -64,15 +76,11 @@ function renderDropdownLabel(route: any) {
} }
function renderDropdownIcon(option: any) { function renderDropdownIcon(option: any) {
return option.icon ? renderIcon(option.icon)() : null return option.icon ? renderIcon(option.icon) : null
}
function renderIcon(icon: string) {
return () => h(NIcon, null, { default: () => h('div', { [icon]: '', class: icon }) })
} }
function handleClick(path: string, route: Recordable<any>) { function handleClick(path: string, route: Recordable<any>) {
const { children, meta, redirect } = route const { children, redirect } = route
if (children?.length && !redirect) { if (children?.length && !redirect) {
return return
} }
@ -92,16 +100,21 @@ function handleClick(path: string, route: Recordable<any>) {
<NBreadcrumb v-if="header.showBreadCrumb"> <NBreadcrumb v-if="header.showBreadCrumb">
<NBreadcrumbItem v-for="(route, index) in routes" :key="index"> <NBreadcrumbItem v-for="(route, index) in routes" :key="index">
<NDropdown <NDropdown
key-field="path" size="small" :options="route.children" :render-label="renderDropdownLabel" key-field="path"
:render-icon="renderDropdownIcon" @select="handleClick" size="small"
:options="route.children"
:render-label="renderDropdownLabel"
:render-icon="renderDropdownIcon"
@select="handleClick"
> >
<NSpace align="center" :size="0"> <NSpace align="center" :size="0">
<div <LIcon
v-if="route.meta.icon && header.showBreadCrumbIcon" class="v-middle" v-if="route.meta.icon && header.showBreadCrumbIcon"
:class="`i-${route.meta.icon}`" class="v-middle"
:icon="route.meta.icon"
/> />
<span class="ml-1.2 mr-1.2">{{ $t(route.meta.title ?? '') }}</span> <span class="ml-1.2 mr-1.2">{{ $t(route.meta.title || '') }}</span>
<div v-if="route.children" class="i-gridicons:dropdown" /> <LIcon v-if="route.children" icon="ant-design:caret-down-filled" />
</NSpace> </NSpace>
</NDropdown> </NDropdown>
</NBreadcrumbItem> </NBreadcrumbItem>

@ -21,10 +21,10 @@ const style = computed(() => (
<template> <template>
<footer :class="bem()" :style="style"> <footer :class="bem()" :style="style">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<template v-for="(item, index) in links" :key="index"> <template v-for="(item, _index) in links" :key="_index">
<NButton text tag="a" :href="item.url" target="_blank" class="mx-1"> <NButton text tag="a" :href="item.url" target="_blank" class="mx-1!">
<span class="flex items-center lh-32px"> <span class="flex items-center lh-32px">
<div :class="`i-${item.icon}`" size="18" /> <LIcon :icon="item.icon" size="18" :depth="2" />
<NText depth="3">{{ item.label }}</NText> <NText depth="3">{{ item.label }}</NText>
</span> </span>
</NButton> </NButton>
@ -35,3 +35,15 @@ const style = computed(() => (
</NText> </NText>
</footer> </footer>
</template> </template>
<style lang="less" scoped>
.footer {
--footer-padding: 0 20px;
--footer-height: 60px;
padding: var(--footer-padding);
box-sizing: border-box;
flex-shrink: 0;
height: var(--footer-height);
text-align: center;
}
</style>

@ -0,0 +1,15 @@
<script setup lang="ts">
const { toggle, isFullscreen } = useFullscreen()
</script>
<template>
<div
class="px-3!"
@click="toggle"
>
<LIcon
class="hover:cursor-pointer"
:icon="isFullscreen ? 'ant-design:fullscreen-exit-outlined' : 'ant-design:fullscreen-outlined'"
/>
</div>
</template>

@ -3,7 +3,10 @@ import Logo from './logo/index.vue'
import HeaderTrigger from './trigger/header-trigger.vue' import HeaderTrigger from './trigger/header-trigger.vue'
import BreadCrumb from './breadcrumb/index.vue' import BreadCrumb from './breadcrumb/index.vue'
import SettingButton from './setting/components/setting-button.vue' import SettingButton from './setting/components/setting-button.vue'
import Fullscreen from './fullscreen/index.vue'
import LayoutTabs from './tabs/index.vue'
import { SettingButtonPositionEnum } from '~/constants' import { SettingButtonPositionEnum } from '~/constants'
import { createNamespace } from '~/utils'
const { const {
isTopMenu, isTopMenu,
@ -13,8 +16,9 @@ const {
const { const {
getShowHeader, getShowHeader,
getShowHeaderLogo, getShowFullScreen,
getShowFullHeaderRef, getShowFullHeaderRef,
getShowLocalePicker,
} = useHeaderSetting() } = useHeaderSetting()
const { const {
@ -24,6 +28,8 @@ const {
const { getShowSettingButton, getSettingButtonPosition } = useRootSetting() const { getShowSettingButton, getSettingButtonPosition } = useRootSetting()
const { getShowMultipleTab } = useMultipleTabSetting()
const getShowSetting = computed(() => { const getShowSetting = computed(() => {
if (!unref(getShowSettingButton)) { if (!unref(getShowSettingButton)) {
return false return false
@ -35,50 +41,105 @@ const getShowSetting = computed(() => {
return settingButtonPosition === SettingButtonPositionEnum.HEADER return settingButtonPosition === SettingButtonPositionEnum.HEADER
}) })
// TODO multiple tab // header
const getShowHeaderMultipleTab = computed(() => unref(getShowMultipleTab) && !unref(isMix))
// headerlogo
const showHeaderLogoRef = computed(() => unref(isTopMenu) || unref(isMix))
// header
const showHeaderBreadcrumb = computed(() => {
return !(unref(isTopMenu) || (unref(isMix) && unref(menu).split))
})
// Logo // Logo
const logoWidth = computed(() => (unref(isTopMenu) ? 150 : getMenuWidth)) const logoWidth = computed(() => (unref(isTopMenu) ? 150 : getMenuWidth))
const { bem } = createNamespace('layout-header')
const { isFullscreen } = useFullscreen()
</script> </script>
<template> <template>
<NSpace vertical> <NSpace :class="bem()" vertical>
<NSpace <NSpace
v-if="getShowFullHeaderRef" class="h-48px shadow" :class="[{ 'mb-8px': !false }]" v-if="getShowFullHeaderRef"
:style="{ '--un-shadow-color': 'var(--n-border-color)' }" justify="space-between" align="center" class="h-48px shadow"
:class="[{ 'mb-8px': !getShowHeaderMultipleTab }]"
:style="{ '--un-shadow-color': 'var(--n-border-color)' }"
justify="space-between"
align="center"
> >
<slot name="logo"> <slot name="logo">
<NSpace align="center" class="items-center" :size="0"> <NSpace align="center" class="items-center" :size="0">
<Logo <Logo
v-if="getShowHeaderLogo" :style="{ v-if="showHeaderLogoRef"
:style="{
width: `${logoWidth}px`, width: `${logoWidth}px`,
maxWidth: `${logoWidth}px`, maxWidth: `${logoWidth}px`,
}" }"
/> />
<HeaderTrigger v-if="getShowHeaderTrigger" class="mx-2" /> <HeaderTrigger v-if="getShowHeaderTrigger" class="mx-2" />
<slot name="breadcrumb"> <slot v-if="showHeaderBreadcrumb" name="breadcrumb">
<BreadCrumb v-if="!(isTopMenu || (isMix && menu.split))" /> <BreadCrumb />
</slot> </slot>
</NSpace> </NSpace>
</slot> </slot>
<slot name="menu" /> <slot v-if="!showHeaderBreadcrumb" name="menu" />
<div class="pl-8px pr-8px"> <div class="pl-8px" :class="bem('action')">
<slot name="buttons"> <NSpace :size="0" align="center" :wrap-item="false">
<NSpace class="p-1" :size="16" align="center"> <!-- Search -->
<!-- Search --> <!-- Notify -->
<!-- Notify --> <NPopover trigger="hover">
<!-- FullScreen --> <template #trigger>
<!-- LocalePicker --> <Fullscreen v-if="getShowFullScreen" :class="bem('action__item')" />
<!-- UserDropDown --> </template>
<SettingButton v-if="getShowSetting" /> <span>{{ unref(isFullscreen) ? $t('layout.header.tooltipExitFull') : $t('layout.header.tooltipEntryFull') }}</span>
</NSpace> </NPopover>
</slot> <NPopover trigger="hover">
<template #trigger>
<LLocalePicker v-if="getShowLocalePicker" :class="bem('action__item')" :reload="true" :show-text="false" />
</template>
<span>{{ $t('layout.header.tooltipLanguage') }}</span>
</NPopover>
<!-- UserDropDown -->
<NPopover trigger="hover">
<template #trigger>
<SettingButton v-if="getShowSetting" :class="bem('action__item')" />
</template>
<span>{{ $t('layout.header.tooltipSettings') }}</span>
</NPopover>
</NSpace>
</div> </div>
</NSpace> </NSpace>
<!-- Multiple tab --> <!-- Multiple tab -->
<template v-if="false"> <template v-if="getShowHeaderMultipleTab">
<slot name="tags"> <slot name="tabs">
<!-- LayoutTabs --> <LayoutTabs />
</slot> </slot>
</template> </template>
</NSpace> </NSpace>
</template> </template>
<style lang="less" scoped>
.layout-header {
&__action {
display: flex;
align-items: center;
&__item {
display: flex!important;
align-items: center;
height: 48px;
padding: 0 2px;
font-size: 1.2em;
cursor: pointer;
}
&__item:hover {
background-color: var(--n-border-color);
}
}
}
</style>

@ -1,42 +1,54 @@
<script setup lang="ts"> <script setup lang="ts">
import { BASIC_HOME_PATH } from '~/constants'
import { createNamespace } from '~/utils'
const props = defineProps({ const props = defineProps({
title: { type: String, default: '' }, showTitle: { type: Boolean, default: true },
showTitle: { homePath: { type: String, default: BASIC_HOME_PATH },
type: Boolean,
default: true,
},
}) })
const { getCollapsed } = useMenuSetting() const { push } = useRouter()
const { logo } = useSiteSetting() const { logo } = useSiteSetting()
const { getShowLogo } = useRootSetting()
const getIsShowLogo = computed(() => unref(getShowLogo))
const title = import.meta.env.VITE_APP_TITLE
const { bem } = createNamespace('app-logo')
function goHome() {
push(props.homePath)
}
</script> </script>
<template> <template>
<div class="logo"> <div v-if="getIsShowLogo" :class="bem()" @click="goHome">
<img :src="logo" alt="" :class="{ 'mr-2': !getCollapsed }"> <img :src="`/${logo}`" alt="logo">
<h2 v-show="!getCollapsed && props.showTitle" class="title"> <div v-show="showTitle" class="ml-2 truncate md:opacity-100" :class="bem('title')">
{{ title }} {{ title }}
</h2> </div>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.logo { .app-logo {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; padding-left: 7px;
height: 64px; cursor: pointer;
line-height: 64px; transition: all 0.2s ease;
overflow: hidden; height: 48px;
white-space: nowrap; background: transparent;
box-sizing: border-box;
img { img {
width: auto; width: 32px;
height: 32px;
} }
.title { &__title {
margin: 0; font-size: 16px;
font-weight: 700;
transition: all 0.5s;
line-height: normal;
} }
} }
</style> </style>

@ -8,7 +8,7 @@ const { bem } = createNamespace('main')
<template> <template>
<LayoutFeature /> <LayoutFeature />
<main :class="bem()"> <main :class="bem()" class="h-full w-full p-6">
<slot /> <slot />
</main> </main>
</template> </template>

@ -1,11 +1,11 @@
<script setup lang="ts" name="LayoutMenu"> <script setup lang="ts" name="LayoutMenu">
import type { MenuInst } from 'naive-ui' import type { MenuInst } from 'naive-ui'
import { NIcon } from 'naive-ui' import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router' import type { MenuMixedOption } from 'naive-ui/es/menu/src/interface'
import Logo from '../logo/index.vue' import Logo from '../logo/index.vue'
import FooterTrigger from '../trigger/footer-trigger.vue' import FooterTrigger from '../trigger/footer-trigger.vue'
import { createNamespace, listenerRouteChange, mapTree } from '~/utils' import { createNamespace, emitter, listenerRouteChange, mapTree } from '~/utils'
import { REDIRECT_NAME } from '~/constants' import { REDIRECT_NAME } from '~/constants'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -16,7 +16,7 @@ const props = withDefaults(defineProps<{
split: false, split: false,
}) })
const title = import.meta.env.VITE_APP_TITLE const { isSidebarDark } = useAppTheme()
const { const {
menu, menu,
@ -24,19 +24,23 @@ const {
getCollapsedShowTitle, getCollapsedShowTitle,
sidebar, sidebar,
isSidebar, isSidebar,
isMix,
} = useAppConfig() } = useAppConfig()
const { getTopMenuAlign, getShowFooterTrigger } = useMenuSetting()
const showSidebarLogo = computed(() => { const {
return unref(isSidebar) || unref(isMixSidebar) getTopMenuAlign,
}) getShowFooterTrigger,
getCollapsed,
} = useMenuSetting()
const { bem } = createNamespace('layout-menu') const { bem } = createNamespace('layout-menu')
const { t } = useI18n() const { t } = useI18n()
const { currentRoute } = useRouter() const { currentRoute } = useRouter()
const { renderIcon } = useIcon()
const menuRef = $ref<MenuInst | null>(null) const menuRef = $ref<MenuInst | null>(null)
let menuListRef = $ref<any[]>([]) const options = shallowRef<MenuMixedOption[]>([])
let menuListRef = $ref<Menu[]>([])
let activeKey = $ref<any>() let activeKey = $ref<any>()
const getMenuCollapsed = computed(() => { const getMenuCollapsed = computed(() => {
@ -46,6 +50,10 @@ const getMenuCollapsed = computed(() => {
return unref(sidebar).collapsed return unref(sidebar).collapsed
}) })
const showSidebarLogo = computed(() => {
return unref(isSidebar) || unref(isMixSidebar)
})
// //
function showOption() { function showOption() {
nextTick(() => { nextTick(() => {
@ -56,12 +64,21 @@ function showOption() {
}) })
} }
const { menuList, generateRoutes } = useMenu() const { getMenuList } = useMenu()
// TODO
onMounted(async () => { onMounted(async () => {
await generateRoutes() const menuList = await getMenuList()
menuListRef = mapTree<any>(unref(menuList), { conversion: menu => routeToOption(menu) }) menuListRef = mapTree(menuList, { conversion: menu => menu })
showOption() options.value = mapTree(menuList, { conversion: menu => routeToOption(menu) })
//
if (props.split) {
//
emitter.on('menuChange', (p) => {
activeKey = p.name
options.value = p.options
})
}
handleMenuChange()
}) })
listenerRouteChange((route: RouteLocationNormalizedLoaded) => { listenerRouteChange((route: RouteLocationNormalizedLoaded) => {
@ -74,73 +91,140 @@ listenerRouteChange((route: RouteLocationNormalizedLoaded) => {
activeKey = currentActiveMenu activeKey = currentActiveMenu
} }
showOption() showOption()
}) }, false)
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) { // keykey
const menu = route || unref(currentRoute) function flatten(arr: Menu[], prefix: string = '') {
activeKey = menu.name let result: any = {}
arr.forEach((item) => {
const key = `${prefix}|${item.key as string}`
if (Array.isArray(item.children)) {
result = { ...result, ...flatten(item.children, key) }
}
else {
result[key] = item
}
})
return result
} }
// // key
function routeToOption(item: RouteRecordRaw) { function findTopKey(flattened: any, key: string) {
const { name, children, meta: metaRef, component } = item return Object.keys(flattened)
const meta = unref(metaRef) .find(k => k.endsWith(key))
const title = meta?.title ? t(meta.title) : '' ?.split('|')
.filter(v => v)[0]
}
const icon = `i-${meta?.icon}` async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
const currentMenu = route || unref(currentRoute)
activeKey = currentMenu.name
//
if (unref(menu).split && unref(isMix)) {
if (!props.split) {
options.value.forEach((v) => {
delete v.children
})
}
// key
activeKey = findTopKey(
flatten(menuListRef),
currentMenu.name as string,
)
// tab
emitter.emit('menuChange', {
name: currentMenu.name,
options: menuListRef.find(v => v.key === activeKey)?.children,
})
}
showOption()
}
//
function routeToOption(item: Menu): MenuMixedOption {
// namechildrenmetaicon
const { name, children, meta, icon } = item
// meta.titlettitle
const title = t(meta?.title as string)
// Option
return { return {
label: () => { label: () => {
if (!component) {
return title
}
if (children && children.length > 0) { if (children && children.length > 0) {
return title return title
} }
// children使RouterLinkname
return h( return h(
RouterLink, RouterLink,
{ {
to: { to: { name },
name,
},
}, },
{ default: () => title }, { default: () => title },
) )
}, },
// keyname
key: name, key: name,
// iconrenderIconicon
icon: () => { icon: () => {
if (!meta?.icon) return renderIcon({ icon })
return true
return h(NIcon, null, { default: () => h('div', { [icon]: '', class: icon }) })
}, },
} }
} }
// function clickMenu(key: any) { function clickMenu(key: any) {
// if (unref(isTopMenu) && menu.value.split && !props.split) { if (unref(isMix) && unref(menu).split && !props.split) {
// // emit // emit
emitter.emit('menuChange', {
// } name: activeKey,
// } options: menuListRef.find(v => v.key === key)?.children,
})
}
}
</script> </script>
<template> <template>
<div :class="bem()"> <div :class="bem()">
<Logo v-if="showSidebarLogo" :class="bem('logo')" :title="title" :show-title="getCollapsedShowTitle" /> <!-- TODO: Mobile -->
<Logo v-if="showSidebarLogo" :class="bem('logo')" :show-title="getCollapsedShowTitle" />
<NScrollbar :class="bem('scrollbar')"> <NScrollbar :class="bem('scrollbar')">
<NMenu <NMenu
ref="menuRef" v-model:value="activeKey" class="w-full" :class="bem('menu')" :style="{ v-if="options && options.length > 0"
ref="menuRef"
v-model:value="activeKey"
class="w-full"
:style="{
justifyContent: getTopMenuAlign === 'center' ? 'center' : `flex-${getTopMenuAlign}`, justifyContent: getTopMenuAlign === 'center' ? 'center' : `flex-${getTopMenuAlign}`,
}" :options="menuListRef" :collapsed="getMenuCollapsed" :collapsed-width="48" }"
:collapsed-icon-size="22" :indent="18" :root-indent="18" :mode="props.mode" :accordion="menu.accordion" :options="options"
:collapsed="getMenuCollapsed"
:collapsed-width="48"
:collapsed-icon-size="22"
:indent="18"
:root-indent="18"
:mode="props.mode"
:accordion="menu.accordion"
:inverted="mode === 'vertical' && isSidebarDark"
@update:value="clickMenu"
/> />
<NEmpty
v-else
class="py-8"
:size="getCollapsed ? 'small' : 'huge'"
description="当前主菜单下无子菜单"
>
<template #icon>
<LIcon icon="ant-design:unordered-list-outlined" :size="getCollapsed ? 24 : 42" />
</template>
</NEmpty>
</NScrollbar> </NScrollbar>
<FooterTrigger v-if="getShowFooterTrigger" /> <FooterTrigger v-if="getShowFooterTrigger" />
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
div:has(> div[class='layout-menu']) {
flex: 1;
}
.layout-menu { .layout-menu {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -155,20 +239,20 @@ function routeToOption(item: RouteRecordRaw) {
flex-basis: auto; flex-basis: auto;
} }
&__menu:not(.n-menu--collapsed) { // &__menu:not(.n-menu--collapsed) {
.n-menu-item-content { // .n-menu-item-content {
&::before { // &::before {
left: 5px; // left: 5px;
right: 5px; // right: 5px;
} // }
&.n-menu-item-content--selected, // &.n-menu-item-content--selected,
&:hover { // &:hover {
&::before { // &::before {
border-left: 4px solid var(--primary-color); // border-left: 4px solid var(--primary-color);
} // }
} // }
} // }
} // }
} }
</style> </style>

@ -0,0 +1,374 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import Logo from '../logo/index.vue'
import SiderTrigger from '../trigger/sider-trigger.vue'
import MixSubMenu from './mix-sub-menu.vue'
import { createNamespace, listenerRouteChange } from '~/utils'
import { MixSidebarTriggerEnum, NavBarModeEnum } from '~/constants'
const props = defineProps({
mixSidebarWidth: { type: Number, default: 40 },
})
const title = import.meta.env.VITE_APP_TITLE
const { t } = useI18n()
const { bem } = createNamespace('layout-mix-menu')
const { currentRoute } = useRouter()
const go = useGo()
let currentRouteRef = $ref<Nullable<RouteLocationNormalized>>(null)
let menuModulesRef = $ref<Menu[]>([])
let activePathRef = $ref<string>('')
let childrenMenus = $ref<Menu[]>([])
let openMenu = $ref(false)
let childrenTitle = $ref('')
const sideRef = ref<ElRef>(null)
const {
getIsFixed,
getIsMixSidebar,
getMixSideFixed,
getMixSideTrigger,
getCloseMixSidebarOnChange,
getMenuWidth,
getCollapsed,
getMenuType,
mixSideHasChildren,
setMenuSetting,
} = useMenuSetting()
const {
getChildrenMenus,
getCurrentParentPath,
getShallowMenus,
} = useMenu()
let oldIsFixed = unref(getIsFixed)
let pushpin = unref(getIsFixed)
onMounted(async () => {
menuModulesRef = await getShallowMenus()
activePathRef = await getCurrentParentPath(currentRoute.value.path)
const currentItem = menuModulesRef.find(item => item.path === activePathRef)
if (currentItem) {
handleModuleClick(currentItem.path, false, currentItem.meta?.title || '')
}
})
listenerRouteChange(async (route) => {
currentRouteRef = route
activePathRef = await getCurrentParentPath(route.path)
setActive(true)
if (unref(getCloseMixSidebarOnChange)) {
closeMenu()
}
})
// Set the currently active menu and submenu
async function setActive(setChildren = false) {
const path = currentRouteRef?.path
if (!path) {
return
}
// activePathRef = await getCurrentParentPath(path)
if (unref(getIsMixSidebar)) {
const activeMenu = unref(menuModulesRef).find(item => item.path === unref(activePathRef))
const p = activeMenu?.path
if (p) {
const children = await getChildrenMenus(p)
if (setChildren) {
childrenMenus = children
if (unref(getMixSideFixed)) {
openMenu = children.length > 0
}
}
if (children.length === 0) {
childrenMenus = []
}
}
}
mixSideHasChildren.value = childrenMenus.length > 0
}
function closeMenu() {
if (!unref(getIsFixed)) {
openMenu = false
setActive(false)
}
}
// Process module menu click
async function handleModuleClick(path: string, hover = false, title = '') {
const children = await getChildrenMenus(path)
childrenTitle = t(title)
if (unref(activePathRef) === path) {
if (!hover) {
if (!unref(openMenu)) {
openMenu = true
}
else {
closeMenu()
}
}
else {
if (!unref(openMenu)) {
openMenu = true
}
}
if (!unref(openMenu)) {
await setActive()
}
}
else {
openMenu = true
activePathRef = path
}
if (!children || children.length === 0) {
if (!hover) {
go(path)
}
childrenMenus = []
closeMenu()
return
}
childrenMenus = children
mixSideHasChildren.value = childrenMenus.length > 0
}
function getItemEvents(item: Menu) {
if (unref(getMixSideTrigger) === MixSidebarTriggerEnum.HOVER) {
return {
onMouseenter: () => {
return handleModuleClick(item.path, true, item.meta?.title || '')
},
onClick: async () => {
const children = await getChildrenMenus(item.path)
if (item.path && (!children || children.length === 0)) {
go(item.path)
}
},
}
}
return {
onClick: () => {
handleModuleClick(item.path, false, item.meta?.title || '')
},
}
}
const getMenuEventsRef = computed(() => {
return !unref(getMixSideFixed)
? {
onMouseleave: () => {
//
// if (!openMenu.value) {
// setActive(true)
// }
// Menu
// closeMenu()
},
onMouseenter: () => {
},
}
: {}
})
function handleFixedMenu() {
setMenuSetting({
mixSideFixed: !unref(getMixSideFixed),
})
pushpin = !unref(getMixSideFixed)
}
const getMenuStyle = computed((): CSSProperties => {
if (unref(getIsFixed)) {
setActive(true)
}
else {
if (oldIsFixed !== unref(getIsFixed) && !pushpin) {
closeMenu()
}
else {
pushpin = false
}
}
oldIsFixed = unref(getIsFixed)
return {
width: unref(openMenu) ? `${unref(getMenuWidth)}px` : 0,
left: `${props.mixSidebarWidth}px`,
}
})
</script>
<template>
<div :class="bem()">
<Logo
v-if="getMenuType === NavBarModeEnum.MIX_SIDEBAR"
class="shadow"
:class="[bem('logo')]"
:style="{ '--un-shadow-color': 'var(--n-border-color)' }"
:show-title="false"
/>
<NScrollbar
:class="bem('scrollbar')"
v-bind="getMenuEventsRef"
>
<ul :class="bem('module')">
<li
v-for="item in menuModulesRef"
:key="item.path"
:class="[
bem('module__item'),
{
[bem('module__item--active') as string]: item.path === activePathRef,
},
]"
v-bind="getItemEvents(item)"
>
<LIcon
:class="bem('module__icon')"
:size="getCollapsed ? 16 : 20"
:icon="item.icon || (item.meta && item.meta.icon)"
/>
<p v-show="!getCollapsed" :class="bem('module__name')">
{{ $t(item.meta?.title || '') }}
</p>
</li>
</ul>
</NScrollbar>
<SiderTrigger />
<div
ref="sideRef"
class="shadow"
:class="[bem('menu-list')]"
:style="getMenuStyle"
@mouseleave="closeMenu"
>
<div
v-show="openMenu && childrenMenus.length > 0"
class="shadow"
:class="[
bem('menu-list__title'),
{
show: openMenu,
},
]"
>
<span class="text">{{ title }}</span>
<LIcon
:size="16"
:icon="getMixSideFixed ? 'ant-design:pushpin-filled' : 'ant-design:pushpin-outlined'"
class="pushpin hover:cursor-pointer"
@click="handleFixedMenu"
/>
</div>
<NH5
v-if="openMenu"
class="mb-2!"
:class="bem('menu-list__children-title')"
:style="{ '--n-text-color': 'var(--n-text-color)' }"
prefix="bar"
strong
>
{{ childrenTitle }}
</NH5>
<MixSubMenu :list="childrenMenus" />
</div>
</div>
</template>
<style lang="less" scoped>
.layout-mix-menu {
display: flex;
flex-direction: column;
height: 100%;
&__logo {
flex-shrink: 0;
justify-content: center;
}
&__scrollbar {
flex: 1;
flex-basis: auto;
}
&__module {
position: relative;
padding: 1px 0 40px 0;
margin: 0;
&__item {
position: relative;
padding: 12px 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: pointer;
transition: all 0.3s ease;
&:hover,
&--active {
// font-weight: 700;
color: #18a058;
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background-color: #18a058;
content: '';
}
}
}
&__icon {
transition: all 0.3s;
}
&__name {
margin-bottom: 0;
margin-top: 5px;
font-size: 12px;
transition: all 0.3s;
}
}
&__menu-list {
position: fixed;
height: 100%;
top: 0;
width: 0px;
height: calc(100%);
background-color: var(--n-color);
transition: all 0.3s;
&__title {
display: flex;
height: 48px;
font-size: 18px;
opacity: 0%;
transition: unset;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding-left: 20px;
&.show {
min-width: 130px;
opacity: 100%;
transition: all 0.5s ease;
}
.pushpin {
margin-right: 8px;
}
}
&__children-title {
padding: 6px 20px;
margin: 0;
}
}
}
</style>

@ -0,0 +1,114 @@
<script setup lang="ts">
import type { MenuInst } from 'naive-ui'
import type { MenuMixedOption } from 'naive-ui/es/menu/src/interface'
import { RouterLink } from 'vue-router'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { REDIRECT_NAME } from '~/constants'
import { createNamespace, listenerRouteChange, mapTree } from '~/utils'
const props = defineProps({
list: {
type: Array as PropType<Menu[]>,
default: () => [],
},
})
const { getAccordion } = useMenuSetting()
const { renderIcon } = useIcon()
const { t } = useI18n()
const { currentRoute } = useRouter()
const menuRef = $ref<Nullable<MenuInst>>(null)
let activeKey = $ref<any>()
//
function showOption() {
nextTick(() => {
if (!menuRef) {
return
}
menuRef.showOption()
})
}
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
const menu = route || unref(currentRoute)
activeKey = menu.name
}
listenerRouteChange((route) => {
if (route.name === REDIRECT_NAME) {
return
}
const currentActiveMenu = route.meta?.currentActiveMenu as string
handleMenuChange(route)
if (currentActiveMenu) {
activeKey.value = currentActiveMenu
}
showOption()
})
//
function routerToMenu(item: Menu): MenuMixedOption {
const { name, children, meta, icon } = item
const title = t(meta?.title as string)
return {
label: () => {
if (children && children.length > 0) {
return title
}
return h(
RouterLink,
{
to: {
name,
},
},
{ default: () => title },
)
},
key: name,
icon: () => renderIcon({ icon }),
}
}
const menuListRef = computed(() => {
return mapTree(props.list, { conversion: menu => routerToMenu(menu) })
})
const { bem } = createNamespace('layout-menu')
const { isSidebarDark } = useAppTheme()
</script>
<template>
<div :class="bem()">
<NScrollbar :class="bem('scrollbar')">
<NMenu
ref="menuRef"
v-model:value="activeKey"
:options="menuListRef"
:collapsed-width="48"
:collapsed="false"
:collapsed-icon-size="22"
:indent="18"
:root-indent="18"
:accordion="getAccordion"
:inverted="isSidebarDark"
/>
</NScrollbar>
</div>
</template>
<style lang="less" scoped>
.layout-menu {
display: flex;
flex-direction: column;
height: 100%;
&__scrollbar {
flex: 1;
flex-basis: auto;
}
}
</style>

@ -26,43 +26,86 @@ const {
} = useMultipleTabSetting() } = useMultipleTabSetting()
const { getShowHeader } = useHeaderSetting() const { getShowHeader } = useHeaderSetting()
const { baseHandler } = useAppConfig()
function eventHandler(evt: HandlerSettingEnum, val: boolean) {
baseHandler(evt, val)
}
</script> </script>
<template> <template>
<NSpace vertical> <NSpace vertical>
<SwitchItem <SwitchItem
:title="$t('layout.setting.breadcrumb')" :def="getShowBreadCrumb" :title="$t('layout.setting.breadcrumb')"
:event="HandlerSettingEnum.SHOW_BREADCRUMB" :disabled="!getShowHeader" :value="getShowBreadCrumb"
:callback="v => eventHandler(HandlerSettingEnum.SHOW_BREADCRUMB, v)"
:disabled="!getShowHeader"
/>
<SwitchItem
:title="$t('layout.setting.breadcrumbIcon')"
:value="getShowBreadCrumbIcon"
:callback="v => eventHandler(HandlerSettingEnum.SHOW_BREADCRUMB_ICON, v)"
:disabled="!getShowBreadCrumb"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.breadcrumbIcon')" :def="getShowBreadCrumbIcon" :title="$t('layout.setting.tabs')"
:event="HandlerSettingEnum.SHOW_BREADCRUMB_ICON" :disabled="!getShowBreadCrumb" :value="getShowMultipleTab"
:callback="v => eventHandler(HandlerSettingEnum.TABS_SHOW, v)"
/> />
<SwitchItem :title="$t('layout.setting.tabs')" :def="getShowMultipleTab" :event="HandlerSettingEnum.TABS_SHOW" />
<SwitchItem <SwitchItem
:title="$t('layout.setting.tabsRedoBtn')" :def="getShowRedo" :event="HandlerSettingEnum.TABS_SHOW_REDO" :title="$t('layout.setting.tabsRedoBtn')"
:value="getShowRedo"
:callback="v => eventHandler(HandlerSettingEnum.TABS_SHOW_REDO, v)"
:disabled="!getShowMultipleTab" :disabled="!getShowMultipleTab"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.tabsQuickBtn')" :def="getShowQuick" :title="$t('layout.setting.tabsQuickBtn')"
:event="HandlerSettingEnum.TABS_SHOW_QUICK" :disabled="!getShowMultipleTab" :value="getShowQuick"
:callback="v => eventHandler(HandlerSettingEnum.TABS_SHOW_QUICK, v)"
:disabled="!getShowMultipleTab"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.tabsFoldBtn')" :def="getShowFold" :event="HandlerSettingEnum.TABS_SHOW_FOLD" :title="$t('layout.setting.tabsFoldBtn')"
:value="getShowFold"
:callback="v => eventHandler(HandlerSettingEnum.TABS_SHOW_FOLD, v)"
:disabled="!getShowMultipleTab" :disabled="!getShowMultipleTab"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.sidebar')" :def="getShowMenu" :event="HandlerSettingEnum.MENU_SHOW_SIDEBAR" :title="$t('layout.setting.sidebar')"
:value="getShowMenu"
:callback="v => eventHandler(HandlerSettingEnum.MENU_SHOW_SIDEBAR, v)"
:disabled="getIsHorizontal" :disabled="getIsHorizontal"
/> />
<SwitchItem :title="$t('layout.setting.header')" :def="getShowHeader" :event="HandlerSettingEnum.HEADER_SHOW" />
<SwitchItem title="Logo" :def="getShowLogo" :event="HandlerSettingEnum.SHOW_LOGO" :disabled="getIsMixSidebar" />
<SwitchItem :title="$t('layout.setting.footer')" :def="getShowFooter" :event="HandlerSettingEnum.SHOW_FOOTER" />
<SwitchItem <SwitchItem
:title="$t('layout.setting.fullContent')" :def="getFullContent" :title="$t('layout.setting.header')"
:event="HandlerSettingEnum.FULL_CONTENT" :value="getShowHeader"
:callback="v => eventHandler(HandlerSettingEnum.HEADER_SHOW, v)"
/>
<SwitchItem
title="Logo"
:value="getShowLogo"
:callback="v => eventHandler(HandlerSettingEnum.SHOW_LOGO, v)"
:disabled="getIsMixSidebar"
/>
<SwitchItem
:title="$t('layout.setting.footer')"
:value="getShowFooter"
:callback="v => eventHandler(HandlerSettingEnum.SHOW_FOOTER, v)"
/>
<SwitchItem
:title="$t('layout.setting.fullContent')"
:value="getFullContent"
:callback="v => eventHandler(HandlerSettingEnum.FULL_CONTENT, v)"
/>
<SwitchItem
:title="$t('layout.setting.grayMode')"
:value="getGrayMode"
:callback="v => eventHandler(HandlerSettingEnum.GRAY_MODE, v)"
/>
<SwitchItem
:title="$t('layout.setting.colorWeak')"
:value="getColorWeak"
:callback="v => eventHandler(HandlerSettingEnum.COLOR_WEAK, v)"
/> />
<SwitchItem :title="$t('layout.setting.grayMode')" :def="getGrayMode" :event="HandlerSettingEnum.GRAY_MODE" />
<SwitchItem :title="$t('layout.setting.colorWeak')" :def="getColorWeak" :event="HandlerSettingEnum.COLOR_WEAK" />
</NSpace> </NSpace>
</template> </template>

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
const { isDark, toggleTheme } = useAppTheme()
</script> </script>
<template> <template>
<NSpace justify="center"> <NSpace justify="center">
<NSwitch :value="isDark" @update:value="toggleDark"> <NSwitch :value="isDark" @update:value="toggleTheme">
<template #checked> <template #checked>
{{ $t('layout.setting.darkMode') }} {{ $t('layout.setting.darkMode') }}
</template> </template>
@ -12,10 +12,10 @@
{{ $t('layout.setting.lightMode') }} {{ $t('layout.setting.lightMode') }}
</template> </template>
<template #checked-icon> <template #checked-icon>
<div class="i-emojione:crescent-moon hover:cursor-pointer" /> <LIcon icon="emojione:crescent-moon" class="hover:cursor-pointer" />
</template> </template>
<template #unchecked-icon> <template #unchecked-icon>
<div class="i-emojione:sun-with-face hover:cursor-pointer" /> <LIcon icon="emojione:sun-with-face" class="hover:cursor-pointer" />
</template> </template>
</NSwitch> </NSwitch>
</NSpace> </NSpace>

@ -46,49 +46,73 @@ const some = triggerOptions.some(item => item.value === unref(getTrigger))
if (!some) { if (!some) {
setMenuSetting({ trigger: TriggerEnum.FOOTER }) setMenuSetting({ trigger: TriggerEnum.FOOTER })
} }
const { baseHandler } = useAppConfig()
function eventHandler(evt: HandlerSettingEnum, val: boolean) {
baseHandler(evt, val)
}
</script> </script>
<template> <template>
<NSpace vertical> <NSpace vertical>
<SwitchItem <SwitchItem
:title="$t('layout.setting.splitMenu')" :def="getSplit" :event="HandlerSettingEnum.MENU_SPLIT" :title="$t('layout.setting.splitMenu')"
:value="getSplit"
:callback="v => eventHandler(HandlerSettingEnum.MENU_SPLIT, v)"
:disabled="!getShowMenuRef || getMenuType !== NavBarModeEnum.MIX" :disabled="!getShowMenuRef || getMenuType !== NavBarModeEnum.MIX"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.mixSidebarFixed')" :def="getMixSideFixed" :title="$t('layout.setting.mixSidebarFixed')"
:event="HandlerSettingEnum.MENU_FIXED_MIX_SIDEBAR" :disabled="!getIsMixSidebar" :value="getMixSideFixed"
:callback="v => eventHandler(HandlerSettingEnum.MENU_FIXED_MIX_SIDEBAR, v)"
:disabled="!getIsMixSidebar"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.closeMixSidebarOnChange')" :def="getCloseMixSidebarOnChange" :title="$t('layout.setting.closeMixSidebarOnChange')"
:event="HandlerSettingEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE" :disabled="!getIsMixSidebar" :value="getCloseMixSidebarOnChange"
:callback="v => eventHandler(HandlerSettingEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE, v)"
:disabled="!getIsMixSidebar"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.menuCollapse')" :def="getCollapsed" :title="$t('layout.setting.menuCollapse')"
:event="HandlerSettingEnum.MENU_COLLAPSED" :disabled="!getShowMenuRef" :value="getCollapsed"
:callback="v => eventHandler(HandlerSettingEnum.MENU_COLLAPSED, v)"
:disabled="!getShowMenuRef"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.menuDrag')" :def="getCanDrag" :event="HandlerSettingEnum.MENU_HAS_DRAG" :title="$t('layout.setting.menuDrag')"
:value="getCanDrag"
:callback="v => eventHandler(HandlerSettingEnum.MENU_HAS_DRAG, v)"
:disabled="!getShowMenuRef" :disabled="!getShowMenuRef"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.menuSearch')" :def="getShowSearch" :event="HandlerSettingEnum.HEADER_SEARCH" :title="$t('layout.setting.menuSearch')"
:value="getShowSearch"
:callback="v => eventHandler(HandlerSettingEnum.HEADER_SEARCH, v)"
:disabled="!getShowHeader" :disabled="!getShowHeader"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.menuAccordion')" :def="getAccordion" :title="$t('layout.setting.menuAccordion')"
:event="HandlerSettingEnum.MENU_ACCORDION" :disabled="!getShowMenuRef" :value="getAccordion"
:callback="v => eventHandler(HandlerSettingEnum.MENU_ACCORDION, v)"
:disabled="!getShowMenuRef"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.collapseMenuDisplayName')" :def="getCollapsedShowTitle" :title="$t('layout.setting.collapseMenuDisplayName')"
:event="HandlerSettingEnum.MENU_COLLAPSED_SHOW_TITLE" :value="getCollapsedShowTitle"
:callback="v => eventHandler(HandlerSettingEnum.MENU_COLLAPSED_SHOW_TITLE, v)"
:disabled="!getShowMenuRef || !getCollapsed || getIsMixSidebar" :disabled="!getShowMenuRef || !getCollapsed || getIsMixSidebar"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.fixedHeader')" :def="getHeaderFixed" :event="HandlerSettingEnum.HEADER_FIXED" :title="$t('layout.setting.fixedHeader')"
:value="getHeaderFixed"
:callback="v => eventHandler(HandlerSettingEnum.HEADER_FIXED, v)"
:disabled="!getShowHeader" :disabled="!getShowHeader"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.fixedSideBar')" :def="getMenuFixed" :event="HandlerSettingEnum.MENU_FIXED" :title="$t('layout.setting.fixedSideBar')"
:value="getMenuFixed"
:callback="v => eventHandler(HandlerSettingEnum.MENU_FIXED, v)"
:disabled="!getShowMenuRef || getIsMixSidebar" :disabled="!getShowMenuRef || getIsMixSidebar"
/> />
<SelectItem <SelectItem

@ -10,19 +10,19 @@ const {
<NSpace vertical> <NSpace vertical>
<NButton type="info" block @click="copyConfigs"> <NButton type="info" block @click="copyConfigs">
<template #icon> <template #icon>
<div class="i-ant-design:snippets-twotone" /> <LIcon icon="ant-design:snippets-twotone" />
</template> </template>
{{ $t('layout.setting.copyBtn') }} {{ $t('button.copy') }}
</NButton> </NButton>
<NButton type="warning" block @click="resetAllConfig"> <NButton type="warning" block @click="resetAllConfig">
<template #icon> <template #icon>
<div class="i-ant-design:reload-outlined" /> <LIcon icon="ant-design:reload-outlined" />
</template> </template>
{{ $t('layout.setting.resetBtn') }} {{ $t('button.reset') }}
</NButton> </NButton>
<NButton type="error" block @click="clearAndRedo"> <NButton type="error" block @click="clearAndRedo">
<template #icon> <template #icon>
<div class="i-ant-design:redo-outlined" /> <LIcon icon="ant-design:redo-outlined" />
</template> </template>
{{ $t('layout.setting.clearBtn') }} {{ $t('layout.setting.clearBtn') }}
</NButton> </NButton>

@ -27,7 +27,7 @@ function handlerPicker(data: any) {
<template> <template>
<div class="setting-navigation-bar-picker"> <div class="setting-navigation-bar-picker">
<NSpace justify="space-between"> <NSpace justify="space-between">
<template v-for="(item, index) in props.typeList" :key="index"> <template v-for="(item, _index) in props.typeList" :key="_index">
<NTooltip placement="bottom" trigger="hover"> <NTooltip placement="bottom" trigger="hover">
<template #trigger> <template #trigger>
<div <div

@ -7,10 +7,12 @@ const { contentRef } = useLayout()
<template> <template>
<NAffix <NAffix
:listen-to="contentRef" :trigger-top="240" class="flex-center right-0 z-999 cursor-pointer border-rd-l bg-[#0960bd] p-10px text-white" :listen-to="contentRef"
:trigger-top="240"
class="right-0 z-999 flex-center cursor-pointer border-rd-l bg-[var(--primary-color)] p-10px text-white"
@click="visible = true" @click="visible = true"
> >
<div class="i-ion:settings-outline hover:cursor-pointer" /> <LIcon icon="ant-design:setting-outlined" class="hover:cursor-pointer" />
</NAffix> </NAffix>
<SettingDrawer $visible="visible" /> <SettingDrawer $visible="visible" />
</template> </template>

@ -5,8 +5,8 @@ const visible = $ref(false)
</script> </script>
<template> <template>
<div class="flex items-center"> <div class="flex items-center px-3!" @click="visible = true">
<div class="i-ion:settings-outline hover:cursor-pointer" @click="visible = true" /> <LIcon icon="ant-design:setting-outlined" class="hover:cursor-pointer" />
<SettingDrawer $visible="visible" /> <SettingDrawer $visible="visible" />
</div> </div>
</template> </template>

@ -2,71 +2,40 @@
import { navigationBarTypeList } from '../constant' import { navigationBarTypeList } from '../constant'
import DarkModeToggle from './dark-mode-toggle.vue' import DarkModeToggle from './dark-mode-toggle.vue'
import NavigationBarPicker from './navigation-bar-picker.vue' import NavigationBarPicker from './navigation-bar-picker.vue'
import ThemeColorPicker from './theme-color-picker.vue'
import Features from './features.vue' import Features from './features.vue'
import Content from './content.vue' import Content from './content.vue'
import Transitions from './transitions.vue' import Transitions from './transitions.vue'
import FooterButtons from './footer-buttons.vue' import FooterButtons from './footer-buttons.vue'
import { import ThemeEditor from './theme-editor.vue'
APP_PRESET_COLOR_LIST,
HEADER_PRESET_BG_COLOR_LIST, import { HandlerSettingEnum } from '~/constants'
HandlerSettingEnum,
SIDE_BAR_BG_COLOR_LIST,
} from '~/constants'
// eslint-disable-next-line unused-imports/no-unused-vars
const { visible } = defineModels<{ const { visible } = defineModels<{
visible: boolean visible: boolean
}>() }>()
const { getShowDarkModeToggle, getThemeColor } = useRootSetting() const { getShowDarkModeToggle } = useRootSetting()
const { baseHandler } = useAppConfig() const { baseHandler } = useAppConfig()
const { getIsHorizontal, getMenuType, getMenuBgColor } = useMenuSetting() const { getIsHorizontal, getMenuType } = useMenuSetting()
const { getHeaderBgColor } = useHeaderSetting()
</script> </script>
<template> <template>
<NDrawer $show="visible" :width="330"> <NDrawer v-model:show="visible" :width="350">
<NDrawerContent closable> <NDrawerContent closable>
<template #header> <template #header>
{{ $t('layout.setting.drawerTitle') }} {{ $t('layout.setting.drawerTitle') }}
</template> </template>
<NDivider title-placement="left" class="mt-0!">
{{ $t('layout.setting.theme') }}
</NDivider>
<template v-if="getShowDarkModeToggle"> <template v-if="getShowDarkModeToggle">
<DarkModeToggle /> <DarkModeToggle />
</template> </template>
<NH6 prefix="bar" :theme-overrides="{ headerMargin6: '12px 0 8px 0', headerFontSize6: '12px' }">
{{
$t('layout.setting.sysTheme') }}
</NH6>
<ThemeColorPicker
:def="getThemeColor" :event="HandlerSettingEnum.CHANGE_THEME_COLOR"
:color-list="APP_PRESET_COLOR_LIST"
/>
<NH6 prefix="bar" :theme-overrides="{ headerMargin6: '12px 0 8px 0', headerFontSize6: '12px' }">
{{ $t('layout.setting.headerTheme') }}
</NH6>
<ThemeColorPicker
:def="getHeaderBgColor" :event="HandlerSettingEnum.HEADER_THEME"
:color-list="HEADER_PRESET_BG_COLOR_LIST"
/>
<NH6 prefix="bar" :theme-overrides="{ headerMargin6: '12px 0 8px 0', headerFontSize6: '12px' }">
{{
$t('layout.setting.sidebarTheme') }}
</NH6>
<ThemeColorPicker
:def="getMenuBgColor" :event="HandlerSettingEnum.MENU_THEME"
:color-list="SIDE_BAR_BG_COLOR_LIST"
/>
<NDivider title-placement="left"> <NDivider title-placement="left">
{{ $t('layout.setting.navMode') }} {{ $t('layout.setting.navMode') }}
</NDivider> </NDivider>
<NavigationBarPicker <NavigationBarPicker
:def="getMenuType" :event="HandlerSettingEnum.CHANGE_LAYOUT" :def="getMenuType"
:type-list="navigationBarTypeList" @handler="(item) => { :event="HandlerSettingEnum.CHANGE_LAYOUT"
:type-list="navigationBarTypeList"
@handler="(item) => {
baseHandler(HandlerSettingEnum.CHANGE_LAYOUT, { baseHandler(HandlerSettingEnum.CHANGE_LAYOUT, {
mode: item.mode, mode: item.mode,
type: item.type, type: item.type,
@ -74,6 +43,10 @@ const { getHeaderBgColor } = useHeaderSetting()
}) })
}" }"
/> />
<NDivider title-placement="left">
{{ $t('layout.setting.themeEditor') }}
</NDivider>
<ThemeEditor />
<NDivider title-placement="left"> <NDivider title-placement="left">
{{ $t('layout.setting.interfaceFunction') }} {{ $t('layout.setting.interfaceFunction') }}
</NDivider> </NDivider>
@ -86,6 +59,7 @@ const { getHeaderBgColor } = useHeaderSetting()
{{ $t('layout.setting.animation') }} {{ $t('layout.setting.animation') }}
</NDivider> </NDivider>
<Transitions /> <Transitions />
<NDivider />
<FooterButtons /> <FooterButtons />
</NDrawerContent> </NDrawerContent>
</NDrawer> </NDrawer>

@ -1,13 +1,13 @@
<script setup lang="ts" name="SwitchItem"> <script setup lang="ts" name="SwitchItem">
import type { HandlerSettingEnum } from '~/constants' import { useThemeVars } from 'naive-ui'
const props = defineProps({ const props = defineProps({
title: { type: String, default: '' }, title: { type: String, default: '' },
def: { value: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
}, },
event: { callback: {
type: Number as PropType<HandlerSettingEnum>, type: Function as PropType<(val: boolean) => void>,
required: true, required: true,
}, },
disabled: { disabled: {
@ -15,23 +15,22 @@ const props = defineProps({
}, },
}) })
const { baseHandler } = useAppConfig() function onChange(value: boolean) {
props.callback(value)
function onChange(value: any) {
baseHandler(props.event, value)
} }
const themeVars = useThemeVars()
</script> </script>
<template> <template>
<div class="switch-item"> <div class="switch-item">
<NSpace justify="space-between" align="center"> <NSpace justify="space-between" align="center">
<span>{{ title }}</span> <span>{{ title }}</span>
<NSwitch :value="def" :disabled="disabled" @update:value="onChange"> <NSwitch :value="value" :disabled="disabled" @update:value="onChange">
<template #checked-icon> <template #checked-icon>
<div class="i-ant-design:check-outlined" color="#18A058" /> <LIcon icon="ant-design:check-outlined" :color="themeVars.primaryColor" />
</template> </template>
<template #unchecked-icon> <template #unchecked-icon>
<div class="i-ant-design:close-outlined" color="#BEBEBE" /> <LIcon icon="ant-design:close-outlined" color="#BEBEBE" />
</template> </template>
</NSwitch> </NSwitch>
</NSpace> </NSpace>

@ -1,52 +1,63 @@
<script setup lang="ts" name="ThemeColorPicker"> <script setup lang="ts" name="ThemeColorPicker">
import type { HandlerSettingEnum } from '~/constants' import { ThemeChangeEnum } from '~/constants'
const props = defineProps({ const props = defineProps({
colorList: { colorList: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
default: () => [], default: () => [],
}, },
title: { type: String, default: '' },
event: { event: {
type: Number as PropType<HandlerSettingEnum>, type: Number as PropType<ThemeChangeEnum>,
required: true, required: true,
}, },
disabled: {
type: Boolean,
default: false,
},
def: { def: {
type: String, type: String,
default: '', default: '',
}, },
}) })
const { baseHandler } = useAppConfig() const { setThemeConfig } = useAppTheme()
const color = $ref(props.def)
function handlerClick(color: any) { function onChange(color: string) {
baseHandler(props.event, color) switch (props.event) {
case ThemeChangeEnum.THEME_PRIMARY_COLOR_CHANGE:
setThemeConfig({ primaryColor: color })
break
case ThemeChangeEnum.THEME_INFO_COLOR_CHANGE:
setThemeConfig({ infoColor: color })
break
case ThemeChangeEnum.THEME_SUCCESS_COLOR_CHANGE:
setThemeConfig({ successColor: color })
break
case ThemeChangeEnum.THEME_WARNING_COLOR_CHANGE:
setThemeConfig({ warningColor: color })
break
case ThemeChangeEnum.THEME_ERROR_COLOR_CHANGE:
setThemeConfig({ errorColor: color })
break
}
} }
</script> </script>
<template> <template>
<div class="theme-color-picker"> <div class="min-w-83px">
<NSpace justify="space-between" :size="0" :wrap="false"> <NColorPicker
<template v-for="color in colorList" :key="color"> v-model:value="color"
<span :show-alpha="false"
class="color-item box-border inline-block h-20px w-20px cursor-pointer border border-gray-300 rounded-sm" :modes="['hex', 'rgb']"
:class="{ active: def === color }" :render-label="() => title"
:style="{ background: color }" @click="handlerClick(color)" :swatches="colorList"
> @complete="onChange"
<NSpace v-if="def === color" justify="center"> @update:value="onChange"
<div class="i-ant-design:check-outlined text-white hover:text-#d1d5db" /> />
</NSpace>
</span>
</template>
</NSpace>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.color-item {
&:hover,
&.active {
border-color: rgba(6, 96, 189, 1);
}
}
</style> </style>

@ -0,0 +1,58 @@
<script setup lang="ts">
import ThemeColorPicker from './theme-color-picker.vue'
import SwitchItem from './switch-item.vue'
import { APP_PRESET_COLOR_LIST, HEADER_PRESET_BG_COLOR_LIST, ThemeChangeEnum } from '~/constants'
const { getHeaderBgColor } = useHeaderSetting()
const {
primaryColor,
infoColor,
successColor,
warningColor,
errorColor,
isSidebarDark,
toggleSidebarTheme,
} = useAppTheme()
</script>
<template>
<NSpace justify="space-between" align="center">
<ThemeColorPicker
:title="$t('layout.setting.primaryColor')"
:def="primaryColor"
:event="ThemeChangeEnum.THEME_PRIMARY_COLOR_CHANGE"
:color-list="APP_PRESET_COLOR_LIST"
/>
<ThemeColorPicker
:title="$t('layout.setting.infoColor')"
:def="infoColor"
:event="ThemeChangeEnum.THEME_INFO_COLOR_CHANGE"
/>
<ThemeColorPicker
:title="$t('layout.setting.successColor')"
:def="successColor"
:event="ThemeChangeEnum.THEME_SUCCESS_COLOR_CHANGE"
/>
<ThemeColorPicker
:title="$t('layout.setting.warningColor')"
:def="warningColor"
:event="ThemeChangeEnum.THEME_WARNING_COLOR_CHANGE"
/>
<ThemeColorPicker
:title="$t('layout.setting.errorColor')"
:def="errorColor"
:event="ThemeChangeEnum.THEME_ERROR_COLOR_CHANGE"
/>
<ThemeColorPicker
:title="$t('layout.setting.headerTheme')"
:def="getHeaderBgColor"
:event="ThemeChangeEnum.THEME_HEADER_BG_COLOR_CHANGE"
:color-list="HEADER_PRESET_BG_COLOR_LIST"
/>
<SwitchItem
:title="$t('layout.setting.sidebarDark')"
:value="isSidebarDark"
:callback="toggleSidebarTheme"
/>
</NSpace>
</template>

@ -10,21 +10,30 @@ const {
getEnableTransition, getEnableTransition,
getOpenNProgress, getOpenNProgress,
} = useTransitionSetting() } = useTransitionSetting()
const { baseHandler } = useAppConfig()
function eventHandler(evt: HandlerSettingEnum, val: boolean) {
baseHandler(evt, val)
}
</script> </script>
<template> <template>
<NSpace vertical> <NSpace vertical>
<SwitchItem <SwitchItem
:title="$t('layout.setting.progress')" :def="getOpenNProgress" :title="$t('layout.setting.progress')"
:event="HandlerSettingEnum.OPEN_PROGRESS" :value="getOpenNProgress"
:callback="v => eventHandler(HandlerSettingEnum.OPEN_PROGRESS, v)"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.switchLoading')" :def="getOpenPageLoading" :title="$t('layout.setting.switchLoading')"
:value="getOpenPageLoading"
:callback="v => eventHandler(HandlerSettingEnum.OPEN_PAGE_LOADING, v)"
:event="HandlerSettingEnum.OPEN_PAGE_LOADING" :event="HandlerSettingEnum.OPEN_PAGE_LOADING"
/> />
<SwitchItem <SwitchItem
:title="$t('layout.setting.switchAnimation')" :def="getEnableTransition" :title="$t('layout.setting.switchAnimation')"
:event="HandlerSettingEnum.OPEN_ROUTE_TRANSITION" :value="getEnableTransition"
:callback="v => eventHandler(HandlerSettingEnum.OPEN_ROUTE_TRANSITION, v)"
/> />
<SelectItem <SelectItem
:title="$t('layout.setting.animationType')" :options="routerTransitionOptions" :def="getBasicTransition" :title="$t('layout.setting.animationType')" :options="routerTransitionOptions" :def="getBasicTransition"

@ -0,0 +1,86 @@
export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>, mix = false) {
const { getMiniWidthNumber, getCollapsed, setMenuSetting } = useMenuSetting()
function getEl(elRef: Ref<ElRef | ComponentRef>): any {
const el = unref(elRef)
if (!el) {
return null
}
if (Reflect.has(el, '$el')) {
return (unref(elRef) as ComponentRef)?.$el
}
return unref(elRef)
}
function handleMouseMove(ele: HTMLElement, wrap: HTMLElement, clientX: number) {
document.onmousemove = function (innerE) {
let iT = (ele as any).left + (innerE.clientX - clientX)
innerE = innerE || window.event
const maxT = 800
const minT = unref(getMiniWidthNumber)
iT < 0 && (iT = 0)
iT > maxT && (iT = maxT)
iT < minT && (iT = minT)
ele.style.left = wrap.style.width = `${iT}px`
return false
}
}
// Drag and drop in the menu area-release the mouse
function removeMouseup(ele: any) {
const wrap = getEl(siderRef)
document.onmouseup = function () {
document.onmousemove = null
document.onmouseup = null
wrap.style.transition = 'width 0.2s'
const width = Number.parseInt(wrap.style.width)
if (!mix) {
const miniWidth = unref(getMiniWidthNumber)
if (!unref(getCollapsed)) {
width > miniWidth + 20
? setMenuSetting({ menuWidth: width })
: setMenuSetting({ collapsed: true })
}
else {
width > miniWidth && setMenuSetting({ collapsed: false, menuWidth: width })
}
}
else {
setMenuSetting({ menuWidth: width })
}
ele.releaseCapture?.()
}
}
function changeWrapWidth() {
const ele = getEl(dragBarRef)
if (!ele) {
return
}
const wrap = getEl(siderRef)
if (!wrap) {
return
}
ele.onmousedown = (e: any) => {
wrap.style.transition = 'unset'
const clientX = e?.clientX
ele.left = ele.offsetLeft
handleMouseMove(ele, wrap, clientX)
removeMouseup(ele)
ele.setCapture?.()
return false
}
}
onMounted(() => {
nextTick(() => {
const exec = useDebounceFn(changeWrapWidth, 80)
exec()
})
})
return {}
}

@ -0,0 +1,116 @@
<script setup lang="ts">
// import { createNamespace } from '~/utils'
//
// fn sider
// width sider
// mix sider
const props = defineProps({
fn: {
type: Function,
},
width: {
type: Number,
},
mix: {
type: Number,
},
})
//
let canMove = $ref(false)
//
let pX = $ref(0)
//
const { x } = useMouse()
//
const { pressed } = useMousePressed()
//
function mouseDown(e: MouseEvent) {
canMove = true
pX = e.clientX
}
//
watch(() => x.value, () => {
if (!canMove) {
return
}
const t = x.value - pX
if (t === 0) {
return
}
const n = (props.width ?? 0) + t
if (n < (props.mix ?? 0)) {
return
}
if (props.fn) {
console.log('dragbarfn')
props.fn(n)
}
pX = x.value
})
//
watch(
() => pressed.value,
() => {
if (!pressed.value) {
canMove = false
}
},
)
// TODO
// const { bem } = createNamespace('drag-bar')
// const { getCanDrag, getCollapsed, getMiniWidthNumber } = useMenuSetting()
// const getClass = computed(() => {
// return [
// bem(),
// {
// [bem('hide') as string]: !unref(getCanDrag),
// },
// ]
// })
// const getStyle = computed(() => {
// if (unref(getCollapsed)) {
// return { left: `${unref(getMiniWidthNumber)}px` }
// }
// return {}
// })
</script>
<template>
<!-- v1 -->
<div
class="dragbar absolute right-0 h-full w-1 hover:cursor-col-resize"
@mousedown="mouseDown"
/>
<!-- v2 -->
<!-- <div :class="getClass" :style="getStyle" /> -->
</template>
<style lang="less" scoped>
// .drag-bar {
// position: absolute;
// z-index: 999;
// top: 0;
// right: -2px;
// width: 2px;
// height: 100%;
// border-top: none;
// border-bottom: none;
// cursor: col-resize;
// &__hide {
// display: none;
// }
// &:hover {
// background-color: var(--primary-color);
// box-shadow: 0 0 4px 0 rgb(28 36 56 / 15%);
// }
// }
</style>

@ -0,0 +1,90 @@
<script setup lang="ts">
import type { RouteLocationNormalized } from 'vue-router'
import { TabActionEnum } from '~/constants'
const { t } = useI18n()
const x = ref(0)
const y = ref(0)
let targetTabRef = $ref<RouteLocationNormalized>()
let showDropdownRef = $ref(false)
const tabStore = useMultipleTabStore()
const { renderIcon } = useIcon()
const optionsRef = computed(() => {
const tab = targetTabRef
if (!tab)
return []
return (
tabStore
.getTabActions(tab)
// tab
?.filter(v => !(v.key === 0 && v.disabled))
//
.map((v) => {
const label = v.label && t(v.label)
return { ...v, label, icon: () => renderIcon({ icon: v.icon }) }
})
)
})
function openDropdown(e: MouseEvent, tabItem: RouteLocationNormalized) {
targetTabRef = tabItem
showDropdownRef = false
nextTick().then(() => {
showDropdownRef = true
x.value = e.clientX
y.value = e.clientY
})
}
defineExpose({ openDropdown })
const {
refreshPage,
close,
closeAll,
closeLeft,
closeRight,
closeOther,
} = useTabs()
async function handleSelect(key: TabActionEnum) {
const tab = unref(targetTabRef)
switch (key) {
case TabActionEnum.REFRESH_PAGE:
await refreshPage()
break
case TabActionEnum.CLOSE_CURRENT:
await close(tab)
break
case TabActionEnum.CLOSE_ALL:
await closeAll()
break
case TabActionEnum.CLOSE_LEFT:
await closeLeft(tab)
break
case TabActionEnum.CLOSE_RIGHT:
await closeRight(tab)
break
case TabActionEnum.CLOSE_OTHER:
await closeOther(tab)
break
}
}
</script>
<template>
<NDropdown
placement="bottom-start"
trigger="manual"
:show-arrow="true"
:x="x"
:y="y"
:options="optionsRef"
$show="showDropdownRef"
@clickoutside="showDropdownRef = false"
@select="handleSelect"
/>
</template>

@ -0,0 +1,28 @@
<script setup lang="ts">
import { triggerWindowResize } from '~/utils'
const { getShowMenu, setMenuSetting } = useMenuSetting()
const { getShowHeader, setHeaderSetting } = useHeaderSetting()
const getIsUnFoldRef = computed(() => !unref(getShowMenu) && !unref(getShowHeader))
const getIconRef = computed(() => unref(getIsUnFoldRef) ? 'codicon:screen-normal' : 'codicon:screen-full')
function handleFold() {
//
const show = unref(getIsUnFoldRef)
setMenuSetting({ show })
setHeaderSetting({ show })
triggerWindowResize()
}
</script>
<template>
<div
class="h-full w-32px flex-center cursor-pointer border-l border-[var(--n-border-color)]"
@click="handleFold"
>
<LIcon :icon="getIconRef" />
</div>
</template>

@ -0,0 +1,70 @@
<script setup lang="ts">
import type { PropType } from 'vue'
import type { RouteLocationNormalized } from 'vue-router'
import { TabActionEnum } from '~/constants'
const props = defineProps({
tabItem: {
type: Object as PropType<RouteLocationNormalized>,
default: null,
},
})
const { t } = useI18n()
const tabStore = useMultipleTabStore()
const { renderIcon } = useIcon()
const optionsRef = computed(() => {
return tabStore.getTabActions(props.tabItem)
?.map((v) => {
const label = v.label && t(v.label)
return { ...v, label, icon: () => renderIcon({ icon: v.icon }) }
})
})
const {
refreshPage,
close,
closeAll,
closeLeft,
closeRight,
closeOther,
} = useTabs()
async function handleSelect(key: TabActionEnum) {
switch (key) {
case TabActionEnum.REFRESH_PAGE:
await refreshPage()
break
case TabActionEnum.CLOSE_CURRENT:
await close(props.tabItem)
break
case TabActionEnum.CLOSE_ALL:
await closeAll()
break
case TabActionEnum.CLOSE_LEFT:
await closeLeft()
break
case TabActionEnum.CLOSE_RIGHT:
await closeRight()
break
case TabActionEnum.CLOSE_OTHER:
await closeOther()
break
}
}
</script>
<template>
<NDropdown
placement="bottom-start"
trigger="click"
:show-arrow="true"
:options="optionsRef"
@select="handleSelect"
>
<div
class="h-full w-32px flex-center cursor-pointer border-l border-[var(--n-border-color)]"
>
<LIcon icon="ant-design:down-outlined" />
</div>
</NDropdown>
</template>

@ -0,0 +1,24 @@
<script setup lang="ts">
const { refreshPage } = useTabs()
function reload() {
return new Promise((resolve) => {
refreshPage().then(() => {
setTimeout(() => {
resolve({})
}, 300)
})
})
}
const { loading, handleFn: handleRedo } = usePromise(reload, { immediate: false })
</script>
<template>
<div
class="h-full w-32px flex-center cursor-pointer border-l border-[var(--n-border-color)]"
@click="handleRedo"
>
<LIcon :spin="loading" icon="ant-design:reload-outlined" />
</div>
</template>

@ -0,0 +1,5 @@
import type { RouteLocationNormalized } from 'vue-router'
export interface TabDropDownInst {
openDropdown: (e: MouseEvent, tabItem: RouteLocationNormalized) => void
}

@ -0,0 +1,114 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import type { RouteLocationNormalized } from 'vue-router'
import TabRedo from './components/tab-redo.vue'
import TabQuick from './components/tab-quick.vue'
import TabFoldButton from './components/tab-foldbutton.vue'
import TabDropDown from './components/tab-dropdown.vue'
import type { TabDropDownInst } from './components/types'
import { REDIRECT_NAME } from '~/constants'
import { Sortable, listenerRouteChange } from '~/utils'
const router = useRouter()
const go = useGo()
const { close } = useTabs()
const multipleTabStore = useMultipleTabStore()
const { tabList } = storeToRefs(multipleTabStore)
const { getShowFold, getShowQuick, getShowRedo } = useMultipleTabSetting()
let activeTabName = $ref<string>('')
const tabDropdownRef = $ref<TabDropDownInst | null>(null)
const tabListRef = computed(() => {
return unref(tabList).filter(item => !item.meta?.hideInTab && router.hasRoute(item.name!))
})
listenerRouteChange((route) => {
const { name } = route
// TODO accessToken
if (name === REDIRECT_NAME || !route) {
return
}
const { path, fullPath, meta = {} } = route
const { currentActiveMenu, hideInTab } = meta
const isHide = !hideInTab ? null : currentActiveMenu
const p = isHide || fullPath || path
if (activeTabName !== p) {
activeTabName = p as string
}
if (isHide) {
const findParentRoute = router.getRoutes().find(item => item.path === currentActiveMenu)
findParentRoute && multipleTabStore.checkTab(findParentRoute as unknown as RouteLocationNormalized)
}
else {
multipleTabStore.checkTab(unref(route))
}
})
function handleChange(value: string) {
go(value, false)
}
// tabsdom
nextTick(() => {
const selection = document.querySelector(
'#drag > div > div > div > div > div.n-tabs-wrapper',
)
Sortable.create(selection as HTMLElement)
})
function handleContextMenu(e: MouseEvent, tabItem: RouteLocationNormalized) {
e.preventDefault()
if (!tabItem) {
return
}
unref(tabDropdownRef)?.openDropdown(e, tabItem)
}
function handleClose(e: MouseEvent, route: RouteLocationNormalized) {
e.stopPropagation()
close(route)
}
</script>
<template>
<div>
<NTabs
id="drag"
v-model:value="activeTabName"
type="card"
:tabs-padding="8"
animated
@update:value="handleChange"
>
<NTab
v-for="(item, index) in tabListRef"
:key="item.query ? item.fullPath : item.path"
:name="item.fullPath"
style="--n-tab-padding: 0"
>
<div
class="group py-4px pl-12px hover:text-[var(--n-tab-text-color-active)]"
:class="[index === 0 ? 'pr-12px' : 'pr-18px']"
@contextmenu="handleContextMenu($event, item)"
>
<span>{{ $t(item.meta.title || '') }}</span>
<LIcon
v-if="index !== 0"
icon="ant-design:close-outlined"
class="hover:nt--7px absolute top-1/2 ml-2px mt--6px inline-flex !transition-all hover:!text-14px"
:class="{ ['!hidden']: activeTabName !== item.fullPath }"
size="14"
@click="handleClose($event, item)"
/>
</div>
</NTab>
<template #suffix>
<TabRedo v-if="getShowRedo" />
<TabQuick v-if="getShowQuick" :tab-item="$route" />
<TabFoldButton v-if="getShowFold" />
</template>
</NTabs>
<TabDropDown ref="tabDropdownRef" />
</div>
</template>

@ -7,7 +7,7 @@ const { bem } = createNamespace('header-trigger')
<template> <template>
<div :class="bem()" class="hover:cursor-pointer" @click="toggleCollapsed"> <div :class="bem()" class="hover:cursor-pointer" @click="toggleCollapsed">
<div v-if="getCollapsed" class="i-ant-design-menu-unfold-outlined" /> <LIcon v-if="getCollapsed" icon="ant-design:menu-unfold-outlined" />
<div v-else class="i-ant-design-menu-fold-outlined" /> <LIcon v-else icon="ant-design:menu-fold-outlined" />
</div> </div>
</template> </template>

@ -1,66 +0,0 @@
<script setup lang="ts">
//
// fn sider
// width sider
// mix sider
const props = defineProps({
fn: {
type: Function,
},
width: {
type: Number,
},
mix: {
type: Number,
},
})
//
let canMove = $ref(false)
//
let pX = $ref(0)
//
const { x } = useMouse()
//
const { pressed } = useMousePressed()
// //
// function mouseDown(e: MouseEvent) {
// canMove = true
// pX = e.clientX
// }
//
watch(() => x.value, () => {
if (!canMove) {
return
}
const t = x.value - pX
if (t === 0) {
return
}
const n = props.width ?? 0 + t
if (n < (props.mix ?? 0)) {
return
}
if (props.fn) {
props.fn(n)
}
pX = x.value
})
//
watch(
() => pressed.value,
() => {
if (!pressed.value) {
canMove = false
}
},
)
// TODO
</script>
<template>
<div class="dragbar absolute right-0 h-full w-1 hover:cursor-col-resize" />
</template>
<style lang="less" scoped></style>

@ -7,7 +7,7 @@ const { bem } = createNamespace('side-trigger')
<template> <template>
<div :class="bem()" @click.stop="toggleCollapsed"> <div :class="bem()" @click.stop="toggleCollapsed">
<i v-if="getCollapsed" i-ph-caret-double-right-bold /> <LIcon v-if="getCollapsed" icon="ant-design:double-right-outlined" />
<i v-else i-ph-caret-double-left-bold /> <LIcon v-else icon="ant-design:double-left-outlined" />
</div> </div>
</template> </template>

@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import LeftMenu from './left-menu.vue' import LeftMenuLayout from './left-menu.vue'
import MixMenuLayout from './mix-menu.vue'
import TopMenuLayout from './top-menu.vue'
import TopMenuMixLayout from './top-menu-mixed.vue'
import { NavBarModeEnum } from '~/constants' import { NavBarModeEnum } from '~/constants'
// TODO lockScreen // TODO lockScreen
@ -9,14 +12,21 @@ const layout = computed<ReturnType<typeof defineComponent>>(() => {
// TODO appInject mobile // TODO appInject mobile
switch (getMenuType.value) { switch (getMenuType.value) {
case NavBarModeEnum.SIDEBAR: case NavBarModeEnum.SIDEBAR:
return LeftMenu return LeftMenuLayout
case NavBarModeEnum.MIX: case NavBarModeEnum.MIX:
return TopMenuMixLayout
case NavBarModeEnum.TOP_MENU: case NavBarModeEnum.TOP_MENU:
return TopMenuLayout
case NavBarModeEnum.MIX_SIDEBAR: case NavBarModeEnum.MIX_SIDEBAR:
return MixMenuLayout
default: default:
return LeftMenu return undefined
} }
}) })
nextTick(() => {
createThemeColorListen()
})
</script> </script>
<template> <template>

@ -3,7 +3,9 @@ import LayoutMenu from './components/menu/index.vue'
import LayoutHeader from './components/header.vue' import LayoutHeader from './components/header.vue'
import LayoutFooter from './components/footer.vue' import LayoutFooter from './components/footer.vue'
import LayoutMain from './components/main.vue' import LayoutMain from './components/main.vue'
import SiderDragBar from './components/trigger/sider-dragbar.vue' import SiderDragBar from './components/sider/sider-dragbar.vue'
// import { useDragLine } from './components/sider/drag'
defineOptions({ defineOptions({
name: 'LeftMenuLayout', name: 'LeftMenuLayout',
@ -13,6 +15,7 @@ const {
getCollapsed, getCollapsed,
getShowCenterTrigger, getShowCenterTrigger,
setSiderWidth, setSiderWidth,
getCanDrag,
} = useMenuSetting() } = useMenuSetting()
const { const {
@ -26,20 +29,35 @@ const {
const { const {
toggleCollapse, toggleCollapse,
sidebar, sidebar,
footer,
} = useAppConfig() } = useAppConfig()
const { getShowFooter } = useRootSetting()
const { isSidebarDark } = useAppTheme()
// useDragLine(siderRef, dragBarRef)
</script> </script>
<template> <template>
<NLayout has-sider class="h-full"> <NLayout has-sider class="h-full">
<NLayoutSider <NLayoutSider
v-if="getShowMenu" :show-trigger="getShowCenterTrigger" bordered v-if="getShowMenu"
:collapsed-width="sidebar.collapsedWidth" :width="sidebar.width" collapse-mode="width" :collapsed="getCollapsed" :show-trigger="getShowCenterTrigger"
bordered
:collapsed-width="sidebar.collapsedWidth"
:width="sidebar.width"
collapse-mode="width"
:collapsed="getCollapsed"
:inverted="!!isSidebarDark"
@update:collapsed="toggleCollapse" @update:collapsed="toggleCollapse"
> >
<slot name="sider"> <slot name="sider">
<div class="static h-full"> <div class="static h-full">
<SiderDragBar :mix="sidebar.mixSidebarWidth" :width="sidebar.width" :fn="setSiderWidth" /> <SiderDragBar
v-if="getCanDrag"
:mix="sidebar.mixSidebarWidth"
:width="sidebar.width"
:fn="setSiderWidth"
/>
<LayoutMenu /> <LayoutMenu />
</div> </div>
</slot> </slot>
@ -56,7 +74,7 @@ const {
<slot name="main" /> <slot name="main" />
</LayoutMain> </LayoutMain>
</NLayoutContent> </NLayoutContent>
<NLayoutFooter v-if="footer.show" ref="footerRef"> <NLayoutFooter v-if="getShowFooter" ref="footerRef">
<slot name="footer"> <slot name="footer">
<LayoutFooter /> <LayoutFooter />
</slot> </slot>

@ -0,0 +1,87 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import LayoutMixMenu from './components/menu/mix-menu.vue'
import LayoutHeader from './components/header.vue'
import LayoutFooter from './components/footer.vue'
import LayoutMain from './components/main.vue'
import { createNamespace } from '~/utils'
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '~/constants'
defineOptions({
name: 'MixMenuLayout',
})
const {
headerRef,
contentStyle,
mainStyle,
footerRef,
contentRef,
} = useLayout()
const {
getCollapsed,
getMenuWidth,
getIsFixed,
getShowSidebar,
} = useMenuSetting()
const { getShowFooter } = useRootSetting()
const getMixSidebarWidth = computed(() => {
return unref(getCollapsed)
? SIDE_BAR_MINI_WIDTH
: SIDE_BAR_SHOW_TIT_MINI_WIDTH
})
const getContainerStyle = computed((): CSSProperties => {
return {
paddingLeft: `${unref(getIsFixed) ? unref(getMenuWidth) : 0}px`,
}
})
const { bem } = createNamespace('layout-mix-sidebar')
const { isSidebarDark } = useAppTheme()
</script>
<template>
<NLayout has-sider class="h-full">
<NLayoutSider
v-if="getShowSidebar"
bordered
:collapsed-width="getMixSidebarWidth"
collapse-mode="width"
:collapsed="true"
:class="bem()"
:inverted="isSidebarDark"
>
<slot name="sider">
<LayoutMixMenu :mix-sidebar-width="getMixSidebarWidth" />
</slot>
</NLayoutSider>
<NLayout :style="getContainerStyle" class="transition-all">
<NLayoutHeader ref="headerRef">
<slot name="header">
<LayoutHeader />
</slot>
</NLayoutHeader>
<NLayout :content-style="contentStyle">
<NLayoutContent ref="contentRef" :content-style="mainStyle">
<LayoutMain>
<slot name="main" />
</LayoutMain>
</NLayoutContent>
<NLayoutFooter v-if="getShowFooter" ref="footerRef">
<slot name="footer">
<LayoutFooter />
</slot>
</NLayoutFooter>
</NLayout>
</NLayout>
</NLayout>
</template>
<style lang="less" scoped>
.layout-mix-sidebar {
z-index: var(--mix-sider-z-index);
}
</style>

@ -0,0 +1,83 @@
<script lang="ts" setup>
import { computed, unref } from 'vue'
import LayoutMenu from './components/menu/index.vue'
import LayoutHeader from './components/header.vue'
import LayoutTabs from './components/tabs/index.vue'
import LayoutMain from './components/main.vue'
import LayoutFooter from './components/footer.vue'
const {
toggleCollapsed,
getCollapsed,
getMenuWidth,
getShowSidebar,
getShowCenterTrigger,
} = useMenuSetting()
const { getShowFooter } = useRootSetting()
const { getShowMultipleTab } = useMultipleTabSetting()
const {
headerRef,
tabRef,
footerRef,
headerHeight,
contentStyle,
mainStyle,
contentRef,
} = useLayout()
const menuHeight = computed(() => `calc(100vh - ${unref(headerHeight)}px)`)
const { isSidebarDark } = useAppTheme()
</script>
<template>
<NLayout class="h-full">
<NLayoutHeader ref="headerRef">
<slot name="header">
<LayoutHeader>
<template #menu>
<LayoutMenu mode="horizontal" />
</template>
</LayoutHeader>
</slot>
</NLayoutHeader>
<NLayout has-sider :style="{ height: menuHeight }">
<NLayoutSider
v-if="getShowSidebar"
:show-trigger="getShowCenterTrigger"
bordered
:collapsed-width="48"
:width="getMenuWidth"
collapse-mode="width"
:collapsed="getCollapsed"
:inverted="isSidebarDark"
@update:collapsed="toggleCollapsed"
>
<slot name="sider">
<LayoutMenu split />
</slot>
</NLayoutSider>
<NLayout>
<NLayoutHeader v-if="getShowMultipleTab">
<slot name="tabs">
<LayoutTabs ref="tabRef" />
</slot>
</NLayoutHeader>
<NLayout :content-style="contentStyle">
<NLayoutContent ref="contentRef" :content-style="mainStyle">
<LayoutMain>
<slot name="main" />
</LayoutMain>
</NLayoutContent>
<NLayoutFooter v-if="getShowFooter" ref="footerRef">
<slot name="footer">
<LayoutFooter />
</slot>
</NLayoutFooter>
</NLayout>
</NLayout>
</NLayout>
</NLayout>
</template>

@ -0,0 +1,48 @@
<script setup lang="ts">
import LayoutMenu from './components/menu/index.vue'
import LayoutHeader from './components/header.vue'
import LayoutFooter from './components/footer.vue'
import LayoutMain from './components/main.vue'
// import { useDragLine } from './components/sider/drag'
defineOptions({
name: 'TopMenuLayout',
})
const {
headerRef,
footerRef,
contentRef,
contentStyle,
mainStyle,
} = useLayout()
const { getShowFooter } = useRootSetting()
</script>
<template>
<NLayout class="h-full">
<NLayoutHeader ref="headerRef">
<slot name="header">
<LayoutHeader>
<template #menu>
<LayoutMenu mode="horizontal" />
</template>
</LayoutHeader>
</slot>
</NLayoutHeader>
<NLayout :content-style="contentStyle">
<NLayoutContent ref="contentRef" :content-style="mainStyle">
<LayoutMain>
<slot name="main" />
</LayoutMain>
</NLayoutContent>
<NLayoutFooter v-if="getShowFooter" ref="footerRef">
<slot name="footer">
<LayoutFooter />
</slot>
</NLayoutFooter>
</NLayout>
</NLayout>
</template>

@ -0,0 +1,117 @@
<script setup lang="ts" name="ExceptionPage">
import { BASIC_LOGIN_PATH, ExceptionEnum } from '~/constants'
import { createNamespace } from '~/utils'
interface MapValue {
title: string
subTitle: string
btnText?: string
icon?: string
handler?: AnyFunction<any>
status?: string
}
const props = defineProps({
//
status: {
type: Number as PropType<number>,
default: ExceptionEnum.PAGE_NOT_FOUND,
},
title: {
type: String as PropType<string>,
default: '',
},
subTitle: {
type: String as PropType<string>,
default: '',
},
full: {
type: Boolean as PropType<boolean>,
default: false,
},
})
const statusMapRef = $ref<Map<string | number, MapValue>>(new Map<string | number, MapValue>())
const { t } = useI18n()
const { query } = useRoute()
const go = useGo()
const redo = useRedo()
const { bem } = createNamespace('exception-page')
const getStatus = computed(() => {
const { status: routeStatus } = query
const { status } = props
return Number(routeStatus) || status
})
const getMapValue = computed((): MapValue => statusMapRef.get(unref(getStatus)) as MapValue)
const backLoginI18n = t('sys.exception.backLogin')
const backHomeI18n = t('sys.exception.backHome')
statusMapRef.set(ExceptionEnum.PAGE_NOT_ACCESS, {
title: '403',
status: `${ExceptionEnum.PAGE_NOT_ACCESS}`,
subTitle: t('sys.exception.subTitle403'),
btnText: props.full ? backLoginI18n : backHomeI18n,
handler: () => (props.full ? go(BASIC_LOGIN_PATH) : go()),
})
statusMapRef.set(ExceptionEnum.PAGE_NOT_FOUND, {
title: '404',
status: `${ExceptionEnum.PAGE_NOT_FOUND}`,
subTitle: t('sys.exception.subTitle404'),
btnText: props.full ? backLoginI18n : backHomeI18n,
handler: () => (props.full ? go(BASIC_LOGIN_PATH) : go()),
})
statusMapRef.set(ExceptionEnum.ERROR, {
title: '500',
status: `${ExceptionEnum.ERROR}`,
subTitle: t('sys.exception.subTitle500'),
btnText: backHomeI18n,
handler: () => go(),
})
statusMapRef.set(ExceptionEnum.PAGE_NOT_DATA, {
title: t('sys.exception.noDataTitle'),
subTitle: '',
btnText: t('common.redo'),
handler: () => redo(),
icon: 'nl-no-data',
})
statusMapRef.set(ExceptionEnum.NET_WORK_ERROR, {
title: t('sys.exception.networkErrorTitle'),
subTitle: t('sys.exception.networkErrorSubTitle'),
btnText: t('common.redo'),
handler: () => redo(),
icon: 'nl-net-error',
})
</script>
<template>
<NResult
:class="bem()" class="m-4" :status="`${getStatus}` as any" :title="props.title || getMapValue.title"
:description="props.subTitle || getMapValue.subTitle"
>
<template v-if="getMapValue.btnText" #footer>
<NButton type="primary" @click="getMapValue.handler">
{{ getMapValue.btnText }}
</NButton>
</template>
<template v-if="getMapValue.icon" #icon>
<LIcon size="400" :icon="getMapValue.icon" />
</template>
</NResult>
</template>
<style lang="less" scoped>
.exception-page {
display: flex;
align-items: center;
flex-direction: column;
}
</style>

@ -0,0 +1,6 @@
<script setup lang="ts" name="IFrame">
</script>
<template>
<div />
</template>

@ -0,0 +1,28 @@
<script setup lang="ts" name="Redirect">
const { currentRoute, replace } = useRouter()
const { params, query } = unref(currentRoute)
const { path, _redirect_type = 'path' } = params
Reflect.deleteProperty(params, '_redirect_type')
Reflect.deleteProperty(params, 'path')
const _path = Array.isArray(path) ? path.join('/') : path
if (_redirect_type === 'name') {
replace({
name: _path,
query,
params,
})
}
else {
replace({
path: _path.startsWith('/') ? _path : `/${_path}`,
query,
params,
})
}
</script>
<template>
<div />
</template>

@ -1,12 +1,14 @@
import { createHead } from '@unhead/vue'
import App from './App.vue' import App from './App.vue'
import AppLoading from './AppLoading.vue'
import '@unocss/reset/tailwind.css' import './styles'
import './styles/main.css'
import 'uno.css'
import { setupPinia } from './modules/pinia' import { setupPinia } from './modules/pinia'
import { setupI18n } from './modules/i18n' import { setupI18n } from './modules/i18n'
import { initRouter, setupRouterGuards } from './modules/router/router' import { initRouter, setupRouterGuards } from './modules/router/router'
import { setupPWA } from './modules/pwa' import { setupPWA } from './modules/pwa'
import 'virtual:svg-icons-register'
let meta = document.createElement('meta') let meta = document.createElement('meta')
meta.name = 'naive-ui-style' meta.name = 'naive-ui-style'
@ -17,16 +19,24 @@ meta.name = 'vueuc-style'
document.head.appendChild(meta) document.head.appendChild(meta)
; (async () => { ; (async () => {
// TODO loadingApp
const appLoading = createApp(AppLoading)
// setupPinia(appLoading)
// setup I18n
// await setupI18n(appLoading)
appLoading.mount('#appLoading')
const app = createApp(App) const app = createApp(App)
setupPinia(app) setupPinia(app)
// initApplication // initApplication
app.use(createHead())
// setup I18n // setup I18n
await setupI18n(app) await setupI18n(app)
// Router // Router
const router = initRouter(import.meta.env.VITE_BASE_URL) const router = await initRouter(import.meta.env.VITE_BASE_URL)
app.use(router) app.use(router)
// Router guards // Router guards
await setupRouterGuards() await setupRouterGuards()

@ -1,50 +0,0 @@
import type { App } from 'vue'
import type { Locale } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
// Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import
//
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
export const i18n = createI18n({
legacy: false,
locale: '',
messages: {},
})
const localesMap = Object.fromEntries(
Object.entries(import.meta.glob('../../locales/*.yml'))
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
export const availableLocales = Object.keys(localesMap)
const loadedLanguages: string[] = []
function setI18nLanguage(lang: Locale) {
i18n.global.locale.value = lang as any
if (typeof document !== 'undefined')
document.querySelector('html')?.setAttribute('lang', lang)
return lang
}
export async function loadLanguageAsync(lang: string): Promise<Locale> {
// If the same language
if (i18n.global.locale.value === lang)
return setI18nLanguage(lang)
// If the language was already loaded
if (loadedLanguages.includes(lang))
return setI18nLanguage(lang)
// If the language hasn't been loaded yet
const messages = await localesMap[lang]()
i18n.global.setLocaleMessage(lang, messages.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
}
export async function setupI18n(app: App) {
app.use(i18n)
await loadLanguageAsync('zh-CN')
}

@ -0,0 +1,74 @@
import type { Locale } from 'vue-i18n'
export const LOCALE: { [key: string]: Locale } = {
ZH_CHS: 'zh-chs', // 简体中文
ZH_CHT: 'zh-cht', // 繁体中文
AR: 'ar', // 阿拉伯语
BG: 'bg', // 保加利亚语
HR: 'hr', // 克罗地亚语
CS: 'cs', // 捷克语
DA: 'da', // 丹麦语言
DE: 'de', // 德语
EL: 'el', // 希腊语
EN: 'en', // 英语
ET: 'et', // 爱沙尼亚语
ES: 'es', // 西班牙语
FI: 'fi', // 芬兰语
FR: 'fr', // 法语
GA: 'ga', // 爱尔兰语
HI: 'hi', // 印地语
HU: 'hu', // 匈牙利语
HE: 'he', // 希伯来语
IT: 'it', // 意大利语
JA: 'ja', // 日语
KO: 'ko', // 朝鲜语
LV: 'lv', // 拉脱维亚语
LT: 'lt', // 立陶宛语
NL: 'nl', // 荷兰语
NO: 'no', // 挪威语
PL: 'pl', // 波兰语
PT: 'pt', // 葡萄牙语
SV: 'sv', // 瑞典语
RO: 'ro', // 罗马尼亚语
RU: 'ru', // 俄语
SR_CS: 'sr-cs', // 塞尔维亚语
SK: 'sk', // 斯洛伐克语
SL: 'sl', // 斯洛文尼亚语
TH: 'th', // 泰语
TR: 'tr', // 土耳其语
UK_UA: 'uk-ua', // 乌克兰语
}
export const localesMap = Object.fromEntries(
Object.entries(import.meta.glob('../../../locales/*.yml'))
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
export const localeSetting: LocaleConfig = {
locale: LOCALE.ZH_CHS,
fallback: LOCALE.ZH_CHS,
availableLocales: Object.keys(localesMap),
}
export const localeList: any[] = [
{
text: '简体中文',
event: LOCALE.ZH_CHS,
icon: 'emojione:flag-for-china',
},
// {
// text: '繁体中文',
// event: LOCALE.ZH_CHT,
// icon: 'emojione:flag-for-hong-kong-sar-china',
// },
{
text: 'English',
event: LOCALE.EN,
icon: 'emojione:flag-for-united-states',
},
{
text: '日本語',
event: LOCALE.JA,
icon: 'emojione:flag-for-japan',
},
]

@ -0,0 +1,65 @@
import type { App } from 'vue'
import type { I18nOptions, Locale } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { localesMap } from './config'
const loadedLocalePool: string[] = []
// eslint-disable-next-line import/no-mutable-exports
export let i18n: ReturnType<typeof createI18n>
async function createI18nOptions(): Promise<I18nOptions> {
// saved locale
const localeStore = useLocaleStore()
const { fallback, availableLocales, getLocale } = storeToRefs(localeStore)
const locale = getLocale.value
return {
legacy: false,
locale,
fallbackLocale: unref(fallback),
messages: {},
availableLocales: unref(availableLocales),
sync: true,
silentTranslationWarn: false,
missingWarn: false,
silentFallbackWarn: true,
}
}
export function setI18nLanguage(lang: Locale) {
i18n.global.locale = lang
if (typeof document !== 'undefined') {
document.querySelector('html')?.setAttribute('lang', lang)
}
return lang
}
export async function loadLanguageAsync(lang: string): Promise<Locale> {
// If the same language
if (i18n.global.locale === lang) {
return setI18nLanguage(lang)
}
// If the language was already loaded
if (loadedLocalePool.includes(lang)) {
return setI18nLanguage(lang)
}
// If the language hasn't been loaded yet
const messages = await localesMap[lang]()
i18n.global.setLocaleMessage(lang, messages.default)
loadedLocalePool.push(lang)
return setI18nLanguage(lang)
}
export async function setupI18n(app: App) {
const options = await createI18nOptions()
i18n = createI18n(options)
await loadLanguageAsync(options.locale!)
app.use(i18n)
}

@ -1,9 +1,13 @@
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate' import { createPersistedState } from 'pinia-plugin-persistedstate'
import type { App } from 'vue' import type { App } from 'vue'
import { resetSetupStorePlugin } from './reset'
const pinia = createPinia() const pinia = createPinia()
// setup-syntax $reset plugin
pinia.use(resetSetupStorePlugin)
// 持久化插件(localStorage) // 持久化插件(localStorage)
pinia.use(createPersistedState({ pinia.use(createPersistedState({
storage: localStorage, storage: localStorage,

@ -0,0 +1,15 @@
import type { PiniaPluginContext } from 'pinia'
import { cloneDeep } from '~/utils'
/**
* setup
*/
export function resetSetupStorePlugin(context: PiniaPluginContext) {
const initialState = cloneDeep(context.store.$state)
context.store.$reset = () => {
context.store.$patch(($state) => {
Object.assign($state, initialState)
})
}
}

@ -10,12 +10,16 @@ import { setRouteChange } from '~/utils'
export let router: Router export let router: Router
export function initRouter(path: string): Router { export function initRouter(path: string): Router {
const routeStore = useRouteStore()
const routes = routeStore.initRoutes()
router = createRouter({ router = createRouter({
history: createWebHistory(path), history: createWebHistory(path),
routes: setupLayouts(getRoutes()),
strict: false, strict: false,
routes: setupLayouts(routes),
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
}) })
return router return router
} }
@ -23,5 +27,5 @@ export function setupRouterGuards() {
createNProgressGuard(router) createNProgressGuard(router)
createTabsGuard(router, setRouteChange) createTabsGuard(router, setRouteChange)
// 暂时的 // 暂时的
useMenuStore() // useMenuStore()
} }

@ -0,0 +1,31 @@
import type { RouteRecordRaw } from 'vue-router'
import { PAGE_NOT_FOUND_NAME, REDIRECT_NAME } from '~/constants'
const PAGE_NOT_FOUND_ROUTE: RouteRecordRaw = {
path: '/:all(.*)*',
name: PAGE_NOT_FOUND_NAME,
component: () => import('~/layouts/page/exception.vue'),
props: true,
meta: {
title: 'ErrorPage',
hideInMenu: true,
hideInBreadcrumb: true,
},
}
const REDIRECT_ROUTE: RouteRecordRaw = {
path: '/redirect/:path(.*):_redirect_type(.*)',
name: REDIRECT_NAME,
component: () => import('~/layouts/page/redirect.vue'),
props: true,
meta: {
title: REDIRECT_NAME,
hideInMenu: true,
hideInBreadcrumb: true,
},
}
export {
PAGE_NOT_FOUND_ROUTE,
REDIRECT_ROUTE,
}

@ -1,6 +1,8 @@
<route lang="yaml"> <route lang="yaml">
meta: meta:
title: menu.readme title: menu.readme
order: 3
icon: carbon:book
</route> </route>
## File-based Routing ## File-based Routing

@ -1,7 +1,5 @@
--- ---
title: menu.home title: menu.home
layout: page-layout
path: /abb
--- ---
<route lang="yaml"> <route lang="yaml">

@ -2,14 +2,75 @@
defineOptions({ defineOptions({
name: 'IndexPage', name: 'IndexPage',
}) })
// useHead({ useHead({
// title: '', title: '首页',
// }) })
</script> </script>
<template> <template>
<div> <div>
Index <NInput />
<LIcon icon="nl-net-error" />
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
<div>placeholder</div>
</div> </div>
</template> </template>

@ -1,10 +0,0 @@
<template>
<RouterView />
</template>
<route lang="yaml">
meta:
layout: page-layout
title: menu.login
hideInMenu: true
</route>

@ -40,7 +40,7 @@ function submit() {
<NFormItem path="subject"> <NFormItem path="subject">
<NInput $value="formModelRef.subject" clearable :placeholder="$t('auth.login.account.subject.placeholder')"> <NInput $value="formModelRef.subject" clearable :placeholder="$t('auth.login.account.subject.placeholder')">
<template #prefix> <template #prefix>
<i i-carbon-user /> <LIcon icon="carbon-user" />
</template> </template>
</NInput> </NInput>
</NFormItem> </NFormItem>
@ -50,7 +50,7 @@ function submit() {
show-password-on="click" type="password" show-password-on="click" type="password"
> >
<template #prefix> <template #prefix>
<i i-carbon-locked /> <LIcon icon="carbon-locked" />
</template> </template>
</NInput> </NInput>
</NFormItem> </NFormItem>
@ -69,5 +69,5 @@ function submit() {
<route lang="yaml"> <route lang="yaml">
meta: meta:
hidden: true ignore: true
</route> </route>

@ -40,9 +40,9 @@ function submit() {
<NFormItem path="subject"> <NFormItem path="subject">
<NInput $value="formModelRef.subject" clearable :placeholder="$t('auth.login.sms.subject.placeholder')"> <NInput $value="formModelRef.subject" clearable :placeholder="$t('auth.login.sms.subject.placeholder')">
<template #prefix> <template #prefix>
<i i-carbon-phone /> <LIcon icon="carbon:phone" />
<NDivider vertical /> <NDivider vertical />
<i i-carbon-email /> <LIcon icon="carbon:email" />
</template> </template>
</NInput> </NInput>
</NFormItem> </NFormItem>
@ -53,14 +53,14 @@ function submit() {
:placeholder="$t('auth.login.sms.credentials.placeholder')" :placeholder="$t('auth.login.sms.credentials.placeholder')"
> >
<template #prefix> <template #prefix>
<i i-carbon-two-factor-authentication /> <LIcon icon="carbon:two-factor-authentication" />
</template> </template>
</NInput> </NInput>
<NButton>发送验证码</NButton> <NButton>发送验证码</NButton>
</NInputGroup> </NInputGroup>
</NFormItem> </NFormItem>
<NButton strong block type="primary" attr-type="submit" :loading="authLoading"> <NButton strong block type="primary" attr-type="submit" :loading="authLoading">
{{ $t('auth.login.submit') }}
</NButton> </NButton>
</NForm> </NForm>
</div> </div>
@ -68,5 +68,5 @@ function submit() {
<route lang="yaml"> <route lang="yaml">
meta: meta:
hidden: true ignore: true
</route> </route>

@ -9,7 +9,7 @@ useHead({
</script> </script>
<template> <template>
<AuthLayout :login="true" to="/register"> <LAuthLayout :login="true" to="/register">
<NTabs default-value="account" size="large" animated pane-wrapper-style="margin: 0 -4px"> <NTabs default-value="account" size="large" animated pane-wrapper-style="margin: 0 -4px">
<NTabPane name="account" :tab="$t('auth.login.accountLogin')"> <NTabPane name="account" :tab="$t('auth.login.accountLogin')">
<AccountLogin /> <AccountLogin />
@ -26,12 +26,12 @@ useHead({
<div class="mt-5 text-xs tracking-widest"> <div class="mt-5 text-xs tracking-widest">
{{ $t('auth.login.agreementTip') }} {{ $t('auth.login.agreementTip') }}
</div> </div>
</AuthLayout> </LAuthLayout>
</template> </template>
<route lang="yaml"> <route lang="yaml">
meta: meta:
title: menu.login title: menu.login
hideInMenu: true hideInMenu: true
# layout: page-layout layout: page-layout
</route> </route>

@ -1,9 +0,0 @@
<template>
<RouterView />
</template>
<route lang="yaml">
meta:
layout: page-layout
hideInMenu: true
</route>

@ -86,7 +86,7 @@ function submit() {
<NFormItem path="username"> <NFormItem path="username">
<NInput $value="formModelRef.username" clearable :placeholder="$t('auth.register.account.username.placeholder')"> <NInput $value="formModelRef.username" clearable :placeholder="$t('auth.register.account.username.placeholder')">
<template #prefix> <template #prefix>
<i i-carbon-user /> <LIcon icon="carbon:user" />
</template> </template>
</NInput> </NInput>
</NFormItem> </NFormItem>
@ -98,7 +98,7 @@ function submit() {
:placeholder="$t('auth.register.account.credentials.placeholder')" show-password-on="click" type="password" :placeholder="$t('auth.register.account.credentials.placeholder')" show-password-on="click" type="password"
> >
<template #prefix> <template #prefix>
<i i-carbon-locked /> <LIcon icon="carbon:locked" />
</template> </template>
</NInput> </NInput>
</template> </template>
@ -110,7 +110,7 @@ function submit() {
:placeholder="$t('auth.register.account.credentials.placeholder2')" show-password-on="click" type="password" :placeholder="$t('auth.register.account.credentials.placeholder2')" show-password-on="click" type="password"
> >
<template #prefix> <template #prefix>
<i i-carbon-locked /> <LIcon icon="carbon:locked" />
</template> </template>
</NInput> </NInput>
</NFormItem> </NFormItem>
@ -124,7 +124,7 @@ function submit() {
:placeholder="$t('auth.register.account.phoneNumber.placeholder')" :allow-input="onlyAllowNumber" :placeholder="$t('auth.register.account.phoneNumber.placeholder')" :allow-input="onlyAllowNumber"
> >
<template #prefix> <template #prefix>
<i i-carbon-phone /> <LIcon icon="carbon:phone" />
</template> </template>
</NInput> </NInput>
</NInputGroup> </NInputGroup>
@ -136,7 +136,7 @@ function submit() {
:placeholder="$t('auth.register.account.code.placeholder')" :placeholder="$t('auth.register.account.code.placeholder')"
> >
<template #prefix> <template #prefix>
<i i-carbon-two-factor-authentication /> <LIcon icon="carbon:two-factor-authentication" />
</template> </template>
</NInput> </NInput>
<NButton :disabled="!formModelRef.phoneNumber"> <NButton :disabled="!formModelRef.phoneNumber">
@ -153,5 +153,5 @@ function submit() {
<route lang="yaml"> <route lang="yaml">
meta: meta:
hidden: true ignore: true
</route> </route>

@ -16,41 +16,41 @@ const props = withDefaults(defineProps<{
<NSpace vertical> <NSpace vertical>
<div> <div>
<span mr-1> <span mr-1>
<i <LIcon
v-if="(props.flags & CredentialsFlags.r1) === CredentialsFlags.r1" class="inline-block text-red" v-if="(props.flags & CredentialsFlags.r1) === CredentialsFlags.r1" class="inline-block text-red"
i-carbon-checkmark-outline-error icon="carbon:checkmark-outline-error"
/> />
<i v-else i-carbon-checkmark-outline class="inline-block text-green" /> <LIcon v-else icon="carbon:checkmark-outline" class="inline-block text-green" />
</span> </span>
<span>{{ $t('auth.register.account.rule.r1') }}</span> <span>{{ $t('auth.register.account.rule.r1') }}</span>
</div> </div>
<div> <div>
<span mr-1> <span mr-1>
<i <LIcon
v-if="(props.flags & CredentialsFlags.r2) === CredentialsFlags.r2" class="inline-block text-red" v-if="(props.flags & CredentialsFlags.r2) === CredentialsFlags.r2" class="inline-block text-red"
i-carbon-checkmark-outline-error icon="carbon:checkmark-outline-error"
/> />
<i v-else i-carbon-checkmark-outline class="inline-block text-green" /> <LIcon v-else icon="carbon:checkmark-outline" class="inline-block text-green" />
</span> </span>
<span>{{ $t('auth.register.account.rule.r2') }}</span> <span>{{ $t('auth.register.account.rule.r2') }}</span>
</div> </div>
<div> <div>
<span mr-1> <span mr-1>
<i <LIcon
v-if="(props.flags & CredentialsFlags.r3) === CredentialsFlags.r3" class="inline-block text-red" v-if="(props.flags & CredentialsFlags.r3) === CredentialsFlags.r3" class="inline-block text-red"
i-carbon-checkmark-outline-error icon="carbon:checkmark-outline-error"
/> />
<i v-else i-carbon-checkmark-outline class="inline-block text-green" /> <LIcon v-else icon="carbon:checkmark-outline" class="inline-block text-green" />
</span> </span>
<span>{{ $t('auth.register.account.rule.r3') }}</span> <span>{{ $t('auth.register.account.rule.r3') }}</span>
</div> </div>
<div> <div>
<span mr-1> <span mr-1>
<i <LIcon
v-if="(props.flags & CredentialsFlags.r4) === CredentialsFlags.r4" class="inline-block text-red" v-if="(props.flags & CredentialsFlags.r4) === CredentialsFlags.r4" class="inline-block text-red"
i-carbon-checkmark-outline-error icon="carbon:checkmark-outline-error"
/> />
<i v-else i-carbon-checkmark-outline class="inline-block text-green" /> <LIcon v-else icon="carbon:checkmark-outline" class="inline-block text-green" />
</span> </span>
<span>{{ $t('auth.register.account.rule.r4') }}</span> <span>{{ $t('auth.register.account.rule.r4') }}</span>
</div> </div>
@ -60,5 +60,5 @@ const props = withDefaults(defineProps<{
<route lang="yaml"> <route lang="yaml">
meta: meta:
hidden: true ignore: true
</route> </route>

@ -48,9 +48,9 @@ function submit() {
:placeholder="$t('auth.register.sms.phoneNumber.tip')" :placeholder="$t('auth.register.sms.phoneNumber.tip')"
> >
<template #prefix> <template #prefix>
<i i-carbon-phone /> <LIcon icon="carbon:phone" />
<NDivider vertical /> <NDivider vertical />
<i i-carbon-email /> <LIcon icon="carbon:email" />
</template> </template>
</NInput> </NInput>
</NFormItem> </NFormItem>
@ -61,7 +61,7 @@ function submit() {
:placeholder="$t('auth.register.sms.code.tip')" :placeholder="$t('auth.register.sms.code.tip')"
> >
<template #prefix> <template #prefix>
<i i-carbon-two-factor-authentication /> <LIcon icon="carbon:two-factor-authentication" />
</template> </template>
</NInput> </NInput>
<NButton>发送验证码</NButton> <NButton>发送验证码</NButton>
@ -76,5 +76,5 @@ function submit() {
<route lang="yaml"> <route lang="yaml">
meta: meta:
hidden: true ignore: true
</route> </route>

@ -4,7 +4,7 @@ import SMS from './comp/sms.vue'
</script> </script>
<template> <template>
<AuthLayout :login="false" to="/login"> <LAuthLayout :login="false" to="/login">
<NTabs default-value="account" size="large" animated pane-wrapper-style="margin: 0 -4px"> <NTabs default-value="account" size="large" animated pane-wrapper-style="margin: 0 -4px">
<NTabPane name="account" :tab="$t('auth.register.accountRegister')"> <NTabPane name="account" :tab="$t('auth.register.accountRegister')">
<Account /> <Account />
@ -13,11 +13,12 @@ import SMS from './comp/sms.vue'
<SMS /> <SMS />
</NTabPane> </NTabPane>
</NTabs> </NTabs>
</AuthLayout> </LAuthLayout>
</template> </template>
<route lang="yaml"> <route lang="yaml">
meta: meta:
layout: page-layout layout: page-layout
hideInMenu: true
title: 注册 title: 注册
</route> </route>

29
src/shims.d.ts vendored

@ -19,32 +19,3 @@ declare module '*.vue' {
const component: DefineComponent<object, object, any> const component: DefineComponent<object, object, any>
export default component export default component
} }
declare module 'vue-router' {
interface RouteMeta extends Record<string | number | symbol, any> {
[key: string]: any
// 忽略路由,菜单中也不可见
hidden?: boolean
// 在菜单中隐藏,但路由存在
hideInMenu?: boolean
// 隐藏子菜单
hideChildrenInMenu?: boolean
// 隐藏Tab
hideInTab?: boolean
// 在面包屑中隐藏
hideInBreadcrumb?: boolean
// 忽略KeepAlive
ignoreKeepAlive?: boolean
// 保持tab
affix?: boolean
// 页面权限
perm?: string | boolean | RouteMeta[]
// 页面标题
title?: string
// 页面图标(菜单)
icon?: string
// 菜单排序
order?: number
}
}

@ -6,6 +6,7 @@ import {
MenuModeEnum, MenuModeEnum,
MixSidebarTriggerEnum, MixSidebarTriggerEnum,
NavBarModeEnum, NavBarModeEnum,
PermissionModeEnum,
PlacementEnum, PlacementEnum,
RouterTransitionEnum, RouterTransitionEnum,
SIDE_BAR_BG_COLOR_LIST, SIDE_BAR_BG_COLOR_LIST,
@ -28,7 +29,7 @@ const initState: DefineAppConfigOptions = {
// permissionCacheType: CacheTypeEnum.LOCAL, // permissionCacheType: CacheTypeEnum.LOCAL,
settingButtonPosition: SettingButtonPositionEnum.AUTO, settingButtonPosition: SettingButtonPositionEnum.AUTO,
openSettingDrawer: false, openSettingDrawer: false,
// permissionMode: PermissionModeEnum.ROUTE_MAPPING, permissionMode: PermissionModeEnum.PERM,
sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP, sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP,
grayMode: false, grayMode: false,
colorWeak: false, colorWeak: false,
@ -117,21 +118,13 @@ export const useAppConfigStore = defineStore('APP_CONFIG', () => {
const isTopMenu = computed(() => state.navBarMode === NavBarModeEnum.TOP_MENU) const isTopMenu = computed(() => state.navBarMode === NavBarModeEnum.TOP_MENU)
const isMixSidebar = computed(() => state.navBarMode === NavBarModeEnum.MIX_SIDEBAR) const isMixSidebar = computed(() => state.navBarMode === NavBarModeEnum.MIX_SIDEBAR)
const isMix = computed(() => state.navBarMode === NavBarModeEnum.MIX) const isMix = computed(() => state.navBarMode === NavBarModeEnum.MIX)
const isMixMode = computed(() => state.navBarMode === NavBarModeEnum.MIX && state.menu.mode === MenuModeEnum.INLINE) const isMixMode = computed(() =>
state.navBarMode === NavBarModeEnum.MIX
&& state.menu.mode === MenuModeEnum.INLINE,
)
const isHorizontal = computed(() => state.menu.mode === MenuModeEnum.HORIZONTAL) const isHorizontal = computed(() => state.menu.mode === MenuModeEnum.HORIZONTAL)
const tagTarCache = computed(() => state.tabTar.cache) const tagTarCache = computed(() => state.tabTar.cache)
const isCollapsedShowTitle = computed(() => {
if (unref(isMixSidebar) || unref(isSidebar))
return state.sidebar.collapsed
return state.menu.collapsedShowTitle && state.sidebar.collapsed
})
function $reset() {
state = Object.assign({}, initState)
}
function setAppConfig(configs: DeepPartial<DefineAppConfigOptions>) { function setAppConfig(configs: DeepPartial<DefineAppConfigOptions>) {
state = _merge(state, configs) state = _merge(state, configs)
} }
@ -169,7 +162,6 @@ export const useAppConfigStore = defineStore('APP_CONFIG', () => {
} }
return { return {
$state: state,
...toRefs(state), ...toRefs(state),
isSidebar, isSidebar,
isTopMenu, isTopMenu,
@ -177,11 +169,9 @@ export const useAppConfigStore = defineStore('APP_CONFIG', () => {
isMix, isMix,
isMixMode, isMixMode,
isHorizontal, isHorizontal,
isCollapsedShowTitle,
tagTarCache, tagTarCache,
setAppConfig,
$reset,
// setter // setter
setAppConfig,
setSidebar, setSidebar,
setMenu, setMenu,
setHeader, setHeader,

@ -0,0 +1,21 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { localeSetting } from '~/modules/i18n/config'
export const useLocaleStore = defineStore('LOCALE', () => {
const state = $ref<LocaleConfig>(localeSetting)
const getLocale = computed(() => unref(state).locale)
const setLocale = (locale: string) => {
state.locale = locale
}
return {
...toRefs(state),
getLocale,
setLocale,
}
}, { persist: {
paths: Object.keys(localeSetting),
} })
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useLocaleStore as any, import.meta.hot))

@ -1,12 +0,0 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
export const useMenuStore = defineStore('MENU', () => {
const menuList = $ref<RouteRecordRaw[]>([])
return {
menuList,
}
}, { persist: true })
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useMenuStore as any, import.meta.hot))

@ -336,13 +336,13 @@ export const useMultipleTabStore = defineStore('MULTIPLE_TAB', () => {
{ {
label: 'layout.multipleTab.reload', label: 'layout.multipleTab.reload',
key: TabActionEnum.REFRESH_PAGE, key: TabActionEnum.REFRESH_PAGE,
icon: 'ion:reload-sharp', icon: 'ant-design:reload-outlined',
disabled: refreshDisabled, disabled: refreshDisabled,
}, },
{ {
label: 'layout.multipleTab.close', label: 'layout.multipleTab.close',
key: TabActionEnum.CLOSE_CURRENT, key: TabActionEnum.CLOSE_CURRENT,
icon: 'clarity:close-line', icon: 'ant-design:close-outlined',
disabled: !!meta?.affix || disabled, disabled: !!meta?.affix || disabled,
}, },
{ {
@ -350,13 +350,13 @@ export const useMultipleTabStore = defineStore('MULTIPLE_TAB', () => {
key: 'divider1', key: 'divider1',
}, },
{ {
icon: 'line-md:arrow-close-left', icon: 'ant-design:vertical-right-outlined',
key: TabActionEnum.CLOSE_LEFT, key: TabActionEnum.CLOSE_LEFT,
label: 'layout.multipleTab.closeLeft', label: 'layout.multipleTab.closeLeft',
disabled: closeLeftDisabled, disabled: closeLeftDisabled,
}, },
{ {
icon: 'line-md:arrow-close-right', icon: 'ant-design:vertical-left-outlined',
key: TabActionEnum.CLOSE_RIGHT, key: TabActionEnum.CLOSE_RIGHT,
label: 'layout.multipleTab.closeRight', label: 'layout.multipleTab.closeRight',
disabled: closeRightDisabled, disabled: closeRightDisabled,
@ -366,7 +366,7 @@ export const useMultipleTabStore = defineStore('MULTIPLE_TAB', () => {
key: 'divider2', key: 'divider2',
}, },
{ {
icon: 'dashicons:align-center', icon: 'ant-design:holder-outlined',
key: TabActionEnum.CLOSE_OTHER, key: TabActionEnum.CLOSE_OTHER,
label: 'layout.multipleTab.closeOther', label: 'layout.multipleTab.closeOther',
disabled, disabled,
@ -374,7 +374,7 @@ export const useMultipleTabStore = defineStore('MULTIPLE_TAB', () => {
{ {
label: 'layout.multipleTab.closeAll', label: 'layout.multipleTab.closeAll',
key: TabActionEnum.CLOSE_ALL, key: TabActionEnum.CLOSE_ALL,
icon: 'clarity:minus-line', icon: 'ant-design:line-outlined',
disabled, disabled,
}, },
] ]

@ -0,0 +1,118 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import pageRoutes from '~pages'
import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '~/modules/router/routes/basic'
import { cloneDeep, filterTree, transformRouteToMenu } from '~/utils'
import { PermissionModeEnum } from '~/constants'
export const useRouteStore = defineStore('ROUTES', () => {
const routesRef = shallowRef<RouteRecordRaw[]>([])
const menuListRef = ref<Menu[]>([])
const initMenu = ref(false)
/**
*
*/
function initRoutes() {
/**
*
*/
const routeIgnoreFilter = (route: RouteRecordRaw) => {
const { meta } = route
const { ignore } = meta || {}
return !ignore
}
const routes: RouteRecordRaw[] = []
routes.push(PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE)
routes.push(...pageRoutes)
routesRef.value = filterTree(routes, routeIgnoreFilter)
return routesRef.value
}
/** 判断给定的route中是否具有给定perms列表的权限 */
function hasPermission(route: RouteRecordRaw, perms: any[] = []) {
if (!route.meta?.perm) {
return true
}
// 递归寻找子节点perm
if (route.meta?.perm === true && route.children?.length) {
return filterAsyncRoutes(route.children, perms).length
}
// 否则直接通过perm进行判断
return perms.includes(
Array.isArray(route.meta?.perm)
? route.meta.perm[0]?.perm
: route.meta?.perm,
)
}
// 过滤掉所有perm不匹配的路由
function filterAsyncRoutes(routes: RouteRecordRaw[], perms: any[]) {
return routes.reduce((rs: RouteRecordRaw[], route) => {
if (hasPermission(route, perms)) {
rs.push({
...route,
children: route.children ? filterAsyncRoutes(route.children, perms) : [],
})
}
return rs
}, [])
}
async function buildMenu() {
initMenu.value = false
const appStore = useAppConfigStore()
const userStore = useUserStore()
const roleList = userStore.userInfo?.roles || []
const permList = userStore.userInfo?.perms || []
const permissionMode = unref(appStore.permissionMode)
const routeRoleFilter = (route: RouteRecordRaw) => {
const { meta } = route
const { roles } = meta || {}
if (!roles) {
return true
}
return roleList.some(role => roles.includes(role))
}
const r = cloneDeep(unref(routesRef))
let menuList: Menu[] = []
switch (permissionMode) {
case PermissionModeEnum.ROLE:
{
const routes = filterTree(r, routeRoleFilter)
menuList = transformRouteToMenu(routes, true)
}
break
case PermissionModeEnum.PERM:
menuList = transformRouteToMenu(filterAsyncRoutes(r, permList), true)
break
}
menuList.sort((a, b) => {
return (a.meta?.order || 0) - (b.meta?.order || 0)
})
menuListRef.value = menuList
initMenu.value = true
}
return {
routesRef,
menuListRef,
initMenu,
initRoutes,
buildMenu,
}
}, {
persist: {
paths: ['menuListRef'],
},
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useRouteStore as any, import.meta.hot))

@ -11,12 +11,14 @@ export const useSiteConfigStore = defineStore('SITE_CONFIG', () => {
copyright: '2022-present NoahLan', copyright: '2022-present NoahLan',
links: [ links: [
{ {
icon: 'mdi:document',
label: t('layout.footer.onlineDocument'), label: t('layout.footer.onlineDocument'),
url: 'https://nadm.noahlan.cn/docs', url: 'https://nadm.noahlan.cn/docs',
}, },
{ {
icon: 'uim:github', icon: 'ant-design:github-filled',
url: 'https://git.noahlan.cn/n-admin', url: 'https://git.noahlan.cn/n-admin',
label: '源码',
}, },
], ],
} }

@ -0,0 +1,55 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ThemeEnum } from '~/constants'
interface ThemeStoreState {
themeConfig: ThemeColorConfig
theme: ThemeEnum
sidebar: ThemeEnum
header: ThemeEnum
}
const initState: ThemeStoreState = {
themeConfig: {
primaryColor: '#2a64d4',
infoColor: '#2080F0',
successColor: '#52c41a',
warningColor: '#faad14',
errorColor: '#D03050',
textBaseColor: '#000000',
bgBaseColor: '#ffffff',
},
theme: ThemeEnum.LIGHT,
sidebar: ThemeEnum.DARK,
header: ThemeEnum.DARK,
}
export const useThemeStore = defineStore('APP_THEME', () => {
const state = $ref<ThemeStoreState>(Object.assign({}, initState))
function setThemeConfig(config: Partial<ThemeColorConfig>) {
state.themeConfig = { ...state.themeConfig, ...config }
}
function setSidebarTheme(value: ThemeEnum) {
state.sidebar = value
}
function setHeaderTheme(value: ThemeEnum) {
state.header = value
}
return {
...toRefs(state),
setThemeConfig,
setSidebarTheme,
setHeaderTheme,
}
}, {
persist: {
paths: Object.keys(initState),
},
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useThemeStore as any, import.meta.hot))

@ -1,16 +1,10 @@
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
export const useUserStore = defineStore('USER', () => { export const useUserStore = defineStore('USER', () => {
let userInfo = $ref<API.UserInfo>() const userInfo = ref<API.UserInfo>()
let authInfo = $ref<API.LoginResp>() const authInfo = ref<API.LoginResp>()
function $reset() {
userInfo = Object.assign({})
authInfo = Object.assign({})
}
return { return {
$reset,
userInfo, userInfo,
authInfo, authInfo,
} }

@ -0,0 +1,172 @@
* > .enter-x:nth-child(1) {
transform: translateX(50px);
}
* > .-enter-x:nth-child(1) {
transform: translateX(-50px);
}
* > .enter-x:nth-child(1),
* > .-enter-x:nth-child(1) {
z-index: 9;
opacity: 0;
animation: enter-x-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.1s;
}
* > .enter-y:nth-child(1) {
transform: translateY(50px);
}
* > .enter-y:nth-child(1),
* > .-enter-y:nth-child(1) {
z-index: 9;
opacity: 0;
animation: enter-y-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.1s;
}
* > .enter-x:nth-child(2) {
transform: translateX(50px);
}
* > .-enter-x:nth-child(2) {
transform: translateX(-50px);
}
* > .enter-x:nth-child(2),
* > .-enter-x:nth-child(2) {
z-index: 8;
opacity: 0;
animation: enter-x-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.2s;
}
* > .enter-y:nth-child(2) {
transform: translateY(50px);
}
* > .enter-y:nth-child(2),
* > .-enter-y:nth-child(2) {
z-index: 8;
opacity: 0;
animation: enter-y-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.2s;
}
* > .enter-x:nth-child(3) {
transform: translateX(50px);
}
* > .-enter-x:nth-child(3) {
transform: translateX(-50px);
}
* > .enter-x:nth-child(3),
* > .-enter-x:nth-child(3) {
z-index: 7;
opacity: 0;
animation: enter-x-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.3s;
}
* > .enter-y:nth-child(3) {
transform: translateY(50px);
}
* > .enter-y:nth-child(3),
* > .-enter-y:nth-child(3) {
z-index: 7;
opacity: 0;
animation: enter-y-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.3s;
}
* > .enter-x:nth-child(4) {
transform: translateX(50px);
}
* > .-enter-x:nth-child(4) {
transform: translateX(-50px);
}
* > .enter-x:nth-child(4),
* > .-enter-x:nth-child(4) {
z-index: 6;
opacity: 0;
animation: enter-x-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.4s;
}
* > .enter-y:nth-child(4) {
transform: translateY(50px);
}
* > .enter-y:nth-child(4),
* > .-enter-y:nth-child(4) {
z-index: 6;
opacity: 0;
animation: enter-y-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.4s;
}
* > .enter-x:nth-child(5) {
transform: translateX(50px);
}
* > .-enter-x:nth-child(5) {
transform: translateX(-50px);
}
* > .enter-x:nth-child(5),
* > .-enter-x:nth-child(5) {
z-index: 5;
opacity: 0;
animation: enter-x-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.5s;
}
* > .enter-y:nth-child(5) {
transform: translateY(50px);
}
* > .enter-y:nth-child(5),
* > .-enter-y:nth-child(5) {
z-index: 5;
opacity: 0;
animation: enter-y-animation 0.4s ease-in-out 0.3s;
animation-fill-mode: forwards;
animation-delay: 0.5s;
}
@keyframes enter-x-animation {
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-y-animation {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes loading-circle {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@ -0,0 +1,5 @@
import './main.css'
import './entry.css'
import './variables.css'
import '@unocss/reset/tailwind.css'
import 'uno.css'

@ -1,29 +1,90 @@
@import './markdown.css'; @import './markdown.css';
html, html {
body, overflow: hidden;
#app { text-size-adjust: 100%;
height: 100%;
margin: 0;
padding: 0;
} }
html.dark { html.dark {
background: #121212; /* background: #121212; */
color-scheme: dark; color-scheme: dark;
} }
html,
body {
width: 100%;
height: 100%;
overflow: visible !important;
overflow-x: hidden !important;
}
*{
box-sizing: border-box;
}
a:focus,
a:active,
button,
div,
svg,
span {
outline: none !important;
}
input:-webkit-autofill {
box-shadow: 0 0 0 1000px white inset !important;
}
:-webkit-autofill {
transition: background-color 5000s ease-in-out 0s !important;
}
/* scrollbar */
::-webkit-scrollbar {
width: 7px;
height: 8px;
}
::-webkit-scrollbar-track {
background-color: rgb(0 0 0 / 5%);
}
::-webkit-scrollbar-thumb {
background-color: rgb(144 147 153 / 30%);
border-radius: 2px;
box-shadow: inset 0 0 6px rgb(0 0 0 / 20%);
}
::-webkit-scrollbar-thumb:hover {
background-color: #b6b7b9;
}
/* nprogress */
#nprogress { #nprogress {
pointer-events: none; pointer-events: none;
} }
#nprogress .bar { #nprogress .bar {
background: rgb(13,148,136);
opacity: 0.75;
position: fixed; position: fixed;
z-index: 1031;
top: 0; top: 0;
left: 0; left: 0;
z-index: 99999;
width: 100%; width: 100%;
height: 2px; height: 2px;
opacity: 0.75;
background-color: var(--primary-color);
} }
/* app */
#app {
width: 100%;
height: 100%;
}
html.color-weak {
filter: invert(80%);
}
html.gray-mode {
filter: grayscale(100%);
filter: progid:dximagetransform.microsoft.basicimage(grayscale=1);
}

@ -0,0 +1,119 @@
:root {
/* --primary-color: #0960bd;
--success-color: #55d187;
--error-color: #ed6f6f;
--warning-color: #efbd47; */
--vxe-primary-color: var(--primary-color);
--vxe-success-color: var(--success-color);
--vxe-error-color: var(--error-color);
--vxe-warning-color: var(--warning-color);
/* component */
--component-background-color: #fff;
/* transition */
--transition-bezier: cubic-bezier(0.4, 0, 0.2, 1);
--transition-all: all 0.3s var(--transition-bezier);
/* layout start */
--layout-color: #fff;
--layout-container-background-color: #fff;
--layout-border-color: rgb(239, 239, 245);
/* header */
--header-height: 48px;
--header-width: calc(100% - var(--aside-width));
--header-background-color: #fff;
--header-text-color: rgba(0, 0, 0, 0.85);
--header-icon-color: var(--header-text-color);
--header-action-hover-bg-color: #f6f6f6;
/* tab */
--tab-bar-height: 36px;
--tab-bar-width: calc(100% - var(--aside-width));
/* aside */
--aside-height: calc(100% - var(--header-height));
--aside-width: 210px;
--aside-background-color: #001529;
--aside-submenu-background-color: #0c2135;
--aside-text-color: #fff;
--trigger-background-color: rgb(72, 72, 78);
/*--trigger-border: 1px solid rgb(239, 239, 245);*/
--trigger-border-color: rgb(239, 239, 245);
/*--trigger-border-color: rgb(239, 239, 245);*/
/*--trigger-icon-color: #f3f1f1;*/
/*--trigger-icon-color: rgb(51, 54, 57);*/
/*--trigger-hover-icon-color: #fff;*/
/* main */
--main-height: calc(100% - var(--footer-height));
--main-width: 100%;
/* footer */
--footer-height: 60px;
--footer-width: 100%;
/* layout end */
--vxe-modal-header-background-color: #8eabf8;
--vxe-modal-header-color: #fff;
--vxe-table-header-font-color: #fff;
--vxe-table-header-background-color: #8eabf8;
--vxe-font-size: 14px;
--vxe-font-color: #666;
/* mix sidebar */
--mix-sider-z-index: 500;
}
:root[class=dark] {
--layout-border-color: rgba(255, 255, 255, 0.09);
--layout-container-background-color: rgb(16, 16, 20);
--component-background-color: rgb(24, 24, 28);
--vxe-modal-header-background-color: #27355d;
--vxe-form-background-color: #212b4b;
--vxe-table-body-background-color: #212b4b;
--vxe-grid-maximize-background-color: #212b4b;
--vxe-textarea-background-color: #212b4b;
--vxe-table-row-current-background-color: #334579;
--vxe-table-column-current-background-color: #334579;
--vxe-table-column-hover-background-color: #334579;
--vxe-table-row-hover-background-color: #405492;
--vxe-table-row-hover-current-background-color: #4d63ad;
--vxe-input-date-picker-hover-background-color: #4d63ad;
--vxe-loading-background-color: #677dc780;
--vxe-loading-color: #75bcea;
--vxe-table-row-checkbox-checked-background-color: #405492;
--vxe-table-row-radio-checked-background-color: #405492;
--vxe-modal-body-background-color: #212b4b;
--vxe-button-default-background-color: #212b4b;
--vxe-pulldown-panel-background-color: #212b4b;
--vxe-input-background-color: #212b4b;
--vxe-select-panel-background-color: #212b4b;
--vxe-radio-button-default-background-color: #212b4b;
--vxe-toolbar-background-color: #212b4b;
--vxe-pager-background-color: #212b4b;
--vxe-pager-perfect-background-color: #212b4b;
--vxe-table-tree-node-line-color: #bec1c5;
--vxe-table-border-color: #e2ebf6;
--vxe-textarea-count-color: #e2ebf6;
--vxe-table-header-font-color: #fff;
--vxe-table-header-background-color: #27355d;
--vxe-select-option-hover-background-color: #27355d;
--vxe-font-size: 14px;
--vxe-font-color: #e2ebf6;
--vxe-table-font-color: #e2ebf6;
--vxe-font-l10-color: #f5f8fc;
--vxe-font-l20-color: #fff;
/*
--vxe-font-l10-color:#{darken(#e2ebf6,10%)};
--vxe-font-l20-color:#{darken(#e2ebf6,20%)};
*/
--vxe-pager-font-color: #ffffffe6;
--vxe-row-selected-bg-color: #d39a26;
}

@ -4,6 +4,7 @@ import type {
MenuModeEnum, MenuModeEnum,
MixSidebarTriggerEnum, MixSidebarTriggerEnum,
NavBarModeEnum, NavBarModeEnum,
PermissionModeEnum,
PlacementEnum, PlacementEnum,
RouterTransitionEnum, RouterTransitionEnum,
SessionTimeoutProcessingEnum, SessionTimeoutProcessingEnum,
@ -36,8 +37,8 @@ declare global {
removeAllHttpPending: boolean removeAllHttpPending: boolean
// // Storage location of permission related information // // Storage location of permission related information
// permissionCacheType: CacheTypeEnum // permissionCacheType: CacheTypeEnum
// // Permission mode // Permission mode
// permissionMode: PermissionModeEnum permissionMode: PermissionModeEnum
// Configure where the button is displayed // Configure where the button is displayed
settingButtonPosition: SettingButtonPositionEnum settingButtonPosition: SettingButtonPositionEnum
// Configuration setting drawer open // Configuration setting drawer open
@ -158,4 +159,14 @@ declare global {
dropdownPlacement: string dropdownPlacement: string
subMenuWidth: number subMenuWidth: number
} }
// 语言配置
interface LocaleConfig {
// current locale
locale: string
// default locale
fallback: string
// available locales
availableLocales: string[]
}
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save