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

@@ -1,4 +1,4 @@
<!-- 一体化门户顶栏:一级横排 + 大面板菜单(方案 A) -->
<!-- 一体化门户顶栏:品牌 + 用户(业务菜单改由首页启动器进入) -->
<template>
<header class="portal-header">
<div class="portal-header-brand" @click="toHome">
@@ -6,9 +6,7 @@
<p class="portal-header-title">{{ AppConfig.systemInfo.name }}</p>
</div>
<div class="portal-header-nav">
<ArtPortalMegaMenu />
</div>
<div class="portal-header-spacer" />
<div class="portal-header-end">
<div class="portal-header-actions">
@@ -34,7 +32,6 @@
import { useCommon } from '@/hooks/core/useCommon'
import { useHeaderBar } from '@/hooks/core/useHeaderBar'
import ArtUserMenu from '../art-header-bar/widget/ArtUserMenu.vue'
import ArtPortalMegaMenu from '../art-portal-mega-menu/index.vue'
defineOptions({ name: 'ArtPortalHeader' })
@@ -44,7 +41,7 @@
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
const toHome = (): void => {
router.push(homePath.value)
router.push(homePath.value || '/home')
}
const toggleFullScreen = (): void => {
@@ -76,7 +73,7 @@
align-items: center;
max-width: none;
padding-left: var(--mint-content-padding, 24px);
padding-right: 16px;
padding-right: 4px;
cursor: pointer;
}
@@ -94,19 +91,9 @@
white-space: nowrap;
}
.portal-header-nav {
position: absolute;
top: 0;
left: 50%;
z-index: 0;
display: flex;
align-items: stretch;
justify-content: center;
width: max-content;
max-width: min(720px, calc(100% - 520px));
height: 100%;
overflow: hidden;
transform: translateX(-50%);
.portal-header-spacer {
flex: 1 1 auto;
min-width: 0;
}
.portal-header-end {
@@ -115,7 +102,7 @@
flex-shrink: 0;
gap: 8px;
align-items: center;
margin-left: auto;
margin-left: 4px;
}
.portal-header-actions {
@@ -68,6 +68,7 @@
import { getPortalDefaultHomePath, getPortalDefaultPathForRoute } from '@/utils/router/portal-menu'
import { formatMenuTitle } from '@/utils/router'
import { useSettingStore } from '@/store/modules/setting'
import { MenuTypeEnum } from '@/enums/appEnum'
import { MenuItemType } from '../../others/art-menu-right/index.vue'
import { useCommon } from '@/hooks/core/useCommon'
import { WorkTab } from '@/types'
@@ -155,8 +156,9 @@
const menuItems = computed(() => {
const { clickedIndex, currentTab, isLastTab, isOneTab, isCurrentTab } = getClickedTabInfo()
const fixedStatus = checkTabsFixedStatus(clickedIndex)
const isPortal = settingStore.menuType === MenuTypeEnum.PORTAL
return [
const items = [
{
key: 'refresh',
label: t('worktab.btn.refresh'),
@@ -195,6 +197,9 @@
disabled: isOneTab || fixedStatus.areAllTabsFixed
}
]
// 门户:收藏与 Tab「固定」分离,隐藏固定项
return isPortal ? items.filter((item) => item.key !== 'fixed') : items
})
return { menuItems }
@@ -454,14 +459,23 @@
const { clickTab, closeWorktab, showMenu, handleSelect } =
useTabOperations(adjustPositionAfterClose)
const isOverflowing = (): boolean => {
const wrap = scrollRef.value
const ul = tabsRef.value
if (!wrap || !ul) return false
return ul.scrollWidth > wrap.clientWidth + 1
}
// 生命周期
onMounted(() => {
setupEventListeners()
autoPositionTab()
store.registerOverflowChecker(isOverflowing)
})
onUnmounted(() => {
cleanupEventListeners()
store.registerOverflowChecker(null)
})
// 监听器
@@ -482,6 +496,15 @@
})
}
)
watch(
() => store.opened.length,
async () => {
await nextTick()
store.evictUntilNoOverflow()
autoPositionTab()
}
)
</script>
<style scoped>