import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { fetchDigitalEmployeeDashboard, fetchFinanceDashboard, fetchSystemDashboard } from '../services/analytics.js' import { fetchRiskObservationDashboard } from '../services/riskObservations.js' import { formatRiskSignalLabel } from '../utils/riskLabels.js' import { buildDigitalEmployeeCategoryRows, buildDigitalEmployeeDailyRows, buildDigitalEmployeeKpiMetrics, buildDigitalEmployeeTaskRanking, emptyDigitalEmployeeDashboard } from '../views/scripts/overviewDigitalEmployeeDashboardModel.js' import { metricBlueprints, systemMetricBlueprints, systemAgentDailyRatio as fallbackSystemAgentDailyRatio, systemTokenDailyWave as fallbackSystemTokenDailyWave, systemUsageDurationSummary as fallbackSystemUsageDurationSummary, systemUserTokenUsage as fallbackSystemUserTokenUsage, systemAccuracyComparison as fallbackSystemAccuracyComparison, systemTrendSeries, systemToolCallMix, systemExecutionMix, systemToolRankings, systemModelUsage, systemFeedbackSummary as fallbackSystemFeedbackSummary, systemLoadHeatmap, systemToolDetailRows as fallbackSystemToolDetailRows } from '../data/metrics.js' const DEFAULT_OVERVIEW_RANGE = '近10日' const DAY_MS = 24 * 60 * 60 * 1000 const RISK_DAILY_TREND_MAX_BUCKETS = 14 const emptyFinanceTotals = { reimbursementAmount: 0, reimbursementCount: 0, pendingPaymentAmount: 0, avgClaimAmount: 0, budgetUsageRate: 0, paymentClearanceRate: 0 } const emptyFinanceTrend = { labels: [], claimCount: [], claimAmount: [], categoryAmountSeries: [], applications: [], approved: [], avgHours: [] } const emptyFinanceDonut = [ { name: '暂无数据', value: 0, color: '#cbd5e1' } ] const emptyFinanceBudgetSummary = { ratio: 0, total: '¥0', used: '¥0', 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' } ] const emptySystemDashboardTotals = { toolCalls: 0, modelTokens: 0, onlineUsers: 0, avgOnlineMinutes: 0, executionSuccessRate: 0, positiveFeedback: 0, negativeFeedback: 0, failedRuns: 0, toolCallsChange: 0, modelTokensChange: 0 } const emptySystemLoginWave = { labels: Array.from({ length: 24 }, (_, hour) => `${String(hour).padStart(2, '0')}:00`), loginUsers: Array.from({ length: 24 }, () => 0), interactions: Array.from({ length: 24 }, () => 0) } function parseLocalDate(value) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '').trim()) if (!match) { return null } const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])) return Number.isNaN(date.getTime()) ? null : date } function clampWindowDays(value) { const days = Number(value || 0) if (!Number.isFinite(days) || days <= 0) { return 10 } return Math.max(1, Math.min(Math.round(days), 90)) } function resolveCustomRangeDays(customRange = {}) { const start = parseLocalDate(customRange.start) const end = parseLocalDate(customRange.end) if (!start || !end) { return 10 } return clampWindowDays(Math.abs(end.getTime() - start.getTime()) / DAY_MS + 1) } function resolveTopRangeDays(range, customRange = {}) { const key = String(range || DEFAULT_OVERVIEW_RANGE).trim() if (key === 'custom') { return resolveCustomRangeDays(customRange) } if (key === '\u4eca\u65e5') { return 1 } if (key === '\u672c\u5468') { const today = new Date() const weekday = today.getDay() || 7 return clampWindowDays(weekday) } if (key === '\u672c\u6708') { return clampWindowDays(new Date().getDate()) } const match = key.match(/\d+/) return clampWindowDays(match ? Number(match[0]) : 10) } function resolveTopRangeKey(range, customRange = {}) { const key = String(range || DEFAULT_OVERVIEW_RANGE).trim() if (key === 'custom') { return 'custom' } if (key === '\u672c\u5468' || key === '\u4eca\u65e5') { return `recent-${resolveTopRangeDays(key, customRange)}-days` } if (/\d+/.test(key)) { return `recent-${resolveTopRangeDays(key, customRange)}-days` } return key || DEFAULT_OVERVIEW_RANGE } function formatRiskTrendDateLabel(value) { const date = parseLocalDate(value) if (!date) { return String(value || '-').trim() || '-' } const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${month}-${day}` } function buildRiskTrendBucketLabel(first, last) { const start = String(first?.date || '').trim() const end = String(last?.date || '').trim() if (!start || start === end) { return formatRiskTrendDateLabel(start) } return `${formatRiskTrendDateLabel(start)}~${formatRiskTrendDateLabel(end)}` } function normalizeRiskTrendRow(item) { return { date: String(item.date || '').trim() || '-', total: Number(item.total || 0), highOrAbove: Number(item.high_or_above ?? item.highOrAbove ?? 0) } } function aggregateRiskDailyTrendRows(rows, maxBuckets = RISK_DAILY_TREND_MAX_BUCKETS) { const normalizedRows = rows .map(normalizeRiskTrendRow) .filter((item) => item.date !== '-' || item.total > 0 || item.highOrAbove > 0) if (normalizedRows.length <= maxBuckets) { return normalizedRows.map((item) => ({ ...item, date: formatRiskTrendDateLabel(item.date), sourceStartDate: item.date, sourceEndDate: item.date })) } const bucketSize = Math.ceil(normalizedRows.length / maxBuckets) const buckets = [] for (let index = 0; index < normalizedRows.length; index += bucketSize) { const bucketRows = normalizedRows.slice(index, index + bucketSize) const first = bucketRows[0] const last = bucketRows[bucketRows.length - 1] buckets.push({ date: buildRiskTrendBucketLabel(first, last), sourceStartDate: first?.date || '', sourceEndDate: last?.date || '', total: bucketRows.reduce((sum, item) => sum + item.total, 0), highOrAbove: bucketRows.reduce((sum, item) => sum + item.highOrAbove, 0) }) } return buckets } export function useOverviewView(options = {}) { const activeDashboardKey = computed(() => { const dashboard = String(options.dashboard || '').trim() if (dashboard === 'system') return 'system' if (dashboard === 'risk') return 'risk' if (dashboard === 'digitalEmployee') return 'digitalEmployee' return 'finance' }) const activeRangeValue = computed(() => String(options.activeRange || DEFAULT_OVERVIEW_RANGE).trim() || DEFAULT_OVERVIEW_RANGE) const customRangeValue = computed(() => options.customRange || {}) const activeRangeLabel = computed(() => { if (activeRangeValue.value !== 'custom') { return activeRangeValue.value } const start = String(customRangeValue.value.start || '').trim() const end = String(customRangeValue.value.end || '').trim() return start && end ? `${start} ~ ${end}` : '\u81ea\u5b9a\u4e49' }) const topRangeDays = computed(() => resolveTopRangeDays(activeRangeValue.value, customRangeValue.value)) const financeDashboardPayload = ref(null) const financeDashboardLoading = ref(false) const financeDashboardError = ref(null) const financeDashboardRenderKey = ref(0) const financeDashboardLoaded = computed(() => Boolean(financeDashboardPayload.value)) let financeDashboardRequestSeq = 0 const systemDashboardPayload = ref(null) const systemDashboardLoading = ref(false) const systemDashboardError = ref(null) const systemDashboardLoaded = computed(() => Boolean(systemDashboardPayload.value)) const riskDashboardPayload = ref(null) const riskDashboardLoading = ref(false) const riskDashboardError = ref(null) const riskDashboardLoaded = computed(() => Boolean(riskDashboardPayload.value)) const riskDashboardLastUpdatedAt = ref('') let riskDashboardRefreshTimer = 0 let riskDashboardRequestSeq = 0 const digitalEmployeeDashboardPayload = ref(null) const digitalEmployeeDashboardLoading = ref(false) const digitalEmployeeDashboardError = ref(null) const digitalEmployeeDashboardLoaded = computed(() => Boolean(digitalEmployeeDashboardPayload.value)) const formatCompact = (value) => { if (value >= 1_000_000) return `¥${(value / 1_000_000).toFixed(1)}M` if (value >= 1_000) return `¥${(value / 1_000).toFixed(1)}K` return `¥${value}` } const formatCurrency = (value) => formatCompact(value) const formatNumberCompact = (value) => { const number = Number(value || 0) if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(1)}M` if (number >= 1_000) return `${(number / 1_000).toFixed(1)}K` return `${Math.round(number)}` } const formatPercent = (value) => `${Math.round(Number(value || 0) * 100)}%` const formatMetricValue = (metric, value) => { 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)}` } const formatSystemMetricValue = (metric, value) => { const numericValue = Number(value || 0) if (metric.key === 'modelTokens') return formatNumberCompact(numericValue) if (metric.key === 'avgOnlineMinutes') return `${numericValue.toFixed(1)} ${metric.unit}` if (metric.key === 'executionSuccessRate') return `${numericValue.toFixed(1)}${metric.unit}` if (metric.key === 'positiveFeedback') { const negativeFeedback = Math.round(Number(systemDashboardTotals.value.negativeFeedback || 0)) return `${Math.round(numericValue)} / ${negativeFeedback}` } if (metric.unit) return `${formatNumberCompact(numericValue)} ${metric.unit}` return formatNumberCompact(numericValue) } const getFinanceRangeParams = () => { const activeRange = activeRangeValue.value const customRange = customRangeValue.value const isCustomRange = activeRange === 'custom' return { rangeKey: activeRange, startDate: isCustomRange ? customRange.start : '', endDate: isCustomRange ? customRange.end : '', trendRange: resolveTopRangeKey(activeRange, customRange), departmentRange: resolveTopRangeKey(activeRange, customRange) } } const loadFinanceDashboard = async () => { const requestSeq = ++financeDashboardRequestSeq financeDashboardLoading.value = true financeDashboardError.value = null try { const payload = await fetchFinanceDashboard(getFinanceRangeParams()) if (requestSeq !== financeDashboardRequestSeq) { return } financeDashboardPayload.value = payload financeDashboardRenderKey.value += 1 } catch (error) { if (requestSeq !== financeDashboardRequestSeq) { return } financeDashboardPayload.value = null financeDashboardError.value = error } finally { if (requestSeq === financeDashboardRequestSeq) { financeDashboardLoading.value = false } } } const loadSystemDashboard = async () => { systemDashboardLoading.value = true systemDashboardError.value = null try { systemDashboardPayload.value = await fetchSystemDashboard({ days: topRangeDays.value }) } catch (error) { systemDashboardPayload.value = null systemDashboardError.value = error } finally { systemDashboardLoading.value = false } } const loadRiskDashboard = async () => { const requestSeq = ++riskDashboardRequestSeq riskDashboardLoading.value = true riskDashboardError.value = null try { const payload = await fetchRiskObservationDashboard({ windowDays: topRangeDays.value, limit: 500 }) if (requestSeq !== riskDashboardRequestSeq) { return } riskDashboardPayload.value = payload riskDashboardLastUpdatedAt.value = new Date().toISOString() } catch (error) { if (requestSeq !== riskDashboardRequestSeq) { return } riskDashboardPayload.value = null riskDashboardError.value = error } finally { if (requestSeq === riskDashboardRequestSeq) { riskDashboardLoading.value = false } } } const startRiskDashboardRealtimeRefresh = () => { if (riskDashboardRefreshTimer) { window.clearInterval(riskDashboardRefreshTimer) } riskDashboardRefreshTimer = window.setInterval(() => { if (document.visibilityState === 'hidden' || riskDashboardLoading.value) { return } void loadRiskDashboard() }, 30_000) } const stopRiskDashboardRealtimeRefresh = () => { if (!riskDashboardRefreshTimer) { return } window.clearInterval(riskDashboardRefreshTimer) riskDashboardRefreshTimer = 0 } const loadDigitalEmployeeDashboard = async () => { digitalEmployeeDashboardLoading.value = true digitalEmployeeDashboardError.value = null try { digitalEmployeeDashboardPayload.value = await fetchDigitalEmployeeDashboard({ days: topRangeDays.value, limit: 300 }) } catch (error) { digitalEmployeeDashboardPayload.value = null digitalEmployeeDashboardError.value = error } finally { digitalEmployeeDashboardLoading.value = false } } const loadActiveDashboard = () => { if (activeDashboardKey.value === 'system') { void loadSystemDashboard() stopRiskDashboardRealtimeRefresh() return } if (activeDashboardKey.value === 'risk') { void loadRiskDashboard() startRiskDashboardRealtimeRefresh() return } if (activeDashboardKey.value === 'digitalEmployee') { void loadDigitalEmployeeDashboard() stopRiskDashboardRealtimeRefresh() return } void loadFinanceDashboard() stopRiskDashboardRealtimeRefresh() } onMounted(() => { loadActiveDashboard() }) onBeforeUnmount(() => { stopRiskDashboardRealtimeRefresh() }) watch( () => [ options.activeRange, options.customRange?.start, options.customRange?.end ], () => { loadActiveDashboard() } ) watch(activeDashboardKey, () => { loadActiveDashboard() }) const systemDashboardTotals = computed(() => ( systemDashboardPayload.value?.totals || emptySystemDashboardTotals )) const systemAgentDailyRatio = computed(() => ( systemDashboardPayload.value?.agentDailyRatio || fallbackSystemAgentDailyRatio )) const systemLoginWave = computed(() => ( systemDashboardPayload.value?.loginWave || emptySystemLoginWave )) const systemTokenDailyWave = computed(() => ( systemDashboardPayload.value?.tokenDailyWave || fallbackSystemTokenDailyWave )) const systemUsageDurationSummary = computed(() => ( systemDashboardPayload.value?.usageDurationSummary || fallbackSystemUsageDurationSummary )) const systemUserTokenUsage = computed(() => ( systemDashboardPayload.value?.userTokenUsage || fallbackSystemUserTokenUsage )) const systemAccuracyComparison = computed(() => ( systemDashboardPayload.value?.accuracyComparison || fallbackSystemAccuracyComparison )) const systemFeedbackSummary = computed(() => ( systemDashboardPayload.value?.feedbackSummary || fallbackSystemFeedbackSummary )) const systemToolDetailRows = computed(() => ( systemDashboardPayload.value?.toolDetailRows || fallbackSystemToolDetailRows )) const riskDashboard = computed(() => ( riskDashboardPayload.value || { windowDays: topRangeDays.value, totalObservations: 0, pendingCount: 0, riskClueCount: 0, highOrAboveCount: 0, confirmedCount: 0, falsePositiveCount: 0, feedbackSampleCount: 0, totalAmount: 0, averageScore: 0, confirmationRate: 0, falsePositiveRate: 0, levelDistribution: {}, statusDistribution: {}, signalDistribution: {}, sourceDistribution: {}, automationDistribution: {}, departmentDistribution: {}, expenseTypeDistribution: {}, riskTypeDistribution: {}, supplierDistribution: {}, employeeGradeDistribution: {}, dailyTrend: [], topRiskSignals: [], topDepartments: [], topEmployees: [], topSuppliers: [], topExpenseTypes: [], topRules: [], recentHighObservations: [] } )) const digitalEmployeeDashboard = computed(() => ( digitalEmployeeDashboardPayload.value || emptyDigitalEmployeeDashboard )) const financeDashboardTotals = computed(() => ( financeDashboardPayload.value?.totals || emptyFinanceTotals )) const financeMetricMeta = computed(() => ( financeDashboardPayload.value?.metricMeta || {} )) const financeTrend = computed(() => ( financeDashboardPayload.value?.trend || emptyFinanceTrend )) const financeSpendByCategory = computed(() => ( financeDashboardPayload.value?.spendByCategory || emptyFinanceDonut )) const financeExceptionMix = computed(() => ( financeDashboardPayload.value?.exceptionMix || emptyFinanceDonut )) const financeDepartmentRanking = computed(() => ( financeDashboardPayload.value?.departmentRanking || [] )) const financeDepartmentEmployeeMix = computed(() => ( financeDashboardPayload.value?.departmentEmployeeMix || emptyFinanceDonut )) 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 const realDashboardLoaded = Boolean(systemDashboardPayload.value) if (!realDashboardLoaded) { return { changeText: metric.change, delta: metric.delta, trend: metric.trend } } if (metric.key === 'toolCalls' || metric.key === 'modelTokens') { const changeValue = Number(totals[`${metric.key}Change`] || 0) return { changeText: `${changeValue >= 0 ? '+' : ''}${changeValue.toFixed(1)}%`, delta: '较上一周期', trend: changeValue < 0 ? 'down' : 'up' } } if (metric.key === 'executionSuccessRate') { const errorRate = Math.max(0, 100 - Number(totals.executionSuccessRate || 0)) return { changeText: '实时', delta: `错误率 ${errorRate.toFixed(1)}%`, trend: 'up' } } if (metric.key === 'positiveFeedback') { return { changeText: '实时', delta: `差评 ${Math.round(Number(totals.negativeFeedback || 0))} 次`, trend: 'up' } } return { changeText: '实时', delta: metric.key === 'onlineUsers' ? '活跃会话' : '按最近会话统计', trend: metric.trend } } const resolveFinanceMetricMeta = (metric) => { const meta = financeMetricMeta.value[metric.key] if (!financeDashboardPayload.value || !meta) { return { changeText: financeDashboardLoading.value ? '加载中' : '实时', delta: financeDashboardError.value ? '真实数据加载失败' : '等待真实数据', trend: metric.trend } } return { changeText: meta.changeText || metric.change, delta: meta.delta || metric.delta, trend: meta.trend || metric.trend } } const kpiMetrics = computed(() => metricBlueprints.map((metric, index) => { const rawValue = Number(financeDashboardTotals.value[metric.key] || 0) const displayValue = formatMetricValue(metric, rawValue) const metricMeta = resolveFinanceMetricMeta(metric) return { ...metric, ...metricMeta, displayValue, delay: index * 55 } })) const systemKpiMetrics = computed(() => systemMetricBlueprints.map((metric, index) => { const rawValue = systemDashboardTotals.value[metric.key] const displayValue = formatSystemMetricValue(metric, rawValue) const metricMeta = resolveSystemMetricMeta(metric) return { ...metric, ...metricMeta, displayValue, delay: index * 55 } })) const riskKpiMetrics = computed(() => { const data = riskDashboard.value const rows = [ { label: '新增风险数', value: formatNumberCompact(data.totalObservations), changeText: `${data.windowDays}天`, delta: '统一观察池', trend: 'up', icon: 'mdi mdi-shield-search', accent: 'var(--theme-primary)' }, { label: '高风险待处置', value: formatNumberCompact(data.highOrAboveCount), changeText: data.highOrAboveCount > 0 ? '需关注' : '稳定', delta: '高/重大风险', trend: data.highOrAboveCount > 0 ? 'down' : 'up', icon: 'mdi mdi-alert-octagon-outline', accent: '#ef4444' }, { label: '涉及金额', value: formatCurrency(Number(data.totalAmount || 0)), changeText: '归集', delta: '关联单据金额', trend: 'up', icon: 'mdi mdi-cash-multiple', accent: '#0f766e' }, { label: '已确认风险', value: formatNumberCompact(data.confirmedCount), changeText: formatPercent(data.confirmationRate), delta: '人工确认', trend: 'up', icon: 'mdi mdi-check-decagram-outline', accent: 'var(--success)' }, { label: '误报数量', value: formatNumberCompact(data.falsePositiveCount), changeText: formatPercent(data.falsePositiveRate), delta: '反馈校准', trend: data.falsePositiveRate > 0.2 ? 'down' : 'up', icon: 'mdi mdi-tune-variant', accent: '#8b5cf6' }, { label: '待复核', value: formatNumberCompact(data.pendingCount), changeText: '待处理', delta: '人工闭环', trend: data.pendingCount > 0 ? 'down' : 'up', icon: 'mdi mdi-account-clock-outline', accent: '#f59e0b' } ] return rows.map((item, index) => ({ ...item, displayValue: item.value, delay: index * 55 })) }) const digitalEmployeeKpiMetrics = computed(() => ( buildDigitalEmployeeKpiMetrics(digitalEmployeeDashboard.value, formatNumberCompact) )) const activeTrend = computed(() => financeTrend.value) const spendTotal = computed(() => financeSpendByCategory.value.reduce((sum, item) => sum + Number(item.value || 0), 0)) const riskTotal = computed(() => financeExceptionMix.value.reduce((sum, item) => sum + Number(item.value || 0), 0)) const departmentEmployeeTotal = computed(() => ( financeDepartmentEmployeeMix.value.reduce((sum, item) => sum + Number(item.value || item.amount || 0), 0) )) const spendCenterValue = computed(() => formatCurrency(Math.round(spendTotal.value))) const departmentEmployeeCenterValue = computed(() => formatCurrency(Math.round(departmentEmployeeTotal.value))) const spendLegend = computed(() => financeSpendByCategory.value.map((item) => ({ ...item, display: spendTotal.value ? `${Math.round((Number(item.value || 0) / spendTotal.value) * 100)}%` : '0%' }))) const riskLegend = computed(() => financeExceptionMix.value.map((item) => ({ ...item, display: `${item.value} 单` }))) const departmentEmployeeLegend = computed(() => financeDepartmentEmployeeMix.value.map((item) => ({ ...item, value: Number(item.value || item.amount || 0), display: departmentEmployeeTotal.value ? `${Math.round((Number(item.value || item.amount || 0) / departmentEmployeeTotal.value) * 100)}%` : '0%' }))) const systemToolTotal = computed(() => systemToolCallMix.reduce((sum, item) => sum + item.value, 0) ) const systemExecutionTotal = computed(() => systemExecutionMix.reduce((sum, item) => sum + item.value, 0) ) const systemToolCallLegend = computed(() => systemToolCallMix.map((item) => ({ ...item, display: `${Math.round((item.value / systemToolTotal.value) * 100)}%` }))) const systemExecutionLegend = computed(() => systemExecutionMix.map((item) => ({ ...item, display: `${Math.round((item.value / systemExecutionTotal.value) * 1000) / 10}%` }))) const rankedDepartments = computed(() => { 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, 6).map((item, index) => ({ ...item, rank: index + 1, shortName: item.name, amountLabel: formatCurrency(item.amount), meta: `${Number(item.employeeCount || 0)} 人 / ${Number(item.count || 0)} 单`, width: `${Math.max((item.amount / max) * 100, 18)}%`, color: item.color })) }) 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), meta: `${item.department || '未归属部门'} / ${Number(item.count || 0)} 单`, 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, shortName: item.name, amount: item.value }))) const systemModelUsageRows = computed(() => systemModelUsage.map((item) => ({ ...item, tokenLabel: `${formatNumberCompact(item.tokens)} tokens`, width: `${Math.max(Math.min(item.share, 100), 8)}%` }))) const systemExecutionRows = computed(() => systemExecutionMix.map((item) => ({ ...item, display: `${Math.round((item.value / systemExecutionTotal.value) * 1000) / 10}%`, width: `${Math.max((item.value / systemExecutionTotal.value) * 100, 1)}%` }))) const systemToolDetailItems = computed(() => { const maxCalls = Math.max(...systemToolDetailRows.value.map((item) => item.calls), 1) return systemToolDetailRows.value.map((item) => ({ ...item, callLabel: `${formatNumberCompact(item.calls)} 次`, tokenLabel: `${formatNumberCompact(item.tokens)} tokens`, width: `${Math.max((item.calls / maxCalls) * 100, 10)}%` })) }) const systemUsageDurationRows = computed(() => { const rows = systemUsageDurationSummary.value.rows || [] const maxValue = Math.max(...rows.map((item) => item.value), 1) return rows.map((item) => ({ ...item, width: `${Math.max((item.value / maxValue) * 100, 12)}%` })) }) const riskLevelLegend = computed(() => buildRiskDistributionLegend( riskDashboard.value.levelDistribution, { critical: '重大风险', high: '高风险', medium: '中风险', low: '低风险' }, { critical: '#b91c1c', high: '#ef4444', medium: '#f59e0b', low: '#3b82f6' } )) const riskCompositionLegend = computed(() => { const signalDistribution = riskDashboard.value.signalDistribution || {} const fallbackDistribution = Object.fromEntries( (Array.isArray(riskDashboard.value.topRiskSignals) ? riskDashboard.value.topRiskSignals : []) .map((item) => [item.name, Number(item.count || 0)]) ) return buildRiskDistributionLegend( Object.keys(signalDistribution).length ? signalDistribution : fallbackDistribution, {}, { duplicate_invoice: '#ef4444', budget_pressure: '#f59e0b', amount_outlier: '#8b5cf6', weekend_or_holiday: '#2563eb', supplier_anomaly: '#0f766e', split_billing: '#dc2626', missing_material: '#64748b' }, formatRiskSignalName ) }) const riskSignalRanking = computed(() => { const rows = Array.isArray(riskDashboard.value.topRiskSignals) ? riskDashboard.value.topRiskSignals : [] const fallbackRows = Object.entries(riskDashboard.value.signalDistribution || {}) .map(([name, count]) => ({ name, count })) return (rows.length ? rows : fallbackRows) .slice(0, 6) .map((item, index) => ({ name: formatRiskSignalName(item.name), shortName: formatRiskSignalName(item.name), value: Number(item.count || 0), color: [ '#ef4444', '#f59e0b', 'var(--theme-primary)', '#3b82f6', '#8b5cf6', '#0f766e' ][index] || '#64748b' })) }) const riskDailyTrendRows = computed(() => { const rows = Array.isArray(riskDashboard.value.dailyTrend) ? riskDashboard.value.dailyTrend : [] const normalizedRows = aggregateRiskDailyTrendRows(rows) const maxValue = Math.max(...normalizedRows.map((item) => item.total), 1) return normalizedRows.map((item) => ({ ...item, width: `${Math.max((item.total / maxValue) * 100, 4)}%`, highWidth: `${Math.max((item.highOrAbove / maxValue) * 100, item.highOrAbove ? 4 : 0)}%` })) }) const digitalEmployeeDailyRows = computed(() => buildDigitalEmployeeDailyRows(digitalEmployeeDashboard.value)) const digitalEmployeeTaskRanking = computed(() => buildDigitalEmployeeTaskRanking(digitalEmployeeDashboard.value)) const digitalEmployeeCategoryRows = computed(() => buildDigitalEmployeeCategoryRows(digitalEmployeeDashboard.value)) function buildRiskDistributionLegend(distribution, labels, colors, formatter = formatRiskSignalName) { const fallbackColors = ['#ef4444', '#f59e0b', 'var(--theme-primary)', '#3b82f6', '#8b5cf6', '#0f766e'] const entries = Object.entries(distribution || {}) .filter(([, value]) => Number(value || 0) > 0) if (!entries.length) { return [ { name: '暂无数据', value: 1, display: '0项', color: '#cbd5e1' } ] } return entries.map(([key, value], index) => ({ name: labels[key] || formatter(key), value: Number(value || 0), display: `${Number(value || 0)}项`, color: colors[key] || fallbackColors[index % fallbackColors.length] })) } function formatRiskSignalName(value) { return formatRiskSignalLabel(value) } 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 return { activeRangeLabel, activeTrend, bottlenecks, budgetMetrics, budgetSummary, departmentEmployeeCenterValue, departmentEmployeeLegend, digitalEmployeeCategoryRows, digitalEmployeeDashboard, digitalEmployeeDashboardError, digitalEmployeeDashboardLoaded, digitalEmployeeDashboardLoading, digitalEmployeeDailyRows, digitalEmployeeKpiMetrics, digitalEmployeeTaskRanking, exceptionMix, financeDashboardError, financeDashboardLoaded, financeDashboardLoading, financeDashboardRenderKey, formatCompact, formatCurrency, formatMetricValue, formatNumberCompact, formatSystemMetricValue, kpiMetrics, metricBlueprints, rankedDepartments, rankedEmployees, riskDashboard, riskDashboardError, riskDashboardLoaded, riskDashboardLastUpdatedAt, riskDashboardLoading, riskDailyTrendRows, riskLegend, riskKpiMetrics, riskLevelLegend, riskSignalRanking, riskCompositionLegend, riskTotal, spendByCategory, spendCenterValue, spendLegend, spendTotal, systemDashboardTotals, systemDashboardError, systemDashboardLoaded, systemDashboardLoading, systemAgentDailyRatio, systemLoginWave, systemTokenDailyWave, systemUsageDurationRows, systemUsageDurationSummary, systemUserTokenUsage, systemAccuracyComparison, systemExecutionLegend, systemExecutionMix, systemExecutionTotal, systemFeedbackSummary, systemKpiMetrics, systemLoadHeatmap, systemExecutionRows, systemMetricBlueprints, systemModelUsageRows, systemToolDetailItems, systemToolCallLegend, systemToolCallMix, systemToolRankingItems, systemToolRankings, systemToolTotal, systemTrendSeries, topClaims } }