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:
zlwuandCursor committed 2026-08-08 12:06:49 +08:00
1 parent 7898d65437
commit 2df2ef0808
17 files changed
+2242 -157

No files matched your search

+5 -1
View File
@@ -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
}
})
+7 -1
View File
@@ -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()
}
// 清除临时存储
+51 -8
View File
@@ -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,