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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user