feat: add portal home launcher, favorites UI, and WorkTab overflow eviction
Replace top-bar module navigation with a card home page, persist favorites via API, reopen favorite tabs on cold login, and enable portal multi-tabs with overflow-based eviction. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
7898d65437
commit
2df2ef0808
17 files changed
+2242
-157
No files matched your search
@@ -55,7 +55,11 @@ export const useMenuStore = defineStore('menuStore', () => {
|
||||
*/
|
||||
const setMenuList = (list: AppRouteRecord[]) => {
|
||||
menuList.value = list
|
||||
setHomePath(HOME_PAGE_PATH || getPortalDefaultHomePath(list) || getFirstMenuPath(list))
|
||||
if (HOME_PAGE_PATH) {
|
||||
setHomePath(HOME_PAGE_PATH)
|
||||
} else {
|
||||
setHomePath(getPortalDefaultHomePath(list) || getFirstMenuPath(list))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
fetchAddMenuFavorite,
|
||||
fetchMenuFavorites,
|
||||
fetchRemoveMenuFavorite,
|
||||
type MenuFavoriteItem
|
||||
} from '@/api/menuFavorite'
|
||||
|
||||
const MAX = 10
|
||||
|
||||
export const useMenuFavoriteStore = defineStore('menuFavoriteStore', () => {
|
||||
const items = ref<MenuFavoriteItem[]>([])
|
||||
const loaded = ref(false)
|
||||
|
||||
const paths = computed(() => new Set(items.value.map((i) => i.path)))
|
||||
const isFavorite = (path: string) => paths.value.has(path)
|
||||
|
||||
async function loadFavorites() {
|
||||
const list = await fetchMenuFavorites()
|
||||
items.value = Array.isArray(list) ? list : []
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
async function removeFavorite(path: string) {
|
||||
await fetchRemoveMenuFavorite(path)
|
||||
items.value = items.value.filter((i) => i.path !== path)
|
||||
}
|
||||
|
||||
async function toggleFavorite(path: string) {
|
||||
if (!path) return
|
||||
if (isFavorite(path)) {
|
||||
await removeFavorite(path)
|
||||
return
|
||||
}
|
||||
if (items.value.length >= MAX) {
|
||||
ElMessage.warning(`最多收藏 ${MAX} 个菜单`)
|
||||
return
|
||||
}
|
||||
const row = await fetchAddMenuFavorite(path)
|
||||
items.value = [...items.value, row]
|
||||
}
|
||||
|
||||
function clear() {
|
||||
items.value = []
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
loaded,
|
||||
isFavorite,
|
||||
loadFavorites,
|
||||
toggleFavorite,
|
||||
removeFavorite,
|
||||
clear
|
||||
}
|
||||
})
|
||||
@@ -17,6 +17,8 @@ import { ref, computed } from 'vue'
|
||||
import { LanguageEnum } from '@/enums/appEnum'
|
||||
import { router } from '@/router'
|
||||
import { useWorktabStore } from './worktab'
|
||||
import { useMenuFavoriteStore } from './menuFavorite'
|
||||
import { clearFavoritePreopenFlag } from '@/utils/navigation/preopen-favorites'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { resetRouterState } from '@/router/guards/beforeEach'
|
||||
import { useMenuStore } from './menu'
|
||||
@@ -142,6 +144,8 @@ export const useUserStore = defineStore(
|
||||
// 清空主页路径
|
||||
useMenuStore().setHomePath('')
|
||||
useMenuBadgeStore().clearCounts()
|
||||
useMenuFavoriteStore().clear()
|
||||
clearFavoritePreopenFlag()
|
||||
// 重置路由状态
|
||||
resetRouterState(500)
|
||||
// 跳转到登录页,携带当前路由作为 redirect 参数
|
||||
@@ -173,11 +177,13 @@ export const useUserStore = defineStore(
|
||||
return
|
||||
}
|
||||
|
||||
// 不同用户登录,清空工作台标签页
|
||||
// 不同用户登录,清空工作台标签页与收藏预开标记
|
||||
if (String(currentUserId) !== lastUserId) {
|
||||
const worktabStore = useWorktabStore()
|
||||
worktabStore.opened = []
|
||||
worktabStore.keepAliveExclude = []
|
||||
useMenuFavoriteStore().clear()
|
||||
clearFavoritePreopenFlag()
|
||||
}
|
||||
|
||||
// 清除临时存储
|
||||
|
||||
@@ -40,15 +40,18 @@
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { router } from '@/router'
|
||||
import { HOME_PAGE_PATH, router } from '@/router'
|
||||
import { LocationQueryRaw, Router } from 'vue-router'
|
||||
import { WorkTab } from '@/types'
|
||||
import { MenuTypeEnum } from '@/enums/appEnum'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { isWorkbenchRoute } from '@/utils/navigation/route'
|
||||
import { getPortalDefaultHomePath, getPortalDefaultPathForRoute } from '@/utils/router/portal-menu'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
|
||||
export type OpenTabOptions = { activate?: boolean }
|
||||
|
||||
interface WorktabState {
|
||||
current: Partial<WorkTab>
|
||||
opened: WorkTab[]
|
||||
@@ -115,9 +118,18 @@ export const useWorktabStore = defineStore(
|
||||
/**
|
||||
* 标签全部关闭后的默认落点:当前一级模块首个二级菜单(无则回退首页路径)
|
||||
*/
|
||||
const isPortalMultiTabMode = (): boolean => {
|
||||
const setting = useSettingStore()
|
||||
return setting.menuType === MenuTypeEnum.PORTAL && setting.showWorkTab
|
||||
}
|
||||
|
||||
const resolveEmptyTabsPath = (): string => {
|
||||
const menuStore = useMenuStore()
|
||||
const { homePath } = useCommon()
|
||||
const home = homePath.value || HOME_PAGE_PATH || '/home'
|
||||
if (isPortalMultiTabMode()) {
|
||||
return home
|
||||
}
|
||||
const menuStore = useMenuStore()
|
||||
const currentPath = String(
|
||||
router.currentRoute.value.meta.activePath || router.currentRoute.value.path
|
||||
)
|
||||
@@ -129,13 +141,35 @@ export const useWorktabStore = defineStore(
|
||||
if (fromMenu) {
|
||||
return fromMenu
|
||||
}
|
||||
const home = homePath.value
|
||||
if (home && !isWorkbenchRoute(home)) {
|
||||
return home
|
||||
}
|
||||
return '/'
|
||||
}
|
||||
|
||||
const overflowCheckHandler = ref<null | (() => boolean)>(null)
|
||||
|
||||
const registerOverflowChecker = (fn: (() => boolean) | null) => {
|
||||
overflowCheckHandler.value = fn
|
||||
}
|
||||
|
||||
const evictUntilNoOverflow = (): void => {
|
||||
if (!isPortalMultiTabMode()) return
|
||||
const checker = overflowCheckHandler.value
|
||||
if (!checker) return
|
||||
let guard = 0
|
||||
while (checker() && guard++ < 50) {
|
||||
if (!evictOldestClosableTab()) break
|
||||
}
|
||||
if (current.value.path && findTabIndex(String(current.value.path)) === -1) {
|
||||
const fallback = opened.value[opened.value.length - 1]
|
||||
if (fallback?.path) {
|
||||
current.value = fallback
|
||||
safeRouterPush(fallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的路由跳转
|
||||
*/
|
||||
@@ -181,15 +215,18 @@ export const useWorktabStore = defineStore(
|
||||
|
||||
/**
|
||||
* 打开或激活一个选项卡
|
||||
* @param options.activate 默认 true;登录预开收藏时传 false,避免切走当前页
|
||||
*/
|
||||
const openTab = (tab: WorkTab): void => {
|
||||
const openTab = (tab: WorkTab, options: OpenTabOptions = {}): void => {
|
||||
if (!tab.path) {
|
||||
console.warn('尝试打开无效的标签页')
|
||||
return
|
||||
}
|
||||
if (shouldIgnoreTab(tab)) return
|
||||
|
||||
const activate = options.activate !== false
|
||||
const singlePage = isSinglePageMode()
|
||||
const portalMulti = isPortalMultiTabMode()
|
||||
|
||||
// 从 keepAlive 排除列表中移除
|
||||
if (tab.name) {
|
||||
@@ -215,8 +252,8 @@ export const useWorktabStore = defineStore(
|
||||
return
|
||||
}
|
||||
|
||||
// 新增标签页:超出上限时淘汰最早的可关闭标签
|
||||
if (!ensureCapacityForNewTab()) {
|
||||
// 非门户:保留固定数量上限;门户改由 DOM 溢出淘汰
|
||||
if (!portalMulti && !ensureCapacityForNewTab()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,7 +265,9 @@ export const useWorktabStore = defineStore(
|
||||
opened.value.push(newTab)
|
||||
}
|
||||
|
||||
current.value = newTab
|
||||
if (activate || !current.value.path) {
|
||||
current.value = newTab
|
||||
}
|
||||
} else {
|
||||
// 更新现有标签页(当动态路由参数或查询变更时,复用同一标签)
|
||||
const existingTab = opened.value[existingIndex]
|
||||
@@ -245,7 +284,9 @@ export const useWorktabStore = defineStore(
|
||||
icon: tab.icon || existingTab.icon
|
||||
}
|
||||
|
||||
current.value = opened.value[existingIndex]
|
||||
if (activate) {
|
||||
current.value = opened.value[existingIndex]
|
||||
}
|
||||
|
||||
if (singlePage) {
|
||||
enforceSinglePageTabs()
|
||||
@@ -673,6 +714,8 @@ export const useWorktabStore = defineStore(
|
||||
toggleFixedTab,
|
||||
validateWorktabs,
|
||||
enforceSinglePageTabs,
|
||||
registerOverflowChecker,
|
||||
evictUntilNoOverflow,
|
||||
clearAll,
|
||||
getStateSnapshot,
|
||||
|
||||
|
||||
Reference in new issue
Block a user