diff --git a/.npmrc b/.npmrc index 68fbcd57..b07f5c51 100644 --- a/.npmrc +++ b/.npmrc @@ -6,3 +6,4 @@ public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier* public-hoist-pattern[]=*vite-plugin-mock* public-hoist-pattern[]=vite +strict-peer-dependencies=false diff --git a/apps/admin/package.json b/apps/admin/package.json index 85ebc47f..0fb2b389 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -32,8 +32,8 @@ "@vben/hooks": "workspace:*", "@vben/layouts": "workspace:*", "@vben/locale": "workspace:*", - "@vben/pinia": "workspace:*", "@vben/request": "workspace:*", + "@vben/stores": "workspace:*", "@vben/styles": "workspace:*", "@vben/utils": "workspace:*", "@vben/vbencomponents": "workspace:*", diff --git a/apps/admin/src/layout/index.vue b/apps/admin/src/layout/index.vue index 0f776d75..5c799f0f 100644 --- a/apps/admin/src/layout/index.vue +++ b/apps/admin/src/layout/index.vue @@ -1,5 +1,6 @@ diff --git a/apps/admin/src/main.ts b/apps/admin/src/main.ts index 8f886201..40aa53e7 100644 --- a/apps/admin/src/main.ts +++ b/apps/admin/src/main.ts @@ -6,7 +6,7 @@ import { createApp } from 'vue' import { router } from '@/router' import { setupRouteGuard } from '@/router/guard' import { setupI18n } from '@vben/locale' -import { setupPinia } from '@vben/pinia' +import { setupPinia } from '@vben/stores' import { initApplication } from './init-application' import { registerComponents } from '../init-components' ;(async () => { diff --git a/apps/admin/src/pages/sys/login/login.vue b/apps/admin/src/pages/sys/login/login.vue index 2f2ca608..cf837c4b 100644 --- a/apps/admin/src/pages/sys/login/login.vue +++ b/apps/admin/src/pages/sys/login/login.vue @@ -6,7 +6,6 @@ import ForgetPasswordForm from './forget-password-form.vue' import RegisterForm from './register-form.vue' import MobileForm from './mobile-form.vue' import QrCodeForm from './qrcode-form.vue' -import { DarkModeToggle } from '@vben/layouts' const { bem } = createNamespace('login') defineProps({ diff --git a/apps/admin/src/store/auth.ts b/apps/admin/src/store/auth.ts index 7aa32aa7..447126cd 100644 --- a/apps/admin/src/store/auth.ts +++ b/apps/admin/src/store/auth.ts @@ -1,4 +1,4 @@ -import { defineStore } from '@vben/pinia' +import { defineStore } from 'pinia' import { asyncRoutes, PAGE_NOT_FOUND_ROUTE } from '@/router/routes' import { filterTree } from '@vben/utils' import { Menu } from '@vben/types' diff --git a/apps/admin/src/store/config.ts b/apps/admin/src/store/config.ts index 296e9157..c6d54ae0 100644 --- a/apps/admin/src/store/config.ts +++ b/apps/admin/src/store/config.ts @@ -6,7 +6,7 @@ import { ProjectConfig, TransitionSetting, } from '@vben/types' -import { defineStore } from '@vben/pinia' +import { defineStore } from 'pinia' import { deepMerge } from '@vben/utils' import { APP_DARK_MODE_KEY_, ThemeEnum } from '@vben/constants' import { darkMode, projectSetting } from '@/setting' diff --git a/apps/admin/src/store/lock.ts b/apps/admin/src/store/lock.ts index b83c1e98..6296215a 100644 --- a/apps/admin/src/store/lock.ts +++ b/apps/admin/src/store/lock.ts @@ -1,5 +1,5 @@ import type { LockInfo } from '@vben/types' -import { defineStore } from '@vben/pinia' +import { defineStore } from 'pinia' import { useUserStore } from '@/store/user' interface LockState { diff --git a/apps/admin/src/store/modules/app.ts b/apps/admin/src/store/modules/app.ts index 03bef14b..6b1afc8c 100644 --- a/apps/admin/src/store/modules/app.ts +++ b/apps/admin/src/store/modules/app.ts @@ -1,4 +1,4 @@ -import { defineStore } from '@vben/pinia' +import { defineStore } from 'pinia' // import { pinia } from '@/pinia' import { resetRouter } from '@/router' diff --git a/apps/admin/src/store/multipleTab.ts b/apps/admin/src/store/multipleTab.ts index 492f88d6..fd896678 100644 --- a/apps/admin/src/store/multipleTab.ts +++ b/apps/admin/src/store/multipleTab.ts @@ -4,7 +4,7 @@ import type { Router, } from 'vue-router' import { toRaw, unref } from 'vue' -import { defineStore } from '@vben/pinia' +import { defineStore } from 'pinia' import { useGo, useRedo } from '@vben/hooks' // import { Persistent } from '/@/utils/cache/persistent'; diff --git a/apps/admin/src/store/test.ts b/apps/admin/src/store/test.ts index 724e63d3..6b6e0c3b 100644 --- a/apps/admin/src/store/test.ts +++ b/apps/admin/src/store/test.ts @@ -1,4 +1,4 @@ -import { defineStore } from '@vben/pinia' +import { defineStore } from 'pinia' export const useTest = defineStore({ id: 'use--test', diff --git a/apps/admin/src/store/user.ts b/apps/admin/src/store/user.ts index b1c6d462..0c1edfee 100644 --- a/apps/admin/src/store/user.ts +++ b/apps/admin/src/store/user.ts @@ -1,5 +1,5 @@ import type { LoginParams } from '@/apis/auth' -import { defineStore } from '@vben/pinia' +import { defineStore } from 'pinia' import { BASIC_HOME_PATH, BASIC_LOGIN_PATH, PageEnum } from '@vben/constants' // import { pinia } from '@/pinia' import { router } from '@/router' diff --git a/apps/grid-admin/.env b/apps/grid-admin/.env new file mode 100644 index 00000000..41883baa --- /dev/null +++ b/apps/grid-admin/.env @@ -0,0 +1,5 @@ +# spa title +VITE_GLOB_APP_TITLE = Vben Admin + +# spa shortname +VITE_GLOB_APP_SHORT_NAME = vben_admin diff --git a/apps/grid-admin/.env.development b/apps/grid-admin/.env.development new file mode 100644 index 00000000..cbc16168 --- /dev/null +++ b/apps/grid-admin/.env.development @@ -0,0 +1,21 @@ +# Whether to open mock +VITE_USE_MOCK = true + +# public path +VITE_PUBLIC_PATH = / + +# Cross-domain proxy, you can configure multiple +# Please note that no line breaks +VITE_PROXY = [["/basic-api","http://localhost:3000"]] +# VITE_PROXY=[["/api","https://vvbin.cn/test"]] + +# Whether to enable https and http2 in the development environment +# it will greatly optimize the loading speed at the first startup +# 是否在开发环境开启 https 和 http2(开启后将大幅优化首次启动时加载速度) +VITE_USE_HTTPS = true + +# Delete console +VITE_DROP_CONSOLE = false + +# Basic interface address SPA +VITE_GLOB_API_URL=/basic-api diff --git a/apps/grid-admin/.env.production b/apps/grid-admin/.env.production new file mode 100644 index 00000000..ac329c9c --- /dev/null +++ b/apps/grid-admin/.env.production @@ -0,0 +1,28 @@ +# Whether to open mock +VITE_USE_MOCK = true + +# public path +VITE_PUBLIC_PATH = / + +# Delete console +VITE_DROP_CONSOLE = true + +# Whether to enable gzip or brotli compression +# Optional: gzip | brotli | none +# If you need multiple forms, you can use `,` to separate +VITE_BUILD_COMPRESS = 'none' + +# Whether to delete origin files when using compress, default false +VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false + +# Basic interface address SPA +VITE_GLOB_API_URL=/basic-api + +# Whether to enable image compression +VITE_USE_IMAGEMIN= true + +# use pwa +VITE_USE_PWA = false + +# Is it compatible with older browsers +VITE_LEGACY = false diff --git a/apps/grid-admin/index.html b/apps/grid-admin/index.html new file mode 100644 index 00000000..9b193a7f --- /dev/null +++ b/apps/grid-admin/index.html @@ -0,0 +1,146 @@ + + + + + + + + <%= title %> + + + + + + + + + + + + + <%= title %> + + + + + + diff --git a/apps/grid-admin/init-components.ts b/apps/grid-admin/init-components.ts new file mode 100644 index 00000000..cf3fa770 --- /dev/null +++ b/apps/grid-admin/init-components.ts @@ -0,0 +1,194 @@ +// 按需导入组件 +import naive, { + darkTheme, + NTag, + NCard, + NDivider, + NSpace, + NPopover, + NButton, + NAvatar, + NStatistic, + NSelect, + NInput, + NThing, + NPopconfirm, + NIcon, + NIconWrapper, + NCheckbox, + NCheckboxGroup, + NButtonGroup, + NModal, + NEmpty, + NUpload, + NUploadDragger, + NTabs, + NTabPane, + NGrid, + NGridItem, + NEllipsis, + NImage, + NMessageProvider, + useMessage, + NPagination, + NRadio, + NRadioButton, + NRadioGroup, + NText, + NConfigProvider, + NDynamicTags, + NColorPicker, + NDescriptions, + NDescriptionsItem, + useNotification, + NNotificationProvider, + NSwitch, + NMenu, + // dateZhCN, + zhCN, + // darkTheme, + NBreadcrumb, + NBreadcrumbItem, + NLayout, + NLayoutSider, + NLayoutFooter, + NLayoutContent, + NLayoutHeader, + NForm, + NFormItem, + NDropdown, + NInputNumber, + NTreeSelect, + NBadge, + NListItem, + NList, + NAutoComplete, + NCascader, + NDatePicker, + NTimePicker, + NRate, + NSlider, + NTransfer, + NMention, + NFormItemGi, + NTree, + NH1, + NH2, + NH3, + NH4, + NH5, + NH6, + NInputGroupLabel, + NInputGroup, + NDrawer, + NDrawerContent, + NDynamicInput, + NPopselect, + NTime, + NScrollbar, + NElement, + NSkeleton, + NTooltip, + NTab, + NResult, + NAffix, NGradientText +} from 'naive-ui' + +import { initVbenComponent, setNotice, setMessage } from '@vben/vbencomponents' + +export async function registerComponents(app) { + initVbenComponent(app, { + Drawer: NDrawer, + DrawerContent: NDrawerContent, + Tag: NTag, + Card: NCard, + Menu: NMenu, + Divider: NDivider, + Space: NSpace, + Popover: NPopover, + Button: NButton, + ButtonGroup: NButtonGroup, + Avatar: NAvatar, + Statistic: NStatistic, + Select: NSelect, + Input: NInput, + InputGroup: NInputGroup, + InputGroupLabel: NInputGroupLabel, + InputNumber: NInputNumber, + Form: NForm, + FormItem: NFormItem, + Thing: NThing, + Popconfirm: NPopconfirm, + Icon: NIcon, + IconWrapper: NIconWrapper, + Checkbox: NCheckbox, + CheckboxGroup: NCheckboxGroup, + Breadcrumb: NBreadcrumb, + BreadcrumbItem: NBreadcrumbItem, + Modal: NModal, + Empty: NEmpty, + Upload: NUpload, + UploadDragger: NUploadDragger, + Tabs: NTabs, + Tab: NTab, + TabPane: NTabPane, + Grid: NGrid, + GridItem: NGridItem, + Ellipsis: NEllipsis, + Image: NImage, + MessageProvider: NMessageProvider, + Pagination: NPagination, + Radio: NRadio, + RadioButton: NRadioButton, + RadioGroup: NRadioGroup, + Text: NText, + Config: NConfigProvider, + DynamicTags: NDynamicTags, + ColorPicker: NColorPicker, + Descriptions: NDescriptions, + DescriptionsItem: NDescriptionsItem, + NotificationProvider: NNotificationProvider, + Switch: NSwitch, + Layout: NLayout, + LayoutHeader: NLayoutHeader, + LayoutFooter: NLayoutFooter, + LayoutSider: NLayoutSider, + LayoutContent: NLayoutContent, + Dropdown: NDropdown, + TreeSelect: NTreeSelect, + Badge: NBadge, + ListItem: NListItem, + List: NList, + AutoComplete: NAutoComplete, + Cascader: NCascader, + DatePicker: NDatePicker, + TimePicker: NTimePicker, + Rate: NRate, + Slider: NSlider, + Transfer: NTransfer, + Mention: NMention, + FormItemGi: NFormItemGi, + Tree: NTree, + H1: NH1, + H2: NH2, + H3: NH3, + H4: NH4, + H5: NH5, + H6: NH6, + DynamicInput: NDynamicInput, + PopSelect: NPopselect, + Time: NTime, + Scrollbar: NScrollbar, + DarkTheme: darkTheme, + Element: NElement, + Skeleton: NSkeleton, + Tooltip: NTooltip, + Result: NResult, + Affix: NAffix, + GradientText: NGradientText + }) + setMessage(useMessage) + setNotice(useNotification) + // @ts-ignore + // app.use(naive) +} diff --git a/apps/grid-admin/mock/_mock-server.ts b/apps/grid-admin/mock/_mock-server.ts new file mode 100644 index 00000000..ff06484a --- /dev/null +++ b/apps/grid-admin/mock/_mock-server.ts @@ -0,0 +1,16 @@ +import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' + +const modules = import.meta.globEager('./**/*.ts') + +const mockModules: any[] = [] +Object.keys(modules).forEach((key) => { + if (key.includes('/_')) { + return + } + mockModules.push(...modules[key].default) +}) + +/** + * Used in a production environment. Need to manually import all modules + */ +export const setupProdMockServer = () => createProdMockServer(mockModules) diff --git a/apps/grid-admin/mock/auth.ts b/apps/grid-admin/mock/auth.ts new file mode 100644 index 00000000..2a6505f0 --- /dev/null +++ b/apps/grid-admin/mock/auth.ts @@ -0,0 +1,121 @@ +import { MockMethod } from 'vite-plugin-mock' +import { + resultError, + resultSuccess, + getRequestToken, + requestParams, +} from '@vben/utils/mock-util' + +export function createFakeUserList() { + return [ + { + userId: '1', + username: 'vben', + realname: 'Vben Admin', + avatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640', + desc: 'manager', + password: '123456', + accessToken: 'fakeToken1', + roles: [ + { + name: 'Super Admin', + value: 'super', + }, + ], + }, + { + userId: '2', + username: 'test', + password: '123456', + realname: 'test user', + avatar: 'https://q1.qlogo.cn/g?b=qq&nk=339449197&s=640', + desc: 'tester', + accessToken: 'fakeToken2', + roles: [ + { + name: 'Tester', + value: 'test', + }, + ], + }, + ] +} + +const fakeCodeList: any = { + '1': ['1000', '3000', '5000'], + + '2': ['2000', '4000', '6000'], +} +export default [ + // mock user login + { + url: '/basic-api/login', + timeout: 200, + method: 'post', + response: ({ body }) => { + const { username, password } = body + + const checkUser = createFakeUserList().find( + (item) => item.username === username && password === item.password, + ) + + if (!checkUser) { + return resultError('Incorrect account or password!') + } + + return resultSuccess(checkUser) + }, + }, + { + url: '/basic-api/getUserInfo', + method: 'get', + response: (request: requestParams) => { + const accessToken = getRequestToken(request) + + if (!accessToken) return resultError('Invalid accessToken.') + const checkUser = createFakeUserList().find( + (item) => item.accessToken === accessToken, + ) + if (!checkUser) { + return resultError( + 'The corresponding user information was not obtained!', + ) + } + return resultSuccess(checkUser) + }, + }, + { + url: '/basic-api/getPermCode', + timeout: 200, + method: 'get', + response: (request: requestParams) => { + const accessToken = getRequestToken(request) + if (!accessToken) return resultError('Invalid accessToken.') + const checkUser = createFakeUserList().find( + (item) => item.accessToken === accessToken, + ) + if (!checkUser) { + return resultError('Invalid accessToken.') + } + const codeList = fakeCodeList[checkUser.userId] + + return resultSuccess(codeList) + }, + }, + { + url: '/basic-api/logout', + timeout: 200, + method: 'get', + response: (request: requestParams) => { + const accessToken = getRequestToken(request) + if (!accessToken) return resultError('Invalid accessToken.') + const checkUser = createFakeUserList().find( + (item) => item.accessToken === accessToken, + ) + if (!checkUser) { + return resultError('Invalid accessToken.') + } + return resultSuccess(undefined, { message: 'Token has been destroyed' }) + }, + }, +] as MockMethod[] diff --git a/apps/grid-admin/mock/demo/form.ts b/apps/grid-admin/mock/demo/form.ts new file mode 100644 index 00000000..f3f33948 --- /dev/null +++ b/apps/grid-admin/mock/demo/form.ts @@ -0,0 +1,271 @@ +import { MockMethod } from 'vite-plugin-mock' +import { resultError, resultSuccess } from '@vben/utils/mock-util' + +export function createSelectData() { + return { + options: [ + { + label: "Everybody's Got Something to Hide Except Me and My Monkey", + value: 'song0', + disabled: true, + }, + { + label: 'Drive My Car', + value: 'song1', + }, + { + label: 'Norwegian Wood', + value: 'song2', + }, + { + label: "You Won't See", + value: 'song3', + disabled: true, + }, + { + label: 'Nowhere Man', + value: 'song4', + }, + { + label: 'Think For Yourself', + value: 'song5', + }, + { + label: 'The Word', + value: 'song6', + }, + { + label: 'Michelle', + value: 'song7', + disabled: true, + }, + { + label: 'What goes on', + value: 'song8', + }, + { + label: 'Girl', + value: 'song9', + }, + { + label: "I'm looking through you", + value: 'song10', + }, + { + label: 'In My Life', + value: 'song11', + }, + { + label: 'Wait', + value: 'song12', + }, + ], + } +} + +export function createTreeSelectData() { + return { + options: [ + { + label: 'Rubber Soul', + key: 'Rubber Soul', + children: [ + { + label: "Everybody's Got Something to Hide Except Me and My Monkey", + key: "Everybody's Got Something to Hide Except Me and My Monkey", + }, + { + label: 'Drive My Car', + key: 'Drive My Car', + disabled: true, + }, + { + label: 'Norwegian Wood', + key: 'Norwegian Wood', + }, + { + label: "You Won't See", + key: "You Won't See", + disabled: true, + }, + { + label: 'Nowhere Man', + key: 'Nowhere Man', + }, + { + label: 'Think For Yourself', + key: 'Think For Yourself', + }, + { + label: 'The Word', + key: 'The Word', + }, + { + label: 'Michelle', + key: 'Michelle', + disabled: true, + }, + { + label: 'What goes on', + key: 'What goes on', + }, + { + label: 'Girl', + key: 'Girl', + }, + { + label: "I'm looking through you", + key: "I'm looking through you", + }, + { + label: 'In My Life', + key: 'In My Life', + }, + { + label: 'Wait', + key: 'Wait', + }, + ], + }, + { + label: 'Let It Be', + key: 'Let It Be Album', + children: [ + { + label: 'Two Of Us', + key: 'Two Of Us', + }, + { + label: 'Dig A Pony', + key: 'Dig A Pony', + }, + { + label: 'Across The Universe', + key: 'Across The Universe', + }, + { + label: 'I Me Mine', + key: 'I Me Mine', + }, + { + label: 'Dig It', + key: 'Dig It', + }, + { + label: 'Let It Be', + key: 'Let It Be', + }, + { + label: 'Maggie Mae', + key: 'Maggie Mae', + }, + { + label: "I've Got A Feeling", + key: "I've Got A Feeling", + }, + { + label: 'One After 909', + key: 'One After 909', + }, + { + label: 'The Long And Winding Road', + key: 'The Long And Winding Road', + }, + { + label: 'For You Blue', + key: 'For You Blue', + }, + { + label: 'Get Back', + key: 'Get Back', + }, + ], + }, + ], + } +} + +export function createRadioData() { + return { + options: [ + { + label: '选项1', + value: '1', + }, + { + label: '选项2', + value: '2', + }, + { + label: '选项3', + value: '3', + disabled: true, + }, + ], + } +} + +export function createCascaderOptions(depth = 3, iterator = 1, prefix = '') { + const length = 12 + const options = [] + for (let i = 1; i <= length; ++i) { + if (iterator === 1) { + options.push({ + value: `v-${i}`, + label: `l-${i}`, + disabled: i % 5 === 0, + children: createCascaderOptions(depth, iterator + 1, '' + String(i)), + }) + } else if (iterator === depth) { + options.push({ + value: `v-${prefix}-${i}`, + label: `l-${prefix}-${i}`, + disabled: i % 5 === 0, + }) + } else { + options.push({ + value: `v-${prefix}-${i}`, + label: `l-${prefix}-${i}`, + disabled: i % 5 === 0, + children: createCascaderOptions(depth, iterator + 1, `${prefix}-${i}`), + }) + } + } + return options +} + +export default [ + // mock user login + { + url: '/basic-api/demo/form/select', + timeout: 200, + method: 'post', + response: ({ body }) => { + return resultSuccess(createSelectData()) + }, + }, + { + url: '/basic-api/demo/form/treeSelect', + timeout: 200, + method: 'post', + response: ({ body }) => { + return resultSuccess(createTreeSelectData()) + }, + }, + { + url: '/basic-api/demo/form/radio', + timeout: 200, + method: 'post', + response: ({ body }) => { + return resultSuccess(createRadioData()) + }, + }, + { + url: '/basic-api/demo/form/cascader', + timeout: 200, + method: 'post', + response: ({ body }) => { + return resultSuccess({ options: createCascaderOptions() }) + }, + }, +] as MockMethod[] diff --git a/apps/grid-admin/mock/demo/table.ts b/apps/grid-admin/mock/demo/table.ts new file mode 100644 index 00000000..1afafa42 --- /dev/null +++ b/apps/grid-admin/mock/demo/table.ts @@ -0,0 +1,52 @@ +import { MockMethod } from 'vite-plugin-mock' +import { resultError, resultSuccess } from '@vben/utils/mock-util' + +export function createFakeTableData() { + return { + items: [ + { + userId: '1', + username: 'vben', + realname: 'Vben Admin', + avatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640', + desc: 'manager', + password: '123456', + accessToken: 'fakeToken1', + roles: [ + { + name: 'Super Admin', + value: 'super', + }, + ], + }, + { + userId: '2', + username: 'test', + password: '123456', + realname: 'test user', + avatar: 'https://q1.qlogo.cn/g?b=qq&nk=339449197&s=640', + desc: 'tester', + accessToken: 'fakeToken2', + roles: [ + { + name: 'Tester', + value: 'test', + }, + ], + }, + ], + total: 2, + } +} + +export default [ + // mock user login + { + url: '/basic-api/demo/table', + timeout: 200, + method: 'post', + response: ({ body }) => { + return resultSuccess(createFakeTableData()) + }, + }, +] as MockMethod[] diff --git a/apps/grid-admin/mock/menu.ts b/apps/grid-admin/mock/menu.ts new file mode 100644 index 00000000..d20aebd1 --- /dev/null +++ b/apps/grid-admin/mock/menu.ts @@ -0,0 +1,269 @@ +import { + resultSuccess, + resultError, + getRequestToken, + requestParams, +} from '@vben/utils/mock-util' +import { MockMethod } from 'vite-plugin-mock' +import { createFakeUserList } from './auth' + +// single +const dashboardRoute = { + path: '/dashboard', + name: 'Dashboard', + component: 'LAYOUT', + redirect: '/dashboard/analysis', + meta: { + title: 'routes.dashboard.dashboard', + icon: 'bx:bx-home', + }, + children: [ + { + path: 'analysis', + name: 'Analysis', + component: '/dashboard/analysis/index', + meta: { + title: 'routes.dashboard.analysis', + icon: 'bx:bx-home', + }, + }, + { + path: 'workbench', + name: 'Workbench', + component: '/dashboard/workbench/index', + meta: { + title: 'routes.dashboard.workbench', + icon: 'bx:bx-home', + }, + }, + ], +} + +const backRoute = { + path: 'back', + name: 'PermissionBackDemo', + meta: { + title: 'routes.demo.permission.back', + }, + + children: [ + { + path: 'page', + name: 'BackAuthPage', + component: '/demo/permission/back/index', + meta: { + title: 'routes.demo.permission.backPage', + }, + }, + { + path: 'btn', + name: 'BackAuthBtn', + component: '/demo/permission/back/Btn', + meta: { + title: 'routes.demo.permission.backBtn', + }, + }, + ], +} + +const authRoute = { + path: '/permission', + name: 'Permission', + component: 'LAYOUT', + redirect: '/permission/front/page', + meta: { + icon: 'carbon:user-role', + title: 'routes.demo.permission.permission', + }, + children: [backRoute], +} + +const levelRoute = { + path: '/level', + name: 'Level', + component: 'LAYOUT', + redirect: '/level/menu1/menu1-1', + meta: { + icon: 'carbon:user-role', + title: 'routes.demo.level.level', + }, + + children: [ + { + path: 'menu1', + name: 'Menu1Demo', + meta: { + title: 'Menu1', + }, + children: [ + { + path: 'menu1-1', + name: 'Menu11Demo', + meta: { + title: 'Menu1-1', + }, + children: [ + { + path: 'menu1-1-1', + name: 'Menu111Demo', + component: '/demo/level/Menu111', + meta: { + title: 'Menu111', + }, + }, + ], + }, + { + path: 'menu1-2', + name: 'Menu12Demo', + component: '/demo/level/Menu12', + meta: { + title: 'Menu1-2', + }, + }, + ], + }, + { + path: 'menu2', + name: 'Menu2Demo', + component: '/demo/level/Menu2', + meta: { + title: 'Menu2', + }, + }, + ], +} + +const sysRoute = { + path: '/system', + name: 'System', + component: 'LAYOUT', + redirect: '/system/account', + meta: { + icon: 'ion:settings-outline', + title: 'routes.demo.system.moduleName', + }, + children: [ + { + path: 'account', + name: 'AccountManagement', + meta: { + title: 'routes.demo.system.account', + ignoreKeepAlive: true, + }, + component: '/demo/system/account/index', + }, + { + path: 'account_detail/:id', + name: 'AccountDetail', + meta: { + title: 'routes.demo.system.account_detail', + ignoreKeepAlive: true, + showMenu: false, + }, + component: '/demo/system/account/AccountDetail', + }, + { + path: 'role', + name: 'RoleManagement', + meta: { + title: 'routes.demo.system.role', + ignoreKeepAlive: true, + }, + component: '/demo/system/role/index', + }, + + { + path: 'menu', + name: 'MenuManagement', + meta: { + title: 'routes.demo.system.menu', + ignoreKeepAlive: true, + }, + component: '/demo/system/menu/index', + }, + { + path: 'dept', + name: 'DeptManagement', + meta: { + title: 'routes.demo.system.dept', + ignoreKeepAlive: true, + }, + component: '/demo/system/dept/index', + }, + { + path: 'changePassword', + name: 'ChangePassword', + meta: { + title: 'routes.demo.system.password', + ignoreKeepAlive: true, + }, + component: '/demo/system/password/index', + }, + ], +} + +const linkRoute = { + path: '/link', + name: 'Link', + component: 'LAYOUT', + meta: { + icon: 'ion:tv-outline', + title: 'routes.demo.iframe.frame', + }, + children: [ + { + path: 'doc', + name: 'Doc', + meta: { + title: 'routes.demo.iframe.doc', + }, + }, + { + path: 'https://vvbin.cn/doc-next/', + name: 'DocExternal', + component: 'LAYOUT', + meta: { + title: 'routes.demo.iframe.docExternal', + }, + }, + ], +} + +export default [ + { + url: '/basic-api/getMenuList', + timeout: 1000, + method: 'get', + response: (request: requestParams) => { + const token = getRequestToken(request) + if (!token) { + return resultError('Invalid token!') + } + const checkUser = createFakeUserList().find( + (item) => item.token === token, + ) + if (!checkUser) { + return resultError('Invalid user token!') + } + const id = checkUser.userId + let menu: Object[] + switch (id) { + case '1': + dashboardRoute.redirect = + dashboardRoute.path + '/' + dashboardRoute.children[0].path + menu = [dashboardRoute, authRoute, levelRoute, sysRoute, linkRoute] + break + case '2': + dashboardRoute.redirect = + dashboardRoute.path + '/' + dashboardRoute.children[1].path + menu = [dashboardRoute, authRoute, levelRoute, linkRoute] + break + default: + menu = [] + } + + return resultSuccess(menu) + }, + }, +] as MockMethod[] diff --git a/apps/grid-admin/package.json b/apps/grid-admin/package.json new file mode 100644 index 00000000..9b5dfa3f --- /dev/null +++ b/apps/grid-admin/package.json @@ -0,0 +1,63 @@ +{ + "name": "@apps/grid-admin", + "version": "3.0.0-bata.1", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "apps/ant-app" + }, + "author": { + "name": "vben", + "email": "anncwb@126.com", + "url": "https://github.com/anncwb" + }, + "scripts": { + "build": "pnpm cross-env NODE_ENV=production vite build", + "clean": "pnpm rimraf node_modules && pnpm rimraf dist && pnpm rimraf .turbo", + "dev": "vite", + "preview": "npm run build && vite preview", + "preview:dist": "vite preview", + "report": "pnpm cross-env REPORT=true npm run build", + "server:br": "pnpx http-server dist --cors --brotli -c-1", + "server:gzip": "pnpx http-server dist --cors --gzip -c-1", + "type:check": "vue-tsc --noEmit --skipLibCheck" + }, + "dependencies": { + "@ant-design/icons-vue": "^6.1.0", + "@iconify/iconify": "^3.1.0", + "@vben/components": "workspace:*", + "@vben/constants": "workspace:*", + "@vben/demo": "workspace:*", + "@vben/directives": "workspace:*", + "@vben/grid-layouts": "workspace:*", + "@vben/hooks": "workspace:*", + "@vben/locale": "workspace:*", + "@vben/request": "workspace:*", + "@vben/stores": "workspace:*", + "@vben/styles": "workspace:*", + "@vben/utils": "workspace:*", + "@vben/vbencomponents": "workspace:*", + "dayjs": "^1.11.7", + "mockjs": "^1.1.0", + "monaco-editor": "^0.34.1", + "nprogress": "^0.2.0", + "path-to-regexp": "^6.2.1", + "pinia": "2.0.30", + "vue": "3.2.47", + "vue-demi": "^0.13.11", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@config/css-preprocess": "workspace:*", + "@config/vite": "workspace:*", + "@types/mockjs": "^1.0.7", + "@types/nprogress": "^0.2.0", + "@vben/types": "workspace:*", + "@vue/compiler-sfc": "3.2.47", + "@vue/test-utils": "^2.2.9", + "naive-ui": "^2.34.3", + "typescript": "^4.9.5", + "vite": "4.0.4", + "vue-tsc": "^1.0.24" + } +} diff --git a/apps/grid-admin/postcss.config.js b/apps/grid-admin/postcss.config.js new file mode 100644 index 00000000..db0f61f9 --- /dev/null +++ b/apps/grid-admin/postcss.config.js @@ -0,0 +1 @@ +module.exports = require('@config/css-preprocess/postcss') diff --git a/apps/grid-admin/public/favicon.ico b/apps/grid-admin/public/favicon.ico new file mode 100644 index 00000000..d92e0b8c Binary files /dev/null and b/apps/grid-admin/public/favicon.ico differ diff --git a/apps/grid-admin/public/logo.png b/apps/grid-admin/public/logo.png new file mode 100644 index 00000000..cd4c33d8 Binary files /dev/null and b/apps/grid-admin/public/logo.png differ diff --git a/apps/grid-admin/src/apis/auth.ts b/apps/grid-admin/src/apis/auth.ts new file mode 100644 index 00000000..d144e2b6 --- /dev/null +++ b/apps/grid-admin/src/apis/auth.ts @@ -0,0 +1,57 @@ +import type { ErrorMessageMode } from '@vben/types' +import { request } from '@vben/request' + +export interface RoleInfo { + name: string + value: string +} + +export interface LoginParams { + username: string + password: string +} + +export interface LoginResultModel { + userId: string | number + accessToken: string + role: RoleInfo +} + +export interface UserInfoModel { + roles: RoleInfo[] + userId: string | number + username: string + realName?: string + avatar: string + desc?: string +} + +export function doLoginApi( + params: LoginParams, + mode: ErrorMessageMode = 'modal', +) { + return request.post( + { + url: '/login', + params, + }, + { + errorMessageMode: mode, + }, + ) +} + +export function getUserInfoApi() { + return request.get( + { url: '/getUserInfo' }, + { errorMessageMode: 'none' }, + ) +} + +export function getPermCode() { + return request.get({ url: '/getPermCode' }) +} + +export function doLogoutApi() { + return request.get({ url: '/logout' }) +} diff --git a/apps/grid-admin/src/apis/menu.ts b/apps/grid-admin/src/apis/menu.ts new file mode 100644 index 00000000..1ba0ccd0 --- /dev/null +++ b/apps/grid-admin/src/apis/menu.ts @@ -0,0 +1,29 @@ +import { request } from '@vben/request' + +export interface RouteItem { + path: string + component: any + meta: any + name?: string + alias?: string | string[] + redirect?: string + caseSensitive?: boolean + children?: RouteItem[] +} + +/** + * @description: Get menu return value + */ +export type GetMenuListResultModel = RouteItem[] + +enum Api { + GetMenuList = '/getMenuList', +} + +/** + * @description: Get user menu based on id + */ + +export const getMenuList = () => { + return request.get({ url: Api.GetMenuList }) +} diff --git a/apps/grid-admin/src/apis/sys/index.ts b/apps/grid-admin/src/apis/sys/index.ts new file mode 100644 index 00000000..d35d236a --- /dev/null +++ b/apps/grid-admin/src/apis/sys/index.ts @@ -0,0 +1,2 @@ +export * from './menu' +export * from './user' diff --git a/apps/grid-admin/src/apis/sys/menu.ts b/apps/grid-admin/src/apis/sys/menu.ts new file mode 100644 index 00000000..1ba0ccd0 --- /dev/null +++ b/apps/grid-admin/src/apis/sys/menu.ts @@ -0,0 +1,29 @@ +import { request } from '@vben/request' + +export interface RouteItem { + path: string + component: any + meta: any + name?: string + alias?: string | string[] + redirect?: string + caseSensitive?: boolean + children?: RouteItem[] +} + +/** + * @description: Get menu return value + */ +export type GetMenuListResultModel = RouteItem[] + +enum Api { + GetMenuList = '/getMenuList', +} + +/** + * @description: Get user menu based on id + */ + +export const getMenuList = () => { + return request.get({ url: Api.GetMenuList }) +} diff --git a/apps/grid-admin/src/apis/sys/user.ts b/apps/grid-admin/src/apis/sys/user.ts new file mode 100644 index 00000000..d18a0e04 --- /dev/null +++ b/apps/grid-admin/src/apis/sys/user.ts @@ -0,0 +1,84 @@ +import type { ErrorMessageMode } from '@vben/types' +import { request } from '@vben/request' + +/** + * @description: Login interface parameters + */ +export interface LoginParams { + username: string + password: string +} + +export interface RoleInfo { + roleName: string + value: string +} + +/** + * @description: Login interface return value + */ +export interface LoginResultModel { + userId: string | number + token: string + role: RoleInfo +} + +/** + * @description: Get user information return value + */ +export interface GetUserInfoModel { + roles: RoleInfo[] + // 用户id + userId: string | number + // 用户名 + username: string + // 真实名字 + realName: string + // 头像 + avatar: string + // 介绍 + desc?: string +} + +enum Api { + Login = '/login', + Logout = '/logout', + GetUserInfo = '/getUserInfo', + GetPermCode = '/getPermCode', +} + +/** + * @description: user login api + */ +export function loginApi( + params: LoginParams, + mode: ErrorMessageMode = 'modal', +) { + return request.post( + { + url: Api.Login, + params, + }, + { + errorMessageMode: mode, + }, + ) +} + +/** + * @description: getUserInfo + */ +export function getUserInfo() { + return request.get( + { url: Api.GetUserInfo }, + { errorMessageMode: 'none' }, + ) +} + +export function getPermCode() { + return request.get({ url: Api.GetPermCode }) +} + +export function doLogout() { + return request.get({ url: Api.Logout }) +} diff --git a/apps/grid-admin/src/apis/system/model/useModel.ts b/apps/grid-admin/src/apis/system/model/useModel.ts new file mode 100644 index 00000000..2415c911 --- /dev/null +++ b/apps/grid-admin/src/apis/system/model/useModel.ts @@ -0,0 +1,38 @@ +/** + * @description: Login interface parameters + */ +export interface LoginParams { + username: string; + password: string; +} + +export interface RoleInfo { + roleName: string; + value: string; +} + +/** + * @description: Login interface return value + */ +export interface LoginResultModel { + userId: string | number; + token: string; + role: RoleInfo; +} + +/** + * @description: Get user information return value + */ +export interface GetUserInfoModel { + roles: RoleInfo[]; + // 用户id + userId: string | number; + // 用户名 + username: string; + // 真实名字 + realName: string; + // 头像 + avatar: string; + // 介绍 + desc?: string; +} diff --git a/apps/grid-admin/src/app.vue b/apps/grid-admin/src/app.vue new file mode 100644 index 00000000..cb506a9e --- /dev/null +++ b/apps/grid-admin/src/app.vue @@ -0,0 +1,59 @@ + + + + + + + + + + + + + diff --git a/apps/grid-admin/src/assets/icons/download-count.svg b/apps/grid-admin/src/assets/icons/download-count.svg new file mode 100644 index 00000000..1c951958 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/download-count.svg @@ -0,0 +1 @@ +Asset 91 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/dynamic-avatar-1.svg b/apps/grid-admin/src/assets/icons/dynamic-avatar-1.svg new file mode 100644 index 00000000..e1553e50 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/dynamic-avatar-1.svg @@ -0,0 +1 @@ +Asset 15 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/dynamic-avatar-2.svg b/apps/grid-admin/src/assets/icons/dynamic-avatar-2.svg new file mode 100644 index 00000000..c4c17223 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/dynamic-avatar-2.svg @@ -0,0 +1 @@ +Asset 16 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/dynamic-avatar-3.svg b/apps/grid-admin/src/assets/icons/dynamic-avatar-3.svg new file mode 100644 index 00000000..81145f9b --- /dev/null +++ b/apps/grid-admin/src/assets/icons/dynamic-avatar-3.svg @@ -0,0 +1 @@ +Asset 17 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/dynamic-avatar-4.svg b/apps/grid-admin/src/assets/icons/dynamic-avatar-4.svg new file mode 100644 index 00000000..e586ed4e --- /dev/null +++ b/apps/grid-admin/src/assets/icons/dynamic-avatar-4.svg @@ -0,0 +1 @@ +Asset 120 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/dynamic-avatar-5.svg b/apps/grid-admin/src/assets/icons/dynamic-avatar-5.svg new file mode 100644 index 00000000..746e4b88 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/dynamic-avatar-5.svg @@ -0,0 +1 @@ +Asset 110 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/dynamic-avatar-6.svg b/apps/grid-admin/src/assets/icons/dynamic-avatar-6.svg new file mode 100644 index 00000000..b2432f22 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/dynamic-avatar-6.svg @@ -0,0 +1 @@ +Asset 100 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/moon.svg b/apps/grid-admin/src/assets/icons/moon.svg new file mode 100644 index 00000000..e6667f0d --- /dev/null +++ b/apps/grid-admin/src/assets/icons/moon.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/apps/grid-admin/src/assets/icons/sun.svg b/apps/grid-admin/src/assets/icons/sun.svg new file mode 100644 index 00000000..a3997cbf --- /dev/null +++ b/apps/grid-admin/src/assets/icons/sun.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/grid-admin/src/assets/icons/total-sales.svg b/apps/grid-admin/src/assets/icons/total-sales.svg new file mode 100644 index 00000000..eff79640 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/total-sales.svg @@ -0,0 +1 @@ +Asset 500 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/transaction.svg b/apps/grid-admin/src/assets/icons/transaction.svg new file mode 100644 index 00000000..7ba9e2f0 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/transaction.svg @@ -0,0 +1 @@ +Asset 480% \ No newline at end of file diff --git a/apps/grid-admin/src/assets/icons/visit-count.svg b/apps/grid-admin/src/assets/icons/visit-count.svg new file mode 100644 index 00000000..ba2a3061 --- /dev/null +++ b/apps/grid-admin/src/assets/icons/visit-count.svg @@ -0,0 +1 @@ +Asset 510 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/images/header.jpg b/apps/grid-admin/src/assets/images/header.jpg new file mode 100644 index 00000000..977584b6 Binary files /dev/null and b/apps/grid-admin/src/assets/images/header.jpg differ diff --git a/apps/grid-admin/src/assets/images/logo.png b/apps/grid-admin/src/assets/images/logo.png new file mode 100644 index 00000000..16f2e7ab Binary files /dev/null and b/apps/grid-admin/src/assets/images/logo.png differ diff --git a/apps/grid-admin/src/assets/svg/illustration.svg b/apps/grid-admin/src/assets/svg/illustration.svg new file mode 100644 index 00000000..b45215b0 --- /dev/null +++ b/apps/grid-admin/src/assets/svg/illustration.svg @@ -0,0 +1 @@ +Asset 336 \ No newline at end of file diff --git a/apps/grid-admin/src/assets/svg/login-bg-dark.svg b/apps/grid-admin/src/assets/svg/login-bg-dark.svg new file mode 100644 index 00000000..888da7af --- /dev/null +++ b/apps/grid-admin/src/assets/svg/login-bg-dark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/grid-admin/src/assets/svg/login-bg.svg b/apps/grid-admin/src/assets/svg/login-bg.svg new file mode 100644 index 00000000..7b66bafc --- /dev/null +++ b/apps/grid-admin/src/assets/svg/login-bg.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/grid-admin/src/assets/svg/login-box-bg.svg b/apps/grid-admin/src/assets/svg/login-box-bg.svg new file mode 100644 index 00000000..ee7dbdc2 --- /dev/null +++ b/apps/grid-admin/src/assets/svg/login-box-bg.svg @@ -0,0 +1 @@ +responsive \ No newline at end of file diff --git a/apps/grid-admin/src/assets/svg/net-error.svg b/apps/grid-admin/src/assets/svg/net-error.svg new file mode 100644 index 00000000..81f20044 --- /dev/null +++ b/apps/grid-admin/src/assets/svg/net-error.svg @@ -0,0 +1 @@ +personal settings \ No newline at end of file diff --git a/apps/grid-admin/src/assets/svg/no-data.svg b/apps/grid-admin/src/assets/svg/no-data.svg new file mode 100644 index 00000000..2b9f2570 --- /dev/null +++ b/apps/grid-admin/src/assets/svg/no-data.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/grid-admin/src/config.ts b/apps/grid-admin/src/config.ts new file mode 100644 index 00000000..c73c4148 --- /dev/null +++ b/apps/grid-admin/src/config.ts @@ -0,0 +1,26 @@ +import type { StaticConfig, DynamicConfig } from '@vben/types' + +// github repo url +export const GITHUB_URL = 'https://github.com/vbenjs/vben3' + +// vue-vben-admin-next-doc +export const DOC_URL = 'https://vbenjs.github.io/vben3-doc/' + +// site url +export const SITE_URL = 'http://vben.mufei88.com/' + +const dynamicConfig: DynamicConfig = { + __: '', +} + +// ! You need to clear the browser cache after the change +const staticConfig: StaticConfig = { + authType: 'frontend', + + // enable + enableProgress: true, +} + +export const config = { ...staticConfig, ...dynamicConfig } + +export const siteSetting = {GITHUB_URL, DOC_URL, SITE_URL} diff --git a/apps/grid-admin/src/init-application.ts b/apps/grid-admin/src/init-application.ts new file mode 100644 index 00000000..e4e198eb --- /dev/null +++ b/apps/grid-admin/src/init-application.ts @@ -0,0 +1,70 @@ +import { initRequest } from '@vben/request' +import { useUserStoreWithout } from '@/store/user' +import { useI18n, useLocale } from '@vben/locale' +import { getGlobalConfig } from '@vben/utils' +import { useConfigStore } from '@/store/config' +import { initComp } from '@vben/vbencomponents' +import { localeList } from '@vben/locale/src/config' +import { useAppStore } from '@/store/modules/app' +// To decouple the modules below `packages/*`, they no longer depend on each other +// If the modules are heavily dependent on each other, you need to provide a decoupling method, and the caller will pass the parameters +// Each module needs to provide `bridge` file as a decoupling method + +// 为了解耦 `packages/*` 下面各模块,不再相互依赖 +// 如果模块相互依赖严重,则需要对外提供解耦方式,由调用方去进行参数传递 +// 各个模块需要提供 `bridge` 文件作为解耦方式 +async function initPackages() { + const _initRequest = async () => { + const { apiUrl } = getGlobalConfig(import.meta.env) + const { t } = useI18n() + await initRequest(() => { + return { + apiUrl, + getTokenFunction: () => { + const userStore = useUserStoreWithout() + return userStore.getAccessToken + }, + errorFunction: null, + noticeFunction: null, + errorModalFunction: null, + timeoutFunction: () => { + const userStore = useUserStoreWithout() + userStore.setAccessToken(undefined) + userStore.logout(true) + }, + unauthorizedFunction: (msg?: string) => { + const userStore = useUserStoreWithout() + userStore.setAccessToken(undefined) + userStore.logout(true) + return msg || t('sys.api.errMsg401') + }, + handleErrorFunction: (msg, mode) => { + if (mode === 'modal') { + Modal.error({ title: t('sys.api.errorTip'), content: msg }) + } else if (mode === 'message') { + message.error(msg) + } + }, + } + }) + } + + const _initComp = async () => { + await initComp(() => { + return { + useLocale, + localeList, + useAppStore, + useConfigStore, + } + }) + + await Promise.all([_initRequest(), _initComp()]) + } +} + +export async function initApplication() { + // ! Need to pay attention to the timing of execution + // ! 需要注意调用时机 + await initPackages() +} diff --git a/apps/grid-admin/src/layout/components/app/AppProvider.tsx b/apps/grid-admin/src/layout/components/app/AppProvider.tsx new file mode 100644 index 00000000..a6f328ff --- /dev/null +++ b/apps/grid-admin/src/layout/components/app/AppProvider.tsx @@ -0,0 +1,46 @@ +import { + createMediaPrefersColorSchemeListen, + createBreakpointListen, +} from '@vben/hooks' +import { namespace } from '@/setting' +import { defineComponent, toRefs, ref } from 'vue' +import { createAppProviderContext } from '@vben/hooks' +import { useMsg, useNotice } from '@vben/vbencomponents' +import { setMsg, setNoice } from '@vben/request' + +const props = { + /** + * class style prefix + */ + prefixCls: { type: String, default: namespace }, +} + +export default defineComponent({ + name: 'AppProvider', + inheritAttrs: false, + props, + setup(props, { slots }) { + const isMobile = ref(false) + //注册msg以及notice,方便全局使用 + const msg = useMsg() + const notice = useNotice() + setMsg(msg) + setNoice(notice) + // Monitor screen breakpoint information changes + createBreakpointListen(({ screenMap, sizeEnum, width }) => { + const lgWidth = screenMap.get(sizeEnum.LG) + if (lgWidth) { + isMobile.value = width.value - 1 < lgWidth + } + }) + // Listen for system theme changes + createMediaPrefersColorSchemeListen() + + const { prefixCls } = toRefs(props) + + // Inject variables into the global + createAppProviderContext({ prefixCls, isMobile }) + + return () => slots.default?.() + }, +}) diff --git a/apps/grid-admin/src/layout/components/logo.vue b/apps/grid-admin/src/layout/components/logo.vue new file mode 100644 index 00000000..d27bc974 --- /dev/null +++ b/apps/grid-admin/src/layout/components/logo.vue @@ -0,0 +1,64 @@ + + + + + + + {{ title }} + + + + + diff --git a/apps/grid-admin/src/layout/components/useLockPage.ts b/apps/grid-admin/src/layout/components/useLockPage.ts new file mode 100644 index 00000000..2d7ed1e5 --- /dev/null +++ b/apps/grid-admin/src/layout/components/useLockPage.ts @@ -0,0 +1,66 @@ +import { computed, onUnmounted, watchEffect } from 'vue' +import { useThrottleFn } from '@vben/utils' +import { useLockStore } from '@/store/lock' +import { useUserStore } from '@/store/user' + +export function useLockPage() { + const lockStore = useLockStore() + const userStore = useUserStore() + + let timeId: TimeoutHandle + + function clear(): void { + window.clearTimeout(timeId) + } + + function resetCalcLockTimeout(): void { + // not login + if (!userStore.getToken) { + clear() + return + } + const lockTime = 0 + if (!lockTime || lockTime < 1) { + clear() + return + } + clear() + + timeId = setTimeout(() => { + lockPage() + }, lockTime * 60 * 1000) + } + + function lockPage(): void { + lockStore.setLockInfo({ + isLock: true, + pwd: undefined, + }) + } + + watchEffect((onClean) => { + if (userStore.getToken) { + resetCalcLockTimeout() + } else { + clear() + } + onClean(() => { + clear() + }) + }) + + onUnmounted(() => { + clear() + }) + + const keyupFn = useThrottleFn(resetCalcLockTimeout, 2000) + + return computed(() => { + if (0) { + return { onKeyup: keyupFn, onMousemove: keyupFn } + } else { + clear() + return {} + } + }) +} diff --git a/apps/grid-admin/src/layout/feature/index.vue b/apps/grid-admin/src/layout/feature/index.vue new file mode 100644 index 00000000..528bc88b --- /dev/null +++ b/apps/grid-admin/src/layout/feature/index.vue @@ -0,0 +1,9 @@ + + + + + + diff --git a/apps/grid-admin/src/layout/index.vue b/apps/grid-admin/src/layout/index.vue new file mode 100644 index 00000000..d460f8f5 --- /dev/null +++ b/apps/grid-admin/src/layout/index.vue @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/apps/grid-admin/src/logics/mitt/routeChange.ts b/apps/grid-admin/src/logics/mitt/routeChange.ts new file mode 100644 index 00000000..0c4e3571 --- /dev/null +++ b/apps/grid-admin/src/logics/mitt/routeChange.ts @@ -0,0 +1,30 @@ +/** + * Used to monitor routing changes to change the status of menus and tabs. There is no need to monitor the route, because the route status change is affected by the page rendering time, which will be slow + */ + +import type { RouteLocationNormalized } from 'vue-router'; +import { getRawRoute, mitt } from "@vben/utils"; + +const emitter = mitt(); + +const key = Symbol(); + +let lastChangeTab: RouteLocationNormalized; + +export function setRouteChange(lastChangeRoute: RouteLocationNormalized) { + const r = getRawRoute(lastChangeRoute); + emitter.emit(key, r); + lastChangeTab = r; +} + +export function listenerRouteChange( + callback: (route: RouteLocationNormalized) => void, + immediate = true, +) { + emitter.on(key, callback); + immediate && lastChangeTab && callback(lastChangeTab); +} + +export function removeTabChangeListener() { + emitter.clear(); +} diff --git a/apps/grid-admin/src/main.ts b/apps/grid-admin/src/main.ts new file mode 100644 index 00000000..40aa53e7 --- /dev/null +++ b/apps/grid-admin/src/main.ts @@ -0,0 +1,36 @@ +import '@vben/styles' +// import 'ant-design-vue/dist/antd.variable.css' +import 'virtual:svg-icons-register' +import App from './app.vue' +import { createApp } from 'vue' +import { router } from '@/router' +import { setupRouteGuard } from '@/router/guard' +import { setupI18n } from '@vben/locale' +import { setupPinia } from '@vben/stores' +import { initApplication } from './init-application' +import { registerComponents } from '../init-components' +;(async () => { + const app = createApp(App) + + setupPinia(app) + + await registerComponents(app) + + await initApplication() + // Register Global Components + + // Multilingual configuration + // Asynchronous case: language files may be obtained from the server side + await setupI18n(app) + // Init Router + app.use(router) + await setupRouteGuard(router) + await router.isReady() + app.mount('#app') + // When Closing mock, Tree Shaking `mockjs` dep + if (__VITE_USE_MOCK__) { + import('../mock/_mock-server').then(({ setupProdMockServer }) => + setupProdMockServer(), + ) + } +})() diff --git a/apps/grid-admin/src/pages/dashboard/analysis/index.vue b/apps/grid-admin/src/pages/dashboard/analysis/index.vue new file mode 100644 index 00000000..ea15ff2c --- /dev/null +++ b/apps/grid-admin/src/pages/dashboard/analysis/index.vue @@ -0,0 +1,6 @@ + + + analysis + diff --git a/apps/grid-admin/src/pages/dashboard/workbench/index.vue b/apps/grid-admin/src/pages/dashboard/workbench/index.vue new file mode 100644 index 00000000..96ac82c0 --- /dev/null +++ b/apps/grid-admin/src/pages/dashboard/workbench/index.vue @@ -0,0 +1 @@ +workbench diff --git a/apps/grid-admin/src/pages/demo/Card.vue b/apps/grid-admin/src/pages/demo/Card.vue new file mode 100644 index 00000000..2a70d82a --- /dev/null +++ b/apps/grid-admin/src/pages/demo/Card.vue @@ -0,0 +1,6 @@ + + + + diff --git a/apps/grid-admin/src/pages/demo/Draggable.vue b/apps/grid-admin/src/pages/demo/Draggable.vue new file mode 100644 index 00000000..28be7eb1 --- /dev/null +++ b/apps/grid-admin/src/pages/demo/Draggable.vue @@ -0,0 +1,6 @@ + + + 拖拽 + + diff --git a/apps/grid-admin/src/pages/demo/Form.vue b/apps/grid-admin/src/pages/demo/Form.vue new file mode 100644 index 00000000..229db5c3 --- /dev/null +++ b/apps/grid-admin/src/pages/demo/Form.vue @@ -0,0 +1,6 @@ + + + + diff --git a/apps/grid-admin/src/pages/demo/Table.vue b/apps/grid-admin/src/pages/demo/Table.vue new file mode 100644 index 00000000..81dde2a3 --- /dev/null +++ b/apps/grid-admin/src/pages/demo/Table.vue @@ -0,0 +1,6 @@ + + + + diff --git a/apps/grid-admin/src/pages/demo/editor/monaco-editor.vue b/apps/grid-admin/src/pages/demo/editor/monaco-editor.vue new file mode 100644 index 00000000..c66a9f90 --- /dev/null +++ b/apps/grid-admin/src/pages/demo/editor/monaco-editor.vue @@ -0,0 +1,18 @@ + + + + diff --git a/apps/grid-admin/src/pages/demo/page/index.vue b/apps/grid-admin/src/pages/demo/page/index.vue new file mode 100644 index 00000000..5db5b320 --- /dev/null +++ b/apps/grid-admin/src/pages/demo/page/index.vue @@ -0,0 +1,4 @@ + +PageTest + + diff --git a/apps/grid-admin/src/pages/demo/table/BasicTable.vue b/apps/grid-admin/src/pages/demo/table/BasicTable.vue new file mode 100644 index 00000000..2ce0dfc9 --- /dev/null +++ b/apps/grid-admin/src/pages/demo/table/BasicTable.vue @@ -0,0 +1,6 @@ + + + + diff --git a/apps/grid-admin/src/pages/sys/exception/index.vue b/apps/grid-admin/src/pages/sys/exception/index.vue new file mode 100644 index 00000000..8085eb6c --- /dev/null +++ b/apps/grid-admin/src/pages/sys/exception/index.vue @@ -0,0 +1,167 @@ + + diff --git a/apps/grid-admin/src/pages/sys/iframe/FrameBlank.vue b/apps/grid-admin/src/pages/sys/iframe/FrameBlank.vue new file mode 100644 index 00000000..7b8b46cb --- /dev/null +++ b/apps/grid-admin/src/pages/sys/iframe/FrameBlank.vue @@ -0,0 +1,3 @@ + + + diff --git a/apps/grid-admin/src/pages/sys/lock/index.vue b/apps/grid-admin/src/pages/sys/lock/index.vue new file mode 100644 index 00000000..5195a055 --- /dev/null +++ b/apps/grid-admin/src/pages/sys/lock/index.vue @@ -0,0 +1,13 @@ + + + + + + diff --git a/apps/grid-admin/src/pages/sys/lock/lock-page.vue b/apps/grid-admin/src/pages/sys/lock/lock-page.vue new file mode 100644 index 00000000..65679232 --- /dev/null +++ b/apps/grid-admin/src/pages/sys/lock/lock-page.vue @@ -0,0 +1,221 @@ + + + + + + + {{ t('sys.lock.unlock') }} + + + + + {{ hour }} + + {{ meridiem }} + + + + {{ minute }} + + + + + + + + + {{ userInfo.realName }} + + + + + {{ t('sys.lock.alert') }} + + + + {{ t('common.back') }} + + + {{ t('sys.lock.backToLogin') }} + + + {{ t('sys.lock.entry') }} + + + + + + + + + {{ hour }}:{{ minute }} {{ meridiem }} + + {{ year }}/{{ month }}/{{ day }} {{ week }} + + + + + diff --git a/apps/grid-admin/src/pages/sys/lock/use-now.ts b/apps/grid-admin/src/pages/sys/lock/use-now.ts new file mode 100644 index 00000000..ce04b7cb --- /dev/null +++ b/apps/grid-admin/src/pages/sys/lock/use-now.ts @@ -0,0 +1,60 @@ +import { dateUtil } from '@vben/utils' +import { tryOnMounted, tryOnUnmounted } from '@vben/utils' +import { reactive, toRefs } from 'vue' + +export function useNow(immediate = true) { + let timer: IntervalHandle + + const state = reactive({ + year: 0, + month: 0, + week: '', + day: 0, + hour: '', + minute: '', + second: 0, + meridiem: '', + }) + + const update = () => { + const now = dateUtil() + + const h = now.format('HH') + const m = now.format('mm') + const s = now.get('s') + + state.year = now.get('y') + state.month = now.get('M') + 1 + state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()] + state.day = now.get('date') + state.hour = h + state.minute = m + state.second = s + + state.meridiem = now.format('A') + } + + function start() { + update() + clearInterval(timer) + timer = setInterval(() => update(), 1000) + } + + function stop() { + clearInterval(timer) + } + + tryOnMounted(() => { + immediate && start() + }) + + tryOnUnmounted(() => { + stop() + }) + + return { + ...toRefs(state), + start, + stop, + } +} diff --git a/apps/grid-admin/src/pages/sys/login/forget-password-form.vue b/apps/grid-admin/src/pages/sys/login/forget-password-form.vue new file mode 100644 index 00000000..c5dc5637 --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/forget-password-form.vue @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + {{ t('common.resetText') }} + + + {{ t('sys.login.backSignIn') }} + + + + + diff --git a/apps/grid-admin/src/pages/sys/login/login-form-title.vue b/apps/grid-admin/src/pages/sys/login/login-form-title.vue new file mode 100644 index 00000000..cf793d38 --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/login-form-title.vue @@ -0,0 +1,27 @@ + + + + {{ formTitle }} + + diff --git a/apps/grid-admin/src/pages/sys/login/login-form.vue b/apps/grid-admin/src/pages/sys/login/login-form.vue new file mode 100644 index 00000000..b81002d1 --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/login-form.vue @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + {{t('sys.login.rememberMe')}} + + + + + + + + {{t('sys.login.forgetPassword')}} + + + + + + + + {{t('sys.login.loginButton')}} + + + + + + + {{t('sys.login.mobileSignInFormTitle')}} + + + + + {{t('sys.login.qrSignInFormTitle')}} + + + + + {{t('sys.login.registerButton')}} + + + + + {{t('sys.login.otherSignIn')}} + + + + + + + + + + diff --git a/apps/grid-admin/src/pages/sys/login/login.vue b/apps/grid-admin/src/pages/sys/login/login.vue new file mode 100644 index 00000000..cf837c4b --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/login.vue @@ -0,0 +1,136 @@ + + + + + + + + {{ title }} + + + + + + + {{ t('sys.login.signInTitle') }} + + + {{ t('sys.login.signInDesc') }} + + + + + + + + + + + + + + + + + + + diff --git a/apps/grid-admin/src/pages/sys/login/mobile-form.vue b/apps/grid-admin/src/pages/sys/login/mobile-form.vue new file mode 100644 index 00000000..92dfec74 --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/mobile-form.vue @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + {{ t('sys.login.loginButton') }} + + + {{ t('sys.login.backSignIn') }} + + + + + diff --git a/apps/grid-admin/src/pages/sys/login/qrcode-form.vue b/apps/grid-admin/src/pages/sys/login/qrcode-form.vue new file mode 100644 index 00000000..7d36f9bd --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/qrcode-form.vue @@ -0,0 +1,35 @@ + + + + + + + + {{ t('sys.login.scanSign') }} + + {{ t('sys.login.backSignIn') }} + + + + diff --git a/apps/grid-admin/src/pages/sys/login/register-form.vue b/apps/grid-admin/src/pages/sys/login/register-form.vue new file mode 100644 index 00000000..702b6fdf --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/register-form.vue @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t('sys.login.policy') }} + + + + + {{ t('sys.login.registerButton') }} + + + {{ t('sys.login.backSignIn') }} + + + + diff --git a/apps/grid-admin/src/pages/sys/login/use-login.ts b/apps/grid-admin/src/pages/sys/login/use-login.ts new file mode 100644 index 00000000..8e443cb8 --- /dev/null +++ b/apps/grid-admin/src/pages/sys/login/use-login.ts @@ -0,0 +1,129 @@ +import { ref, computed, unref, Ref } from 'vue' +import { useI18n } from '@vben/locale' + +export enum LoginStateEnum { + LOGIN, + REGISTER, + RESET_PASSWORD, + MOBILE, + QR_CODE, +} + +const currentState = ref(LoginStateEnum.LOGIN) + +export function useLoginState() { + function setLoginState(state: LoginStateEnum) { + currentState.value = state + } + + const getLoginState = computed(() => currentState.value) + + function handleBackLogin() { + setLoginState(LoginStateEnum.LOGIN) + } + + return { setLoginState, getLoginState, handleBackLogin } +} + +export function useFormValid(formRef: Ref) { + async function validForm() { + const form = unref(formRef) + if (!form) return + const data = await form.validate() + return data as T + } + + return { validForm } +} + +export function useFormRules(formData?: Recordable) { + const { t } = useI18n() + + const getAccountFormRule = computed(() => + createRule(t('sys.login.accountPlaceholder')), + ) + const getPasswordFormRule = computed(() => + createRule(t('sys.login.passwordPlaceholder')), + ) + const getSmsFormRule = computed(() => + createRule(t('sys.login.smsPlaceholder')), + ) + const getMobileFormRule = computed(() => + createRule(t('sys.login.mobilePlaceholder')), + ) + + const validatePolicy = async (_: any, value: boolean) => { + return !value + ? Promise.reject(t('sys.login.policyPlaceholder')) + : Promise.resolve() + } + + const validateConfirmPassword = (password: string) => { + return async (_: any, value: string) => { + if (!value) { + return Promise.reject(t('sys.login.passwordPlaceholder')) + } + if (value !== password) { + return Promise.reject(t('sys.login.diffPwd')) + } + return Promise.resolve() + } + } + + const getFormRules = computed((): { [k: string]: any | any[] } => { + const accountFormRule = unref(getAccountFormRule) + const passwordFormRule = unref(getPasswordFormRule) + const smsFormRule = unref(getSmsFormRule) + const mobileFormRule = unref(getMobileFormRule) + + const mobileRule = { + sms: smsFormRule, + mobile: mobileFormRule, + } + switch (unref(currentState)) { + // register form rules + case LoginStateEnum.REGISTER: + return { + account: accountFormRule, + password: passwordFormRule, + confirmPassword: [ + { + validator: validateConfirmPassword(formData?.password), + trigger: 'change', + }, + ], + policy: [{ validator: validatePolicy, trigger: 'change' }], + ...mobileRule, + } + + // reset password form rules + case LoginStateEnum.RESET_PASSWORD: + return { + account: accountFormRule, + ...mobileRule, + } + + // mobile form rules + case LoginStateEnum.MOBILE: + return mobileRule + + // login form rules + default: + return { + account: accountFormRule, + password: passwordFormRule, + } + } + }) + return { getFormRules } +} + +function createRule(message: string): any[] { + return [ + { + required: true, + message, + trigger: 'change', + }, + ] +} diff --git a/apps/grid-admin/src/router/guard.ts b/apps/grid-admin/src/router/guard.ts new file mode 100644 index 00000000..867a3aef --- /dev/null +++ b/apps/grid-admin/src/router/guard.ts @@ -0,0 +1,240 @@ +import type { Router } from 'vue-router' +import nProgress from 'nprogress' +import { config } from '@/config' +import { + BASIC_LOCK_PATH, + BASIC_LOGIN_PATH, + PageEnum, + PermissionModeEnum, +} from '@vben/constants' +import { useUserStoreWithout } from '@/store/user' +import { useAuthStoreWithout } from '@/store/auth' +import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic' +import { setRouteChange } from '@/logics/mitt/routeChange' +import { ROOT_ROUTE } from './routes' +import { useLockStore } from '@/store/lock' +import type { Menu } from '@vben/types' +import { useConfigStoreWithOut } from '@/store/config' +import { projectSetting } from '@/setting' +import { configureDynamicParamsMenu } from '@/router/helper' + +const LOADED_PAGE_POOL = new Map() +const LOCK_PATH = BASIC_LOCK_PATH +const LOGIN_PATH = BASIC_LOGIN_PATH +const whitePathList: string[] = [LOGIN_PATH, LOCK_PATH] +const ROOT_PATH = ROOT_ROUTE.path + +async function setupRouteGuard(router: Router) { + const { enableProgress } = config + router.beforeEach(async (to) => { + // The page has already been loaded, it will be faster to open it again, you don’t need to do loading and other processing + to.meta.loaded = !!LOADED_PAGE_POOL.get(to.path) + + // Display a progress bar at the top when switching pages + // Only works when the page is loaded for the first time + if (enableProgress && !to.meta.loaded) { + nProgress.start() + } + return true + }) + + router.afterEach((to) => { + // Indicates that the page has been loaded + // When opening again, you can turn off some progress display interactions + LOADED_PAGE_POOL.set(to.path, true) + // console.log(to) + // Close the page loading progress bar + if (enableProgress && !to.meta.loaded) { + nProgress.done() + } + }) + createAuthGuard(router) + createTabsGuard(router) + createParamMenuGuard(router) // must after createPermissionGuard (menu has been built.) +} + +export function createAuthGuard(router: Router) { + const userStore = useUserStoreWithout() + const permissionStore = useAuthStoreWithout() + const lockStore = useLockStore() + const configStore = useConfigStoreWithOut() + router.beforeEach(async (to, from, next) => { + if ( + from.path === ROOT_PATH && + to.path === PageEnum.BASE_HOME && + userStore.getUserInfo?.homePath && + userStore.getUserInfo?.homePath !== PageEnum.BASE_HOME + ) { + next(userStore.getUserInfo?.homePath) + return + } + + const token = userStore.getAccessToken + + // TODO Whitelist can be directly entered + if (whitePathList.includes(to.path as PageEnum)) { + if (to.path === LOGIN_PATH && token) { + const isSessionTimeout = userStore.getSessionTimeout + try { + await userStore.afterLoginAction() + if (!isSessionTimeout) { + next((to.query?.redirect as string) || '/') + return + } + } catch {} + } + if (to.path === LOCK_PATH && !lockStore.getLockInfo?.isLock) { + next({ path: from.path }) + return + } + next() + return + } + // token does not exist + if (!token) { + // You can access without permission. You need to set the routing meta.ignoreAuth to true + if (to.meta.ignoreAuth) { + next() + return + } + + // redirect login page + const redirectData: { + path: string + replace: boolean + query?: Recordable + } = { + path: LOGIN_PATH, + replace: true, + } + if (to.path) { + redirectData.query = { + ...redirectData.query, + redirect: to.path, + } + } + + next(redirectData) + return + } + + if (lockStore.getLockInfo?.isLock) { + // redirect lock page + const redirectData: { + path: string + replace: boolean + query?: Recordable + } = { + path: LOCK_PATH, + replace: true, + } + if (to.path) { + redirectData.query = { + ...redirectData.query, + redirect: to.path, + } + } + next(redirectData) + return + } + + // Jump to the 404 page after processing the login + if ( + from.path === LOGIN_PATH && + to.name === PAGE_NOT_FOUND_ROUTE.name && + to.fullPath !== (userStore.getUserInfo?.homePath || PageEnum.BASE_HOME) + ) { + next(userStore.getUserInfo?.homePath || PageEnum.BASE_HOME) + return + } + const { permissionMode = projectSetting.permissionMode } = + configStore.getProjectConfig + // TODO get userinfo while last fetch time is empty + if ( + userStore.getLastUpdateTime === 0 && + permissionMode == PermissionModeEnum.BACK + ) { + try { + await userStore.getUserInfoAction() + } catch (err) { + next() + return + } + } + // console.log(permissionStore.getIsDynamicAddedRoute, to) + if (permissionStore.getIsDynamicAddedRoute) { + next() + return + } + + // console.log(to.params) + const routes = await permissionStore.buildRoutesAction() + + routes.forEach((route) => { + router.addRoute(route) + }) + + router.addRoute(PAGE_NOT_FOUND_ROUTE) + + permissionStore.setDynamicAddedRoute(true) + + if (to.name === PAGE_NOT_FOUND_ROUTE.name) { + // 动态添加路由后,此处应当重定向到fullPath,否则会加载404页面内容 + next({ path: to.fullPath, replace: true, query: to.query }) + } else { + const redirectPath = (from.query.redirect || to.path) as string + const redirect = decodeURIComponent(redirectPath) + const nextData = + to.path === redirect ? { ...to, replace: true } : { path: redirect } + next(nextData) + } + }) +} + +// 路由守卫:进入路由,增加Tabs +export function createTabsGuard(router: Router) { + router.beforeEach(async (to) => { + if (whitePathList.includes(to.path)) return + // Notify routing changes + setRouteChange(to) + }) +} + +function createParamMenuGuard(router: Router) { + const authStore = useAuthStoreWithout() + router.beforeEach(async (to, _, next) => { + // filter no name route + if (!to.name) { + next() + return + } + + // menu has been built. + if (!authStore.getIsDynamicAddedRoute) { + next() + return + } + let menus: Menu[] = [] + if (isBackMode()) { + menus = authStore.getBackMenuList + } else if (isRouteMappingMode()) { + menus = authStore.getFrontMenuList + } + menus.forEach((item) => configureDynamicParamsMenu(item, to.params)) + next() + }) +} +const getPermissionMode = () => { + const configStore = useConfigStoreWithOut() + const { permissionMode = projectSetting.permissionMode } = + configStore.getProjectConfig + return permissionMode +} +const isBackMode = () => { + return getPermissionMode() === PermissionModeEnum.BACK +} +const isRouteMappingMode = () => { + return getPermissionMode() === PermissionModeEnum.ROUTE_MAPPING +} + +export { setupRouteGuard } diff --git a/apps/grid-admin/src/router/helper/index.ts b/apps/grid-admin/src/router/helper/index.ts new file mode 100644 index 00000000..4637bce6 --- /dev/null +++ b/apps/grid-admin/src/router/helper/index.ts @@ -0,0 +1,2 @@ +export * from './menu' +export * from './route' diff --git a/apps/grid-admin/src/router/helper/menu.ts b/apps/grid-admin/src/router/helper/menu.ts new file mode 100644 index 00000000..485ca352 --- /dev/null +++ b/apps/grid-admin/src/router/helper/menu.ts @@ -0,0 +1,105 @@ +import { isUrl, cloneDeep, findParentPath, mapTree } from '@vben/utils' +import { RouteParams } from 'vue-router' +import { toRaw } from 'vue' +import { Menu, MenuModule } from '@vben/types' + +export function getAllParentPath>( + treeData: T[], + path: string, +) { + const menuList = findParentPath(treeData, (n) => n.path === path) as Menu[] + return (menuList || []).map((item) => item.path) +} + +function joinParentPath(menus: RouteRecordItem[], parentPath = '') { + for (let index = 0; index < menus.length; index++) { + const menu = menus[index] + // https://next.router.vuejs.org/guide/essentials/nested-routes.html + // Note that nested paths that start with / will be treated as a root path. + // This allows you to leverage the component nesting without having to use a nested URL. + if (!(menu.path.startsWith('/') || isUrl(menu.path))) { + // path doesn't start with /, nor is it a url, join parent path + menu.path = `${parentPath}/${menu.path}` + } + if (menu?.children?.length) { + joinParentPath( + menu.children, + menu.meta?.hidePathForChildren ? parentPath : menu.path, + ) + } + } +} + +// Parsing the menu module +export function transformMenuModule(menuModule: MenuModule): Menu { + const { menu } = menuModule + + const menuList = [menu] + + joinParentPath(menuList) + return menuList[0] +} + +export function transformRouteToMenu( + routeModList: RouteRecordItem[], + routerMapping = false, +) { + const cloneRouteModList = cloneDeep(routeModList) + const routeList: RouteRecordItem[] = [] + + cloneRouteModList.forEach((item) => { + if ( + routerMapping && + item.meta?.hideChildrenInMenu && + typeof item.redirect === 'string' + ) { + item.path = item.redirect + } + if (item.meta?.single) { + const realItem = item?.children?.[0] + realItem && routeList.push(realItem) + } else { + routeList.push(item) + } + }) + const list = mapTree(routeList, { + conversion: (node: RouteRecordItem) => { + const { meta: { hideMenu = false } = {} } = node + + return { + ...(node.meta || {}), + meta: node.meta, + name: node.name, + hideMenu, + path: node.path, + ...(node.redirect ? { redirect: node.redirect } : {}), + } + }, + }) + joinParentPath(list) + return cloneDeep(list) +} + +/** + * config menu with given params + */ +const menuParamRegex = /(?::)([\s\S]+?)((?=\/)|$)/g +export function configureDynamicParamsMenu(menu: Menu, params: RouteParams) { + const { path, paramPath } = toRaw(menu) + let realPath = paramPath ? paramPath : path + const matchArr = realPath.match(menuParamRegex) + + matchArr?.forEach((it) => { + const realIt = it.substr(1) + if (params[realIt]) { + realPath = realPath.replace(`:${realIt}`, params[realIt] as string) + } + }) + // save original param path. + if (!paramPath && matchArr && matchArr.length > 0) { + menu.paramPath = path + } + menu.path = realPath + // children + menu.children?.forEach((item) => configureDynamicParamsMenu(item, params)) +} diff --git a/apps/grid-admin/src/router/helper/route.ts b/apps/grid-admin/src/router/helper/route.ts new file mode 100644 index 00000000..719a2b91 --- /dev/null +++ b/apps/grid-admin/src/router/helper/route.ts @@ -0,0 +1,182 @@ +import type { RouteMeta, Router, RouteRecordNormalized } from 'vue-router' + +import { omit, cloneDeep } from '@vben/utils' +import { createRouter, createWebHashHistory } from 'vue-router' +import { LAYOUT, PARENT_LAYOUT } from '../routes' + +export type LayoutMapKey = 'LAYOUT' + +const IFRAME = () => import('@/pages/sys/iframe/FrameBlank.vue') +export const EXCEPTION_COMPONENT = () => + import('@/pages/sys/exception/index.vue') + +const LayoutMap = new Map Promise>() + +LayoutMap.set('LAYOUT', LAYOUT) +LayoutMap.set('IFRAME', IFRAME) + +let dynamicViewsModules: Record Promise>> + +// Dynamic introduction +function asyncImportRoute(routes: RouteRecordItem[] | undefined) { + dynamicViewsModules = + dynamicViewsModules || import.meta.glob('../../pages/**/*.{vue,tsx}') + + if (!routes) return + routes.forEach((item: any) => { + if (!item.component && item.meta?.frameSrc) { + item.component = 'IFRAME' + } + const { component, name } = item + const { children } = item + if (component) { + const layoutFound = LayoutMap.get(component.toUpperCase()) + if (layoutFound) { + item.component = layoutFound + } else { + item.component = dynamicImport(dynamicViewsModules, component as string) + } + } else if (name) { + item.component = PARENT_LAYOUT() + } + children && asyncImportRoute(children) + }) +} + +function dynamicImport( + dynamicViewsModules: Record Promise>>, + component: string, +) { + const keys = Object.keys(dynamicViewsModules) + const matchKeys = keys.filter((key) => { + const k = key.replace('../../pages', '') + const startFlag = component.startsWith('/') + const endFlag = component.endsWith('.vue') || component.endsWith('.tsx') + const startIndex = startFlag ? 0 : 1 + const lastIndex = endFlag ? k.length : k.lastIndexOf('.') + return k.substring(startIndex, lastIndex) === component + }) + if (matchKeys?.length === 1) { + const matchKey = matchKeys[0] + return dynamicViewsModules[matchKey] + } else if (matchKeys?.length > 1) { + console.warn( + 'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure', + ) + return + } else { + console.warn( + '在src/views/下找不到`' + + component + + '.vue` 或 `' + + component + + '.tsx`, 请自行创建!', + ) + return EXCEPTION_COMPONENT + } +} + +// Turn background objects into routing objects +export function transformObjToRoute( + routeList: RouteRecordItem[], +): T[] { + routeList.forEach((route) => { + const _route: any = route + const component = _route.component as string + if (component) { + console.log(component) + if (component.toUpperCase() === 'LAYOUT') { + route.component = LayoutMap.get(component.toUpperCase()) + } else { + route.children = [cloneDeep(route)] + route.component = LAYOUT + route.name = `${_route.name}Parent` + route.path = '' + const meta = route.meta || ({} as RouteMeta) + meta.single = true + meta.affix = false + route.meta = meta + } + } else { + console.warn('请正确配置路由:' + _route?.name + '的component属性') + } + route.children && asyncImportRoute(route.children) + }) + return routeList as unknown as T[] +} + +/** + * Convert multi-level routing to level 2 routing + */ +export function flatMultiLevelRoutes(routeModules: RouteRecordItem[]) { + const modules: RouteRecordItem[] = cloneDeep(routeModules) + for (let index = 0; index < modules.length; index++) { + const routeModule = modules[index] + if (!isMultipleRoute(routeModule)) { + continue + } + promoteRouteLevel(routeModule) + } + return modules +} + +// Routing level upgrade +function promoteRouteLevel(routeModule: RouteRecordItem) { + // Use vue-router to splice menus + let router: Router | null = createRouter({ + routes: [routeModule as unknown as RouteRecordNormalized], + history: createWebHashHistory(), + }) + + const routes = router.getRoutes() + addToChildren(routes, routeModule.children || [], routeModule) + router = null + routeModule.children = routeModule.children?.map((item): any => + omit(item, 'children'), + ) +} + +// Add all sub-routes to the secondary route +function addToChildren( + routes: RouteRecordNormalized[], + children: RouteRecordItem[], + routeModule: RouteRecordItem, +) { + for (let index = 0; index < children.length; index++) { + const child = children[index] + const route = routes.find((item) => item.name === child.name) + if (!route) { + continue + } + routeModule.children = routeModule.children || [] + if (!routeModule.children.find((item) => item.name === route.name)) { + routeModule.children?.push(route as unknown as RouteRecordItem) + } + if (child.children?.length) { + addToChildren(routes, child.children, routeModule) + } + } +} + +// Determine whether the level exceeds 2 levels +function isMultipleRoute(routeModule: RouteRecordItem) { + if ( + !routeModule || + !Reflect.has(routeModule, 'children') || + !routeModule.children?.length + ) { + return false + } + + const children = routeModule.children + + let flag = false + for (let index = 0; index < children.length; index++) { + const child = children[index] + if (child.children?.length) { + flag = true + break + } + } + return flag +} diff --git a/apps/grid-admin/src/router/index.ts b/apps/grid-admin/src/router/index.ts new file mode 100644 index 00000000..6b9d96fa --- /dev/null +++ b/apps/grid-admin/src/router/index.ts @@ -0,0 +1,37 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import { routes } from './routes' + +export * from './menus' +export * from './helper' + +// The whitelist should contain basic static routes +const WHITE_NAME_LIST: string[] = [] +;(() => { + const getRouteNames = (routeRecords: RouteRecordItem[]) => + routeRecords.forEach((item) => { + WHITE_NAME_LIST.push(item.name) + if (item?.children?.length) { + getRouteNames(item.children) + } + }) + + getRouteNames(routes) +})() + +// app router +export const router = createRouter({ + history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH), + routes, + strict: true, + scrollBehavior: () => ({ left: 0, top: 0 }), +}) + +// reset router +export function resetRouter() { + router.getRoutes().forEach((route) => { + const { name } = route + if (name && !WHITE_NAME_LIST.includes(name as string)) { + router.hasRoute(name) && router.removeRoute(name) + } + }) +} diff --git a/apps/grid-admin/src/router/menus/index.ts b/apps/grid-admin/src/router/menus/index.ts new file mode 100644 index 00000000..f06ffa34 --- /dev/null +++ b/apps/grid-admin/src/router/menus/index.ts @@ -0,0 +1,106 @@ +import type { RouteRecordNormalized } from 'vue-router' + +import { useAuthStoreWithout } from '@/store/auth' +import { getAllParentPath } from '@/router/helper/menu' +import { isUrl, filterTree } from '@vben/utils' +import { router } from '@/router' +import { PermissionModeEnum } from '@vben/constants' +import { pathToRegexp } from 'path-to-regexp' +import { useConfigStoreWithOut } from '@/store/config' +import { Menu } from '@vben/types' + +// =========================== +// ==========Helper=========== +// =========================== + +const getPermissionMode = () => { + const appStore = useConfigStoreWithOut() + return appStore.getProjectConfig.permissionMode +} +const isBackMode = () => { + return getPermissionMode() === PermissionModeEnum.BACK +} + +const isRoleMode = () => { + return getPermissionMode() === PermissionModeEnum.ROLE +} + +async function getAsyncMenus() { + const permissionStore = useAuthStoreWithout() + if (isBackMode()) { + return permissionStore.getBackMenuList.filter( + (item) => !item.meta?.hideMenu && !item.hideMenu, + ) + } + + return permissionStore.getFrontMenuList.filter( + (item) => !item.hideMenu && !item.meta?.hideMenu, + ) +} + +export const getMenus = async (): Promise => { + const menus = await getAsyncMenus() + + if (isRoleMode()) { + const routes = router.getRoutes() + return filterTree(menus, basicFilter(routes)) + } + return menus +} + +export async function getCurrentParentPath(currentPath: string) { + const menus = await getAsyncMenus() + const allParentPath = await getAllParentPath(menus, currentPath) + return allParentPath?.[0] +} + +// Get the level 1 menu, delete children +export async function getShallowMenus(): Promise { + const menus = await getAsyncMenus() + const shallowMenuList = menus.map((item) => ({ + ...item, + children: undefined, + })) + if (isRoleMode()) { + const routes = router.getRoutes() + return shallowMenuList.filter(basicFilter(routes)) + } + return shallowMenuList +} + +// Get the children of the menu +export async function getChildrenMenus(parentPath: string) { + const menus = await getMenus() + 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 +} + +function basicFilter(routes: RouteRecordNormalized[]) { + return (menu: Menu) => { + const matchRoute = routes.find((route) => { + if (isUrl(menu.path)) return true + + if (route.meta?.carryParam) { + return pathToRegexp(route.path).test(menu.path) + } + const isSame = route.path === menu.path + if (!isSame) return false + + if (route.meta?.ignoreAuth) return true + + return isSame || pathToRegexp(route.path).test(menu.path) + }) + + if (!matchRoute) return false + menu.icon = (menu.icon || matchRoute.meta.icon) as string + menu.meta = matchRoute.meta + return true + } +} diff --git a/apps/grid-admin/src/router/routes/basic.ts b/apps/grid-admin/src/router/routes/basic.ts new file mode 100644 index 00000000..d084d2ab --- /dev/null +++ b/apps/grid-admin/src/router/routes/basic.ts @@ -0,0 +1,95 @@ +import { + REDIRECT_NAME, + PAGE_NOT_FOUND_NAME, + BASIC_HOME_PATH, +} from '@vben/constants' +import { Redirect } from '@vben/components' +import { t } from '@vben/locale' + +const LAYOUT = () => import('@/layout/index.vue') + +const PARENT_LAYOUT = () => () => + new Promise((resolve) => { + resolve({ name: 'ParentLayout' }) + }) + +const PAGE_NOT_FOUND_ROUTE: RouteRecordItem = { + path: '/:path(.*)*', + name: PAGE_NOT_FOUND_NAME, + component: LAYOUT, + meta: { + title: 'ErrorPage', + key: 333, + }, + children: [ + { + path: '/:path(.*)*', + name: PAGE_NOT_FOUND_NAME, + component: () => import('@/pages/sys/exception/index.vue'), + meta: { + title: 'ErrorPage', + key: 3333, + }, + }, + ], +} + +// 404 on a page +const REDIRECT_ROUTE: RouteRecordItem = { + path: '/redirect', + component: LAYOUT, + name: 'RedirectTo', + meta: { + title: REDIRECT_NAME, + hideBreadcrumb: true, + hideMenu: true, + }, + children: [ + { + path: '/redirect/:path(.*)', + name: REDIRECT_NAME, + component: Redirect, + meta: { + title: REDIRECT_NAME, + hideBreadcrumb: true, + }, + }, + ], +} + +const ROOT_ROUTE: RouteRecordItem = { + path: '/', + name: 'Root', + redirect: BASIC_HOME_PATH, + meta: { + title: 'Root', + }, +} + +const LOGIN_ROUTE: RouteRecordItem = { + path: '/login', + name: 'Login', + component: () => import('@/pages/sys/login/login.vue'), + meta: { + title: t('routes.basic.login'), + }, +} + +const LOCK_SCREEN_ROUTE: RouteRecordItem = { + path: '/lock', + name: 'Lock', + component: () => import('@/pages/sys/lock/index.vue'), + meta: { + title: t('routes.basic.lock'), + }, +} + +export { + LAYOUT, + PARENT_LAYOUT, + PAGE_NOT_FOUND_ROUTE, + REDIRECT_ROUTE, + ROOT_ROUTE, + LOGIN_ROUTE, + LOCK_SCREEN_ROUTE +} diff --git a/apps/grid-admin/src/router/routes/index.ts b/apps/grid-admin/src/router/routes/index.ts new file mode 100644 index 00000000..4e8cdff6 --- /dev/null +++ b/apps/grid-admin/src/router/routes/index.ts @@ -0,0 +1,30 @@ +import { + LOGIN_ROUTE, + PAGE_NOT_FOUND_ROUTE, + REDIRECT_ROUTE, + ROOT_ROUTE, + LOCK_SCREEN_ROUTE +} from './basic' + +export * from './basic' + +const routeModuleRecord = import.meta.globEager('./modules/**/*.ts') as any + +const routeModules: RouteRecordItem[] = [] + +Object.keys(routeModuleRecord).forEach((key) => { + const routeModule = routeModuleRecord[key].default || {} + routeModules.push( + ...(Array.isArray(routeModule) ? [...routeModule] : [routeModule]), + ) +}) + +export const asyncRoutes = [...routeModules] + +export const routes = [ + LOGIN_ROUTE, + LOCK_SCREEN_ROUTE, + ROOT_ROUTE, + REDIRECT_ROUTE, + PAGE_NOT_FOUND_ROUTE, +] diff --git a/apps/grid-admin/src/router/routes/modules/dashboard.ts b/apps/grid-admin/src/router/routes/modules/dashboard.ts new file mode 100644 index 00000000..c58bacef --- /dev/null +++ b/apps/grid-admin/src/router/routes/modules/dashboard.ts @@ -0,0 +1,34 @@ +import { LAYOUT } from '../basic' + +const dashboard: RouteRecordItem = { + path: '/dashboard', + name: 'Dashboard', + component: LAYOUT, + redirect: '/dashboard/analysis', + meta: { + orderNo: 1, + icon: 'mdi:monitor-dashboard', + title: 'routes.dashboard.dashboard', + }, + children: [ + { + path: 'analysis', + name: 'Analysis', + component: () => import('@/pages/dashboard/analysis/index.vue'), + meta: { + icon: 'mdi:monitor-dashboard', + title: 'routes.dashboard.analysis', + }, + }, + { + path: 'workbench', + name: 'Workbench', + component: () => import('@/pages/dashboard/workbench/index.vue'), + meta: { + title: 'routes.dashboard.workbench', + }, + }, + ], +} + +export default dashboard diff --git a/apps/grid-admin/src/router/routes/modules/demo/demo.ts b/apps/grid-admin/src/router/routes/modules/demo/demo.ts new file mode 100644 index 00000000..4dee29aa --- /dev/null +++ b/apps/grid-admin/src/router/routes/modules/demo/demo.ts @@ -0,0 +1,57 @@ +import {LAYOUT} from '../../basic' + +const dashboard: RouteRecordItem = { + path: '/demo', + name: 'Demo', + component: LAYOUT, + redirect: '/dashboard/form', + meta: { + orderNo: 2, + icon: 'line-md:clipboard-list-twotone', + title: 'routes.demo.demo', + }, + children: [ + { + path: 'table', + name: 'Table', + redirect: '/demo/form/basic', + meta: { + title: 'routes.demo.table.table', + }, + children: [{ + path: 'useTable', + name: 'UseTableDemo', + component: () => import('@/pages/demo/table/BasicTable.vue'), + meta: { + title: 'routes.demo.table.basic', + }, + }] + }, + { + path: 'form', + name: 'Form', + component: () => import('@/pages/demo/Form.vue'), + meta: { + title: 'routes.demo.form', + }, + }, + { + path: 'card', + name: 'Card', + component: () => import('@/pages/demo/Card.vue'), + meta: { + title: 'routes.demo.card', + }, + }, + { + path: 'draggable', + name: 'Draggable', + component: () => import('@/pages/demo/Draggable.vue'), + meta: { + title: '拖拽', + }, + }, + ], +} + +export default dashboard diff --git a/apps/grid-admin/src/router/routes/modules/demo/editor.ts b/apps/grid-admin/src/router/routes/modules/demo/editor.ts new file mode 100644 index 00000000..72955692 --- /dev/null +++ b/apps/grid-admin/src/router/routes/modules/demo/editor.ts @@ -0,0 +1,24 @@ +import { LAYOUT } from '@/router/routes' + +const Editor: RouteRecordItem = { + path: '/editor', + name: 'Editor', + component: LAYOUT, + meta: { + orderNo: 3, + title: '编辑器', + icon: 'openmoji:code-editor' + }, + children: [ + { + path: 'monaco-editor', + name: 'monaco-editor', + component: () => import('@/pages/demo/editor/monaco-editor.vue'), + meta: { + title: 'monaco-editor', + }, + }, + ], +} + +export default Editor diff --git a/apps/grid-admin/src/router/routes/modules/demo/page.ts b/apps/grid-admin/src/router/routes/modules/demo/page.ts new file mode 100644 index 00000000..2c7099b2 --- /dev/null +++ b/apps/grid-admin/src/router/routes/modules/demo/page.ts @@ -0,0 +1,13 @@ +const Page: RouteRecordItem = { + path: '/pageTest', + name: 'PageTest', + component: () => import('@/pages/demo/page/index.vue'), + meta: { + orderNo: 4, + title: '非框架页面', + icon: 'tabler:frame-off', + hideTab: true + }, +} + +export default Page diff --git a/apps/grid-admin/src/setting/design.ts b/apps/grid-admin/src/setting/design.ts new file mode 100644 index 00000000..6ec88ce9 --- /dev/null +++ b/apps/grid-admin/src/setting/design.ts @@ -0,0 +1,23 @@ +import { + ThemeEnum, + SIDE_BAR_BG_COLOR_LIST, + HEADER_PRESET_BG_COLOR_LIST, + APP_PRESET_COLOR_LIST +} from '@vben/constants' + + +export const namespace = 'vben' + +export const primaryColor = '#0960bd' + +export const darkMode = ThemeEnum.LIGHT + + +export { + // app theme preset color + SIDE_BAR_BG_COLOR_LIST, + // header preset color + HEADER_PRESET_BG_COLOR_LIST, + // sider preset color + APP_PRESET_COLOR_LIST +} diff --git a/apps/grid-admin/src/setting/index.ts b/apps/grid-admin/src/setting/index.ts new file mode 100644 index 00000000..0632d759 --- /dev/null +++ b/apps/grid-admin/src/setting/index.ts @@ -0,0 +1,2 @@ +export * from './design' +export * from './project' diff --git a/apps/grid-admin/src/setting/project.ts b/apps/grid-admin/src/setting/project.ts new file mode 100644 index 00000000..b0958a52 --- /dev/null +++ b/apps/grid-admin/src/setting/project.ts @@ -0,0 +1,185 @@ +import { ProjectConfig } from '@vben/types' +import { + CacheTypeEnum, + ContentLayoutEnum, + MenuModeEnum, + MenuTypeEnum, + MixSidebarTriggerEnum, + PermissionModeEnum, + SessionTimeoutProcessingEnum, + SettingButtonPositionEnum, + ThemeEnum, + TriggerEnum, + RouterTransitionEnum, +} from '@vben/constants' + +import { + SIDE_BAR_BG_COLOR_LIST, + HEADER_PRESET_BG_COLOR_LIST, + primaryColor, +} from './design' + +// ! You need to clear the browser cache after the change +export const projectSetting: ProjectConfig = { + // Whether to show the configuration button + showSettingButton: true, + + // Whether to show the theme switch button + showDarkModeToggle: true, + + // `Settings` button position + settingButtonPosition: SettingButtonPositionEnum.AUTO, + + // Permission mode + permissionMode: PermissionModeEnum.ROUTE_MAPPING, + + // Permission-related cache is stored in sessionStorage or localStorage + permissionCacheType: CacheTypeEnum.LOCAL, + + // Session timeout processing + sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP, + + // color + themeColor: primaryColor, + + // Website gray mode, open for possible mourning dates + grayMode: false, + + // Color Weakness Mode + colorWeak: false, + + // Whether to cancel the menu, the top, the multi-tab page display, for possible embedded in other systems + fullContent: false, + + // content mode + contentMode: ContentLayoutEnum.FULL, + + // Whether to display the logo + showLogo: true, + + // Whether to show footer + showFooter: false, + + // Header configuration + headerSetting: { + // header bg color + bgColor: HEADER_PRESET_BG_COLOR_LIST[0], + // Fixed at the top + fixed: true, + // Whether to show top + show: true, + // theme + theme: ThemeEnum.LIGHT, + // Whether to enable the lock screen function + useLockPage: true, + // Whether to show the full screen button + showFullScreen: true, + // Whether to show the document button + showDoc: true, + // Whether to show the notification button + showNotice: true, + // Whether to display the menu search + showSearch: true, + showLocalePicker: true, + }, + + // Menu configuration + menuSetting: { + // sidebar menu bg color + bgColor: SIDE_BAR_BG_COLOR_LIST[0], + // Whether to fix the left menu + fixed: true, + // Menu collapse + collapsed: false, + // Whether to display the menu name when folding the menu + collapsedShowTitle: false, + // Whether it can be dragged + // Only limited to the opening of the left menu, the mouse has a drag bar on the right side of the menu + canDrag: false, + // Whether to show no dom + show: true, + // Whether to show dom + hidden: false, + // Menu width + menuWidth: 210, + // Menu mode + mode: MenuModeEnum.INLINE, + // Menu type + type: MenuTypeEnum.SIDEBAR, + // Menu theme + theme: ThemeEnum.DARK, + // Split menu + split: false, + // Top menu layout + topMenuAlign: 'center', + // Fold trigger position + trigger: TriggerEnum.HEADER, + // Turn on accordion mode, only show a menu + accordion: true, + // Switch page to close menu + closeMixSidebarOnChange: false, + // Module opening method ‘click’ |'hover' + mixSideTrigger: MixSidebarTriggerEnum.CLICK, + // Fixed expanded menu + mixSideFixed: false, + }, + // Transition Setting + transitionSetting: { + // Whether to open the page switching animation + // The disabled state will also disable pageLoading + enable: true, + + // Route basic switching animation + basicTransition: RouterTransitionEnum.FADE_SIDE, + + // Whether to open page switching loading + // Only open when enable=true + openPageLoading: true, + + // Whether to open the top progress bar + openNProgress: false, + }, + + // Multi-label + multiTabsSetting: { + cache: false, + // Turn on + show: true, + // Is it possible to drag and drop sorting tabs + canDrag: true, + // Turn on quick actions + showQuick: true, + // Whether to show the refresh button + showRedo: true, + // Whether to show the collapse button + showFold: true, + }, + + // Whether to open the top progress bar + openNProgress: false, + + // Whether to enable KeepAlive cache is best to close during development, otherwise the cache needs to be cleared every time + openKeepAlive: true, + + // Automatic screen lock time, 0 does not lock the screen. Unit minute default 0 + lockTime: 0, + + // Whether to show breadcrumbs + showBreadCrumb: true, + + // Whether to show the breadcrumb icon + showBreadCrumbIcon: true, + + // Whether to open back to top + useOpenBackTop: true, + + // Is it possible to embed iframe pages + canEmbedIFramePage: true, + + // Whether to delete unclosed messages and notify when switching the interface + closeMessageOnSwitch: true, + + // Whether to cancel the http request that has been sent but not responded when switching the interface. + // If it is enabled, I want to overwrite a single interface. Can be set in a separate interface + removeAllHttpPending: false, +} diff --git a/apps/grid-admin/src/store/auth.ts b/apps/grid-admin/src/store/auth.ts new file mode 100644 index 00000000..447126cd --- /dev/null +++ b/apps/grid-admin/src/store/auth.ts @@ -0,0 +1,210 @@ +import { defineStore } from 'pinia' +import { asyncRoutes, PAGE_NOT_FOUND_ROUTE } from '@/router/routes' +import { filterTree } from '@vben/utils' +import { Menu } from '@vben/types' +import { useConfigStoreWithOut } from './config' +import { useUserStore } from './user' +import { getPermCode } from '@/apis/auth' +import { toRaw } from 'vue' +import { projectSetting } from '@/setting' +import { PermissionModeEnum, PageEnum } from '@vben/constants' +import { + flatMultiLevelRoutes, + transformObjToRoute, + transformRouteToMenu, +} from '@/router' +import { getMenuList } from '@/apis/sys' + +interface AuthState { + // Permission code list + permCodeList: string[] | number[] + // Whether the route has been dynamically added + isDynamicAddedRoute: boolean + // To trigger a menu update + lastBuildMenuTime: number + // Backstage menu list + backMenuList: Menu[] + frontMenuList: Menu[] +} + +export const useAuthStore = defineStore({ + id: 'app-auth-store', + state: (): AuthState => ({ + permCodeList: [], + // Whether the route has been dynamically added + isDynamicAddedRoute: false, + // To trigger a menu update + lastBuildMenuTime: 0, + // Backstage menu list + backMenuList: [], + // menu List + frontMenuList: [], + }), + getters: { + getPermCodeList(): string[] | number[] { + return this.permCodeList + }, + getBackMenuList(): Menu[] { + return this.backMenuList + }, + getFrontMenuList(): Menu[] { + return this.frontMenuList + }, + getLastBuildMenuTime(): number { + return this.lastBuildMenuTime + }, + getIsDynamicAddedRoute(): boolean { + return this.isDynamicAddedRoute + }, + }, + actions: { + setPermCodeList(codeList: string[]) { + this.permCodeList = codeList + }, + + setBackMenuList(list: Menu[]) { + this.backMenuList = list + list?.length > 0 && this.setLastBuildMenuTime() + }, + + setFrontMenuList(list: Menu[]) { + this.frontMenuList = list + }, + + setLastBuildMenuTime() { + this.lastBuildMenuTime = new Date().getTime() + }, + + setDynamicAddedRoute(added: boolean) { + this.isDynamicAddedRoute = added + }, + resetState(): void { + this.isDynamicAddedRoute = false + this.permCodeList = [] + this.backMenuList = [] + this.lastBuildMenuTime = 0 + }, + async changePermissionCode() { + const codeList = await getPermCode() + this.setPermCodeList(codeList) + }, + async buildRoutesAction(): Promise { + // const { t } = useI18n() + const userStore = useUserStore() + const configStore = useConfigStoreWithOut() + + let routes: RouteRecordItem[] = [] + const roleList = toRaw(userStore.getRoles) || [] + const { permissionMode = projectSetting.permissionMode } = + configStore.getProjectConfig + + const routeFilter = (route: RouteRecordItem) => { + const { meta } = route + const { roles } = meta || {} + if (!roles) return true + return roleList.some((role) => roles.includes(role)) + } + + const routeRemoveIgnoreFilter = (route: RouteRecordItem) => { + const { meta } = route + const { ignoreRoute } = meta || {} + return !ignoreRoute + } + + /** + * @description 根据设置的首页path,修正routes中的affix标记(固定首页) + * */ + const patchHomeAffix = (routes: RouteRecordItem[]) => { + if (!routes || routes.length === 0) return + let homePath: string = + userStore.getUserInfo?.homePath || PageEnum.BASE_HOME + function patcher(routes: RouteRecordItem[], parentPath = '') { + if (parentPath) parentPath = parentPath + '/' + routes.forEach((route: RouteRecordItem) => { + const { path, children, redirect } = route + const currentPath = path.startsWith('/') ? path : parentPath + path + if (currentPath === homePath) { + if (redirect) { + homePath = route.redirect! as string + } else { + route.meta = Object.assign({}, route.meta, { affix: true }) + throw new Error('end') + } + } + children && children.length > 0 && patcher(children, currentPath) + }) + } + try { + patcher(routes) + } catch (e) { + // 已处理完毕跳出循环 + } + return + } + + switch (permissionMode) { + case PermissionModeEnum.ROLE: + routes = filterTree(asyncRoutes, routeFilter) + routes = routes.filter(routeFilter) + // Convert multi-level routing to level 2 routing + routes = flatMultiLevelRoutes(routes) + break + + case PermissionModeEnum.ROUTE_MAPPING: + routes = filterTree(asyncRoutes, routeFilter) + routes = routes.filter(routeFilter) + const menuList = transformRouteToMenu(routes, true) + routes = filterTree(routes, routeRemoveIgnoreFilter) + routes = routes.filter(routeRemoveIgnoreFilter) + menuList.sort((a, b) => { + return (a.meta?.orderNo || 0) - (b.meta?.orderNo || 0) + }) + + this.setFrontMenuList(menuList as Menu[]) + + // Convert multi-level routing to level 2 routing + routes = flatMultiLevelRoutes(routes) + break + + // If you are sure that you do not need to do background dynamic permissions, please comment the entire judgment below + case PermissionModeEnum.BACK: + // const { createMessage } = useMessage() + + // createMessage.loading(t('sys.app.menuLoading')) + + // !Simulate to obtain permission codes from the background, + // this function may only need to be executed once, and the actual project can be put at the right time by itself + let routeList: RouteRecordItem[] = [] + try { + this.changePermissionCode() + routeList = (await getMenuList()) as RouteRecordItem[] + } catch (error) { + console.error(error) + } + + // Dynamically introduce components + routeList = transformObjToRoute(routeList) + + // Background routing to menu structure + const backMenuList = transformRouteToMenu(routeList) + this.setBackMenuList(backMenuList as Menu[]) + + // remove meta.ignoreRoute item + routeList = filterTree(routeList, routeRemoveIgnoreFilter) + routeList = routeList.filter(routeRemoveIgnoreFilter) + + routeList = flatMultiLevelRoutes(routeList) + routes = [PAGE_NOT_FOUND_ROUTE, ...routeList] + break + } + + patchHomeAffix(routes) + return routes + }, + }, +}) + +// Need to be used outside the setup +export function useAuthStoreWithout() { + return useAuthStore() +} diff --git a/apps/grid-admin/src/store/config.ts b/apps/grid-admin/src/store/config.ts new file mode 100644 index 00000000..c6d54ae0 --- /dev/null +++ b/apps/grid-admin/src/store/config.ts @@ -0,0 +1,87 @@ +import { + BeforeMiniState, + HeaderSetting, + MenuSetting, + MultiTabsSetting, + ProjectConfig, + TransitionSetting, +} from '@vben/types' +import { defineStore } from 'pinia' +import { deepMerge } from '@vben/utils' +import { APP_DARK_MODE_KEY_, ThemeEnum } from '@vben/constants' +import { darkMode, projectSetting } from '@/setting' + +export interface ConfigStoreState { + darkMode?: ThemeEnum + // Page loading status + pageLoading: boolean + // project config + projectConfig: ProjectConfig | null + // When the window shrinks, remember some states, and restore these states when the window is restored + beforeMiniInfo: BeforeMiniState +} + +export const useConfigStore = defineStore({ + id: 'app-config-store', + persist: { + paths: ['darkMode', 'projectConfig'], + }, + state: (): ConfigStoreState => ({ + darkMode: undefined, + pageLoading: false, + projectConfig: {} as any, + beforeMiniInfo: {}, + }), + getters: { + getDarkMode(): ThemeEnum { + return ( + this.darkMode || + (localStorage.getItem(APP_DARK_MODE_KEY_) as ThemeEnum) || + darkMode + ) + }, + getBeforeMiniInfo(): BeforeMiniState { + return this.beforeMiniInfo + }, + getProjectConfig(): ProjectConfig { + return this.projectConfig || ({} as ProjectConfig) + }, + getHeaderSetting(): HeaderSetting { + return this.getProjectConfig.headerSetting + }, + getMenuSetting(): MenuSetting { + return this.getProjectConfig.menuSetting + }, + getMultiTabsSetting(): MultiTabsSetting { + return this.getProjectConfig.multiTabsSetting + }, + getPageLoading(): boolean { + return this.pageLoading + }, + getTransitionSetting(): TransitionSetting { + return this.getProjectConfig.transitionSetting + }, + }, + actions: { + setBeforeMiniInfo(state: BeforeMiniState): void { + this.beforeMiniInfo = state + }, + setProjectConfig(config: DeepPartial): void { + this.projectConfig = deepMerge(this.projectConfig || {}, config) + }, + setDarkMode(mode: ThemeEnum): void { + this.darkMode = mode + localStorage.setItem(APP_DARK_MODE_KEY_, mode) + }, + async setPageLoadingAction(loading: boolean): Promise { + console.log(loading) + }, + resetProjectConfig() { + this.setProjectConfig(projectSetting) + }, + }, +}) + +export function useConfigStoreWithOut() { + return useConfigStore() +} diff --git a/apps/grid-admin/src/store/lock.ts b/apps/grid-admin/src/store/lock.ts new file mode 100644 index 00000000..6296215a --- /dev/null +++ b/apps/grid-admin/src/store/lock.ts @@ -0,0 +1,55 @@ +import type { LockInfo } from '@vben/types' +import { defineStore } from 'pinia' +import { useUserStore } from '@/store/user' + +interface LockState { + lockInfo: Nullable +} + +export const useLockStore = defineStore({ + id: 'app-lock', + persist: { + paths: ['lockInfo'], + }, + state: (): LockState => ({ + lockInfo: {}, + }), + getters: { + getLockInfo(): Nullable { + return this.lockInfo + }, + }, + actions: { + setLockInfo(info: LockInfo) { + this.lockInfo = Object.assign({}, this.lockInfo, info) + }, + resetLockInfo() { + this.lockInfo = null + }, + async unLock(password?: string) { + const userStore = useUserStore() + if (this.lockInfo?.pwd === password) { + this.resetLockInfo() + return true + } + const tryLogin = async () => { + try { + const username = userStore.getUserInfo?.username ?? '' + const res = await userStore.login({ + username, + password: password!, + goHome: false, + mode: 'none', + }) + if (res) { + this.resetLockInfo() + } + return res + } catch (error) { + return false + } + } + return await tryLogin() + }, + }, +}) diff --git a/apps/grid-admin/src/store/modules/app.ts b/apps/grid-admin/src/store/modules/app.ts new file mode 100644 index 00000000..6b1afc8c --- /dev/null +++ b/apps/grid-admin/src/store/modules/app.ts @@ -0,0 +1,45 @@ +import { defineStore } from 'pinia' +// import { pinia } from '@/pinia' +import { resetRouter } from '@/router' + +interface AppState { + pageLoading: boolean +} +let timeId: TimeoutHandle +export const useAppStore = defineStore({ + id: 'app', + state: (): AppState => ({ + pageLoading: false, + }), + getters: { + getPageLoading(): boolean { + return this.pageLoading + }, + }, + actions: { + setPageLoading(loading: boolean): void { + this.pageLoading = loading + }, + async resetAllState() { + resetRouter() + // Persistent.clearAll() + }, + async setPageLoadingAction(loading: boolean): Promise { + if (loading) { + clearTimeout(timeId) + // Prevent flicker + timeId = setTimeout(() => { + this.setPageLoading(loading) + }, 50) + } else { + this.setPageLoading(loading) + clearTimeout(timeId) + } + }, + }, +}) + +// Need to be used outside the setup +export function useAppStoreWithOut() { + return useAppStore() +} diff --git a/apps/grid-admin/src/store/multipleTab.ts b/apps/grid-admin/src/store/multipleTab.ts new file mode 100644 index 00000000..fd896678 --- /dev/null +++ b/apps/grid-admin/src/store/multipleTab.ts @@ -0,0 +1,387 @@ +import type { + RouteLocationNormalized, + RouteLocationRaw, + Router, +} from 'vue-router' +import { toRaw, unref } from 'vue' +import { defineStore } from 'pinia' + +import { useGo, useRedo } from '@vben/hooks' +// import { Persistent } from '/@/utils/cache/persistent'; + +import { PageEnum } from '@vben/constants' +import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '@/router/routes/basic' +import { getRawRoute, RemovableRef } from '@vben/utils' +import { useUserStore } from '@/store/user' +// import { useRouter, useRoute } from 'vue-router' +// import { router } from '@/router' +// import {debug} from "util"; +// +// import projectSetting from '/@/settings/projectSetting'; +// import { useUserStore } from '/@/store/user'; +// useUserStore +export interface MultipleTabState { + cacheTabList: Set + tabList: RouteLocationNormalized[] | RemovableRef + lastDragEndIndex: number +} + +function handleGotoPage(router: Router) { + const go = useGo(router) + go(unref(router.currentRoute).path, true) +} + +const getToTarget = (tabItem: RouteLocationNormalized) => { + const { params, path, query } = tabItem + return { + params: params || {}, + path, + query: query || {}, + } +} +// const TabsStorage = useLocalStorage('MULTIPLE_TABS_KEY', []) +// const cacheTab = true +// const cacheTab = projectSetting.multiTabsSetting.cache + +export const useMultipleTabStore = defineStore({ + id: 'app-multiple-tab', + persist: { + paths: ['tabList'], + }, + state: (): MultipleTabState => ({ + // Tabs that need to be cached + cacheTabList: new Set(), + // multiple tab list + tabList: [], + // Index of the last moved tab + lastDragEndIndex: 0, + }), + getters: { + getTabList(): RouteLocationNormalized[] { + return this.tabList + }, + getCachedTabList(): string[] { + return Array.from(this.cacheTabList) + }, + getLastDragEndIndex(): number { + return this.lastDragEndIndex + }, + }, + actions: { + /** + * Update the cache according to the currently opened tabs + */ + async updateCacheTab() { + const cacheMap: Set = new Set() + + for (const tab of this.tabList) { + const item = getRawRoute(tab) + // Ignore the cache + const needCache = !item.meta?.ignoreKeepAlive + if (!needCache) { + continue + } + const name = item.name as string + cacheMap.add(name) + } + this.cacheTabList = cacheMap + }, + + /** + * Refresh tabs + */ + async refreshPage(router: Router) { + const { currentRoute } = router + const route = unref(currentRoute) + const name = route.name + + const findTab = this.getCachedTabList.find((item) => item === name) + if (findTab) { + this.cacheTabList.delete(findTab) + } + const redo = useRedo(router) + await redo() + }, + clearCacheTabs(): void { + this.cacheTabList = new Set() + }, + resetState(): void { + this.tabList = [] + this.clearCacheTabs() + }, + goToPage(router: Router) { + const go = useGo(router) + const len = this.tabList.length + const { path } = unref(router.currentRoute) + + let toPath: PageEnum | string = PageEnum.BASE_HOME + + if (len > 0) { + const page = this.tabList[len - 1] + const p = page.fullPath || page.path + if (p) { + toPath = p + } + } + // Jump to the current page and report an error + path !== toPath && go(toPath as PageEnum, true) + }, + async checkTab(route: RouteLocationNormalized) { + // await router.isReady() + + const { path, name, meta } = getRawRoute(route) + // 404 The page does not need to add a tab + if ( + [PageEnum.ERROR_PAGE, PageEnum.BASE_LOGIN, PageEnum.BASE_LOCK].includes( + path as PageEnum, + ) || + meta?.hideTab || + !name || + [REDIRECT_ROUTE.name, PAGE_NOT_FOUND_ROUTE.name].includes( + name as string, + ) + ) { + return + } + await this.addTab(route) + }, + async addTab(route: RouteLocationNormalized) { + const { path, fullPath, params, query, meta } = getRawRoute(route) + + let updateIndex = -1 + // Existing pages, do not add tabs repeatedly + const tabHasExits = this.tabList.some((tab, index) => { + updateIndex = index + return (tab.fullPath || tab.path) === (fullPath || path) + }) + // If the tab already exists, perform the update operation + if (tabHasExits) { + const curTab = toRaw(this.tabList)[updateIndex] + if (!curTab) { + return + } + curTab.params = params || curTab.params + curTab.query = query || curTab.query + curTab.fullPath = fullPath || curTab.fullPath + this.tabList.splice(updateIndex, 1, curTab) + } else { + // Add tab + // 获取动态路由打开数,超过 0 即代表需要控制打开数 + const dynamicLevel = meta?.dynamicLevel ?? -1 + if (dynamicLevel > 0) { + // 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了 + // 首先获取到真实的路由,使用配置方式减少计算开销. + // const realName: string = path.match(/(\S*)\//)![1]; + const realPath = meta?.realPath ?? '' + // 获取到已经打开的动态路由数, 判断是否大于某一个值 + if ( + this.tabList.filter((e) => e.meta?.realPath ?? '' === realPath) + .length >= dynamicLevel + ) { + // 关闭第一个 + const index = this.tabList.findIndex( + (item) => item.meta.realPath === realPath, + ) + index !== -1 && this.tabList.splice(index, 1) + } + } + this.tabList.push(route) + } + await this.updateCacheTab() + // cacheTab && Persistent.setLocal(MULTIPLE_TABS_KEY, this.tabList) + }, + + async closeTab(tab: RouteLocationNormalized, router: Router) { + const close = (route: RouteLocationNormalized) => { + const { fullPath, meta: { affix } = {} } = route + if (affix) { + return + } + const index = this.tabList.findIndex( + (item) => item.fullPath === fullPath, + ) + index !== -1 && this.tabList.splice(index, 1) + } + + const { currentRoute, replace } = router + + const { path } = unref(currentRoute) + if (path !== tab.path) { + // Closed is not the activation tab + close(tab) + return + } + + // Closed is activated atb + let toTarget: RouteLocationRaw = {} + + const index = this.tabList.findIndex((item) => item.path === path) + + // If the current is the leftmost tab + if (index === 0) { + // There is only one tab, then jump to the homepage, otherwise jump to the right tab + if (this.tabList.length === 1) { + const userStore = useUserStore() + toTarget = userStore.getUserInfo?.homePath || PageEnum.BASE_HOME + } else { + // Jump to the right tab + const page = this.tabList[index + 1] + toTarget = getToTarget(page) + } + } else { + // Close the current tab + const page = this.tabList[index - 1] + toTarget = getToTarget(page) + } + close(currentRoute.value) + await replace(toTarget) + }, + + // Close according to key + async closeTabByKey(key: string, router: Router) { + const index = this.tabList.findIndex( + (item) => (item.fullPath || item.path) === key, + ) + if (index !== -1) { + await this.closeTab(this.tabList[index], router) + const { currentRoute, replace } = router + // 检查当前路由是否存在于tabList中 + const isActivated = this.tabList.findIndex((item) => { + return item.fullPath === currentRoute.value.fullPath + }) + // 如果当前路由不存在于TabList中,尝试切换到其它路由 + if (isActivated === -1) { + let pageIndex + if (index > 0) { + pageIndex = index - 1 + } else if (index < this.tabList.length - 1) { + pageIndex = index + 1 + } else { + pageIndex = -1 + } + if (pageIndex >= 0) { + const page = this.tabList[index - 1] + const toTarget = getToTarget(page) + await replace(toTarget) + } + } + } + }, + + // Sort the tabs + async sortTabs(oldIndex: number, newIndex: number) { + const currentTab = this.tabList[oldIndex] + this.tabList.splice(oldIndex, 1) + this.tabList.splice(newIndex, 0, currentTab) + this.lastDragEndIndex = this.lastDragEndIndex + 1 + }, + + // Close the tab on the right and jump + async closeLeftTabs(route: RouteLocationNormalized, router: Router) { + const index = this.tabList.findIndex((item) => item.path === route.path) + + if (index > 0) { + const leftTabs = this.tabList.slice(0, index) + const pathList: string[] = [] + for (const item of leftTabs) { + const affix = item?.meta?.affix ?? false + if (!affix) { + pathList.push(item.fullPath) + } + } + this.bulkCloseTabs(pathList) + } + this.updateCacheTab() + handleGotoPage(router) + }, + + // Close the tab on the left and jump + async closeRightTabs(route: RouteLocationNormalized, router: Router) { + const index = this.tabList.findIndex( + (item) => item.fullPath === route.fullPath, + ) + + if (index >= 0 && index < this.tabList.length - 1) { + const rightTabs = this.tabList.slice(index + 1, this.tabList.length) + + const pathList: string[] = [] + for (const item of rightTabs) { + const affix = item?.meta?.affix ?? false + if (!affix) { + pathList.push(item.fullPath) + } + } + this.bulkCloseTabs(pathList) + } + this.updateCacheTab() + handleGotoPage(router) + }, + + async closeAllTab(router: Router) { + this.tabList = this.tabList.filter((item) => item?.meta?.affix ?? false) + this.clearCacheTabs() + this.goToPage(router) + }, + + /** + * Close other tabs + */ + async closeOtherTabs(route: RouteLocationNormalized, router: Router) { + const closePathList = this.tabList.map((item) => item.fullPath) + + const pathList: string[] = [] + + for (const path of closePathList) { + if (path !== route.fullPath) { + const closeItem = this.tabList.find((item) => item.path === path) + if (!closeItem) { + continue + } + const affix = closeItem?.meta?.affix ?? false + if (!affix) { + pathList.push(closeItem.fullPath) + } + } + } + this.bulkCloseTabs(pathList) + this.updateCacheTab() + handleGotoPage(router) + }, + + /** + * Close tabs in bulk + */ + async bulkCloseTabs(pathList: string[]) { + this.tabList = this.tabList.filter( + (item) => !pathList.includes(item.fullPath), + ) + }, + + /** + * Set tab's title + */ + async setTabTitle(title: string, route: RouteLocationNormalized) { + const findTab = this.getTabList.find((item) => item === route) + if (findTab) { + findTab.meta.title = title + await this.updateCacheTab() + } + }, + /** + * replace tab's path + * **/ + async updateTabPath(fullPath: string, route: RouteLocationNormalized) { + const findTab = this.getTabList.find((item) => item === route) + if (findTab) { + findTab.fullPath = fullPath + findTab.path = fullPath + await this.updateCacheTab() + } + }, + }, +}) + +// Need to be used outside the setup +export function useMultipleTabWithOut() { + return useMultipleTabStore() +} diff --git a/apps/grid-admin/src/store/test.ts b/apps/grid-admin/src/store/test.ts new file mode 100644 index 00000000..6b6e0c3b --- /dev/null +++ b/apps/grid-admin/src/store/test.ts @@ -0,0 +1,9 @@ +import { defineStore } from 'pinia' + +export const useTest = defineStore({ + id: 'use--test', + state: () => ({ + test: '人孔盖福利卡', + }), + persist: true, +}) diff --git a/apps/grid-admin/src/store/user.ts b/apps/grid-admin/src/store/user.ts new file mode 100644 index 00000000..0c1edfee --- /dev/null +++ b/apps/grid-admin/src/store/user.ts @@ -0,0 +1,166 @@ +import type { LoginParams } from '@/apis/auth' +import { defineStore } from 'pinia' +import { BASIC_HOME_PATH, BASIC_LOGIN_PATH, PageEnum } from '@vben/constants' +// import { pinia } from '@/pinia' +import { router } from '@/router' +import { doLogoutApi, getUserInfoApi, doLoginApi } from '@/apis/auth' +import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes' +import { useAuthStoreWithout } from './auth' +import { GetUserInfoModel } from '@/apis/sys/user' +import { UserInfo, RoleInfo } from '@vben/types' +import { ErrorMessageMode } from '@vben/types' +import { isArray } from '@vben/utils' + +interface UserState { + userInfo: Nullable + accessToken?: string + roles: RoleInfo[] + sessionTimeout?: boolean + lastUpdateTime: number +} + +export const useUserStore = defineStore({ + id: 'app-user-store', + persist: { + paths: ['userInfo', 'accessToken', 'roles'], + }, + state: (): UserState => ({ + userInfo: null, + accessToken: undefined, + roles: [], + // Whether the login expired + sessionTimeout: false, + // Last fetch time + lastUpdateTime: 0, + }), + getters: { + getUserInfo(): UserInfo | null { + return this.userInfo + }, + getAccessToken(): string | undefined { + return this.accessToken + }, + getRoles(): RoleInfo[] { + return this.roles.length > 0 ? this.roles : [] + }, + getSessionTimeout(): boolean { + return !!this.sessionTimeout + }, + getLastUpdateTime(): number { + return this.lastUpdateTime + }, + }, + actions: { + setAccessToken(info: string | undefined) { + this.accessToken = info ? info : '' + }, + setRoles(roles: RoleInfo[]) { + this.roles = roles + }, + setUserInfo(info: UserInfo | null) { + this.userInfo = info + this.lastUpdateTime = new Date().getTime() + }, + setSessionTimeout(flag: boolean) { + this.sessionTimeout = flag + }, + resetState() { + this.userInfo = null + this.accessToken = undefined + this.roles = [] + this.sessionTimeout = false + }, + + async login( + params: LoginParams & { + goHome?: boolean + mode?: ErrorMessageMode + }, + ): Promise { + try { + const { goHome = true, mode, ...loginParams } = params + const { accessToken } = await doLoginApi(loginParams, mode) + + // save token + this.setAccessToken(accessToken) + if (!this.getAccessToken) { + return null + } + const userInfo = await this.getUserInfoAction() + if (goHome) { + await router.replace(BASIC_HOME_PATH) + } + return userInfo + } catch (error) { + return Promise.reject(error) + } + }, + async afterLoginAction(goHome?: boolean): Promise { + if (!this.getAccessToken) { + return null + } + // get user info + const userInfo = await this.getUserInfoAction() + + const sessionTimeout = this.sessionTimeout + if (sessionTimeout) { + this.setSessionTimeout(false) + } else { + const permissionStore = useAuthStoreWithout() + if (!permissionStore.isDynamicAddedRoute) { + const routes = await permissionStore.buildRoutesAction() + routes.forEach((route) => { + router.addRoute(route) + }) + router.addRoute(PAGE_NOT_FOUND_ROUTE) + permissionStore.setDynamicAddedRoute(true) + } + goHome && + (await router.replace(userInfo?.homePath || PageEnum.BASE_HOME)) + } + return userInfo + }, + + async getUserInfoAction(): Promise { + if (!this.getAccessToken) { + return null + } + + const userInfo = (await getUserInfoApi()) as unknown as UserInfo + const { roles = [] } = userInfo + if (isArray(roles)) { + const roleList = roles.map( + (item) => item.value, + ) as unknown as RoleInfo[] + this.setRoles(roleList) + } else { + userInfo.roles = [] + this.setRoles([]) + } + this.setUserInfo(userInfo) + + return userInfo + }, + + async logout(goLogin = false) { + if (this.getAccessToken) { + try { + await doLogoutApi() + } catch (error: any) { + console.log('logout error:' + error.toString()) + } + } + this.setAccessToken(undefined) + this.setSessionTimeout(false) + this.setUserInfo(null) + if (goLogin) { + router.push(BASIC_LOGIN_PATH) + } + }, + }, +}) + +// Need to be used outside the setup +export function useUserStoreWithout() { + return useUserStore() +} diff --git a/apps/grid-admin/tsconfig.json b/apps/grid-admin/tsconfig.json new file mode 100644 index 00000000..7cfe3b67 --- /dev/null +++ b/apps/grid-admin/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "#/*": ["src/*"] + }, + "lib": ["dom", "esnext"], + "types": [ + "vite/client", + "@vben/constant", + "@vben/types/shims/pinia", + "@vben/types/shims/vue-router", + ] + }, + "include": [ + "src", + "mock", + "vite.config.ts", + "../../node_modules/.pnpm/vue@3.2.45/node_modules/vue/macros-global.d.ts" + ] +} diff --git a/apps/grid-admin/vite.config.ts b/apps/grid-admin/vite.config.ts new file mode 100644 index 00000000..62f5a739 --- /dev/null +++ b/apps/grid-admin/vite.config.ts @@ -0,0 +1,7 @@ +import { createViteConfig } from '@config/vite' +import { defineConfig, UserConfig } from 'vite' + +export default defineConfig(async ({ command, mode }): Promise => { + const cfg = await createViteConfig(command, mode, process.cwd(), { preset: 'antd' }) + return cfg +}) diff --git a/configs/vite/src/plugins/unocss.ts b/configs/vite/src/plugins/unocss.ts index 6a534e6f..2b6dd01e 100644 --- a/configs/vite/src/plugins/unocss.ts +++ b/configs/vite/src/plugins/unocss.ts @@ -11,6 +11,7 @@ export function configUnocssPlugin() { presets: [presetIcons(), presetMini({ dark: 'class' })], shortcuts: { 'flex-center': 'flex justify-center items-center', + 'grid-center': 'grid place-content-center', }, theme: { colors: { diff --git a/packages/constants/src/appEnum.ts b/packages/constants/src/appEnum.ts index 8fe8a722..90f32045 100644 --- a/packages/constants/src/appEnum.ts +++ b/packages/constants/src/appEnum.ts @@ -11,6 +11,20 @@ export enum ThemeEnum { LIGHT = 'light', } +/** + * @description: Navigation bar mode + */ +export enum NavBarModeEnum { + // left menu + SIDEBAR = 'sidebar', + // mix-sidebar + MIX_SIDEBAR = 'mix-sidebar', + // mixin menu + MIX = 'mix', + // top menu + TOP_MENU = 'top-menu', +} + export enum SettingButtonPositionEnum { AUTO = 'auto', HEADER = 'header', diff --git a/packages/constants/src/menuEnum.ts b/packages/constants/src/menuEnum.ts index 5903c3e1..98289e33 100644 --- a/packages/constants/src/menuEnum.ts +++ b/packages/constants/src/menuEnum.ts @@ -18,6 +18,8 @@ export enum TriggerEnum { NONE = 'NONE', // 菜单底部 FOOTER = 'FOOTER', + // 菜单中间 + CENTER = 'CENTER', // 头部 HEADER = 'HEADER', } diff --git a/packages/grid-layouts/index.ts b/packages/grid-layouts/index.ts new file mode 100644 index 00000000..25ad9b79 --- /dev/null +++ b/packages/grid-layouts/index.ts @@ -0,0 +1 @@ +export { default as Layout } from './src/index.vue' diff --git a/packages/grid-layouts/package.json b/packages/grid-layouts/package.json new file mode 100644 index 00000000..966f580c --- /dev/null +++ b/packages/grid-layouts/package.json @@ -0,0 +1,25 @@ +{ + "name": "@vben/grid-layouts", + "version": "3.0.0-bata.1", + "main": "index.ts", + "module": "index.ts", + "license": "MIT", + "scripts": { + "clean": "pnpm rimraf node_modules && pnpm rimraf dist && pnpm rimraf .turbo" + }, + "dependencies": { + "@vben/constants": "workspace:*", + "@vben/directives": "workspace:*", + "@vben/hooks": "workspace:*", + "@vben/locale": "workspace:*", + "@vben/stores": "workspace:*", + "@vben/utils": "workspace:*", + "@vben/vbencomponents": "workspace:*", + "pinia": "^2.0.30", + "vue": "3.2.45", + "vue-router": "^4.1.6" + }, + "devDependencies": { + "@vben/types": "workspace:*" + } +} diff --git a/packages/grid-layouts/src/components/Layout.vue b/packages/grid-layouts/src/components/Layout.vue new file mode 100644 index 00000000..65e9cd9d --- /dev/null +++ b/packages/grid-layouts/src/components/Layout.vue @@ -0,0 +1,65 @@ + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/Main.vue b/packages/grid-layouts/src/components/Main.vue new file mode 100644 index 00000000..7ca3398d --- /dev/null +++ b/packages/grid-layouts/src/components/Main.vue @@ -0,0 +1,26 @@ + + + + headerWidth: {{ headerWidth }}, headerHeight: {{ headerHeight }} + ___________________________________________________________________________ + useAppConfig : {{ sidebar }} + isMobile: {{ isMobile }} + + + 大丰收的 + + + + + diff --git a/packages/grid-layouts/src/components/breadcrumb/index.vue b/packages/grid-layouts/src/components/breadcrumb/index.vue new file mode 100644 index 00000000..bfb0aba5 --- /dev/null +++ b/packages/grid-layouts/src/components/breadcrumb/index.vue @@ -0,0 +1,13 @@ + + + + + + + + breadcrumb + diff --git a/packages/grid-layouts/src/components/comm/SecondaryBorder.vue b/packages/grid-layouts/src/components/comm/SecondaryBorder.vue new file mode 100644 index 00000000..f69f0c49 --- /dev/null +++ b/packages/grid-layouts/src/components/comm/SecondaryBorder.vue @@ -0,0 +1,26 @@ + + + + diff --git a/packages/grid-layouts/src/components/comm/TopButtonWrapper.vue b/packages/grid-layouts/src/components/comm/TopButtonWrapper.vue new file mode 100644 index 00000000..2ae01435 --- /dev/null +++ b/packages/grid-layouts/src/components/comm/TopButtonWrapper.vue @@ -0,0 +1,9 @@ + + + + + + + diff --git a/packages/grid-layouts/src/components/composables/LayoutContent.vue b/packages/grid-layouts/src/components/composables/LayoutContent.vue new file mode 100644 index 00000000..3bf776e7 --- /dev/null +++ b/packages/grid-layouts/src/components/composables/LayoutContent.vue @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/packages/grid-layouts/src/components/composables/LayoutFooter.vue b/packages/grid-layouts/src/components/composables/LayoutFooter.vue new file mode 100644 index 00000000..6889813f --- /dev/null +++ b/packages/grid-layouts/src/components/composables/LayoutFooter.vue @@ -0,0 +1,15 @@ + + + + diff --git a/packages/grid-layouts/src/components/composables/LayoutHeader.vue b/packages/grid-layouts/src/components/composables/LayoutHeader.vue new file mode 100644 index 00000000..e4687b56 --- /dev/null +++ b/packages/grid-layouts/src/components/composables/LayoutHeader.vue @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/composables/LayoutSidebar.vue b/packages/grid-layouts/src/components/composables/LayoutSidebar.vue new file mode 100644 index 00000000..4198b098 --- /dev/null +++ b/packages/grid-layouts/src/components/composables/LayoutSidebar.vue @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/composables/LayoutTab.vue b/packages/grid-layouts/src/components/composables/LayoutTab.vue new file mode 100644 index 00000000..63a847f5 --- /dev/null +++ b/packages/grid-layouts/src/components/composables/LayoutTab.vue @@ -0,0 +1,27 @@ + + + + + Multiple Tab + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/constant.ts b/packages/grid-layouts/src/components/constant.ts new file mode 100644 index 00000000..98080870 --- /dev/null +++ b/packages/grid-layouts/src/components/constant.ts @@ -0,0 +1,112 @@ +import { + MenuModeEnum, + MenuTypeEnum, + ContentLayoutEnum, + TopMenuAlignEnum, + TriggerEnum, + RouterTransitionEnum, + MixSidebarTriggerEnum, +} from '@vben/constants' +import { useI18n } from '@vben/locale' + +const { t } = useI18n() + +export const navigationBarTypeList = [ + { + title: t('layout.setting.menuTypeSidebar'), + mode: MenuModeEnum.INLINE, + type: MenuTypeEnum.SIDEBAR, + }, + { + title: t('layout.setting.menuTypeMix'), + mode: MenuModeEnum.INLINE, + type: MenuTypeEnum.MIX, + }, + + { + title: t('layout.setting.menuTypeTopMenu'), + mode: MenuModeEnum.HORIZONTAL, + type: MenuTypeEnum.TOP_MENU, + }, + { + title: t('layout.setting.menuTypeMixSidebar'), + mode: MenuModeEnum.INLINE, + type: MenuTypeEnum.MIX_SIDEBAR, + }, +] + +export const contentModeOptions = [ + { + value: ContentLayoutEnum.FULL, + label: t('layout.setting.contentModeFull'), + }, + { + value: ContentLayoutEnum.FIXED, + label: t('layout.setting.contentModeFixed'), + }, +] + +export const topMenuAlignOptions = [ + { + value: TopMenuAlignEnum.CENTER, + label: t('layout.setting.topMenuAlignRight'), + }, + { + value: TopMenuAlignEnum.START, + label: t('layout.setting.topMenuAlignLeft'), + }, + { + value: TopMenuAlignEnum.END, + label: t('layout.setting.topMenuAlignCenter'), + }, +] + +export const getMenuTriggerOptions = (split: boolean) => { + return [ + { + value: TriggerEnum.NONE, + label: t('layout.setting.menuTriggerNone'), + }, + { + value: TriggerEnum.FOOTER, + label: t('layout.setting.menuTriggerBottom'), + }, + { + value: TriggerEnum.CENTER, + label: t('layout.setting.menuTriggerCenter'), + }, + ...(split + ? [] + : [ + { + value: TriggerEnum.HEADER, + label: t('layout.setting.menuTriggerTop'), + }, + ]), + ] +} + +export const routerTransitionOptions = [ + RouterTransitionEnum.ZOOM_FADE, + RouterTransitionEnum.FADE, + RouterTransitionEnum.ZOOM_OUT, + RouterTransitionEnum.FADE_SIDE, + RouterTransitionEnum.FADE_BOTTOM, + RouterTransitionEnum.FADE_SCALE, +].map((item) => { + return { + label: item, + value: item, + } +}) + +export const mixSidebarTriggerOptions = [ + { + value: MixSidebarTriggerEnum.HOVER, + label: t('layout.setting.triggerHover'), + }, + { + value: MixSidebarTriggerEnum.CLICK, + label: t('layout.setting.triggerClick'), + }, +] diff --git a/packages/grid-layouts/src/components/feature/index.vue b/packages/grid-layouts/src/components/feature/index.vue new file mode 100644 index 00000000..df3d2c9c --- /dev/null +++ b/packages/grid-layouts/src/components/feature/index.vue @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/grid-layouts/src/components/logo/index.vue b/packages/grid-layouts/src/components/logo/index.vue new file mode 100644 index 00000000..93436a3c --- /dev/null +++ b/packages/grid-layouts/src/components/logo/index.vue @@ -0,0 +1,51 @@ + + + + + + + + + {{ title }} + + + + diff --git a/packages/grid-layouts/src/components/notify/index.vue b/packages/grid-layouts/src/components/notify/index.vue new file mode 100644 index 00000000..7f0f7655 --- /dev/null +++ b/packages/grid-layouts/src/components/notify/index.vue @@ -0,0 +1,9 @@ + + + + + + + diff --git a/packages/grid-layouts/src/components/render.ts b/packages/grid-layouts/src/components/render.ts new file mode 100644 index 00000000..4e287e53 --- /dev/null +++ b/packages/grid-layouts/src/components/render.ts @@ -0,0 +1,7 @@ +import { h } from 'vue' +import { VbenIconify } from '@vben/vbencomponents' + +export const renderIcon = (icon: string) => { + if (!icon) return undefined + return () => h(VbenIconify, { icon }) +} diff --git a/packages/grid-layouts/src/components/setting-drawer/components/DarkModeToggle.vue b/packages/grid-layouts/src/components/setting-drawer/components/DarkModeToggle.vue new file mode 100644 index 00000000..68bcaaae --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/DarkModeToggle.vue @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/HandleButtons.vue b/packages/grid-layouts/src/components/setting-drawer/components/HandleButtons.vue new file mode 100644 index 00000000..2e80e63b --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/HandleButtons.vue @@ -0,0 +1,30 @@ + + + + + + + + {{ t('layout.setting.copyBtn') }} + + + + + + {{ t('common.resetText') }} + + + + + + {{ t('layout.setting.clearBtn') }} + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/InputNumberItem.vue b/packages/grid-layouts/src/components/setting-drawer/components/InputNumberItem.vue new file mode 100644 index 00000000..8f638ed7 --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/InputNumberItem.vue @@ -0,0 +1,41 @@ + + + + {{ title }} + + + {{ suffix }} + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/InterfaceDisplay.vue b/packages/grid-layouts/src/components/setting-drawer/components/InterfaceDisplay.vue new file mode 100644 index 00000000..7ea8ebc9 --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/InterfaceDisplay.vue @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/InterfaceFunction.vue b/packages/grid-layouts/src/components/setting-drawer/components/InterfaceFunction.vue new file mode 100644 index 00000000..5f51dcb0 --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/InterfaceFunction.vue @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/NavigationBarPicker.vue b/packages/grid-layouts/src/components/setting-drawer/components/NavigationBarPicker.vue new file mode 100644 index 00000000..37fff21d --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/NavigationBarPicker.vue @@ -0,0 +1,108 @@ + + + + + + + + + + + + {{ t(item.title) }} + + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/SelectItem.vue b/packages/grid-layouts/src/components/setting-drawer/components/SelectItem.vue new file mode 100644 index 00000000..d1498c96 --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/SelectItem.vue @@ -0,0 +1,50 @@ + + + + + {{ title }} + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/SwitchItem.vue b/packages/grid-layouts/src/components/setting-drawer/components/SwitchItem.vue new file mode 100644 index 00000000..5661b0e6 --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/SwitchItem.vue @@ -0,0 +1,36 @@ + + + + {{ title }} + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/ThemeColorPicker.vue b/packages/grid-layouts/src/components/setting-drawer/components/ThemeColorPicker.vue new file mode 100644 index 00000000..715f8bca --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/ThemeColorPicker.vue @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/components/Transitions.vue b/packages/grid-layouts/src/components/setting-drawer/components/Transitions.vue new file mode 100644 index 00000000..6b6a259f --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/components/Transitions.vue @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/packages/grid-layouts/src/components/setting-drawer/index.vue b/packages/grid-layouts/src/components/setting-drawer/index.vue new file mode 100644 index 00000000..3acf86ce --- /dev/null +++ b/packages/grid-layouts/src/components/setting-drawer/index.vue @@ -0,0 +1,110 @@ + + + + + + {{ t('layout.setting.drawerTitle') }} + + + {{ t('layout.setting.darkMode') }} + + + + {{ t('layout.setting.navMode') }} + + { + baseHandler(HandlerSettingEnum.CHANGE_LAYOUT, { + mode: item.mode, + type: item.type, + split: isHorizontal ? false : undefined, + }) + } + " + /> + + + {{ t('layout.setting.sysTheme') }} + + + + + {{ t('layout.setting.headerTheme') }} + + + {{ + t('layout.setting.sidebarTheme') + }} + + + + + {{ t('layout.setting.interfaceFunction') }} + + + + + {{ t('layout.setting.interfaceDisplay') }} + + + + + {{ t('layout.setting.animation') }} + + + + + + + + + diff --git a/packages/grid-layouts/src/components/tab-bar/FoldButton.vue b/packages/grid-layouts/src/components/tab-bar/FoldButton.vue new file mode 100644 index 00000000..8af927ce --- /dev/null +++ b/packages/grid-layouts/src/components/tab-bar/FoldButton.vue @@ -0,0 +1,28 @@ + + + + + + + diff --git a/packages/grid-layouts/src/components/tab-bar/SuffixItemWrapper.vue b/packages/grid-layouts/src/components/tab-bar/SuffixItemWrapper.vue new file mode 100644 index 00000000..12671ca9 --- /dev/null +++ b/packages/grid-layouts/src/components/tab-bar/SuffixItemWrapper.vue @@ -0,0 +1,9 @@ + + + + + + + diff --git a/packages/grid-layouts/src/components/tab-bar/TabQuick.vue b/packages/grid-layouts/src/components/tab-bar/TabQuick.vue new file mode 100644 index 00000000..18872a37 --- /dev/null +++ b/packages/grid-layouts/src/components/tab-bar/TabQuick.vue @@ -0,0 +1,12 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/tab-bar/TabRedo.vue b/packages/grid-layouts/src/components/tab-bar/TabRedo.vue new file mode 100644 index 00000000..8c4c8b4f --- /dev/null +++ b/packages/grid-layouts/src/components/tab-bar/TabRedo.vue @@ -0,0 +1,23 @@ + + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/FixedSetting.vue b/packages/grid-layouts/src/components/widgets/FixedSetting.vue new file mode 100644 index 00000000..5229a269 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/FixedSetting.vue @@ -0,0 +1,30 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/FullScreen.vue b/packages/grid-layouts/src/components/widgets/FullScreen.vue new file mode 100644 index 00000000..c3837b40 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/FullScreen.vue @@ -0,0 +1,35 @@ + + + + + + + + + {{ getTitle }} + + diff --git a/packages/grid-layouts/src/components/widgets/HeaderTrigger.vue b/packages/grid-layouts/src/components/widgets/HeaderTrigger.vue new file mode 100644 index 00000000..0d3adef2 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/HeaderTrigger.vue @@ -0,0 +1,17 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/Locale.vue b/packages/grid-layouts/src/components/widgets/Locale.vue new file mode 100644 index 00000000..18aecf29 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/Locale.vue @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/Search.vue b/packages/grid-layouts/src/components/widgets/Search.vue new file mode 100644 index 00000000..31d4eb48 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/Search.vue @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/Setting.vue b/packages/grid-layouts/src/components/widgets/Setting.vue new file mode 100644 index 00000000..6cb4f996 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/Setting.vue @@ -0,0 +1,27 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/SiderCenterTrigger.vue b/packages/grid-layouts/src/components/widgets/SiderCenterTrigger.vue new file mode 100644 index 00000000..b75bb586 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/SiderCenterTrigger.vue @@ -0,0 +1,18 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/SiderFooterTrigger.vue b/packages/grid-layouts/src/components/widgets/SiderFooterTrigger.vue new file mode 100644 index 00000000..a13ddb97 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/SiderFooterTrigger.vue @@ -0,0 +1,18 @@ + + + + + + diff --git a/packages/grid-layouts/src/components/widgets/UserDropdown.vue b/packages/grid-layouts/src/components/widgets/UserDropdown.vue new file mode 100644 index 00000000..21d435b8 --- /dev/null +++ b/packages/grid-layouts/src/components/widgets/UserDropdown.vue @@ -0,0 +1,13 @@ + + + + + + Vben Admin + + + + diff --git a/packages/grid-layouts/src/index.vue b/packages/grid-layouts/src/index.vue new file mode 100644 index 00000000..2db24aa0 --- /dev/null +++ b/packages/grid-layouts/src/index.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/grid-layouts/tsconfig.json b/packages/grid-layouts/tsconfig.json new file mode 100644 index 00000000..53114773 --- /dev/null +++ b/packages/grid-layouts/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "baseUrl": ".", + "lib": ["dom", "esnext"] + }, + "include": [] +} diff --git a/packages/hooks/package.json b/packages/hooks/package.json index a554c255..07103d22 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -8,8 +8,9 @@ "clean": "pnpm rimraf node_modules && pnpm rimraf dist && pnpm rimraf .turbo" }, "dependencies": { - "@vben/locale": "workspace:*", "@vben/constants": "workspace:*", + "@vben/locale": "workspace:*", + "@vben/stores": "workspace:*", "@vben/utils": "workspace:*", "vue": "^3.2.47", "vue-router": "^4.1.6" @@ -17,6 +18,5 @@ "devDependencies": { "@types/lodash-es": "^4.17.6", "@vben/types": "workspace:*" - } } diff --git a/packages/hooks/src/config/index.ts b/packages/hooks/src/config/index.ts new file mode 100644 index 00000000..ddfcd2b2 --- /dev/null +++ b/packages/hooks/src/config/index.ts @@ -0,0 +1 @@ +export * from './useAppConfig' diff --git a/packages/hooks/src/config/useAppConfig.ts b/packages/hooks/src/config/useAppConfig.ts new file mode 100644 index 00000000..d787a0d0 --- /dev/null +++ b/packages/hooks/src/config/useAppConfig.ts @@ -0,0 +1,239 @@ +import { + StoreGeneric, + storeToRefs, + useAppConfig as appConfigStore, +} from '@vben/stores' +import { DefineAppConfigOptions } from '@vben/types' +import { HandlerSettingEnum, ThemeEnum } from '@vben/constants' +import { _merge, toggleClass } from '@vben/utils' +import { reactive, unref } from 'vue' +import { useClipboard, _omit } from '@vben/utils' + +type DefineAppConfigStoreGetters = { + isSidebar: boolean + isTopMenu: boolean + isMixSidebar: boolean + isMix: boolean + isMixMode: boolean + isHorizontal: boolean +} +export const useAppConfig = () => { + const useAppConfigStore = appConfigStore() + const appConfigOptions = storeToRefs( + useAppConfigStore as StoreGeneric, + ) as unknown as DefineAppConfigOptions & DefineAppConfigStoreGetters + const { openSettingDrawer, sidebar, grayMode, colorWeak } = appConfigOptions + const setAppConfig = (configs: DeepPartial) => { + useAppConfigStore.$patch((state) => { + _merge(state, configs) + }) + } + + function toggleOpenSettingDrawer() { + useAppConfigStore.setOpenSettingDrawer(!unref(openSettingDrawer)) + } + + function toggleCollapse() { + useAppConfigStore.setSidebar({ collapsed: !unref(sidebar).collapsed }) + } + + function baseHandler(event: HandlerSettingEnum, value: any) { + setAppConfig(handlerResults(event, value, appConfigOptions)) + } + + function toggleGrayMode(isGrayMode = unref(grayMode)) { + toggleClass(isGrayMode, 'gray-mode', document.body) + } + + function toggleColorWeak(isColorWeak = colorWeak) { + toggleClass(isColorWeak, 'color-weak', document.body) + } + + async function copyConfigs() { + try { + const { copy, isSupported } = useClipboard() + if (!isSupported) + return console.error('Your browser does not support Clipboard API') + const source = reactive(_omit(appConfigOptions, ['openSettingDrawer'])) + await copy(JSON.stringify(source)) + } catch (e) { + console.error(e) + } + } + + function clearAndRedo() { + localStorage.clear() + sessionStorage.clear() + location.reload() + } + + function resetAllConfig() { + useAppConfigStore.$reset() + } + return { + ...appConfigOptions, + setAppConfig, + toggleOpenSettingDrawer, + baseHandler, + copyConfigs, + clearAndRedo, + resetAllConfig, + toggleCollapse, + toggleGrayMode, + toggleColorWeak, + } +} + +function handlerResults( + event: HandlerSettingEnum, + value: any, + configOptions: DefineAppConfigOptions, +): DeepPartial { + const { themeColor, theme, sidebar, header } = configOptions + switch (event) { + case HandlerSettingEnum.CHANGE_LAYOUT: + const { mode, type, split } = value + const splitOpt = split === undefined ? { split } : {} + return { + navBarMode: type, + menu: { + ...splitOpt, + mode, + }, + } + + case HandlerSettingEnum.CHANGE_THEME_COLOR: + if (unref(themeColor) === value) { + return {} + } + // changeTheme(value); + return { themeColor: value } + + case HandlerSettingEnum.CHANGE_THEME: + if (unref(theme) === value) { + return {} + } + // updateDarkTheme(value); + return { theme: value ? ThemeEnum.DARK : ThemeEnum.LIGHT } + + case HandlerSettingEnum.MENU_HAS_DRAG: + return { menu: { canDrag: value } } + + case HandlerSettingEnum.MENU_ACCORDION: + return { menu: { accordion: value } } + + case HandlerSettingEnum.MENU_TRIGGER: + return { sidebar: { trigger: value } } + + case HandlerSettingEnum.MENU_TOP_ALIGN: + return { menu: { topMenuAlign: value } } + + case HandlerSettingEnum.MENU_COLLAPSED: + return { sidebar: { collapsed: value } } + + case HandlerSettingEnum.MENU_WIDTH: + return { sidebar: { width: value } } + + case HandlerSettingEnum.MENU_SHOW_SIDEBAR: + return { sidebar: { show: value, visible: value } } + + case HandlerSettingEnum.MENU_COLLAPSED_SHOW_TITLE: + return { menu: { collapsedShowLabel: value } } + + case HandlerSettingEnum.MENU_THEME: + // updateSidebarBgColor(value); + if (unref(sidebar).bgColor === value) return {} + return { sidebar: { bgColor: value } } + + case HandlerSettingEnum.MENU_SPLIT: + return { menu: { split: value } } + + case HandlerSettingEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE: + return { closeMixSidebarOnChange: value } + + case HandlerSettingEnum.MENU_FIXED: + return { sidebar: { fixed: value } } + + case HandlerSettingEnum.MENU_TRIGGER_MIX_SIDEBAR: + return { menu: { mixSideTrigger: value } } + + case HandlerSettingEnum.MENU_FIXED_MIX_SIDEBAR: + return { menu: { mixSideFixed: value } } + + // ============transition================== + case HandlerSettingEnum.OPEN_PAGE_LOADING: + return { transition: { openPageLoading: value } } + + case HandlerSettingEnum.ROUTER_TRANSITION: + return { transition: { basicTransition: value } } + + case HandlerSettingEnum.OPEN_ROUTE_TRANSITION: + return { transition: { enable: value } } + + case HandlerSettingEnum.OPEN_PROGRESS: + return { transition: { openNProgress: value } } + // ============root================== + + case HandlerSettingEnum.LOCK_TIME: + return { lockTime: value } + + case HandlerSettingEnum.FULL_CONTENT: + return { + content: { fullScreen: value }, + sidebar: { visible: !value, show: !value }, + header: { visible: !value, show: !value }, + tabTar: { visible: !value, show: !value }, + } + + case HandlerSettingEnum.CONTENT_MODE: + return { content: { mode: value } } + + case HandlerSettingEnum.SHOW_BREADCRUMB: + return { header: { showBreadCrumb: value } } + + case HandlerSettingEnum.SHOW_BREADCRUMB_ICON: + return { header: { showBreadCrumbIcon: value } } + + case HandlerSettingEnum.GRAY_MODE: + return { grayMode: value } + + case HandlerSettingEnum.SHOW_FOOTER: + return { footer: { show: value, visible: value } } + + case HandlerSettingEnum.COLOR_WEAK: + return { colorWeak: value } + + case HandlerSettingEnum.SHOW_LOGO: + return { logo: { show: value, visible: value } } + + // ============tabs================== + case HandlerSettingEnum.TABS_SHOW_QUICK: + return { tabTar: { showQuick: value } } + + case HandlerSettingEnum.TABS_SHOW: + return { tabTar: { show: value, visible: value } } + + case HandlerSettingEnum.TABS_SHOW_REDO: + return { tabTar: { showRedo: value } } + + case HandlerSettingEnum.TABS_SHOW_FOLD: + return { tabTar: { showFold: value } } + + // ============header================== + case HandlerSettingEnum.HEADER_THEME: + // updateHeaderBgColor(value); + if (unref(header).bgColor === value) return {} + return { header: { bgColor: value } } + + case HandlerSettingEnum.HEADER_SEARCH: + return { header: { showSearch: value } } + + case HandlerSettingEnum.HEADER_FIXED: + return { header: { fixed: value } } + + case HandlerSettingEnum.HEADER_SHOW: + return { header: { show: value, visible: value } } + default: + return {} + } +} diff --git a/packages/hooks/src/event/index.ts b/packages/hooks/src/event/index.ts new file mode 100644 index 00000000..9db1c087 --- /dev/null +++ b/packages/hooks/src/event/index.ts @@ -0,0 +1,2 @@ +export * from './use-breakpoint' +export * from './use-scroll-to' diff --git a/packages/hooks/src/event/use-breakpoint.ts b/packages/hooks/src/event/use-breakpoint.ts new file mode 100644 index 00000000..079dc353 --- /dev/null +++ b/packages/hooks/src/event/use-breakpoint.ts @@ -0,0 +1,91 @@ +import { ref, computed, ComputedRef, unref } from 'vue' +import { screenMap, ScreenSizeEnum, ScreenValueEnum } from '@vben/constants' +import { useEventListener } from './use-event-listener' + +let globalScreenRef: ComputedRef +let globalWidthRef: ComputedRef +let globalRealWidthRef: ComputedRef + +export interface CreateCallbackParams { + screen: ComputedRef + width: ComputedRef + realWidth: ComputedRef + screenEnum: typeof ScreenValueEnum + screenMap: Map + sizeEnum: typeof ScreenSizeEnum +} + +export function useBreakpoint() { + return { + screenRef: computed(() => unref(globalScreenRef)), + widthRef: globalWidthRef, + screenEnum: ScreenValueEnum, + realWidthRef: globalRealWidthRef, + } +} + +// Just call it once +export function createBreakpointListen( + fn?: (opt: CreateCallbackParams) => void, +) { + const screenRef = ref(ScreenSizeEnum.XL) + const realWidthRef = ref(window.innerWidth) + + function getWindowWidth() { + const width = document.body.clientWidth + const xs = screenMap.get(ScreenSizeEnum.XS)! + const sm = screenMap.get(ScreenSizeEnum.SM)! + const md = screenMap.get(ScreenSizeEnum.MD)! + const lg = screenMap.get(ScreenSizeEnum.LG)! + const xl = screenMap.get(ScreenSizeEnum.XL)! + if (width < xs) { + screenRef.value = ScreenSizeEnum.XS + } else if (width < sm) { + screenRef.value = ScreenSizeEnum.SM + } else if (width < md) { + screenRef.value = ScreenSizeEnum.MD + } else if (width < lg) { + screenRef.value = ScreenSizeEnum.LG + } else if (width < xl) { + screenRef.value = ScreenSizeEnum.XL + } else { + screenRef.value = ScreenSizeEnum.XXL + } + realWidthRef.value = width + } + + useEventListener({ + el: window, + name: 'resize', + + listener: () => { + getWindowWidth() + resizeFn() + }, + // wait: 100, + }) + + getWindowWidth() + globalScreenRef = computed(() => unref(screenRef)) + globalWidthRef = computed((): number => screenMap.get(unref(screenRef)!)!) + globalRealWidthRef = computed((): number => unref(realWidthRef)) + + function resizeFn() { + fn?.({ + screen: globalScreenRef, + width: globalWidthRef, + realWidth: globalRealWidthRef, + screenEnum: ScreenValueEnum, + screenMap, + sizeEnum: ScreenSizeEnum, + }) + } + + resizeFn() + return { + screenRef: globalScreenRef, + screenEnum: ScreenValueEnum, + widthRef: globalWidthRef, + realWidthRef: globalRealWidthRef, + } +} diff --git a/packages/hooks/src/event/use-event-listener.ts b/packages/hooks/src/event/use-event-listener.ts new file mode 100644 index 00000000..f1bb9f99 --- /dev/null +++ b/packages/hooks/src/event/use-event-listener.ts @@ -0,0 +1,61 @@ +import type { Ref } from 'vue' +import { ref, watch, unref } from 'vue' +import { useThrottleFn, useDebounceFn } from '@vben/utils' + +export type RemoveEventFn = () => void +export interface UseEventParams { + el?: Element | Ref | Window | any + name: string + listener: EventListener + options?: boolean | AddEventListenerOptions + autoRemove?: boolean + isDebounce?: boolean + wait?: number +} +export function useEventListener({ + el = window, + name, + listener, + options, + autoRemove = true, + isDebounce = true, + wait = 80, +}: UseEventParams): { removeEvent: RemoveEventFn } { + /* eslint-disable-next-line */ + let remove: RemoveEventFn = () => {} + const isAddRef = ref(false) + + if (el) { + const element = ref(el as Element) as Ref + + const handler = isDebounce + ? useDebounceFn(listener, wait) + : useThrottleFn(listener, wait) + const realHandler = wait ? handler : listener + const removeEventListener = (e: Element) => { + isAddRef.value = true + e.removeEventListener(name, realHandler, options) + } + const addEventListener = (e: Element) => + e.addEventListener(name, realHandler, options) + + const removeWatch = watch( + element, + (v, _ov, cleanUp) => { + if (v) { + !unref(isAddRef) && addEventListener(v) + cleanUp(() => { + autoRemove && removeEventListener(v) + }) + } + }, + { immediate: true }, + ) + + remove = () => { + removeEventListener(element.value) + removeWatch() + } + } + return { removeEvent: remove } +} diff --git a/packages/hooks/src/event/use-scroll-to.ts b/packages/hooks/src/event/use-scroll-to.ts new file mode 100644 index 00000000..9ce3e257 --- /dev/null +++ b/packages/hooks/src/event/use-scroll-to.ts @@ -0,0 +1,65 @@ +import { isFunction, isUndefined } from '@vben/utils' +import { ref, unref } from 'vue' + +export interface ScrollToParams { + el: any + to: number + duration?: number + callback?: () => any +} + +const easeInOutQuad = (t: number, b: number, c: number, d: number) => { + t /= d / 2 + if (t < 1) { + return (c / 2) * t * t + b + } + t-- + return (-c / 2) * (t * (t - 2) - 1) + b +} +const move = (el: HTMLElement, amount: number) => { + el.scrollTop = amount +} + +const position = (el: HTMLElement) => { + return el.scrollTop +} + +export function useScrollTo({ + el, + to, + duration = 500, + callback, +}: ScrollToParams) { + const isActiveRef = ref(false) + const start = position(el) + const change = to - start + const increment = 20 + let currentTime = 0 + duration = isUndefined(duration) ? 500 : duration + + const animateScroll = function () { + if (!unref(isActiveRef)) { + return + } + currentTime += increment + const val = easeInOutQuad(currentTime, start, change, duration) + move(el, val) + if (currentTime < duration && unref(isActiveRef)) { + requestAnimationFrame(animateScroll) + } else { + if (callback && isFunction(callback)) { + callback() + } + } + } + const run = () => { + isActiveRef.value = true + animateScroll() + } + + const stop = () => { + isActiveRef.value = false + } + + return { start: run, stop } +} diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 1b3a62b3..3f367a53 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -4,3 +4,5 @@ export * from './useTitle' export * from './usePage' export * from './useContext' export * from './useRefs' +export * from './config' +export * from './event' diff --git a/packages/hooks/src/web/index.ts b/packages/hooks/src/web/index.ts index 8b82d36f..f47471e8 100644 --- a/packages/hooks/src/web/index.ts +++ b/packages/hooks/src/web/index.ts @@ -1 +1,5 @@ export * from './useCopyToClipboard' +export * from './useLayout' +export * from './useTheme' +export * from './useAppInject' +export * from './useAppContext' diff --git a/packages/hooks/src/web/useAppContext.ts b/packages/hooks/src/web/useAppContext.ts new file mode 100644 index 00000000..0ccd38f3 --- /dev/null +++ b/packages/hooks/src/web/useAppContext.ts @@ -0,0 +1,17 @@ +import { createContext, useContext } from '../useContext' +import { InjectionKey, Ref } from 'vue' + +export interface AppProviderContextProps { + prefixCls: Ref + isMobile: Ref +} + +const key: InjectionKey = Symbol() + +export function createAppProviderContext(context: AppProviderContextProps) { + return createContext(context, key) +} + +export function useAppProviderContext() { + return useContext(key) +} diff --git a/packages/hooks/src/web/useAppInject.ts b/packages/hooks/src/web/useAppInject.ts new file mode 100644 index 00000000..77d7e2e9 --- /dev/null +++ b/packages/hooks/src/web/useAppInject.ts @@ -0,0 +1,10 @@ +import { computed, unref } from 'vue' +import { useAppProviderContext } from './useAppContext' + +export function useAppInject() { + const values = useAppProviderContext() + + return { + isMobile: computed(() => unref(values.isMobile)), + } +} diff --git a/packages/hooks/src/web/useCopyToClipboard.ts b/packages/hooks/src/web/useCopyToClipboard.ts index dc0a7c61..88ccb7a1 100644 --- a/packages/hooks/src/web/useCopyToClipboard.ts +++ b/packages/hooks/src/web/useCopyToClipboard.ts @@ -1,78 +1,79 @@ -import { ref, watch } from 'vue'; +import { ref, watch } from 'vue' -import { isUndefined } from '@vben/utils'; +import { isUndefined } from '@vben/utils' interface Options { - target?: HTMLElement; + target?: HTMLElement } export function useCopyToClipboard(initial?: string) { - const clipboardRef = ref(initial || ''); - const isSuccessRef = ref(false); - const copiedRef = ref(false); + const clipboardRef = ref(initial || '') + const isSuccessRef = ref(false) + const copiedRef = ref(false) watch( clipboardRef, (str?: string) => { if (!isUndefined(str)) { - copiedRef.value = true; - isSuccessRef.value = copyTextToClipboard(str); + copiedRef.value = true + isSuccessRef.value = copyTextToClipboard(str) } }, { immediate: !!initial, flush: 'sync' }, - ); + ) - return { clipboardRef, isSuccessRef, copiedRef }; + return { clipboardRef, isSuccessRef, copiedRef } } - -export async function writeTextToClipboard(initial: string){ +export async function writeTextToClipboard(initial: string) { try { - await navigator.clipboard.writeText(initial); - }catch (err: any){ - throw new Error(err); + await navigator.clipboard.writeText(initial) + } catch (err: any) { + throw new Error(err) } } -export function copyTextToClipboard(input: string, { target = document.body }: Options = {}) { - console.log('dsfasdf',input) - const element = document.createElement('textarea'); - const previouslyFocusedElement = document.activeElement; - - element.value = input; +export function copyTextToClipboard( + input: string, + { target = document.body }: Options = {}, +) { + console.log('dsfasdf', input) + const element = document.createElement('textarea') + const previouslyFocusedElement = document.activeElement - element.setAttribute('readonly', ''); + element.value = input - (element.style as any).contain = 'strict'; - element.style.position = 'absolute'; - element.style.left = '-9999px'; - element.style.fontSize = '12pt'; + element.setAttribute('readonly', '') + ;(element.style as any).contain = 'strict' + element.style.position = 'absolute' + element.style.left = '-9999px' + element.style.fontSize = '12pt' - const selection = document.getSelection(); - let originalRange; + const selection = document.getSelection() + let originalRange if (selection && selection.rangeCount > 0) { - originalRange = selection.getRangeAt(0); + originalRange = selection.getRangeAt(0) } - target.append(element); - element.select(); + target.append(element) + element.select() - element.selectionStart = 0; - element.selectionEnd = input.length; + element.selectionStart = 0 + element.selectionEnd = input.length - let isSuccess = false; + let isSuccess = false try { - isSuccess = document.execCommand('copy'); + isSuccess = document.execCommand('copy') } catch (e: any) { - throw new Error(e); + throw new Error(e) } - element.remove(); + element.remove() if (originalRange && selection) { - selection.removeAllRanges(); - selection.addRange(originalRange); + selection.removeAllRanges() + selection.addRange(originalRange) } if (previouslyFocusedElement) { - (previouslyFocusedElement as HTMLElement).focus(); + ;(previouslyFocusedElement as HTMLElement).focus() } - return isSuccess; + return isSuccess } diff --git a/packages/hooks/src/web/useLayout.ts b/packages/hooks/src/web/useLayout.ts new file mode 100644 index 00000000..ee83e92e --- /dev/null +++ b/packages/hooks/src/web/useLayout.ts @@ -0,0 +1,147 @@ +import { useAppConfig } from '../config' +import { unref, watchEffect } from 'vue' +import { useLayoutHeader, useLayoutSidebar } from '@vben/stores' +import { + useCssVar, + MaybeElementRef, + lighten, + pickTextColorBasedOnBgColor, + darken, + addClass, +} from '@vben/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 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 createThemeColorListen(el: MaybeElementRef | null) { + const { + sidebar, + header, + grayMode, + colorWeak, + theme, + toggleGrayMode, + toggleColorWeak, + } = useAppConfig() + + const { sidebarRef } = useLayoutSidebar() + const { headerRef } = useLayoutHeader() + + const headerBgColor = useCssVar( + HEADER_BG_COLOR_VAR, + headerRef as MaybeElementRef, + { + initialValue: `${unref(header).bgColor}px`, + }, + ) + + const headerTextColor = useCssVar( + HEADER_TEXT_COLOR_VAR, + headerRef as MaybeElementRef, + { + initialValue: LIGHT_TEXT_COLOR, + }, + ) + const headerActionHoverBgColor = useCssVar( + HEADER_ACTION_HOVER_BG_COLOR_VAR, + headerRef as MaybeElementRef, + ) + + const sidebarBgColor = useCssVar( + ASIDE_DARK_BG_COLOR, + sidebarRef as MaybeElementRef, + { + initialValue: `${unref(sidebar).bgColor}px`, + }, + ) + + const asideTextColor = useCssVar( + ASIDE_TEXT_COLOR_VAR, + sidebarRef as MaybeElementRef, + { + initialValue: LIGHT_TEXT_COLOR, + }, + ) + + watchEffect(() => { + headerBgColor.value = unref(header).bgColor + headerTextColor.value = pickTextColorBasedOnBgColor( + unref(header).bgColor, + LIGHT_TEXT_COLOR, + DARK_TEXT_COLOR, + ) + headerActionHoverBgColor.value = ['#fff', '#ffffff'].includes( + unref(header).bgColor.toLowerCase(), + ) + ? darken(unref(header).bgColor, 6) + : lighten(unref(header).bgColor, 6) + + sidebarBgColor.value = unref(sidebar).bgColor + asideTextColor.value = pickTextColorBasedOnBgColor( + unref(sidebar).bgColor, + LIGHT_TEXT_COLOR, + DARK_TEXT_COLOR, + ) + toggleGrayMode(unref(grayMode)) + toggleColorWeak(unref(colorWeak)) + addClass(document.documentElement, unref(theme)) + }) +} + +export function createGridLayoutListen(el: MaybeElementRef | null) { + const { isTopMenu, isMixSidebar, sidebar, header, footer, tabTar } = + 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(sidebar).collapsed) return unref(sidebar).collapsedWidth + if (unref(isMixSidebar)) return unref(sidebar).mixSidebarWidth + 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` + }) +} diff --git a/packages/hooks/src/web/useTheme.ts b/packages/hooks/src/web/useTheme.ts new file mode 100644 index 00000000..e245daaa --- /dev/null +++ b/packages/hooks/src/web/useTheme.ts @@ -0,0 +1,27 @@ +import { useAppConfig } from '../config' +import { ThemeEnum } from '@vben/constants' +import { useEventListener } from '@vben/utils' +import { computed, unref } from 'vue' + +export function createMediaPrefersColorSchemeListen() { + const { setAppConfig } = useAppConfig() + // 监听系统主题 + useEventListener( + window.matchMedia('(prefers-color-scheme: dark)'), + 'change', + (e: Event) => { + // @ts-ignore + setAppConfig({ theme: e.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT }) + }, + ) +} + +export const useAppTheme = () => { + const { theme } = useAppConfig() + + const isDark = computed(() => { + return unref(theme) === ThemeEnum.DARK + }) + + return { isDark, theme } +} diff --git a/packages/layouts/package.json b/packages/layouts/package.json index 5f6124f4..248f1c4d 100644 --- a/packages/layouts/package.json +++ b/packages/layouts/package.json @@ -12,7 +12,6 @@ "@vben/directives": "workspace:*", "@vben/hooks": "workspace:*", "@vben/locale": "workspace:*", - "@vben/pinia": "workspace:*", "@vben/types": "workspace:*", "@vben/utils": "workspace:*", "@vben/vbencomponents": "workspace:*", diff --git a/packages/layouts/src/components/lock/LockModal.vue b/packages/layouts/src/components/lock/LockModal.vue index 8c9b66e8..19219198 100644 --- a/packages/layouts/src/components/lock/LockModal.vue +++ b/packages/layouts/src/components/lock/LockModal.vue @@ -1,25 +1,25 @@ - + - + {{ getUserInfo.realName }} - + - {{ t('layout.header.lockScreenBtn')}} + {{ t('layout.header.lockScreenBtn') }} - + diff --git a/packages/layouts/src/components/setting/components/SettingDrawer.vue b/packages/layouts/src/components/setting/components/SettingDrawer.vue index 7ab1992f..6b0c2b20 100644 --- a/packages/layouts/src/components/setting/components/SettingDrawer.vue +++ b/packages/layouts/src/components/setting/components/SettingDrawer.vue @@ -1,5 +1,4 @@ @@ -48,32 +39,65 @@ const onVisible = (show: boolean)=>{ {{ t('layout.setting.drawerTitle') }} - {{ t('layout.setting.darkMode') }} - + {{ + t('layout.setting.darkMode') + }} + - {{ t('layout.setting.navMode') }} - { - baseHandler(HandlerSettingEnum.CHANGE_LAYOUT,{ - mode: item.mode, - type: item.type, - split: getIsHorizontal ? false : undefined, - }) - }"/> - {{ t('layout.setting.sysTheme') }} - - {{ t('layout.setting.headerTheme') }} - - {{ t('layout.setting.sidebarTheme') }} - - {{ t('layout.setting.interfaceFunction') }} + {{ + t('layout.setting.navMode') + }} + { + baseHandler(HandlerSettingEnum.CHANGE_LAYOUT, { + mode: item.mode, + type: item.type, + split: getIsHorizontal ? false : undefined, + }) + } + " + /> + {{ + t('layout.setting.sysTheme') + }} + + {{ + t('layout.setting.headerTheme') + }} + + {{ + t('layout.setting.sidebarTheme') + }} + + {{ t('layout.setting.interfaceFunction') }} - - {{ t('layout.setting.interfaceDisplay') }} - - {{ t('layout.setting.animation') }} - - - + + {{ + t('layout.setting.interfaceDisplay') + }} + + {{ + t('layout.setting.animation') + }} + + + diff --git a/packages/layouts/src/store/modules/test.ts b/packages/layouts/src/store/modules/test.ts deleted file mode 100644 index cec98ad0..00000000 --- a/packages/layouts/src/store/modules/test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineStore } from '@vben/pinia' - -export const useLayoutTest = defineStore({ - id: 'use--layout--test', - state: () => ({ - test: 'layout 测试', - }), - actions: { - setTest(value: string) { - this.test = value - }, - }, - persist: true, -}) diff --git a/packages/locale/src/lang/en/layout.ts b/packages/locale/src/lang/en/layout.ts index d3c0390c..01824e06 100644 --- a/packages/locale/src/lang/en/layout.ts +++ b/packages/locale/src/lang/en/layout.ts @@ -39,6 +39,7 @@ export default { menuTriggerNone: 'Not Show', menuTriggerBottom: 'Bottom', menuTriggerTop: 'Top', + menuTriggerCenter: 'Sideways', // menu type menuTypeSidebar: 'Left menu mode', menuTypeMixSidebar: 'Left menu mixed mode', diff --git a/packages/locale/src/lang/zh-CN/layout.ts b/packages/locale/src/lang/zh-CN/layout.ts index 3e4e50c0..40f091c4 100644 --- a/packages/locale/src/lang/zh-CN/layout.ts +++ b/packages/locale/src/lang/zh-CN/layout.ts @@ -40,6 +40,7 @@ export default { menuTriggerNone: '不显示', menuTriggerBottom: '底部', menuTriggerTop: '顶部', + menuTriggerCenter: '侧边', // menu type menuTypeSidebar: '左侧菜单模式', menuTypeMixSidebar: '左侧菜单混合模式', diff --git a/packages/pinia/src/index.ts b/packages/pinia/src/index.ts deleted file mode 100644 index 231b2ab8..00000000 --- a/packages/pinia/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './pinia' diff --git a/packages/pinia/index.ts b/packages/stores/index.ts similarity index 100% rename from packages/pinia/index.ts rename to packages/stores/index.ts diff --git a/packages/pinia/package.json b/packages/stores/package.json similarity index 73% rename from packages/pinia/package.json rename to packages/stores/package.json index 140c16a1..6098203d 100644 --- a/packages/pinia/package.json +++ b/packages/stores/package.json @@ -1,5 +1,5 @@ { - "name": "@vben/pinia", + "name": "@vben/stores", "version": "3.0.0-bata.1", "main": "index.ts", "module": "index.ts", @@ -8,9 +8,13 @@ "clean": "pnpm rimraf node_modules && pnpm rimraf dist && pnpm rimraf .turbo" }, "dependencies": { + "@vben/constants": "workspace:*", "@vben/utils": "workspace:*", "pinia": "^2.0.30", "pinia-plugin-persistedstate": "^3.0.2", "vue": "^3.2.47" + }, + "devDependencies": { + "@vben/types": "workspace:*" } } diff --git a/packages/stores/src/index.ts b/packages/stores/src/index.ts new file mode 100644 index 00000000..354a2cf4 --- /dev/null +++ b/packages/stores/src/index.ts @@ -0,0 +1,2 @@ +export * from './pinia' +export * from './modules/index' diff --git a/packages/stores/src/modules/appConfig.ts b/packages/stores/src/modules/appConfig.ts new file mode 100644 index 00000000..878b6621 --- /dev/null +++ b/packages/stores/src/modules/appConfig.ts @@ -0,0 +1,244 @@ +import { defineStore } from 'pinia' +import { + ContentConfigOptions, + DefineAppConfigOptions, + FooterConfigOptions, + HeaderConfigOptions, + LogoConfigOptions, + MenuConfigOptions, + SidebarConfigOptions, + TabTbrConfigOptions, + TransitionConfigOptions, +} from '@vben/types' +import { _assign } from '@vben/utils' +import { + CacheTypeEnum, + ContentLayoutEnum, + HEADER_PRESET_BG_COLOR_LIST, + MenuModeEnum, + MixSidebarTriggerEnum, + NavBarModeEnum, + PermissionModeEnum, + RouterTransitionEnum, + SessionTimeoutProcessingEnum, + SettingButtonPositionEnum, + SIDE_BAR_BG_COLOR_LIST, + ThemeEnum, + TriggerEnum, +} from '@vben/constants' + +export const useAppConfig = defineStore({ + id: 'APP_CONFIG', + state: (): DefineAppConfigOptions => ({ + theme: ThemeEnum.LIGHT, + navBarMode: NavBarModeEnum.SIDEBAR, + themeColor: '', + showThemeModeToggle: true, + openKeepAlive: true, + useOpenBackTop: true, + closeMessageOnSwitch: false, + removeAllHttpPending: true, + permissionCacheType: CacheTypeEnum.LOCAL, + settingButtonPosition: SettingButtonPositionEnum.AUTO, + openSettingDrawer: false, + permissionMode: PermissionModeEnum.BACK, + sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP, + grayMode: false, + colorWeak: false, + lockTime: 0, + useLockPage: false, + canEmbedIFramePage: true, + closeMixSidebarOnChange: false, + sidebar: { + show: true, + visible: true, + bgColor: SIDE_BAR_BG_COLOR_LIST[0], + fixed: false, + width: 210, + mixSidebarWidth: 80, + collapsedWidth: 48, + collapsed: false, + trigger: TriggerEnum.CENTER, + }, + menu: { + canDrag: false, + split: false, + mode: MenuModeEnum.INLINE, + accordion: false, + collapsedShowLabel: false, + mixSideTrigger: MixSidebarTriggerEnum.CLICK, + mixSideFixed: false, + topMenuAlign: 'start', + dropdownPlacement: 'top-start', + }, + header: { + show: true, + visible: true, + bgColor: HEADER_PRESET_BG_COLOR_LIST[0], + fixed: false, + height: 48, + showDoc: true, + showBreadCrumb: true, + showBreadCrumbIcon: true, + showFullScreen: true, + showNotice: true, + showSearch: true, + showLocalePicker: true, + showSetting: true, + }, + logo: { + show: true, + visible: true, + showTitle: true, + }, + tabTar: { + show: true, + visible: true, + height: 36, + cache: true, + canDrag: false, + showFold: true, + showQuick: true, + showRedo: true, + }, + content: { + fullScreen: false, + mode: ContentLayoutEnum.FULL, + }, + footer: { + height: 60, + show: false, + visible: false, + }, + transition: { + enable: true, + basicTransition: RouterTransitionEnum.FADE_SIDE, + openPageLoading: true, + openNProgress: false, + }, + }), + getters: { + isSidebar: (state) => state.navBarMode === NavBarModeEnum.SIDEBAR, + isTopMenu: (state) => state.navBarMode === NavBarModeEnum.TOP_MENU, + isMixSidebar: (state) => state.navBarMode === NavBarModeEnum.MIX_SIDEBAR, + isMix: (state) => state.navBarMode === NavBarModeEnum.MIX, + isMixMode: (state) => + state.menu.mode === MenuModeEnum.INLINE && + state.navBarMode === NavBarModeEnum.MIX, + isHorizontal: (state) => state.menu.mode === MenuModeEnum.HORIZONTAL, + }, + actions: { + setTheme(value: ThemeEnum) { + this.theme = value + }, + setNavBarMode(value: NavBarModeEnum) { + this.navBarMode = value + }, + setThemeColor(value: string) { + this.themeColor = value + }, + setShowThemeModeToggle(value: boolean) { + this.showThemeModeToggle = value + }, + setOpenKeepAlive(value: boolean) { + this.openKeepAlive = value + }, + setUseOpenBackTop(value: boolean) { + this.useOpenBackTop = value + }, + setCloseMessageOnSwitch(value: boolean) { + this.closeMessageOnSwitch = value + }, + setRemoveAllHttpPending(value: boolean) { + this.removeAllHttpPending = value + }, + setPermissionCacheType(value: CacheTypeEnum) { + this.permissionCacheType = value + }, + setSettingButtonPosition(value: SettingButtonPositionEnum) { + this.settingButtonPosition = value + }, + setOpenSettingDrawer(value: boolean) { + this.openSettingDrawer = value + }, + setPermissionMode(value: PermissionModeEnum) { + this.permissionMode = value + }, + setSessionTimeoutProcessing(value: SessionTimeoutProcessingEnum) { + this.sessionTimeoutProcessing = value + }, + setGrayMode(value: boolean) { + this.grayMode = value + }, + setColorWeak(value: boolean) { + this.colorWeak = value + }, + setLockTime(value: number) { + this.lockTime = value + }, + setUseLockPage(value: boolean) { + this.useLockPage = value + }, + setCanEmbedIFramePage(value: boolean) { + this.canEmbedIFramePage = value + }, + setSidebar( + value: Partial< + Omit + >, + ) { + _assign(this.sidebar, value) + }, + setMenu(value: Partial) { + _assign(this.menu, value) + }, + setHeader(value: Partial>) { + _assign(this.header, value) + }, + setLogo(value: Partial) { + _assign(this.logo, value) + }, + setTabTar(value: Partial) { + _assign(this.tabTar, value) + }, + setContent(value: Partial) { + _assign(this.content, value) + }, + setFooter(value: Partial>) { + _assign(this.footer, value) + }, + setTransition(value: Partial) { + _assign(this.transition, value) + }, + }, + persist: { + // excludedPaths: ['openSettingDrawer'], // Temporarily invalid + paths: [ + 'theme', + 'navBarMode', + 'themeColor', + 'showThemeModeToggle', + 'openKeepAlive', + 'useOpenBackTop', + 'closeMessageOnSwitch', + 'removeAllHttpPending', + 'permissionCacheType', + 'settingButtonPosition', + 'permissionMode', + 'sessionTimeoutProcessing', + 'grayMode', + 'colorWeak', + 'lockTime', + 'useLockPage', + 'canEmbedIFramePage', + 'sidebar', + 'menu', + 'header', + 'logo', + 'tabTar', + 'content', + 'footer', + 'transition', + ], + }, +}) diff --git a/packages/stores/src/modules/index.ts b/packages/stores/src/modules/index.ts new file mode 100644 index 00000000..fc809df8 --- /dev/null +++ b/packages/stores/src/modules/index.ts @@ -0,0 +1,2 @@ +export * from './layoutComposables' +export * from './appConfig' diff --git a/packages/stores/src/modules/layoutComposables.ts b/packages/stores/src/modules/layoutComposables.ts new file mode 100644 index 00000000..b9dfb3f3 --- /dev/null +++ b/packages/stores/src/modules/layoutComposables.ts @@ -0,0 +1,65 @@ +import { defineStore } from 'pinia' +import { useElementSize } from '@vben/utils' +import { ref } from 'vue' + +export const useLayoutHeader = defineStore('LAYOUT_HEADER', () => { + const headerRef = ref() + + const { height: headerHeight, width: headerWidth } = useElementSize(headerRef) + + return { + headerRef, + headerHeight, + headerWidth, + } +}) + +export const useLayoutSidebar = defineStore('LAYOUT_SIDEBAR', () => { + const sidebarRef = ref() + + const { height: sidebarHeight, width: sidebarWidth } = + useElementSize(sidebarRef) + + return { + sidebarRef, + sidebarHeight, + sidebarWidth, + } +}) + +export const useLayoutTab = defineStore('LAYOUT_TAB', () => { + const tabRef = ref() + + const { height: tabHeight, width: tabWidth } = useElementSize(tabRef) + + return { + tabRef, + tabHeight, + tabWidth, + } +}) + +export const useLayoutContent = defineStore('LAYOUT_FOOTER', () => { + const contentRef = ref() + + const { height: contentHeight, width: contentWidth } = + useElementSize(contentRef) + + return { + contentRef, + contentHeight, + contentWidth, + } +}) + +export const useLayoutFooter = defineStore('LAYOUT_FOOTER', () => { + const footerRef = ref() + + const { height: footerHeight, width: footerWidth } = useElementSize(footerRef) + + return { + footerRef, + footerHeight, + footerWidth, + } +}) diff --git a/packages/pinia/src/persist.ts b/packages/stores/src/persist.ts similarity index 100% rename from packages/pinia/src/persist.ts rename to packages/stores/src/persist.ts diff --git a/packages/pinia/src/pinia.ts b/packages/stores/src/pinia.ts similarity index 96% rename from packages/pinia/src/pinia.ts rename to packages/stores/src/pinia.ts index 4355df16..37c34961 100644 --- a/packages/pinia/src/pinia.ts +++ b/packages/stores/src/pinia.ts @@ -7,6 +7,7 @@ import { createStorageName } from '@vben/utils' const pinia = createPinia() pinia.use( + // @ts-ignore createPersistedState(persistGlobalConfig(createStorageName(import.meta.env))), ) diff --git a/packages/styles/src/common.css b/packages/styles/src/common.css index fca86706..62ab6c4f 100644 --- a/packages/styles/src/common.css +++ b/packages/styles/src/common.css @@ -10,6 +10,9 @@ body { overflow: visible !important; overflow-x: hidden !important; } +*{ + box-sizing: border-box; +} a:focus, a:active, diff --git a/packages/styles/src/variables.css b/packages/styles/src/variables.css index aebc45ec..5c0887c1 100644 --- a/packages/styles/src/variables.css +++ b/packages/styles/src/variables.css @@ -7,12 +7,56 @@ /* 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-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-background-color: #001628; + --aside-height: calc(100% - var(--header-height)); + --aside-width: 210px; + --aside-background-color: #001529; --aside-submenu-background-color: #0c2135; + --aside-text-color: #fff; + + --trigger-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 */ + } @media (prefers-color-scheme: dark) { :root { + visibility: visible; } } diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index de51fc39..df796429 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -1,3 +1,17 @@ +import { + CacheTypeEnum, + ContentLayoutEnum, + MenuModeEnum, + NavBarModeEnum, + MixSidebarTriggerEnum, + PermissionModeEnum, + SessionTimeoutProcessingEnum, + SettingButtonPositionEnum, + ThemeEnum, + TriggerEnum, + RouterTransitionEnum, +} from '@vben/constants' + export type LocaleType = 'zh_CN' | 'en' export interface LocaleConfig { @@ -42,3 +56,143 @@ export interface GlobEnvConfig { // Project abbreviation VITE_GLOB_APP_SHORT_NAME: string } + +export interface DefineAppConfigOptions { + // Navigation bar mode + navBarMode: NavBarModeEnum + // Theme + theme: ThemeEnum + // Theme color + themeColor: string + // Whether to show the theme switch button + showThemeModeToggle: boolean + // pageLayout whether to enable keep-alive + openKeepAlive: boolean + // Whether to open back to top + useOpenBackTop: boolean + // Is it possible to embed iframe pages + canEmbedIFramePage: boolean + // Whether to delete unclosed messages and notify when switching the interface + closeMessageOnSwitch: boolean + closeMixSidebarOnChange: boolean + // Whether to cancel the http request that has been sent but not responded when switching the interface. + removeAllHttpPending: boolean + // Storage location of permission related information + permissionCacheType: CacheTypeEnum + // Configure where the button is displayed + settingButtonPosition: SettingButtonPositionEnum + // Configuration setting drawer open + openSettingDrawer: boolean + // Permission mode + permissionMode: PermissionModeEnum + // Session timeout processing + sessionTimeoutProcessing: SessionTimeoutProcessingEnum + // Website gray mode, open for possible mourning dates + grayMode: boolean + // Whether to turn on the color weak mode + colorWeak: boolean + // Lock screen time + lockTime: number + // Whether to show the lock screen + useLockPage: boolean + sidebar: SidebarConfigOptions + menu: MenuConfigOptions + header: HeaderConfigOptions + logo: LogoConfigOptions + tabTar: TabTbrConfigOptions + content: ContentConfigOptions + footer: FooterConfigOptions + transition: TransitionConfigOptions +} + +export interface SidebarConfigOptions { + show: boolean + visible: boolean + fixed: boolean + bgColor: string + collapsed: boolean + width: number + trigger: TriggerEnum + readonly mixSidebarWidth: number + readonly collapsedWidth: number +} +export interface MenuConfigOptions { + canDrag: boolean + split: boolean + mode: MenuModeEnum + accordion: boolean + collapsedShowLabel: boolean + mixSideTrigger: MixSidebarTriggerEnum + mixSideFixed: boolean + topMenuAlign: 'start' | 'center' | 'end' + dropdownPlacement: + | 'top-start' + | 'top' + | 'top-end' + | 'right-start' + | 'right' + | 'right-end' + | 'bottom-start' + | 'bottom' + | 'bottom-end' + | 'left-start' + | 'left' + | 'left-end' +} + +export interface HeaderConfigOptions { + show: boolean + visible: boolean + bgColor: string + fixed: boolean + showFullScreen: boolean + showDoc: boolean + showNotice: boolean + showSearch: boolean + showLocalePicker: boolean + showSetting: boolean + readonly height: number + // Show breadcrumbs + showBreadCrumb: boolean + // Show breadcrumb icon + showBreadCrumbIcon: boolean +} + +export interface LogoConfigOptions { + show: boolean + visible: boolean + showTitle: boolean +} + +export interface TabTbrConfigOptions { + show: boolean + visible: boolean + cache: boolean + canDrag: boolean + showQuick: boolean + showRedo: boolean + showFold: boolean + readonly height: number +} + +export interface ContentConfigOptions { + fullScreen: boolean + mode: ContentLayoutEnum +} + +export interface FooterConfigOptions { + show: boolean + visible: boolean + readonly height: number +} + +export interface TransitionConfigOptions { + // Whether to open the page switching animation + enable: boolean + // Route basic switching animation + basicTransition: RouterTransitionEnum + // Whether to open page switching loading + openPageLoading: boolean + // Whether to open the top progress bar + openNProgress: boolean +} diff --git a/packages/types/src/global.ts b/packages/types/src/global.ts index 87e3ed03..97426155 100644 --- a/packages/types/src/global.ts +++ b/packages/types/src/global.ts @@ -1,7 +1,7 @@ import type { VNodeChild, PropType as VuePropType } from 'vue' import type { RouteRecordItem as IRouteRecordItem } from './router' -declare global { +declare global { // define global const __VITE_USE_MOCK__: boolean const __APP_INFO__: { @@ -35,33 +35,33 @@ declare global { } interface Fn { - (...arg: T[]): R; + (...arg: T[]): R } interface PromiseFn { - (...arg: T[]): Promise; + (...arg: T[]): Promise } - type RefType = T | null; + type RefType = T | null type LabelValueOptions = { - label: string; - value: any; - [key: string]: string | number | boolean; - }[]; + label: string + value: any + [key: string]: string | number | boolean + }[] - type EmitType = (event: string, ...args: any[]) => void; + type EmitType = (event: string, ...args: any[]) => void - type TargetContext = '_self' | '_blank'; + type TargetContext = '_self' | '_blank' interface ComponentElRef { - $el: T; + $el: T } - type ComponentRef = ComponentElRef | null; - - type ElRef = Nullable; + type ComponentRef = + ComponentElRef | null + type ElRef = Nullable // import.meta interface ImportMetaEnv extends ViteEnv { diff --git a/packages/types/src/setting.ts b/packages/types/src/setting.ts index 02614616..e40ee86c 100644 --- a/packages/types/src/setting.ts +++ b/packages/types/src/setting.ts @@ -9,7 +9,7 @@ import { SettingButtonPositionEnum, ThemeEnum, TriggerEnum, - RouterTransitionEnum + RouterTransitionEnum, } from '@vben/constants' import { LocaleType } from './config' // export type LocaleType = 'zh_CN' | 'en' | 'ru' | 'ja' | 'ko' @@ -33,21 +33,27 @@ export interface MenuSetting { collapsedShowTitle: boolean mixSideTrigger: MixSidebarTriggerEnum mixSideFixed: boolean + readonly width: number + readonly mixSidebarWidth: number + readonly collapsedWidth: number } export interface MultiTabsSetting { cache: boolean show: boolean + hidden: boolean showQuick: boolean canDrag: boolean showRedo: boolean showFold: boolean + readonly height: number } export interface HeaderSetting { bgColor: string fixed: boolean show: boolean + hidden: boolean theme: ThemeEnum // Turn on full screen showFullScreen: boolean @@ -59,6 +65,7 @@ export interface HeaderSetting { showNotice: boolean showSearch: boolean showLocalePicker: boolean + readonly height: number } export interface LocaleSetting { @@ -73,16 +80,34 @@ export interface LocaleSetting { export interface TransitionSetting { // Whether to open the page switching animation - enable: boolean; + enable: boolean // Route basic switching animation - basicTransition: RouterTransitionEnum; + basicTransition: RouterTransitionEnum // Whether to open page switching loading - openPageLoading: boolean; + openPageLoading: boolean // Whether to open the top progress bar - openNProgress: boolean; + openNProgress: boolean } -export interface ProjectConfig { +export interface SporadicSetting { + // Whether to open the top progress bar + openNProgress: boolean + // pageLayout whether to enable keep-alive + openKeepAlive: boolean + // Lock screen time + lockTime: number + // Show breadcrumbs + showBreadCrumb: boolean + // Show breadcrumb icon + showBreadCrumbIcon: boolean + // Whether to open back to top + useOpenBackTop: boolean + // Is it possible to embed iframe pages + canEmbedIFramePage: boolean + // Whether to delete unclosed messages and notify when switching the interface + closeMessageOnSwitch: boolean + // Whether to cancel the http request that has been sent but not responded when switching the interface. + removeAllHttpPending: boolean // Storage location of permission related information permissionCacheType: CacheTypeEnum // Whether to show the configuration button @@ -91,6 +116,8 @@ export interface ProjectConfig { showDarkModeToggle: boolean // Configure where the button is displayed settingButtonPosition: SettingButtonPositionEnum + // Configure where the Setting Drawer is displayed + showSettingDrawer: boolean // Permission mode permissionMode: PermissionModeEnum // Session timeout processing @@ -110,6 +137,11 @@ export interface ProjectConfig { showLogo: boolean // Whether to show the global footer showFooter: boolean + hiddenFooter: boolean + readonly footerHeight: number +} + +export interface ProjectConfig extends SporadicSetting { // menuType: MenuTypeEnum; headerSetting: HeaderSetting // menuSetting @@ -117,23 +149,5 @@ export interface ProjectConfig { // Multi-tab settings multiTabsSetting: MultiTabsSetting // Animation configuration - transitionSetting: TransitionSetting; - // Whether to open the top progress bar - openNProgress: false - // pageLayout whether to enable keep-alive - openKeepAlive: boolean - // Lock screen time - lockTime: number - // Show breadcrumbs - showBreadCrumb: boolean - // Show breadcrumb icon - showBreadCrumbIcon: boolean - // Whether to open back to top - useOpenBackTop: boolean - // Is it possible to embed iframe pages - canEmbedIFramePage: boolean - // Whether to delete unclosed messages and notify when switching the interface - closeMessageOnSwitch: boolean - // Whether to cancel the http request that has been sent but not responded when switching the interface. - removeAllHttpPending: boolean + transitionSetting: TransitionSetting } diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 986b306b..41e86323 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -8,6 +8,10 @@ export { clone, isArray, isString, + toString, + assign as _assign, + merge as _merge, + omit as _omit, } from 'lodash-es' // @ts-ignore import Sortable from 'sortablejs' diff --git a/packages/utils/src/color.ts b/packages/utils/src/color.ts new file mode 100644 index 00000000..6347797a --- /dev/null +++ b/packages/utils/src/color.ts @@ -0,0 +1,178 @@ +/** + * 判断是否 十六进制颜色值. + * 输入形式可为 #fff000 #f00 + * + * @param String color 十六进制颜色值 + * @return Boolean + */ +export function isHexColor(color: string) { + const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/ + return reg.test(color) +} + +/** + * RGB 颜色值转换为 十六进制颜色值. + * r, g, 和 b 需要在 [0, 255] 范围内 + * + * @return String 类似#ff00ff + * @param r + * @param g + * @param b + */ +export function rgbToHex(r: number, g: number, b: number) { + // tslint:disable-next-line:no-bitwise + const hex = ((r << 16) | (g << 8) | b).toString(16) + return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex +} + +/** + * Transform a HEX color to its RGB representation + * @param {string} hex The color to transform + * @returns The RGB representation of the passed color + */ +export function hexToRGB(hex: string) { + let sHex = hex.toLowerCase() + if (isHexColor(hex)) { + if (sHex.length === 4) { + let sColorNew = '#' + for (let i = 1; i < 4; i += 1) { + sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1)) + } + sHex = sColorNew + } + const sColorChange: number[] = [] + for (let i = 1; i < 7; i += 2) { + sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2))) + } + return 'RGB(' + sColorChange.join(',') + ')' + } + return sHex +} + +export function colorIsDark(color: string) { + if (!isHexColor(color)) return + const [r, g, b] = hexToRGB(color) + .replace(/(?:\(|\)|rgb|RGB)*/g, '') + .split(',') + .map((item) => Number(item)) + return r * 0.299 + g * 0.578 + b * 0.114 < 192 +} + +/** + * Darkens a HEX color given the passed percentage + * @param {string} color The color to process + * @param {number} amount The amount to change the color by + * @returns {string} The HEX representation of the processed color + */ +export function darken(color: string, amount: number) { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( + color.substring(2, 4), + amount, + )}${subtractLight(color.substring(4, 6), amount)}` +} + +/** + * Lightens a 6 char HEX color according to the passed percentage + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed color represented as HEX + */ +export function lighten(color: string, amount: number) { + color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color + amount = Math.trunc((255 * amount) / 100) + return `#${addLight(color.substring(0, 2), amount)}${addLight( + color.substring(2, 4), + amount, + )}${addLight(color.substring(4, 6), amount)}` +} + +/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ +/** + * Sums the passed percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +function addLight(color: string, amount: number) { + const cc = parseInt(color, 16) + amount + const c = cc > 255 ? 255 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +/** + * Calculates luminance of an rgb color + * @param {number} r red + * @param {number} g green + * @param {number} b blue + */ +function luminanace(r: number, g: number, b: number) { + const a = [r, g, b].map((v) => { + v /= 255 + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) + }) + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 +} + +/** + * Calculates contrast between two rgb colors + * @param {string} rgb1 rgb color 1 + * @param {string} rgb2 rgb color 2 + */ +function contrast(rgb1: string[], rgb2: number[]) { + return ( + (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / + (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05) + ) +} + +/** + * Determines what the best text color is (black or white) based con the contrast with the background + * @param hexColor - Last selected color by the user + */ +export function calculateBestTextColor(hexColor: string) { + const rgbColor = hexToRGB(hexColor.substring(1)) + const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]) + + return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF' +} + +/** + * Subtracts the indicated percentage to the R, G or B of a HEX color + * @param {string} color The color to change + * @param {number} amount The amount to change the color by + * @returns {string} The processed part of the color + */ +function subtractLight(color: string, amount: number) { + const cc = parseInt(color, 16) - amount + const c = cc < 0 ? 0 : cc + return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` +} + +/** + * Pick text color based on background color + * @param bgColor {string} + * @param lightColor {string} + * @param darkColor {string} + * @returns {string} text color + */ +export function pickTextColorBasedOnBgColor( + bgColor: string, + lightColor: string, + darkColor: string, +) { + const color = bgColor.charAt(0) === '#' ? bgColor.substring(1, 7) : bgColor + const r = parseInt(color.substring(0, 2), 16) // hexToR + const g = parseInt(color.substring(2, 4), 16) // hexToG + const b = parseInt(color.substring(4, 6), 16) // hexToB + const uicolors = [r / 255, g / 255, b / 255] + const c = uicolors.map((col) => { + if (col <= 0.03928) { + return col / 12.92 + } + return Math.pow((col + 0.055) / 1.055, 2.4) + }) + const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] + return L < 0.8 ? darkColor : lightColor +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 621b5f7e..b7455ccc 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -9,3 +9,4 @@ export * from './tree' export * from './router' export * from './mitt' export * from './theme' +export * from './color' diff --git a/packages/vbenComponents/package.json b/packages/vbenComponents/package.json index 4a9e303b..0d0134ab 100644 --- a/packages/vbenComponents/package.json +++ b/packages/vbenComponents/package.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@iconify/iconify": "^3.0.0", + "@vben/hooks": "workspace:*", "@vben/utils": "workspace:*", "@vben/constants": "workspace:*", "sass": "^1.55.0", diff --git a/packages/vbenComponents/src/config/src/Config.vue b/packages/vbenComponents/src/config/src/Config.vue index ee20bd3b..fc004ad0 100644 --- a/packages/vbenComponents/src/config/src/Config.vue +++ b/packages/vbenComponents/src/config/src/Config.vue @@ -1,18 +1,15 @@ - + diff --git a/packages/vbenComponents/src/iconify/src/Iconify.vue b/packages/vbenComponents/src/iconify/src/Iconify.vue index e901d333..607a8b9f 100644 --- a/packages/vbenComponents/src/iconify/src/Iconify.vue +++ b/packages/vbenComponents/src/iconify/src/Iconify.vue @@ -87,7 +87,7 @@ onMounted(update)
+ {{ userInfo.realName }} +