Просмотр исходного кода

feat(bx-mobile): render home entries from portal-filtered menu catalog

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 месяцев назад
Родитель
Сommit
8eb0ad14e7
1 измененных файлов с 54 добавлено и 132 удалено
  1. 54 132
      bx-mobile/src/views/home/HomePage.vue

+ 54 - 132
bx-mobile/src/views/home/HomePage.vue

@@ -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
-}
-
-/** 仅包含有可展示子菜单卡片的一级分组 */
-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',
-}
+  return [...map.entries()].map(([menuName, items]) => ({
+    id: menuName,
+    menuName,
+    items,
+  }))
+})
 
-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>
-