139 changed files with 2205 additions and 1450 deletions
@ -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', |
|||
}, |
|||
}; |
|||
@ -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,3 +1,3 @@ |
|||
# @vben-core |
|||
|
|||
系统一些比较基础的SDK和UI组件库,请勿将任何业务逻辑和业务包放在这里。 |
|||
系统一些比较基础的SDK和UI组件库,该目录后续可能会迁移出去或者发布到npm,请勿将任何业务逻辑和业务包放在该目录。 |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
# @vben-core/forward |
|||
|
|||
该目录内的包,可直接被app所引用 |
|||
@ -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, |
|||
}; |
|||
@ -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 }; |
|||
@ -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 './app'; |
|||
export type * from './flatten'; |
|||
export type * from './menu-record'; |
|||
export type * from './preference'; |
|||
export type * from './tabs'; |
|||
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, |
|||
}; |
|||
@ -0,0 +1 @@ |
|||
# packages |
|||
@ -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 }; |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue