Files
FastapiAdmin/frontend/web/src/store/modules/worktab.store.ts
T
zhangtao 0ce31936aa chore: 清理冗余代码与配置,优化项目结构
1.  删除无用文件与废弃代码:移除locale枚举、element-plus插件、sse路由、api token模块等
2.  简化类型导入与依赖:移除大量未使用的类型导入,统一echarts导入方式
3.  优化配置与样式:调整gitignore、样式引入顺序,新增列表动画样式
4.  修复接口与模型:修正接口返回类型、查询参数配置,更新部门模型字段
5.  优化性能与体验:添加图片懒加载,优化加载逻辑与表格渲染
6.  调整环境配置:新增并更新开发/生产环境配置文件
2026-07-23 20:49:52 +08:00

647 lines
19 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 工作标签页状态管理模块
*
* 提供多标签页功能的完整状态管理
*
* ## 主要功能
*
* - 标签页打开和关闭
* - 标签页固定和取消固定
* - 批量关闭(左侧、右侧、其他、全部)
* - 标签页缓存管理(KeepAlive)
* - 标签页标题自定义
* - 标签页路由验证
* - 动态路由参数处理
*
* ## 使用场景
*
* - 多标签页导航
* - 页面缓存控制
* - 标签页右键菜单
* - 固定常用页面
* - 批量关闭标签
*
* ## 核心特性
*
* - 标签以 path 标识;同一路由组件可对不同 path 多开
* - 固定标签页保护(不可关闭)
* - 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 { useCommon } from "@/hooks/core/useCommon";
import { ROUTE_PATH_LOGIN_ALT } from "@/router/staticRoutes";
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) {
if (import.meta.env.DEV) console.warn("尝试跳转到无效路径的标签页");
return;
}
try {
router.push({
path: tab.path,
query: tab.query as LocationQueryRaw,
});
} catch (error) {
if (import.meta.env.DEV) console.error("路由跳转失败:", error);
}
};
/**
* 批量关标签后:若当前 URL 已不在 opened 中,则跳到 `current` 对应标签(避免路由与工作栏脱节)。
*/
const ensureRouterMatchesOpenedTab = (): void => {
if (!opened.value.length) return;
const locPath = router.currentRoute.value.path;
if (opened.value.some((t) => t.path === locPath)) return;
const tab = current.value as Partial<WorkTab>;
if (tab.path) {
safeRouterPush(tab as WorkTab);
}
};
/**
* 打开或激活一个选项卡
*/
const openTab = (tab: WorkTab): void => {
if (!tab.path) {
if (import.meta.env.DEV) console.warn("尝试打开无效的标签页");
return;
}
// 从 keepAlive 排除列表中移除
if (tab.name) {
removeKeepAliveExclude(tab.name);
}
// 仅以 path 识别标签:同一 route_name(同一组件)可对不同 path 同时保留多条标签
const 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) {
if (import.meta.env.DEV) console.warn(`尝试关闭不存在的标签页: ${path}`);
return;
}
if (targetTab && !isTabClosable(targetTab)) {
if (import.meta.env.DEV) 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;
const nextTab = opened.value[newIndex];
if (!nextTab) return;
current.value = nextTab;
safeRouterPush(nextTab);
}
};
/**
* 关闭左侧选项卡
*/
const removeLeft = (path: string): void => {
const targetIndex = findTabIndex(path);
if (targetIndex === -1) {
if (import.meta.env.DEV) console.warn(`尝试关闭左侧标签页,但目标标签页不存在: ${path}`);
return;
}
// 获取左侧可关闭的标签页
const leftTabs = opened.value.slice(0, targetIndex);
const closableLeftTabs = leftTabs.filter(isTabClosable);
if (closableLeftTabs.length === 0) {
if (import.meta.env.DEV) 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;
}
ensureRouterMatchesOpenedTab();
};
/**
* 关闭右侧选项卡
*/
const removeRight = (path: string): void => {
const targetIndex = findTabIndex(path);
if (targetIndex === -1) {
if (import.meta.env.DEV) console.warn(`尝试关闭右侧标签页,但目标标签页不存在: ${path}`);
return;
}
// 获取右侧可关闭的标签页
const rightTabs = opened.value.slice(targetIndex + 1);
const closableRightTabs = rightTabs.filter(isTabClosable);
if (closableRightTabs.length === 0) {
if (import.meta.env.DEV) 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;
}
ensureRouterMatchesOpenedTab();
};
/**
* 关闭其他选项卡
*/
const removeOthers = (path: string): void => {
const targetTab = getTab(path);
if (!targetTab) {
if (import.meta.env.DEV) console.warn(`尝试关闭其他标签页,但目标标签页不存在: ${path}`);
return;
}
// 获取其他可关闭的标签页
const otherTabs = opened.value.filter((tab) => tab.path !== path);
const closableTabs = otherTabs.filter(isTabClosable);
if (closableTabs.length === 0) {
if (import.meta.env.DEV) console.warn("没有其他可关闭的标签页");
return;
}
// 标记为缓存排除
markTabsToRemove(closableTabs);
// 只保留当前标签和固定标签
opened.value = opened.value.filter((tab) => tab.path === path || !isTabClosable(tab));
// 确保当前标签是激活状态
current.value = targetTab;
ensureRouterMatchesOpenedTab();
};
/**
* 关闭所有可关闭的标签页
*/
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) {
if (import.meta.env.DEV) 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];
if (!targetTab) return;
current.value = targetTab;
safeRouterPush(targetTab as Partial<WorkTab>);
};
/**
* KeepAlive 的 exclude 按「组件名」匹配,会一次性清掉所有同名实例。
* 仅当 remaining 中已无任何同名标签时,才把该组件名加入 exclude(支持同 route_name 多 path 并存)。
*/
const pushExcludeIfLastSiblingOfName = (
name: string | undefined,
remaining: WorkTab[],
tab: Pick<WorkTab, "keepAlive">
): void => {
if (!name || tab.keepAlive === false) return;
if (remaining.some((t) => t.name === name)) return;
if (!keepAliveExclude.value.includes(name)) {
keepAliveExclude.value.push(name);
}
};
/**
* 将指定选项卡添加到 keepAlive 排除列表中。
* 须与布局「keepAlive !== false 即缓存」一致:仅用 `=== false` 跳过(undefined/true 均应排除),
* 否则关标签时无法剔除原先 meta 未显式写 keepAlive 的页的缓存。
*/
const addKeepAliveExclude = (tab: WorkTab): void => {
pushExcludeIfLastSiblingOfName(tab.name, opened.value, tab);
};
/**
* 从 keepAlive 排除列表中移除指定组件名称
*/
const removeKeepAliveExclude = (name: string): void => {
if (!name) return;
keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name);
};
/**
* 将传入的一组选项卡的组件名称标记为排除缓存
*/
const markTabsToRemove = (tabs: WorkTab[]): void => {
const removedPaths = new Set(tabs.map((t) => t.path));
const futureOpened = opened.value.filter((t) => !removedPaths.has(t.path));
for (const tab of tabs) {
if (tab.name) {
pushExcludeIfLastSiblingOfName(tab.name, futureOpened, tab);
}
}
};
/**
* 切换指定标签页的固定状态
*/
const toggleFixedTab = (path: string): void => {
const targetIndex = findTabIndex(path);
if (targetIndex === -1) {
if (import.meta.env.DEV) 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 {
const allRoutes = routerInstance.getRoutes(); // 只调用一次,避免在循环中重复拷贝
// 动态路由校验:优先使用路由 name 判断有效性;否则用 resolve 匹配参数化路径
const isTabRouteValid = (tab: Partial<WorkTab>): boolean => {
try {
if (tab.name) {
if (allRoutes.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 isLoginWorktab = (tab: Partial<WorkTab>): boolean =>
tab.name === "Login" || tab.path === "/login" || tab.path === ROUTE_PATH_LOGIN_ALT;
// 过滤出有效的标签页
const validTabs = opened.value.filter(
(tab) => isTabRouteValid(tab) && !isLoginWorktab(tab)
);
if (validTabs.length !== opened.value.length) {
if (import.meta.env.DEV) console.warn("发现无效的标签页路由,已自动清理");
const validPaths = new Set(validTabs.map((t) => t.path));
// 未走 removeTab 批量剔除的标签:须同步 exclude;同名多标签时仅最后一条删尽才 exclude
for (const tab of opened.value) {
if (!validPaths.has(tab.path) && tab.name && tab.keepAlive !== false) {
pushExcludeIfLastSiblingOfName(tab.name, validTabs, tab);
}
}
opened.value = validTabs;
}
// 验证当前激活标签的有效性(登录页不应作为当前工作台标签)
const isCurrentValid =
current.value && isTabRouteValid(current.value) && !isLoginWorktab(current.value);
if (!isCurrentValid && validTabs.length > 0) {
if (import.meta.env.DEV) console.warn("当前激活标签无效,已自动切换");
const firstValid = validTabs[0];
if (firstValid) current.value = firstValid;
} else if (!isCurrentValid) {
current.value = {};
}
} catch (error) {
if (import.meta.env.DEV) console.error("验证工作台标签页失败:", error);
}
};
/**
* 清空所有状态(用于登出等场景)
*
* 保留固定标签页(fixedTab: true),登出后重新登录时首页等常驻标签依然存在。
*/
const clearAll = (): void => {
const fixedTabs = opened.value.filter((tab) => tab.fixedTab);
if (fixedTabs.length > 0) {
opened.value = fixedTabs;
current.value = { ...fixedTabs[0] };
} else {
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 = "";
}
};
/**
* 关闭「多标签栏」时:visited 不写入 `opened`(与 KeepAlive include 策略一致),
* 但路由每次完成导航后应更新 `current`,避免持久化/内部状态与 `router` 真值脱节。
*/
const syncCurrentFromRoute = (tab: {
path: string;
name: string;
title: string;
icon?: string;
keepAlive: boolean;
params?: object;
query?: LocationQueryRaw;
}): void => {
current.value = {
path: tab.path,
name: tab.name,
title: tab.title,
icon: tab.icon,
keepAlive: tab.keepAlive,
params: tab.params,
query: tab.query,
};
};
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,
syncCurrentFromRoute,
};
},
{
persist: {
key: "worktab",
storage: localStorage,
},
}
);