zlwu 5 meses atrás
pai
commit
32e64ac37c

+ 23 - 0
bx/bx-web/src/components/base/WorkflowNodeCell.vue

@@ -0,0 +1,23 @@
+<script setup lang="ts">
+import {
+  workflowNodeLabel,
+  workflowNodeTone,
+  shouldShowWorkflowNodeTag,
+} from '../../utils/format'
+import BaseTag from './BaseTag.vue'
+
+defineProps<{
+  currentNode?: unknown
+  status?: unknown
+}>()
+</script>
+
+<template>
+  <BaseTag
+    v-if="shouldShowWorkflowNodeTag(currentNode, status)"
+    :tone="workflowNodeTone(currentNode, status)"
+  >
+    {{ workflowNodeLabel(currentNode, status) }}
+  </BaseTag>
+  <span v-else class="node-current-empty">-</span>
+</template>

+ 17 - 0
bx/bx-web/src/components/shell/MenuNavLabel.vue

@@ -0,0 +1,17 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { getMenuPendingCount } from '../../hooks/useApproveMenuBadge'
+
+const props = defineProps<{
+  menuCode?: string | null
+  menuName: string
+}>()
+
+const pendingCount = computed(() => getMenuPendingCount(props.menuCode))
+</script>
+
+<template>
+  <span class="menu-link-text">
+    {{ menuName }}<span v-if="pendingCount > 0" class="menu-pending-count">({{ pendingCount }})</span>
+  </span>
+</template>

+ 60 - 0
bx/bx-web/src/hooks/useApproveMenuBadge.ts

@@ -0,0 +1,60 @@
+import { ref } from 'vue'
+import { advanceApi, reimburseApi } from '../api'
+import type { MenuItem } from '../stores/auth'
+
+export const advanceApproverStats = ref({ pendingTotal: 0, monthPendingAmount: 0 })
+export const reimburseApproverStats = ref({ pendingTotal: 0, monthPendingAmount: 0 })
+
+function collectMenuCodes(items: MenuItem[]): Set<string> {
+  const codes = new Set<string>()
+  function walk(list: MenuItem[]) {
+    for (const item of list) {
+      if (item.menuCode) codes.add(item.menuCode)
+      if (item.children?.length) walk(item.children)
+    }
+  }
+  walk(items)
+  return codes
+}
+
+export function getMenuPendingCount(menuCode?: string | null): number {
+  if (menuCode === 'advance-approve') return advanceApproverStats.value.pendingTotal
+  if (menuCode === 'reimburse-approve') return reimburseApproverStats.value.pendingTotal
+  return 0
+}
+
+/** 刷新侧栏「备用金审批 / 报销审批」待办数量(按用户菜单权限按需请求) */
+export async function refreshApproveMenuBadges(menuList: MenuItem[]) {
+  const codes = collectMenuCodes(menuList)
+  const tasks: Promise<void>[] = []
+
+  if (codes.has('advance-approve')) {
+    tasks.push(
+      advanceApi.pendingApprovalStats().then((res: any) => {
+        const d = res.data || {}
+        advanceApproverStats.value = {
+          pendingTotal: Number(d.pendingTotal ?? 0),
+          monthPendingAmount: Number(d.monthPendingAmount ?? 0),
+        }
+      })
+    )
+  } else {
+    advanceApproverStats.value = { pendingTotal: 0, monthPendingAmount: 0 }
+  }
+
+  if (codes.has('reimburse-approve')) {
+    tasks.push(
+      reimburseApi.pendingApprovalStats().then((res: any) => {
+        const d = res.data || {}
+        reimburseApproverStats.value = {
+          pendingTotal: Number(d.pendingTotal ?? 0),
+          monthPendingAmount: Number(d.monthPendingAmount ?? 0),
+        }
+      })
+    )
+  } else {
+    reimburseApproverStats.value = { pendingTotal: 0, monthPendingAmount: 0 }
+  }
+
+  await Promise.allSettled(tasks)
+}

+ 92 - 22
bx/bx-web/src/styles/index.css

@@ -304,6 +304,19 @@ body {
   white-space: nowrap;
 }
 
