|
|
@@ -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>
|
|
|
-
|