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接口前缀
VITE_API_URL_PREFIX=
# 本地ICON前缀
VITE_ICON_LOCAL_PREFIX=nl
# UNOCSS ICON前缀
VITE_ICON_UNOCSS_PREFIX=i
# 是否开启请求代理
VITE_HTTP_PROXY=false
# 是否开启打包文件大小结果分析

@ -18,7 +18,7 @@
"vmodel",
"vueuse"
],
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.sourceLanguage": "zh-chs",
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": "locales",
"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:
about: 关于
back: 返回
@ -5,6 +34,9 @@ button:
home: 首页
toggle_dark: 切换深色模式
toggle_langs: 切换语言
copy: 复制
reset: 重置
reload: 刷新
not-found: 未找到页面
menu:
home: 首页
@ -14,8 +46,20 @@ menu:
dialog:
errTitle: 错误提示
layout:
header:
tooltipExitFull: 退出全屏
tooltipEntryFull: 全屏模式
tooltipSettings: 设置
tooltipLanguage: 切换语言
footer:
onlineDocument: 在线文档
multipleTab:
reload: 重新加载
close: 关闭标签页
closeLeft: 关闭左侧标签页
closeRight: 关闭右侧标签页
closeOther: 关闭其它标签页
closeAll: 关闭所有标签页
setting:
# content mode
contentModeFull: 流式
@ -43,8 +87,6 @@ layout:
operatingContent: 复制成功,请到 src/settings/projectSetting.ts 中修改配置!
resetSuccess: 重置成功!
copyBtn: 拷贝
resetBtn: 重置配置
clearBtn: 清空缓存并返回登录页
drawerTitle: 项目配置
@ -59,9 +101,14 @@ layout:
splitMenu: 分割菜单
closeMixSidebarOnChange: 切换页面关闭菜单
sysTheme: 系统主题
themeEditor: 主题配置
primaryColor: 主题色
successColor: 成功色
warningColor: 警告色
errorColor: 错误色
infoColor: 信息色
headerTheme: 顶栏主题
sidebarTheme: 菜单主题
sidebarDark: 侧边栏深色
menuDrag: 侧边菜单拖拽
menuSearch: 菜单搜索

