From 8eb0ad14e7780ca87e77badf0cfba2784317f876 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 9 Aug 2026 16:08:09 +0800 Subject: [PATCH] feat(bx-mobile): render home entries from portal-filtered menu catalog Co-authored-by: Cursor --- bx-mobile/src/views/home/HomePage.vue | 186 ++++++++------------------ 1 file changed, 54 insertions(+), 132 deletions(-) diff --git a/bx-mobile/src/views/home/HomePage.vue b/bx-mobile/src/views/home/HomePage.vue index ce81e9ff9..d09ffaf71 100644 --- a/bx-mobile/src/views/home/HomePage.vue +++ b/bx-mobile/src/views/home/HomePage.vue @@ -18,7 +18,7 @@ -
+
{{ todoItems.length }} @@ -29,7 +29,13 @@
{{ item.title }}
{{ item.subText }} · {{ item.timeText }}
- +
@@ -39,27 +45,27 @@
- +
-
{{ group.menuName }}
+
{{ group.menuName }}
@@ -70,7 +76,7 @@
-
+
@@ -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>({}) interface TodoItem { - id: number + id: string title: string subText: string statusText: string @@ -111,13 +117,12 @@ interface TodoItem { const todoItems = ref([]) 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() + 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 = { - '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 = { - '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 = { - '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 = { - '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); } -