feat: 迁移前端资源文件并重构项目结构

refactor: 优化前端代码结构和资源管理

style: 调整前端代码格式和样式

chore: 更新.gitignore和构建配置

fix: 修复前端资源路径和引用问题

docs: 更新前端文档和注释

perf: 优化前端性能和加载速度

test: 更新前端测试用例

build: 调整前端构建配置

ci: 更新CI/CD配置
This commit is contained in:
zhangtao
2026-05-09 00:46:43 +08:00
parent 1bd125d99b
commit bf81480ada
967 changed files with 21018 additions and 83019 deletions
Regular → Executable
+33 -48
View File
@@ -1,60 +1,47 @@
import type { App } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import { router } from "@/router";
import { resetDynamicRoutesSync } from "@/router/beforeEach";
import { useUserStore } from "./modules/user.store";
import { useDictStore } from "./modules/dict.store";
import { useNoticeStore } from "./modules/notice.store";
import { useConfigStore } from "./modules/config.store";
import { useTagsViewStore } from "./modules/tags-view.store";
const store = createPinia();
store.use(piniaPluginPersistedstate);
// 全局注册 store
export function initStore(app: App<Element>) {
app.use(store);
}
export * from "./modules/app.store";
export * from "./modules/settings.store";
export * from "./modules/tags-view.store";
export * from "./modules/lock.store";
export * from "./modules/permission.store";
export * from "./modules/user.store";
export * from "./modules/dict.store";
export * from "./modules/config.store";
export * from "./modules/dict.store";
export * from "./modules/lock.store";
export * from "./modules/menu.store";
export * from "./modules/notice.store";
export { store };
export * from "./modules/setting.store";
export * from "./modules/table.store";
export * from "./modules/tags-view.store";
export * from "./modules/user.store";
export * from "./modules/worktab.store";
// ------------------------------
// 🔄 刷新缓存统一入口
// ------------------------------
import router from "@/router";
import { useUserStoreHook } from "./modules/user.store";
import { usePermissionStoreHook } from "./modules/permission.store";
import { useDictStoreHook } from "./modules/dict.store";
import { useConfigStoreHook } from "./modules/config.store";
import { useNoticeStoreHook } from "./modules/notice.store";
import { useTagsViewStore } from "./modules/tags-view.store";
import { RouteRecordRaw } from "vue-router";
export { store };
export { useUserStore, useDictStore, useNoticeStore, useConfigStore, useTagsViewStore };
export interface RefreshCacheOptions {
/** 需要刷新的字典类型列表,不传则不刷新字典 */
dictTypes?: string[];
/** 是否刷新用户信息(含角色与权限) */
refreshUser?: boolean; // 默认 true
/** 是否重置并重新生成动态路由 */
refreshRoutes?: boolean; // 默认 true
/** 是否刷新系统配置 */
refreshConfig?: boolean; // 默认 true
/** 是否刷新通知公告 */
refreshNotice?: boolean; // 默认 true
/** 是否清空标签视图(避免路由变化后出现不一致) */
clearTags?: boolean; // 默认 false
/** 刷新字典前是否先清空本地字典缓存 */
clearDictBefore?: boolean; // 默认 false
refreshUser?: boolean;
refreshRoutes?: boolean;
refreshConfig?: boolean;
refreshNotice?: boolean;
clearTags?: boolean;
clearDictBefore?: boolean;
}
/**
* 一键刷新常用缓存项,可按需选择。
* 建议:服务端数据(配置、字典、公告、用户/权限)适合刷新;UI 偏好(app/settings/lock)不刷新。
*/
export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
const {
dictTypes,
@@ -66,11 +53,10 @@ export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
clearDictBefore = false,
} = opts;
const userStore = useUserStoreHook();
const permStore = usePermissionStoreHook();
const dictStore = useDictStoreHook();
const noticeStore = useNoticeStoreHook();
const configStore = useConfigStoreHook();
const userStore = useUserStore(store);
const dictStore = useDictStore(store);
const noticeStore = useNoticeStore(store);
const configStore = useConfigStore(store);
const tagsViewStore = useTagsViewStore(store);
const tasks: Promise<any>[] = [];
@@ -89,18 +75,17 @@ export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
tasks.push(dictStore.getDict(dictTypes));
}
// 并行刷新服务端数据
await Promise.allSettled(tasks);
// 路由需要串行处理,先重置再生成
if (refreshRoutes) {
permStore.resetRouter();
const dynamicRoutes = await permStore.generateRoutes();
// 将新生成的动态路由注册到路由器,确保可用
dynamicRoutes.forEach((route: RouteRecordRaw) => router.addRoute(route));
resetDynamicRoutesSync();
await router.replace({
path: router.currentRoute.value.path,
query: router.currentRoute.value.query,
hash: router.currentRoute.value.hash,
});
}
// 可选:清空标签视图,避免路由变更后的不一致
if (clearTags) {
await tagsViewStore.delAllViews();
}
+110 -117
View File
@@ -1,129 +1,122 @@
import { defaultSettings } from "@/settings";
// 导入 Element Plus 中英文语言包
/**
* 应用状态管理模块(兼容 web 项目)
*
* 提供应用相关的状态管理
*
* ## 主要功能
*
* - 设备类型管理(桌面/平板/移动端)
* - 布局大小管理(默认/紧凑/宽松)
* - 语言管理(中文/英文)
* - 侧边栏状态管理(展开/收起)
* - 顶部菜单激活路径管理
* - 引导功能可见性管理
*
* ## 使用场景
*
* - 响应式布局适配
* - 多语言切换
* - 侧边栏折叠展开
* - 顶部菜单导航
* - 新手引导展示
*
* @module store/modules/app.store
* @author FastapiAdmin Team
*/
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { DeviceEnum } from "@/enums/settings/device.enum";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import en from "element-plus/es/locale/lang/en";
import { store } from "@/store";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { SidebarStatus } from "@/enums/settings/layout.enum";
import { defineStore } from "pinia";
import { useStorage } from "@vueuse/core";
import { computed, reactive } from "vue";
export const useAppStore = defineStore("app", () => {
// 设备类型
const device = useStorage("device", DeviceEnum.DESKTOP);
// 布局大小
const size = useStorage("size", defaultSettings.size);
// 语言
const language = useStorage("language", defaultSettings.language);
// 侧边栏状态
const sidebarStatus = useStorage("sidebarStatus", SidebarStatus.CLOSED);
const sidebar = reactive({
opened: sidebarStatus.value === SidebarStatus.OPENED,
withoutAnimation: false,
});
export const useAppStore = defineStore(
"appStore",
() => {
/** 设备类型 */
const device = ref<string>(DeviceEnum.DESKTOP);
// 顶部菜单激活路径
const activeTopMenuPath = useStorage("activeTopMenuPath", "");
// 项目引导
const guideVisible = useStorage("guideVisible", defaultSettings.guideVisible);
/** 布局大小 */
const size = ref<string>("default");
/**
* 根据语言标识读取对应的语言包
*/
const locale = computed(() => {
if (language?.value == "en") {
return en;
} else {
return zhCn;
/** 语言 */
const language = ref<string>("zh");
/** 侧边栏状态 */
const sidebar = ref({
opened: true,
withoutAnimation: false,
});
/** 顶部菜单激活路径 */
const activeTopMenuPath = ref<string>("");
/** 引导可见性 */
const guideVisible = ref<boolean>(false);
/** 语言区域 */
const locale = computed(() => {
return language.value === "en" ? en : zhCn;
});
/** 切换侧边栏 */
function toggleSidebar() {
sidebar.value.opened = !sidebar.value.opened;
}
});
/**
* 切换侧边栏
*/
function toggleSidebar() {
sidebar.opened = !sidebar.opened;
sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED;
}
/** 关闭侧边栏 */
function closeSideBar() {
sidebar.value.opened = false;
}
/**
* 关闭侧边栏
*/
function closeSideBar() {
sidebar.opened = false;
sidebarStatus.value = SidebarStatus.CLOSED;
}
/** 打开侧边栏 */
function openSideBar() {
sidebar.value.opened = true;
}
/**
* 打开侧边栏
*/
function openSideBar() {
sidebar.opened = true;
sidebarStatus.value = SidebarStatus.OPENED;
}
/** 切换设备 */
function toggleDevice(val: string) {
device.value = val;
}
/**
* 切换设备
* @param val 设备类型
*/
function toggleDevice(val: string) {
device.value = val;
}
/** 改变布局大小 */
function changeSize(val: string) {
size.value = val;
}
/**
* 改变布局大小
* @param val 布局大小 default | small | large
*/
function changeSize(val: string) {
size.value = val;
}
/**
* 切换语言
* @param val
*/
function changeLanguage(val: string) {
language.value = val;
}
/**
* 混合模式顶部切换
* @param val
*/
function activeTopMenu(val: string) {
activeTopMenuPath.value = val;
}
/**
* 显示或隐藏引导
* @param val 是否显示引导
*/
function showGuide(val: boolean) {
guideVisible.value = val;
}
return {
device,
sidebar,
language,
locale,
size,
activeTopMenu,
toggleDevice,
showGuide,
changeSize,
changeLanguage,
toggleSidebar,
closeSideBar,
openSideBar,
activeTopMenuPath,
guideVisible,
};
});
/** 改变语言 */
function changeLanguage(val: string) {
language.value = val;
}
/**
* 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。
* 官方文档解释了如何在组件外部使用 Pinia Store:
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
*/
export function useAppStoreHook() {
return useAppStore(store);
}
/** 顶部菜单切换 */
function activeTopMenu(val: string) {
activeTopMenuPath.value = val;
}
/** 显示或隐藏引导 */
function showGuide(val: boolean) {
guideVisible.value = val;
}
return {
device,
sidebar,
language,
locale,
size,
activeTopMenu,
toggleDevice,
showGuide,
changeSize,
changeLanguage,
toggleSidebar,
closeSideBar,
openSideBar,
activeTopMenuPath,
guideVisible,
};
},
{
persist: true,
}
);
+69 -43
View File
@@ -1,59 +1,85 @@
import { store } from "@/store";
/**
* 系统配置状态管理模块
*
* 提供系统配置参数的状态管理
*
* ## 主要功能
*
* - 网站基础信息管理(标题、版本、描述)
* - 网站图标配置(登录背景、favicon、Logo)
* - 安全隐私配置(服务条款、版权、隐私政策)
* - 接口安全配置(白名单、黑名单)
* - 演示环境配置
*
* ## 使用场景
*
* - 系统初始化配置加载
* - 登录页背景和Logo显示
* - 底部版权信息展示
* - 接口安全控制
*
* ## 持久化
*
* - 使用 localStorage 存储
* - 自动缓存已加载的配置
* - 支持强制刷新配置
*
* @module store/modules/config.store
* @author FastapiAdmin Team
*/
import { store } from "@stores";
import ParamsAPI, { ConfigTable } from "@/api/module_system/params";
import { defineStore } from "pinia";
import { ref } from "vue";
interface ConfigState {
// 网站信息
sys_web_title: ConfigTable;
sys_web_version: ConfigTable;
sys_web_description: ConfigTable;
// 网站图标
sys_login_background: ConfigTable;
sys_web_favicon: ConfigTable;
sys_web_logo: ConfigTable;
// 安全隐私配置
sys_keep_record: ConfigTable;
sys_web_clause: ConfigTable;
sys_web_copyright: ConfigTable;
sys_web_privacy: ConfigTable;
sys_git_code: ConfigTable;
sys_help_doc: ConfigTable;
// 接口安全配置
white_api_list_path: ConfigTable;
ip_black_list: ConfigTable;
// 演示环境配置
demo_enable: ConfigTable;
ip_white_list: ConfigTable;
}
export const useConfigStore = defineStore(
"configStore",
() => {
// 配置数据
const configData = ref<Record<string, ConfigTable>>({});
// 是否已加载配置
const isConfigLoaded = ref(false);
// 是否正在加载配置
const configLoading = ref(false);
export const useConfigStore = defineStore("config", {
state: () => ({
configData: {} as ConfigState,
isConfigLoaded: false,
configLoading: false,
}),
actions: {
async getConfig(force = false) {
if ((this.isConfigLoaded && !force) || this.configLoading) {
/**
* 获取系统配置
* @param force 是否强制刷新配置
*/
async function getConfig(force = false) {
if ((isConfigLoaded.value && !force) || configLoading.value) {
return;
}
this.configLoading = true;
configLoading.value = true;
try {
const response = await ParamsAPI.getInitConfig();
response.data.data.forEach((item: ConfigTable) => {
if (item.config_value !== undefined) {
this.configData[item.config_key as keyof ConfigState] = item;
const list = response?.data?.data;
if (!Array.isArray(list)) {
console.warn("[configStore] getInitConfig: 响应 data 非数组", response?.data);
return;
}
list.forEach((item: ConfigTable) => {
if (item.config_value !== undefined && item.config_key) {
configData.value[item.config_key] = item;
}
});
this.isConfigLoaded = true;
isConfigLoaded.value = true;
} finally {
this.configLoading = false;
configLoading.value = false;
}
},
}
return {
configData,
isConfigLoaded,
configLoading,
getConfig,
};
},
persist: true,
});
{
persist: true,
}
);
export function useConfigStoreHook() {
return useConfigStore(store);
+102 -38
View File
@@ -1,47 +1,89 @@
import { store } from "@/store";
/**
* 字典状态管理模块
*
* 提供数据字典的状态管理和缓存机制
*
* ## 主要功能
*
* - 字典数据缓存和管理
* - 批量获取字典数据
* - 字典标签查找
* - 字典数据清空
*
* ## 使用场景
*
* - 表单下拉选择框数据填充
* - 表格列数据格式化显示
* - 状态字段标签转换
* - 动态表单配置
*
* ## 核心特性
*
* - 自动去重缓存
* - 按需加载字典
* - 统一的数据格式
* - 支持批量获取
*
* ## 持久化
*
* - 使用 localStorage 存储
* - 缓存已加载的字典数据
* - 减少重复请求
*
* @module store/modules/dict.store
* @author FastapiAdmin Team
*/
import { store } from "@stores";
import DictAPI, { DictDataTable } from "@/api/module_system/dict";
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useDictStore = defineStore("dict", {
state: () => ({
dictData: {} as Record<string, DictDataTable[]>,
isLoaded: false,
}),
getters: {
getDictData(): Record<string, DictDataTable[]> {
return this.dictData;
},
export const useDictStore = defineStore(
"dictStore",
() => {
// 字典数据
const dictData = ref<Record<string, DictDataTable[]>>({});
// 是否已加载
const isLoaded = ref(false);
// 获取指定类型的字典数据,确保返回数组
getDictArray() {
return (type: string): Array<{ dict_value: string; dict_label: string }> => {
return (this.dictData[type] || [])
.filter((item) => item.dict_value !== undefined && item.dict_label !== undefined)
.map((item) => ({
dict_value: item.dict_value!,
dict_label: item.dict_label!,
}));
};
},
},
actions: {
// 批量获取字典数据
async getDict(types: string[]): Promise<Record<string, DictDataTable[]>> {
/**
* 获取所有字典数据
*/
const getDictData = computed(() => dictData.value);
/**
* 获取指定类型的字典数据,确保返回数组
*/
const getDictArray = (type: string): Array<{ dict_value: string; dict_label: string }> => {
return (dictData.value[type] || [])
.filter((item) => item.dict_value !== undefined && item.dict_label !== undefined)
.map((item) => ({
dict_value: item.dict_value!,
dict_label: item.dict_label!,
}));
};
/**
* 批量获取字典数据
* @param types 字典类型数组
* @returns 指定类型的字典数据
*/
async function getDict(types: string[]): Promise<Record<string, DictDataTable[]>> {
try {
for (const type of types) {
if (!this.dictData[type]) {
if (!dictData.value[type]) {
const response = await DictAPI.getInitDict(type);
// 确保数据格式正确
this.dictData[type] = ((response.data.data as DictDataTable[]) || []).filter(
dictData.value[type] = ((response.data.data as DictDataTable[]) || []).filter(
(item) => item.dict_value !== undefined && item.dict_label !== undefined
);
this.isLoaded = true;
isLoaded.value = true;
}
}
// 返回请求的字典数据
return types.reduce(
(result, type) => {
result[type] = this.getDictArray(type);
result[type] = getDictArray(type);
return result;
},
{} as Record<string, DictDataTable[]>
@@ -50,10 +92,16 @@ export const useDictStore = defineStore("dict", {
console.error("获取字典数据失败", error);
return {};
}
},
}
getDictLabel(type: string, value: string) {
const result = this.dictData[type].find((item) => item.dict_value === value);
/**
* 根据类型和值获取字典标签
* @param type 字典类型
* @param value 字典值
* @returns 字典标签对象或原值
*/
function getDictLabel(type: string, value: string) {
const result = dictData.value[type]?.find((item) => item.dict_value === value);
if (!result) {
return value;
}
@@ -74,13 +122,29 @@ export const useDictStore = defineStore("dict", {
updated_time: result.updated_time,
};
return dict_data;
},
clearDictData() {
this.dictData = {};
},
}
/**
* 清空字典数据
*/
function clearDictData() {
dictData.value = {};
}
return {
dictData,
isLoaded,
getDictData,
getDictArray,
getDict,
getDictLabel,
clearDictData,
};
},
persist: true,
});
{
persist: true,
}
);
export function useDictStoreHook() {
return useDictStore(store);
+83 -38
View File
@@ -1,48 +1,93 @@
/**
* 锁屏状态管理模块
*
* 提供锁屏功能的状态管理
*
* ## 主要功能
*
* - 锁屏状态管理
* - 锁屏密码验证
* - 锁屏信息存储
* - 解锁功能
*
* ## 使用场景
*
* - 用户离开时锁定屏幕
* - 保护敏感操作
* - 临时离开保护
*
* ## 持久化
*
* - 使用 localStorage 存储
* - 锁屏状态跨页面保持
*
* @module store/modules/lock.store
* @author FastapiAdmin Team
*/
import { defineStore } from "pinia";
import { store } from "../index";
import { ref } from "vue";
interface lockInfo {
isLock?: boolean;
password?: string;
}
export const useLockStore = defineStore(
"lockStore",
() => {
// 是否锁定屏幕
const isLock = ref(false);
// 锁屏密码
const password = ref("");
interface LockState {
lockInfo: lockInfo;
}
/**
* 获取锁屏信息
*/
const getLockInfo = () => ({
isLock: isLock.value,
password: password.value,
});
export const useLockStore = defineStore("lock", {
state: (): LockState => {
return {
lockInfo: {
// isLock: false, // 是否锁定屏幕
// password: '' // 锁屏密码
},
/**
* 设置锁屏信息
* @param lockInfo 锁屏信息对象
*/
const setLockInfo = (lockInfo: { isLock?: boolean; password?: string }) => {
if (lockInfo.isLock !== undefined) {
isLock.value = lockInfo.isLock;
}
if (lockInfo.password !== undefined) {
password.value = lockInfo.password;
}
};
},
getters: {
getLockInfo(): lockInfo {
return this.lockInfo;
},
},
actions: {
setLockInfo(lockInfo: lockInfo) {
this.lockInfo = lockInfo;
},
resetLockInfo() {
this.lockInfo = {};
},
unLock(password: string) {
if (this.lockInfo?.password === password) {
this.resetLockInfo();
/**
* 重置锁屏信息
*/
const resetLockInfo = () => {
isLock.value = false;
password.value = "";
};
/**
* 解锁屏幕
* @param inputPassword 输入的解锁密码
* @returns 是否解锁成功
*/
const unLock = (inputPassword: string) => {
if (password.value === inputPassword) {
resetLockInfo();
return true;
} else {
return false;
}
},
},
persist: true,
});
};
export const useLockStoreWithOut = () => {
return useLockStore(store);
};
return {
isLock,
password,
getLockInfo,
setLockInfo,
resetLockInfo,
unLock,
};
},
{
persist: true,
}
);
+119
View File
@@ -0,0 +1,119 @@
/**
* 菜单状态管理模块
*
* 提供菜单数据和动态路由的状态管理
*
* ## 主要功能
*
* - 菜单列表存储和管理
* - 首页路径配置
* - 动态路由注册和移除
* - 路由移除函数管理
* - 菜单宽度配置
*
* ## 使用场景
*
* - 动态菜单加载和渲染
* - 路由权限控制
* - 首页路径动态设置
* - 登出时清理动态路由
*
* ## 工作流程
*
* 1. 获取菜单数据(前端/后端模式)
* 2. 设置菜单列表和首页路径
* 3. 注册动态路由并保存移除函数
* 4. 登出时调用移除函数清理路由
*
* @module store/modules/menu.store
* @author FastapiAdmin Team
*/
import { defineStore } from "pinia";
import { ref } from "vue";
import { AppRouteRecord } from "@/types/router";
import { getFirstMenuPath } from "@utils";
import { HOME_PAGE_PATH } from "@/router";
import { mergeShellRoutesIntoMenu } from "@/router/staticRoutes";
/**
* 菜单状态管理
* 管理应用的菜单列表、首页路径、菜单宽度和动态路由移除函数
*/
export const useMenuStore = defineStore(
"menuStore",
() => {
/** 首页路径 */
const homePath = ref(HOME_PAGE_PATH);
/** 菜单列表 */
const menuList = ref<AppRouteRecord[]>([]);
/** 菜单宽度 */
const menuWidth = ref("");
/** 存储路由移除函数的数组 */
const removeRouteFns = ref<(() => void)[]>([]);
/**
* 设置菜单列表
* @param list 菜单路由记录数组
*/
const setMenuList = (list: AppRouteRecord[]) => {
const merged = mergeShellRoutesIntoMenu(list);
menuList.value = merged;
setHomePath(HOME_PAGE_PATH || getFirstMenuPath(merged));
};
/**
* 获取首页路径
* @returns 首页路径字符串
*/
const getHomePath = () => homePath.value;
/**
* 设置主页路径
* @param path 主页路径
*/
const setHomePath = (path: string) => {
homePath.value = path;
};
/**
* 添加路由移除函数
* @param fns 要添加的路由移除函数数组
*/
const addRemoveRouteFns = (fns: (() => void)[]) => {
removeRouteFns.value.push(...fns);
};
/**
* 移除所有动态路由
* 执行所有存储的路由移除函数并清空数组
*/
const removeAllDynamicRoutes = () => {
removeRouteFns.value.forEach((fn) => fn());
removeRouteFns.value = [];
};
/**
* 清空路由移除函数数组
*/
const clearRemoveRouteFns = () => {
removeRouteFns.value = [];
};
return {
menuList,
menuWidth,
removeRouteFns,
setMenuList,
getHomePath,
setHomePath,
addRemoveRouteFns,
removeAllDynamicRoutes,
clearRemoveRouteFns,
};
},
{
persist: {
pick: ["homePath", "menuWidth"],
},
}
);
+101 -40
View File
@@ -1,62 +1,123 @@
import { store } from "@/store";
/**
* 通知状态管理模块
*
* 提供系统通知的状态管理和持久化
*
* ## 主要功能
*
* - 通知列表获取和管理
* - 已读通知标记
* - 批量标记已读
* - 通知数量统计
* - 已读状态持久化
*
* ## 使用场景
*
* - 通知中心展示
* - 未读通知提醒
* - 通知阅读状态跟踪
* - 登录后的通知加载
*
* ## 持久化
*
* - 使用 localStorage 存储
* - 已读通知ID集合持久化
* - 刷新页面后保留已读状态
*
* @module store/modules/notice.store
* @author FastapiAdmin Team
*/
import { store } from "@stores";
import NoticeAPI, { NoticeTable } from "@/api/module_system/notice";
import { defineStore } from "pinia";
import { ref } from "vue";
export const useNoticeStore = defineStore("notice", {
state: () => ({
noticeList: [] as NoticeTable[],
total: 0,
isNoticeLoaded: false,
export const useNoticeStore = defineStore(
"noticeStore",
() => {
// 通知列表
const noticeList = ref<NoticeTable[]>([]);
// 通知总数
const total = ref(0);
// 是否已加载通知
const isNoticeLoaded = ref(false);
// 已读通知ID集合(前端持久化,避免刷新后重复展示)
readIds: [] as number[],
}),
actions: {
async getNotice() {
const readIds = ref<number[]>([]);
/**
* 获取通知列表
*/
async function getNotice() {
const response = await NoticeAPI.listNoticeAvailable();
const items = response.data.data.items || [];
// 过滤掉已读的通知
const readSet = new Set(this.readIds);
const readSet = new Set(readIds.value);
const filtered = items.filter(
(item) => item.id !== undefined && !readSet.has(item.id as number)
(item: NoticeTable) => item.id !== undefined && !readSet.has(item.id as number)
);
this.noticeList = filtered;
this.total = filtered.length;
this.isNoticeLoaded = true;
},
// 标记单条通知为已读
markAsRead(id?: number) {
noticeList.value = filtered;
total.value = filtered.length;
isNoticeLoaded.value = true;
}
/**
* 标记单条通知为已读
* @param id 通知ID
*/
const markAsRead = (id?: number) => {
if (id === undefined) return;
if (!this.readIds.includes(id)) {
this.readIds.push(id);
if (!readIds.value.includes(id)) {
readIds.value.push(id);
}
// 同步过滤当前列表
this.noticeList = this.noticeList.filter((item) => item.id !== id);
this.total = this.noticeList.length;
},
// 标记当前列表全部为已读
markAllAsRead(ids: number[] = []) {
noticeList.value = noticeList.value.filter((item) => item.id !== id);
total.value = noticeList.value.length;
};
/**
* 标记当前列表全部为已读
* @param ids 指定的通知ID数组,不传则标记所有
*/
const markAllAsRead = (ids: number[] = []) => {
const targets = ids.length
? ids
: this.noticeList.map((item) => item.id!).filter((id): id is number => id !== undefined);
const readSet = new Set(this.readIds);
: noticeList.value.map((item) => item.id!).filter((id): id is number => id !== undefined);
const readSet = new Set(readIds.value);
targets.forEach((id) => {
if (!readSet.has(id)) this.readIds.push(id);
if (!readSet.has(id)) readIds.value.push(id);
});
// 同步过滤当前列表
this.noticeList = this.noticeList.filter(
(item) => item.id !== undefined && !this.readIds.includes(item.id as number)
noticeList.value = noticeList.value.filter(
(item) => item.id !== undefined && !readIds.value.includes(item.id as number)
);
this.total = this.noticeList.length;
},
clearUserInfo() {
this.noticeList = [];
this.total = 0;
this.isNoticeLoaded = false;
this.readIds = [];
},
total.value = noticeList.value.length;
};
/**
* 清空用户通知信息(用于登出)
*/
const clearUserInfo = () => {
noticeList.value = [];
total.value = 0;
isNoticeLoaded.value = false;
readIds.value = [];
};
return {
noticeList,
total,
isNoticeLoaded,
readIds,
getNotice,
markAsRead,
markAllAsRead,
clearUserInfo,
};
},
persist: true,
});
{
persist: true,
}
);
export function useNoticeStoreHook() {
return useNoticeStore(store);
@@ -1,225 +0,0 @@
import type { RouteRecordRaw } from "vue-router";
import router, { constantRoutes } from "@/router";
import { store, useUserStore } from "@/store";
import { MenuTable } from "@/api/module_system/menu";
import { defineStore } from "pinia";
import { Component, ref } from "vue";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layouts/index.vue");
export interface Meta {
/** 【目录】只有一个子路由是否始终显示 */
alwaysShow?: boolean;
/** 是否隐藏(true-是 false-否) */
hidden?: boolean;
/** 菜单终端(后端 sys_menu.client,PC 动态路由仅下发 pc) */
client?: string;
/** ICON */
icon?: string;
/** 【菜单】是否开启页面缓存 */
keepAlive?: boolean;
/** 路由title */
title?: string;
/** 排序 */
order?: number;
/** 参数 */
params?: { key: string; value: string }[];
/** 是否固定路由 */
affix?: boolean;
}
// 修改 component 类型,使其能接受动态导入函数
export interface RouteVO {
/** 子路由列表 */
children: RouteVO[];
/** 组件路径或组件导入函数 */
component?: string | (() => Promise<Component>);
/** 路由属性 */
meta?: Meta;
/** 路由名称 */
name?: string;
/** 路由路径 */
path?: string;
/** 跳转链接 */
redirect?: string;
}
/**
* 后端常把「模块目录」与「其下功能菜单」写成同一绝对 route_path(如均为 /module_xxx/demo02)。
* Vue Router 嵌套子路由应使用空 path 挂到父 path 下,否则重复绝对路径会导致匹配异常。
*/
function normalizeMenuNestedPaths(items: MenuTable[], parentAbsolutePath?: string): MenuTable[] {
return items.map((node) => {
const raw = (node.route_path ?? "").trim();
let routePath: string | null | undefined = node.route_path;
if (parentAbsolutePath && raw && raw === parentAbsolutePath) {
routePath = "";
}
const canonical = raw.startsWith("/") ? raw : parentAbsolutePath;
const children = node.children?.length
? normalizeMenuNestedPaths(node.children, canonical)
: undefined;
return { ...node, route_path: routePath, children };
});
}
export const generator = (routers: MenuTable[]): RouteVO[] => {
return routers.map((item) => {
const currentRouter: RouteVO = {
children: [],
path: item.route_path,
name: item.route_name,
component: item.component_path,
redirect: item.redirect,
meta: {
title: item.title,
icon: item.icon || undefined,
client: item.client,
keepAlive: item.keep_alive,
hidden: item.hidden,
order: item.order,
alwaysShow: item.always_show,
params: item.params,
affix: item.affix,
},
};
if (item.children && item.children.length > 0) {
currentRouter.children = item.children ? generator(item.children) : [];
}
return currentRouter;
});
};
export const usePermissionStore = defineStore("permission", () => {
// 存储所有路由,包括静态路由和动态路由
const routes = ref<RouteRecordRaw[]>([]);
// 混合模式左侧菜单路由
const mixLayoutSideMenus = ref<RouteRecordRaw[]>([]);
// 路由是否加载完成
const isRouteGenerated = ref(false);
/**
* 获取后台动态路由数据,解析并注册到全局路由
*
* @returns Promise<RouteRecordRaw[]> 解析后的动态路由列表
*/
async function generateRoutes(): Promise<RouteRecordRaw[]> {
try {
const userStore = useUserStore();
// 确保获取用户信息和路由列表
if (!userStore.hasGetRoute) {
await userStore.getUserInfo();
}
const data = generator(normalizeMenuNestedPaths(userStore.routeList));
// 解析动态路由
const dynamicRoutes = transformRoutes(data);
routes.value = [...constantRoutes, ...dynamicRoutes];
isRouteGenerated.value = true;
return dynamicRoutes;
} catch (error: any) {
// 即使失败也要设置状态,避免无限重试
isRouteGenerated.value = false;
throw error;
}
}
/**
* 根据父菜单路径设置侧边菜单
*
* @param parentPath 父菜单的路径,用于查找对应的菜单项
*/
const setMixLayoutSideMenus = (parentPath: string) => {
const parentMenu = routes.value.find((item) => item.path === parentPath);
mixLayoutSideMenus.value = parentMenu?.children || [];
};
/**
* 重置路由
*/
const resetRouter = () => {
// 创建常量路由名称集合,用于O(1)时间复杂度的查找
const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean));
// 从 router 实例中移除动态路由
routes.value.forEach((route) => {
if (route.name && !constantRouteNames.has(route.name)) {
router.removeRoute(route.name);
}
});
// 重置所有状态
routes.value = [...constantRoutes];
mixLayoutSideMenus.value = [];
isRouteGenerated.value = false;
};
return {
routes,
mixLayoutSideMenus,
isRouteGenerated,
generateRoutes,
setMixLayoutSideMenus,
resetRouter,
};
});
/**
* 转换后端路由数据为Vue Router配置
* 处理组件路径映射和Layout层级嵌套
*/
const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => {
return routes.map((route) => {
// 创建路由对象,保留所有路由属性
const normalizedRoute = { ...route } as RouteRecordRaw;
// 处理组件路径映射
// normalizedRoute.component = !normalizedRoute.component
// ? Layout
// : modules[`../../views/${normalizedRoute.component}.vue`] ||
// modules["../../views/error/404.vue"];
// 关键优化:
// 1. 顶级路由(一级目录)使用Layout组件,确保菜单和navbar能正常显示
// 2. 二级及以上的父路由不使用Layout组件,只作为路由容器,避免Layout嵌套
// 3. 叶子路由使用实际组件
// 4. 递归处理子路由,实现无限层级菜单
if (normalizedRoute.children && normalizedRoute.children.length > 0) {
// normalizedRoute.children = transformRoutes(route.children);
// 非叶子路由
if (isTopLevel) {
// 顶级路由(一级目录),使用Layout组件
normalizedRoute.component = Layout;
} else {
// 二级及以上的父路由,不使用Layout组件,只作为路由容器
normalizedRoute.component = undefined;
}
// 递归处理子路由,标记为非顶级路由
normalizedRoute.children = transformRoutes(route.children, false);
} else {
// 叶子路由,使用实际组件
normalizedRoute.component = normalizedRoute.component
? modules[`../../views/${normalizedRoute.component}.vue`] ||
modules["../../views/error/404.vue"]
: modules["../../views/error/404.vue"];
}
return normalizedRoute;
});
};
/**
* 导出此hook函数用于在非组件环境(如其他store、工具函数等)中获取权限store实例
*
* 在组件中可直接使用usePermissionStore(),但在组件外部需要传入store实例
* 此函数简化了这个过程,避免每次都手动传入store参数
*/
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
@@ -0,0 +1,509 @@
/** Settings store. */
import { defineStore } from "pinia";
import { ref, computed, watch } from "vue";
import { MenuThemeType } from "@/types/store";
import AppConfig from "@/config";
import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from "@/enums/appEnum";
import { setElementThemeColor } from "@utils/ui";
import { formatToDate } from "@utils/common";
import { StorageConfig } from "@utils/storage";
import { SETTING_DEFAULT_CONFIG } from "@/config/setting";
import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@utils/ui";
import { SETTINGS_KEYS } from "@/constants";
import { useStorage } from "@vueuse/core";
import { defaultSettings } from "@/config/setting";
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
import type { LayoutMode } from "@/enums/settings/layout.enum";
import type { Ref } from "vue";
export const useSettingsStore = defineStore(
"settingStore",
() => {
// 菜单
const menuType = ref(SETTING_DEFAULT_CONFIG.menuType);
const menuOpenWidth = ref(SETTING_DEFAULT_CONFIG.menuOpenWidth);
const menuOpen = ref(SETTING_DEFAULT_CONFIG.menuOpen);
const dualMenuShowText = ref(SETTING_DEFAULT_CONFIG.dualMenuShowText);
// 主题
const systemThemeType = ref(SETTING_DEFAULT_CONFIG.systemThemeType);
const systemThemeMode = ref(SETTING_DEFAULT_CONFIG.systemThemeMode);
const menuThemeType = ref(SETTING_DEFAULT_CONFIG.menuThemeType);
const systemThemeColor = ref(SETTING_DEFAULT_CONFIG.systemThemeColor);
// 显示
const showMenuButton = ref(SETTING_DEFAULT_CONFIG.showMenuButton);
const showFastEnter = ref(SETTING_DEFAULT_CONFIG.showFastEnter);
const showRefreshButton = ref(SETTING_DEFAULT_CONFIG.showRefreshButton);
const showCrumbs = ref(SETTING_DEFAULT_CONFIG.showCrumbs);
const showWorkTab = ref(SETTING_DEFAULT_CONFIG.showWorkTab);
const showLanguage = ref(SETTING_DEFAULT_CONFIG.showLanguage);
const showNprogress = ref(SETTING_DEFAULT_CONFIG.showNprogress);
const showSettingGuide = ref(SETTING_DEFAULT_CONFIG.showSettingGuide);
const showFestivalText = ref(SETTING_DEFAULT_CONFIG.showFestivalText);
const watermarkVisible = ref(SETTING_DEFAULT_CONFIG.watermarkVisible);
// 功能
const autoClose = ref(SETTING_DEFAULT_CONFIG.autoClose);
const uniqueOpened = ref(SETTING_DEFAULT_CONFIG.uniqueOpened);
const colorWeak = ref(SETTING_DEFAULT_CONFIG.colorWeak);
const refresh = ref(SETTING_DEFAULT_CONFIG.refresh);
const holidayFireworksLoaded = ref(SETTING_DEFAULT_CONFIG.holidayFireworksLoaded);
// 样式
const boxBorderMode = ref(SETTING_DEFAULT_CONFIG.boxBorderMode);
const pageTransition = ref(SETTING_DEFAULT_CONFIG.pageTransition);
const tabStyle = ref(SETTING_DEFAULT_CONFIG.tabStyle);
const customRadius = ref(SETTING_DEFAULT_CONFIG.customRadius);
const containerWidth = ref(SETTING_DEFAULT_CONFIG.containerWidth);
// 节日
const festivalDate = ref("");
// 面板开关(非持久化)
const settingsVisible = ref<boolean>(false);
// 持久化(web-style:useStorage)
const showTagsView = useStorage<boolean>(
SETTINGS_KEYS.SHOW_TAGS_VIEW,
defaultSettings.showTagsView
);
const showAppLogo = useStorage<boolean>(
SETTINGS_KEYS.SHOW_APP_LOGO,
defaultSettings.showAppLogo
);
const showWatermark = useStorage<boolean>(
SETTINGS_KEYS.SHOW_WATERMARK,
defaultSettings.showWatermark
);
const showSettings = useStorage<boolean>(
SETTINGS_KEYS.SHOW_SETTINGS,
defaultSettings.showSettings
);
const showGuide = useStorage<boolean>(SETTINGS_KEYS.SHOW_GUIDE, defaultSettings.showGuide);
// 桌面端工具设置 - 持久化
const showMenuSearch = useStorage<boolean>(
SETTINGS_KEYS.SHOW_MENU_SEARCH,
defaultSettings.showMenuSearch
);
const showFullscreen = useStorage<boolean>(
SETTINGS_KEYS.SHOW_FULLSCREEN,
defaultSettings.showFullscreen
);
const showSizeSelect = useStorage<boolean>(
SETTINGS_KEYS.SHOW_SIZE_SELECT,
defaultSettings.showSizeSelect
);
const showLangSelect = useStorage<boolean>(
SETTINGS_KEYS.SHOW_LANG_SELECT,
defaultSettings.showLangSelect
);
const showNotification = useStorage<boolean>(
SETTINGS_KEYS.SHOW_NOTIFICATION,
defaultSettings.showNotification
);
// 布局和主题设置 - 持久化
const sidebarColorScheme = useStorage<string>(
SETTINGS_KEYS.SIDEBAR_COLOR_SCHEME,
defaultSettings.sidebarColorScheme
);
const layout = useStorage<LayoutMode>(
SETTINGS_KEYS.LAYOUT,
defaultSettings.layout as LayoutMode
);
const themeColor = useStorage<string>(SETTINGS_KEYS.THEME_COLOR, defaultSettings.themeColor);
const theme = useStorage<ThemeMode>(SETTINGS_KEYS.THEME, defaultSettings.theme);
// 系统设置 - 持久化
const grayMode = useStorage<boolean>(SETTINGS_KEYS.GRAY_MODE, defaultSettings.grayMode);
const userEnableAi = useStorage<boolean>(SETTINGS_KEYS.AI_ENABLED, defaultSettings.aiEnabled);
const pageSwitchingAnimation = useStorage<string>(
SETTINGS_KEYS.PAGE_SWITCHING_ANIMATION,
defaultSettings.pageSwitchingAnimation
);
const getMenuTheme = computed((): MenuThemeType => {
const list = AppConfig.themeList.filter((item) => item.theme === menuThemeType.value);
if (isDark.value) {
return AppConfig.darkMenuStyles[0];
} else {
return list[0];
}
});
const isDark = computed((): boolean => {
return systemThemeType.value === SystemThemeEnum.DARK;
});
const getMenuOpenWidth = computed((): string => {
return menuOpenWidth.value + "px" || SETTING_DEFAULT_CONFIG.menuOpenWidth + "px";
});
const getCustomRadius = computed((): string => {
return customRadius.value + "rem" || SETTING_DEFAULT_CONFIG.customRadius + "rem";
});
/** festivalDate 存「上次完成烟花播放」的自然日 YYYY-MM-DD,与今天相同则当天不再播 */
const isShowFireworks = computed((): boolean => {
const today = formatToDate(new Date());
return festivalDate.value !== today;
});
const settingsMap = {
showTagsView,
showAppLogo,
showWatermark,
showSettings,
showGuide,
showMenuSearch,
showFullscreen,
showSizeSelect,
showLangSelect,
showNotification,
sidebarColorScheme,
layout,
grayMode,
userEnableAi,
} as const;
watch(
[theme, themeColor],
([newTheme, newThemeColor]) => {
toggleDarkMode(newTheme === ThemeMode.DARK);
const colors = generateThemeColors(newThemeColor, newTheme);
applyTheme(colors);
},
{ immediate: true }
);
watch(
[sidebarColorScheme],
([newSidebarColorScheme]) => {
toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE);
},
{ immediate: true }
);
watch(
grayMode,
(v) => {
document.documentElement.style.filter = v ? "grayscale(100%)" : "";
},
{ immediate: true }
);
const switchMenuLayouts = (type: MenuTypeEnum) => {
menuType.value = type;
};
const setMenuOpenWidth = (width: number) => {
menuOpenWidth.value = width;
};
const setGlopTheme = (theme: SystemThemeEnum, themeMode: SystemThemeEnum) => {
systemThemeType.value = theme;
systemThemeMode.value = themeMode;
localStorage.setItem(StorageConfig.THEME_KEY, theme);
};
const switchMenuStyles = (theme: MenuThemeEnum) => {
menuThemeType.value = theme;
};
const setElementTheme = (theme: string) => {
systemThemeColor.value = theme;
setElementThemeColor(theme);
};
const setBorderMode = () => {
boxBorderMode.value = !boxBorderMode.value;
};
const setContainerWidth = (width: ContainerWidthEnum) => {
containerWidth.value = width;
};
const setUniqueOpened = () => {
uniqueOpened.value = !uniqueOpened.value;
};
const setButton = () => {
showMenuButton.value = !showMenuButton.value;
};
const setFastEnter = () => {
showFastEnter.value = !showFastEnter.value;
};
const setAutoClose = () => {
autoClose.value = !autoClose.value;
};
const setShowRefreshButton = () => {
showRefreshButton.value = !showRefreshButton.value;
};
const setCrumbs = () => {
showCrumbs.value = !showCrumbs.value;
};
const setWorkTab = (show: boolean) => {
showWorkTab.value = show;
};
const setLanguage = () => {
showLanguage.value = !showLanguage.value;
};
const setNprogress = () => {
showNprogress.value = !showNprogress.value;
};
const setColorWeak = () => {
colorWeak.value = !colorWeak.value;
};
const hideSettingGuide = () => {
showSettingGuide.value = false;
};
const openSettingGuide = () => {
showSettingGuide.value = true;
};
const setPageTransition = (transition: string) => {
pageTransition.value = transition;
};
const setTabStyle = (style: string) => {
tabStyle.value = style;
};
const setMenuOpen = (open: boolean) => {
menuOpen.value = open;
};
const reload = () => {
refresh.value = !refresh.value;
};
const setWatermarkVisible = (visible: boolean) => {
watermarkVisible.value = visible;
};
const setCustomRadius = (radius: string) => {
customRadius.value = radius;
document.documentElement.style.setProperty("--custom-radius", `${radius}rem`);
};
const setholidayFireworksLoaded = (isLoad: boolean) => {
holidayFireworksLoaded.value = isLoad;
};
const setShowFestivalText = (show: boolean) => {
showFestivalText.value = show;
};
const setFestivalDate = (date: string) => {
festivalDate.value = date;
};
const setDualMenuShowText = (show: boolean) => {
dualMenuShowText.value = show;
};
function updateSetting<K extends keyof typeof settingsMap>(
key: K,
value: boolean | string
): void {
const setting = settingsMap[key];
if (setting) {
(setting as Ref<any>).value = value;
}
}
function updateTheme(newTheme: ThemeMode): void {
theme.value = newTheme;
}
function updateThemeColor(newColor: string): void {
themeColor.value = newColor;
}
function updateSidebarColorScheme(newScheme: string): void {
sidebarColorScheme.value = newScheme;
}
function updateLayout(newLayout: LayoutMode): void {
layout.value = newLayout;
}
function toggleSettingsPanel(): void {
settingsVisible.value = !settingsVisible.value;
}
function showSettingsPanel(): void {
settingsVisible.value = true;
}
function hideSettingsPanel(): void {
settingsVisible.value = false;
}
function updateUserEnableAi(newValue: boolean): void {
userEnableAi.value = newValue;
}
function updateGrayMode(newValue: boolean): void {
grayMode.value = newValue;
}
function updatePageSwitchingAnimation(newValue: string): void {
pageSwitchingAnimation.value = newValue;
}
function resetSettings(): void {
// 界面显示设置
showTagsView.value = defaultSettings.showTagsView;
showAppLogo.value = defaultSettings.showAppLogo;
showWatermark.value = defaultSettings.showWatermark;
showSettings.value = defaultSettings.showSettings;
showGuide.value = defaultSettings.showGuide;
// 桌面端工具设置
showMenuSearch.value = defaultSettings.showMenuSearch;
showFullscreen.value = defaultSettings.showFullscreen;
showSizeSelect.value = defaultSettings.showSizeSelect;
showLangSelect.value = defaultSettings.showLangSelect;
showNotification.value = defaultSettings.showNotification;
// 布局和主题设置
sidebarColorScheme.value = defaultSettings.sidebarColorScheme;
layout.value = defaultSettings.layout as LayoutMode;
themeColor.value = defaultSettings.themeColor;
theme.value = defaultSettings.theme;
// 系统设置
grayMode.value = defaultSettings.grayMode;
userEnableAi.value = defaultSettings.aiEnabled;
pageSwitchingAnimation.value = defaultSettings.pageSwitchingAnimation;
}
// ==============================================
// 返回所有状态和方法
// ==============================================
return {
// 从 setting.ts 来的状态
menuType,
menuOpenWidth,
menuOpen,
dualMenuShowText,
systemThemeType,
systemThemeMode,
menuThemeType,
systemThemeColor,
showMenuButton,
showFastEnter,
showRefreshButton,
showCrumbs,
showWorkTab,
showLanguage,
showNprogress,
showSettingGuide,
showFestivalText,
watermarkVisible,
autoClose,
uniqueOpened,
colorWeak,
refresh,
holidayFireworksLoaded,
boxBorderMode,
pageTransition,
tabStyle,
customRadius,
containerWidth,
festivalDate,
// 从 settings.store.ts 来的状态
settingsVisible,
showTagsView,
showAppLogo,
showWatermark,
showSettings,
showGuide,
showMenuSearch,
showFullscreen,
showSizeSelect,
showLangSelect,
showNotification,
sidebarColorScheme,
layout,
themeColor,
theme,
grayMode,
userEnableAi,
pageSwitchingAnimation,
// 计算属性
getMenuTheme,
isDark,
getMenuOpenWidth,
getCustomRadius,
isShowFireworks,
// 从 setting.ts 来的方法
switchMenuLayouts,
setMenuOpenWidth,
setGlopTheme,
switchMenuStyles,
setElementTheme,
setBorderMode,
setContainerWidth,
setUniqueOpened,
setButton,
setFastEnter,
setAutoClose,
setShowRefreshButton,
setCrumbs,
setWorkTab,
setLanguage,
setNprogress,
setColorWeak,
hideSettingGuide,
openSettingGuide,
setPageTransition,
setTabStyle,
setMenuOpen,
reload,
setWatermarkVisible,
setCustomRadius,
setholidayFireworksLoaded,
setShowFestivalText,
setFestivalDate,
setDualMenuShowText,
// 从 settings.store.ts 来的方法
updateSetting,
updateTheme,
updateThemeColor,
updateSidebarColorScheme,
updateLayout,
toggleSettingsPanel,
showSettingsPanel,
hideSettingsPanel,
updateUserEnableAi,
updateGrayMode,
updatePageSwitchingAnimation,
resetSettings,
};
},
{
persist: {
key: "setting",
storage: localStorage,
},
}
);
@@ -1,303 +0,0 @@
import { defaultSettings } from "@/settings";
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
import type { LayoutMode } from "@/enums/settings/layout.enum";
import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
import { SETTINGS_KEYS } from "@/constants";
import { defineStore } from "pinia";
import { Ref, ref, watch } from "vue";
import { useStorage } from "@vueuse/core";
// 🎯 设置项类型定义
interface SettingsState {
// 界面显示设置
settingsVisible: boolean;
showTagsView: boolean;
showAppLogo: boolean;
showWatermark: boolean;
showSettings: boolean;
showGuide: boolean;
// 桌面端工具显示设置
showMenuSearch: boolean;
showFullscreen: boolean;
showSizeSelect: boolean;
showLangSelect: boolean;
showNotification: boolean;
// 布局设置
layout: LayoutMode;
sidebarColorScheme: string;
grayMode: boolean;
userEnableAi: boolean;
pageSwitchingAnimation: string;
// 主题设置
theme: ThemeMode;
themeColor: string;
}
// 🎯 可变更的设置项类型
type MutableSetting = Exclude<keyof SettingsState, "settingsVisible">;
type SettingValue<K extends MutableSetting> = SettingsState[K];
export const useSettingsStore = defineStore("setting", () => {
// 🎯 基础设置 - 非持久化
const settingsVisible = ref<boolean>(false);
// 🎯 界面显示设置 - 持久化
const showTagsView = useStorage<boolean>(
SETTINGS_KEYS.SHOW_TAGS_VIEW,
defaultSettings.showTagsView
);
const showAppLogo = useStorage<boolean>(SETTINGS_KEYS.SHOW_APP_LOGO, defaultSettings.showAppLogo);
// 是否显示水印
const showWatermark = useStorage<boolean>(
SETTINGS_KEYS.SHOW_WATERMARK,
defaultSettings.showWatermark
);
// 是否显示系统设置
const showSettings = useStorage<boolean>(
SETTINGS_KEYS.SHOW_SETTINGS,
defaultSettings.showSettings
);
// 是否显示引导功能
const showGuide = useStorage<boolean>(SETTINGS_KEYS.SHOW_GUIDE, defaultSettings.showGuide);
// 🎯 桌面端工具设置 - 持久化
const showMenuSearch = useStorage<boolean>(
SETTINGS_KEYS.SHOW_MENU_SEARCH,
defaultSettings.showMenuSearch
);
// 是否显示全屏切换
const showFullscreen = useStorage<boolean>(
SETTINGS_KEYS.SHOW_FULLSCREEN,
defaultSettings.showFullscreen
);
// 是否显示布局大小选择
const showSizeSelect = useStorage<boolean>(
SETTINGS_KEYS.SHOW_SIZE_SELECT,
defaultSettings.showSizeSelect
);
// 是否显示语言选择
const showLangSelect = useStorage<boolean>(
SETTINGS_KEYS.SHOW_LANG_SELECT,
defaultSettings.showLangSelect
);
// 是否显示通知
const showNotification = useStorage<boolean>(
SETTINGS_KEYS.SHOW_NOTIFICATION,
defaultSettings.showNotification
);
// 🎯 布局和主题设置 - 持久化
const sidebarColorScheme = useStorage<string>(
SETTINGS_KEYS.SIDEBAR_COLOR_SCHEME,
defaultSettings.sidebarColorScheme
);
// 布局设置
const layout = useStorage<LayoutMode>(SETTINGS_KEYS.LAYOUT, defaultSettings.layout as LayoutMode);
// 主题颜色
const themeColor = useStorage<string>(SETTINGS_KEYS.THEME_COLOR, defaultSettings.themeColor);
// 主题模式
const theme = useStorage<ThemeMode>(SETTINGS_KEYS.THEME, defaultSettings.theme);
// 是否开启灰色模式
const grayMode = useStorage<boolean>(SETTINGS_KEYS.GRAY_MODE, defaultSettings.grayMode);
// 是否开启AI助手
const userEnableAi = useStorage<boolean>(SETTINGS_KEYS.AI_ENABLED, defaultSettings.aiEnabled);
// 页面切换动画
const pageSwitchingAnimation = useStorage<string>(
SETTINGS_KEYS.PAGE_SWITCHING_ANIMATION,
defaultSettings.pageSwitchingAnimation
);
// 🎯 设置项映射
const settingsMap = {
showTagsView,
showAppLogo,
showWatermark,
showSettings,
showGuide,
showMenuSearch,
showFullscreen,
showSizeSelect,
showLangSelect,
showNotification,
sidebarColorScheme,
layout,
grayMode,
userEnableAi,
} as const;
// 🎯 监听器 - 主题变化
watch(
[theme, themeColor],
([newTheme, newThemeColor]) => {
toggleDarkMode(newTheme === ThemeMode.DARK);
const colors = generateThemeColors(newThemeColor, newTheme);
applyTheme(colors);
},
{ immediate: true }
);
// 🎯 监听器 - 侧边栏配色方案变化
watch(
[sidebarColorScheme],
([newSidebarColorScheme]) => {
toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE);
},
{ immediate: true }
);
// 灰色模式监听
watch(
grayMode,
(v) => {
document.documentElement.style.filter = v ? "grayscale(100%)" : "";
},
{ immediate: true }
);
// 🎯 统一的设置更新方法 - 类型安全
function updateSetting<K extends keyof typeof settingsMap>(key: K, value: SettingValue<K>): void {
const setting = settingsMap[key];
if (setting) {
(setting as Ref<any>).value = value;
}
}
// 🎯 主题相关的专用更新方法
function updateTheme(newTheme: ThemeMode): void {
theme.value = newTheme;
}
// 更新主题颜色
function updateThemeColor(newColor: string): void {
themeColor.value = newColor;
}
// 更新侧边栏配色方案
function updateSidebarColorScheme(newScheme: string): void {
sidebarColorScheme.value = newScheme;
}
// 更新布局
function updateLayout(newLayout: LayoutMode): void {
layout.value = newLayout;
}
// 🎯 设置面板显示控制
function toggleSettingsPanel(): void {
settingsVisible.value = !settingsVisible.value;
}
// 显示设置面板
function showSettingsPanel(): void {
settingsVisible.value = true;
}
// 隐藏设置面板
function hideSettingsPanel(): void {
settingsVisible.value = false;
}
// 更新AI助手状态
function updateUserEnableAi(newValue: boolean): void {
userEnableAi.value = newValue;
}
// 更新灰色模式状态
function updateGrayMode(newValue: boolean): void {
grayMode.value = newValue;
}
// 更新页面切换动画
function updatePageSwitchingAnimation(newValue: string): void {
pageSwitchingAnimation.value = newValue;
}
// 🎯 批量重置设置
function resetSettings(): void {
// 界面显示设置
showTagsView.value = defaultSettings.showTagsView;
showAppLogo.value = defaultSettings.showAppLogo;
showWatermark.value = defaultSettings.showWatermark;
showSettings.value = defaultSettings.showSettings;
showGuide.value = defaultSettings.showGuide;
// 桌面端工具设置
showMenuSearch.value = defaultSettings.showMenuSearch;
showFullscreen.value = defaultSettings.showFullscreen;
showSizeSelect.value = defaultSettings.showSizeSelect;
showLangSelect.value = defaultSettings.showLangSelect;
showNotification.value = defaultSettings.showNotification;
// 布局和主题设置
sidebarColorScheme.value = defaultSettings.sidebarColorScheme;
layout.value = defaultSettings.layout as LayoutMode;
themeColor.value = defaultSettings.themeColor;
theme.value = defaultSettings.theme;
// 系统设置
grayMode.value = defaultSettings.grayMode;
userEnableAi.value = defaultSettings.aiEnabled;
pageSwitchingAnimation.value = defaultSettings.pageSwitchingAnimation;
}
return {
// 🎯 基础状态
settingsVisible,
// 🎯 界面显示状态
showTagsView,
showAppLogo,
showWatermark,
showSettings,
showGuide,
// 🎯 桌面端工具状态
showMenuSearch,
showFullscreen,
showSizeSelect,
showLangSelect,
showNotification,
// 🎯 布局和主题状态
sidebarColorScheme,
layout,
themeColor,
theme,
// 🎯 更新方法
updateSetting,
updateTheme,
updateThemeColor,
updateSidebarColorScheme,
updateLayout,
// 🎯 面板控制
toggleSettingsPanel,
showSettingsPanel,
hideSettingsPanel,
// 🎯 系统设置状态
grayMode,
userEnableAi,
pageSwitchingAnimation,
updatePageSwitchingAnimation,
// 🎯 更新系统设置方法
updateUserEnableAi,
updateGrayMode,
// 🎯 重置功能
resetSettings,
};
});
@@ -0,0 +1,104 @@
/**
* 表格状态管理模块
*
* 提供表格显示配置的状态管理
*
* ## 主要功能
*
* - 表格尺寸配置(紧凑、默认、宽松)
* - 斑马纹显示开关
* - 边框显示开关
* - 表头背景显示开关
* - 全屏模式开关
* - 表格行拖拽排序(仅当前页数据,需数据为可变数组)
*
* ## 使用场景
* - 表格组件样式配置
* - 用户表格偏好设置
* - 表格工具栏功能控制
*
* ## 持久化
*
* - 使用 localStorage 存储
* - 存储键:sys-v{version}-table
* - 用户配置跨页面保持
*
* @module store/modules/table.store
* @author FastapiAdmin Team
*/
import { defineStore } from "pinia";
import { ref } from "vue";
import { TableSizeEnum } from "@/enums/formEnum";
// 表格
export const useTableStore = defineStore(
"tableStore",
() => {
// 表格大小
const tableSize = ref(TableSizeEnum.DEFAULT);
// 斑马纹
const isZebra = ref(false);
// 边框
const isBorder = ref(false);
// 表头背景
const isHeaderBackground = ref(false);
// 是否全屏
const isFullScreen = ref(false);
// 是否开启表格行拖拽排序(工具栏开关,与 ArtTable + VueDraggable 配合)
const isRowDrag = ref(false);
/**
* 设置表格大小
* @param size 表格大小枚举值
*/
const setTableSize = (size: TableSizeEnum) => (tableSize.value = size);
/**
* 设置斑马纹显示状态
* @param value 是否显示斑马纹
*/
const setIsZebra = (value: boolean) => (isZebra.value = value);
/**
* 设置表格边框显示状态
* @param value 是否显示边框
*/
const setIsBorder = (value: boolean) => (isBorder.value = value);
/**
* 设置表头背景显示状态
* @param value 是否显示表头背景
*/
const setIsHeaderBackground = (value: boolean) => (isHeaderBackground.value = value);
/**
* 设置是否全屏
* @param value 是否全屏
*/
const setIsFullScreen = (value: boolean) => (isFullScreen.value = value);
const setIsRowDrag = (value: boolean) => (isRowDrag.value = value);
return {
tableSize,
isZebra,
isBorder,
isHeaderBackground,
setTableSize,
setIsZebra,
setIsBorder,
setIsHeaderBackground,
isFullScreen,
setIsFullScreen,
isRowDrag,
setIsRowDrag,
};
},
{
persist: {
key: "table",
storage: localStorage,
},
}
);
+283 -232
View File
@@ -1,262 +1,313 @@
/**
* 标签页状态管理模块
*
* 提供标签页的状态管理和缓存机制
*
* ## 主要功能
*
* - 已访问视图列表管理
* - 缓存视图列表管理
* - 添加、删除、更新标签页
* - 关闭当前标签页并跳转
* - 关闭其他标签页/关闭左侧/关闭右侧
* - 固定标签页支持
*
* ## 使用场景
*
* - 多标签页导航
* - 页面缓存管理
* - 标签页右键菜单操作
* - 路由切换时的标签状态同步
*
* ## 核心特性
*
* - 支持固定标签(affix)
* - 支持keepAlive缓存
* - 关闭标签时自动跳转上一个标签
* - 区分已访问视图和缓存视图
*
* @module store/modules/tags-view.store
* @author FastapiAdmin Team
*/
import { defineStore } from "pinia";
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { store } from "@stores";
// tags缓存
export const useTagsViewStore = defineStore("tagsView", () => {
const visitedViews = ref<TagView[]>([]);
const cachedViews = ref<string[]>([]);
const router = useRouter();
const route = useRoute();
export const useTagsViewStore = defineStore(
"tagsViewStore",
() => {
const visitedViews = ref<TagView[]>([]);
const cachedViews = ref<string[]>([]);
const router = useRouter();
const route = useRoute();
/**
* 添加已访问视图到已访问视图列表中
*/
function addVisitedView(view: TagView) {
// 如果已经存在于已访问的视图列表中或者是重定向地址,则不再添加
if (view.path.startsWith("/redirect")) {
return;
}
if (visitedViews.value.some((v) => v.name === view.name)) {
return;
}
// 如果视图是固定的(affix),则在已访问的视图列表的开头添加
if (view.affix) {
visitedViews.value.unshift(view);
} else {
// 如果视图不是固定的,则在已访问的视图列表的末尾添加
visitedViews.value.push(view);
}
}
/**
* 添加缓存视图到缓存视图列表中
*/
function addCachedView(view: TagView) {
const viewName = view.name;
// 如果缓存视图名称已经存在于缓存视图列表中,则不再添加
if (cachedViews.value.includes(viewName)) {
return;
/**
* 添加已访问视图到已访问视图列表中
*/
function addVisitedView(view: TagView) {
// 如果已经存在于已访问的视图列表中或者是重定向地址,则不再添加
if (view.path.startsWith("/redirect")) {
return;
}
if (visitedViews.value.some((v) => v.name === view.name)) {
return;
}
// 如果视图是固定的(affix),则在已访问的视图列表的开头添加
if (view.affix) {
visitedViews.value.unshift(view);
} else {
// 如果视图不是固定的,则在已访问的视图列表的末尾添加
visitedViews.value.push(view);
}
}
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
if (view.keepAlive) {
cachedViews.value.push(viewName);
}
}
/**
* 添加缓存视图到缓存视图列表中
*/
function addCachedView(view: TagView) {
const viewName = view.name;
// 如果缓存视图名称已经存在于缓存视图列表中,则不再添加
if (cachedViews.value.includes(viewName)) {
return;
}
/**
* 从已访问视图列表中删除指定的视图
*/
function delVisitedView(view: TagView) {
return new Promise((resolve) => {
for (const [i, v] of visitedViews.value.entries()) {
// 找到与指定视图路径匹配的视图,在已访问视图列表中删除该视图
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
if (view.keepAlive) {
cachedViews.value.push(viewName);
}
}
/**
* 从已访问视图列表中删除指定的视图
*/
function delVisitedView(view: TagView) {
return new Promise((resolve) => {
for (const [i, v] of visitedViews.value.entries()) {
// 找到与指定视图路径匹配的视图,在已访问视图列表中删除该视图
if (v.path === view.path) {
visitedViews.value.splice(i, 1);
break;
}
}
resolve([...visitedViews.value]);
});
}
function delCachedView(view: TagView) {
const viewName = view.name;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
cachedViews.value.splice(index, 1);
}
resolve([...cachedViews.value]);
});
}
function delOtherVisitedViews(view: TagView) {
return new Promise((resolve) => {
visitedViews.value = visitedViews.value.filter((v) => {
return v?.affix || v.path === view.path;
});
resolve([...visitedViews.value]);
});
}
function delOtherCachedViews(view: TagView) {
const viewName = view.name as string;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
cachedViews.value = cachedViews.value.slice(index, index + 1);
} else {
// if index = -1, there is no cached tags
cachedViews.value = [];
}
resolve([...cachedViews.value]);
});
}
function updateVisitedView(view: TagView) {
for (let v of visitedViews.value) {
if (v.path === view.path) {
visitedViews.value.splice(i, 1);
v = Object.assign(v, view);
break;
}
}
resolve([...visitedViews.value]);
});
}
}
function delCachedView(view: TagView) {
const viewName = view.name;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
cachedViews.value.splice(index, 1);
}
resolve([...cachedViews.value]);
});
}
function delOtherVisitedViews(view: TagView) {
return new Promise((resolve) => {
visitedViews.value = visitedViews.value.filter((v) => {
return v?.affix || v.path === view.path;
function addView(view: TagView) {
addVisitedView(view);
addCachedView(view);
}
function delView(view: TagView) {
return new Promise((resolve) => {
delVisitedView(view);
delCachedView(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
resolve([...visitedViews.value]);
});
}
}
function delOtherCachedViews(view: TagView) {
const viewName = view.name as string;
return new Promise((resolve) => {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
cachedViews.value = cachedViews.value.slice(index, index + 1);
} else {
// if index = -1, there is no cached tags
function delOtherViews(view: TagView) {
return new Promise((resolve) => {
delOtherVisitedViews(view);
delOtherCachedViews(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function delLeftViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
if (index >= currIndex || item?.affix) {
return true;
}
const cacheIndex = cachedViews.value.indexOf(item.name);
if (cacheIndex > -1) {
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
function delRightViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
if (index <= currIndex || item?.affix) {
return true;
}
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
function delAllViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
visitedViews.value = affixTags;
cachedViews.value = [];
}
resolve([...cachedViews.value]);
});
}
function updateVisitedView(view: TagView) {
for (let v of visitedViews.value) {
if (v.path === view.path) {
v = Object.assign(v, view);
break;
}
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
}
function addView(view: TagView) {
addVisitedView(view);
addCachedView(view);
}
function delView(view: TagView) {
return new Promise((resolve) => {
delVisitedView(view);
delCachedView(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
function delAllVisitedViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
visitedViews.value = affixTags;
resolve([...visitedViews.value]);
});
});
}
}
function delOtherViews(view: TagView) {
return new Promise((resolve) => {
delOtherVisitedViews(view);
delOtherCachedViews(view);
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
function delAllCachedViews() {
return new Promise((resolve) => {
cachedViews.value = [];
resolve([...cachedViews.value]);
});
});
}
}
function delLeftViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
if (index >= currIndex || item?.affix) {
return true;
}
const cacheIndex = cachedViews.value.indexOf(item.name);
if (cacheIndex > -1) {
cachedViews.value.splice(cacheIndex, 1);
}
return false;
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
function delRightViews(view: TagView) {
return new Promise((resolve) => {
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
if (currIndex === -1) {
return;
}
visitedViews.value = visitedViews.value.filter((item, index) => {
if (index <= currIndex || item?.affix) {
return true;
/**
* 关闭当前tagView
*/
function closeCurrentView() {
const tags: TagView = {
name: route.name as string,
title: route.meta.title as string,
path: route.path,
fullPath: route.fullPath,
affix: route.meta?.affix,
keepAlive: route.meta?.keepAlive,
query: route.query,
};
delView(tags).then((res: any) => {
if (isActive(tags)) {
toLastView(res.visitedViews, tags);
}
});
resolve({
visitedViews: [...visitedViews.value],
});
});
}
}
function delAllViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
visitedViews.value = affixTags;
cachedViews.value = [];
resolve({
visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value],
});
});
}
function isActive(tag: TagView) {
return tag.path === route.path;
}
function delAllVisitedViews() {
return new Promise((resolve) => {
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
visitedViews.value = affixTags;
resolve([...visitedViews.value]);
});
}
function delAllCachedViews() {
return new Promise((resolve) => {
cachedViews.value = [];
resolve([...cachedViews.value]);
});
}
/**
* 关闭当前tagView
*/
function closeCurrentView() {
const tags: TagView = {
name: route.name as string,
title: route.meta.title as string,
path: route.path,
fullPath: route.fullPath,
affix: route.meta?.affix,
keepAlive: route.meta?.keepAlive,
query: route.query,
};
delView(tags).then((res: any) => {
if (isActive(tags)) {
toLastView(res.visitedViews, tags);
}
});
}
function isActive(tag: TagView) {
return tag.path === route.path;
}
function toLastView(visitedViews: TagView[], view?: TagView) {
const latestView = visitedViews.slice(-1)[0];
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath);
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view?.name === "Dashboard") {
// to reload home page
router.replace("/redirect" + view.fullPath);
function toLastView(visitedViews: TagView[], view?: TagView) {
const latestView = visitedViews.slice(-1)[0];
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath);
} else {
router.push("/");
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
const vn = view?.name != null ? String(view.name) : "";
if (
vn === "Home" ||
vn === "Workplace" ||
vn === "Dashboard" ||
vn.startsWith("Dashboard")
) {
// to reload home page
if (view?.fullPath) {
router.replace("/redirect" + view.fullPath);
} else {
router.push("/");
}
} else {
router.push("/");
}
}
}
}
return {
visitedViews,
cachedViews,
addVisitedView,
addCachedView,
delVisitedView,
delCachedView,
delOtherVisitedViews,
delOtherCachedViews,
updateVisitedView,
addView,
delView,
delOtherViews,
delLeftViews,
delRightViews,
delAllViews,
delAllVisitedViews,
delAllCachedViews,
closeCurrentView,
isActive,
toLastView,
};
});
return {
visitedViews,
cachedViews,
addVisitedView,
addCachedView,
delVisitedView,
delCachedView,
delOtherVisitedViews,
delOtherCachedViews,
updateVisitedView,
addView,
delView,
delOtherViews,
delLeftViews,
delRightViews,
delAllViews,
delAllVisitedViews,
delAllCachedViews,
closeCurrentView,
isActive,
toLastView,
};
},
{
persist: true,
}
);
export function useTagsViewStoreHook() {
return useTagsViewStore(store);
}
+352 -120
View File
@@ -1,106 +1,239 @@
import { store, useTagsViewStore, usePermissionStoreHook, useDictStoreHook } from "@/store";
import AuthAPI, { type LoginFormData } from "@/api/module_system/auth";
import UserAPI, { type UserInfo } from "@/api/module_system/user";
import type { MenuTable } from "@/api/module_system/menu";
import { Auth } from "@/utils/auth";
import { ResultEnum } from "@/enums";
/** User store. */
import { defineStore } from "pinia";
import { ref } from "vue";
import { ref, computed } from "vue";
import { LanguageEnum } from "@/enums/appEnum";
import { router } from "@/router";
import { useSettingsStore } from "./setting.store";
import { useWorktabStore } from "./worktab.store";
import { useMenuStore } from "./menu.store";
import { AppRouteRecord } from "@/types/router";
import { setPageTitle } from "@utils/navigation";
import { resetRouterState, resetRouteInitState } from "@/router/beforeEach";
import { StorageConfig } from "@utils/storage";
import AuthAPI from "@/api/module_system/auth";
import UserAPI from "@/api/module_system/user";
import type { MenuTable } from "@/api/module_system/menu";
import { Auth } from "@utils/auth";
import { ResultEnum } from "@/enums/api/result.enum";
import { ElNotification } from "element-plus";
import { store, useTagsViewStore, useDictStore } from "@stores";
import type { UserInfo } from "@/api/module_system/user";
export const useUserStore = defineStore("user", {
state: () => ({
basicInfo: {} as UserInfo,
routeList: [] as MenuTable[],
prems: [] as string[],
hasGetRoute: false,
/** {@link useUserStore} 的 `logout` 可选参数 */
export interface LogoutOptions {
/**
* 为 false 时仅清理状态,不跳转登录(由调用方自行 next/router,如锁屏页 replace)
* @default true
*/
navigate?: boolean;
}
/**
* 用户状态管理
* 管理用户登录状态、个人信息、语言设置、搜索历史、锁屏状态等
*/
export const useUserStore = defineStore(
"userStore",
() => {
// 语言设置
const language = ref(LanguageEnum.ZH);
// 登录状态
const isLogin = ref(false);
// 锁屏状态
const isLock = ref(false);
// 锁屏密码
const lockPassword = ref("");
// 用户信息
const info = ref<Partial<UserInfo>>({});
// 搜索历史记录
const searchHistory = ref<AppRouteRecord[]>([]);
// 访问令牌
const accessToken = ref("");
// 刷新令牌
const refreshToken = ref("");
// 路由列表
const routeList = ref<MenuTable[]>([]);
// 权限列表
const prems = ref<string[]>([]);
// 是否已获取路由
const hasGetRoute = ref(false);
// 记住我状态
rememberMe: ref(Auth.getRememberMe()),
}),
const rememberMe = ref(Auth.getRememberMe());
// 计算属性:基础用户信息(兼容 web 原有结构)
const basicInfo = computed(() => info.value as any);
// 计算属性:获取设置状态
const getSettingState = computed(() => useSettingsStore().$state);
// 计算属性:获取工作台状态
const getWorktabState = computed(() => useWorktabStore().$state);
// 计算属性:获取基础信息(兼容 web 项目)
const getBasicInfo = computed(() => info.value as any);
// 计算属性:获取路由列表
const getRouteList = computed(() => routeList.value);
// 计算属性:获取权限列表
const getPerms = computed(() => prems.value);
// 计算属性:是否已获取路由
const getHasGetRoute = computed(() => hasGetRoute.value);
getters: {
getBasicInfo: (state) => state.basicInfo,
getRouteList: (state) => state.routeList,
getPerms: (state) => state.prems,
getHasGetRoute: (state) => state.hasGetRoute,
},
actions: {
// 获取用户信息
async getUserInfo() {
const response = await UserAPI.getCurrentUserInfo();
const routers: MenuTable[] = response.data.data.menus || [];
delete response.data.data.menus;
this.basicInfo = { ...this.basicInfo, ...response.data.data };
// 然后设置路由信息
this.setRoute(routers);
},
// 设置用户信息
setUserInfo(info: UserInfo) {
this.basicInfo = info;
/**
* 设置用户信息
* @param newInfo 新的用户信息
*/
const setUserInfo = (newInfo: UserInfo | UserInfo) => {
info.value = newInfo;
// 设置用户信息后自动更新权限
this.setPermissions([]);
},
setPermissions([]);
};
// 设置路由
setRoute(routers: MenuTable[]) {
this.routeList = routers;
this.hasGetRoute = true;
// 设置路由后自动更新权限
this.setPermissions(routers);
},
/**
* 设置登录状态
* @param status 登录状态
*/
const setLoginStatus = (status: boolean) => {
isLogin.value = status;
};
setPermissions(menus: MenuTable[]) {
this.prems = []; // 先清空现有权限
/**
* 设置语言
* @param lang 语言枚举值
*/
const setLanguage = (lang: LanguageEnum) => {
setPageTitle(router.currentRoute.value);
language.value = lang;
};
// 确保 basicInfo.roles 存在,避免空值错误
if (!this.basicInfo.roles) {
/**
* 设置搜索历史
* @param list 搜索历史列表
*/
const setSearchHistory = (list: AppRouteRecord[]) => {
searchHistory.value = list;
};
/**
* 设置锁屏状态
* @param status 锁屏状态
*/
const setLockStatus = (status: boolean) => {
isLock.value = status;
};
/**
* 设置锁屏密码
* @param password 锁屏密码
*/
const setLockPassword = (password: string) => {
lockPassword.value = password;
};
/**
* 设置令牌
* @param newAccessToken 访问令牌
* @param newRefreshToken 刷新令牌(可选)
*/
const setToken = (newAccessToken: string, newRefreshToken?: string) => {
accessToken.value = newAccessToken;
if (newRefreshToken) {
refreshToken.value = newRefreshToken;
}
};
/**
* 检查并清理工作台标签页
* 如果不是同一用户登录,清空工作台标签页
* 应在登录成功后调用
*/
const checkAndClearWorktabs = () => {
const lastUserId = localStorage.getItem(StorageConfig.LAST_USER_ID_KEY);
const currentUserId = (info.value as any).id || (info.value as any).user_id;
// 无法获取当前用户 ID,跳过检查
if (!currentUserId) return;
// 首次登录或缓存已清除,保留现有标签页
if (!lastUserId) {
return;
}
// 合并所有角色的菜单列表,使用数组扁平化方法简化代码并过滤undefined
const roleMenus = this.basicInfo.roles
// 不同用户登录,清空工作台标签页
if (String(currentUserId) !== lastUserId) {
const worktabStore = useWorktabStore();
worktabStore.opened = [];
worktabStore.keepAliveExclude = [];
}
// 清除临时存储
localStorage.removeItem(StorageConfig.LAST_USER_ID_KEY);
};
/**
* 获取用户信息
*/
async function getUserInfo() {
try {
const response = await UserAPI.getCurrentUserInfo();
const data = response.data.data;
const menus: MenuTable[] = data?.menus || [];
delete data?.menus;
info.value = { ...info.value, ...data } as Partial<UserInfo>;
setRoute(menus);
} catch (error) {
console.error("获取用户信息失败:", error);
throw error;
}
}
/**
* 设置头像
*/
function setAvatar(avatar: string) {
info.value = { ...info.value, avatar };
}
/**
* 设置路由
*/
function setRoute(routers: MenuTable[]) {
routeList.value = routers;
hasGetRoute.value = true;
setPermissions(routers);
}
/**
* 设置权限
*/
function setPermissions(menus: MenuTable[]) {
prems.value = [];
if (!info.value.roles) return;
const roleMenus = info.value.roles
.filter((role) => role.menus && role.menus.length > 0)
.flatMap((role) => role.menus)
.filter((menu): menu is MenuTable => menu !== undefined);
// 合并传入的菜单和角色菜单
const allMenus = [...menus, ...roleMenus];
const permissionSet = new Set<string>();
const collect = (items: MenuTable[]) => {
items.forEach((item) => {
// 收集当前菜单的权限
if (item.permission) {
permissionSet.add(item.permission);
}
// 递归收集子菜单的权限,确保子菜单不包含undefined
if (item.children && item.children.length > 0) {
collect(item.children.filter((child): child is MenuTable => child !== undefined));
}
});
};
// 收集所有权限
collect(allMenus);
this.prems = Array.from(permissionSet);
},
setAvatar(avatar: string) {
this.basicInfo = { ...this.basicInfo, avatar };
},
clearUserInfo() {
this.basicInfo = {};
this.routeList = [];
this.hasGetRoute = false;
},
prems.value = Array.from(permissionSet);
}
// 登录
async login(LoginFormData: LoginFormData) {
const response = await AuthAPI.login(LoginFormData);
/**
* 登录
*/
async function login(loginForm: any) {
const response = await AuthAPI.login(loginForm);
const data = response.data.data;
if (response.data.code === ResultEnum.SUCCESS) {
ElNotification({
title: "通知",
@@ -108,76 +241,175 @@ export const useUserStore = defineStore("user", {
type: "success",
});
}
this.rememberMe = LoginFormData.remember;
Auth.setTokens(
response.data.data.access_token,
response.data.data.refresh_token,
this.rememberMe
);
},
rememberMe.value = loginForm.remember;
// 清除上次会话里「动态路由初始化失败」标记,避免重新登录后侧栏/菜单不注册
resetRouteInitState();
Auth.setTokens(data.access_token, data.refresh_token, rememberMe.value);
setToken(data.access_token, data.refresh_token);
setLoginStatus(true);
await getUserInfo();
}
// 登出
async logout() {
const response = await AuthAPI.logout({ token: Auth.getAccessToken() });
if (response.data.code === ResultEnum.SUCCESS) {
ElNotification({
title: "通知",
message: response.data.msg,
type: "success",
/**
* 登出:有 token 时请求后端;统一清理状态;默认跳转登录页并带 redirect
*/
async function logout(options?: LogoutOptions) {
const shouldNavigate = options?.navigate !== false;
const currentUserId = (info.value as any).id || (info.value as any).user_id;
if (currentUserId) {
localStorage.setItem(StorageConfig.LAST_USER_ID_KEY, String(currentUserId));
}
let apiError: unknown;
const token = Auth.getAccessToken();
if (token) {
try {
const response = await AuthAPI.logout({ token });
if (response.data.code === ResultEnum.SUCCESS) {
ElNotification({
title: "通知",
message: response.data.msg,
type: "success",
});
}
} catch (e) {
apiError = e;
}
}
resetAllState();
sessionStorage.removeItem("iframeRoutes");
useMenuStore().setHomePath("");
resetRouterState(500);
if (shouldNavigate) {
const currentRoute = router.currentRoute.value;
const redirect = currentRoute.path !== "/login" ? currentRoute.fullPath : undefined;
await router.push({
name: "Login",
query: redirect ? { redirect } : undefined,
});
}
this.resetAllState();
},
// 重置所有状态
resetAllState() {
// 重置用户状态
if (apiError) {
throw apiError;
}
}
/**
* 重置所有状态
*/
function resetAllState() {
Auth.clearAuth();
// 重置用户信息
this.clearUserInfo();
// 重置路由
usePermissionStoreHook().resetRouter();
// 清除标签视图
useTagsViewStore().delAllViews();
// 重置字典
useDictStoreHook().clearDictData();
info.value = {};
routeList.value = [];
hasGetRoute.value = false;
isLogin.value = false;
isLock.value = false;
lockPassword.value = "";
accessToken.value = "";
refreshToken.value = "";
prems.value = [];
}
return Promise.resolve();
},
/**
* 清空用户信息
*/
function clearUserInfo() {
info.value = {};
routeList.value = [];
hasGetRoute.value = false;
}
// 刷新token
refreshToken() {
const refreshToken = Auth.getRefreshToken();
/**
* 刷新token
*/
function refreshTokenFn() {
const currentRefreshToken = Auth.getRefreshToken();
if (!refreshToken) {
if (!currentRefreshToken) {
return Promise.reject(new Error("没有有效的刷新令牌"));
}
return new Promise<void>((resolve, reject) => {
AuthAPI.refreshToken({ refresh_token: refreshToken })
.then((response) => {
AuthAPI.refreshToken({ refresh_token: currentRefreshToken })
.then((response: any) => {
const data = response.data.data;
// 更新令牌,保持当前记住我状态
Auth.setTokens(
response.data.data.access_token,
response.data.data.refresh_token,
Auth.getRememberMe()
);
Auth.setTokens(data.access_token, data.refresh_token, Auth.getRememberMe());
setToken(data.access_token, data.refresh_token);
resolve();
})
.catch((error) => {
.catch((error: any) => {
reject(error);
});
});
},
}
/**
* 完全重置所有状态(包括路由、标签页、字典等)
*/
function fullResetAllState() {
// 重置用户状态
Auth.clearAuth();
// 重置用户信息
clearUserInfo();
// 清除标签视图
useTagsViewStore().delAllViews();
// 重置字典
useDictStore(store).clearDictData();
return Promise.resolve();
}
return {
language,
isLogin,
isLock,
lockPassword,
info,
searchHistory,
accessToken,
routeList,
prems,
hasGetRoute,
rememberMe,
getUserInfo,
getSettingState,
getWorktabState,
basicInfo,
getBasicInfo,
getRouteList,
getPerms,
getHasGetRoute,
setUserInfo,
setLoginStatus,
setLanguage,
setSearchHistory,
setLockStatus,
setLockPassword,
setToken,
setAvatar,
setRoute,
setPermissions,
login,
logout,
checkAndClearWorktabs,
clearUserInfo,
refreshTokenFn,
resetAllState,
fullResetAllState,
};
},
{
persist: {
key: "user",
storage: localStorage,
},
}
);
persist: true,
});
/**
* 在组件外部使用UserStore的钩子函数
* @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html
*/
export function useUserStoreHook() {
return useUserStore(store);
}
+568
View File
@@ -0,0 +1,568 @@
/**
* 工作标签页状态管理模块
*
* 提供多标签页功能的完整状态管理
*
* ## 主要功能
*
* - 标签页打开和关闭
* - 标签页固定和取消固定
* - 批量关闭(左侧、右侧、其他、全部)
* - 标签页缓存管理(KeepAlive)
* - 标签页标题自定义
* - 标签页路由验证
* - 动态路由参数处理
*
* ## 使用场景
*
* - 多标签页导航
* - 页面缓存控制
* - 标签页右键菜单
* - 固定常用页面
* - 批量关闭标签
*
* ## 核心特性
*
* - 智能标签页复用(同路由名称复用)
* - 固定标签页保护(不可关闭)
* - KeepAlive 缓存排除管理
* - 路由有效性验证
* - 首页自动保留
*
* ## 持久化
* - 使用 localStorage 存储
* - 存储键:sys-v{version}-worktab
* - 刷新页面保持标签状态
*
* @module store/modules/worktab.store
* @author FastapiAdmin Team
*/
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { router } from "@/router";
import { LocationQueryRaw, Router } from "vue-router";
import { WorkTab } from "@/types";
import { useCommon } from "@/hooks/core/useCommon";
interface WorktabState {
current: Partial<WorkTab>;
opened: WorkTab[];
keepAliveExclude: string[];
}
/**
* 工作台标签页管理 Store
*/
export const useWorktabStore = defineStore(
"worktabStore",
() => {
// 状态定义
const current = ref<Partial<WorkTab>>({});
const opened = ref<WorkTab[]>([]);
const keepAliveExclude = ref<string[]>([]);
// 计算属性
const hasOpenedTabs = computed(() => opened.value.length > 0);
const hasMultipleTabs = computed(() => opened.value.length > 1);
const currentTabIndex = computed(() =>
current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1
);
/**
* 查找标签页索引
*/
const findTabIndex = (path: string): number => {
return opened.value.findIndex((tab) => tab.path === path);
};
/**
* 获取标签页
*/
const getTab = (path: string): WorkTab | undefined => {
return opened.value.find((tab) => tab.path === path);
};
/**
* 检查标签页是否可关闭
*/
const isTabClosable = (tab: WorkTab): boolean => {
return !tab.fixedTab;
};
/**
* 安全的路由跳转
*/
const safeRouterPush = (tab: Partial<WorkTab>): void => {
if (!tab.path) {
console.warn("尝试跳转到无效路径的标签页");
return;
}
try {
router.push({
path: tab.path,
query: tab.query as LocationQueryRaw,
});
} catch (error) {
console.error("路由跳转失败:", error);
}
};
/**
* 打开或激活一个选项卡
*/
const openTab = (tab: WorkTab): void => {
if (!tab.path) {
console.warn("尝试打开无效的标签页");
return;
}
// 从 keepAlive 排除列表中移除
if (tab.name) {
removeKeepAliveExclude(tab.name);
}
// 先根据路由名称查找(应对动态路由参数导致的多开问题),找不到再根据路径查找
let existingIndex = -1;
if (tab.name) {
existingIndex = opened.value.findIndex((t) => t.name === tab.name);
}
if (existingIndex === -1) {
existingIndex = findTabIndex(tab.path);
}
if (existingIndex === -1) {
// 新增标签页
const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length;
const newTab = { ...tab };
if (tab.fixedTab) {
opened.value.splice(insertIndex, 0, newTab);
} else {
opened.value.push(newTab);
}
current.value = newTab;
} else {
// 更新现有标签页(当动态路由参数或查询变更时,复用同一标签)
const existingTab = opened.value[existingIndex];
opened.value[existingIndex] = {
...existingTab,
path: tab.path,
params: tab.params,
query: tab.query,
title: tab.title || existingTab.title,
fixedTab: tab.fixedTab ?? existingTab.fixedTab,
keepAlive: tab.keepAlive ?? existingTab.keepAlive,
name: tab.name || existingTab.name,
icon: tab.icon || existingTab.icon,
};
current.value = opened.value[existingIndex];
}
};
/**
* 查找固定标签页的插入位置
*/
const findFixedTabInsertIndex = (): number => {
let insertIndex = 0;
for (let i = 0; i < opened.value.length; i++) {
if (opened.value[i].fixedTab) {
insertIndex = i + 1;
} else {
break;
}
}
return insertIndex;
};
/**
* 关闭指定的选项卡
*/
const removeTab = (path: string): void => {
const targetTab = getTab(path);
const targetIndex = findTabIndex(path);
if (targetIndex === -1) {
console.warn(`尝试关闭不存在的标签页: ${path}`);
return;
}
if (targetTab && !isTabClosable(targetTab)) {
console.warn(`尝试关闭固定标签页: ${path}`);
return;
}
// 从标签页列表中移除
opened.value.splice(targetIndex, 1);
// 处理缓存排除
if (targetTab?.name) {
addKeepAliveExclude(targetTab);
}
const { homePath } = useCommon();
// 如果关闭后无标签页,跳转首页
if (!hasOpenedTabs.value) {
if (path !== homePath.value) {
current.value = {};
safeRouterPush({ path: homePath.value });
}
return;
}
// 如果关闭的是当前激活标签,需要激活其他标签
if (current.value.path === path) {
const newIndex = targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex;
current.value = opened.value[newIndex];
safeRouterPush(current.value);
}
};
/**
* 关闭左侧选项卡
*/
const removeLeft = (path: string): void => {
const targetIndex = findTabIndex(path);
if (targetIndex === -1) {
console.warn(`尝试关闭左侧标签页,但目标标签页不存在: ${path}`);
return;
}
// 获取左侧可关闭的标签页
const leftTabs = opened.value.slice(0, targetIndex);
const closableLeftTabs = leftTabs.filter(isTabClosable);
if (closableLeftTabs.length === 0) {
console.warn("左侧没有可关闭的标签页");
return;
}
// 标记为缓存排除
markTabsToRemove(closableLeftTabs);
// 移除左侧可关闭的标签页
opened.value = opened.value.filter(
(tab, index) => index >= targetIndex || !isTabClosable(tab)
);
// 确保当前标签是激活状态
const targetTab = getTab(path);
if (targetTab) {
current.value = targetTab;
}
};
/**
* 关闭右侧选项卡
*/
const removeRight = (path: string): void => {
const targetIndex = findTabIndex(path);
if (targetIndex === -1) {
console.warn(`尝试关闭右侧标签页,但目标标签页不存在: ${path}`);
return;
}
// 获取右侧可关闭的标签页
const rightTabs = opened.value.slice(targetIndex + 1);
const closableRightTabs = rightTabs.filter(isTabClosable);
if (closableRightTabs.length === 0) {
console.warn("右侧没有可关闭的标签页");
return;
}
// 标记为缓存排除
markTabsToRemove(closableRightTabs);
// 移除右侧可关闭的标签页
opened.value = opened.value.filter(
(tab, index) => index <= targetIndex || !isTabClosable(tab)
);
// 确保当前标签是激活状态
const targetTab = getTab(path);
if (targetTab) {
current.value = targetTab;
}
};
/**
* 关闭其他选项卡
*/
const removeOthers = (path: string): void => {
const targetTab = getTab(path);
if (!targetTab) {
console.warn(`尝试关闭其他标签页,但目标标签页不存在: ${path}`);
return;
}
// 获取其他可关闭的标签页
const otherTabs = opened.value.filter((tab) => tab.path !== path);
const closableTabs = otherTabs.filter(isTabClosable);
if (closableTabs.length === 0) {
console.warn("没有其他可关闭的标签页");
return;
}
// 标记为缓存排除
markTabsToRemove(closableTabs);
// 只保留当前标签和固定标签
opened.value = opened.value.filter((tab) => tab.path === path || !isTabClosable(tab));
// 确保当前标签是激活状态
current.value = targetTab;
};
/**
* 关闭所有可关闭的标签页
*/
const removeAll = (): void => {
const { homePath } = useCommon();
const hasFixedTabs = opened.value.some((tab) => tab.fixedTab);
// 获取可关闭的标签页
const closableTabs = opened.value.filter((tab) => {
if (!isTabClosable(tab)) return false;
// 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页
return hasFixedTabs || tab.path !== homePath.value;
});
if (closableTabs.length === 0) {
console.warn("没有可关闭的标签页");
return;
}
// 标记为缓存排除
markTabsToRemove(closableTabs);
// 保留不可关闭的标签页和首页(当没有固定标签时)
opened.value = opened.value.filter((tab) => {
return !isTabClosable(tab) || (!hasFixedTabs && tab.path === homePath.value);
});
// 处理激活状态
if (!hasOpenedTabs.value) {
current.value = {};
safeRouterPush({ path: homePath.value });
return;
}
// 选择激活的标签页:优先首页,其次第一个可用标签
const homeTab = opened.value.find((tab) => tab.path === homePath.value);
const targetTab = homeTab || opened.value[0];
current.value = targetTab;
safeRouterPush(targetTab);
};
/**
* 将指定选项卡添加到 keepAlive 排除列表中
*/
const addKeepAliveExclude = (tab: WorkTab): void => {
if (!tab.keepAlive || !tab.name) return;
if (!keepAliveExclude.value.includes(tab.name)) {
keepAliveExclude.value.push(tab.name);
}
};
/**
* 从 keepAlive 排除列表中移除指定组件名称
*/
const removeKeepAliveExclude = (name: string): void => {
if (!name) return;
keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name);
};
/**
* 将传入的一组选项卡的组件名称标记为排除缓存
*/
const markTabsToRemove = (tabs: WorkTab[]): void => {
tabs.forEach((tab) => {
if (tab.name) {
addKeepAliveExclude(tab);
}
});
};
/**
* 切换指定标签页的固定状态
*/
const toggleFixedTab = (path: string): void => {
const targetIndex = findTabIndex(path);
if (targetIndex === -1) {
console.warn(`尝试切换不存在标签页的固定状态: ${path}`);
return;
}
const tab = { ...opened.value[targetIndex] };
tab.fixedTab = !tab.fixedTab;
// 移除原位置
opened.value.splice(targetIndex, 1);
if (tab.fixedTab) {
// 固定标签插入到所有固定标签的末尾
const firstNonFixedIndex = opened.value.findIndex((t) => !t.fixedTab);
const insertIndex = firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex;
opened.value.splice(insertIndex, 0, tab);
} else {
// 非固定标签插入到所有固定标签后
const fixedCount = opened.value.filter((t) => t.fixedTab).length;
opened.value.splice(fixedCount, 0, tab);
}
// 更新当前标签引用
if (current.value.path === path) {
current.value = tab;
}
};
/**
* 验证工作台标签页的路由有效性
*/
const validateWorktabs = (routerInstance: Router): void => {
try {
// 动态路由校验:优先使用路由 name 判断有效性;否则用 resolve 匹配参数化路径
const isTabRouteValid = (tab: Partial<WorkTab>): boolean => {
try {
if (tab.name) {
const routes = routerInstance.getRoutes();
if (routes.some((r) => r.name === tab.name)) return true;
}
if (tab.path) {
const resolved = routerInstance.resolve({
path: tab.path,
query: (tab.query as LocationQueryRaw) || undefined,
});
return resolved.matched.length > 0;
}
return false;
} catch {
return false;
}
};
// 过滤出有效的标签页
const validTabs = opened.value.filter((tab) => isTabRouteValid(tab));
if (validTabs.length !== opened.value.length) {
console.warn("发现无效的标签页路由,已自动清理");
opened.value = validTabs;
}
// 验证当前激活标签的有效性
const isCurrentValid = current.value && isTabRouteValid(current.value);
if (!isCurrentValid && validTabs.length > 0) {
console.warn("当前激活标签无效,已自动切换");
current.value = validTabs[0];
} else if (!isCurrentValid) {
current.value = {};
}
} catch (error) {
console.error("验证工作台标签页失败:", error);
}
};
/**
* 清空所有状态(用于登出等场景)
*/
const clearAll = (): void => {
current.value = {};
opened.value = [];
keepAliveExclude.value = [];
};
/**
* 获取状态快照(用于持久化存储)
*/
const getStateSnapshot = (): WorktabState => {
return {
current: { ...current.value },
opened: [...opened.value],
keepAliveExclude: [...keepAliveExclude.value],
};
};
/**
* 获取标签页标题
*/
const getTabTitle = (path: string): WorkTab | undefined => {
const tab = getTab(path);
return tab;
};
/**
* 更新标签页标题
*/
const updateTabTitle = (path: string, title: string): void => {
const tab = getTab(path);
if (tab) {
tab.customTitle = title;
}
};
/**
* 重置标签页标题
*/
const resetTabTitle = (path: string): void => {
const tab = getTab(path);
if (tab) {
tab.customTitle = "";
}
};
return {
// 状态
current,
opened,
keepAliveExclude,
// 计算属性
hasOpenedTabs,
hasMultipleTabs,
currentTabIndex,
// 方法
openTab,
removeTab,
removeLeft,
removeRight,
removeOthers,
removeAll,
toggleFixedTab,
validateWorktabs,
clearAll,
getStateSnapshot,
// 工具方法
findTabIndex,
getTab,
isTabClosable,
addKeepAliveExclude,
removeKeepAliveExclude,
markTabsToRemove,
getTabTitle,
updateTabTitle,
resetTabTitle,
};
},
{
persist: {
key: "worktab",
storage: localStorage,
},
}
);