+.menu-pending-count {
+  flex-shrink: 0;
+  margin-left: 2px;
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--danger);
+  line-height: 1.2;
+}
+
+.menu-link.active .menu-pending-count {
+  color: var(--danger);
+}
+
 .menu-link:hover {
   background: var(--row-alt);
   color: var(--text);
@@ -761,6 +774,7 @@ body {
   border-bottom: 1px solid var(--border);
   font-size: 14px;
   font-weight: 600;
+  color: #000;
 }
 
 .table-scroll {
@@ -1016,6 +1030,7 @@ body {
 .panel :is(.el-table__footer-wrapper td, .el-table__fixed-footer-wrapper td) .cell {
   white-space: nowrap;
   text-align: center;
+  color: #000;
 }
 
 /* 合计行吸底固定(跟随列表滚动容器) */
@@ -1073,8 +1088,8 @@ body {
   --el-table-border-color: var(--border);
   --el-table-header-bg-color: var(--table-head);
   --el-table-row-hover-bg-color: var(--row-hover);
-  --el-table-text-color: var(--text);
-  --el-table-header-text-color: var(--text-muted);
+  --el-table-text-color: #000;
+  --el-table-header-text-color: #000;
   --el-table-bg-color: var(--surface);
   --el-table-tr-bg-color: var(--surface);
 }
@@ -1082,7 +1097,15 @@ body {
 .bx-table .el-table__header-wrapper th {
   font-size: 12px;
   font-weight: 600;
-  color: var(--text-muted);
+  color: #000;
+}
+
+.bx-table .el-table__header-wrapper th .cell {
+  color: #000;
+}
+
+.bx-table .el-table__body .el-table__cell .cell {
+  color: #000;
 }
 
 .bx-table .el-table__cell {
@@ -1109,13 +1132,38 @@ body {
   background: var(--surface) !important;
 }
 
-/* 列表:除金额列(col-money)外单元格居中;金额列右对齐 */
+/* 列表:默认单元格居中;金额列右对齐、显式左对齐列等覆盖 */
 .bx-table .el-table__cell .cell,
 .view-detail-table .el-table__cell .cell,
 .dlg-view .el-table .el-table__cell .cell,
 .bx-dialog .el-dialog__body .el-table .el-table__cell .cell,
-.form-section .el-table .el-table__cell .cell {
+.form-section .el-table .el-table__cell .cell,
+.panel .el-table .el-table__cell .cell {
   text-align: center;
+  color: #000;
+}
+
+.view-detail-table .el-table__header-wrapper th,
+.view-detail-table .el-table__header-wrapper th .cell,
+.dlg-view .el-table .el-table__header-wrapper th,
+.dlg-view .el-table .el-table__header-wrapper th .cell,
+.bx-dialog .el-dialog__body .el-table .el-table__header-wrapper th,
+.bx-dialog .el-dialog__body .el-table .el-table__header-wrapper th .cell,
+.form-section .el-table .el-table__header-wrapper th,
+.form-section .el-table .el-table__header-wrapper th .cell,
+.panel .el-table .el-table__header-wrapper th,
+.panel .el-table .el-table__header-wrapper th .cell {
+  color: #000;
+}
+
+.bx-table :is(.col-align-left) .cell,
+.view-detail-table :is(.col-align-left) .cell,
+.dlg-view .el-table :is(.col-align-left) .cell,
+.bx-dialog .el-dialog__body .el-table :is(.col-align-left) .cell,
+.form-section .el-table :is(.col-align-left) .cell,
+.bx-table th.col-align-left .cell,
+.view-detail-table th.col-align-left .cell {
+  text-align: left !important;
 }
 
 .bx-table .col-money .cell,
@@ -1139,6 +1187,17 @@ body {
   font-variant-numeric: tabular-nums;
 }
 
+/* 多选列:勾选框水平垂直居中 */
+.bx-table .el-table-column--selection .cell,
+.form-section .el-table .el-table-column--selection .cell,
+.bx-dialog .el-dialog__body .el-table .el-table-column--selection .cell {
+  display: flex !important;
+  justify-content: center;
+  align-items: center;
+  padding-left: 0;
+  padding-right: 0;
+}
+
 .bx-table .col-actions .cell {
   white-space: nowrap;
   display: inline-flex;
@@ -1148,9 +1207,9 @@ body {
   justify-content: flex-start;
 }
 
-/* 列表「当前节点」为空:无标签,占位与次要色 */
+/* 列表「当前节点」为空:无标签 */
 .node-current-empty {
-  color: var(--text-muted);
+  color: #000;
 }
 
 /* 列表合计:第 4 列常为「本页合计(n单)」加粗(备用金申请页除外,见下条) */
@@ -1159,7 +1218,7 @@ body {
   td:nth-child(4)
   .cell {
   font-weight: 600;
-  color: var(--text);
+  color: #000;
 }
 
 /* 备用金申请:合计行首格 colspan=4 合并多选+序号+单号+申请人 */
@@ -1168,7 +1227,7 @@ body {
   td:first-child
   .cell {
   font-weight: 600;
-  color: var(--text);
+  color: #000;
   text-align: center;
 }
 
@@ -1197,27 +1256,24 @@ body {
   font-size: 12px;
   font-weight: 600;
   background: #fff;
-  border: 1px solid transparent;
+  border: none;
   white-space: nowrap;
 }
 
-.bx-tag--pending { color: #475569; border-color: #cbd5e1; }
-.bx-tag--processing { color: #00a874; border-color: #66d9b3; }
-.bx-tag--done { color: #0f766e; border-color: #5eead4; }
-/* 审批中等:浅底表格上需可见描边,勿用近白/透明边 */
+.bx-tag--pending { color: #475569; }
+.bx-tag--processing { color: #00a874; }
+.bx-tag--done { color: #0f766e; }
 .bx-tag--warning {
   color: #b45309;
   background: #fffbeb;
-  border-color: #f59e0b;
 }
 /* 历史误用 tone=warn 时与 warning 对齐 */
 .bx-tag--warn {
   color: #b45309;
   background: #fffbeb;
-  border-color: #f59e0b;
 }
-.bx-tag--error { color: #dc2626; border-color: #fca5a5; }
-.bx-tag--info { color: #0f766e; border-color: #5eead4; }
+.bx-tag--error { color: #dc2626; }
+.bx-tag--info { color: #0f766e; }
 
 .bx-btn.el-button {
   height: var(--control-h);
@@ -1426,6 +1482,17 @@ body {
   font-weight: 700;
 }
 
+/* 弹窗内关键金额字段(实际发放、冲抵金额等) */
+.field-amount-danger > label {
+  color: var(--danger);
+  font-weight: 600;
+}
+
+.field-amount-danger .bx-input .el-input__inner {
+  color: var(--danger);
+  font-weight: 600;
+}
+
 .section-gap-tight {
   margin-top: 10px;
 }
@@ -1642,30 +1709,33 @@ body {
   font-weight: 600;
   line-height: 1.3;
   background: #fff;
+  border: none;
+  box-shadow: none;
+  --el-tag-border-color: transparent;
 }
 
 .el-tag.el-tag--success {
   color: #0f766e;
-  border-color: #5eead4;
   background: #fff;
+  border: none;
 }
 
 .el-tag.el-tag--warning {
   color: #d97706;
-  border-color: #f6c453;
   background: #fff;
+  border: none;
 }
 
 .el-tag.el-tag--danger {
   color: #dc2626;
-  border-color: #fca5a5;
   background: #fff;
+  border: none;
 }
 
 .el-tag.el-tag--info {
   color: #0f766e;
-  border-color: #5eead4;
   background: #fff;
+  border: none;
 }
 
 .el-alert {

+ 61 - 1
bx/bx-web/src/utils/format.ts

@@ -37,9 +37,26 @@ export function menuTypeLabel(type: number): string {
   return map[type] ?? String(type)
 }
 
+/** 金额展示:固定两位小数(500 → 500.00,500.1 → 500.10) */
+export function formatMoney(value: unknown): string {
+  const n = Number(value)
+  if (!Number.isFinite(n)) return '0.00'
+  return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+}
+
+/** 带 ¥ 前缀的金额展示 */
+export function formatMoneyYuan(value: unknown): string {
+  return `¥${formatMoney(value)}`
+}
+
 /** 列表合计行金额展示 */
 export function formatSumMoney(n: number): string {
-  return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+  return formatMoney(n)
+}
+
+/** el-table-column 金额列 formatter */
+export function moneyTableFormatter(_row: unknown, _column: unknown, cellValue: unknown): string {
+  return formatMoney(cellValue)
 }
 
 /** 列表「当前节点」是否有可展示文案(空/null/空白串视为无) */
@@ -47,3 +64,46 @@ export function hasWorkflowNode(value: unknown): boolean {
   if (value == null) return false
   return String(value).trim() !== ''
 }
+
+const DRAFT_STATUS_SET = new Set(['draft', '待提交', '未提交'])
+const COMPLETED_NODE_STATUS_SET = new Set([
+  'approved',
+  'issued',
+  'paid',
+  'offset',
+  'processed',
+  '审批通过',
+  '已发放',
+  '已冲抵',
+  '已处理',
+])
+
+/** 当前节点展示文案:无节点名时按单据状态回退(如未提交、已完成) */
+export function workflowNodeLabel(currentNode: unknown, status: unknown): string | null {
+  if (hasWorkflowNode(currentNode)) return String(currentNode).trim()
+  const s = String(status ?? '').trim()
+  if (!s) return null
+  if (DRAFT_STATUS_SET.has(s)) return '未提交'
+  if (COMPLETED_NODE_STATUS_SET.has(s)) return '已完成'
+  if (s === 'rejected' || s === '审批驳回') return '已驳回'
+  return null
+}
+
+/** 当前节点标签语义色 */
+export function workflowNodeTone(currentNode: unknown, status: unknown): string {
+  if (hasWorkflowNode(currentNode)) return statusTone(currentNode)
+  const label = workflowNodeLabel(currentNode, status)
+  if (label === '未提交') return 'info'
+  if (label === '已完成') return 'done'
+  if (label === '已驳回') return 'error'
+  return 'info'
+}
+
+export function shouldShowWorkflowNodeTag(currentNode: unknown, status: unknown): boolean {
+  return workflowNodeLabel(currentNode, status) != null
+}
+
+/** 详情键值区等纯文本展示 */
+export function workflowNodeDisplay(currentNode: unknown, status: unknown): string {
+  return workflowNodeLabel(currentNode, status) ?? '-'
+}

+ 27 - 4
bx/bx-web/src/views/MainLayout.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import type { Component } from 'vue'
-import { computed, onMounted, provide, ref } from 'vue'
+import { computed, onMounted, provide, ref, watch } from 'vue'
 import { useRouter } from 'vue-router'
 import {
   Bell,
@@ -31,6 +31,8 @@ import {
 } from '@element-plus/icons-vue'
 import { useAuthStore, type MenuItem } from '../stores/auth'
 import { openMenuTabKey } from '../shell/openMenuTabKey'
+import { refreshApproveMenuBadges } from '../hooks/useApproveMenuBadge'
+import MenuNavLabel from '../components/shell/MenuNavLabel.vue'
 import logoGif from '../assets/logo.gif'
 
 const router = useRouter()
@@ -253,10 +255,16 @@ function openContextMenu(event: MouseEvent, tabId: string) {
   contextMenuVisible.value = true
 }
 
+async function loadApproveMenuBadges() {
+  if (!auth.menuList.length) return
+  await refreshApproveMenuBadges(auth.menuList)
+}
+
 onMounted(async () => {
   if (auth.token && !auth.userInfo) {
     await auth.fetchUser()
   }
+  await loadApproveMenuBadges()
   if (!activeTab.value && auth.menuList.length) {
     const first = findFirstLeaf(auth.menuList)
     if (first) {
@@ -265,6 +273,21 @@ onMounted(async () => {
     }
   }
 })
+
+watch(
+  () => auth.menuList,
+  () => {
+    loadApproveMenuBadges()
+  },
+  { deep: true }
+)
+
+watch(activeTab, (tab, prev) => {
+  const approveTabs = ['advance-approve', 'reimburse-approve']
+  if (approveTabs.includes(tab) || (prev && approveTabs.includes(prev))) {
+    loadApproveMenuBadges()
+  }
+})
 </script>
 
 <template>
@@ -288,7 +311,7 @@ onMounted(async () => {
               <el-icon class="menu-link-ico">
                 <component :is="resolveMenuIcon(group.menuCode, group.menuName)" />
               </el-icon>
-              <span class="menu-link-text">{{ group.menuName }}</span>
+              <MenuNavLabel :menu-code="group.menuCode" :menu-name="group.menuName" />
             </a>
             <details v-else class="nav-details nav-details--l1" open>
               <summary class="nav-summary nav-summary--l1">
@@ -321,7 +344,7 @@ onMounted(async () => {
                         <el-icon class="menu-link-ico">
                           <component :is="resolveMenuIcon(leaf.menuCode, leaf.menuName)" />
                         </el-icon>
-                        <span class="menu-link-text">{{ leaf.menuName }}</span>
+                        <MenuNavLabel :menu-code="leaf.menuCode" :menu-name="leaf.menuName" />
                       </a>
                     </div>
                   </details>
@@ -334,7 +357,7 @@ onMounted(async () => {
                     <el-icon class="menu-link-ico">
                       <component :is="resolveMenuIcon(item.menuCode, item.menuName)" />
                     </el-icon>
-                    <span class="menu-link-text">{{ item.menuName }}</span>
+                    <MenuNavLabel :menu-code="item.menuCode" :menu-name="item.menuName" />
                   </a>
                 </template>
               </div>

+ 19 - 20
bx/bx-web/src/views/pages/AdvanceApplyPage.vue

@@ -3,7 +3,8 @@ import { reactive, ref, computed, onMounted, nextTick, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Plus, EditPen, Upload, View, Close, Delete } from '@element-plus/icons-vue'
 import { advanceApi, paymentApi } from '../../api'
-import { statusLabel, statusTone, formatSumMoney, hasWorkflowNode } from '../../utils/format'
+import { statusLabel, statusTone, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import { useServerPaged } from '../../hooks/useServerPaged'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
 import BaseButton from '../../components/base/BaseButton.vue'
@@ -61,9 +62,9 @@ const stats = computed(() => {
   const s = myAdvanceStats.value
   return [
     { name: '待审批申请', value: String(s.pendingCount) },
-    { name: '本月申请金额(元)', value: s.monthApplyAmount.toLocaleString('zh-CN') },
-    { name: '已发放金额(元)', value: s.issuedAmount.toLocaleString('zh-CN') },
-    { name: '累计已冲抵金额(元)', value: s.offsetTotal.toLocaleString('zh-CN') },
+    { name: '本月申请金额(元)', value: formatMoney(s.monthApplyAmount) },
+    { name: '已发放金额(元)', value: formatMoney(s.issuedAmount) },
+    { name: '累计已冲抵金额(元)', value: formatMoney(s.offsetTotal) },
     { name: '待发放申请', value: String(s.pendingIssueCount) },
   ]
 })
@@ -433,22 +434,23 @@ function summaryMethod(params: { data: any[] }) {
       <template #table-columns>
         <el-table-column type="selection" width="48" />
         <el-table-column type="index" label="序号" width="90" />
+        <el-table-column prop="applyTime" label="申请时间" min-width="160" />
         <el-table-column prop="applyCode" label="申请单号" min-width="140" />
         <el-table-column prop="applicantName" label="申请人" min-width="100" />
         <el-table-column prop="reason" label="申请原因" min-width="160" />
-        <el-table-column prop="amount" label="申请金额(元)" class-name="col-money" min-width="120" />
-        <el-table-column label="实际发放金额(元)" class-name="col-money" min-width="130">
+        <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
+        <el-table-column label="实际发放金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="130">
           <template #default="{ row }">
             {{
               row.actualAmount != null && row.actualAmount !== ''
-                ? Number(row.actualAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
+                ? formatMoney(row.actualAmount)
                 : '—'
             }}
           </template>
         </el-table-column>
-        <el-table-column label="冲抵总金额(元)" class-name="col-money" min-width="130">
+        <el-table-column label="冲抵总金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="130">
           <template #default="{ row }">
-            {{ Number(row.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+            {{ formatMoney(row.offsetAmount || 0) }}
           </template>
         </el-table-column>
         <el-table-column prop="status" label="状态" min-width="110">
@@ -458,10 +460,7 @@ function summaryMethod(params: { data: any[] }) {
         </el-table-column>
         <el-table-column prop="currentNode" label="当前节点" min-width="120">
           <template #default="{ row }">
-            <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-              {{ statusLabel(row.currentNode) }}
-            </BaseTag>
-            <span v-else class="node-current-empty">-</span>
+            <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
           </template>
         </el-table-column>
         <el-table-column label="操作" width="300" fixed="right" class-name="col-actions">
@@ -514,7 +513,7 @@ function summaryMethod(params: { data: any[] }) {
               <BaseTag :tone="statusTone(viewData.status)">{{ statusLabel(viewData.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">申请金额(元):<b class="money-em">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b></p>
+          <p class="dlg-view-meta">申请金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b></p>
         </div>
 
         <div class="dlg-view-section">
@@ -527,16 +526,16 @@ function summaryMethod(params: { data: any[] }) {
             <div class="v">
               {{
                 viewData.actualAmount != null
-                  ? '¥' + Number(viewData.actualAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
+                  ? formatMoneyYuan(viewData.actualAmount)
                   : '—'
               }}
             </div>
             <div class="k">申请金额(元)</div>
-            <div class="v">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(viewData.amount || 0) }}</div>
             <div class="k">冲抵总金额(元)</div>
-            <div class="v">¥{{ Number(viewData.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(viewData.offsetAmount || 0) }}</div>
             <div class="k">当前节点</div>
-            <div class="v">{{ viewData.currentNode || '-' }}</div>
+            <div class="v">{{ workflowNodeDisplay(viewData.currentNode, viewData.status) }}</div>
             <div class="k">申请时间</div>
             <div class="v">{{ viewData.applyTime || '-' }}</div>
           </div>
@@ -547,9 +546,9 @@ function summaryMethod(params: { data: any[] }) {
             <el-table-column prop="reimburseApplyCode" label="报销单号" min-width="130" />
             <el-table-column prop="reimburseApplicantName" label="报销申请人" min-width="110" />
             <el-table-column prop="reimburseTitle" label="报销标题" min-width="160" show-overflow-tooltip />
-            <el-table-column label="冲抵金额(元)" min-width="120" class-name="col-money">
+            <el-table-column label="冲抵金额(元)" min-width="120" :formatter="moneyTableFormatter" class-name="col-money">
               <template #default="{ row: offsetRow }">
-                ¥{{ Number(offsetRow.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+                {{ formatMoneyYuan(offsetRow.offsetAmount || 0) }}
               </template>
             </el-table-column>
             <el-table-column prop="createTime" label="冲抵时间" min-width="160" />

+ 17 - 33
bx/bx-web/src/views/pages/AdvanceApprovePage.vue

@@ -3,7 +3,10 @@ import { reactive, ref, computed, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import { Check, Close, View } from '@element-plus/icons-vue'
 import { advanceApi } from '../../api'
-import { statusLabel, statusTone, formatSumMoney, hasWorkflowNode } from '../../utils/format'
+import { advanceApproverStats, refreshApproveMenuBadges } from '../../hooks/useApproveMenuBadge'
+import { useAuthStore } from '../../stores/auth'
+import { statusLabel, statusTone, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import { useServerPaged } from '../../hooks/useServerPaged'
 import { useApplicantOptions } from '../../hooks/useApplicantOptions'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
@@ -42,26 +45,13 @@ const approvedPager = useServerPaged(
 const tabLoaded = reactive({ pending: false, approved: false })
 const activeTab = ref('pending')
 
-const advanceApproverStats = ref({ pendingTotal: 0, monthPendingAmount: 0 })
-
-async function loadAdvanceApproverStats() {
-  try {
-    const res: any = await advanceApi.pendingApprovalStats()
-    const d = res.data || {}
-    advanceApproverStats.value = {
-      pendingTotal: Number(d.pendingTotal ?? 0),
-      monthPendingAmount: Number(d.monthPendingAmount ?? 0),
-    }
-  } catch {
-    /* 拦截器已提示 */
-  }
-}
+const auth = useAuthStore()
 
 const stats = computed(() => {
   const s = advanceApproverStats.value
   return [
     { name: '待审批', value: String(s.pendingTotal) },
-    { name: '本月审批金额(元)', value: s.monthPendingAmount.toLocaleString('zh-CN') },
+    { name: '本月审批金额(元)', value: formatMoney(s.monthPendingAmount) },
   ]
 })
 
@@ -123,7 +113,7 @@ function buildApprovedQuery() {
 
 onMounted(async () => {
   loadApplicantOptions()
-  await loadAdvanceApproverStats()
+  await refreshApproveMenuBadges(auth.menuList)
   pendingPager.search()
   tabLoaded.pending = true
 })
@@ -225,7 +215,7 @@ async function handleConfirmApprove() {
     ElMessage.success(approveRows.value.length > 1 ? `批量${msg}` : msg)
     dialogs.approve = false
     await pendingPager.refresh()
-    await loadAdvanceApproverStats()
+    await refreshApproveMenuBadges(auth.menuList)
   } catch (e: any) {
     ElMessage.error(e?.message || '操作失败')
   } finally {
@@ -309,20 +299,17 @@ async function handleConfirmApprove() {
           <template #table-columns>
             <el-table-column type="selection" width="48" />
             <el-table-column type="index" label="序号" width="90" />
+            <el-table-column prop="applyTime" label="申请时间" min-width="160" />
             <el-table-column prop="applyCode" label="申请单号" min-width="140" />
             <el-table-column prop="applicantName" label="申请人" min-width="100" />
             <el-table-column prop="reason" label="申请原因" min-width="160" />
             <el-table-column prop="purpose" label="申请目的" min-width="160" />
-            <el-table-column prop="amount" label="申请金额(元)" class-name="col-money" min-width="120" />
+            <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
             <el-table-column prop="currentNode" label="当前节点" min-width="120">
               <template #default="{ row }">
-                <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-                  {{ statusLabel(row.currentNode) }}
-                </BaseTag>
-                <span v-else class="node-current-empty">-</span>
+                <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
               </template>
             </el-table-column>
-            <el-table-column prop="applyTime" label="申请时间" min-width="140" />
             <el-table-column label="操作" width="260" fixed="right" class-name="col-actions">
               <template #default="{ row }">
                 <BaseOpButton primary :icon="View" @click="openView(row)">查看</BaseOpButton>
@@ -389,11 +376,12 @@ async function handleConfirmApprove() {
 
           <template #table-columns>
             <el-table-column type="index" label="序号" width="90" />
+            <el-table-column prop="applyTime" label="申请时间" min-width="160" />
             <el-table-column prop="applyCode" label="申请单号" min-width="140" />
             <el-table-column prop="applicantName" label="申请人" min-width="100" />
             <el-table-column prop="reason" label="申请原因" min-width="160" />
             <el-table-column prop="purpose" label="申请目的" min-width="160" />
-            <el-table-column prop="amount" label="申请金额(元)" class-name="col-money" min-width="120" />
+            <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
             <el-table-column prop="status" label="状态" min-width="100">
               <template #default="{ row }">
                 <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
@@ -401,13 +389,9 @@ async function handleConfirmApprove() {
             </el-table-column>
             <el-table-column prop="currentNode" label="当前节点" min-width="120">
               <template #default="{ row }">
-                <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-                  {{ statusLabel(row.currentNode) }}
-                </BaseTag>
-                <span v-else class="node-current-empty">-</span>
+                <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
               </template>
             </el-table-column>
-            <el-table-column prop="applyTime" label="申请时间" min-width="140" />
             <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
               <template #default="{ row }">
                 <BaseOpButton primary :icon="View" @click="openView(row)">查看</BaseOpButton>
@@ -428,7 +412,7 @@ async function handleConfirmApprove() {
               <BaseTag :tone="statusTone(viewData.status)">{{ statusLabel(viewData.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">申请金额(元):<b class="money-em">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b></p>
+          <p class="dlg-view-meta">申请金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b></p>
         </div>
 
         <div class="dlg-view-section">
@@ -440,9 +424,9 @@ async function handleConfirmApprove() {
             <div class="k">申请目的</div>
             <div class="v kv-full"><BaseClampText :text="viewData.purpose" /></div>
             <div class="k">申请金额(元)</div>
-            <div class="v">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(viewData.amount || 0) }}</div>
             <div class="k">当前节点</div>
-            <div class="v">{{ viewData.currentNode || '-' }}</div>
+            <div class="v">{{ workflowNodeDisplay(viewData.currentNode, viewData.status) }}</div>
             <div class="k">申请时间</div>
             <div class="v">{{ viewData.applyTime || '-' }}</div>
           </div>

+ 11 - 10
bx/bx-web/src/views/pages/AdvanceIssuePage.vue

@@ -4,7 +4,7 @@ import { ElMessage } from 'element-plus'
 import { Check, Download, View, Upload, Close, Plus, Delete, ZoomIn, Document } from '@element-plus/icons-vue'
 import { paymentApi, attachmentApi, advanceApi } from '../../api'
 import { ElMessageBox } from 'element-plus'
-import { statusLabel, statusTone, formatSumMoney } from '../../utils/format'
+import { statusLabel, statusTone, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter } from '../../utils/format'
 import { useServerPaged } from '../../hooks/useServerPaged'
 import { useApplicantOptions } from '../../hooks/useApplicantOptions'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
@@ -63,8 +63,8 @@ async function loadAdvanceIssueStats() {
     stats.value = [
       { name: '待发放', value: String(d.pendingCount ?? 0) },
       { name: '已发放', value: String(d.issuedCount ?? 0) },
-      { name: '本月发放金额(元)', value: Number(d.issuedMonthAmount ?? 0).toLocaleString('zh-CN') },
-      { name: '待发放金额(元)', value: Number(d.pendingAmount ?? 0).toLocaleString('zh-CN') }
+      { name: '本月发放金额(元)', value: formatMoney(d.issuedMonthAmount ?? 0) },
+      { name: '待发放金额(元)', value: formatMoney(d.pendingAmount ?? 0) }
     ]
   } catch {
     /* interceptor */
@@ -414,11 +414,12 @@ async function openView(row: any) {
       <template #table-columns>
         <el-table-column type="selection" width="48" />
         <el-table-column type="index" label="序号" width="90" />
+        <el-table-column prop="applyTime" label="申请时间" min-width="160" />
         <el-table-column prop="applyCode" label="申请单号" min-width="140" />
         <el-table-column prop="applicantName" label="申请人" min-width="100" />
         <el-table-column prop="reason" label="申请原因" min-width="160" />
-        <el-table-column prop="amount" label="申请金额(元)" class-name="col-money" min-width="120" />
-        <el-table-column label="实际发放金额(元)" class-name="col-money" min-width="130">
+        <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
+        <el-table-column label="实际发放金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="130">
           <template #default="{ row }">
             {{
               row.actualAmount != null && row.actualAmount !== ''
@@ -427,7 +428,7 @@ async function openView(row: any) {
             }}
           </template>
         </el-table-column>
-        <el-table-column label="冲抵总金额(元)" class-name="col-money" min-width="130">
+        <el-table-column label="冲抵总金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="130">
           <template #default="{ row }">
             {{ formatSumMoney(Number(row.offsetAmount ?? 0)) }}
           </template>
@@ -541,9 +542,9 @@ async function openView(row: any) {
             </div>
           </div>
           <p class="dlg-view-meta">
-            申请金额(元):<b class="money-em">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b>
+            申请金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b>
             <template v-if="viewData.actualAmount != null">
-              &nbsp;&nbsp;实际发放金额(元):<b class="money-em">¥{{ Number(viewData.actualAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b>
+              &nbsp;&nbsp;实际发放金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.actualAmount) }}</b>
             </template>
           </p>
         </div>
@@ -583,9 +584,9 @@ async function openView(row: any) {
           <el-table :data="offsetRecords" size="small" border>
             <el-table-column prop="reimburseApplyCode" label="报销单号" min-width="130" />
             <el-table-column prop="reimburseApplicantName" label="报销申请人" min-width="100" />
-            <el-table-column label="冲抵金额(元)" min-width="120" class-name="col-money">
+            <el-table-column label="冲抵金额(元)" min-width="120" :formatter="moneyTableFormatter" class-name="col-money">
               <template #default="{ row: offsetRow }">
-                {{ Number(offsetRow.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+                {{ formatMoney(offsetRow.offsetAmount || 0) }}
               </template>
             </el-table-column>
             <el-table-column prop="createTime" label="冲抵时间" min-width="160" />

+ 9 - 11
bx/bx-web/src/views/pages/AllPage.vue

@@ -3,7 +3,8 @@ import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { View } from '@element-plus/icons-vue'
 import { advanceApi, reimburseApi } from '../../api'
-import { statusLabel, statusTone, hasWorkflowNode } from '../../utils/format'
+import { statusLabel, statusTone, formatMoney, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
 import BaseDialog from '../../components/base/BaseDialog.vue'
 import BaseInput from '../../components/base/BaseInput.vue'
@@ -178,7 +179,7 @@ function summaryMethod(params: { data: Array<{ amount?: number }> }) {
     '',
     '',
     '',
-    totalAmount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
+    formatMoney(totalAmount),
     '',
     '',
     ''
@@ -299,6 +300,7 @@ onMounted(() => {
 
       <template #table-columns>
         <el-table-column type="index" label="序号" width="90" />
+        <el-table-column prop="applyTime" label="申请时间" min-width="160" />
         <el-table-column prop="applyCode" label="申请单号" min-width="140" />
         <el-table-column prop="bizType" label="业务类型" min-width="100">
           <template #default="{ row }">
@@ -307,7 +309,7 @@ onMounted(() => {
         </el-table-column>
         <el-table-column prop="applicantName" label="申请人" min-width="100" />
         <el-table-column prop="title" label="申请标题" min-width="180" />
-        <el-table-column prop="amount" label="申请金额(元)" class-name="col-money" min-width="120" />
+        <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
         <el-table-column prop="status" label="状态" min-width="110">
           <template #default="{ row }">
             <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
@@ -315,13 +317,9 @@ onMounted(() => {
         </el-table-column>
         <el-table-column prop="currentNode" label="当前节点" min-width="130">
           <template #default="{ row }">
-            <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-              {{ statusLabel(row.currentNode) }}
-            </BaseTag>
-            <span v-else class="node-current-empty">-</span>
+            <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
           </template>
         </el-table-column>
-        <el-table-column prop="applyTime" label="申请时间" min-width="150" />
         <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
           <template #default="{ row }">
             <BaseOpButton primary :icon="View" @click="openViewDetail(row)">查看</BaseOpButton>
@@ -340,7 +338,7 @@ onMounted(() => {
               <BaseTag v-if="currentRow?.status" :tone="statusTone(currentRow.status)">{{ statusLabel(currentRow.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">金额(元):<b class="money-em">¥{{ Number(currentRow?.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b> · 申请人:{{ currentRow?.applicantName || '-' }}</p>
+          <p class="dlg-view-meta">金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b> · 申请人:{{ currentRow?.applicantName || '-' }}</p>
         </div>
         <div class="dlg-view-section">
           <div class="dlg-kv dlg-kv--triple">
@@ -353,11 +351,11 @@ onMounted(() => {
             <div class="k">申请标题</div>
             <div class="v"><BaseClampText :text="currentRow?.title" /></div>
             <div class="k">申请金额(元)</div>
-            <div class="v">¥{{ Number(currentRow?.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(currentRow?.amount || 0) }}</div>
             <div class="k">状态</div>
             <div class="v">{{ statusLabel(currentRow?.status) }}</div>
             <div class="k">当前节点</div>
-            <div class="v">{{ currentRow?.currentNode || '-' }}</div>
+            <div class="v">{{ workflowNodeDisplay(currentRow?.currentNode, currentRow?.status) }}</div>
             <div class="k">申请时间</div>
             <div class="v">{{ currentRow?.applyTime || '-' }}</div>
           </div>

+ 5 - 5
bx/bx-web/src/views/pages/DemoPage.vue

@@ -10,7 +10,7 @@ import {
 } from '@element-plus/icons-vue'
 import { reimburseApi, expenseItemApi } from '../../api'
 import { useExpenseItemTree, formatExpenseName } from '../../hooks/useExpenseItemTree'
-import { statusLabel, statusTone } from '../../utils/format'
+import { statusLabel, statusTone, formatMoney, moneyTableFormatter } from '../../utils/format'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
 import BaseButton from '../../components/base/BaseButton.vue'
 import BaseDialog from '../../components/base/BaseDialog.vue'
@@ -57,8 +57,8 @@ const reimburseStats = computed(() => {
   const s = myReimburseStats.value
   return [
     { name: '待审批申请', value: String(s.pendingCount) },
-    { name: '本月申请金额(元)', value: s.monthApplyAmount.toLocaleString('zh-CN') },
-    { name: '已审批金额(元)', value: s.approvedAmount.toLocaleString('zh-CN') },
+    { name: '本月申请金额(元)', value: formatMoney(s.monthApplyAmount) },
+    { name: '已审批金额(元)', value: formatMoney(s.approvedAmount) },
     { name: '已通过申请', value: String(s.approvedCount) },
   ]
 })
@@ -115,7 +115,7 @@ function reimburseSummaryMethod(params: { data: Array<{ amount?: number }> }) {
     '',
     '',
     '',
-    totalAmount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
+    formatMoney(totalAmount),
     '',
     '',
     ''
@@ -276,7 +276,7 @@ onMounted(() => {
       <el-table-column prop="code" label="报销单号" min-width="140" />
       <el-table-column prop="user" label="申请人" min-width="100" />
       <el-table-column prop="project" label="费用项目" min-width="160" />
-      <el-table-column prop="amount" label="报销金额(元)" class-name="col-money" min-width="120" />
+      <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
       <el-table-column prop="invoiceCount" label="发票" min-width="80" />
       <el-table-column prop="attachCount" label="附件" min-width="80" />
       <el-table-column prop="status" label="审批状态" min-width="110">

+ 13 - 22
bx/bx-web/src/views/pages/HomePage.vue

@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import { inject, onMounted, reactive, ref } from 'vue'
 import { advanceApi, reimburseApi } from '../../api'
-import { statusLabel, statusTone, hasWorkflowNode } from '../../utils/format'
+import { statusLabel, statusTone, formatMoney, moneyTableFormatter } from '../../utils/format'
 import { useServerPaged } from '../../hooks/useServerPaged'
 import BaseTag from '../../components/base/BaseTag.vue'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import BaseOpButton from '../../components/base/BaseOpButton.vue'
 import { openMenuTabKey } from '../../shell/openMenuTabKey'
 
@@ -100,10 +101,6 @@ async function loadStats() {
   }
 }
 
-function money(n: number) {
-  return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
-}
-
 type TagTone = 'pending' | 'processing' | 'done' | 'warning' | 'error' | 'info'
 
 function elTagTone(s: unknown): TagTone {
@@ -178,15 +175,15 @@ onMounted(() => {
           </div>
           <div class="home-stat-cell">
             <span class="k">已发放金额(元)</span>
-            <span class="v">{{ money(advanceStats.issuedAmount) }}</span>
+            <span class="v">{{ formatMoney(advanceStats.issuedAmount) }}</span>
           </div>
           <div class="home-stat-cell">
             <span class="k">本月申请金额 / 笔数</span>
-            <span class="v">{{ money(advanceStats.monthApplyAmount) }} / {{ advanceStats.monthApplyCount }}</span>
+            <span class="v">{{ formatMoney(advanceStats.monthApplyAmount) }} / {{ advanceStats.monthApplyCount }}</span>
           </div>
           <div class="home-stat-cell">
             <span class="k">冲抵合计(元)</span>
-            <span class="v">{{ money(advanceStats.offsetTotal) }}</span>
+            <span class="v">{{ formatMoney(advanceStats.offsetTotal) }}</span>
           </div>
         </div>
         </div>
@@ -214,11 +211,11 @@ onMounted(() => {
           </div>
           <div class="home-stat-cell">
             <span class="k">已通过金额(元)</span>
-            <span class="v">{{ money(reimburseStats.approvedAmount) }}</span>
+            <span class="v">{{ formatMoney(reimburseStats.approvedAmount) }}</span>
           </div>
           <div class="home-stat-cell">
             <span class="k">本月申请金额 / 笔数</span>
-            <span class="v">{{ money(reimburseStats.monthApplyAmount) }} / {{ reimburseStats.monthApplyCount }}</span>
+            <span class="v">{{ formatMoney(reimburseStats.monthApplyAmount) }} / {{ reimburseStats.monthApplyCount }}</span>
           </div>
         </div>
         </div>
@@ -231,9 +228,10 @@ onMounted(() => {
           <div class="table-scroll" v-loading="advanceLoading">
             <el-table :data="advanceRows" stripe empty-text="暂无数据">
               <el-table-column type="index" label="序号" width="70" />
+              <el-table-column prop="applyTime" label="申请时间" min-width="160" />
               <el-table-column prop="applyCode" label="申请单号" min-width="140" />
               <el-table-column prop="reason" label="事由" min-width="140" show-overflow-tooltip />
-              <el-table-column prop="amount" label="金额(元)" width="120" class-name="col-money" />
+              <el-table-column prop="amount" label="金额(元)" width="120" :formatter="moneyTableFormatter" class-name="col-money" />
               <el-table-column prop="status" label="状态" width="110">
                 <template #default="{ row }">
                   <BaseTag :tone="elTagTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
@@ -241,13 +239,9 @@ onMounted(() => {
               </el-table-column>
               <el-table-column prop="currentNode" label="当前节点" min-width="120">
                 <template #default="{ row }">
-                  <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="elTagTone(row.currentNode)">
-                    {{ statusLabel(row.currentNode) }}
-                  </BaseTag>
-                  <span v-else class="node-current-empty">-</span>
+                  <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
                 </template>
               </el-table-column>
-              <el-table-column prop="applyTime" label="申请时间" min-width="160" />
               <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
                 <template #default>
                   <BaseOpButton primary @click="openMenuTab('advance-apply')">去办理</BaseOpButton>
@@ -277,10 +271,11 @@ onMounted(() => {
           <div class="table-scroll" v-loading="reimburseLoading">
             <el-table :data="reimburseRows" stripe empty-text="暂无数据">
               <el-table-column type="index" label="序号" width="70" />
+              <el-table-column prop="applyTime" label="申请时间" min-width="160" />
               <el-table-column prop="applyCode" label="申请单号" min-width="140" />
               <el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
               <el-table-column prop="expenseName" label="费用项目" min-width="120" show-overflow-tooltip />
-              <el-table-column prop="amount" label="金额(元)" width="120" class-name="col-money" />
+              <el-table-column prop="amount" label="金额(元)" width="120" :formatter="moneyTableFormatter" class-name="col-money" />
               <el-table-column prop="status" label="状态" width="110">
                 <template #default="{ row }">
                   <BaseTag :tone="elTagTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
@@ -288,13 +283,9 @@ onMounted(() => {
               </el-table-column>
               <el-table-column prop="currentNode" label="当前节点" min-width="120">
                 <template #default="{ row }">
-                  <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="elTagTone(row.currentNode)">
-                    {{ statusLabel(row.currentNode) }}
-                  </BaseTag>
-                  <span v-else class="node-current-empty">-</span>
+                  <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
                 </template>
               </el-table-column>
-              <el-table-column prop="applyTime" label="申请时间" min-width="160" />
               <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
                 <template #default>
                   <BaseOpButton primary @click="openMenuTab('reimburse-apply')">去办理</BaseOpButton>

+ 74 - 8
bx/bx-web/src/views/pages/MenuManagePage.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { reactive, ref, onMounted } from 'vue'
+import { reactive, ref, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Plus, EditPen, Close, ArrowDown, ArrowUp, Delete } from '@element-plus/icons-vue'
 import { menuApi } from '../../api'
@@ -37,6 +37,50 @@ const form = reactive({
 
 const selectedRow = ref<any>(null)
 
+type MenuTreeNode = {
+  id: number
+  menuName: string
+  children?: MenuTreeNode[]
+}
+
+function findMenuNode(nodes: MenuTreeNode[], id: number): MenuTreeNode | null {
+  for (const node of nodes) {
+    if (node.id === id) return node
+    const found = findMenuNode(node.children ?? [], id)
+    if (found) return found
+  }
+  return null
+}
+
+function collectDescendantIds(node: MenuTreeNode): Set<number> {
+  const ids = new Set<number>([node.id])
+  for (const child of node.children ?? []) {
+    collectDescendantIds(child).forEach(id => ids.add(id))
+  }
+  return ids
+}
+
+function filterMenuTree(nodes: MenuTreeNode[], excludeIds: Set<number>): MenuTreeNode[] {
+  return nodes
+    .filter(node => !excludeIds.has(node.id))
+    .map(node => ({
+      ...node,
+      children: filterMenuTree(node.children ?? [], excludeIds)
+    }))
+}
+
+/** 上级菜单树形下拉数据(含「无(顶级)」根节点) */
+const parentMenuSelectData = computed<MenuTreeNode[]>(() => {
+  let children = menuTree.value as MenuTreeNode[]
+  if (dialogs.editMenu && selectedRow.value?.id != null) {
+    const current = findMenuNode(children, selectedRow.value.id)
+    if (current) {
+      children = filterMenuTree(children, collectDescendantIds(current))
+    }
+  }
+  return [{ id: 0, menuName: '无(顶级目录)', children: children.length ? children : undefined }]
+})
+
 const menuTypeOptions = [
   { label: '目录', value: 1 },
   { label: '菜单', value: 2 },
@@ -190,7 +234,15 @@ onMounted(() => {
 
       <div class="table-scroll">
         <BaseTable ref="tableRef" :data="menuTree" row-key="id" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
-          <el-table-column prop="menuName" label="菜单名称" min-width="200" />
+          <el-table-column
+            prop="menuName"
+            label="菜单名称"
+            min-width="200"
+            align="left"
+            header-align="left"
+            class-name="col-align-left"
+            label-class-name="col-align-left"
+          />
           <el-table-column prop="menuCode" label="菜单编码" min-width="150" />
           <el-table-column prop="menuType" label="菜单类型" min-width="100">
             <template #default="{ row }">
@@ -220,9 +272,16 @@ onMounted(() => {
         <div class="form-grid-2">
           <div class="field">
             <label>上级菜单</label>
-            <BaseSelect v-model="form.parentId" placeholder="请选择上级菜单(空为顶级)">
-              <el-option label="无(顶级目录)" :value="0" />
-            </BaseSelect>
+            <el-tree-select
+              v-model="form.parentId"
+              :data="parentMenuSelectData"
+              :props="{ value: 'id', label: 'menuName', children: 'children' }"
+              placeholder="请选择上级菜单"
+              check-strictly
+              filterable
+              :render-after-expand="false"
+              class="fill-100"
+            />
           </div>
           <div class="field">
             <label>菜单名称</label>
@@ -265,9 +324,16 @@ onMounted(() => {
         <div class="form-grid-2">
           <div class="field">
             <label>上级菜单</label>
-            <BaseSelect v-model="form.parentId">
-              <el-option label="无(顶级目录)" :value="0" />
-            </BaseSelect>
+            <el-tree-select
+              v-model="form.parentId"
+              :data="parentMenuSelectData"
+              :props="{ value: 'id', label: 'menuName', children: 'children' }"
+              placeholder="请选择上级菜单"
+              check-strictly
+              filterable
+              :render-after-expand="false"
+              class="fill-100"
+            />
           </div>
           <div class="field">
             <label>菜单名称</label>

+ 8 - 10
bx/bx-web/src/views/pages/MinePage.vue

@@ -3,7 +3,8 @@ import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { View } from '@element-plus/icons-vue'
 import { advanceApi, reimburseApi } from '../../api'
-import { statusLabel, statusTone, hasWorkflowNode } from '../../utils/format'
+import { statusLabel, statusTone, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
 import BaseDialog from '../../components/base/BaseDialog.vue'
 import BaseInput from '../../components/base/BaseInput.vue'
@@ -224,6 +225,7 @@ onMounted(() => {
 
       <template #table-columns>
         <el-table-column type="index" label="序号" width="90" />
+        <el-table-column prop="applyTime" label="申请时间" min-width="160" />
         <el-table-column prop="applyCode" label="申请单号" min-width="140" />
         <el-table-column prop="bizType" label="业务类型" min-width="100">
           <template #default="{ row }">
@@ -231,7 +233,7 @@ onMounted(() => {
           </template>
         </el-table-column>
         <el-table-column prop="title" label="申请标题" min-width="180" />
-        <el-table-column prop="amount" label="申请金额(元)" class-name="col-money" min-width="120" />
+        <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
         <el-table-column prop="status" label="状态" min-width="110">
           <template #default="{ row }">
             <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
@@ -239,13 +241,9 @@ onMounted(() => {
         </el-table-column>
         <el-table-column prop="currentNode" label="当前节点" min-width="130">
           <template #default="{ row }">
-            <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-              {{ statusLabel(row.currentNode) }}
-            </BaseTag>
-            <span v-else class="node-current-empty">-</span>
+            <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
           </template>
         </el-table-column>
-        <el-table-column prop="applyTime" label="申请时间" min-width="150" />
         <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
           <template #default="{ row }">
             <BaseOpButton primary :icon="View" @click="openViewDetail(row)">查看</BaseOpButton>
@@ -263,7 +261,7 @@ onMounted(() => {
               <BaseTag :tone="statusTone(currentRow?.status)">{{ statusLabel(currentRow?.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">金额(元):<b class="money-em">¥{{ Number(currentRow?.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b> · 业务类型:{{ currentRow?.bizType || '-' }}</p>
+          <p class="dlg-view-meta">金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b> · 业务类型:{{ currentRow?.bizType || '-' }}</p>
         </div>
         <div class="dlg-view-section">
           <div class="dlg-kv dlg-kv--triple">
@@ -274,11 +272,11 @@ onMounted(() => {
             <div class="k">申请标题</div>
             <div class="v"><BaseClampText :text="currentRow?.title" /></div>
             <div class="k">申请金额(元)</div>
-            <div class="v">¥{{ Number(currentRow?.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(currentRow?.amount || 0) }}</div>
             <div class="k">状态</div>
             <div class="v">{{ statusLabel(currentRow?.status) }}</div>
             <div class="k">当前节点</div>
-            <div class="v">{{ currentRow?.currentNode || '-' }}</div>
+            <div class="v">{{ workflowNodeDisplay(currentRow?.currentNode, currentRow?.status) }}</div>
             <div class="k">申请时间</div>
             <div class="v">{{ currentRow?.applyTime || '-' }}</div>
           </div>

+ 79 - 44
bx/bx-web/src/views/pages/PaymentPage.vue

@@ -3,7 +3,7 @@ import { reactive, ref, computed, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { View, Money, Switch, Close, Upload, Plus, Delete, Download, ZoomIn, Document } from '@element-plus/icons-vue'
 import { paymentApi, attachmentApi, reimburseApi } from '../../api'
-import { statusLabel, statusTone, formatSumMoney } from '../../utils/format'
+import { statusLabel, statusTone, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter } from '../../utils/format'
 import { useServerPaged } from '../../hooks/useServerPaged'
 import { useApplicantOptions } from '../../hooks/useApplicantOptions'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
@@ -57,7 +57,7 @@ async function loadPaymentFinanceStats() {
       { name: '待处理单据', value: String(d.pendingCount ?? 0) },
       { name: '已打款', value: String(d.paidCount ?? 0) },
       { name: '冲抵备用金', value: String(d.offsetCount ?? 0) },
-      { name: '本月处理金额(元)', value: Number(d.monthProcessedAmount ?? 0).toLocaleString('zh-CN') }
+      { name: '本月处理金额(元)', value: formatMoney(d.monthProcessedAmount ?? 0) }
     ]
   } catch {
     /* interceptor */
@@ -313,6 +313,23 @@ function openOffsetDialog(row: any) {
   fetchOffsetAdvances()
 }
 
+function buildOffsetItems(totalOffset: number) {
+  const items: Array<{ advanceId: number; offsetAmount: number; applyCode: string }> = []
+  let remaining = totalOffset
+  for (const adv of offsetSelectedAdvances.value) {
+    if (remaining <= 0) break
+    const available = Number(adv.actualAmount || adv.amount || 0) - Number(adv.offsetAmount || 0)
+    const toOffset = Math.min(remaining, available)
+    items.push({
+      advanceId: adv.id,
+      offsetAmount: toOffset,
+      applyCode: adv.applyCode || String(adv.id)
+    })
+    remaining -= toOffset
+  }
+  return items
+}
+
 async function submitOffset() {
   if (offsetSelectedAdvances.value.length === 0) {
     ElMessage.warning('请选择要冲抵的备用金申请单')
@@ -323,23 +340,42 @@ async function submitOffset() {
     ElMessage.warning('请填写冲抵金额')
     return
   }
+  if (uploadedFiles.value.length === 0) {
+    ElMessage.warning('请至少上传 1 个冲抵附件')
+    return
+  }
 
-  // Calculate overflow distribution
-  const items: Array<{ advanceId: number; offsetAmount: number }> = []
-  let remaining = totalOffset
-  for (const adv of offsetSelectedAdvances.value) {
-    if (remaining <= 0) break
-    const available = Number(adv.actualAmount || adv.amount || 0) - Number(adv.offsetAmount || 0)
-    const toOffset = Math.min(remaining, available)
-    items.push({ advanceId: adv.id, offsetAmount: toOffset })
-    remaining -= toOffset
+  const items = buildOffsetItems(totalOffset)
+  const payAmount = Number(offsetPayAmount.value) || 0
+  const advanceLines = items
+    .map((it) => `${it.applyCode}(冲抵 ${formatMoneyYuan(it.offsetAmount)})`)
+    .join('\n')
+  const confirmMsg = [
+    '请确认冲抵信息:',
+    '',
+    `冲抵金额:${formatMoneyYuan(totalOffset)}`,
+    `冲抵备用金:`,
+    advanceLines,
+    `发放金额:${formatMoneyYuan(payAmount)}`,
+    '',
+    '确定提交冲抵吗?'
+  ].join('\n')
+
+  try {
+    await ElMessageBox.confirm(confirmMsg, '确认冲抵', {
+      confirmButtonText: '确定冲抵',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+  } catch {
+    return
   }
 
   loadingMap.offsetSubmit = true
   try {
     await paymentApi.batchOffset(currentRow.value.id, {
-      items,
-      payAmount: Number(offsetPayAmount.value) || 0,
+      items: items.map(({ advanceId, offsetAmount }) => ({ advanceId, offsetAmount })),
+      payAmount,
       remark: offsetRemark.value,
     })
     ElMessage.success('冲抵成功')
@@ -578,17 +614,17 @@ function processedSummaryMethod(params: { data: any[] }) {
           <template #table-columns>
             <el-table-column type="selection" width="48" />
             <el-table-column type="index" label="序号" width="90" />
+            <el-table-column prop="applyTime" label="申请时间" min-width="160" />
             <el-table-column prop="applyCode" label="单号" min-width="140" />
             <el-table-column prop="applicantName" label="申请人" min-width="100" />
             <el-table-column prop="title" label="报销标题" min-width="180" />
-            <el-table-column prop="applyTime" label="审批完成时间" min-width="160" />
-            <el-table-column prop="amount" label="报销金额(元)" class-name="col-money" min-width="120" />
-            <el-table-column label="冲抵金额(元)" class-name="col-money" min-width="120">
+            <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
+            <el-table-column label="冲抵金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120">
               <template #default="{ row }">
                 {{ formatSumMoney(Number(row.offsetAmount || 0)) }}
               </template>
             </el-table-column>
-            <el-table-column label="发放金额(元)" class-name="col-money" min-width="120">
+            <el-table-column label="发放金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120">
               <template #default="{ row }">
                 {{
                   row.actualAmount != null && row.actualAmount !== ''
@@ -668,16 +704,17 @@ function processedSummaryMethod(params: { data: any[] }) {
 
           <template #table-columns>
             <el-table-column type="index" label="序号" width="90" />
+            <el-table-column prop="applyTime" label="申请时间" min-width="160" />
             <el-table-column prop="applyCode" label="单号" min-width="140" />
             <el-table-column prop="applicantName" label="申请人" min-width="100" />
             <el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
-            <el-table-column prop="amount" label="报销金额(元)" class-name="col-money" min-width="120" />
-            <el-table-column label="冲抵金额(元)" class-name="col-money" min-width="120">
+            <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
+            <el-table-column label="冲抵金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120">
               <template #default="{ row }">
                 {{ formatSumMoney(Number(row.offsetAmount || 0)) }}
               </template>
             </el-table-column>
-            <el-table-column label="发放金额(元)" class-name="col-money" min-width="120">
+            <el-table-column label="发放金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120">
               <template #default="{ row }">
                 {{
                   row.actualAmount != null && row.actualAmount !== ''
@@ -715,7 +752,7 @@ function processedSummaryMethod(params: { data: any[] }) {
               <BaseTag :tone="statusTone(viewData.status)">{{ statusLabel(viewData.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">金额(元):<b class="money-em">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b></p>
+          <p class="dlg-view-meta">金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b></p>
         </div>
         <div class="dlg-view-section">
           <div class="dlg-kv dlg-kv--triple">
@@ -728,7 +765,7 @@ function processedSummaryMethod(params: { data: any[] }) {
             <div class="k">审批完成时间</div>
             <div class="v">{{ viewData.applyTime || '-' }}</div>
             <div class="k">金额(元)</div>
-            <div class="v">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(viewData.amount || 0) }}</div>
             <div class="k">状态</div>
             <div class="v">{{ statusLabel(viewData.status) }}</div>
           </div>
@@ -760,7 +797,7 @@ function processedSummaryMethod(params: { data: any[] }) {
             <label>发放金额(元)</label>
             <BaseInput :model-value="String(currentRow.amount || '')" disabled placeholder="发放金额" />
           </div>
-          <div class="field">
+          <div class="field field-amount-danger">
             <label>实际发放金额(元)</label>
             <BaseInput v-model="actualAmount" placeholder="请输入实际发放金额" />
           </div>
@@ -822,7 +859,7 @@ function processedSummaryMethod(params: { data: any[] }) {
             <label>报销金额(元)</label>
             <BaseInput :model-value="String(currentRow.amount || '')" disabled />
           </div>
-          <div class="field">
+          <div class="field field-amount-danger">
             <label>冲抵金额(元)</label>
             <BaseInput v-model="offsetAmount" placeholder="请输入冲抵金额" />
           </div>
@@ -842,30 +879,30 @@ function processedSummaryMethod(params: { data: any[] }) {
           <BaseButton size="small" @click="offsetAdvanceFilter.keyword = ''; offsetAdvancePage = 1; fetchOffsetAdvances()">重置</BaseButton>
         </div>
         <el-table
+          class="bx-table"
           :data="offsetAdvances"
           :loading="offsetAdvanceLoading"
           max-height="300"
           @selection-change="(rows) => offsetSelectedAdvances = rows"
           border
-          size="small"
         >
-          <el-table-column type="selection" width="40" />
+          <el-table-column type="selection" width="48" align="center" />
           <el-table-column prop="applyCode" label="申请单号" min-width="130" />
           <el-table-column prop="applicantName" label="申请人" min-width="80" />
           <el-table-column prop="reason" label="申请原因" min-width="120" show-overflow-tooltip />
-          <el-table-column label="已发放金额(元)" class-name="col-money" min-width="100">
+          <el-table-column label="已发放金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="100">
             <template #default="{ row }">
-              {{ Number(row.actualAmount || row.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+              {{ formatMoney(row.actualAmount || row.amount || 0) }}
             </template>
           </el-table-column>
-          <el-table-column label="已冲抵金额(元)" class-name="col-money" min-width="100">
+          <el-table-column label="已冲抵金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="100">
             <template #default="{ row }">
-              {{ Number(row.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+              {{ formatMoney(row.offsetAmount || 0) }}
             </template>
           </el-table-column>
-          <el-table-column label="剩余可冲抵(元)" class-name="col-money" min-width="100">
+          <el-table-column label="剩余可冲抵(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="100">
             <template #default="{ row }">
-              {{ (Number(row.actualAmount || row.amount || 0) - Number(row.offsetAmount || 0)).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+              {{ formatMoney(Number(row.actualAmount || row.amount || 0) - Number(row.offsetAmount || 0)) }}
             </template>
           </el-table-column>
         </el-table>
@@ -892,7 +929,7 @@ function processedSummaryMethod(params: { data: any[] }) {
 
       <!-- 附件 -->
       <div class="form-section form-section-last">
-        <h4>冲抵附件</h4>
+        <h4>冲抵附件(必填)</h4>
         <div class="attachment-upload-area">
           <el-upload :http-request="handleUpload" :show-file-list="false" multiple :disabled="uploadLoading">
             <div class="upload-trigger">
@@ -934,7 +971,7 @@ function processedSummaryMethod(params: { data: any[] }) {
             </div>
           </div>
           <p class="dlg-view-meta">
-            金额(元):<b class="money-em">¥{{ Number(processedViewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b>
+            金额(元):<b class="money-em">{{ formatMoneyYuan(processedViewData.amount || 0) }}</b>
           </p>
         </div>
 
@@ -949,7 +986,7 @@ function processedSummaryMethod(params: { data: any[] }) {
             <div class="k">标题</div>
             <div class="v"><BaseClampText :text="processedViewData.title || processedViewData.reason" /></div>
             <div class="k">金额(元)</div>
-            <div class="v">¥{{ Number(processedViewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(processedViewData.amount || 0) }}</div>
           </div>
           <template v-if="processedAttachments['申请附件']?.length">
             <div class="section-attachment-title section-gap-tight">附件</div>
@@ -983,14 +1020,12 @@ function processedSummaryMethod(params: { data: any[] }) {
           <h4>发放信息</h4>
           <div class="dlg-kv dlg-kv--triple">
             <div class="k">发放金额(元)</div>
-            <div class="v">¥{{ Number((processedViewData.amount || 0) - (processedViewData.offsetAmount || 0)).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan((processedViewData.amount || 0) - (processedViewData.offsetAmount || 0)) }}</div>
             <div class="k">实际发放金额(元)</div>
             <div class="v">
-              ¥{{
-                Number(
-                  processedViewData.actualAmount ?? ((processedViewData.amount || 0) - (processedViewData.offsetAmount || 0))
-                ).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
-              }}
+              {{ formatMoneyYuan(
+                processedViewData.actualAmount ?? ((processedViewData.amount || 0) - (processedViewData.offsetAmount || 0))
+              ) }}
             </div>
             <template v-if="processedViewData.payTime">
               <div class="k">发放时间</div>
@@ -1025,7 +1060,7 @@ function processedSummaryMethod(params: { data: any[] }) {
           <h4>冲抵信息</h4>
           <div class="dlg-kv dlg-kv--triple">
             <div class="k">冲抵金额(元)</div>
-            <div class="v">¥{{ Number(processedViewData.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(processedViewData.offsetAmount || 0) }}</div>
             <template v-if="processedViewData.payTime">
               <div class="k">处理时间</div>
               <div class="v">{{ processedViewData.payTime }}</div>
@@ -1064,9 +1099,9 @@ function processedSummaryMethod(params: { data: any[] }) {
               <el-table-column prop="advanceApplyCode" label="备用金单号" min-width="130" />
               <el-table-column prop="advanceApplicantName" label="备用金申请人" min-width="110" />
               <el-table-column prop="advanceReason" label="申请原因" min-width="160" show-overflow-tooltip />
-              <el-table-column label="冲抵金额(元)" min-width="120" class-name="col-money">
+              <el-table-column label="冲抵金额(元)" min-width="120" :formatter="moneyTableFormatter" class-name="col-money">
                 <template #default="{ row: offsetRow }">
-                  ¥{{ Number(offsetRow.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+                  {{ formatMoneyYuan(offsetRow.offsetAmount || 0) }}
                 </template>
               </el-table-column>
               <el-table-column prop="createTime" label="冲抵时间" min-width="160" />

+ 16 - 17
bx/bx-web/src/views/pages/ReimburseApplyPage.vue

@@ -4,7 +4,8 @@ import { ElMessage, ElMessageBox } from 'element-plus'
 import { Plus, EditPen, Upload, View, Close, Delete, Download, ZoomIn, Document } from '@element-plus/icons-vue'
 import { reimburseApi, attachmentApi } from '../../api'
 import { useExpenseItemTree } from '../../hooks/useExpenseItemTree'
-import { statusLabel, statusTone, formatSumMoney, hasWorkflowNode } from '../../utils/format'
+import { statusLabel, statusTone, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import { useServerPaged } from '../../hooks/useServerPaged'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
 import BaseButton from '../../components/base/BaseButton.vue'
@@ -68,8 +69,8 @@ const stats = computed(() => {
   const s = myReimburseStats.value
   return [
     { name: '待审批申请', value: String(s.pendingCount) },
-    { name: '本月申请金额(元)', value: s.monthApplyAmount.toLocaleString('zh-CN') },
-    { name: '已审批金额(元)', value: s.approvedAmount.toLocaleString('zh-CN') },
+    { name: '本月申请金额(元)', value: formatMoney(s.monthApplyAmount) },
+    { name: '已审批金额(元)', value: formatMoney(s.approvedAmount) },
     { name: '已通过申请', value: String(s.approvedCount) },
   ]
 })
@@ -471,7 +472,7 @@ function summaryMethod(params: { data: any[] }) {
 
 function viewDetailTotal(details: any[]): string {
   if (!details?.length) return '0.00'
-  return details.reduce((s: number, d: any) => s + Number(d.amount || 0), 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
+  return formatMoney(details.reduce((s: number, d: any) => s + Number(d.amount || 0), 0))
 }
 
 function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
@@ -483,7 +484,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
     }
     if (index === 2) {
       const total = data.reduce((sum: number, row: any) => sum + Number(row.amount || 0), 0)
-      sums[index] = `¥${total.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`
+      sums[index] = formatMoneyYuan(total)
       return
     }
     sums[index] = ''
@@ -574,10 +575,11 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
       <template #table-columns>
         <el-table-column type="selection" width="48" />
         <el-table-column type="index" label="序号" width="90" />
+        <el-table-column prop="applyTime" label="申请时间" min-width="160" />
         <el-table-column prop="applyCode" label="报销单号" min-width="140" />
         <el-table-column prop="title" label="报销标题" min-width="180" />
         <el-table-column prop="expenseName" label="费用项目" min-width="140" />
-        <el-table-column prop="amount" label="报销金额(元)" class-name="col-money" min-width="120" />
+        <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
         <el-table-column prop="status" label="状态" min-width="110">
           <template #default="{ row }">
             <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
@@ -585,10 +587,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
         </el-table-column>
         <el-table-column prop="currentNode" label="当前节点" min-width="120">
           <template #default="{ row }">
-            <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-              {{ statusLabel(row.currentNode) }}
-            </BaseTag>
-            <span v-else class="node-current-empty">-</span>
+            <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
           </template>
         </el-table-column>
         <el-table-column label="操作" width="300" fixed="right" class-name="col-actions">
@@ -674,7 +673,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
         </div>
         <div class="detail-footer">
           <BaseButton size="small" :icon="Plus" @click="addDetailLine">新增明细行</BaseButton>
-          <span class="detail-total">合计金额(元):<b>¥{{ detailTotalAmount.toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b></span>
+          <span class="detail-total">合计金额(元):<b>{{ formatMoneyYuan(detailTotalAmount) }}</b></span>
         </div>
       </div>
 
@@ -740,7 +739,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
               <BaseTag :tone="statusTone(viewData.status)">{{ statusLabel(viewData.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">报销金额(元):<b class="money-em">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b></p>
+          <p class="dlg-view-meta">报销金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b></p>
         </div>
 
         <div class="dlg-view-section">
@@ -750,7 +749,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
             <div class="k">申请人</div>
             <div class="v">{{ viewData.applicantName || '-' }}</div>
             <div class="k">当前节点</div>
-            <div class="v">{{ viewData.currentNode || '-' }}</div>
+            <div class="v">{{ workflowNodeDisplay(viewData.currentNode, viewData.status) }}</div>
             <div class="k">申请时间</div>
             <div class="v">{{ viewData.applyTime || '-' }}</div>
           </div>
@@ -768,9 +767,9 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
           >
             <el-table-column type="index" label="序号" width="90" align="center" />
             <el-table-column prop="expenseName" label="费用项目" min-width="160" />
-            <el-table-column prop="amount" label="金额(元)" width="120" class-name="col-money">
+            <el-table-column prop="amount" label="金额(元)" width="120" :formatter="moneyTableFormatter" class-name="col-money">
               <template #default="{ row }">
-                ¥{{ Number(row.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+                {{ formatMoneyYuan(row.amount || 0) }}
               </template>
             </el-table-column>
             <el-table-column prop="remark" label="报销事由" min-width="140">
@@ -812,10 +811,10 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
             <div class="k">处理方式</div>
             <div class="v">{{ viewProcessMode(viewData) }}</div>
             <div class="k">冲抵金额(元)</div>
-            <div class="v">¥{{ Number(viewData.offsetAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(viewData.offsetAmount || 0) }}</div>
             <div class="k">发放金额(元)</div>
             <div class="v">
-              ¥{{ Number(viewData.actualAmount ?? (Number(viewData.amount || 0) - Number(viewData.offsetAmount || 0))).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+              {{ formatMoneyYuan(viewData.actualAmount ?? (Number(viewData.amount || 0) - Number(viewData.offsetAmount || 0))) }}
             </div>
             <div class="k">处理时间</div>
             <div class="v">{{ viewData.payTime || '-' }}</div>

+ 19 - 35
bx/bx-web/src/views/pages/ReimburseApprovePage.vue

@@ -3,7 +3,10 @@ import { reactive, ref, computed, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import { View, Check, Close, Download, ZoomIn, Document } from '@element-plus/icons-vue'
 import { reimburseApi, attachmentApi } from '../../api'
-import { statusLabel, statusTone, formatSumMoney, hasWorkflowNode } from '../../utils/format'
+import { reimburseApproverStats, refreshApproveMenuBadges } from '../../hooks/useApproveMenuBadge'
+import { useAuthStore } from '../../stores/auth'
+import { statusLabel, statusTone, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import { useServerPaged } from '../../hooks/useServerPaged'
 import { useExpenseItemTree } from '../../hooks/useExpenseItemTree'
 import { useApplicantOptions } from '../../hooks/useApplicantOptions'
@@ -47,26 +50,13 @@ const approvedPager = useServerPaged(
   }
 )
 
-const reimburseApproverStats = ref({ pendingTotal: 0, monthPendingAmount: 0 })
-
-async function loadReimburseApproverStats() {
-  try {
-    const res: any = await reimburseApi.pendingApprovalStats()
-    const d = res.data || {}
-    reimburseApproverStats.value = {
-      pendingTotal: Number(d.pendingTotal ?? 0),
-      monthPendingAmount: Number(d.monthPendingAmount ?? 0),
-    }
-  } catch {
-    /* 拦截器已提示 */
-  }
-}
+const auth = useAuthStore()
 
 const stats = computed(() => {
   const s = reimburseApproverStats.value
   return [
     { name: '待审批', value: String(s.pendingTotal) },
-    { name: '本月审批金额(元)', value: s.monthPendingAmount.toLocaleString('zh-CN') },
+    { name: '本月审批金额(元)', value: formatMoney(s.monthPendingAmount) },
   ]
 })
 
@@ -136,7 +126,7 @@ function buildApprovedQuery() {
 
 onMounted(async () => {
   loadApplicantOptions()
-  await loadReimburseApproverStats()
+  await refreshApproveMenuBadges(auth.menuList)
   pendingPager.search()
   pendingLoaded.value = true
 })
@@ -282,7 +272,7 @@ async function handleConfirmApprove() {
     ElMessage.success(approveRows.value.length > 1 ? `批量${msg}` : msg)
     dialogs.approve = false
     await pendingPager.refresh()
-    await loadReimburseApproverStats()
+    await refreshApproveMenuBadges(auth.menuList)
   } catch (e: any) {
     ElMessage.error(e?.message || '操作失败')
   } finally {
@@ -315,7 +305,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
     }
     if (index === 2) {
       const total = data.reduce((sum: number, row: any) => sum + Number(row.amount || 0), 0)
-      sums[index] = `¥${total.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`
+      sums[index] = formatMoneyYuan(total)
       return
     }
     sums[index] = ''
@@ -429,20 +419,17 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
           <template #table-columns>
             <el-table-column type="selection" width="48" />
             <el-table-column type="index" label="序号" width="90" />
+            <el-table-column prop="applyTime" label="申请时间" min-width="160" />
             <el-table-column prop="applyCode" label="报销单号" min-width="140" />
             <el-table-column prop="applicantName" label="申请人" min-width="100" />
             <el-table-column prop="title" label="报销标题" min-width="180" />
             <el-table-column prop="expenseName" label="费用项目" min-width="140" />
-            <el-table-column prop="amount" label="报销金额(元)" class-name="col-money" min-width="120" />
+            <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
             <el-table-column prop="currentNode" label="当前节点" min-width="130">
               <template #default="{ row }">
-                <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-                  {{ statusLabel(row.currentNode) }}
-                </BaseTag>
-                <span v-else class="node-current-empty">-</span>
+                <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
               </template>
             </el-table-column>
-            <el-table-column prop="applyTime" label="申请时间" min-width="150" />
             <el-table-column label="操作" width="260" fixed="right" class-name="col-actions">
               <template #default="{ row }">
                 <BaseOpButton primary :icon="View" @click="openView(row)">查看</BaseOpButton>
@@ -539,11 +526,12 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
 
           <template #table-columns>
             <el-table-column type="index" label="序号" width="90" />
+            <el-table-column prop="applyTime" label="申请时间" min-width="160" />
             <el-table-column prop="applyCode" label="报销单号" min-width="140" />
             <el-table-column prop="applicantName" label="申请人" min-width="100" />
             <el-table-column prop="title" label="报销标题" min-width="180" />
             <el-table-column prop="expenseName" label="费用项目" min-width="140" />
-            <el-table-column prop="amount" label="报销金额(元)" class-name="col-money" min-width="120" />
+            <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
             <el-table-column prop="status" label="状态" min-width="110">
               <template #default="{ row }">
                 <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
@@ -551,13 +539,9 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
             </el-table-column>
             <el-table-column prop="currentNode" label="当前节点" min-width="130">
               <template #default="{ row }">
-                <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-                  {{ statusLabel(row.currentNode) }}
-                </BaseTag>
-                <span v-else class="node-current-empty">-</span>
+                <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
               </template>
             </el-table-column>
-            <el-table-column prop="applyTime" label="申请时间" min-width="150" />
             <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
               <template #default="{ row }">
                 <BaseOpButton primary :icon="View" @click="openView(row)">查看</BaseOpButton>
@@ -578,14 +562,14 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
               <BaseTag :tone="statusTone(viewData.status)">{{ statusLabel(viewData.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">金额(元):<b class="money-em">¥{{ Number(viewData.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b></p>
+          <p class="dlg-view-meta">金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b></p>
         </div>
         <div class="dlg-view-section">
           <div class="dlg-kv dlg-kv--triple">
             <div class="k">申请人</div>
             <div class="v">{{ viewData.applicantName || '-' }}</div>
             <div class="k">当前节点</div>
-            <div class="v">{{ viewData.currentNode || '-' }}</div>
+            <div class="v">{{ workflowNodeDisplay(viewData.currentNode, viewData.status) }}</div>
             <div class="k">申请时间</div>
             <div class="v">{{ viewData.applyTime || '-' }}</div>
           </div>
@@ -603,9 +587,9 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
           >
             <el-table-column type="index" label="序号" width="90" align="center" />
             <el-table-column prop="expenseName" label="费用项目" min-width="160" />
-            <el-table-column prop="amount" label="金额(元)" width="120" class-name="col-money">
+            <el-table-column prop="amount" label="金额(元)" width="120" :formatter="moneyTableFormatter" class-name="col-money">
               <template #default="{ row }">
-                ¥{{ Number(row.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}
+                {{ formatMoneyYuan(row.amount || 0) }}
               </template>
             </el-table-column>
             <el-table-column prop="remark" label="报销事由" min-width="140">

+ 22 - 14
bx/bx-web/src/views/pages/TodoPage.vue

@@ -3,7 +3,18 @@ import { computed, onMounted, reactive, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { View, Check, Close } from '@element-plus/icons-vue'
 import { advanceApi, reimburseApi } from '../../api'
-import { statusLabel, statusTone, hasWorkflowNode } from '../../utils/format'
+import {
+  statusLabel,
+  statusTone,
+  formatMoney,
+  formatMoneyYuan,
+  moneyTableFormatter,
+  workflowNodeDisplay,
+  shouldShowWorkflowNodeTag,
+  workflowNodeLabel,
+  workflowNodeTone,
+} from '../../utils/format'
+import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
 import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
 import BaseButton from '../../components/base/BaseButton.vue'
 import BaseDialog from '../../components/base/BaseDialog.vue'
@@ -228,7 +239,7 @@ function summaryMethod(params: { data: Array<{ amount?: number }> }) {
     '',
     '',
     '',
-    totalAmount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
+    formatMoney(totalAmount),
     '',
     '',
     ''
@@ -342,6 +353,7 @@ onMounted(() => {
       <template #table-columns>
         <el-table-column type="selection" width="48" />
         <el-table-column type="index" label="序号" width="90" />
+        <el-table-column prop="applyTime" label="申请时间" min-width="160" />
         <el-table-column prop="applyCode" label="申请单号" min-width="140" />
         <el-table-column prop="bizType" label="业务类型" min-width="100">
           <template #default="{ row }">
@@ -350,16 +362,12 @@ onMounted(() => {
         </el-table-column>
         <el-table-column prop="applicantName" label="申请人" min-width="100" />
         <el-table-column prop="title" label="申请标题" min-width="180" />
-        <el-table-column prop="amount" label="申请金额(元)" class-name="col-money" min-width="120" />
+        <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
         <el-table-column prop="currentNode" label="当前节点" min-width="130">
           <template #default="{ row }">
-            <BaseTag v-if="hasWorkflowNode(row.currentNode)" :tone="statusTone(row.currentNode)">
-              {{ statusLabel(row.currentNode) }}
-            </BaseTag>
-            <span v-else class="node-current-empty">-</span>
+            <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
           </template>
         </el-table-column>
-        <el-table-column prop="applyTime" label="申请时间" min-width="150" />
         <el-table-column label="操作" width="260" fixed="right" class-name="col-actions">
           <template #default="{ row }">
             <BaseOpButton primary :icon="View" @click="openViewDetail(row)">查看</BaseOpButton>
@@ -378,12 +386,12 @@ onMounted(() => {
             <div class="dlg-view-tags">
               <BaseTag :tone="currentRow?.bizType === '报销单' ? 'processing' : 'info'">{{ currentRow?.bizType || '-' }}</BaseTag>
               <BaseTag
-                v-if="currentRow && hasWorkflowNode(currentRow.currentNode)"
-                :tone="statusTone(currentRow.currentNode)"
-              >{{ statusLabel(currentRow.currentNode) }}</BaseTag>
+                v-if="currentRow && shouldShowWorkflowNodeTag(currentRow.currentNode, currentRow.status)"
+                :tone="workflowNodeTone(currentRow.currentNode, currentRow.status)"
+              >{{ workflowNodeLabel(currentRow.currentNode, currentRow.status) }}</BaseTag>
             </div>
           </div>
-          <p class="dlg-view-meta">金额(元):<b class="money-em">¥{{ Number(currentRow?.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</b> · 申请人:{{ currentRow?.applicantName || '-' }}</p>
+          <p class="dlg-view-meta">金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b> · 申请人:{{ currentRow?.applicantName || '-' }}</p>
         </div>
         <div class="dlg-view-section">
           <div class="dlg-kv dlg-kv--triple">
@@ -396,9 +404,9 @@ onMounted(() => {
             <div class="k">申请标题</div>
             <div class="v"><BaseClampText :text="currentRow?.title" /></div>
             <div class="k">申请金额(元)</div>
-            <div class="v">¥{{ Number(currentRow?.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</div>
+            <div class="v">{{ formatMoneyYuan(currentRow?.amount || 0) }}</div>
             <div class="k">当前节点</div>
-            <div class="v">{{ currentRow?.currentNode || '-' }}</div>
+            <div class="v">{{ workflowNodeDisplay(currentRow?.currentNode, currentRow?.status) }}</div>
             <div class="k">申请时间</div>
             <div class="v">{{ currentRow?.applyTime || '-' }}</div>
           </div>