feat(bx-mobile): render home entries from portal-filtered menu catalog
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
a0eebfdbd3
commit
8eb0ad14e7
1 file changed
+54
-132
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Todo card(置于功能菜单上方) -->
|
||||
<div class="home-panels" style="margin-top:12px;display:flex;flex-direction:column;gap:10px">
|
||||
<div class="home-panels" style="margin-top: 12px; display: flex; flex-direction: column; gap: 10px">
|
||||
<div class="todo-card">
|
||||
<div v-if="todoItems.length" class="todo-head todo-head--badge-only">
|
||||
<span class="badge">{{ todoItems.length }}</span>
|
||||
@@ -29,7 +29,13 @@
|
||||
<div class="name">{{ item.title }}</div>
|
||||
<div class="meta">{{ item.subText }} · {{ item.timeText }}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="router.push(item.primaryRoute)">去处理</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="router.push(item.primaryRoute)"
|
||||
>
|
||||
去处理
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="empty-state empty-state--embed">
|
||||
@@ -39,27 +45,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu groups(无子项则不展示该分组;「首页」不展示一级标题,其子项仍显示) -->
|
||||
<!-- 门户 menuCode 过滤后的入口目录 -->
|
||||
<div v-for="group in groupsWithItems" :key="group.id">
|
||||
<div v-if="(group.menuName || '').trim() !== '首页'" class="section-title">{{ group.menuName }}</div>
|
||||
<div class="section-title">{{ group.menuName }}</div>
|
||||
<div class="menu-grid">
|
||||
<div
|
||||
v-for="item in getItems(group)"
|
||||
:key="item.id"
|
||||
v-for="item in group.items"
|
||||
:key="item.menuCode"
|
||||
class="menu-item"
|
||||
@click="navigateTo(item.menuCode)"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<div
|
||||
class="icon"
|
||||
:style="{
|
||||
background: getColor(item.menuCode) + '18',
|
||||
color: getColor(item.menuCode),
|
||||
background: item.color + '18',
|
||||
color: item.color,
|
||||
}"
|
||||
>
|
||||
<van-icon :name="getIcon(item.menuCode)" size="22" />
|
||||
<van-icon :name="item.icon" size="22" />
|
||||
</div>
|
||||
<span v-if="hasDot(item.menuCode)" class="msg-dot"></span>
|
||||
<div class="txt">{{ menuDisplayName(item) }}</div>
|
||||
<div class="txt">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +76,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 20px;"></div>
|
||||
<div style="height: 20px"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -79,13 +85,13 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showConfirmDialog } from 'vant'
|
||||
import { PRIMARY_CONFIRM_BUTTON_COLOR } from '@/constants/dialog'
|
||||
import { useAuthStore, type MenuItem } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { BxMenuEntry } from '@/constants/bxMenuCatalog'
|
||||
import { advanceApi, reimburseApi } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
/* ───── Reactive Stats ───── */
|
||||
const stats = ref({
|
||||
submittedThisMonth: 0,
|
||||
pendingCount: 0,
|
||||
@@ -97,7 +103,7 @@ const stats = ref({
|
||||
const pendingApprovalCounts = ref<Record<string, number>>({})
|
||||
|
||||
interface TodoItem {
|
||||
id: number
|
||||
id: string
|
||||
title: string
|
||||
subText: string
|
||||
statusText: string
|
||||
@@ -111,13 +117,12 @@ interface TodoItem {
|
||||
const todoItems = ref<TodoItem[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
if (!auth.menuList.length) {
|
||||
await auth.fetchMenus()
|
||||
if (!auth.user) {
|
||||
await auth.fetchUser()
|
||||
}
|
||||
await fetchHomeData()
|
||||
})
|
||||
|
||||
/* ───── Fetch & Compute Stats ───── */
|
||||
function pagedRows(res: any): any[] {
|
||||
const d = res?.data
|
||||
if (!d) return []
|
||||
@@ -138,11 +143,15 @@ async function fetchHomeData() {
|
||||
const approvedCount =
|
||||
Number(r.approvedCount ?? 0) + Number(a.approvedPassCount ?? 0) + Number(a.issuedCount ?? 0)
|
||||
const totalCount = Number(r.totalCount ?? 0) + Number(a.totalCount ?? 0)
|
||||
const totalAmount = 0
|
||||
|
||||
stats.value = { submittedThisMonth, pendingCount, approvedCount, totalAmount, totalCount }
|
||||
stats.value = {
|
||||
submittedThisMonth,
|
||||
pendingCount,
|
||||
approvedCount,
|
||||
totalAmount: 0,
|
||||
totalCount,
|
||||
}
|
||||
|
||||
// Fetch pending approvals for todo area & red dots
|
||||
const [pendingAdvanceRes, pendingReimburseRes] = await Promise.all([
|
||||
advanceApi.pendingApproval({ page: 1, pageSize: 5 }).catch(() => null),
|
||||
reimburseApi.pendingApproval({ page: 1, pageSize: 5 }).catch(() => null),
|
||||
@@ -154,24 +163,22 @@ async function fetchHomeData() {
|
||||
const pendingReimburseTotal = (pendingReimburseRes as any)?.data?.total || 0
|
||||
|
||||
pendingApprovalCounts.value = {
|
||||
'advance-approve': pendingAdvanceTotal,
|
||||
'reimburse-approve': pendingReimburseTotal,
|
||||
todo: pendingAdvanceTotal + pendingReimburseTotal,
|
||||
mine: pendingCount,
|
||||
'bx:advanceApprove:list': pendingAdvanceTotal,
|
||||
'bx:reimburseApprove:list': pendingReimburseTotal,
|
||||
'bx:viewTodo:list': pendingAdvanceTotal + pendingReimburseTotal,
|
||||
}
|
||||
|
||||
// Build todo items
|
||||
const items: TodoItem[] = []
|
||||
|
||||
for (const item of pendingAdvanceItems) {
|
||||
items.push({
|
||||
id: item.id,
|
||||
id: String(item.id),
|
||||
title: item.title || '备用金单待审批',
|
||||
subText: [item.title, item.applicantName, item.departmentName].filter(Boolean).join(' · '),
|
||||
statusText: '待处理',
|
||||
statusVariant: 'warning',
|
||||
amountText: `金额 ¥${Number(item.amount || 0).toFixed(2)}`,
|
||||
timeText: formatTime(item.createdAt),
|
||||
timeText: formatTime(item.createTime || item.createdAt),
|
||||
primaryRoute: '/approval',
|
||||
secondaryRoute: '/approval',
|
||||
})
|
||||
@@ -179,13 +186,13 @@ async function fetchHomeData() {
|
||||
|
||||
for (const item of pendingReimburseItems) {
|
||||
items.push({
|
||||
id: item.id,
|
||||
id: String(item.id),
|
||||
title: item.title || '报销申请待审批',
|
||||
subText: [item.title, item.applicantName, item.departmentName].filter(Boolean).join(' · '),
|
||||
statusText: '待处理',
|
||||
statusVariant: 'warning',
|
||||
amountText: `金额 ¥${Number(item.amount || 0).toFixed(2)}`,
|
||||
timeText: formatTime(item.createdAt),
|
||||
timeText: formatTime(item.createTime || item.createdAt),
|
||||
primaryRoute: '/approval',
|
||||
secondaryRoute: '/approval',
|
||||
})
|
||||
@@ -194,7 +201,7 @@ async function fetchHomeData() {
|
||||
items.sort((a, b) => b.timeText.localeCompare(a.timeText))
|
||||
todoItems.value = items.slice(0, 5)
|
||||
} catch {
|
||||
// Keep default empty state
|
||||
/* keep empty */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +218,6 @@ function formatTime(dateStr: string): string {
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${hh}:${mm}`
|
||||
}
|
||||
|
||||
/* ───── Computed for Template ───── */
|
||||
const greeting = computed(() => {
|
||||
const h = new Date().getHours()
|
||||
if (h < 6) return '凌晨好'
|
||||
@@ -221,109 +227,26 @@ const greeting = computed(() => {
|
||||
return '晚上好'
|
||||
})
|
||||
|
||||
/* ───── Menu Logic ───── */
|
||||
function getItems(group: MenuItem): MenuItem[] {
|
||||
if (!group.children?.length) return []
|
||||
const result: MenuItem[] = []
|
||||
for (const child of group.children) {
|
||||
if (child.children?.length) {
|
||||
result.push(...child.children.filter(c => c.menuCode))
|
||||
} else if (child.menuCode) {
|
||||
result.push(child)
|
||||
}
|
||||
const groupsWithItems = computed(() => {
|
||||
const map = new Map<string, BxMenuEntry[]>()
|
||||
for (const e of auth.visibleEntries) {
|
||||
const list = map.get(e.group) || []
|
||||
list.push(e)
|
||||
map.set(e.group, list)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return [...map.entries()].map(([menuName, items]) => ({
|
||||
id: menuName,
|
||||
menuName,
|
||||
items,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 仅包含有可展示子菜单卡片的一级分组 */
|
||||
const groupsWithItems = computed(() => (auth.menuList || []).filter((g) => getItems(g).length > 0))
|
||||
|
||||
/** 与列表顶栏命名统一;后端菜单名可能是旧文案(如「我的报销」)时覆盖展示 */
|
||||
const MOBILE_MENU_DISPLAY: Record<string, string> = {
|
||||
'reimburse-apply': '报销申请',
|
||||
'advance-issue': '备用金\n发放管理',
|
||||
payment: '发放/冲抵\n支付管理',
|
||||
}
|
||||
|
||||
function menuDisplayName(item: MenuItem): string {
|
||||
const code = item.menuCode || ''
|
||||
return MOBILE_MENU_DISPLAY[code] ?? item.menuName
|
||||
}
|
||||
|
||||
const ROUTE_MAP: Record<string, string> = {
|
||||
'advance-apply': '/advance/list',
|
||||
'advance-approve': '/advance/approve',
|
||||
'advance-issue': '/advance/issue',
|
||||
'reimburse-apply': '/reimburse/list',
|
||||
'reimburse-approve': '/reimburse/approve',
|
||||
payment: '/payment',
|
||||
mine: '/mine',
|
||||
todo: '/approval',
|
||||
all: '/all',
|
||||
'user-manage': '/admin/users',
|
||||
'role-manage': '/admin/roles',
|
||||
'menu-manage': '/admin/menus',
|
||||
project: '/admin/expenses',
|
||||
'wf-advance': '/admin/wf-advance',
|
||||
'wf-reimburse': '/admin/wf-reimburse',
|
||||
demo: '/advance/list',
|
||||
}
|
||||
|
||||
function navigateTo(code: string | null) {
|
||||
if (!code) return
|
||||
const path = ROUTE_MAP[code]
|
||||
function navigateTo(path: string) {
|
||||
if (path) router.push(path)
|
||||
}
|
||||
|
||||
const ICON_MAP: Record<string, string> = {
|
||||
'advance-apply': 'cash-back-record',
|
||||
'advance-approve': 'checked',
|
||||
'advance-issue': 'paid',
|
||||
'reimburse-apply': 'description',
|
||||
'reimburse-approve': 'todo-list-o',
|
||||
payment: 'balance-pay',
|
||||
mine: 'records',
|
||||
todo: 'clock-o',
|
||||
all: 'list-switch',
|
||||
'user-manage': 'manager-o',
|
||||
'role-manage': 'shield-o',
|
||||
'menu-manage': 'apps-o',
|
||||
project: 'label-o',
|
||||
'wf-advance': 'exchange',
|
||||
'wf-reimburse': 'exchange',
|
||||
demo: 'smile-o',
|
||||
}
|
||||
|
||||
function getIcon(code: string | null): string {
|
||||
return (code && ICON_MAP[code]) || 'bookmark-o'
|
||||
}
|
||||
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
'advance-apply': '#10b981',
|
||||
'advance-approve': '#3b82f6',
|
||||
'advance-issue': '#8b5cf6',
|
||||
'reimburse-apply': '#f59e0b',
|
||||
'reimburse-approve': '#ef4444',
|
||||
payment: '#06b6d4',
|
||||
mine: '#6366f1',
|
||||
todo: '#f97316',
|
||||
all: '#64748b',
|
||||
'user-manage': '#3b82f6',
|
||||
'role-manage': '#8b5cf6',
|
||||
'menu-manage': '#10b981',
|
||||
project: '#f59e0b',
|
||||
'wf-advance': '#06b6d4',
|
||||
'wf-reimburse': '#ec4899',
|
||||
demo: '#94a3b8',
|
||||
}
|
||||
|
||||
function getColor(code: string | null): string {
|
||||
return (code && COLOR_MAP[code]) || '#64748b'
|
||||
}
|
||||
|
||||
function hasDot(code: string | null): boolean {
|
||||
if (!code) return false
|
||||
return (pendingApprovalCounts.value[code] || 0) > 0
|
||||
function hasDot(menuCode: string): boolean {
|
||||
return (pendingApprovalCounts.value[menuCode] || 0) > 0
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -333,7 +256,7 @@ async function handleLogout() {
|
||||
message: '确定退出登录?',
|
||||
confirmButtonColor: PRIMARY_CONFIRM_BUTTON_COLOR,
|
||||
})
|
||||
auth.logout()
|
||||
await auth.logout()
|
||||
router.push('/login')
|
||||
} catch {
|
||||
/* cancelled */
|
||||
@@ -347,4 +270,3 @@ async function handleLogout() {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in new issue
Block a user