refactor: refacotr preference
parent
f7b97e8a83
commit
fed47f5e05
|
@ -22,14 +22,14 @@
|
||||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vben-core/preferences": "workspace:*",
|
||||||
|
"@vben-core/stores": "workspace:*",
|
||||||
"@vben/common-ui": "workspace:*",
|
"@vben/common-ui": "workspace:*",
|
||||||
"@vben/constants": "workspace:*",
|
"@vben/constants": "workspace:*",
|
||||||
"@vben/hooks": "workspace:*",
|
"@vben/hooks": "workspace:*",
|
||||||
"@vben/icons": "workspace:*",
|
"@vben/icons": "workspace:*",
|
||||||
"@vben/layouts": "workspace:*",
|
"@vben/layouts": "workspace:*",
|
||||||
"@vben/locales": "workspace:*",
|
"@vben/locales": "workspace:*",
|
||||||
"@vben/preference": "workspace:*",
|
|
||||||
"@vben/stores": "workspace:*",
|
|
||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
"ant-design-vue": "^4.2.1",
|
"ant-design-vue": "^4.2.1",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
"pinia": "2.1.7",
|
||||||
"vue": "3.4.27",
|
"vue": "3.4.27",
|
||||||
"vue-router": "^4.3.2"
|
"vue-router": "^4.3.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||||
|
|
||||||
import { GlobalProvider } from '@vben/common-ui';
|
import { GlobalProvider } from '@vben/common-ui';
|
||||||
import { preference, usePreference } from '@vben/preference';
|
|
||||||
import { ConfigProvider, theme } from 'ant-design-vue';
|
import { ConfigProvider, theme } from 'ant-design-vue';
|
||||||
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
@ -12,21 +13,20 @@ defineOptions({ name: 'App' });
|
||||||
|
|
||||||
dayjs.locale(zhCN.locale);
|
dayjs.locale(zhCN.locale);
|
||||||
|
|
||||||
const { isDark } = usePreference();
|
const { isDark } = usePreferences();
|
||||||
|
|
||||||
const tokenTheme = computed(() => {
|
const tokenTheme = computed(() => {
|
||||||
const { colorPrimary, compact } = preference;
|
|
||||||
const algorithms = isDark.value
|
const algorithms = isDark.value
|
||||||
? [theme.darkAlgorithm]
|
? [theme.darkAlgorithm]
|
||||||
: [theme.defaultAlgorithm];
|
: [theme.defaultAlgorithm];
|
||||||
|
|
||||||
// antd 紧凑模式算法
|
// antd 紧凑模式算法
|
||||||
if (compact) {
|
if (preferences.app.compact) {
|
||||||
algorithms.push(theme.compactAlgorithm);
|
algorithms.push(theme.compactAlgorithm);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
algorithms,
|
algorithms,
|
||||||
token: { colorPrimary },
|
token: { colorPrimary: preferences.theme.colorPrimary },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import '@vben/styles';
|
import '@vben/styles';
|
||||||
|
|
||||||
|
import { preferences } from '@vben-core/preferences';
|
||||||
|
|
||||||
|
import { setupStore } from '@/store';
|
||||||
import { setupI18n } from '@vben/locales';
|
import { setupI18n } from '@vben/locales';
|
||||||
import { preference } from '@vben/preference';
|
|
||||||
import { setupStore } from '@vben/stores';
|
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
|
|
||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
|
@ -12,7 +13,7 @@ async function bootstrap(namespace: string) {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
// 国际化 i18n 配置
|
// 国际化 i18n 配置
|
||||||
await setupI18n(app, { defaultLocale: preference.locale });
|
await setupI18n(app, { defaultLocale: preferences.app.locale });
|
||||||
|
|
||||||
// 配置 pinia-store
|
// 配置 pinia-store
|
||||||
await setupStore(app, { namespace });
|
await setupStore(app, { namespace });
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { NotificationItem } from '@vben/common-ui';
|
import type { NotificationItem } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { preferences } from '@vben-core/preferences';
|
||||||
|
import { useAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
import { Notification, UserDropdown } from '@vben/common-ui';
|
import { Notification, UserDropdown } from '@vben/common-ui';
|
||||||
import { IcRoundCreditScore, MdiDriveDocument, MdiGithub } from '@vben/icons';
|
import { IcRoundCreditScore, MdiDriveDocument, MdiGithub } from '@vben/icons';
|
||||||
import { BasicLayout } from '@vben/layouts';
|
import { BasicLayout } from '@vben/layouts';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { preference } from '@vben/preference';
|
|
||||||
import { useAccessStore } from '@vben/stores';
|
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
@ -93,7 +94,7 @@ function handleNoticeClear() {
|
||||||
<BasicLayout>
|
<BasicLayout>
|
||||||
<template #user-dropdown>
|
<template #user-dropdown>
|
||||||
<UserDropdown
|
<UserDropdown
|
||||||
:avatar="preference.defaultAvatar"
|
:avatar="preferences.app.defaultAvatar"
|
||||||
:menus="menus"
|
:menus="menus"
|
||||||
text="Vben Admin"
|
text="Vben Admin"
|
||||||
description="ann.vben@gmail.com"
|
description="ann.vben@gmail.com"
|
||||||
|
|
|
@ -2,7 +2,7 @@ const BasicLayout = () => import('./basic.vue');
|
||||||
|
|
||||||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||||
|
|
||||||
const AuthPageLayout = () =>
|
const AuthPageLayoutType = () =>
|
||||||
import('@vben/layouts').then((m) => m.AuthPageLayout);
|
import('@vben/layouts').then((m) => m.AuthPageLayoutType);
|
||||||
|
|
||||||
export { AuthPageLayout, BasicLayout, IFrameView };
|
export { AuthPageLayoutType, BasicLayout, IFrameView };
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import { setupPreference } from '@vben/preference';
|
import { preferencesManager } from '@vben-core/preferences';
|
||||||
|
|
||||||
import { overridesPreference } from './preference';
|
import { overridesPreferences } from './preferences';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用初始化完成之后再进行页面加载渲染
|
* 应用初始化完成之后再进行页面加载渲染
|
||||||
*/
|
*/
|
||||||
async function initApplication() {
|
async function initApplication() {
|
||||||
|
// name用于指定项目唯一标识
|
||||||
|
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
||||||
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
||||||
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${env}`;
|
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${env}`;
|
||||||
|
|
||||||
// app偏好设置初始化
|
// app偏好设置初始化
|
||||||
await setupPreference({
|
await preferencesManager.initPreferences({
|
||||||
namespace,
|
namespace,
|
||||||
overrides: overridesPreference,
|
overrides: overridesPreferences,
|
||||||
});
|
});
|
||||||
|
|
||||||
import('./bootstrap').then((m) => m.bootstrap(namespace));
|
import('./bootstrap').then((m) => m.bootstrap(namespace));
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import type { Preference } from '@vben/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 项目配置文件
|
|
||||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
|
||||||
*/
|
|
||||||
export const overridesPreference: Partial<Preference> = {};
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { DeepPartial, Preferences } from '@vben/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 项目配置文件
|
||||||
|
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||||
|
*/
|
||||||
|
export const overridesPreferences: DeepPartial<Preferences> = {
|
||||||
|
app: {
|
||||||
|
name: 'Vben Admin',
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,9 +1,9 @@
|
||||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben/types';
|
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben/types';
|
||||||
|
|
||||||
|
import { useAccessStore } from '@vben-core/stores';
|
||||||
import type { RouteRecordRaw, Router } from 'vue-router';
|
import type { RouteRecordRaw, Router } from 'vue-router';
|
||||||
|
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
import { useAccessStore } from '@vben/stores';
|
|
||||||
import { filterTree, mapTree, traverseTreeValues } from '@vben/utils';
|
import { filterTree, mapTree, traverseTreeValues } from '@vben/utils';
|
||||||
|
|
||||||
import { dynamicRoutes } from '@/router/routes';
|
import { dynamicRoutes } from '@/router/routes';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { preferences } from '@vben-core/preferences';
|
||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { preference } from '@vben/preference';
|
|
||||||
import { startProgress, stopProgress } from '@vben/utils';
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
import { useTitle } from '@vueuse/core';
|
import { useTitle } from '@vueuse/core';
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ function configCommonGuard(router: Router) {
|
||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
// 页面加载进度条
|
// 页面加载进度条
|
||||||
if (preference.pageProgress) {
|
if (preferences.transition.progress) {
|
||||||
startProgress();
|
startProgress();
|
||||||
}
|
}
|
||||||
to.meta.loaded = loadedPaths.has(to.path);
|
to.meta.loaded = loadedPaths.has(to.path);
|
||||||
|
@ -29,14 +29,14 @@ function configCommonGuard(router: Router) {
|
||||||
loadedPaths.add(to.path);
|
loadedPaths.add(to.path);
|
||||||
|
|
||||||
// 关闭页面加载进度条
|
// 关闭页面加载进度条
|
||||||
if (preference.pageProgress) {
|
if (preferences.transition.progress) {
|
||||||
stopProgress();
|
stopProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态修改标题
|
// 动态修改标题
|
||||||
if (preference.dynamicTitle) {
|
if (preferences.app.dynamicTitle) {
|
||||||
const { title } = to.meta;
|
const { title } = to.meta;
|
||||||
useTitle(`${$t(title)} - ${preference.appName}`);
|
useTitle(`${$t(title)} - ${preferences.app.name}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import { AuthPageLayout } from '@/layouts';
|
import { AuthPageLayoutType } from '@/layouts';
|
||||||
import { Fallback } from '@vben/common-ui';
|
import { Fallback } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import Login from '@/views/_essential/authentication/login.vue';
|
||||||
/** 基本路由,这些路由是必须存在的 */
|
/** 基本路由,这些路由是必须存在的 */
|
||||||
const essentialRoutes: RouteRecordRaw[] = [
|
const essentialRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
component: AuthPageLayout,
|
component: AuthPageLayoutType,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Authentication',
|
title: 'Authentication',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
|
import { preferences } from '@vben-core/preferences';
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import { BasicLayout, IFrameView } from '@/layouts';
|
import { BasicLayout, IFrameView } from '@/layouts';
|
||||||
import { VBEN_GITHUB_URL } from '@vben/constants';
|
import { VBEN_GITHUB_URL } from '@vben/constants';
|
||||||
import { $t } from '@vben/locales/helper';
|
import { $t } from '@vben/locales/helper';
|
||||||
import { preference } from '@vben/preference';
|
|
||||||
|
|
||||||
export const vbenRoutes: RouteRecordRaw[] = [
|
export const vbenRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
component: BasicLayout,
|
component: BasicLayout,
|
||||||
meta: {
|
meta: {
|
||||||
icon: preference.logo,
|
icon: preferences.logo.source,
|
||||||
title: 'Vben',
|
title: 'Vben',
|
||||||
},
|
},
|
||||||
name: 'AboutLayout',
|
name: 'AboutLayout',
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
* 该文件可自行根据业务逻辑进行调整
|
* 该文件可自行根据业务逻辑进行调整
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import axios, {
|
import axios, {
|
||||||
AxiosError,
|
AxiosError,
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { InitStoreOptions } from '@vben-core/stores';
|
||||||
|
|
||||||
|
import { initStore } from '@vben-core/stores';
|
||||||
|
|
||||||
|
import type { App } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 初始化pinia
|
||||||
|
* @param app vue app 实例
|
||||||
|
*/
|
||||||
|
async function setupStore(app: App, options: InitStoreOptions) {
|
||||||
|
const pinia = await initStore(options);
|
||||||
|
app.use(pinia);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setupStore };
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import {
|
||||||
|
// beforeEach,
|
||||||
|
describe,
|
||||||
|
// expect,
|
||||||
|
it,
|
||||||
|
} from 'vitest';
|
||||||
|
|
||||||
|
// import { useAccessStore } from '../modules/access';
|
||||||
|
|
||||||
|
describe('useCounterStore', () => {
|
||||||
|
it('app Name with test', () => {
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
// let referenceStore = usePreferencesStore();
|
||||||
|
|
||||||
|
// beforeEach(() => {
|
||||||
|
// referenceStore = usePreferencesStore();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// expect(referenceStore.appName).toBe('vben-admin');
|
||||||
|
// referenceStore.setAppName('vbenAdmin');
|
||||||
|
// expect(referenceStore.getAppName).toBe('vbenAdmin');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', {
|
||||||
|
actions: {
|
||||||
|
increment() {
|
||||||
|
this.count++;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
double: (state) => state.count * 2,
|
||||||
|
},
|
||||||
|
state: () => ({ count: 0 }),
|
||||||
|
});
|
|
@ -1,11 +1,12 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { LoginAndRegisterParams } from '@vben/common-ui';
|
import type { LoginAndRegisterParams } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useAccessStore } from '@vben-core/stores';
|
||||||
|
|
||||||
import { getUserInfo, userLogin } from '@/services';
|
import { getUserInfo, userLogin } from '@/services';
|
||||||
import { AuthenticationLogin } from '@vben/common-ui';
|
import { AuthenticationLogin } from '@vben/common-ui';
|
||||||
import { useRequest } from '@vben/hooks';
|
import { useRequest } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { useAccessStore } from '@vben/stores';
|
|
||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
"eslint-plugin-command": "^0.2.3"
|
"eslint-plugin-command": "^0.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.3.0",
|
"@eslint/js": "^9.4.0",
|
||||||
"@types/eslint": "^8.56.10",
|
"@types/eslint": "^8.56.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||||
"@typescript-eslint/parser": "^7.11.0",
|
"@typescript-eslint/parser": "^7.11.0",
|
||||||
|
@ -54,6 +54,6 @@
|
||||||
"eslint-plugin-vue": "^9.26.0",
|
"eslint-plugin-vue": "^9.26.0",
|
||||||
"globals": "^15.3.0",
|
"globals": "^15.3.0",
|
||||||
"jsonc-eslint-parser": "^2.4.0",
|
"jsonc-eslint-parser": "^2.4.0",
|
||||||
"vue-eslint-parser": "^9.4.2"
|
"vue-eslint-parser": "^9.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prettier": "^3.2.5",
|
"prettier": "3.3.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.0"
|
"prettier-plugin-tailwindcss": "^0.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"postcss-html": "^1.7.0",
|
"postcss-html": "^1.7.0",
|
||||||
"postcss-scss": "^4.0.9",
|
"postcss-scss": "^4.0.9",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "3.3.0",
|
||||||
"stylelint": "^16.6.1",
|
"stylelint": "^16.6.1",
|
||||||
"stylelint-config-recommended": "^14.0.0",
|
"stylelint-config-recommended": "^14.0.0",
|
||||||
"stylelint-config-recommended-scss": "^14.0.0",
|
"stylelint-config-recommended-scss": "^14.0.0",
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"find-up": "^7.0.0",
|
"find-up": "^7.0.0",
|
||||||
"pkg-types": "^1.1.1",
|
"pkg-types": "^1.1.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "3.3.0",
|
||||||
"rimraf": "^5.0.7",
|
"rimraf": "^5.0.7",
|
||||||
"zx": "^7.2.3"
|
"zx": "^7.2.3"
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/html-minifier-terser": "^7.0.2",
|
"@types/html-minifier-terser": "^7.0.2",
|
||||||
"@vben/node-utils": "workspace:*",
|
"@vben/node-utils": "workspace:*",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
"@changesets/cli": "^2.27.5",
|
"@changesets/cli": "^2.27.5",
|
||||||
"@ls-lint/ls-lint": "^2.2.3",
|
"@ls-lint/ls-lint": "^2.2.3",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^20.12.13",
|
"@types/node": "^20.13.0",
|
||||||
"@vben/commitlint-config": "workspace:*",
|
"@vben/commitlint-config": "workspace:*",
|
||||||
"@vben/eslint-config": "workspace:*",
|
"@vben/eslint-config": "workspace:*",
|
||||||
"@vben/lint-staged-config": "workspace:*",
|
"@vben/lint-staged-config": "workspace:*",
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# @vben-core
|
# @vben-core
|
||||||
|
|
||||||
系统一些比较基础的SDK和UI组件库,请勿将任何业务逻辑和业务包放在这里。
|
系统一些比较基础的SDK和UI组件库,该目录后续可能会迁移出去或者发布到npm,请勿将任何业务逻辑和业务包放在该目录。
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# @vben-core/forward
|
||||||
|
|
||||||
|
该目录内的包,可直接被app所引用
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@vben/preference",
|
"name": "@vben-core/preferences",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
"directory": "packages/preference"
|
"directory": "packages/@vben-core/preferences"
|
||||||
},
|
},
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -32,6 +32,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vben-core/cache": "workspace:*",
|
||||||
|
"@vben-core/helpers": "workspace:*",
|
||||||
"@vben-core/toolkit": "workspace:*",
|
"@vben-core/toolkit": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"@vueuse/core": "^10.10.0",
|
"@vueuse/core": "^10.10.0",
|
|
@ -0,0 +1,77 @@
|
||||||
|
import type { Preferences } from './types';
|
||||||
|
|
||||||
|
const defaultPreferences: Preferences = {
|
||||||
|
app: {
|
||||||
|
authPageLayout: 'panel-right',
|
||||||
|
colorGrayMode: false,
|
||||||
|
colorWeakMode: false,
|
||||||
|
compact: false,
|
||||||
|
contentCompact: 'wide',
|
||||||
|
copyright: 'Copyright © 2024 Vben Admin PRO',
|
||||||
|
defaultAvatar:
|
||||||
|
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.0/source/avatar-v1.webp',
|
||||||
|
dynamicTitle: true,
|
||||||
|
isMobile: false,
|
||||||
|
layout: 'side-nav',
|
||||||
|
locale: 'zh-CN',
|
||||||
|
name: 'Vben Admin Pro',
|
||||||
|
semiDarkMenu: true,
|
||||||
|
showPreference: true,
|
||||||
|
themeMode: 'dark',
|
||||||
|
},
|
||||||
|
breadcrumb: {
|
||||||
|
enable: true,
|
||||||
|
hideOnlyOne: false,
|
||||||
|
showHome: false,
|
||||||
|
showIcon: true,
|
||||||
|
styleType: 'normal',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
enable: true,
|
||||||
|
fixed: true,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
enable: true,
|
||||||
|
hidden: false,
|
||||||
|
mode: 'fixed',
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
enable: true,
|
||||||
|
source:
|
||||||
|
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.0/source/logo-v1.webp',
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
accordion: true,
|
||||||
|
split: true,
|
||||||
|
styleType: 'rounded',
|
||||||
|
},
|
||||||
|
|
||||||
|
shortcutKeys: { enable: true },
|
||||||
|
sidebar: {
|
||||||
|
collapse: false,
|
||||||
|
collapseShowTitle: true,
|
||||||
|
enable: true,
|
||||||
|
expandOnHover: true,
|
||||||
|
extraCollapse: true,
|
||||||
|
hidden: false,
|
||||||
|
width: 240,
|
||||||
|
},
|
||||||
|
|
||||||
|
tabbar: {
|
||||||
|
enable: true,
|
||||||
|
keepAlive: true,
|
||||||
|
showIcon: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
colorPrimary: 'hsl(211 91% 39%)',
|
||||||
|
},
|
||||||
|
|
||||||
|
transition: {
|
||||||
|
enable: true,
|
||||||
|
name: 'fade-slide',
|
||||||
|
progress: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { defaultPreferences };
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { LocaleSupportType } from './types';
|
||||||
|
|
||||||
|
interface Language {
|
||||||
|
key: LocaleSupportType;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const COLOR_PRIMARY_RESETS = [
|
||||||
|
'hsl(211 91% 39%)',
|
||||||
|
'hsl(212 100% 45%)',
|
||||||
|
'hsl(181 84% 32%)',
|
||||||
|
'hsl(230 99% 66%)',
|
||||||
|
'hsl(245 82% 67%)',
|
||||||
|
'hsl(340 100% 68%)',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SUPPORT_LANGUAGES: Language[] = [
|
||||||
|
{
|
||||||
|
key: 'zh-CN',
|
||||||
|
text: '简体中文',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'en-US',
|
||||||
|
text: 'English',
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Flatten } from '@vben-core/typings';
|
||||||
|
|
||||||
|
import { preferencesManager } from './preferences';
|
||||||
|
|
||||||
|
import type { Preferences } from './types';
|
||||||
|
|
||||||
|
// 偏好设置(带有层级关系)
|
||||||
|
const preferences: Preferences = preferencesManager.getPreferences();
|
||||||
|
|
||||||
|
// 扁平化后的偏好设置
|
||||||
|
const flatPreferences: Flatten<Preferences> =
|
||||||
|
preferencesManager.getFlatPreferences();
|
||||||
|
|
||||||
|
// 更新偏好设置
|
||||||
|
const updatePreferences =
|
||||||
|
preferencesManager.updatePreferences.bind(preferencesManager);
|
||||||
|
|
||||||
|
// 重置偏好设置
|
||||||
|
const resetPreferences =
|
||||||
|
preferencesManager.resetPreferences.bind(preferencesManager);
|
||||||
|
|
||||||
|
export {
|
||||||
|
flatPreferences,
|
||||||
|
preferences,
|
||||||
|
preferencesManager,
|
||||||
|
resetPreferences,
|
||||||
|
updatePreferences,
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from './constants';
|
||||||
|
export type * from './types';
|
||||||
|
export * from './use-preferences';
|
|
@ -0,0 +1,289 @@
|
||||||
|
import type {
|
||||||
|
DeepPartial,
|
||||||
|
Flatten,
|
||||||
|
FlattenObjectKeys,
|
||||||
|
} from '@vben-core/typings';
|
||||||
|
|
||||||
|
import { StorageManager } from '@vben-core/cache';
|
||||||
|
import { flattenObject, toNestedObject } from '@vben-core/helpers';
|
||||||
|
import { convertToHslCssVar, merge } from '@vben-core/toolkit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
breakpointsTailwind,
|
||||||
|
useBreakpoints,
|
||||||
|
useCssVar,
|
||||||
|
useDebounceFn,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
import { markRaw, reactive, watch } from 'vue';
|
||||||
|
|
||||||
|
import { defaultPreferences } from './config';
|
||||||
|
|
||||||
|
import type { Preferences } from './types';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'preferences';
|
||||||
|
|
||||||
|
interface initialOptions {
|
||||||
|
namespace: string;
|
||||||
|
overrides?: DeepPartial<Preferences>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDarkTheme(theme: string) {
|
||||||
|
let dark = theme === 'dark';
|
||||||
|
if (theme === 'auto') {
|
||||||
|
dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
}
|
||||||
|
return dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreferenceManager {
|
||||||
|
private cache: StorageManager<Preferences> | null = null;
|
||||||
|
private flattenedState: Flatten<Preferences>;
|
||||||
|
private initialPreferences: Preferences = defaultPreferences;
|
||||||
|
private isInitialized: boolean = false;
|
||||||
|
private savePreferences: (preference: Preferences) => void;
|
||||||
|
private state: Preferences = reactive<Preferences>({
|
||||||
|
...this.loadPreferences(),
|
||||||
|
});
|
||||||
|
constructor() {
|
||||||
|
this.cache = new StorageManager();
|
||||||
|
this.flattenedState = reactive(flattenObject(this.state));
|
||||||
|
|
||||||
|
this.savePreferences = useDebounceFn(
|
||||||
|
(preference: Preferences) => this._savePreferences(preference),
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存偏好设置
|
||||||
|
* @param {Preferences} preference - 需要保存的偏好设置
|
||||||
|
*/
|
||||||
|
private _savePreferences(preference: Preferences) {
|
||||||
|
this.cache?.setItem(STORAGE_KEY, preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更新的键值
|
||||||
|
* 根据更新的键值执行相应的操作。
|
||||||
|
*
|
||||||
|
* @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
|
||||||
|
*/
|
||||||
|
private handleUpdates(updates: DeepPartial<Preferences>) {
|
||||||
|
const themeUpdates = updates.theme || {};
|
||||||
|
const appUpdates = updates.app || {};
|
||||||
|
|
||||||
|
if (themeUpdates.colorPrimary) {
|
||||||
|
this.updateCssVar(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appUpdates.themeMode) {
|
||||||
|
this.updateTheme(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appUpdates.colorGrayMode || appUpdates.colorWeakMode) {
|
||||||
|
this.updateColorMode(this.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载偏好设置
|
||||||
|
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
|
||||||
|
* @returns {Preferences} 加载的偏好设置
|
||||||
|
*/
|
||||||
|
private loadPreferences(): Preferences {
|
||||||
|
const savedPreferences = this.cache?.getItem(STORAGE_KEY);
|
||||||
|
return savedPreferences || { ...defaultPreferences };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 监听状态和系统偏好设置的变化。
|
||||||
|
*/
|
||||||
|
private setupWatcher() {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debounceWaterState = useDebounceFn(() => {
|
||||||
|
const newFlattenedState = flattenObject(this.state);
|
||||||
|
for (const k in newFlattenedState) {
|
||||||
|
const key = k as FlattenObjectKeys<Preferences>;
|
||||||
|
this.flattenedState[key] = newFlattenedState[key];
|
||||||
|
}
|
||||||
|
this.savePreferences(this.state);
|
||||||
|
}, 16);
|
||||||
|
|
||||||
|
const debounceWaterFlattenedState = useDebounceFn(
|
||||||
|
(val: Flatten<Preferences>) => {
|
||||||
|
this.updateState(val);
|
||||||
|
this.savePreferences(this.state);
|
||||||
|
},
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听 state 的变化
|
||||||
|
watch(this.state, debounceWaterState, { deep: true });
|
||||||
|
|
||||||
|
// 监听 flattenedState 的变化并触发 set 方法
|
||||||
|
watch(this.flattenedState, debounceWaterFlattenedState, { deep: true });
|
||||||
|
|
||||||
|
// 监听断点,判断是否移动端
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
|
const isMobile = breakpoints.smaller('md');
|
||||||
|
watch(
|
||||||
|
() => isMobile.value,
|
||||||
|
(val) => {
|
||||||
|
this.updatePreferences({
|
||||||
|
app: { isMobile: val },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听系统主题偏好设置变化
|
||||||
|
window
|
||||||
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
.addEventListener('change', ({ matches: isDark }) => {
|
||||||
|
this.updatePreferences({
|
||||||
|
app: { themeMode: isDark ? 'dark' : 'light' },
|
||||||
|
});
|
||||||
|
this.updateTheme(this.state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新页面颜色模式(灰色、色弱)
|
||||||
|
* @param preference
|
||||||
|
*/
|
||||||
|
private updateColorMode(preference: Preferences) {
|
||||||
|
if (preference.app) {
|
||||||
|
const { colorGrayMode, colorWeakMode } = preference.app;
|
||||||
|
const body = document.body;
|
||||||
|
const COLOR_WEAK = 'invert-mode';
|
||||||
|
const COLOR_GRAY = 'grayscale-mode';
|
||||||
|
colorWeakMode
|
||||||
|
? body.classList.add(COLOR_WEAK)
|
||||||
|
: body.classList.remove(COLOR_WEAK);
|
||||||
|
colorGrayMode
|
||||||
|
? body.classList.add(COLOR_GRAY)
|
||||||
|
: body.classList.remove(COLOR_GRAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 CSS 变量
|
||||||
|
* @param preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。
|
||||||
|
*/
|
||||||
|
private updateCssVar(preference: Preferences) {
|
||||||
|
if (preference.theme) {
|
||||||
|
for (const [key, value] of Object.entries(preference.theme)) {
|
||||||
|
if (['colorPrimary'].includes(key)) {
|
||||||
|
const cssVarKey = key.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
|
||||||
|
const cssVarValue = useCssVar(`--${cssVarKey}`);
|
||||||
|
cssVarValue.value = convertToHslCssVar(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新状态
|
||||||
|
* 将新的扁平对象转换为嵌套对象,并与当前状态合并。
|
||||||
|
* @param {FlattenObject<Preferences>} newValue - 新的扁平对象
|
||||||
|
*/
|
||||||
|
private updateState(newValue: Flatten<Preferences>) {
|
||||||
|
const nestObj = toNestedObject(newValue, 2);
|
||||||
|
Object.assign(this.state, merge(nestObj, this.state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新主题
|
||||||
|
* @param preferences - 当前偏好设置对象,它的主题值将被用来设置文档的主题。
|
||||||
|
*/
|
||||||
|
private updateTheme(preferences: Preferences) {
|
||||||
|
// 当修改到颜色变量时,更新 css 变量
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (root) {
|
||||||
|
const themeMode = preferences?.app?.themeMode;
|
||||||
|
if (!themeMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dark = isDarkTheme(themeMode);
|
||||||
|
root.classList.toggle('dark', dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFlatPreferences() {
|
||||||
|
return this.flattenedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInitialPreferences() {
|
||||||
|
return this.initialPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPreferences() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 覆盖偏好设置
|
||||||
|
* @param overrides - 要覆盖的偏好设置
|
||||||
|
* @param namespace - 命名空间
|
||||||
|
*/
|
||||||
|
public async initPreferences({ namespace, overrides }: initialOptions) {
|
||||||
|
// 是否初始化过
|
||||||
|
if (this.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 初始化存储管理器
|
||||||
|
this.cache = new StorageManager({ prefix: namespace });
|
||||||
|
// 合并初始偏好设置
|
||||||
|
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
||||||
|
|
||||||
|
// 加载并合并当前存储的偏好设置
|
||||||
|
const mergedPreference = merge({}, this.loadPreferences(), overrides);
|
||||||
|
|
||||||
|
// 更新偏好设置
|
||||||
|
this.updatePreferences(mergedPreference);
|
||||||
|
|
||||||
|
this.setupWatcher();
|
||||||
|
// 标记为已初始化
|
||||||
|
this.isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置偏好设置
|
||||||
|
* 偏好设置将被重置为初始值,并从 localStorage 中移除。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 假设 initialPreferences 为 { theme: 'light', language: 'en' }
|
||||||
|
* 当前 state 为 { theme: 'dark', language: 'fr' }
|
||||||
|
* this.resetPreferences();
|
||||||
|
* 调用后,state 将被重置为 { theme: 'light', language: 'en' }
|
||||||
|
* 并且 localStorage 中的对应项将被移除
|
||||||
|
*/
|
||||||
|
resetPreferences() {
|
||||||
|
// 将状态重置为初始偏好设置
|
||||||
|
Object.assign(this.state, this.initialPreferences);
|
||||||
|
// 保存重置后的偏好设置
|
||||||
|
this.savePreferences(this.state);
|
||||||
|
// 从存储中移除偏好设置项
|
||||||
|
this.cache?.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新偏好设置
|
||||||
|
* @param updates - 要更新的偏好设置
|
||||||
|
*/
|
||||||
|
public updatePreferences(updates: DeepPartial<Preferences>) {
|
||||||
|
const mergedState = merge(updates, markRaw(this.state));
|
||||||
|
|
||||||
|
Object.assign(this.state, mergedState);
|
||||||
|
Object.assign(this.flattenedState, flattenObject(this.state));
|
||||||
|
|
||||||
|
// 根据更新的键值执行相应的操作
|
||||||
|
this.handleUpdates(updates);
|
||||||
|
this.savePreferences(this.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferencesManager = new PreferenceManager();
|
||||||
|
export { isDarkTheme, preferencesManager };
|
|
@ -0,0 +1,189 @@
|
||||||
|
import type {
|
||||||
|
ContentCompactType,
|
||||||
|
LayoutHeaderModeType,
|
||||||
|
LayoutType,
|
||||||
|
LocaleSupportType,
|
||||||
|
ThemeModeType,
|
||||||
|
} from '@vben-core/typings';
|
||||||
|
|
||||||
|
type BreadcrumbStyleType = 'background' | 'normal';
|
||||||
|
|
||||||
|
type NavigationStyleType = 'plain' | 'rounded';
|
||||||
|
|
||||||
|
type PageTransitionType = 'fade-slide';
|
||||||
|
|
||||||
|
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
|
||||||
|
|
||||||
|
interface AppPreferences {
|
||||||
|
/** 登录注册页面布局 */
|
||||||
|
authPageLayout: AuthPageLayoutType;
|
||||||
|
/** 是否开启灰色模式 */
|
||||||
|
colorGrayMode: boolean;
|
||||||
|
/** 是否开启色弱模式 */
|
||||||
|
colorWeakMode: boolean;
|
||||||
|
/** 是否开启紧凑模式 */
|
||||||
|
compact: boolean;
|
||||||
|
/** 是否开启内容紧凑模式 */
|
||||||
|
contentCompact: ContentCompactType;
|
||||||
|
/** 页脚Copyright */
|
||||||
|
copyright: string;
|
||||||
|
// /** 应用默认头像 */
|
||||||
|
defaultAvatar: string;
|
||||||
|
// /** 开启动态标题 */
|
||||||
|
dynamicTitle: boolean;
|
||||||
|
/** 是否移动端 */
|
||||||
|
isMobile: boolean;
|
||||||
|
/** 布局方式 */
|
||||||
|
layout: LayoutType;
|
||||||
|
/** 支持的语言 */
|
||||||
|
locale: LocaleSupportType;
|
||||||
|
/** 应用名 */
|
||||||
|
name: string;
|
||||||
|
/** 是否开启半深色菜单(只在theme='light'时生效) */
|
||||||
|
semiDarkMenu: boolean;
|
||||||
|
/** 是否显示偏好设置 */
|
||||||
|
showPreference: boolean;
|
||||||
|
/** 当前主题 */
|
||||||
|
themeMode: ThemeModeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreadcrumbPreferences {
|
||||||
|
/** 面包屑是否启用 */
|
||||||
|
enable: boolean;
|
||||||
|
/** 面包屑是否只有一个时隐藏 */
|
||||||
|
hideOnlyOne: boolean;
|
||||||
|
/** 面包屑首页图标是否可见 */
|
||||||
|
showHome: boolean;
|
||||||
|
/** 面包屑图标是否可见 */
|
||||||
|
showIcon: boolean;
|
||||||
|
/** 面包屑风格 */
|
||||||
|
styleType: BreadcrumbStyleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FooterPreferences {
|
||||||
|
/** 底栏是否可见 */
|
||||||
|
enable: boolean;
|
||||||
|
/** 底栏是否固定 */
|
||||||
|
fixed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeaderPreferences {
|
||||||
|
/** 顶栏是否启用 */
|
||||||
|
enable: boolean;
|
||||||
|
/** 顶栏是否隐藏,css-隐藏 */
|
||||||
|
hidden: boolean;
|
||||||
|
/** header显示模式 */
|
||||||
|
mode: LayoutHeaderModeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogoPreferences {
|
||||||
|
/** logo是否可见 */
|
||||||
|
enable: boolean;
|
||||||
|
/** logo地址 */
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavigationPreferences {
|
||||||
|
/** 导航菜单手风琴模式 */
|
||||||
|
accordion: boolean;
|
||||||
|
/** 导航菜单是否切割,只在 layout=mixed-nav 生效 */
|
||||||
|
split: boolean;
|
||||||
|
/** 导航菜单风格 */
|
||||||
|
styleType: NavigationStyleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarPreferences {
|
||||||
|
/** 侧边栏是否折叠 */
|
||||||
|
collapse: boolean;
|
||||||
|
/** 侧边栏折叠时,是否显示title */
|
||||||
|
collapseShowTitle: boolean;
|
||||||
|
/** 侧边栏是否可见 */
|
||||||
|
enable: boolean;
|
||||||
|
/** 菜单自动展开状态 */
|
||||||
|
expandOnHover: boolean;
|
||||||
|
/** 侧边栏扩展区域是否折叠 */
|
||||||
|
extraCollapse: boolean;
|
||||||
|
/** 侧边栏是否隐藏 - css */
|
||||||
|
hidden: boolean;
|
||||||
|
/** 侧边栏宽度 */
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShortcutKeyPreferences {
|
||||||
|
/** 是否启用快捷键-全局 */
|
||||||
|
enable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabbarPreferences {
|
||||||
|
/** 是否开启多标签页 */
|
||||||
|
enable: boolean;
|
||||||
|
/** 开启标签页缓存功能 */
|
||||||
|
keepAlive: boolean;
|
||||||
|
/** 是否开启多标签页图标 */
|
||||||
|
showIcon: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThemePreferences {
|
||||||
|
/** 主题色 */
|
||||||
|
colorPrimary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransitionPreferences {
|
||||||
|
/** 页面切换动画是否启用 */
|
||||||
|
enable: boolean;
|
||||||
|
/** 页面切换动画 */
|
||||||
|
name: PageTransitionType;
|
||||||
|
/** 是否开启页面加载进度动画 */
|
||||||
|
progress: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Preferences {
|
||||||
|
/** 全局配置 */
|
||||||
|
app: AppPreferences;
|
||||||
|
/** 顶栏配置 */
|
||||||
|
breadcrumb: BreadcrumbPreferences;
|
||||||
|
/** 底栏配置 */
|
||||||
|
footer: FooterPreferences;
|
||||||
|
/** 面包屑配置 */
|
||||||
|
header: HeaderPreferences;
|
||||||
|
/** logo配置 */
|
||||||
|
logo: LogoPreferences;
|
||||||
|
/** 导航配置 */
|
||||||
|
navigation: NavigationPreferences;
|
||||||
|
/** 快捷键配置 */
|
||||||
|
shortcutKeys: ShortcutKeyPreferences;
|
||||||
|
/** 侧边栏配置 */
|
||||||
|
sidebar: SidebarPreferences;
|
||||||
|
/** 标签页配置 */
|
||||||
|
tabbar: TabbarPreferences;
|
||||||
|
/** 主题配置 */
|
||||||
|
theme: ThemePreferences;
|
||||||
|
/** 动画配置 */
|
||||||
|
transition: TransitionPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreferencesKeys = keyof Preferences;
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AppPreferences,
|
||||||
|
AuthPageLayoutType,
|
||||||
|
BreadcrumbPreferences,
|
||||||
|
BreadcrumbStyleType,
|
||||||
|
ContentCompactType,
|
||||||
|
FooterPreferences,
|
||||||
|
HeaderPreferences,
|
||||||
|
LayoutHeaderModeType,
|
||||||
|
LayoutType,
|
||||||
|
LocaleSupportType,
|
||||||
|
LogoPreferences,
|
||||||
|
NavigationPreferences,
|
||||||
|
PageTransitionType,
|
||||||
|
Preferences,
|
||||||
|
PreferencesKeys,
|
||||||
|
ShortcutKeyPreferences,
|
||||||
|
SidebarPreferences,
|
||||||
|
TabbarPreferences,
|
||||||
|
ThemeModeType,
|
||||||
|
ThemePreferences,
|
||||||
|
TransitionPreferences,
|
||||||
|
};
|
|
@ -2,28 +2,26 @@ import { diff } from '@vben-core/toolkit';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import {
|
import { isDarkTheme, preferencesManager } from './preferences';
|
||||||
initialPreference,
|
|
||||||
isDarkTheme,
|
|
||||||
currentPreference as preference,
|
|
||||||
} from './preference';
|
|
||||||
|
|
||||||
function usePreference() {
|
function usePreferences() {
|
||||||
|
const preferences = preferencesManager.getPreferences();
|
||||||
|
const flatPreferences = preferencesManager.getFlatPreferences();
|
||||||
|
const initialPreferences = preferencesManager.getInitialPreferences();
|
||||||
/**
|
/**
|
||||||
* @zh_CN 计算偏好设置的变化
|
* @zh_CN 计算偏好设置的变化
|
||||||
*/
|
*/
|
||||||
const diffPreference = computed(() => {
|
const diffPreference = computed(() => {
|
||||||
return diff(initialPreference.value, preference);
|
return diff(initialPreferences, preferences);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 判断是否为暗黑模式
|
* @zh_CN 判断是否为暗黑模式
|
||||||
* @param preference - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。
|
* @param preferences - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。
|
||||||
* @returns 如果主题为暗黑模式,返回 true,否则返回 false。
|
* @returns 如果主题为暗黑模式,返回 true,否则返回 false。
|
||||||
*/
|
*/
|
||||||
const isDark = computed(() => {
|
const isDark = computed(() => {
|
||||||
const theme = preference.theme;
|
return isDarkTheme(flatPreferences.appThemeMode);
|
||||||
return isDarkTheme(theme);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = computed(() => {
|
const theme = computed(() => {
|
||||||
|
@ -34,33 +32,39 @@ function usePreference() {
|
||||||
* @zh_CN 布局方式
|
* @zh_CN 布局方式
|
||||||
*/
|
*/
|
||||||
const layout = computed(() =>
|
const layout = computed(() =>
|
||||||
preference.isMobile ? 'side-nav' : preference.layout,
|
flatPreferences.appIsMobile ? 'side-nav' : flatPreferences.appLayout,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否全屏显示content,不需要侧边、底部、顶部、tab区域
|
* @zh_CN 是否全屏显示content,不需要侧边、底部、顶部、tab区域
|
||||||
*/
|
*/
|
||||||
const isFullContent = computed(() => preference.layout === 'full-content');
|
const isFullContent = computed(
|
||||||
|
() => flatPreferences.appLayout === 'full-content',
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否侧边导航模式
|
* @zh_CN 是否侧边导航模式
|
||||||
*/
|
*/
|
||||||
const isSideNav = computed(() => preference.layout === 'side-nav');
|
const isSideNav = computed(() => flatPreferences.appLayout === 'side-nav');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否侧边混合模式
|
* @zh_CN 是否侧边混合模式
|
||||||
*/
|
*/
|
||||||
const isSideMixedNav = computed(() => preference.layout === 'side-mixed-nav');
|
const isSideMixedNav = computed(
|
||||||
|
() => flatPreferences.appLayout === 'side-mixed-nav',
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否为头部导航模式
|
* @zh_CN 是否为头部导航模式
|
||||||
*/
|
*/
|
||||||
const isHeaderNav = computed(() => preference.layout === 'header-nav');
|
const isHeaderNav = computed(
|
||||||
|
() => flatPreferences.appLayout === 'header-nav',
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否为混合导航模式
|
* @zh_CN 是否为混合导航模式
|
||||||
*/
|
*/
|
||||||
const isMixedNav = computed(() => preference.layout === 'mixed-nav');
|
const isMixedNav = computed(() => flatPreferences.appLayout === 'mixed-nav');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 是否包含侧边导航模式
|
* @zh_CN 是否包含侧边导航模式
|
||||||
|
@ -74,28 +78,28 @@ function usePreference() {
|
||||||
* 在tabs可见以及开启keep-alive的情况下才开启
|
* 在tabs可见以及开启keep-alive的情况下才开启
|
||||||
*/
|
*/
|
||||||
const keepAlive = computed(
|
const keepAlive = computed(
|
||||||
() => preference.keepAlive && preference.tabsVisible,
|
() => flatPreferences.tabbarKeepAlive && flatPreferences.tabbarEnable,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 登录注册页面布局是否为左侧
|
* @zh_CN 登录注册页面布局是否为左侧
|
||||||
*/
|
*/
|
||||||
const authPanelLeft = computed(() => {
|
const authPanelLeft = computed(() => {
|
||||||
return preference.authPageLayout === 'panel-left';
|
return flatPreferences.appAuthPageLayout === 'panel-left';
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 登录注册页面布局是否为左侧
|
* @zh_CN 登录注册页面布局是否为左侧
|
||||||
*/
|
*/
|
||||||
const authPanelRight = computed(() => {
|
const authPanelRight = computed(() => {
|
||||||
return preference.authPageLayout === 'panel-right';
|
return flatPreferences.appAuthPageLayout === 'panel-right';
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 登录注册页面布局是否为中间
|
* @zh_CN 登录注册页面布局是否为中间
|
||||||
*/
|
*/
|
||||||
const authPanelCenter = computed(() => {
|
const authPanelCenter = computed(() => {
|
||||||
return preference.authPageLayout === 'panel-center';
|
return flatPreferences.appAuthPageLayout === 'panel-center';
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -116,4 +120,4 @@ function usePreference() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { usePreference };
|
export { usePreferences };
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@vben/stores",
|
"name": "@vben-core/stores",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
"directory": "packages/stores"
|
"directory": "packages/@vben-core/stores"
|
||||||
},
|
},
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"scripts": {
|
"scripts": {
|
|
@ -11,10 +11,10 @@ import {
|
||||||
describe('useAccessStore', () => {
|
describe('useAccessStore', () => {
|
||||||
it('app Name with test', () => {
|
it('app Name with test', () => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
// let referenceStore = usePreferenceStore();
|
// let referenceStore = usePreferencesStore();
|
||||||
|
|
||||||
// beforeEach(() => {
|
// beforeEach(() => {
|
||||||
// referenceStore = usePreferenceStore();
|
// referenceStore = usePreferencesStore();
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// expect(referenceStore.appName).toBe('vben-admin');
|
// expect(referenceStore.appName).toBe('vben-admin');
|
|
@ -1,10 +1,8 @@
|
||||||
import type { App } from 'vue';
|
|
||||||
|
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
interface SetupStoreOptions {
|
interface InitStoreOptions {
|
||||||
/**
|
/**
|
||||||
* @zh_CN 应用名,由于 @vben/stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名
|
* @zh_CN 应用名,由于 @vben-core/stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名
|
||||||
* 应用名将被用于持久化的前缀
|
* 应用名将被用于持久化的前缀
|
||||||
*/
|
*/
|
||||||
namespace: string;
|
namespace: string;
|
||||||
|
@ -12,20 +10,21 @@ interface SetupStoreOptions {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 初始化pinia
|
* @zh_CN 初始化pinia
|
||||||
* @param app vue app 实例
|
|
||||||
*/
|
*/
|
||||||
async function setupStore(app: App, options: SetupStoreOptions) {
|
async function initStore(options: InitStoreOptions) {
|
||||||
const { createPersistedState } = await import('pinia-plugin-persistedstate');
|
const { createPersistedState } = await import('pinia-plugin-persistedstate');
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const { namespace } = options;
|
const { namespace } = options;
|
||||||
pinia.use(
|
pinia.use(
|
||||||
createPersistedState({
|
createPersistedState({
|
||||||
// key $appName-$store.id
|
// key $appName-$store.id
|
||||||
key: (storeKey) => `__${namespace}-${storeKey}__`,
|
key: (storeKey) => `${namespace}-${storeKey}`,
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(pinia);
|
return pinia;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { setupStore };
|
export { initStore };
|
||||||
|
|
||||||
|
export type { InitStoreOptions };
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: ['src/index'],
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "@vben-core/helpers",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
|
"directory": "packages/@vben-core/helpers"
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm unbuild",
|
||||||
|
"stub": "pnpm unbuild --stub"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"main": "./dist/index.mjs",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"imports": {
|
||||||
|
"#*": "./src/*"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vben-core/toolkit": "workspace:*",
|
||||||
|
"@vben-core/typings": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './object';
|
|
@ -0,0 +1,245 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { flattenObject, toCamelCase, toNestedObject } from './object';
|
||||||
|
|
||||||
|
describe('toCamelCase', () => {
|
||||||
|
it('should return the key if parentKey is empty', () => {
|
||||||
|
expect(toCamelCase('child', '')).toBe('child');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should combine parentKey and key in camel case', () => {
|
||||||
|
expect(toCamelCase('child', 'parent')).toBe('parentChild');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty key and parentKey', () => {
|
||||||
|
expect(toCamelCase('', '')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle key with capital letters', () => {
|
||||||
|
expect(toCamelCase('Child', 'parent')).toBe('parentChild');
|
||||||
|
expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flattenObject', () => {
|
||||||
|
it('should flatten a nested object correctly', () => {
|
||||||
|
const nestedObject = {
|
||||||
|
language: 'en',
|
||||||
|
notifications: {
|
||||||
|
email: true,
|
||||||
|
push: {
|
||||||
|
sound: true,
|
||||||
|
vibration: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme: 'light',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
language: 'en',
|
||||||
|
notificationsEmail: true,
|
||||||
|
notificationsPushSound: true,
|
||||||
|
notificationsPushVibration: false,
|
||||||
|
theme: 'light',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty objects', () => {
|
||||||
|
const nestedObject = {};
|
||||||
|
const expected = {};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle objects with primitive values', () => {
|
||||||
|
const nestedObject = {
|
||||||
|
active: true,
|
||||||
|
age: 30,
|
||||||
|
name: 'Alice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
active: true,
|
||||||
|
age: 30,
|
||||||
|
name: 'Alice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle objects with null values', () => {
|
||||||
|
const nestedObject = {
|
||||||
|
user: {
|
||||||
|
age: null,
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
userAge: null,
|
||||||
|
userName: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested empty objects', () => {
|
||||||
|
const nestedObject = {
|
||||||
|
a: {},
|
||||||
|
b: { c: {} },
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle arrays within objects', () => {
|
||||||
|
const nestedObject = {
|
||||||
|
hobbies: ['reading', 'gaming'],
|
||||||
|
name: 'Alice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
hobbies: ['reading', 'gaming'],
|
||||||
|
name: 'Alice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
it('should flatten objects with nested arrays correctly', () => {
|
||||||
|
const nestedObject = {
|
||||||
|
person: {
|
||||||
|
hobbies: ['reading', 'gaming'],
|
||||||
|
name: 'Alice',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
personHobbies: ['reading', 'gaming'],
|
||||||
|
personName: 'Alice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle objects with undefined values', () => {
|
||||||
|
const nestedObject = {
|
||||||
|
user: {
|
||||||
|
age: undefined,
|
||||||
|
name: 'Alice',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
userAge: undefined,
|
||||||
|
userName: 'Alice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = flattenObject(nestedObject);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toNestedObject', () => {
|
||||||
|
it('should convert flat object to nested object with level 1', () => {
|
||||||
|
const flatObject = {
|
||||||
|
anotherKeyExample: 2,
|
||||||
|
commonAppName: 1,
|
||||||
|
someOtherKey: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedNestedObject = {
|
||||||
|
anotherKeyExample: 2,
|
||||||
|
commonAppName: 1,
|
||||||
|
someOtherKey: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert flat object to nested object with level 2', () => {
|
||||||
|
const flatObject = {
|
||||||
|
appAnotherKeyExample: 2,
|
||||||
|
appCommonName: 1,
|
||||||
|
appSomeOtherKey: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedNestedObject = {
|
||||||
|
app: {
|
||||||
|
anotherKeyExample: 2,
|
||||||
|
commonName: 1,
|
||||||
|
someOtherKey: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(toNestedObject(flatObject, 2)).toEqual(expectedNestedObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert flat object to nested object with level 3', () => {
|
||||||
|
const flatObject = {
|
||||||
|
appAnotherKeyExampleValue: 2,
|
||||||
|
appCommonNameKey: 1,
|
||||||
|
appSomeOtherKeyItem: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedNestedObject = {
|
||||||
|
app: {
|
||||||
|
another: {
|
||||||
|
keyExampleValue: 2,
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
nameKey: 1,
|
||||||
|
},
|
||||||
|
some: {
|
||||||
|
otherKeyItem: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(toNestedObject(flatObject, 3)).toEqual(expectedNestedObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty object', () => {
|
||||||
|
const flatObject = {};
|
||||||
|
|
||||||
|
const expectedNestedObject = {};
|
||||||
|
|
||||||
|
expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single key object', () => {
|
||||||
|
const flatObject = {
|
||||||
|
singleKey: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedNestedObject = {
|
||||||
|
singleKey: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex keys', () => {
|
||||||
|
const flatObject = {
|
||||||
|
anotherComplexKeyWithParts: 2,
|
||||||
|
complexKeyWithMultipleParts: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedNestedObject = {
|
||||||
|
anotherComplexKeyWithParts: 2,
|
||||||
|
complexKeyWithMultipleParts: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(toNestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,164 @@
|
||||||
|
import type { Flatten } from '@vben-core/typings';
|
||||||
|
|
||||||
|
import {
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
toLowerCaseFirstLetter,
|
||||||
|
} from '@vben-core/toolkit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成驼峰命名法的键名
|
||||||
|
* @param key
|
||||||
|
* @param parentKey
|
||||||
|
*/
|
||||||
|
function toCamelCase(key: string, parentKey: string): string {
|
||||||
|
if (!parentKey) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return parentKey + key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将嵌套对象扁平化
|
||||||
|
* @param obj - 需要扁平化的对象
|
||||||
|
* @param parentKey - 父键名,用于递归时拼接键名
|
||||||
|
* @param result - 存储结果的对象
|
||||||
|
* @returns 扁平化后的对象
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* const nestedObj = {
|
||||||
|
* user: {
|
||||||
|
* name: 'Alice',
|
||||||
|
* address: {
|
||||||
|
* city: 'Wonderland',
|
||||||
|
* zip: '12345'
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* items: [
|
||||||
|
* { id: 1, name: 'Item 1' },
|
||||||
|
* { id: 2, name: 'Item 2' }
|
||||||
|
* ],
|
||||||
|
* active: true
|
||||||
|
* };
|
||||||
|
* const flatObj = flattenObject(nestedObj);
|
||||||
|
* console.log(flatObj);
|
||||||
|
* 输出:
|
||||||
|
* {
|
||||||
|
* userName: 'Alice',
|
||||||
|
* userAddressCity: 'Wonderland',
|
||||||
|
* userAddressZip: '12345',
|
||||||
|
* items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ],
|
||||||
|
* active: true
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
function flattenObject<T extends Record<string, any>>(
|
||||||
|
obj: T,
|
||||||
|
parentKey: string = '',
|
||||||
|
result: Record<string, any> = {},
|
||||||
|
): Flatten<T> {
|
||||||
|
Object.keys(obj).forEach((key) => {
|
||||||
|
const newKey = parentKey
|
||||||
|
? `${parentKey}${capitalizeFirstLetter(key)}`
|
||||||
|
: key;
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
flattenObject(value, newKey, result);
|
||||||
|
} else {
|
||||||
|
result[newKey] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result as Flatten<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将扁平对象转换为嵌套对象。
|
||||||
|
*
|
||||||
|
* @template T - 输入对象值的类型
|
||||||
|
* @param {Record<string, T>} obj - 要转换的扁平对象
|
||||||
|
* @param {number} level - 嵌套的层级
|
||||||
|
* @returns {T} 嵌套对象
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 将扁平对象转换为嵌套对象,嵌套层级为 1
|
||||||
|
* const flatObject = {
|
||||||
|
* 'commonAppName': 1,
|
||||||
|
* 'anotherKeyExample': 2,
|
||||||
|
* 'someOtherKey': 3
|
||||||
|
* };
|
||||||
|
* const nestedObject = toNestedObject(flatObject, 1);
|
||||||
|
* console.log(nestedObject);
|
||||||
|
* 输出:
|
||||||
|
* {
|
||||||
|
* commonAppName: 1,
|
||||||
|
* anotherKeyExample: 2,
|
||||||
|
* someOtherKey: 3
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 将扁平对象转换为嵌套对象,嵌套层级为 2
|
||||||
|
* const flatObject = {
|
||||||
|
* 'appCommonName': 1,
|
||||||
|
* 'appAnotherKeyExample': 2,
|
||||||
|
* 'appSomeOtherKey': 3
|
||||||
|
* };
|
||||||
|
* const nestedObject = toNestedObject(flatObject, 2);
|
||||||
|
* console.log(nestedObject);
|
||||||
|
* 输出:
|
||||||
|
* {
|
||||||
|
* app: {
|
||||||
|
* commonName: 1,
|
||||||
|
* anotherKeyExample: 2,
|
||||||
|
* someOtherKey: 3
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
function toNestedObject<T>(obj: Record<string, T>, level: number): T {
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
const keys = key.split(/(?=[A-Z])/);
|
||||||
|
// 将驼峰式分割为数组;
|
||||||
|
let current = result;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const lowerKey = keys[i].toLowerCase();
|
||||||
|
if (i === level - 1) {
|
||||||
|
const remainingKeys = keys.slice(i).join(''); // 保留后续部分作为键的一部分
|
||||||
|
current[toLowerCaseFirstLetter(remainingKeys)] = obj[key];
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
current[lowerKey] = current[lowerKey] || {};
|
||||||
|
current = current[lowerKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { flattenObject, toCamelCase, toNestedObject };
|
||||||
|
|
||||||
|
// 定义递归类型,用于推断扁平化后的对象类型
|
||||||
|
// 限制递归深度的辅助类型
|
||||||
|
// type FlattenDepth<
|
||||||
|
// T,
|
||||||
|
// Depth extends number,
|
||||||
|
// CurrentDepth extends number[] = [],
|
||||||
|
// > = {
|
||||||
|
// [K in keyof T as CurrentDepth['length'] extends Depth
|
||||||
|
// ? K
|
||||||
|
// : T[K] extends object
|
||||||
|
// ? `${CurrentDepth['length'] extends 0 ? Uncapitalize<K & string> : Capitalize<K & string>}${keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]> extends string ? Capitalize<keyof FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>> : ''}`
|
||||||
|
// : `${CurrentDepth['length'] extends 0 ? Uncapitalize<K & string> : Capitalize<K & string>}`]: CurrentDepth['length'] extends Depth
|
||||||
|
// ? T[K]
|
||||||
|
// : T[K] extends object
|
||||||
|
// ? FlattenDepth<T[K], Depth, [...CurrentDepth, 1]>[keyof FlattenDepth<
|
||||||
|
// T[K],
|
||||||
|
// Depth,
|
||||||
|
// [...CurrentDepth, 1]
|
||||||
|
// >]
|
||||||
|
// : T[K];
|
||||||
|
// };
|
||||||
|
|
||||||
|
// type Flatten<T, Depth extends number = 4> = FlattenDepth<T, Depth>;
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/library.json",
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
export * from './storage-cache';
|
export * from './storage-manager';
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { StorageCache } from './storage-cache';
|
|
||||||
|
|
||||||
describe('storageCache', () => {
|
|
||||||
let localStorageCache: StorageCache;
|
|
||||||
let sessionStorageCache: StorageCache;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
localStorageCache = new StorageCache('prefix_', 'localStorage');
|
|
||||||
sessionStorageCache = new StorageCache('prefix_', 'sessionStorage');
|
|
||||||
localStorage.clear();
|
|
||||||
sessionStorage.clear();
|
|
||||||
vi.useFakeTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set and get an item with prefix in localStorage', () => {
|
|
||||||
localStorageCache.setItem('testKey', 'testValue');
|
|
||||||
const value = localStorageCache.getItem<string>('testKey');
|
|
||||||
expect(value).toBe('testValue');
|
|
||||||
expect(localStorage.getItem('prefix_testKey')).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set and get an item with prefix in sessionStorage', () => {
|
|
||||||
sessionStorageCache.setItem('testKey', 'testValue');
|
|
||||||
const value = sessionStorageCache.getItem<string>('testKey');
|
|
||||||
expect(value).toBe('testValue');
|
|
||||||
expect(sessionStorage.getItem('prefix_testKey')).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null for expired item in localStorage', () => {
|
|
||||||
localStorageCache.setItem('testKey', 'testValue', 1 / 60); // 1 second expiry
|
|
||||||
vi.advanceTimersByTime(2000); // Fast-forward 2 seconds
|
|
||||||
const value = localStorageCache.getItem<string>('testKey');
|
|
||||||
expect(value).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null for expired item in sessionStorage', () => {
|
|
||||||
sessionStorageCache.setItem('testKey', 'testValue', 1 / 60); // 1 second expiry
|
|
||||||
vi.advanceTimersByTime(2000); // Fast-forward 2 seconds
|
|
||||||
const value = sessionStorageCache.getItem<string>('testKey');
|
|
||||||
expect(value).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove an item with prefix in localStorage', () => {
|
|
||||||
localStorageCache.setItem('testKey', 'testValue');
|
|
||||||
localStorageCache.removeItem('testKey');
|
|
||||||
const value = localStorageCache.getItem<string>('testKey');
|
|
||||||
expect(value).toBeNull();
|
|
||||||
expect(localStorage.getItem('prefix_testKey')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove an item with prefix in sessionStorage', () => {
|
|
||||||
sessionStorageCache.setItem('testKey', 'testValue');
|
|
||||||
sessionStorageCache.removeItem('testKey');
|
|
||||||
const value = sessionStorageCache.getItem<string>('testKey');
|
|
||||||
expect(value).toBeNull();
|
|
||||||
expect(sessionStorage.getItem('prefix_testKey')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear all items in localStorage', () => {
|
|
||||||
localStorageCache.setItem('testKey1', 'testValue1');
|
|
||||||
localStorageCache.setItem('testKey2', 'testValue2');
|
|
||||||
localStorageCache.clear();
|
|
||||||
expect(localStorageCache.length()).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear all items in sessionStorage', () => {
|
|
||||||
sessionStorageCache.setItem('testKey1', 'testValue1');
|
|
||||||
sessionStorageCache.setItem('testKey2', 'testValue2');
|
|
||||||
sessionStorageCache.clear();
|
|
||||||
expect(sessionStorageCache.length()).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct length in localStorage', () => {
|
|
||||||
localStorageCache.setItem('testKey1', 'testValue1');
|
|
||||||
localStorageCache.setItem('testKey2', 'testValue2');
|
|
||||||
expect(localStorageCache.length()).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct length in sessionStorage', () => {
|
|
||||||
sessionStorageCache.setItem('testKey1', 'testValue1');
|
|
||||||
sessionStorageCache.setItem('testKey2', 'testValue2');
|
|
||||||
expect(sessionStorageCache.length()).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct key by index in localStorage', () => {
|
|
||||||
localStorageCache.setItem('testKey1', 'testValue1');
|
|
||||||
localStorageCache.setItem('testKey2', 'testValue2');
|
|
||||||
expect(localStorageCache.key(0)).toBe('prefix_testKey1');
|
|
||||||
expect(localStorageCache.key(1)).toBe('prefix_testKey2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct key by index in sessionStorage', () => {
|
|
||||||
sessionStorageCache.setItem('testKey1', 'testValue1');
|
|
||||||
sessionStorageCache.setItem('testKey2', 'testValue2');
|
|
||||||
expect(sessionStorageCache.key(0)).toBe('prefix_testKey1');
|
|
||||||
expect(sessionStorageCache.key(1)).toBe('prefix_testKey2');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,145 +0,0 @@
|
||||||
import type { IStorageCache, StorageType, StorageValue } from './types';
|
|
||||||
|
|
||||||
class StorageCache implements IStorageCache {
|
|
||||||
protected prefix: string;
|
|
||||||
protected storage: Storage;
|
|
||||||
|
|
||||||
constructor(prefix: string = '', storageType: StorageType = 'localStorage') {
|
|
||||||
this.prefix = prefix;
|
|
||||||
this.storage =
|
|
||||||
storageType === 'localStorage' ? localStorage : sessionStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取带前缀的键名
|
|
||||||
private getFullKey(key: string): string {
|
|
||||||
return this.prefix + key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取项之后的钩子方法
|
|
||||||
protected afterGetItem<T>(_key: string, _value: T | null): void {}
|
|
||||||
|
|
||||||
// 设置项之后的钩子方法
|
|
||||||
protected afterSetItem<T>(
|
|
||||||
_key: string,
|
|
||||||
_value: T,
|
|
||||||
_expiryInMinutes?: number,
|
|
||||||
): void {}
|
|
||||||
|
|
||||||
// 获取项之前的钩子方法
|
|
||||||
protected beforeGetItem(_key: string): void {}
|
|
||||||
|
|
||||||
// 设置项之前的钩子方法
|
|
||||||
protected beforeSetItem<T>(
|
|
||||||
_key: string,
|
|
||||||
_value: T,
|
|
||||||
_expiryInMinutes?: number,
|
|
||||||
): void {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空存储
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
try {
|
|
||||||
this.storage.clear();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error clearing storage', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取存储项
|
|
||||||
* @param key 存储键
|
|
||||||
* @returns 存储值或 null
|
|
||||||
*/
|
|
||||||
getItem<T>(key: string): T | null {
|
|
||||||
const fullKey = this.getFullKey(key);
|
|
||||||
this.beforeGetItem(fullKey);
|
|
||||||
|
|
||||||
let value: T | null = null;
|
|
||||||
try {
|
|
||||||
const item = this.storage.getItem(fullKey);
|
|
||||||
if (item) {
|
|
||||||
const storageValue: StorageValue<T> = JSON.parse(item);
|
|
||||||
if (storageValue.expiry && storageValue.expiry < Date.now()) {
|
|
||||||
this.storage.removeItem(fullKey);
|
|
||||||
} else {
|
|
||||||
value = storageValue.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting item from storage', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.afterGetItem(fullKey, value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取存储中的键
|
|
||||||
* @param index 键的索引
|
|
||||||
* @returns 存储键或 null
|
|
||||||
*/
|
|
||||||
key(index: number): null | string {
|
|
||||||
try {
|
|
||||||
return this.storage.key(index);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting key from storage', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取存储项的数量
|
|
||||||
* @returns 存储项的数量
|
|
||||||
*/
|
|
||||||
length(): number {
|
|
||||||
try {
|
|
||||||
return this.storage.length;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting storage length', error);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除存储项
|
|
||||||
* @param key 存储键
|
|
||||||
*/
|
|
||||||
removeItem(key: string): void {
|
|
||||||
const fullKey = this.getFullKey(key);
|
|
||||||
try {
|
|
||||||
this.storage.removeItem(fullKey);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error removing item from storage', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置存储项
|
|
||||||
* @param key 存储键
|
|
||||||
* @param value 存储值
|
|
||||||
* @param expiryInMinutes 过期时间(分钟)
|
|
||||||
*/
|
|
||||||
setItem<T>(key: string, value: T, expiryInMinutes?: number): void {
|
|
||||||
const fullKey = this.getFullKey(key);
|
|
||||||
this.beforeSetItem(fullKey, value, expiryInMinutes);
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const expiry = expiryInMinutes ? now + expiryInMinutes * 60_000 : null;
|
|
||||||
|
|
||||||
const storageValue: StorageValue<T> = {
|
|
||||||
data: value,
|
|
||||||
expiry,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.storage.setItem(fullKey, JSON.stringify(storageValue));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error setting item in storage', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.afterSetItem(fullKey, value, expiryInMinutes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { StorageCache };
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { StorageManager } from './storage-manager';
|
||||||
|
|
||||||
|
describe('storageManager', () => {
|
||||||
|
let storageManager: StorageManager<{ age: number; name: string }>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
localStorage.clear();
|
||||||
|
storageManager = new StorageManager<{ age: number; name: string }>({
|
||||||
|
prefix: 'test_',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set and get an item', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default value if item does not exist', () => {
|
||||||
|
const user = storageManager.getItem('nonexistent', {
|
||||||
|
age: 0,
|
||||||
|
name: 'Default User',
|
||||||
|
});
|
||||||
|
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove an item', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.removeItem('user');
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear all items with the prefix', () => {
|
||||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||||
|
storageManager.clear();
|
||||||
|
expect(storageManager.getItem('user1')).toBeNull();
|
||||||
|
expect(storageManager.getItem('user2')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear expired items', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||||
|
vi.advanceTimersByTime(1001); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not clear non-expired items', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||||
|
vi.advanceTimersByTime(5000); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle JSON parse errors gracefully', () => {
|
||||||
|
localStorage.setItem('test_user', '{ invalid JSON }');
|
||||||
|
const user = storageManager.getItem('user', {
|
||||||
|
age: 0,
|
||||||
|
name: 'Default User',
|
||||||
|
});
|
||||||
|
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||||
|
});
|
||||||
|
it('should return null for non-existent items without default value', () => {
|
||||||
|
const user = storageManager.getItem('nonexistent');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing items', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.setItem('user', { age: 25, name: 'Jane Doe' });
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 25, name: 'Jane Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle items without expiry correctly', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove expired items when accessed', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||||
|
vi.advanceTimersByTime(1001); // 快进时间
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove non-expired items when accessed', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||||
|
vi.advanceTimersByTime(5000); // 快进时间
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple items with different expiry times', () => {
|
||||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
|
||||||
|
vi.advanceTimersByTime(1500); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user1 = storageManager.getItem('user1');
|
||||||
|
const user2 = storageManager.getItem('user2');
|
||||||
|
expect(user1).toBeNull();
|
||||||
|
expect(user2).toEqual({ age: 25, name: 'Jane Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle items with no expiry', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
vi.advanceTimersByTime(10_000); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear all items correctly', () => {
|
||||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||||
|
storageManager.clear();
|
||||||
|
const user1 = storageManager.getItem('user1');
|
||||||
|
const user2 = storageManager.getItem('user2');
|
||||||
|
expect(user1).toBeNull();
|
||||||
|
expect(user2).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,118 @@
|
||||||
|
type StorageType = 'localStorage' | 'sessionStorage';
|
||||||
|
|
||||||
|
interface StorageManagerOptions {
|
||||||
|
prefix?: string;
|
||||||
|
storageType?: StorageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StorageItem<T> {
|
||||||
|
expiry?: number;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageManager<T> {
|
||||||
|
private prefix: string;
|
||||||
|
private storage: Storage;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
prefix = '',
|
||||||
|
storageType = 'localStorage',
|
||||||
|
}: StorageManagerOptions = {}) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.storage =
|
||||||
|
storageType === 'localStorage'
|
||||||
|
? window.localStorage
|
||||||
|
: window.sessionStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整的存储键
|
||||||
|
* @param key 原始键
|
||||||
|
* @returns 带前缀的完整键
|
||||||
|
*/
|
||||||
|
private getFullKey(key: string): string {
|
||||||
|
return `${this.prefix}-${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有带前缀的存储项
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
const keysToRemove: string[] = [];
|
||||||
|
for (let i = 0; i < this.storage.length; i++) {
|
||||||
|
const key = this.storage.key(i);
|
||||||
|
if (key && key.startsWith(this.prefix)) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToRemove.forEach((key) => this.storage.removeItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有过期的存储项
|
||||||
|
*/
|
||||||
|
clearExpiredItems(): void {
|
||||||
|
for (let i = 0; i < this.storage.length; i++) {
|
||||||
|
const key = this.storage.key(i);
|
||||||
|
if (key && key.startsWith(this.prefix)) {
|
||||||
|
const shortKey = key.replace(this.prefix, '');
|
||||||
|
this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存储项
|
||||||
|
* @param key 键
|
||||||
|
* @param defaultValue 当项不存在或已过期时返回的默认值
|
||||||
|
* @returns 值,如果项已过期或解析错误则返回默认值
|
||||||
|
*/
|
||||||
|
getItem(key: string, defaultValue: T | null = null): T | null {
|
||||||
|
const fullKey = this.getFullKey(key);
|
||||||
|
const itemStr = this.storage.getItem(fullKey);
|
||||||
|
if (!itemStr) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item: StorageItem<T> = JSON.parse(itemStr);
|
||||||
|
if (item.expiry && Date.now() > item.expiry) {
|
||||||
|
this.storage.removeItem(fullKey);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return item.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing item with key "${fullKey}":`, error);
|
||||||
|
this.storage.removeItem(fullKey); // 如果解析失败,删除该项
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除存储项
|
||||||
|
* @param key 键
|
||||||
|
*/
|
||||||
|
removeItem(key: string): void {
|
||||||
|
const fullKey = this.getFullKey(key);
|
||||||
|
this.storage.removeItem(fullKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置存储项
|
||||||
|
* @param key 键
|
||||||
|
* @param value 值
|
||||||
|
* @param ttl 存活时间(毫秒)
|
||||||
|
*/
|
||||||
|
setItem(key: string, value: T, ttl?: number): void {
|
||||||
|
const fullKey = this.getFullKey(key);
|
||||||
|
const expiry = ttl ? Date.now() + ttl : undefined;
|
||||||
|
const item: StorageItem<T> = { expiry, value };
|
||||||
|
try {
|
||||||
|
this.storage.setItem(fullKey, JSON.stringify(item));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error setting item with key "${fullKey}":`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StorageManager };
|
|
@ -3,6 +3,7 @@ export * from './date';
|
||||||
export * from './diff';
|
export * from './diff';
|
||||||
export * from './hash';
|
export * from './hash';
|
||||||
export * from './inference';
|
export * from './inference';
|
||||||
|
export * from './letter';
|
||||||
export * from './merge';
|
export * from './merge';
|
||||||
export * from './namespace';
|
export * from './namespace';
|
||||||
export * from './nprogress';
|
export * from './nprogress';
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { capitalizeFirstLetter, toLowerCaseFirstLetter } from './letter';
|
||||||
|
|
||||||
|
// 编写测试用例
|
||||||
|
describe('capitalizeFirstLetter', () => {
|
||||||
|
it('should capitalize the first letter of a string', () => {
|
||||||
|
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
||||||
|
expect(capitalizeFirstLetter('world')).toBe('World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty strings', () => {
|
||||||
|
expect(capitalizeFirstLetter('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single character strings', () => {
|
||||||
|
expect(capitalizeFirstLetter('a')).toBe('A');
|
||||||
|
expect(capitalizeFirstLetter('b')).toBe('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change the case of other characters', () => {
|
||||||
|
expect(capitalizeFirstLetter('hElLo')).toBe('HElLo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toLowerCaseFirstLetter', () => {
|
||||||
|
it('should convert the first letter to lowercase', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('CommonAppName')).toBe('commonAppName');
|
||||||
|
expect(toLowerCaseFirstLetter('AnotherKeyExample')).toBe(
|
||||||
|
'anotherKeyExample',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same string if the first letter is already lowercase', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('alreadyLowerCase')).toBe('alreadyLowerCase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty strings', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single character strings', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('A')).toBe('a');
|
||||||
|
expect(toLowerCaseFirstLetter('a')).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with only one uppercase letter', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('A')).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with special characters', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('!Special')).toBe('!Special');
|
||||||
|
expect(toLowerCaseFirstLetter('123Number')).toBe('123Number');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* 将字符串的首字母大写
|
||||||
|
* @param string
|
||||||
|
*/
|
||||||
|
function capitalizeFirstLetter(string: string): string {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串的首字母转换为小写。
|
||||||
|
*
|
||||||
|
* @param str 要转换的字符串
|
||||||
|
* @returns 首字母小写的字符串
|
||||||
|
*/
|
||||||
|
function toLowerCaseFirstLetter(str: string): string {
|
||||||
|
if (!str) return str; // 如果字符串为空,直接返回
|
||||||
|
return str.charAt(0).toLowerCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { capitalizeFirstLetter, toLowerCaseFirstLetter };
|
|
@ -0,0 +1,22 @@
|
||||||
|
type LocaleSupportType = 'en-US' | 'zh-CN';
|
||||||
|
|
||||||
|
type LayoutType =
|
||||||
|
| 'full-content'
|
||||||
|
| 'header-nav'
|
||||||
|
| 'mixed-nav'
|
||||||
|
| 'side-mixed-nav'
|
||||||
|
| 'side-nav';
|
||||||
|
|
||||||
|
type ThemeModeType = 'auto' | 'dark' | 'light';
|
||||||
|
|
||||||
|
type ContentCompactType = 'compact' | 'wide';
|
||||||
|
|
||||||
|
type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ContentCompactType,
|
||||||
|
LayoutHeaderModeType,
|
||||||
|
LayoutType,
|
||||||
|
LocaleSupportType,
|
||||||
|
ThemeModeType,
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
// `Prev` 类型用于表示递归深度的递减。它是一个元组,其索引代表了递归的层数,通过索引访问可以得到减少后的层数。
|
||||||
|
// 例如,Prev[3] 等于 2,表示递归深度从 3 减少到 2。
|
||||||
|
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
|
||||||
|
|
||||||
|
// `FlattenDepth` 类型用于将一个嵌套的对象类型“展平”,同时考虑到了递归的深度。
|
||||||
|
// 它接受三个泛型参数:T(要处理的类型),Prefix(属性名前缀,默认为空字符串),Depth(递归深度,默认为3)。
|
||||||
|
// 如果当前深度(Depth)为 0,则停止递归并返回 `never`。否则,如果属性值是对象类型,则递归调用 `FlattenDepth` 并递减深度。
|
||||||
|
// 对于非对象类型的属性,将其直接映射到结果类型中,并根据前缀构造属性名。
|
||||||
|
|
||||||
|
type FlattenDepth<T, Prefix extends string = '', Depth extends number = 4> = {
|
||||||
|
[K in keyof T]: T[K] extends object
|
||||||
|
? Depth extends 0
|
||||||
|
? never
|
||||||
|
: FlattenDepth<
|
||||||
|
T[K],
|
||||||
|
`${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`,
|
||||||
|
Prev[Depth]
|
||||||
|
>
|
||||||
|
: {
|
||||||
|
[P in `${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`]: T[K];
|
||||||
|
};
|
||||||
|
}[keyof T] extends infer O
|
||||||
|
? { [P in keyof O]: O[P] }
|
||||||
|
: never;
|
||||||
|
|
||||||
|
// `UnionToIntersection` 类型用于将一个联合类型转换为交叉类型。
|
||||||
|
// 这个类型通过条件类型和类型推断的方式来实现。它先尝试将输入类型(U)映射为一个函数类型,
|
||||||
|
// 然后通过推断这个函数类型的返回类型(infer I),最终得到一个交叉类型。
|
||||||
|
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||||
|
k: infer I,
|
||||||
|
) => void
|
||||||
|
? I
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type Flatten<T> = UnionToIntersection<FlattenDepth<T>>;
|
||||||
|
|
||||||
|
type FlattenObject<T> = FlattenDepth<T>;
|
||||||
|
type FlattenObjectKeys<T> = keyof FlattenObject<T>;
|
||||||
|
|
||||||
|
export type { Flatten, FlattenObject, FlattenObjectKeys, UnionToIntersection };
|
|
@ -1,5 +1,6 @@
|
||||||
export type * from './access';
|
export type * from './access';
|
||||||
|
export type * from './app';
|
||||||
|
export type * from './flatten';
|
||||||
export type * from './menu-record';
|
export type * from './menu-record';
|
||||||
export type * from './preference';
|
|
||||||
export type * from './tabs';
|
export type * from './tabs';
|
||||||
export type * from './tools';
|
export type * from './tools';
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
type LayoutType =
|
|
||||||
| 'full-content'
|
|
||||||
| 'header-nav'
|
|
||||||
| 'mixed-nav'
|
|
||||||
| 'side-mixed-nav'
|
|
||||||
| 'side-nav';
|
|
||||||
|
|
||||||
type BreadcrumbStyle = 'background' | 'normal';
|
|
||||||
|
|
||||||
type NavigationStyle = 'plain' | 'rounded';
|
|
||||||
|
|
||||||
type ThemeType = 'auto' | 'dark' | 'light';
|
|
||||||
|
|
||||||
type ContentCompactType = 'compact' | 'wide';
|
|
||||||
|
|
||||||
type LayoutHeaderMode = 'auto' | 'auto-scroll' | 'fixed' | 'static';
|
|
||||||
|
|
||||||
type PageTransitionType = 'fade-slide';
|
|
||||||
|
|
||||||
type AuthPageLayout = 'panel-center' | 'panel-left' | 'panel-right';
|
|
||||||
|
|
||||||
type SupportLocale = 'en-US' | 'zh-CN';
|
|
||||||
|
|
||||||
interface Language {
|
|
||||||
key: SupportLocale;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Preference {
|
|
||||||
/** 应用名 */
|
|
||||||
appName: string;
|
|
||||||
/** 登录注册页面布局 */
|
|
||||||
authPageLayout: AuthPageLayout;
|
|
||||||
/** 面包屑是否只有一个时隐藏 */
|
|
||||||
breadcrumbHideOnlyOne: boolean;
|
|
||||||
/** 面包屑首页图标是否可见 */
|
|
||||||
breadcrumbHome: boolean;
|
|
||||||
/** 面包屑图标是否可见 */
|
|
||||||
breadcrumbIcon: boolean;
|
|
||||||
/** 面包屑类型 */
|
|
||||||
breadcrumbStyle: BreadcrumbStyle;
|
|
||||||
/** 面包屑是否可见 */
|
|
||||||
breadcrumbVisible: boolean;
|
|
||||||
/** 是否开启灰色模式 */
|
|
||||||
colorGrayMode: boolean;
|
|
||||||
/** 主题色 */
|
|
||||||
colorPrimary: string;
|
|
||||||
/** 是否开启色弱模式 */
|
|
||||||
colorWeakMode: boolean;
|
|
||||||
/** 是否开启紧凑模式 */
|
|
||||||
compact: boolean;
|
|
||||||
/** 是否开启内容紧凑模式 */
|
|
||||||
contentCompact: ContentCompactType;
|
|
||||||
/** 页脚Copyright */
|
|
||||||
copyright: string;
|
|
||||||
/** 应用默认头像 */
|
|
||||||
defaultAvatar: string;
|
|
||||||
/** 开启动态标题 */
|
|
||||||
dynamicTitle: boolean;
|
|
||||||
/** 页脚是否固定 */
|
|
||||||
footerFixed: boolean;
|
|
||||||
/** 页脚是否可见 */
|
|
||||||
footerVisible: boolean;
|
|
||||||
/** 顶栏是否隐藏 */
|
|
||||||
headerHidden: boolean;
|
|
||||||
/** header显示模式 */
|
|
||||||
headerMode: LayoutHeaderMode;
|
|
||||||
/** 顶栏是否可见 */
|
|
||||||
headerVisible: boolean;
|
|
||||||
/** 是否移动端 */
|
|
||||||
isMobile: boolean;
|
|
||||||
/** 开启标签页缓存功能 */
|
|
||||||
keepAlive: boolean;
|
|
||||||
/** 布局方式 */
|
|
||||||
layout: LayoutType;
|
|
||||||
/** 支持的语言 */
|
|
||||||
locale: SupportLocale;
|
|
||||||
/** 应用Logo */
|
|
||||||
logo: string;
|
|
||||||
/** logo是否可见 */
|
|
||||||
logoVisible: boolean;
|
|
||||||
/** 导航菜单手风琴模式 */
|
|
||||||
navigationAccordion: boolean;
|
|
||||||
/** 导航菜单是否切割,只在 layout=mixed-nav 生效 */
|
|
||||||
navigationSplit: boolean;
|
|
||||||
/** 导航菜单风格 */
|
|
||||||
navigationStyle: NavigationStyle;
|
|
||||||
/** 是否开启页面加载进度条 */
|
|
||||||
pageProgress: boolean;
|
|
||||||
/** 页面切换动画 */
|
|
||||||
pageTransition: PageTransitionType;
|
|
||||||
/** 页面切换动画是否启用 */
|
|
||||||
pageTransitionEnable: boolean;
|
|
||||||
/** 是否开启半深色菜单(只在theme='light'时生效) */
|
|
||||||
semiDarkMenu: boolean;
|
|
||||||
/** 是否启用快捷键 */
|
|
||||||
shortcutKeys: boolean;
|
|
||||||
/** 是否显示偏好设置 */
|
|
||||||
showPreference: boolean;
|
|
||||||
/** 侧边栏是否折叠 */
|
|
||||||
sideCollapse: boolean;
|
|
||||||
/** 侧边栏折叠时,是否显示title */
|
|
||||||
sideCollapseShowTitle: boolean;
|
|
||||||
/** 菜单自动展开状态 */
|
|
||||||
sideExpandOnHover: boolean;
|
|
||||||
/** 侧边栏扩展区域是否折叠 */
|
|
||||||
sideExtraCollapse: boolean;
|
|
||||||
/** 侧边栏是否隐藏 */
|
|
||||||
sideHidden: boolean;
|
|
||||||
/** 侧边栏是否可见 */
|
|
||||||
sideVisible: boolean;
|
|
||||||
/** 侧边栏宽度 */
|
|
||||||
sideWidth: number;
|
|
||||||
/** 是否开启多标签页图标 */
|
|
||||||
tabsIcon: boolean;
|
|
||||||
/** 是否开启多标签页 */
|
|
||||||
tabsVisible: boolean;
|
|
||||||
/** 当前主题 */
|
|
||||||
theme: ThemeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这些属性是静态的,不会随着用户的操作而改变
|
|
||||||
interface StaticPreference {
|
|
||||||
/** 主题色预设 */
|
|
||||||
colorPrimaryPresets: string[];
|
|
||||||
/** 支持的语言 */
|
|
||||||
supportLanguages: Language[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type PreferenceKeys = keyof Preference;
|
|
||||||
|
|
||||||
export type {
|
|
||||||
AuthPageLayout,
|
|
||||||
BreadcrumbStyle,
|
|
||||||
ContentCompactType,
|
|
||||||
LayoutHeaderMode,
|
|
||||||
LayoutType,
|
|
||||||
PageTransitionType,
|
|
||||||
Preference,
|
|
||||||
PreferenceKeys,
|
|
||||||
StaticPreference,
|
|
||||||
SupportLocale,
|
|
||||||
ThemeType,
|
|
||||||
};
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type {
|
import type {
|
||||||
ContentCompactType,
|
ContentCompactType,
|
||||||
LayoutHeaderMode,
|
LayoutHeaderModeType,
|
||||||
LayoutType,
|
LayoutType,
|
||||||
ThemeType,
|
ThemeModeType,
|
||||||
} from '@vben-core/typings';
|
} from '@vben-core/typings';
|
||||||
|
|
||||||
interface VbenLayoutProps {
|
interface VbenLayoutProps {
|
||||||
|
@ -86,7 +86,7 @@ interface VbenLayoutProps {
|
||||||
* header 显示模式
|
* header 显示模式
|
||||||
* @default 'fixed'
|
* @default 'fixed'
|
||||||
*/
|
*/
|
||||||
headerMode?: LayoutHeaderMode;
|
headerMode?: LayoutHeaderModeType;
|
||||||
/**
|
/**
|
||||||
* header是否显示
|
* header是否显示
|
||||||
* @default true
|
* @default true
|
||||||
|
@ -146,7 +146,7 @@ interface VbenLayoutProps {
|
||||||
* 侧边栏
|
* 侧边栏
|
||||||
* @default dark
|
* @default dark
|
||||||
*/
|
*/
|
||||||
sideTheme?: ThemeType;
|
sideTheme?: ThemeModeType;
|
||||||
/**
|
/**
|
||||||
* 侧边栏是否可见
|
* 侧边栏是否可见
|
||||||
* @default true
|
* @default true
|
||||||
|
|
|
@ -460,7 +460,7 @@ function handleOpenMenu() {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative flex min-h-full w-full">
|
<div class="relative flex min-h-full w-full">
|
||||||
<slot name="preference"></slot>
|
<slot name="preferences"></slot>
|
||||||
<slot name="floating-button-group"></slot>
|
<slot name="floating-button-group"></slot>
|
||||||
<LayoutSide
|
<LayoutSide
|
||||||
v-if="sideVisibleState"
|
v-if="sideVisibleState"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { MenuRecordBadgeRaw, ThemeType } from '@vben-core/typings';
|
import type { MenuRecordBadgeRaw, ThemeModeType } from '@vben-core/typings';
|
||||||
|
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ interface MenuProps {
|
||||||
* @zh_CN 菜单主题
|
* @zh_CN 菜单主题
|
||||||
* @default dark
|
* @default dark
|
||||||
*/
|
*/
|
||||||
theme?: ThemeType;
|
theme?: ThemeModeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubMenuProps extends MenuRecordBadgeRaw {
|
interface SubMenuProps extends MenuRecordBadgeRaw {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
# packages
|
|
@ -46,10 +46,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben-core/design": "workspace:*",
|
"@vben-core/design": "workspace:*",
|
||||||
"@vben-core/iconify": "workspace:*",
|
"@vben-core/iconify": "workspace:*",
|
||||||
|
"@vben-core/preferences": "workspace:*",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben-core/toolkit": "workspace:*",
|
"@vben-core/toolkit": "workspace:*",
|
||||||
"@vben/locales": "workspace:*",
|
"@vben/locales": "workspace:*",
|
||||||
"@vben/preference": "workspace:*",
|
|
||||||
"@vueuse/core": "^10.10.0",
|
"@vueuse/core": "^10.10.0",
|
||||||
"@vueuse/integrations": "^10.10.0",
|
"@vueuse/integrations": "^10.10.0",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IcRoundColorLens } from '@vben-core/iconify';
|
import { IcRoundColorLens } from '@vben-core/iconify';
|
||||||
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
preference,
|
COLOR_PRIMARY_RESETS,
|
||||||
staticPreference,
|
preferences,
|
||||||
updatePreference,
|
updatePreferences,
|
||||||
} from '@vben/preference';
|
} from '@vben-core/preferences';
|
||||||
|
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AuthenticationColorToggle',
|
name: 'AuthenticationColorToggle',
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleUpdate(value: string) {
|
function handleUpdate(value: string) {
|
||||||
updatePreference({
|
updatePreferences({
|
||||||
|
theme: {
|
||||||
colorPrimary: value,
|
colorPrimary: value,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -24,10 +25,7 @@ function handleUpdate(value: string) {
|
||||||
<div
|
<div
|
||||||
class="ease-ou flex w-0 overflow-hidden transition-all duration-500 group-hover:w-48"
|
class="ease-ou flex w-0 overflow-hidden transition-all duration-500 group-hover:w-48"
|
||||||
>
|
>
|
||||||
<template
|
<template v-for="color in COLOR_PRIMARY_RESETS" :key="color">
|
||||||
v-for="color in staticPreference.colorPrimaryPresets"
|
|
||||||
:key="color"
|
|
||||||
>
|
|
||||||
<VbenIconButton
|
<VbenIconButton
|
||||||
class="flex-center flex-shrink-0"
|
class="flex-center flex-shrink-0"
|
||||||
@click="handleUpdate(color)"
|
@click="handleUpdate(color)"
|
||||||
|
@ -35,7 +33,9 @@ function handleUpdate(value: string) {
|
||||||
<div
|
<div
|
||||||
class="relative h-3.5 w-3.5 rounded-[2px] before:absolute before:left-0.5 before:top-0.5 before:h-2.5 before:w-2.5 before:rounded-[2px] before:border before:border-gray-900 before:opacity-0 before:transition-all before:duration-150 before:content-[''] hover:scale-110"
|
class="relative h-3.5 w-3.5 rounded-[2px] before:absolute before:left-0.5 before:top-0.5 before:h-2.5 before:w-2.5 before:rounded-[2px] before:border before:border-gray-900 before:opacity-0 before:transition-all before:duration-150 before:content-[''] hover:scale-110"
|
||||||
:class="[
|
:class="[
|
||||||
preference.colorPrimary === color ? `before:opacity-100` : '',
|
preferences.theme.colorPrimary === color
|
||||||
|
? `before:opacity-100`
|
||||||
|
: '',
|
||||||
]"
|
]"
|
||||||
:style="{ backgroundColor: color }"
|
:style="{ backgroundColor: color }"
|
||||||
></div>
|
></div>
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AuthPageLayout } from '@vben/types';
|
|
||||||
import type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';
|
import type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { MdiDockBottom, MdiDockLeft, MdiDockRight } from '@vben-core/iconify';
|
import { MdiDockBottom, MdiDockLeft, MdiDockRight } from '@vben-core/iconify';
|
||||||
|
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||||
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
|
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { preference, updatePreference, usePreference } from '@vben/preference';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AuthenticationLayoutToggle',
|
name: 'AuthenticationLayoutToggle',
|
||||||
// inheritAttrs: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const menus = computed((): VbenDropdownMenuItem[] => [
|
const menus = computed((): VbenDropdownMenuItem[] => [
|
||||||
|
@ -32,20 +30,13 @@ const menus = computed((): VbenDropdownMenuItem[] => [
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function handleUpdate(value: string) {
|
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();
|
||||||
updatePreference({
|
|
||||||
authPageLayout: value as AuthPageLayout,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreference();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VbenDropdownRadioMenu
|
<VbenDropdownRadioMenu
|
||||||
|
v-model="preferences.app.authPageLayout"
|
||||||
:menus="menus"
|
:menus="menus"
|
||||||
:model-value="preference.authPageLayout"
|
|
||||||
@update:model-value="handleUpdate"
|
|
||||||
>
|
>
|
||||||
<VbenIconButton>
|
<VbenIconButton>
|
||||||
<MdiDockRight v-if="authPanelRight" class="size-5" />
|
<MdiDockRight v-if="authPanelRight" class="size-5" />
|
||||||
|
|
|
@ -4,7 +4,7 @@ export * from './global-provider';
|
||||||
export * from './global-search';
|
export * from './global-search';
|
||||||
export * from './language-toggle';
|
export * from './language-toggle';
|
||||||
export * from './notification';
|
export * from './notification';
|
||||||
export * from './preference';
|
export * from './preferences';
|
||||||
export * from './spinner';
|
export * from './spinner';
|
||||||
export * from './theme-toggle';
|
export * from './theme-toggle';
|
||||||
export * from './user-dropdown';
|
export * from './user-dropdown';
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SupportLocale } from '@vben/types';
|
import type { LocaleSupportType } from '@vben/types';
|
||||||
|
|
||||||
import { IcBaselineLanguage } from '@vben-core/iconify';
|
import { IcBaselineLanguage } from '@vben-core/iconify';
|
||||||
|
import {
|
||||||
|
SUPPORT_LANGUAGES,
|
||||||
|
preferences,
|
||||||
|
updatePreferences,
|
||||||
|
} from '@vben-core/preferences';
|
||||||
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
|
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { loadLocaleMessages } from '@vben/locales';
|
import { loadLocaleMessages } from '@vben/locales';
|
||||||
import {
|
|
||||||
preference,
|
|
||||||
staticPreference,
|
|
||||||
updatePreference,
|
|
||||||
} from '@vben/preference';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LanguageToggle',
|
name: 'LanguageToggle',
|
||||||
});
|
});
|
||||||
|
|
||||||
const menus = staticPreference.supportLanguages;
|
const menus = SUPPORT_LANGUAGES;
|
||||||
|
|
||||||
async function handleUpdate(value: string) {
|
async function handleUpdate(value: string) {
|
||||||
const locale = value as SupportLocale;
|
const locale = value as LocaleSupportType;
|
||||||
updatePreference({
|
updatePreferences({
|
||||||
|
app: {
|
||||||
locale,
|
locale,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
// 更改预览
|
// 更改预览
|
||||||
await loadLocaleMessages(locale);
|
await loadLocaleMessages(locale);
|
||||||
|
@ -31,7 +33,7 @@ async function handleUpdate(value: string) {
|
||||||
<div>
|
<div>
|
||||||
<VbenDropdownRadioMenu
|
<VbenDropdownRadioMenu
|
||||||
:menus="menus"
|
:menus="menus"
|
||||||
:model-value="preference.locale"
|
:model-value="preferences.app.locale"
|
||||||
@update:model-value="handleUpdate"
|
@update:model-value="handleUpdate"
|
||||||
>
|
>
|
||||||
<VbenIconButton>
|
<VbenIconButton>
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as PreferenceWidget } from './preference-widget.vue';
|
|
|
@ -1,102 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { PreferenceKeys, SupportLocale } from '@vben/types';
|
|
||||||
|
|
||||||
import { loadLocaleMessages } from '@vben/locales';
|
|
||||||
import {
|
|
||||||
preference,
|
|
||||||
staticPreference,
|
|
||||||
updatePreference,
|
|
||||||
} from '@vben/preference';
|
|
||||||
|
|
||||||
import Preference from './preference.vue';
|
|
||||||
|
|
||||||
function handleUpdate(key: PreferenceKeys, value: boolean | string) {
|
|
||||||
updatePreference({
|
|
||||||
[key]: value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLocale(value: string) {
|
|
||||||
const locale = value as SupportLocale;
|
|
||||||
updatePreference({
|
|
||||||
locale,
|
|
||||||
});
|
|
||||||
// 更改预览
|
|
||||||
loadLocaleMessages(locale);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<Preference
|
|
||||||
:color-primary-presets="staticPreference.colorPrimaryPresets"
|
|
||||||
:breadcrumb-visible="preference.breadcrumbVisible"
|
|
||||||
:breadcrumb-style="preference.breadcrumbStyle"
|
|
||||||
:color-gray-mode="preference.colorGrayMode"
|
|
||||||
:breadcrumb-icon="preference.breadcrumbIcon"
|
|
||||||
:color-primary="preference.colorPrimary"
|
|
||||||
:color-weak-mode="preference.colorWeakMode"
|
|
||||||
:content-compact="preference.contentCompact"
|
|
||||||
:breadcrumb-home="preference.breadcrumbHome"
|
|
||||||
:side-collapse="preference.sideCollapse"
|
|
||||||
:layout="preference.layout"
|
|
||||||
:semi-dark-menu="preference.semiDarkMenu"
|
|
||||||
:side-visible="preference.sideVisible"
|
|
||||||
:footer-visible="preference.footerVisible"
|
|
||||||
:tabs-visible="preference.tabsVisible"
|
|
||||||
:header-visible="preference.headerVisible"
|
|
||||||
:footer-fixed="preference.footerFixed"
|
|
||||||
:header-mode="preference.headerMode"
|
|
||||||
:theme="preference.theme"
|
|
||||||
:dynamic-title="preference.dynamicTitle"
|
|
||||||
:breadcrumb-hide-only-one="preference.breadcrumbHideOnlyOne"
|
|
||||||
:page-transition="preference.pageTransition"
|
|
||||||
:page-progress="preference.pageProgress"
|
|
||||||
:tabs-icon="preference.tabsIcon"
|
|
||||||
:locale="preference.locale"
|
|
||||||
:navigation-accordion="preference.navigationAccordion"
|
|
||||||
:navigation-style="preference.navigationStyle"
|
|
||||||
:shortcut-keys="preference.shortcutKeys"
|
|
||||||
:navigation-split="preference.navigationSplit"
|
|
||||||
:side-collapse-show-title="preference.sideCollapseShowTitle"
|
|
||||||
:page-transition-enable="preference.pageTransitionEnable"
|
|
||||||
@update:shortcut-keys="(value) => handleUpdate('shortcutKeys', value)"
|
|
||||||
@update:navigation-style="(value) => handleUpdate('navigationStyle', value)"
|
|
||||||
@update:navigation-accordion="
|
|
||||||
(value) => handleUpdate('navigationAccordion', value)
|
|
||||||
"
|
|
||||||
@update:navigation-split="(value) => handleUpdate('navigationSplit', value)"
|
|
||||||
@update:dynamic-title="(value) => handleUpdate('dynamicTitle', value)"
|
|
||||||
@update:tabs-icon="(value) => handleUpdate('tabsIcon', value)"
|
|
||||||
@update:side-collapse="(value) => handleUpdate('sideCollapse', value)"
|
|
||||||
@update:locale="updateLocale"
|
|
||||||
@update:header-visible="(value) => handleUpdate('headerVisible', value)"
|
|
||||||
@update:side-visible="(value) => handleUpdate('sideVisible', value)"
|
|
||||||
@update:footer-visible="(value) => handleUpdate('footerVisible', value)"
|
|
||||||
@update:tabs-visible="(value) => handleUpdate('tabsVisible', value)"
|
|
||||||
@update:header-mode="(value) => handleUpdate('headerMode', value)"
|
|
||||||
@update:footer-fixed="(value) => handleUpdate('footerFixed', value)"
|
|
||||||
@update:breadcrumb-visible="
|
|
||||||
(value) => handleUpdate('breadcrumbVisible', value)
|
|
||||||
"
|
|
||||||
@update:breadcrumb-hide-only-one="
|
|
||||||
(value) => handleUpdate('breadcrumbHideOnlyOne', value)
|
|
||||||
"
|
|
||||||
@update:side-collapse-show-title="
|
|
||||||
(value) => handleUpdate('sideCollapseShowTitle', value)
|
|
||||||
"
|
|
||||||
@update:breadcrumb-home="(value) => handleUpdate('breadcrumbHome', value)"
|
|
||||||
@update:breadcrumb-icon="(value) => handleUpdate('breadcrumbIcon', value)"
|
|
||||||
@update:breadcrumb-style="(value) => handleUpdate('breadcrumbStyle', value)"
|
|
||||||
@update:page-transition-enable="
|
|
||||||
(value) => handleUpdate('pageTransitionEnable', value)
|
|
||||||
"
|
|
||||||
@update:color-gray-mode="(value) => handleUpdate('colorGrayMode', value)"
|
|
||||||
@update:page-transition="(value) => handleUpdate('pageTransition', value)"
|
|
||||||
@update:page-progress="(value) => handleUpdate('pageProgress', value)"
|
|
||||||
@update:color-primary="(value) => handleUpdate('colorPrimary', value)"
|
|
||||||
@update:color-weak-mode="(value) => handleUpdate('colorWeakMode', value)"
|
|
||||||
@update:content-compact="(value) => handleUpdate('contentCompact', value)"
|
|
||||||
@update:layout="(value) => handleUpdate('layout', value)"
|
|
||||||
@update:semi-dark-menu="(value) => handleUpdate('semiDarkMenu', value)"
|
|
||||||
@update:theme="(value) => handleUpdate('theme', value)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const openPreference = ref(false);
|
|
||||||
|
|
||||||
function useOpenPreference() {
|
|
||||||
function handleOpenPreference() {
|
|
||||||
openPreference.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleOpenPreference,
|
|
||||||
openPreference,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useOpenPreference };
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SelectListItem } from '@vben/types';
|
import type { SelectListItem } from '@vben/types';
|
||||||
|
|
||||||
|
import { SUPPORT_LANGUAGES } from '@vben-core/preferences';
|
||||||
|
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { staticPreference } from '@vben/preference';
|
|
||||||
|
|
||||||
import SelectItem from '../select-item.vue';
|
import SelectItem from '../select-item.vue';
|
||||||
import SwitchItem from '../switch-item.vue';
|
import SwitchItem from '../switch-item.vue';
|
||||||
|
@ -15,12 +16,10 @@ const locale = defineModel<string>('locale');
|
||||||
const dynamicTitle = defineModel<boolean>('dynamicTitle');
|
const dynamicTitle = defineModel<boolean>('dynamicTitle');
|
||||||
const shortcutKeys = defineModel<boolean>('shortcutKeys');
|
const shortcutKeys = defineModel<boolean>('shortcutKeys');
|
||||||
|
|
||||||
const localeItems: SelectListItem[] = staticPreference.supportLanguages.map(
|
const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
|
||||||
(item) => ({
|
|
||||||
label: item.text,
|
label: item.text,
|
||||||
value: item.key,
|
value: item.key,
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LayoutHeaderMode, SelectListItem } from '@vben/types';
|
import type { LayoutHeaderModeType, SelectListItem } from '@vben/types';
|
||||||
|
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ defineOptions({
|
||||||
defineProps<{ disabled: boolean }>();
|
defineProps<{ disabled: boolean }>();
|
||||||
|
|
||||||
const headerVisible = defineModel<boolean>('headerVisible');
|
const headerVisible = defineModel<boolean>('headerVisible');
|
||||||
const headerMode = defineModel<LayoutHeaderMode>('headerMode');
|
const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
|
||||||
|
|
||||||
const localeItems: SelectListItem[] = [
|
const localeItems: SelectListItem[] = [
|
||||||
{
|
{
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue