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

+5 -2
View File
@@ -191,9 +191,13 @@
"189": "Community 189",
"190": "ViewKvTableCell.vue",
"191": "handleConfirmApproval",
"192": "handleDisburse",
"192": "index.vue",
"193": "navigation.ts",
"194": "canDeleteByCreator",
"195": "Community 195",
"196": "Community 196",
"197": "v2.4.2.1 - 2025-06-16",
"198": "index.vue",
"200": "index.js",
"201": "index.vue",
"202": "Community 202",
@@ -202,7 +206,6 @@
"208": "Community 208",
"209": "Community 209",
"210": "Community 210",
"211": "v2.4.2.1 - 2025-06-16",
"212": "index.js",
"213": "Community 213",
"214": "Community 214",
@@ -191,11 +191,8 @@
"189": "Community 189",
"190": "ViewKvTableCell.vue",
"191": "handleConfirmApproval",
"192": "handleDisburse",
"194": "handlePublish",
"195": "Community 195",
"196": "Community 196",
"199": "index.vue",
"200": "index.js",
"201": "index.vue",
"202": "Community 202",
@@ -204,7 +201,6 @@
"208": "Community 208",
"209": "Community 209",
"210": "Community 210",
"211": "v2.4.2.1 - 2025-06-16",
"212": "index.js",
"213": "Community 213",
"214": "Community 214",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -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;