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, systemDashboardTotals as fallbackSystemDashboardTotals, systemAgentDailyRatio as fallbackSystemAgentDailyRatio, systemLoginWave as fallbackSystemLoginWave, 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 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' } ] 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 } 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 || fallbackSystemDashboardTotals )) const systemAgentDailyRatio = computed(() => ( systemDashboardPayload.value?.agentDailyRatio || fallbackSystemAgentDailyRatio )) const systemLoginWave = computed(() => ( systemDashboardPayload.value?.loginWave || fallbackSystemLoginWave )) 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 = rows.slice(-7).map((item) => ({ date: String(item.date || '').trim() || '-', total: Number(item.total || 0), highOrAbove: Number(item.high_or_above ?? item.highOrAbove ?? 0) })) 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 } }