@ -4,13 +4,18 @@
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@8.5.1",
"description": "description",
"author": {
"name": "NorthLan"
},
"engines": {
"node": ">=16.15.1",
"pnpm": ">=8.1.0"
},
"scripts": {
"build": "vite build",
"dev": "vite --port 3333",
"predev": "esno build/generate/index.ts && pnpm run icon:dev",
"dev": "vite --port 8088",
"lint": "eslint .",
"preview": "vite preview",
"preview-https": "serve dist",
@ -20,13 +25,14 @@
"typecheck": "vue-tsc --noEmit",
"up": "taze major -I",
"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": {
"@unhead/vue": "^1.7.4",
"@unocss/reset": "^0.55.7",
"@vueuse/core": "^10.5.0",
"@vueuse/head": "^2.0.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"vue": "^3.3.6",
@ -35,30 +41,37 @@
"vue-router": "^4.2.5"
},
"devDependencies": {
"@ant-design/colors": "^7.0.0",
"@antfu/eslint-config": "1.0.0-beta.28",
"@iconify-json/ant-design": "^1.1.10",
"@iconify-json/carbon": "^1.1.21",
"@iconify-json/emojione": "^1.1.7",
"@iconify-json/gridicons": "^1.1.11",
"@iconify-json/ion": "^1.1.12",
"@iconify/json": "^2.2.141",
"@iconify/tools": "^3.0.6",
"@iconify/types": "^2.0.0",
"@iconify/utils": "^2.1.11",
"@iconify/vue": "^4.1.1",
"@intlify/unplugin-vue-i18n": "^1.4.0",
"@types/markdown-it-link-attributes": "^3.0.3",
"@types/nprogress": "^0.2.2",
"@types/qs": "^6.9.9",
"@types/sortablejs": "^1.15.4",
"@unocss/eslint-config": "^0.57.0",
"@vitejs/plugin-vue": "^4.4.0",
"@vue-macros/reactivity-transform": "^0.3.23",
"@vue-macros/short-vmodel": "^1.3.0",
"@vue-macros/volar": "^0.14.3",
"@vue/test-utils": "^2.4.1",
"chalk": "^5.3.0",
"critters": "^0.0.20",
"cross-env": "^7.0.3",
"cypress": "^13.3.2",
"cypress-vite": "^1.4.2",
"easy-fns-ts": "^1.2.3",
"eslint": "^8.52.0",
"eslint-plugin-cypress": "^2.15.1",
"esno": "^4.0.0",
"fast-glob": "^3.3.2",
"https-localhost": "^4.7.1",
"less": "^4.2.0",
"less-loader": "^11.1.3",
"lint-staged": "^14.0.1",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-shiki": "^0.9.0",
@ -71,6 +84,7 @@
"sortablejs": "^1.15.0",
"taze": "^0.11.4",
"typescript": "^5.2.2",
"typescript-json-schema": "^0.62.0",
"unocss": "^0.55.7",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
@ -78,10 +92,12 @@
"unplugin-vue-markdown": "^0.24.3",
"vite": "^4.5.0",
"vite-bundle-visualizer": "^0.10.0",
"vite-plugin-banner": "^0.7.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-inspect": "^0.7.40",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pwa": "^0.16.5",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-component-preview": "^1.1.6",
"vite-plugin-vue-devtools": "1.0.0-rc.5",
"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 { darkTheme, dateZhCN, zhCN } from 'naive-ui'
const { isDark, themeColors } = useAppTheme()
// https://github.com/vueuse/head
// you can use this to manipulate the document head in any components,
// they will be rendered correctly in the html results with vite-ssg
// useHead({
// title: `${import.meta.env.VITE_TITLE}`,
// meta: [
// { name: 'description', content: `${import.meta.env.VITE_DESCRIPTION}` },
// {
// name: 'theme-color',
// content: () => isDark.value ? '#00aba9' : '#ffffff',
// },
// ],
// link: [
// {
// rel: 'icon',
// type: 'image/svg+xml',
// href: () => preferredDark.value ? '/favicon-dark.svg' : '/favicon.svg',
// },
// ],
// })
// https://unhead.unjs.io/
useHead({
title: `${import.meta.env.VITE_APP_TITLE}`,
meta: [
{ name: 'description', content: `${import.meta.env.VITE_APP_DESC}` },
{
name: 'theme-color',
content: () => isDark.value ? '#00aba9' : '#ffffff',
},
],
link: [
{
rel: 'icon',
type: 'image/svg+xml',
href: () => isDark.value ? '/favicon-dark.svg' : '/favicon.svg',
},
],
})
const theme = computed(() => {
return isDark.value ? darkTheme : null
return unref(isDark) ? darkTheme : null
})
// const i18n = useI18n()
@ -32,12 +33,9 @@ const theme = computed(() => {
const themeOverridesRef = computed((): GlobalThemeOverrides => {
return {
// common: {
// primaryColor: '#0052D9',
// primaryColorHover: '#366ef4',
// primaryColorPressed: '#003cab',
// primaryColorSuppl: '#003ccc',
// },
common: {
...unref(themeColors),
},
}
})
</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 createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createMediaPrefersColorSchemeListen: typeof import('./composables/web/theme')['createMediaPrefersColorSchemeListen']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createThemeColorListen: typeof import('./composables/web/theme')['createThemeColorListen']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
@ -43,12 +45,14 @@ declare global {
const effectScope: typeof import('vue')['effectScope']
const envD: typeof import('./types/env.d')['default']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActiveHead: typeof import('@unhead/vue')['getActiveHead']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getRoutes: typeof import('./composables/router/routes')['getRoutes']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectHead: typeof import('@unhead/vue')['injectHead']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDark: typeof import('./composables/dark')['isDark']
const isDefined: typeof import('@vueuse/core')['isDefined']
@ -104,6 +108,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const routeLink: typeof import('vue-router/auto')['routeLink']
const setGlobalOptions: typeof import('./composables/request')['setGlobalOptions']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
@ -132,6 +137,7 @@ declare global {
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAppConfig: typeof import('./composables/config/app-config')['useAppConfig']
const useAppConfigStore: typeof import('./stores/app-config')['useAppConfigStore']
const useAppTheme: typeof import('./composables/web/theme')['useAppTheme']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
@ -199,9 +205,12 @@ declare global {
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
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 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 useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
@ -215,6 +224,8 @@ declare global {
const useLoadMore: typeof import('./composables/request')['useLoadMore']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
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 useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
@ -255,6 +266,7 @@ declare global {
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const usePromise: typeof import('./composables/promise')['usePromise']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRedo: typeof import('./composables/page')['useRedo']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
@ -263,13 +275,17 @@ declare global {
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRootSetting: typeof import('./composables/setting/root-setting')['useRootSetting']
const useRoute: typeof import('vue-router')['useRoute']
const useRouteStore: typeof import('./stores/routes')['useRouteStore']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
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 useShare: typeof import('@vueuse/core')['useShare']
const useSiteConfigStore: typeof import('./stores/site')['useSiteConfigStore']
@ -289,6 +305,7 @@ declare global {
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThemeStore: typeof import('./stores/theme')['useThemeStore']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
@ -366,10 +383,12 @@ declare module 'vue' {
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
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 createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
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 customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
@ -379,14 +398,14 @@ declare module 'vue' {
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActiveHead: UnwrapRef<typeof import('@unhead/vue')['getActiveHead']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getRoutes: UnwrapRef<typeof import('./composables/router/routes')['getRoutes']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectHead: UnwrapRef<typeof import('@unhead/vue')['injectHead']>
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 isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@ -420,7 +439,6 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
@ -454,7 +472,6 @@ declare module 'vue' {
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@ -468,6 +485,7 @@ declare module 'vue' {
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']>
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 useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@ -535,9 +553,11 @@ declare module 'vue' {
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
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 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 useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@ -551,6 +571,8 @@ declare module 'vue' {
readonly useLoadMore: UnwrapRef<typeof import('./composables/request')['useLoadMore']>
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
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 useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
@ -559,7 +581,6 @@ declare module 'vue' {
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']>
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 useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
@ -590,6 +611,7 @@ declare module 'vue' {
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
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 useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
@ -598,13 +620,17 @@ declare module 'vue' {
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']>
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 useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
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 useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSiteConfigStore: UnwrapRef<typeof import('./stores/site')['useSiteConfigStore']>
@ -624,6 +650,7 @@ declare module 'vue' {
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
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 useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
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 createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
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 createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
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 customRef: UnwrapRef<typeof import('vue')['customRef']>
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 effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActiveHead: UnwrapRef<typeof import('@unhead/vue')['getActiveHead']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getRoutes: UnwrapRef<typeof import('./composables/router/routes')['getRoutes']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectHead: UnwrapRef<typeof import('@unhead/vue')['injectHead']>
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 isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
@ -749,7 +778,6 @@ declare module '@vue/runtime-core' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
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 toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
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 useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']>
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 useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
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 useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
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 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 useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
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 useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
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 useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
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 useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']>
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 useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
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 usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
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 useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']>
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 useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']>
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 useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
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 useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
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 useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
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 useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>

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

@ -12,7 +12,7 @@ const desc = computed(() => import.meta.env.VITE_APP_DESC)
<template>
<div flex="~ col" h-screen w-screen bg="zinc-50 dark:zinc-900">
<AuthBg />
<LAuthBg />
<!-- 头部 -->
<div
bg="white dark:zinc-800" flex="~ col"
@ -27,7 +27,7 @@ const desc = computed(() => import.meta.env.VITE_APP_DESC)
</div>
</RouterLink>
<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 whitespace-nowrap font-extrabold tracking-widest text="4xl gray-700 dark:gray-100">
{{ 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 { HandlerSettingEnum, ThemeEnum } from '~/constants'
import { _omit } from '~/utils'
import { HandlerSettingEnum, NavBarModeEnum } from '~/constants'
import { _merge, _omit } from '~/utils'
export function useAppConfig() {
const configStore = useAppConfigStore()
@ -14,7 +14,19 @@ export function useAppConfig() {
} = appConfigOptions
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() {
@ -82,12 +94,11 @@ function handlerResults(
value: any,
configOptions: DefineAppConfigOptions,
): DeepPartial<DefineAppConfigOptions> {
const { themeColor, theme, sidebar, header } = configOptions
const { themeColor, sidebar, header } = configOptions
switch (event) {
case HandlerSettingEnum.CHANGE_LAYOUT:
// eslint-disable-next-line no-case-declarations
{
const { mode, type, split } = value
// eslint-disable-next-line no-case-declarations
const splitOpt = split === undefined ? { split } : {}
return {
navBarMode: type,
@ -97,19 +108,17 @@ function handlerResults(
},
sidebar: { collapsed: false },
}
}
// TODO remove it
case HandlerSettingEnum.CHANGE_THEME_COLOR:
if (unref(themeColor) === value)
return {}
// changeTheme(value);
return { themeColor: value }
// TODO remove it
case HandlerSettingEnum.CHANGE_THEME:
if (unref(theme) === value)
return {}
return { theme: value ? ThemeEnum.DARK : ThemeEnum.LIGHT }
return { theme: value }
case HandlerSettingEnum.MENU_HAS_DRAG:
return { menu: { canDrag: value } }
@ -137,8 +146,9 @@ function handlerResults(
case HandlerSettingEnum.MENU_THEME:
// updateSidebarBgColor(value);
if (unref(sidebar).bgColor === value)
if (unref(sidebar).bgColor === value) {
return {}
}
return { sidebar: { bgColor: value } }
case HandlerSettingEnum.MENU_SPLIT:
@ -219,8 +229,9 @@ function handlerResults(
// ============header==================
case HandlerSettingEnum.HEADER_THEME:
// updateHeaderBgColor(value);
if (unref(header).bgColor === value)
if (unref(header).bgColor === value) {
return {}
}
return { header: { bgColor: value } }
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]
@ -22,13 +31,14 @@ export function useI18n(namespace?: string) {
return normalFn
}
const { t, ...other } = i18n.global
const tFn = (key: string, ...arg: any[]) => {
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
if (!key) {
return ''
}
if (!key.includes('.') && !namespace) {
return key
}
// @ts-expect-error no-err
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
}
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() {
const menuStore = useMenuStore()
const userStore = useUserStore()
const { resolve } = useRouter()
const routeStore = useRouteStore()
const { initMenu, menuListRef } = storeToRefs(routeStore)
/** 判断给定的route中是否具有给定perms列表的权限 */
function hasPermission(route: RouteRecordRaw, perms: any[] = []) {
if (!route.meta?.perm) {
return true
const getMenuList = async () => {
if (!unref(initMenu)) {
await routeStore.buildMenu()
}
// 递归寻找子节点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,
)
const menuList = unref(menuListRef)
return filterTree(menuList, hideFilter)
}
// 过滤掉所有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
}, [])
const getCurrentParentPath = async (currentPath: string) => {
const menus = await getMenuList()
const allParentPath = await getAllParentPath(menus, currentPath)
return allParentPath?.[0]
}
async function generateRoutes() {
const perms = userStore.userInfo?.perms ?? ['role', 'role/post']
menuStore.menuList = filterAsyncRoutes(getRoutes(), perms)
const getShallowMenus = async () => {
const menus = await getMenuList()
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[]) {
return routes.reduce((rs: RouteRecordRaw[], route) => {
if (!route.meta?.hidden && !route.meta?.hideInMenu) {
rs.push({
...route,
path: resolve(<string>route.redirect || route).path,
children: route.children ? getMenuList(route.children) : [],
})
}
return rs
}, []).sort((a, b) => {
return (a.meta?.order || 999) - (b.meta?.order || 999)
})
const getChildrenMenus = async (parentPath: string) => {
const menus = await getMenuList()
const parent = menus.find(item => item.path === parentPath)
if (!parent || !parent.children || !!parent?.meta?.hideChildrenInMenu) {
return [] as Menu[]
}
// if (isRoleMode()) {
// const routes = router.getRoutes()
// return filterTree(parent.children, basicFilter(routes))
// }
return parent.children
}
const menuList = computed(() => getMenuList(menuStore.menuList))
return {
menuList,
generateRoutes,
getMenuList,
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 {
MenuModeEnum,
NavBarModeEnum,
SIDE_BAR_MINI_WIDTH,
SIDE_BAR_SHOW_TIT_MINI_WIDTH,
TriggerEnum,
@ -8,59 +10,66 @@ const mixSideHasChildren = ref(false)
export function useMenuSetting() {
const { getFullContent: fullContent } = useFullContent()
const configStore = useAppConfigStore()
const appConfig = useAppConfig()
const { getShowLogo } = useRootSetting()
const getCollapsed = computed(() => configStore.sidebar.collapsed)
const getMenuType = computed(() => configStore.navBarMode)
const getMenuMode = computed(() => configStore.menu.mode)
const getMenuFixed = computed(() => configStore.sidebar.fixed)
const getShowMenu = computed(() => configStore.menu.show)
const getMenuHidden = computed(() => !configStore.sidebar.visible)
const getMenuWidth = computed(() => configStore.sidebar.width)
const getTrigger = computed(() => configStore.sidebar.trigger)
const getMenuTheme = computed(() => configStore.sidebar.theme)
const getSplit = computed(() => configStore.menu.split)
const getMenuBgColor = computed(() => configStore.sidebar.bgColor)
const getMixSideTrigger = computed(() => configStore.menu.mixSideTrigger)
const getCollapsed = computed(() => appConfig.sidebar.value.collapsed)
const getMenuType = computed(() => unref(appConfig.navBarMode))
const getMenuMode = computed(() => appConfig.menu.value.mode)
const getMenuFixed = computed(() => appConfig.sidebar.value.fixed)
const getShowMenu = computed(() => appConfig.menu.value.show)
const getMenuHidden = computed(() => !appConfig.sidebar.value.visible)
const getMenuWidth = computed(() => appConfig.sidebar.value.width)
const getTrigger = computed(() => appConfig.sidebar.value.trigger)
const getMenuTheme = computed(() => appConfig.sidebar.value.theme)
const getSplit = computed(() => appConfig.menu.value.split)
const getMenuBgColor = computed(() => appConfig.sidebar.value.bgColor)
const getMixSideTrigger = computed(() => appConfig.menu.value.mixSideTrigger)
const getShowSidebar = computed(() => {
return (
unref(getSplit)
|| (unref(getShowMenu)
&& !unref(configStore.isHorizontal)
&& !unref(appConfig.isHorizontal)
&& !unref(fullContent))
)
})
const getCanDrag = computed(() => configStore.menu.canDrag)
const getAccordion = computed(() => configStore.menu.accordion)
const getMixSideFixed = computed(() => configStore.menu.mixSideFixed)
const getTopMenuAlign = computed(() => configStore.menu.topMenuAlign)
const getCloseMixSidebarOnChange = computed(() => configStore.closeMixSidebarOnChange)
const getIsSidebarType = computed(() => configStore.isSidebar)
const getIsTopMenu = computed(() => configStore.isTopMenu)
const getCanDrag = computed(() => appConfig.menu.value.canDrag)
const getAccordion = computed(() => appConfig.menu.value.accordion)
const getMixSideFixed = computed(() => appConfig.menu.value.mixSideFixed)
const getTopMenuAlign = computed(() => appConfig.menu.value.topMenuAlign)
const getCloseMixSidebarOnChange = computed(() => appConfig.closeMixSidebarOnChange.value)
const getIsSidebarType = computed(() => unref(getMenuType) === NavBarModeEnum.SIDEBAR)
const getIsTopMenu = computed(() => unref(getMenuType) === NavBarModeEnum.TOP_MENU)
const getMenuShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType))
const getCollapsedShowTitle = computed(() => configStore.isCollapsedShowTitle)
const getShowTopMenu = computed(() => unref(configStore.isHorizontal) || unref(getSplit))
const getCollapsedShowTitle = computed(() => appConfig.menu.value.collapsedShowTitle)
const getShowTopMenu = computed(() => unref(getMenuMode) === MenuModeEnum.HORIZONTAL || unref(getSplit))
const getShowHeaderTrigger = computed(() => {
if (
configStore.isTopMenu
unref(getMenuType) === NavBarModeEnum.TOP_MENU
|| !unref(getShowMenu)
|| unref(getMenuHidden)
)
) {
return false
}
return unref(getTrigger) === TriggerEnum.HEADER
})
const getShowCenterTrigger = computed(() => unref(getTrigger) === TriggerEnum.CENTER)
const getShowFooterTrigger = computed(() => unref(getTrigger) === TriggerEnum.FOOTER)
const getIsHorizontal = computed(() => configStore.isHorizontal)
const getIsMixSidebar = computed(() => configStore.isMixSidebar)
const getIsMixMode = computed(() => configStore.isMixMode)
const getIsHorizontal = computed(() => unref(getMenuMode) === MenuModeEnum.HORIZONTAL)
const getIsMixSidebar = computed(() => unref(getMenuType) === NavBarModeEnum.MIX_SIDEBAR)
const getIsMixMode = computed(() => {
return (
unref(getMenuMode) === MenuModeEnum.INLINE
&& unref(getMenuType) === NavBarModeEnum.MIX
)
})
const getMiniWidthNumber = computed(() => {
const { collapsedShowTitle } = configStore.menu
const { collapsedShowTitle } = appConfig.menu.value
return collapsedShowTitle
? SIDE_BAR_SHOW_TIT_MINI_WIDTH
: SIDE_BAR_MINI_WIDTH
@ -93,14 +102,18 @@ export function useMenuSetting() {
return `calc(100% - ${unref(width)}px)`
})
const getIsFixed = computed(() => {
return unref(getMixSideFixed) && unref(mixSideHasChildren)
})
function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
configStore.setMenu(menuSetting)
appConfig.setAppConfig({ menu: menuSetting })
}
function setSidebarSetting(
sidebarSetting: Partial<SidebarConfigOptions>,
): void {
configStore.setSidebar(sidebarSetting)
appConfig.setAppConfig({ sidebar: sidebarSetting })
}
function setSiderWidth(width: number) {
@ -148,6 +161,7 @@ export function useMenuSetting() {
getCloseMixSidebarOnChange,
getMixSideTrigger,
getMixSideFixed,
getIsFixed,
mixSideHasChildren,
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',
}
/**
*
*/
export enum PermissionModeEnum {
// 角色权限模式
ROLE = 'role',
// 权限代码模式
PERM = 'perm',
}
// Session过期处理方式
export enum SessionTimeoutProcessingEnum {
ROUTE_JUMP, // 路由跳转
@ -54,3 +64,17 @@ export enum ErrorTypeEnum {
AJAX = 'ajax',
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',
]
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
export enum HandlerSettingEnum {
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">
import { NIcon } from 'naive-ui'
import type { RouteLocationMatched } from 'vue-router'
import { REDIRECT_NAME } from '~/constants'
import { filterTree, isString } from '~/utils'
import { filterTree, getAllParentPath, isString } from '~/utils'
const { header } = useAppConfig()
const { currentRoute } = useRouter()
const { t } = useI18n()
const go = useGo()
const routes = $ref<RouteLocationMatched[]>([])
let routes = $ref<RouteLocationMatched[]>([])
const { renderIcon } = useIcon()
// const { menuList } = useMenu()
const { getMenuList } = useMenu()
watchEffect(async () => {
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]
// let path = currentRoute.value.path
// if(cur && cur?.meta?.currentActiveMenu) {
// path = cur.meta.currentActiveMenu as string
// }
// const parent =
const parent = getAllParentPath(menus, path)
const filterMenus = menus.filter(item => item.path === parent[0])
const matched = getMatched(filterMenus, parent) as any
if (!matched || matched.length === 0) {
return
}
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[]) {
@ -64,15 +76,11 @@ function renderDropdownLabel(route: any) {
}
function renderDropdownIcon(option: any) {
return option.icon ? renderIcon(option.icon)() : null
}
function renderIcon(icon: string) {
return () => h(NIcon, null, { default: () => h('div', { [icon]: '', class: icon }) })
return option.icon ? renderIcon(option.icon) : null
}
function handleClick(path: string, route: Recordable<any>) {
const { children, meta, redirect } = route
const { children, redirect } = route
if (children?.length && !redirect) {
return
}
@ -92,16 +100,21 @@ function handleClick(path: string, route: Recordable<any>) {
<NBreadcrumb v-if="header.showBreadCrumb">
<NBreadcrumbItem v-for="(route, index) in routes" :key="index">
<NDropdown
key-field="path" size="small" :options="route.children" :render-label="renderDropdownLabel"
:render-icon="renderDropdownIcon" @select="handleClick"
key-field="path"
size="small"
:options="route.children"
:render-label="renderDropdownLabel"
:render-icon="renderDropdownIcon"
@select="handleClick"
>
<NSpace align="center" :size="0">
<div
v-if="route.meta.icon && header.showBreadCrumbIcon" class="v-middle"
:class="`i-${route.meta.icon}`"
<LIcon
v-if="route.meta.icon && header.showBreadCrumbIcon"
class="v-middle"
:icon="route.meta.icon"
/>
<span class="ml-1.2 mr-1.2">{{ $t(route.meta.title ?? '') }}</span>
<div v-if="route.children" class="i-gridicons:dropdown" />
<span class="ml-1.2 mr-1.2">{{ $t(route.meta.title || '') }}</span>
<LIcon v-if="route.children" icon="ant-design:caret-down-filled" />
</NSpace>
</NDropdown>
</NBreadcrumbItem>

@ -21,10 +21,10 @@ const style = computed(() => (
<template>
<footer :class="bem()" :style="style">
<div class="flex items-center justify-center">
<template v-for="(item, index) in links" :key="index">
<NButton text tag="a" :href="item.url" target="_blank" class="mx-1">
<template v-for="(item, _index) in links" :key="_index">
<NButton text tag="a" :href="item.url" target="_blank" class="mx-1!">
<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>
</span>
</NButton>
@ -35,3 +35,15 @@ const style = computed(() => (
</NText>
</footer>
</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 BreadCrumb from './breadcrumb/index.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 { createNamespace } from '~/utils'
const {
isTopMenu,
@ -13,8 +16,9 @@ const {
const {
getShowHeader,
getShowHeaderLogo,
getShowFullScreen,
getShowFullHeaderRef,
getShowLocalePicker,
} = useHeaderSetting()
const {
@ -24,6 +28,8 @@ const {
const { getShowSettingButton, getSettingButtonPosition } = useRootSetting()
const { getShowMultipleTab } = useMultipleTabSetting()
const getShowSetting = computed(() => {
if (!unref(getShowSettingButton)) {
return false
@ -35,50 +41,105 @@ const getShowSetting = computed(() => {
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
const logoWidth = computed(() => (unref(isTopMenu) ? 150 : getMenuWidth))
const { bem } = createNamespace('layout-header')
const { isFullscreen } = useFullscreen()
</script>
<template>
<NSpace vertical>
<NSpace :class="bem()" vertical>
<NSpace
v-if="getShowFullHeaderRef" class="h-48px shadow" :class="[{ 'mb-8px': !false }]"
:style="{ '--un-shadow-color': 'var(--n-border-color)' }" justify="space-between" align="center"
v-if="getShowFullHeaderRef"
class="h-48px shadow"
:class="[{ 'mb-8px': !getShowHeaderMultipleTab }]"
:style="{ '--un-shadow-color': 'var(--n-border-color)' }"
justify="space-between"
align="center"
>
<slot name="logo">
<NSpace align="center" class="items-center" :size="0">
<Logo
v-if="getShowHeaderLogo" :style="{
v-if="showHeaderLogoRef"
:style="{
width: `${logoWidth}px`,
maxWidth: `${logoWidth}px`,
}"
/>
<HeaderTrigger v-if="getShowHeaderTrigger" class="mx-2" />
<slot name="breadcrumb">
<BreadCrumb v-if="!(isTopMenu || (isMix && menu.split))" />
<slot v-if="showHeaderBreadcrumb" name="breadcrumb">
<BreadCrumb />
</slot>
</NSpace>
</slot>
<slot name="menu" />
<div class="pl-8px pr-8px">
<slot name="buttons">
<NSpace class="p-1" :size="16" align="center">
<!-- Search -->
<!-- Notify -->
<!-- FullScreen -->
<!-- LocalePicker -->
<!-- UserDropDown -->
<SettingButton v-if="getShowSetting" />
</NSpace>
</slot>
<slot v-if="!showHeaderBreadcrumb" name="menu" />
<div class="pl-8px" :class="bem('action')">
<NSpace :size="0" align="center" :wrap-item="false">
<!-- Search -->
<!-- Notify -->
<NPopover trigger="hover">
<template #trigger>
<Fullscreen v-if="getShowFullScreen" :class="bem('action__item')" />
</template>
<span>{{ unref(isFullscreen) ? $t('layout.header.tooltipExitFull') : $t('layout.header.tooltipEntryFull') }}</span>
</NPopover>
<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>
</NSpace>
<!-- Multiple tab -->
<template v-if="false">
<slot name="tags">
<!-- LayoutTabs -->
<template v-if="getShowHeaderMultipleTab">
<slot name="tabs">
<LayoutTabs />
</slot>
</template>
</NSpace>
</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">
import { BASIC_HOME_PATH } from '~/constants'
import { createNamespace } from '~/utils'
const props = defineProps({
title: { type: String, default: '' },
showTitle: {
type: Boolean,
default: true,
},
showTitle: { type: Boolean, default: true },
homePath: { type: String, default: BASIC_HOME_PATH },
})
const { getCollapsed } = useMenuSetting()
const { push } = useRouter()
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>
<template>
<div class="logo">
<img :src="logo" alt="" :class="{ 'mr-2': !getCollapsed }">
<h2 v-show="!getCollapsed && props.showTitle" class="title">
<div v-if="getIsShowLogo" :class="bem()" @click="goHome">
<img :src="`/${logo}`" alt="logo">
<div v-show="showTitle" class="ml-2 truncate md:opacity-100" :class="bem('title')">
{{ title }}
</h2>
</div>
</div>
</template>
<style lang="less" scoped>
.logo {
display: flex;
align-items: center;
justify-content: center;
height: 64px;
line-height: 64px;
overflow: hidden;
white-space: nowrap;
.app-logo {
display: flex;
align-items: center;
padding-left: 7px;
cursor: pointer;
transition: all 0.2s ease;
height: 48px;
background: transparent;
box-sizing: border-box;
img {
width: auto;
height: 32px;
width: 32px;
}
.title {
margin: 0;
&__title {
font-size: 16px;
font-weight: 700;
transition: all 0.5s;
line-height: normal;
}
}
</style>

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

@ -1,11 +1,11 @@
<script setup lang="ts" name="LayoutMenu">
import type { MenuInst } from 'naive-ui'
import { NIcon } from 'naive-ui'
import type { RouteLocationNormalizedLoaded } 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 FooterTrigger from '../trigger/footer-trigger.vue'
import { createNamespace, listenerRouteChange, mapTree } from '~/utils'
import { createNamespace, emitter, listenerRouteChange, mapTree } from '~/utils'
import { REDIRECT_NAME } from '~/constants'
const props = withDefaults(defineProps<{
@ -16,7 +16,7 @@ const props = withDefaults(defineProps<{
split: false,
})
const title = import.meta.env.VITE_APP_TITLE
const { isSidebarDark } = useAppTheme()
const {
menu,
@ -24,19 +24,23 @@ const {
getCollapsedShowTitle,
sidebar,
isSidebar,
isMix,
} = useAppConfig()
const { getTopMenuAlign, getShowFooterTrigger } = useMenuSetting()
const showSidebarLogo = computed(() => {
return unref(isSidebar) || unref(isMixSidebar)
})
const {
getTopMenuAlign,
getShowFooterTrigger,
getCollapsed,
} = useMenuSetting()
const { bem } = createNamespace('layout-menu')
const { t } = useI18n()
const { currentRoute } = useRouter()
const { renderIcon } = useIcon()
const menuRef = $ref<MenuInst | null>(null)
let menuListRef = $ref<any[]>([])
const options = shallowRef<MenuMixedOption[]>([])
let menuListRef = $ref<Menu[]>([])
let activeKey = $ref<any>()
const getMenuCollapsed = computed(() => {
@ -46,6 +50,10 @@ const getMenuCollapsed = computed(() => {
return unref(sidebar).collapsed
})
const showSidebarLogo = computed(() => {
return unref(isSidebar) || unref(isMixSidebar)
})
//
function showOption() {
nextTick(() => {
@ -56,12 +64,21 @@ function showOption() {
})
}
const { menuList, generateRoutes } = useMenu()
// TODO
const { getMenuList } = useMenu()
onMounted(async () => {
await generateRoutes()
menuListRef = mapTree<any>(unref(menuList), { conversion: menu => routeToOption(menu) })
showOption()
const menuList = await getMenuList()
menuListRef = mapTree(menuList, { conversion: menu => menu })
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) => {
@ -74,73 +91,140 @@ listenerRouteChange((route: RouteLocationNormalizedLoaded) => {
activeKey = currentActiveMenu
}
showOption()
})
}, false)
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
const menu = route || unref(currentRoute)
activeKey = menu.name
// keykey
function flatten(arr: Menu[], prefix: string = '') {
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
}
//
function routeToOption(item: RouteRecordRaw) {
const { name, children, meta: metaRef, component } = item
const meta = unref(metaRef)
const title = meta?.title ? t(meta.title) : ''
// key
function findTopKey(flattened: any, key: string) {
return Object.keys(flattened)
.find(k => k.endsWith(key))
?.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 {
label: () => {
if (!component) {
return title
}
if (children && children.length > 0) {
return title
}
// children使RouterLinkname
return h(
RouterLink,
{
to: {
name,
},
to: { name },
},
{ default: () => title },
)
},
// keyname
key: name,
// iconrenderIconicon
icon: () => {
if (!meta?.icon)
return true
return h(NIcon, null, { default: () => h('div', { [icon]: '', class: icon }) })
return renderIcon({ icon })
},
}
}
// function clickMenu(key: any) {
// if (unref(isTopMenu) && menu.value.split && !props.split) {
// // emit
// }
// }
function clickMenu(key: any) {
if (unref(isMix) && unref(menu).split && !props.split) {
// emit
emitter.emit('menuChange', {
name: activeKey,
options: menuListRef.find(v => v.key === key)?.children,
})
}
}
</script>
<template>
<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')">
<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}`,
}" :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>
<FooterTrigger v-if="getShowFooterTrigger" />
</div>
</template>
<style lang="less" scoped>
div:has(> div[class='layout-menu']) {
flex: 1;
}
.layout-menu {
display: flex;
flex-direction: column;
@ -155,20 +239,20 @@ function routeToOption(item: RouteRecordRaw) {
flex-basis: auto;
}
&__menu:not(.n-menu--collapsed) {
.n-menu-item-content {
&::before {
left: 5px;
right: 5px;
}
// &__menu:not(.n-menu--collapsed) {
// .n-menu-item-content {
// &::before {
// left: 5px;
// right: 5px;
// }
&.n-menu-item-content--selected,
&:hover {
&::before {
border-left: 4px solid var(--primary-color);
}
}
}
}
// &.n-menu-item-content--selected,
// &:hover {
// &::before {
// border-left: 4px solid var(--primary-color);
// }
// }
// }
// }
}
</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()
const { getShowHeader } = useHeaderSetting()
const { baseHandler } = useAppConfig()
function eventHandler(evt: HandlerSettingEnum, val: boolean) {
baseHandler(evt, val)
}
</script>
<template>
<NSpace vertical>
<SwitchItem
:title="$t('layout.setting.breadcrumb')" :def="getShowBreadCrumb"
:event="HandlerSettingEnum.SHOW_BREADCRUMB" :disabled="!getShowHeader"
:title="$t('layout.setting.breadcrumb')"
: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
:title="$t('layout.setting.breadcrumbIcon')" :def="getShowBreadCrumbIcon"
:event="HandlerSettingEnum.SHOW_BREADCRUMB_ICON" :disabled="!getShowBreadCrumb"
:title="$t('layout.setting.tabs')"
:value="getShowMultipleTab"
:callback="v => eventHandler(HandlerSettingEnum.TABS_SHOW, v)"
/>
<SwitchItem :title="$t('layout.setting.tabs')" :def="getShowMultipleTab" :event="HandlerSettingEnum.TABS_SHOW" />
<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"
/>
<SwitchItem
:title="$t('layout.setting.tabsQuickBtn')" :def="getShowQuick"
:event="HandlerSettingEnum.TABS_SHOW_QUICK" :disabled="!getShowMultipleTab"
:title="$t('layout.setting.tabsQuickBtn')"
:value="getShowQuick"
:callback="v => eventHandler(HandlerSettingEnum.TABS_SHOW_QUICK, v)"
:disabled="!getShowMultipleTab"
/>
<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"
/>
<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"
/>
<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
:title="$t('layout.setting.fullContent')" :def="getFullContent"
:event="HandlerSettingEnum.FULL_CONTENT"
:title="$t('layout.setting.header')"
: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>
</template>

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

@ -46,49 +46,73 @@ const some = triggerOptions.some(item => item.value === unref(getTrigger))
if (!some) {
setMenuSetting({ trigger: TriggerEnum.FOOTER })
}
const { baseHandler } = useAppConfig()
function eventHandler(evt: HandlerSettingEnum, val: boolean) {
baseHandler(evt, val)
}
</script>
<template>
<NSpace vertical>
<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"
/>
<SwitchItem
:title="$t('layout.setting.mixSidebarFixed')" :def="getMixSideFixed"
:event="HandlerSettingEnum.MENU_FIXED_MIX_SIDEBAR" :disabled="!getIsMixSidebar"
:title="$t('layout.setting.mixSidebarFixed')"
:value="getMixSideFixed"
:callback="v => eventHandler(HandlerSettingEnum.MENU_FIXED_MIX_SIDEBAR, v)"
:disabled="!getIsMixSidebar"
/>
<SwitchItem
:title="$t('layout.setting.closeMixSidebarOnChange')" :def="getCloseMixSidebarOnChange"
:event="HandlerSettingEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE" :disabled="!getIsMixSidebar"
:title="$t('layout.setting.closeMixSidebarOnChange')"
:value="getCloseMixSidebarOnChange"
:callback="v => eventHandler(HandlerSettingEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE, v)"
:disabled="!getIsMixSidebar"
/>
<SwitchItem
:title="$t('layout.setting.menuCollapse')" :def="getCollapsed"
:event="HandlerSettingEnum.MENU_COLLAPSED" :disabled="!getShowMenuRef"
:title="$t('layout.setting.menuCollapse')"
:value="getCollapsed"
:callback="v => eventHandler(HandlerSettingEnum.MENU_COLLAPSED, v)"
:disabled="!getShowMenuRef"
/>
<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"
/>
<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"
/>
<SwitchItem
:title="$t('layout.setting.menuAccordion')" :def="getAccordion"
:event="HandlerSettingEnum.MENU_ACCORDION" :disabled="!getShowMenuRef"
:title="$t('layout.setting.menuAccordion')"
:value="getAccordion"
:callback="v => eventHandler(HandlerSettingEnum.MENU_ACCORDION, v)"
:disabled="!getShowMenuRef"
/>
<SwitchItem
:title="$t('layout.setting.collapseMenuDisplayName')" :def="getCollapsedShowTitle"
:event="HandlerSettingEnum.MENU_COLLAPSED_SHOW_TITLE"
:title="$t('layout.setting.collapseMenuDisplayName')"
:value="getCollapsedShowTitle"
:callback="v => eventHandler(HandlerSettingEnum.MENU_COLLAPSED_SHOW_TITLE, v)"
:disabled="!getShowMenuRef || !getCollapsed || getIsMixSidebar"
/>
<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"
/>
<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"
/>
<SelectItem

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

@ -27,7 +27,7 @@ function handlerPicker(data: any) {
<template>
<div class="setting-navigation-bar-picker">
<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">
<template #trigger>
<div

@ -7,10 +7,12 @@ const { contentRef } = useLayout()
<template>
<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"
>
<div class="i-ion:settings-outline hover:cursor-pointer" />
<LIcon icon="ant-design:setting-outlined" class="hover:cursor-pointer" />
</NAffix>
<SettingDrawer $visible="visible" />
</template>

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

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

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

@ -1,52 +1,63 @@
<script setup lang="ts" name="ThemeColorPicker">
import type { HandlerSettingEnum } from '~/constants'
import { ThemeChangeEnum } from '~/constants'
const props = defineProps({
colorList: {
type: Array as PropType<string[]>,
default: () => [],
},
title: { type: String, default: '' },
event: {
type: Number as PropType<HandlerSettingEnum>,
type: Number as PropType<ThemeChangeEnum>,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
def: {
type: String,
default: '',
},
})
const { baseHandler } = useAppConfig()
const { setThemeConfig } = useAppTheme()
const color = $ref(props.def)
function handlerClick(color: any) {
baseHandler(props.event, color)
function onChange(color: string) {
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>
<template>
<div class="theme-color-picker">
<NSpace justify="space-between" :size="0" :wrap="false">
<template v-for="color in colorList" :key="color">
<span
class="color-item box-border inline-block h-20px w-20px cursor-pointer border border-gray-300 rounded-sm"
:class="{ active: def === color }"
:style="{ background: color }" @click="handlerClick(color)"
>
<NSpace v-if="def === color" justify="center">
<div class="i-ant-design:check-outlined text-white hover:text-#d1d5db" />
</NSpace>
</span>
</template>
</NSpace>
<div class="min-w-83px">
<NColorPicker
v-model:value="color"
:show-alpha="false"
:modes="['hex', 'rgb']"
:render-label="() => title"
:swatches="colorList"
@complete="onChange"
@update:value="onChange"
/>
</div>
</template>
<style lang="less" scoped>
.color-item {
&:hover,
&.active {
border-color: rgba(6, 96, 189, 1);
}
}
</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,
getOpenNProgress,
} = useTransitionSetting()
const { baseHandler } = useAppConfig()
function eventHandler(evt: HandlerSettingEnum, val: boolean) {
baseHandler(evt, val)
}
</script>
<template>
<NSpace vertical>
<SwitchItem
:title="$t('layout.setting.progress')" :def="getOpenNProgress"
:event="HandlerSettingEnum.OPEN_PROGRESS"
:title="$t('layout.setting.progress')"
:value="getOpenNProgress"
:callback="v => eventHandler(HandlerSettingEnum.OPEN_PROGRESS, v)"
/>
<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"
/>
<SwitchItem
:title="$t('layout.setting.switchAnimation')" :def="getEnableTransition"
:event="HandlerSettingEnum.OPEN_ROUTE_TRANSITION"
:title="$t('layout.setting.switchAnimation')"
:value="getEnableTransition"
:callback="v => eventHandler(HandlerSettingEnum.OPEN_ROUTE_TRANSITION, v)"
/>
<SelectItem
: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>
<div :class="bem()" class="hover:cursor-pointer" @click="toggleCollapsed">
<div v-if="getCollapsed" class="i-ant-design-menu-unfold-outlined" />
<div v-else class="i-ant-design-menu-fold-outlined" />
<LIcon v-if="getCollapsed" icon="ant-design:menu-unfold-outlined" />
<LIcon v-else icon="ant-design:menu-fold-outlined" />
</div>
</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>
<div :class="bem()" @click.stop="toggleCollapsed">
<i v-if="getCollapsed" i-ph-caret-double-right-bold />
<i v-else i-ph-caret-double-left-bold />
<LIcon v-if="getCollapsed" icon="ant-design:double-right-outlined" />
<LIcon v-else icon="ant-design:double-left-outlined" />
</div>
</template>

@ -1,5 +1,8 @@
<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'
// TODO lockScreen
@ -9,14 +12,21 @@ const layout = computed<ReturnType<typeof defineComponent>>(() => {
// TODO appInject mobile
switch (getMenuType.value) {
case NavBarModeEnum.SIDEBAR:
return LeftMenu
return LeftMenuLayout
case NavBarModeEnum.MIX:
return TopMenuMixLayout
case NavBarModeEnum.TOP_MENU:
return TopMenuLayout
case NavBarModeEnum.MIX_SIDEBAR:
return MixMenuLayout
default:
return LeftMenu
return undefined
}
})
nextTick(() => {
createThemeColorListen()
})
</script>
<template>

@ -3,7 +3,9 @@ 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 SiderDragBar from './components/trigger/sider-dragbar.vue'
import SiderDragBar from './components/sider/sider-dragbar.vue'
// import { useDragLine } from './components/sider/drag'
defineOptions({
name: 'LeftMenuLayout',
@ -13,6 +15,7 @@ const {
getCollapsed,
getShowCenterTrigger,
setSiderWidth,
getCanDrag,
} = useMenuSetting()
const {
@ -26,20 +29,35 @@ const {
const {
toggleCollapse,
sidebar,
footer,
} = useAppConfig()
const { getShowFooter } = useRootSetting()
const { isSidebarDark } = useAppTheme()
// useDragLine(siderRef, dragBarRef)
</script>
<template>
<NLayout has-sider class="h-full">
<NLayoutSider
v-if="getShowMenu" :show-trigger="getShowCenterTrigger" bordered
:collapsed-width="sidebar.collapsedWidth" :width="sidebar.width" collapse-mode="width" :collapsed="getCollapsed"
v-if="getShowMenu"
:show-trigger="getShowCenterTrigger"
bordered
:collapsed-width="sidebar.collapsedWidth"
:width="sidebar.width"
collapse-mode="width"
:collapsed="getCollapsed"
:inverted="!!isSidebarDark"
@update:collapsed="toggleCollapse"
>
<slot name="sider">
<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 />
</div>
</slot>
@ -56,7 +74,7 @@ const {
<slot name="main" />
</LayoutMain>
</NLayoutContent>
<NLayoutFooter v-if="footer.show" ref="footerRef">
<NLayoutFooter v-if="getShowFooter" ref="footerRef">
<slot name="footer">
<LayoutFooter />
</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 AppLoading from './AppLoading.vue'
import '@unocss/reset/tailwind.css'
import './styles/main.css'
import 'uno.css'
import './styles'
import { setupPinia } from './modules/pinia'
import { setupI18n } from './modules/i18n'
import { initRouter, setupRouterGuards } from './modules/router/router'
import { setupPWA } from './modules/pwa'
import 'virtual:svg-icons-register'
let meta = document.createElement('meta')
meta.name = 'naive-ui-style'
@ -17,16 +19,24 @@ meta.name = 'vueuc-style'
document.head.appendChild(meta)
; (async () => {
// TODO loadingApp
const appLoading = createApp(AppLoading)
// setupPinia(appLoading)
// setup I18n
// await setupI18n(appLoading)
appLoading.mount('#appLoading')
const app = createApp(App)
setupPinia(app)
// initApplication
app.use(createHead())
// setup I18n
await setupI18n(app)
// Router
const router = initRouter(import.meta.env.VITE_BASE_URL)
const router = await initRouter(import.meta.env.VITE_BASE_URL)
app.use(router)
// Router guards
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 { createPersistedState } from 'pinia-plugin-persistedstate'
import type { App } from 'vue'
import { resetSetupStorePlugin } from './reset'
const pinia = createPinia()
// setup-syntax $reset plugin
pinia.use(resetSetupStorePlugin)
// 持久化插件(localStorage)
pinia.use(createPersistedState({
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 function initRouter(path: string): Router {
const routeStore = useRouteStore()
const routes = routeStore.initRoutes()
router = createRouter({
history: createWebHistory(path),
routes: setupLayouts(getRoutes()),
strict: false,
routes: setupLayouts(routes),
scrollBehavior: () => ({ left: 0, top: 0 }),
})
return router
}
@ -23,5 +27,5 @@ export function setupRouterGuards() {
createNProgressGuard(router)
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">
meta:
title: menu.readme
order: 3
icon: carbon:book
</route>
## File-based Routing

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

@ -2,14 +2,75 @@
defineOptions({
name: 'IndexPage',
})
// useHead({
// title: '',
// })
useHead({
title: '首页',
})
</script>
<template>
<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>
</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">
<NInput $value="formModelRef.subject" clearable :placeholder="$t('auth.login.account.subject.placeholder')">
<template #prefix>
<i i-carbon-user />
<LIcon icon="carbon-user" />
</template>
</NInput>
</NFormItem>
@ -50,7 +50,7 @@ function submit() {
show-password-on="click" type="password"
>
<template #prefix>
<i i-carbon-locked />
<LIcon icon="carbon-locked" />
</template>
</NInput>
</NFormItem>
@ -69,5 +69,5 @@ function submit() {
<route lang="yaml">
meta:
hidden: true
ignore: true
</route>

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

@ -9,7 +9,7 @@ useHead({
</script>
<template>
<AuthLayout :login="true" to="/register">
<LAuthLayout :login="true" to="/register">
<NTabs default-value="account" size="large" animated pane-wrapper-style="margin: 0 -4px">
<NTabPane name="account" :tab="$t('auth.login.accountLogin')">
<AccountLogin />
@ -26,12 +26,12 @@ useHead({
<div class="mt-5 text-xs tracking-widest">
{{ $t('auth.login.agreementTip') }}
</div>
</AuthLayout>
</LAuthLayout>
</template>
<route lang="yaml">
meta:
title: menu.login
hideInMenu: true
# layout: page-layout
layout: page-layout
</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">
<NInput $value="formModelRef.username" clearable :placeholder="$t('auth.register.account.username.placeholder')">
<template #prefix>
<i i-carbon-user />
<LIcon icon="carbon:user" />
</template>
</NInput>
</NFormItem>
@ -98,7 +98,7 @@ function submit() {
:placeholder="$t('auth.register.account.credentials.placeholder')" show-password-on="click" type="password"
>
<template #prefix>
<i i-carbon-locked />
<LIcon icon="carbon:locked" />
</template>
</NInput>
</template>
@ -110,7 +110,7 @@ function submit() {
:placeholder="$t('auth.register.account.credentials.placeholder2')" show-password-on="click" type="password"
>
<template #prefix>
<i i-carbon-locked />
<LIcon icon="carbon:locked" />
</template>
</NInput>
</NFormItem>
@ -124,7 +124,7 @@ function submit() {
:placeholder="$t('auth.register.account.phoneNumber.placeholder')" :allow-input="onlyAllowNumber"
>
<template #prefix>
<i i-carbon-phone />
<LIcon icon="carbon:phone" />
</template>
</NInput>
</NInputGroup>
@ -136,7 +136,7 @@ function submit() {
:placeholder="$t('auth.register.account.code.placeholder')"
>
<template #prefix>
<i i-carbon-two-factor-authentication />
<LIcon icon="carbon:two-factor-authentication" />
</template>
</NInput>
<NButton :disabled="!formModelRef.phoneNumber">
@ -153,5 +153,5 @@ function submit() {
<route lang="yaml">
meta:
hidden: true
ignore: true
</route>

@ -16,41 +16,41 @@ const props = withDefaults(defineProps<{
<NSpace vertical>
<div>
<span mr-1>
<i
<LIcon
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>{{ $t('auth.register.account.rule.r1') }}</span>
</div>
<div>
<span mr-1>
<i
<LIcon
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>{{ $t('auth.register.account.rule.r2') }}</span>
</div>
<div>
<span mr-1>
<i
<LIcon
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>{{ $t('auth.register.account.rule.r3') }}</span>
</div>
<div>
<span mr-1>
<i
<LIcon
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>{{ $t('auth.register.account.rule.r4') }}</span>
</div>
@ -60,5 +60,5 @@ const props = withDefaults(defineProps<{
<route lang="yaml">
meta:
hidden: true
ignore: true
</route>

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

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

29
src/shims.d.ts vendored

@ -19,32 +19,3 @@ declare module '*.vue' {
const component: DefineComponent<object, object, any>
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,
MixSidebarTriggerEnum,
NavBarModeEnum,
PermissionModeEnum,
PlacementEnum,
RouterTransitionEnum,
SIDE_BAR_BG_COLOR_LIST,
@ -28,7 +29,7 @@ const initState: DefineAppConfigOptions = {
// permissionCacheType: CacheTypeEnum.LOCAL,
settingButtonPosition: SettingButtonPositionEnum.AUTO,
openSettingDrawer: false,
// permissionMode: PermissionModeEnum.ROUTE_MAPPING,
permissionMode: PermissionModeEnum.PERM,
sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP,
grayMode: false,
colorWeak: false,
@ -117,21 +118,13 @@ export const useAppConfigStore = defineStore('APP_CONFIG', () => {
const isTopMenu = computed(() => state.navBarMode === NavBarModeEnum.TOP_MENU)
const isMixSidebar = computed(() => state.navBarMode === NavBarModeEnum.MIX_SIDEBAR)
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 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>) {
state = _merge(state, configs)
}
@ -169,7 +162,6 @@ export const useAppConfigStore = defineStore('APP_CONFIG', () => {
}
return {
$state: state,
...toRefs(state),
isSidebar,
isTopMenu,
@ -177,11 +169,9 @@ export const useAppConfigStore = defineStore('APP_CONFIG', () => {
isMix,
isMixMode,
isHorizontal,
isCollapsedShowTitle,
tagTarCache,
setAppConfig,
$reset,
// setter
setAppConfig,
setSidebar,
setMenu,
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',
key: TabActionEnum.REFRESH_PAGE,
icon: 'ion:reload-sharp',
icon: 'ant-design:reload-outlined',
disabled: refreshDisabled,
},
{
label: 'layout.multipleTab.close',
key: TabActionEnum.CLOSE_CURRENT,
icon: 'clarity:close-line',
icon: 'ant-design:close-outlined',
disabled: !!meta?.affix || disabled,
},
{
@ -350,13 +350,13 @@ export const useMultipleTabStore = defineStore('MULTIPLE_TAB', () => {
key: 'divider1',
},
{
icon: 'line-md:arrow-close-left',
icon: 'ant-design:vertical-right-outlined',
key: TabActionEnum.CLOSE_LEFT,
label: 'layout.multipleTab.closeLeft',
disabled: closeLeftDisabled,
},
{
icon: 'line-md:arrow-close-right',
icon: 'ant-design:vertical-left-outlined',
key: TabActionEnum.CLOSE_RIGHT,
label: 'layout.multipleTab.closeRight',
disabled: closeRightDisabled,
@ -366,7 +366,7 @@ export const useMultipleTabStore = defineStore('MULTIPLE_TAB', () => {
key: 'divider2',
},
{
icon: 'dashicons:align-center',
icon: 'ant-design:holder-outlined',
key: TabActionEnum.CLOSE_OTHER,
label: 'layout.multipleTab.closeOther',
disabled,
@ -374,7 +374,7 @@ export const useMultipleTabStore = defineStore('MULTIPLE_TAB', () => {
{
label: 'layout.multipleTab.closeAll',
key: TabActionEnum.CLOSE_ALL,
icon: 'clarity:minus-line',
icon: 'ant-design:line-outlined',
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',
links: [
{
icon: 'mdi:document',
label: t('layout.footer.onlineDocument'),
url: 'https://nadm.noahlan.cn/docs',
},
{
icon: 'uim:github',
icon: 'ant-design:github-filled',
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'
export const useUserStore = defineStore('USER', () => {
let userInfo = $ref<API.UserInfo>()
let authInfo = $ref<API.LoginResp>()
function $reset() {
userInfo = Object.assign({})
authInfo = Object.assign({})
}
const userInfo = ref<API.UserInfo>()
const authInfo = ref<API.LoginResp>()
return {
$reset,
userInfo,
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';
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
html {
overflow: hidden;
text-size-adjust: 100%;
}
html.dark {
background: #121212;
/* background: #121212; */
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 {
pointer-events: none;
}
#nprogress .bar {
background: rgb(13,148,136);
opacity: 0.75;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
z-index: 99999;
width: 100%;
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,
MixSidebarTriggerEnum,
NavBarModeEnum,
PermissionModeEnum,
PlacementEnum,
RouterTransitionEnum,
SessionTimeoutProcessingEnum,
@ -36,8 +37,8 @@ declare global {
removeAllHttpPending: boolean
// // Storage location of permission related information
// permissionCacheType: CacheTypeEnum
// // Permission mode
// permissionMode: PermissionModeEnum
// Permission mode
permissionMode: PermissionModeEnum
// Configure where the button is displayed
settingButtonPosition: SettingButtonPositionEnum
// Configuration setting drawer open
@ -158,4 +159,14 @@ declare global {
dropdownPlacement: string
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