This commit is contained in:
zlwu committed 2026-08-09 19:42:32 +08:00
1 parent 47ef575524
commit 38383b35d9
92 files changed
+1051117 -37355

No files matched your search

@@ -7,6 +7,8 @@ export interface PortalHomeCardItem {
path: string
title: string
icon?: string
/** 与侧栏 meta.menuCode 一致,用于待办数字角标 */
menuCode?: string
}
export interface PortalHomeSection {
@@ -64,10 +66,12 @@ function buildPinyinMeta(title: string): { py: string; abbr: string; letter: str
function toCard(item: AppRouteRecord): PortalHomeCardItem | null {
const path = String(item.meta?.activePath || item.path || '')
if (!path) return null
const menuCode = String(item.meta?.menuCode || '').trim()
return {
path,
title: routeTitle(item),
icon: item.meta?.icon ? String(item.meta.icon) : undefined
icon: item.meta?.icon ? String(item.meta.icon) : undefined,
menuCode: menuCode || undefined
}
}
+137 -8
View File
@@ -51,6 +51,13 @@
class="portal-home-card portal-home-card--fav"
@click="onFavCardClick(card.path)"
>
<span
v-if="pendingCountFor(card) > 0"
class="portal-home-card__pending"
:aria-label="`${pendingCountFor(card)} 条待审批`"
>
{{ formatPendingCount(pendingCountFor(card)) }}
</span>
<span class="portal-home-card__badge portal-home-card__badge--fav" aria-hidden="true">
{{ titleAbbr(card.title) }}
</span>
@@ -80,6 +87,13 @@
class="portal-home-card"
@click="openCard(card.path)"
>
<span
v-if="pendingCountFor(card) > 0"
class="portal-home-card__pending"
:aria-label="`${pendingCountFor(card)} 条待审批`"
>
{{ formatPendingCount(pendingCountFor(card)) }}
</span>
<span
class="portal-home-card__badge"
:data-tone="toneIndex(activeModule)"
@@ -115,13 +129,19 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onActivated, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { Star, StarFilled } from '@element-plus/icons-vue'
import { VueDraggable } from 'vue-draggable-plus'
import { listQuote } from '@/api/crm/quote'
import { listContract } from '@/api/crm/contract'
import { MENU_BADGE_CODES, MENU_BADGE_CODE_SET } from '@/constants/menu-badge-codes'
import { useMenuStore } from '@/store/modules/menu'
import { useMenuBadgeStore } from '@/store/modules/menu-badge'
import { useMenuFavoriteStore } from '@/store/modules/menuFavorite'
import { useUserStore } from '@/store/modules/user'
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
import {
collectPortalHomeSections,
findHomeCardByPath,
@@ -131,17 +151,38 @@
defineOptions({ name: 'PortalHome' })
const FAV_KEY = '__favorites__'
/** CRM 报价/合同审批:侧栏角标体系尚未覆盖,首页单独拉取 */
const PENDING_CARD_RULES: Array<{
key: 'quote' | 'contract'
pathIncludes: string
titles: string[]
}> = [
{ key: 'quote', pathIncludes: 'quote-approval', titles: ['报价单审批'] },
{ key: 'contract', pathIncludes: 'contract-approval', titles: ['合同审批'] }
]
/** 标题兜底 → 侧栏 menuCode(与 menu-badge-registry 一致) */
const TITLE_BADGE_CODE: Record<string, string> = {
备用金审批: MENU_BADGE_CODES.BX_ADVANCE_APPROVE,
备用金发放: MENU_BADGE_CODES.BX_ADVANCE_ISSUE,
报销审批: MENU_BADGE_CODES.BX_REIMBURSE_APPROVE,
支付发放冲抵: MENU_BADGE_CODES.BX_PAYMENT,
待我审批: MENU_BADGE_CODES.BX_VIEW_TODO
}
const router = useRouter()
const menuStore = useMenuStore()
const badgeStore = useMenuBadgeStore()
const favStore = useMenuFavoriteStore()
const userStore = useUserStore()
const { menuList } = storeToRefs(menuStore)
const { items: favItems } = storeToRefs(favStore)
const { counts: badgeCounts, loaded: badgeLoaded } = storeToRefs(badgeStore)
const activeModule = ref(FAV_KEY)
const favoriteList = ref<PortalHomeCardItem[]>([])
const favDragging = ref(false)
const suppressFavClick = ref(false)
const pendingCounts = ref({ quote: 0, contract: 0 })
const modules = computed(() => collectPortalHomeSections(menuList.value || []))
@@ -207,6 +248,71 @@
)
}
const resolveCurrentUserId = (): string => {
const info = (userStore.info || {}) as { id?: string | number; userId?: string | number }
return String(info.id || info.userId || '')
}
const resolveBadgeMenuCode = (card: PortalHomeCardItem) => {
const code = String(card.menuCode || '').trim()
if (code && MENU_BADGE_CODE_SET.has(code)) return code
const title = String(card.title || '').trim()
const byTitle = TITLE_BADGE_CODE[title]
if (byTitle) return byTitle
return ''
}
const matchPendingRule = (card: PortalHomeCardItem) => {
const path = String(card.path || '')
const title = String(card.title || '').trim()
return PENDING_CARD_RULES.find(
(r) => path.includes(r.pathIncludes) || r.titles.includes(title)
)
}
const pendingCountFor = (card: PortalHomeCardItem) => {
// 依赖 badgeCounts / badgeLoaded,保证角标刷新后视图更新
void badgeCounts.value
void badgeLoaded.value
const badgeCode = resolveBadgeMenuCode(card)
if (badgeCode) return badgeStore.getCount(badgeCode)
const rule = matchPendingRule(card)
if (!rule) return 0
return Number(pendingCounts.value[rule.key] || 0)
}
const formatPendingCount = (n: number) => (n > 99 ? '99+' : String(n))
const loadCrmPendingCounts = async () => {
const userId = resolveCurrentUserId()
if (!userId) {
pendingCounts.value = { quote: 0, contract: 0 }
return
}
const [quoteRes, contractRes] = await Promise.all([
listQuote({
pageNum: 1,
pageSize: 1,
status: 'submitted',
pendingApprovalUserId: userId
}).catch(() => ({ total: 0 })),
listContract({
pageNum: 1,
pageSize: 1,
approvalStatus: 'pending',
pendingApprovalUserId: userId
}).catch(() => ({ total: 0 }))
])
pendingCounts.value = {
quote: Number(quoteRes.total || 0),
contract: Number(contractRes.total || 0)
}
}
const loadPendingCounts = async () => {
await Promise.all([refreshMenuBadges(menuList.value), loadCrmPendingCounts()])
}
watch(modules, ensureActiveModule, { immediate: true, deep: true })
watch([favItems, modules], syncFavoriteList, { immediate: true, deep: true })
@@ -214,6 +320,11 @@
if (!favStore.loaded) {
await favStore.loadFavorites()
}
await loadPendingCounts()
})
onActivated(() => {
void loadPendingCounts()
})
const openCard = (path: string) => {
@@ -391,10 +502,6 @@
&--fav {
margin-top: 14px;
padding: 12px 12px 14px;
background: linear-gradient(180deg, #ecfdf5 0%, #fff 72%);
border: 1px solid #bbf7d0;
border-radius: 10px;
}
}
@@ -434,7 +541,7 @@
.portal-home-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
gap: 10px;
gap: 16px;
margin-top: 10px;
}
@@ -444,7 +551,7 @@
gap: 10px;
align-items: center;
min-height: 64px;
padding: 12px 34px 12px 12px;
padding: 12px 34px 28px 12px;
text-align: left;
cursor: pointer;
background: #fff;
@@ -573,10 +680,32 @@
white-space: nowrap;
}
.portal-home-card__star {
.portal-home-card__pending {
position: absolute;
top: 6px;
right: 6px;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1;
color: #fff;
pointer-events: none;
background: #dc2626;
border-radius: 9px;
box-shadow: 0 1px 2px rgba(220, 38, 38, 0.25);
}
.portal-home-card__star {
position: absolute;
right: 6px;
bottom: 6px;
display: inline-flex;
align-items: center;
justify-content: center;