refactor: refacotr preference
This commit is contained in:
3
packages/@vben-core/forward/README.md
Normal file
3
packages/@vben-core/forward/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @vben-core/forward
|
||||
|
||||
该目录内的包,可直接被app所引用
|
7
packages/@vben-core/forward/preferences/build.config.ts
Normal file
7
packages/@vben-core/forward/preferences/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
42
packages/@vben-core/forward/preferences/package.json
Normal file
42
packages/@vben-core/forward/preferences/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@vben-core/preferences",
|
||||
"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/preferences"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"stub": "pnpm unbuild --stub"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/cache": "workspace:*",
|
||||
"@vben-core/helpers": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"vue": "3.4.27"
|
||||
}
|
||||
}
|
77
packages/@vben-core/forward/preferences/src/config.ts
Normal file
77
packages/@vben-core/forward/preferences/src/config.ts
Normal file
@@ -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 };
|
26
packages/@vben-core/forward/preferences/src/constants.ts
Normal file
26
packages/@vben-core/forward/preferences/src/constants.ts
Normal file
@@ -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',
|
||||
},
|
||||
];
|
32
packages/@vben-core/forward/preferences/src/index.ts
Normal file
32
packages/@vben-core/forward/preferences/src/index.ts
Normal file
@@ -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';
|
289
packages/@vben-core/forward/preferences/src/preferences.ts
Normal file
289
packages/@vben-core/forward/preferences/src/preferences.ts
Normal file
@@ -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 };
|
189
packages/@vben-core/forward/preferences/src/types.ts
Normal file
189
packages/@vben-core/forward/preferences/src/types.ts
Normal file
@@ -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,
|
||||
};
|
123
packages/@vben-core/forward/preferences/src/use-preferences.ts
Normal file
123
packages/@vben-core/forward/preferences/src/use-preferences.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { diff } from '@vben-core/toolkit';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { isDarkTheme, preferencesManager } from './preferences';
|
||||
|
||||
function usePreferences() {
|
||||
const preferences = preferencesManager.getPreferences();
|
||||
const flatPreferences = preferencesManager.getFlatPreferences();
|
||||
const initialPreferences = preferencesManager.getInitialPreferences();
|
||||
/**
|
||||
* @zh_CN 计算偏好设置的变化
|
||||
*/
|
||||
const diffPreference = computed(() => {
|
||||
return diff(initialPreferences, preferences);
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 判断是否为暗黑模式
|
||||
* @param preferences - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。
|
||||
* @returns 如果主题为暗黑模式,返回 true,否则返回 false。
|
||||
*/
|
||||
const isDark = computed(() => {
|
||||
return isDarkTheme(flatPreferences.appThemeMode);
|
||||
});
|
||||
|
||||
const theme = computed(() => {
|
||||
return isDark.value ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 布局方式
|
||||
*/
|
||||
const layout = computed(() =>
|
||||
flatPreferences.appIsMobile ? 'side-nav' : flatPreferences.appLayout,
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否全屏显示content,不需要侧边、底部、顶部、tab区域
|
||||
*/
|
||||
const isFullContent = computed(
|
||||
() => flatPreferences.appLayout === 'full-content',
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否侧边导航模式
|
||||
*/
|
||||
const isSideNav = computed(() => flatPreferences.appLayout === 'side-nav');
|
||||
|
||||
/**
|
||||
* @zh_CN 是否侧边混合模式
|
||||
*/
|
||||
const isSideMixedNav = computed(
|
||||
() => flatPreferences.appLayout === 'side-mixed-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否为头部导航模式
|
||||
*/
|
||||
const isHeaderNav = computed(
|
||||
() => flatPreferences.appLayout === 'header-nav',
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 是否为混合导航模式
|
||||
*/
|
||||
const isMixedNav = computed(() => flatPreferences.appLayout === 'mixed-nav');
|
||||
|
||||
/**
|
||||
* @zh_CN 是否包含侧边导航模式
|
||||
*/
|
||||
const isSideMode = computed(() => {
|
||||
return isMixedNav.value || isSideMixedNav.value || isSideNav.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 是否开启keep-alive
|
||||
* 在tabs可见以及开启keep-alive的情况下才开启
|
||||
*/
|
||||
const keepAlive = computed(
|
||||
() => flatPreferences.tabbarKeepAlive && flatPreferences.tabbarEnable,
|
||||
);
|
||||
|
||||
/**
|
||||
* @zh_CN 登录注册页面布局是否为左侧
|
||||
*/
|
||||
const authPanelLeft = computed(() => {
|
||||
return flatPreferences.appAuthPageLayout === 'panel-left';
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 登录注册页面布局是否为左侧
|
||||
*/
|
||||
const authPanelRight = computed(() => {
|
||||
return flatPreferences.appAuthPageLayout === 'panel-right';
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 登录注册页面布局是否为中间
|
||||
*/
|
||||
const authPanelCenter = computed(() => {
|
||||
return flatPreferences.appAuthPageLayout === 'panel-center';
|
||||
});
|
||||
|
||||
return {
|
||||
authPanelCenter,
|
||||
authPanelLeft,
|
||||
authPanelRight,
|
||||
diffPreference,
|
||||
isDark,
|
||||
isFullContent,
|
||||
isHeaderNav,
|
||||
isMixedNav,
|
||||
isSideMixedNav,
|
||||
isSideMode,
|
||||
isSideNav,
|
||||
keepAlive,
|
||||
layout,
|
||||
theme,
|
||||
};
|
||||
}
|
||||
|
||||
export { usePreferences };
|
5
packages/@vben-core/forward/preferences/tsconfig.json
Normal file
5
packages/@vben-core/forward/preferences/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src"]
|
||||
}
|
0
packages/@vben-core/forward/request/.gitkeep
Normal file
0
packages/@vben-core/forward/request/.gitkeep
Normal file
7
packages/@vben-core/forward/stores/build.config.ts
Normal file
7
packages/@vben-core/forward/stores/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
51
packages/@vben-core/forward/stores/package.json
Normal file
51
packages/@vben-core/forward/stores/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@vben-core/stores",
|
||||
"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/stores"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"stub": "pnpm unbuild --stub"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"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:*",
|
||||
"pinia": "2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"vue": "3.4.27",
|
||||
"vue-router": "^4.3.2"
|
||||
}
|
||||
}
|
9
packages/@vben-core/forward/stores/shim-pinia.d.ts
vendored
Normal file
9
packages/@vben-core/forward/stores/shim-pinia.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// TODO: https://github.com/vuejs/pinia/issues/2098
|
||||
declare module 'pinia' {
|
||||
export function acceptHMRUpdate(
|
||||
initialUseStore: StoreDefinition | any,
|
||||
hot: any,
|
||||
): (newModule: any) => any;
|
||||
}
|
||||
|
||||
export {};
|
3
packages/@vben-core/forward/stores/src/index.ts
Normal file
3
packages/@vben-core/forward/stores/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './modules';
|
||||
export * from './setup';
|
||||
export { storeToRefs } from 'pinia';
|
@@ -0,0 +1,24 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import {
|
||||
// beforeEach,
|
||||
describe,
|
||||
// expect,
|
||||
it,
|
||||
} from 'vitest';
|
||||
|
||||
// import { useAccessStore } from '../modules/access';
|
||||
|
||||
describe('useAccessStore', () => {
|
||||
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');
|
||||
});
|
||||
});
|
95
packages/@vben-core/forward/stores/src/modules/access.ts
Normal file
95
packages/@vben-core/forward/stores/src/modules/access.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { MenuRecordRaw, UserInfo } from '@vben-core/typings';
|
||||
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||
|
||||
type AccessToken = null | string;
|
||||
|
||||
interface AccessState {
|
||||
/**
|
||||
* 可访问的菜单列表
|
||||
*/
|
||||
accessMenus: MenuRecordRaw[];
|
||||
/**
|
||||
* 可访问的路由列表
|
||||
*/
|
||||
accessRoutes: RouteRecordRaw[];
|
||||
/**
|
||||
* 登录 accessToken
|
||||
*/
|
||||
accessToken: AccessToken;
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
userInfo: UserInfo | null;
|
||||
/**
|
||||
* 用户角色
|
||||
*/
|
||||
userRoles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 访问权限相关
|
||||
*/
|
||||
const useAccessStore = defineStore('access', {
|
||||
actions: {
|
||||
setAccessMenus(menus: MenuRecordRaw[]) {
|
||||
this.accessMenus = menus;
|
||||
},
|
||||
setAccessRoutes(routes: RouteRecordRaw[]) {
|
||||
this.accessRoutes = routes;
|
||||
},
|
||||
setAccessToken(token: AccessToken) {
|
||||
this.accessToken = token;
|
||||
},
|
||||
setUserInfo(userInfo: UserInfo) {
|
||||
// 设置用户信息
|
||||
this.userInfo = userInfo;
|
||||
// 设置角色信息
|
||||
const roles = userInfo?.roles ?? [];
|
||||
const roleValues = roles.map((item) => item.value);
|
||||
this.setUserRoles(roleValues);
|
||||
},
|
||||
setUserRoles(roles: string[]) {
|
||||
this.userRoles = roles;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
getAccessMenus(): MenuRecordRaw[] {
|
||||
return this.accessMenus;
|
||||
},
|
||||
getAccessRoutes(): RouteRecordRaw[] {
|
||||
return this.accessRoutes;
|
||||
},
|
||||
getAccessToken(): AccessToken {
|
||||
return this.accessToken;
|
||||
},
|
||||
getUserInfo(): UserInfo | null {
|
||||
return this.userInfo;
|
||||
},
|
||||
getUserRoles(): string[] {
|
||||
return this.userRoles;
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
// 持久化
|
||||
// TODO: accessToken 过期时间
|
||||
paths: ['accessToken', 'userRoles', 'userInfo'],
|
||||
},
|
||||
state: (): AccessState => ({
|
||||
accessMenus: [],
|
||||
accessRoutes: [],
|
||||
accessToken: null,
|
||||
userInfo: null,
|
||||
userRoles: [],
|
||||
}),
|
||||
});
|
||||
|
||||
// 解决热更新问题
|
||||
const hot = import.meta.hot;
|
||||
if (hot) {
|
||||
hot.accept(acceptHMRUpdate(useAccessStore, hot));
|
||||
}
|
||||
|
||||
export { useAccessStore };
|
2
packages/@vben-core/forward/stores/src/modules/index.ts
Normal file
2
packages/@vben-core/forward/stores/src/modules/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './access';
|
||||
export * from './tabs';
|
399
packages/@vben-core/forward/stores/src/modules/tabs.ts
Normal file
399
packages/@vben-core/forward/stores/src/modules/tabs.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
import { startProgress, stopProgress } from '@vben-core/toolkit';
|
||||
import { TabItem } from '@vben-core/typings';
|
||||
import type { RouteRecordNormalized, Router } from 'vue-router';
|
||||
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
/**
|
||||
* @zh_CN 克隆路由,防止路由被修改
|
||||
* @param route
|
||||
*/
|
||||
function cloneTab(route: TabItem): TabItem {
|
||||
if (!route) {
|
||||
return route;
|
||||
}
|
||||
const { matched, ...opt } = route;
|
||||
return {
|
||||
...opt,
|
||||
matched: (matched
|
||||
? matched.map((item) => ({
|
||||
meta: item.meta,
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
}))
|
||||
: undefined) as RouteRecordNormalized[],
|
||||
};
|
||||
}
|
||||
|
||||
function routeToTab(route: RouteRecordNormalized) {
|
||||
return {
|
||||
meta: route.meta,
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
} as unknown as TabItem;
|
||||
}
|
||||
|
||||
interface TabsState {
|
||||
/**
|
||||
* @zh_CN 固定的标签页列表
|
||||
*/
|
||||
affixTabs: RouteRecordNormalized[];
|
||||
/**
|
||||
* @zh_CN 当前打开的标签页列表缓存
|
||||
*/
|
||||
cacheTabs: Set<string>;
|
||||
/**
|
||||
* @zh_CN 需要排除缓存的标签页
|
||||
*/
|
||||
excludeCacheTabs: Set<string>;
|
||||
/**
|
||||
* @zh_CN 是否刷新
|
||||
*/
|
||||
renderRouteView?: boolean;
|
||||
/**
|
||||
* @zh_CN 当前打开的标签页列表
|
||||
*/
|
||||
tabs: TabItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 访问权限相关
|
||||
*/
|
||||
const useTabsStore = defineStore('tabs', {
|
||||
actions: {
|
||||
/**
|
||||
* Close tabs in bulk
|
||||
*/
|
||||
async _bulkCloseByPaths(paths: string[]) {
|
||||
this.tabs = this.tabs.filter((item) => {
|
||||
return !paths.includes(this.getTabPath(item));
|
||||
});
|
||||
|
||||
this.updateCacheTab();
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭标签页
|
||||
* @param tab
|
||||
*/
|
||||
_close(tab: TabItem) {
|
||||
const { fullPath } = tab;
|
||||
if (this._isAffixTab(tab)) {
|
||||
return;
|
||||
}
|
||||
const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
|
||||
index !== -1 && this.tabs.splice(index, 1);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 跳转到默认标签页
|
||||
*/
|
||||
async _goToDefaultTab(router: Router) {
|
||||
if (this.getTabs.length <= 0) {
|
||||
// TODO: 跳转首页
|
||||
return;
|
||||
}
|
||||
const firstTab = this.getTabs[0];
|
||||
await this._goToTab(firstTab, router);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 跳转到标签页
|
||||
* @param tab
|
||||
*/
|
||||
async _goToTab(tab: TabItem, router: Router) {
|
||||
const { params, path, query } = tab;
|
||||
const toParams = {
|
||||
params: params || {},
|
||||
path,
|
||||
query: query || {},
|
||||
};
|
||||
await router.replace(toParams);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 是否是固定标签页
|
||||
* @param tab
|
||||
*/
|
||||
_isAffixTab(tab: TabItem) {
|
||||
return tab?.meta?.affixTab ?? false;
|
||||
},
|
||||
/**
|
||||
* @zh_CN 添加标签页
|
||||
* @param routeTab
|
||||
*/
|
||||
addTab(routeTab: TabItem) {
|
||||
const tab = cloneTab(routeTab);
|
||||
const { fullPath, meta, params, query } = tab;
|
||||
if (meta?.hideInTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = this.tabs.findIndex((tab) => {
|
||||
return this.getTabPath(tab) === this.getTabPath(routeTab);
|
||||
});
|
||||
|
||||
if (tabIndex === -1) {
|
||||
this.tabs.push(tab);
|
||||
} else {
|
||||
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
|
||||
const currentTab = toRaw(this.tabs)[tabIndex];
|
||||
if (!currentTab) {
|
||||
return;
|
||||
}
|
||||
currentTab.params = params || currentTab.params;
|
||||
currentTab.query = query || currentTab.query;
|
||||
currentTab.fullPath = fullPath || currentTab.fullPath;
|
||||
this.tabs.splice(tabIndex, 1, currentTab);
|
||||
}
|
||||
this.updateCacheTab();
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭所有标签页
|
||||
*/
|
||||
async closeAllTabs(router: Router) {
|
||||
this.tabs = this.tabs.filter((tab) => this._isAffixTab(tab));
|
||||
await this._goToDefaultTab(router);
|
||||
this.updateCacheTab();
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭左侧标签页
|
||||
* @param tab
|
||||
*/
|
||||
async closeLeftTabs(tab: TabItem) {
|
||||
const index = this.tabs.findIndex(
|
||||
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
||||
);
|
||||
|
||||
if (index > 0) {
|
||||
const leftTabs = this.tabs.slice(0, index);
|
||||
const paths: string[] = [];
|
||||
for (const item of leftTabs) {
|
||||
if (!this._isAffixTab(tab)) {
|
||||
paths.push(this.getTabPath(item));
|
||||
}
|
||||
}
|
||||
await this._bulkCloseByPaths(paths);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 关闭其他标签页
|
||||
* @param tab
|
||||
*/
|
||||
async closeOtherTabs(tab: TabItem) {
|
||||
const closePaths = this.tabs.map((item) => this.getTabPath(item));
|
||||
|
||||
const paths: string[] = [];
|
||||
|
||||
for (const path of closePaths) {
|
||||
if (path !== tab.fullPath) {
|
||||
const closeTab = this.tabs.find(
|
||||
(item) => this.getTabPath(item) === path,
|
||||
);
|
||||
if (!closeTab) {
|
||||
continue;
|
||||
}
|
||||
if (!this._isAffixTab(tab)) {
|
||||
paths.push(this.getTabPath(closeTab));
|
||||
}
|
||||
}
|
||||
}
|
||||
await this._bulkCloseByPaths(paths);
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 关闭右侧标签页
|
||||
* @param tab
|
||||
*/
|
||||
async closeRightTabs(tab: TabItem) {
|
||||
const index = this.tabs.findIndex(
|
||||
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
||||
);
|
||||
|
||||
if (index >= 0 && index < this.tabs.length - 1) {
|
||||
const rightTabs = this.tabs.slice(index + 1, this.tabs.length);
|
||||
|
||||
const paths: string[] = [];
|
||||
for (const item of rightTabs) {
|
||||
if (!this._isAffixTab(tab)) {
|
||||
paths.push(this.getTabPath(item));
|
||||
}
|
||||
}
|
||||
await this._bulkCloseByPaths(paths);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 关闭标签页
|
||||
* @param tab
|
||||
* @param router
|
||||
*/
|
||||
async closeTab(tab: TabItem, router: Router) {
|
||||
const { currentRoute } = router;
|
||||
|
||||
// 关闭不是激活选项卡
|
||||
if (this.getTabPath(currentRoute.value) !== this.getTabPath(tab)) {
|
||||
this._close(tab);
|
||||
this.updateCacheTab();
|
||||
return;
|
||||
}
|
||||
const index = this.getTabs.findIndex(
|
||||
(item) => this.getTabPath(item) === this.getTabPath(currentRoute.value),
|
||||
);
|
||||
|
||||
const before = this.getTabs[index - 1];
|
||||
const after = this.getTabs[index + 1];
|
||||
|
||||
// 下一个tab存在,跳转到下一个
|
||||
if (after) {
|
||||
this._close(currentRoute.value);
|
||||
await this._goToTab(after, router);
|
||||
// 上一个tab存在,跳转到上一个
|
||||
} else if (before) {
|
||||
this._close(currentRoute.value);
|
||||
await this._goToTab(before, router);
|
||||
} else {
|
||||
console.error('关闭标签页失败,当前只剩一个标签页。');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @zh_CN 通过key关闭标签页
|
||||
* @param key
|
||||
*/
|
||||
async closeTabByKey(key: string, router: Router) {
|
||||
const index = this.tabs.findIndex(
|
||||
(item) => this.getTabPath(item) === key,
|
||||
);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.closeTab(this.tabs[index], router);
|
||||
},
|
||||
getTabPath(tab: RouteRecordNormalized | TabItem) {
|
||||
return decodeURIComponent((tab as TabItem).fullPath || tab.path);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 固定标签页
|
||||
* @param tab
|
||||
*/
|
||||
async pushPinTab(tab: TabItem) {
|
||||
const index = this.tabs.findIndex(
|
||||
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.tabs[index].meta.affixTab = true;
|
||||
}
|
||||
// TODO: 这里应该把tab从tbs中移除
|
||||
this.affixTabs.push(tab as unknown as RouteRecordNormalized);
|
||||
},
|
||||
/**
|
||||
* 刷新标签页
|
||||
*/
|
||||
async refreshTab(router: Router) {
|
||||
const { currentRoute } = router;
|
||||
const { name } = currentRoute.value;
|
||||
|
||||
this.excludeCacheTabs.add(name as string);
|
||||
this.renderRouteView = false;
|
||||
startProgress();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
this.excludeCacheTabs.delete(name as string);
|
||||
this.renderRouteView = true;
|
||||
stopProgress();
|
||||
},
|
||||
/**
|
||||
* 设置固定标签页
|
||||
* @param tabs
|
||||
*/
|
||||
setAffixTabs(tabs: RouteRecordNormalized[]) {
|
||||
for (const tab of tabs) {
|
||||
this.addTab(routeToTab(tab));
|
||||
}
|
||||
this.affixTabs = tabs;
|
||||
},
|
||||
/**
|
||||
* @zh_CN 取消固定标签页
|
||||
* @param tab
|
||||
*/
|
||||
async unPushPinTab(tab: TabItem) {
|
||||
const index = this.affixTabs.findIndex(
|
||||
(item) => this.getTabPath(item) === this.getTabPath(tab),
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
this.affixTabs[index].meta.affixTab = false;
|
||||
this.affixTabs.splice(index, 1);
|
||||
this.addTab(tab);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据当前打开的选项卡更新缓存
|
||||
*/
|
||||
async updateCacheTab() {
|
||||
const cacheMap = new Set<string>();
|
||||
|
||||
for (const tab of this.tabs) {
|
||||
// 跳过不需要持久化的标签页
|
||||
const keepAlive = tab.meta?.keepAlive;
|
||||
if (!keepAlive) {
|
||||
continue;
|
||||
}
|
||||
tab.matched.forEach((t, i) => {
|
||||
if (i > 0) {
|
||||
cacheMap.add(t.name as string);
|
||||
}
|
||||
});
|
||||
|
||||
const name = tab.name as string;
|
||||
cacheMap.add(name);
|
||||
}
|
||||
this.cacheTabs = cacheMap;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
getCacheTabs(): string[] {
|
||||
return [...this.cacheTabs];
|
||||
},
|
||||
getExcludeTabs(): string[] {
|
||||
return [...this.excludeCacheTabs];
|
||||
},
|
||||
|
||||
getTabs(): TabItem[] {
|
||||
const tabs: TabItem[] = [];
|
||||
const affixTabPaths = new Set<string>();
|
||||
for (const tab of this.affixTabs) {
|
||||
if (!tab.meta.hideInTab) {
|
||||
tabs.push(routeToTab(tab));
|
||||
affixTabPaths.add(tab.path);
|
||||
}
|
||||
}
|
||||
for (const tab of this.tabs) {
|
||||
if (!affixTabPaths.has(tab.path) && !tab.meta.hideInTab) {
|
||||
tabs.push(tab);
|
||||
}
|
||||
}
|
||||
return tabs;
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
// 持久化
|
||||
paths: [],
|
||||
},
|
||||
state: (): TabsState => ({
|
||||
affixTabs: [],
|
||||
cacheTabs: new Set(),
|
||||
excludeCacheTabs: new Set(),
|
||||
renderRouteView: true,
|
||||
tabs: [],
|
||||
}),
|
||||
});
|
||||
|
||||
// 解决热更新问题
|
||||
const hot = import.meta.hot;
|
||||
if (hot) {
|
||||
hot.accept(acceptHMRUpdate(useTabsStore, hot));
|
||||
}
|
||||
|
||||
export { useTabsStore };
|
30
packages/@vben-core/forward/stores/src/setup.ts
Normal file
30
packages/@vben-core/forward/stores/src/setup.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
interface InitStoreOptions {
|
||||
/**
|
||||
* @zh_CN 应用名,由于 @vben-core/stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名
|
||||
* 应用名将被用于持久化的前缀
|
||||
*/
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 初始化pinia
|
||||
*/
|
||||
async function initStore(options: InitStoreOptions) {
|
||||
const { createPersistedState } = await import('pinia-plugin-persistedstate');
|
||||
const pinia = createPinia();
|
||||
const { namespace } = options;
|
||||
pinia.use(
|
||||
createPersistedState({
|
||||
// key $appName-$store.id
|
||||
key: (storeKey) => `${namespace}-${storeKey}`,
|
||||
storage: localStorage,
|
||||
}),
|
||||
);
|
||||
return pinia;
|
||||
}
|
||||
|
||||
export { initStore };
|
||||
|
||||
export type { InitStoreOptions };
|
5
packages/@vben-core/forward/stores/tsconfig.json
Normal file
5
packages/@vben-core/forward/stores/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src", "shim-pinia.d.ts"]
|
||||
}
|
Reference in New Issue
Block a user