feat: 财务看板口径重构与半年模拟数据及报销状态注册表

- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选
- 引入 expense_claim_status_registry 统一报销状态流转
- 完善报销草稿流程、Item Sync 与本体解析器
- 优化总览页趋势图、分页组件与请求进度步骤
- 增强报销申请快速预览、本体工具与详情展示
- 新增半年报销模拟数据种子脚本与状态审计工具
- 补充财务看板、报销状态注册与模拟数据测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-02 16:22:59 +08:00
parent ca691f3ee0
commit 0c74b4ab4a
54 changed files with 6810 additions and 1238 deletions

View File

@@ -37,16 +37,18 @@ import {
} from '../data/metrics.js'
const emptyFinanceTotals = {
pendingCount: 0,
pendingAmount: 0,
avgSla: 0,
autoPassRate: 0,
riskCount: 0,
slaRate: 0
reimbursementAmount: 0,
reimbursementCount: 0,
pendingPaymentAmount: 0,
avgClaimAmount: 0,
budgetUsageRate: 0,
paymentClearanceRate: 0
}
const emptyFinanceTrend = {
labels: [],
claimCount: [],
claimAmount: [],
applications: [],
approved: [],
avgHours: []
@@ -63,6 +65,15 @@ const emptyFinanceBudgetSummary = {
left: '¥0'
}
const emptyFinanceBudgetMetrics = [
{ label: '预算池数量', value: '0 个', detail: '年度有效预算池', tone: 'neutral', icon: 'mdi mdi-database-outline' },
{ label: '总预算', value: '¥0', detail: '原始预算 + 调整', tone: 'neutral', icon: 'mdi mdi-cash-register' },
{ label: '已用预算', value: '¥0', detail: '使用率 0.0%', tone: 'success', icon: 'mdi mdi-chart-arc' },
{ label: '预占预算', value: '¥0', detail: '待流转单据占用', tone: 'success', icon: 'mdi mdi-lock-outline' },
{ label: '可用预算', value: '¥0', detail: '可继续使用额度', tone: 'success', icon: 'mdi mdi-wallet-outline' },
{ label: '预警预算池', value: '0 个', detail: '超支 0 个', tone: 'success', icon: 'mdi mdi-alert-outline' }
]
export function useOverviewView(options = {}) {
const activeTrendRange = ref(trendRanges[0])
const activeDepartmentRange = ref(departmentRangeOptions[0])
@@ -103,8 +114,9 @@ export function useOverviewView(options = {}) {
const formatPercent = (value) => `${Math.round(Number(value || 0) * 100)}%`
const formatMetricValue = (metric, value) => {
if (metric.key === 'pendingAmount') return formatCurrency(Math.round(value))
if (metric.key === 'avgSla') return `${value.toFixed(1)} ${metric.unit}`
if (['reimbursementAmount', 'pendingPaymentAmount', 'avgClaimAmount'].includes(metric.key)) {
return formatCurrency(Math.round(value))
}
if (metric.unit === '%') return `${Math.round(value)} ${metric.unit}`
if (metric.unit) return `${Math.round(value)} ${metric.unit}`
return `${Math.round(value)}`
@@ -311,12 +323,21 @@ export function useOverviewView(options = {}) {
const financeDepartmentRanking = computed(() => (
financeDashboardPayload.value?.departmentRanking || []
))
const financeEmployeeRanking = computed(() => (
financeDashboardPayload.value?.employeeRanking || []
))
const financeTopClaims = computed(() => (
financeDashboardPayload.value?.topClaims || []
))
const financeBottlenecks = computed(() => (
financeDashboardPayload.value?.bottlenecks || []
))
const financeBudgetSummary = computed(() => (
financeDashboardPayload.value?.budgetSummary || emptyFinanceBudgetSummary
))
const financeBudgetMetrics = computed(() => (
financeDashboardPayload.value?.budgetMetrics || emptyFinanceBudgetMetrics
))
const resolveSystemMetricMeta = (metric) => {
const totals = systemDashboardTotals.value
@@ -508,13 +529,15 @@ export function useOverviewView(options = {}) {
})))
const rankedDepartments = computed(() => {
const rows = financeDepartmentRanking.value.map((item) => ({
...item,
amount: Number(item.amount || item.value || 0)
}))
const rows = financeDepartmentRanking.value
.filter((item) => !isMissingDimension(item.name))
.map((item) => ({
...item,
amount: Number(item.amount || item.value || 0)
}))
const max = Math.max(...rows.map((item) => item.amount), 1)
return rows.slice(0, 5).map((item, index) => ({
return rows.slice(0, 6).map((item, index) => ({
...item,
rank: index + 1,
shortName: item.name,
@@ -524,6 +547,32 @@ export function useOverviewView(options = {}) {
}))
})
const rankedEmployees = computed(() => {
const rows = financeEmployeeRanking.value
.filter((item) => !isMissingDimension(item.name))
.map((item) => ({
...item,
amount: Number(item.amount || item.value || 0)
}))
const max = Math.max(...rows.map((item) => item.amount), 1)
return rows.slice(0, 6).map((item, index) => ({
...item,
rank: index + 1,
shortName: item.name,
amountLabel: formatCurrency(item.amount),
width: `${Math.max((item.amount / max) * 100, 18)}%`,
color: item.color
}))
})
const topClaims = computed(() => (
financeTopClaims.value.map((item) => ({
...item,
amountLabel: item.amountLabel || formatCurrency(Number(item.amount || 0))
}))
))
const systemToolRankingItems = computed(() => systemToolRankings.map((item, index) => ({
...item,
rank: index + 1,
@@ -670,8 +719,14 @@ export function useOverviewView(options = {}) {
return labels[text] || text.replace(/_/g, ' ') || '未知风险'
}
function isMissingDimension(value) {
const text = String(value || '').trim()
return !text || ['待补充', '待确认', '未归属部门', '未归属', 'N/A', 'n/a', '-'].includes(text)
}
const bottlenecks = financeBottlenecks
const budgetSummary = financeBudgetSummary
const budgetMetrics = financeBudgetMetrics
const spendByCategory = financeSpendByCategory
const exceptionMix = financeExceptionMix
@@ -681,6 +736,7 @@ export function useOverviewView(options = {}) {
activeTrend,
activeTrendRange,
bottlenecks,
budgetMetrics,
budgetSummary,
departmentRangeOptions,
digitalEmployeeCategoryRows,
@@ -701,6 +757,7 @@ export function useOverviewView(options = {}) {
kpiMetrics,
metricBlueprints,
rankedDepartments,
rankedEmployees,
riskDashboard,
riskDashboardError,
riskDashboardLoading,
@@ -743,6 +800,7 @@ export function useOverviewView(options = {}) {
systemToolRankings,
systemToolTotal,
systemTrendSeries,
topClaims,
trendRanges
}
}