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
@@ -3,6 +3,7 @@
|
||||
<div v-if="isPortalLayout" class="app-layout app-layout-portal" :style="appLayoutStyle">
|
||||
<div id="app-portal-header">
|
||||
<ArtPortalHeader />
|
||||
<ArtWorkTab />
|
||||
</div>
|
||||
|
||||
<div id="app-portal-body">
|
||||
@@ -42,13 +43,16 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { MenuTypeEnum, MenuWidth } from '@/enums/appEnum'
|
||||
import { HOME_PAGE_PATH } from '@/router'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { useWorktabStore } from '@/store/modules/worktab'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
|
||||
defineOptions({ name: 'AppLayout' })
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const worktabStore = useWorktabStore()
|
||||
const { homePath } = useCommon()
|
||||
const { menuType, getMenuOpenWidth } = storeToRefs(settingStore)
|
||||
|
||||
const isPortalLayout = computed(() => menuType.value === MenuTypeEnum.PORTAL)
|
||||
@@ -68,9 +72,21 @@
|
||||
if (menuType.value !== MenuTypeEnum.PORTAL) {
|
||||
settingStore.switchMenuLayouts(MenuTypeEnum.PORTAL)
|
||||
}
|
||||
// 门户布局强制严格单页(忽略 localStorage 中历史 showWorkTab=true)
|
||||
settingStore.setWorkTab(false)
|
||||
worktabStore.enforceSinglePageTabs()
|
||||
// 门户:开启多页签;首页固定 Tab
|
||||
settingStore.setWorkTab(true)
|
||||
const home = homePath.value || HOME_PAGE_PATH || '/home'
|
||||
if (home && !worktabStore.opened.some((t) => t.path === home)) {
|
||||
worktabStore.openTab(
|
||||
{
|
||||
path: home,
|
||||
title: '首页',
|
||||
name: 'PortalHome',
|
||||
fixedTab: true,
|
||||
keepAlive: true
|
||||
},
|
||||
{ activate: false }
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="portal-home">
|
||||
<section v-if="favoriteCards.length" class="portal-home-section">
|
||||
<h2 class="title-bar-heading">收藏</h2>
|
||||
<div class="portal-home-grid">
|
||||
<button
|
||||
v-for="card in favoriteCards"
|
||||
:key="'fav-' + card.path"
|
||||
type="button"
|
||||
class="portal-home-card"
|
||||
@click="openCard(card.path)"
|
||||
>
|
||||
<span class="portal-home-card__title">{{ card.title }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="portal-home-card__star is-on"
|
||||
aria-label="取消收藏"
|
||||
@click.stop="onToggleStar(card.path)"
|
||||
>
|
||||
<ElIcon><StarFilled /></ElIcon>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-for="sec in sections" :key="sec.modulePath" class="portal-home-section">
|
||||
<h2 class="title-bar-heading">{{ sec.moduleTitle }}</h2>
|
||||
<div class="portal-home-grid">
|
||||
<button
|
||||
v-for="card in sec.cards"
|
||||
:key="card.path"
|
||||
type="button"
|
||||
class="portal-home-card"
|
||||
@click="openCard(card.path)"
|
||||
>
|
||||
<span class="portal-home-card__title">{{ card.title }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="portal-home-card__star"
|
||||
:class="{ 'is-on': favStore.isFavorite(card.path) }"
|
||||
:aria-label="favStore.isFavorite(card.path) ? '取消收藏' : '收藏'"
|
||||
@click.stop="onToggleStar(card.path)"
|
||||
>
|
||||
<ElIcon>
|
||||
<StarFilled v-if="favStore.isFavorite(card.path)" />
|
||||
<Star v-else />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="!sections.length" class="portal-home-empty">暂无可用菜单</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { Star, StarFilled } from '@element-plus/icons-vue'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { useMenuFavoriteStore } from '@/store/modules/menuFavorite'
|
||||
import {
|
||||
collectPortalHomeSections,
|
||||
findHomeCardByPath
|
||||
} from '@/utils/router/portal-home-cards'
|
||||
|
||||
defineOptions({ name: 'PortalHome' })
|
||||
|
||||
const router = useRouter()
|
||||
const menuStore = useMenuStore()
|
||||
const favStore = useMenuFavoriteStore()
|
||||
const { menuList } = storeToRefs(menuStore)
|
||||
|
||||
const sections = computed(() => collectPortalHomeSections(menuList.value || []))
|
||||
|
||||
const favoriteCards = computed(() =>
|
||||
favStore.items.map(
|
||||
(i) => findHomeCardByPath(sections.value, i.path) || { path: i.path, title: i.path }
|
||||
)
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!favStore.loaded) {
|
||||
await favStore.loadFavorites()
|
||||
}
|
||||
})
|
||||
|
||||
const openCard = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const onToggleStar = async (path: string) => {
|
||||
await favStore.toggleFavorite(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.portal-home {
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
.portal-home-section {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.portal-home-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.portal-home-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 72px;
|
||||
padding: 12px 36px 12px 14px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--mint-primary-bg, #ecfdf5);
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-home-card__title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text, #1e293b);
|
||||
}
|
||||
|
||||
.portal-home-card__star {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&.is-on {
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-home-empty {
|
||||
margin-top: 48px;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user