All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传 purpose 只能使用 attachment。 Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。 Confidence: high Scope-risk: broad Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与 order_package_invalidate_task:create/detail 按钮权限。 Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。 Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
531 lines
14 KiB
TypeScript
531 lines
14 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { HOME_PAGE } from '@/router/routesAlias'
|
|
import { router } from '@/router'
|
|
import { LocationQueryRaw, Router } from 'vue-router'
|
|
import { WorkTab } from '@/types'
|
|
|
|
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 initializeAfterRefresh = (currentPath?: string): void => {
|
|
// 保留固定标签页
|
|
const fixedTabs = opened.value.filter((tab) => tab.fixedTab)
|
|
|
|
// 如果当前路径存在且不是固定标签页,则将其从已打开的标签页中移除
|
|
if (currentPath) {
|
|
const currentIsFixed = fixedTabs.some((tab) => tab.path === currentPath)
|
|
if (!currentIsFixed) {
|
|
// 移除所有非固定的标签页
|
|
opened.value = fixedTabs
|
|
current.value = {}
|
|
} else {
|
|
// 当前路径是固定标签页,保留所有固定标签页
|
|
opened.value = fixedTabs
|
|
current.value = fixedTabs.find((tab) => tab.path === currentPath) || {}
|
|
}
|
|
} else {
|
|
// 如果没有当前路径,只保留固定标签页
|
|
opened.value = fixedTabs
|
|
current.value = fixedTabs.length > 0 ? fixedTabs[0] : {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 查询对象比较
|
|
*/
|
|
const areQueriesEqual = (
|
|
query1: LocationQueryRaw | undefined,
|
|
query2: LocationQueryRaw | undefined
|
|
): boolean => {
|
|
if (!query1 && !query2) return true
|
|
if (!query1 || !query2) return false
|
|
return JSON.stringify(query1) === JSON.stringify(query2)
|
|
}
|
|
|
|
/**
|
|
* 查找标签页索引
|
|
*/
|
|
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)
|
|
}
|
|
|
|
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]
|
|
|
|
if (!areQueriesEqual(existingTab.query, tab.query)) {
|
|
opened.value[existingIndex] = {
|
|
...existingTab,
|
|
query: tab.query,
|
|
title: tab.title || existingTab.title
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 如果关闭后无标签页,跳转首页
|
|
if (!hasOpenedTabs.value) {
|
|
if (path !== HOME_PAGE) {
|
|
current.value = {}
|
|
safeRouterPush({ path: HOME_PAGE })
|
|
}
|
|
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 hasFixedTabs = opened.value.some((tab) => tab.fixedTab)
|
|
|
|
// 获取可关闭的标签页
|
|
const closableTabs = opened.value.filter((tab) => {
|
|
if (!isTabClosable(tab)) return false
|
|
// 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页
|
|
return hasFixedTabs || tab.path !== HOME_PAGE
|
|
})
|
|
|
|
if (closableTabs.length === 0) {
|
|
console.warn('没有可关闭的标签页')
|
|
return
|
|
}
|
|
|
|
// 标记为缓存排除
|
|
markTabsToRemove(closableTabs)
|
|
|
|
// 保留不可关闭的标签页和首页(当没有固定标签时)
|
|
opened.value = opened.value.filter((tab) => {
|
|
return !isTabClosable(tab) || (!hasFixedTabs && tab.path === HOME_PAGE)
|
|
})
|
|
|
|
// 处理激活状态
|
|
if (!hasOpenedTabs.value) {
|
|
current.value = {}
|
|
safeRouterPush({ path: HOME_PAGE })
|
|
return
|
|
}
|
|
|
|
// 选择激活的标签页:优先首页,其次第一个可用标签
|
|
const homeTab = opened.value.find((tab) => tab.path === HOME_PAGE)
|
|
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 {
|
|
const validPaths = new Set(routerInstance.getRoutes().map((route) => route.path))
|
|
|
|
// 过滤出有效的标签页
|
|
const validTabs = opened.value.filter((tab) => validPaths.has(tab.path))
|
|
|
|
if (validTabs.length !== opened.value.length) {
|
|
console.warn('发现无效的标签页路由,已自动清理')
|
|
opened.value = validTabs
|
|
}
|
|
|
|
// 验证当前激活标签的有效性
|
|
const isCurrentValid =
|
|
current.value.path && validTabs.some((tab) => tab.path === current.value.path)
|
|
|
|
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]
|
|
}
|
|
}
|
|
|
|
return {
|
|
// 状态
|
|
current,
|
|
opened,
|
|
keepAliveExclude,
|
|
|
|
// 计算属性
|
|
hasOpenedTabs,
|
|
hasMultipleTabs,
|
|
currentTabIndex,
|
|
|
|
// 方法
|
|
openTab,
|
|
removeTab,
|
|
removeLeft,
|
|
removeRight,
|
|
removeOthers,
|
|
removeAll,
|
|
toggleFixedTab,
|
|
validateWorktabs,
|
|
clearAll,
|
|
getStateSnapshot,
|
|
initializeAfterRefresh,
|
|
|
|
// 工具方法
|
|
findTabIndex,
|
|
getTab,
|
|
isTabClosable,
|
|
addKeepAliveExclude,
|
|
removeKeepAliveExclude,
|
|
markTabsToRemove
|
|
}
|
|
},
|
|
{
|
|
persist: {
|
|
key: 'worktab',
|
|
storage: localStorage,
|
|
pick: ['opened'],
|
|
// 自定义序列化,只保存固定标签页
|
|
serializer: {
|
|
serialize: (value: any) => {
|
|
const fixedTabs = value.opened?.filter((tab: WorkTab) => tab.fixedTab) || []
|
|
return JSON.stringify({
|
|
opened: fixedTabs,
|
|
keepAliveExclude: value.keepAliveExclude || []
|
|
})
|
|
},
|
|
deserialize: (value: string) => {
|
|
try {
|
|
const parsed = JSON.parse(value)
|
|
return {
|
|
opened: parsed.opened || [],
|
|
keepAliveExclude: parsed.keepAliveExclude || [],
|
|
current: {} // 刷新后不恢复当前标签状态
|
|
}
|
|
} catch {
|
|
return { opened: [], keepAliveExclude: [], current: {} }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|