feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -1,19 +1,53 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { fetchFinanceDashboard, fetchSystemDashboard } from '../services/analytics.js'
|
||||
import { fetchRiskObservationDashboard } from '../services/riskObservations.js'
|
||||
|
||||
import {
|
||||
metricBlueprints,
|
||||
systemMetricBlueprints,
|
||||
trendRanges,
|
||||
trendSeries,
|
||||
spendByCategory,
|
||||
exceptionMix,
|
||||
spendByCategory as fallbackSpendByCategory,
|
||||
exceptionMix as fallbackExceptionMix,
|
||||
departmentRangeOptions,
|
||||
bottlenecks,
|
||||
budgetSummary
|
||||
bottlenecks as fallbackBottlenecks,
|
||||
budgetSummary as fallbackBudgetSummary,
|
||||
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'
|
||||
|
||||
export function useOverviewView() {
|
||||
export function useOverviewView(options = {}) {
|
||||
const activeTrendRange = ref(trendRanges[0])
|
||||
const activeDepartmentRange = ref(departmentRangeOptions[0])
|
||||
const riskWindowOptions = [
|
||||
{ label: '近 7 天', value: 7 },
|
||||
{ label: '近 30 天', value: 30 },
|
||||
{ label: '近 90 天', value: 90 }
|
||||
]
|
||||
const activeRiskWindowDays = ref(30)
|
||||
const financeDashboardPayload = ref(null)
|
||||
const financeDashboardLoading = ref(false)
|
||||
const financeDashboardError = ref(null)
|
||||
const systemDashboardPayload = ref(null)
|
||||
const systemDashboardLoading = ref(false)
|
||||
const systemDashboardError = ref(null)
|
||||
const riskDashboardPayload = ref(null)
|
||||
const riskDashboardLoading = ref(false)
|
||||
const riskDashboardError = ref(null)
|
||||
|
||||
const demoTotals = {
|
||||
pendingCount: 128,
|
||||
@@ -40,6 +74,15 @@ export function useOverviewView() {
|
||||
|
||||
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 (metric.key === 'pendingAmount') return formatCurrency(Math.round(value))
|
||||
if (metric.key === 'avgSla') return `${value.toFixed(1)} ${metric.unit}`
|
||||
@@ -48,34 +91,382 @@ export function useOverviewView() {
|
||||
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 = String(options.activeRange || '近10日')
|
||||
const customRange = options.customRange || {}
|
||||
const isCustomRange = activeRange === 'custom'
|
||||
|
||||
return {
|
||||
rangeKey: activeRange,
|
||||
startDate: isCustomRange ? customRange.start : '',
|
||||
endDate: isCustomRange ? customRange.end : '',
|
||||
trendRange: activeTrendRange.value,
|
||||
departmentRange: activeDepartmentRange.value
|
||||
}
|
||||
}
|
||||
|
||||
const loadFinanceDashboard = async () => {
|
||||
financeDashboardLoading.value = true
|
||||
financeDashboardError.value = null
|
||||
|
||||
try {
|
||||
financeDashboardPayload.value = await fetchFinanceDashboard(getFinanceRangeParams())
|
||||
} catch (error) {
|
||||
financeDashboardPayload.value = null
|
||||
financeDashboardError.value = error
|
||||
} finally {
|
||||
financeDashboardLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadSystemDashboard = async () => {
|
||||
systemDashboardLoading.value = true
|
||||
systemDashboardError.value = null
|
||||
|
||||
try {
|
||||
systemDashboardPayload.value = await fetchSystemDashboard({ days: 7 })
|
||||
} catch (error) {
|
||||
systemDashboardPayload.value = null
|
||||
systemDashboardError.value = error
|
||||
} finally {
|
||||
systemDashboardLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadRiskDashboard = async () => {
|
||||
riskDashboardLoading.value = true
|
||||
riskDashboardError.value = null
|
||||
|
||||
try {
|
||||
riskDashboardPayload.value = await fetchRiskObservationDashboard({
|
||||
windowDays: activeRiskWindowDays.value,
|
||||
limit: 500
|
||||
})
|
||||
} catch (error) {
|
||||
riskDashboardPayload.value = null
|
||||
riskDashboardError.value = error
|
||||
} finally {
|
||||
riskDashboardLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const setRiskWindowDays = (value) => {
|
||||
const days = Number(value || 30)
|
||||
const matched = riskWindowOptions.some((item) => Number(item.value) === days)
|
||||
activeRiskWindowDays.value = matched ? days : 30
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadFinanceDashboard()
|
||||
void loadSystemDashboard()
|
||||
void loadRiskDashboard()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [
|
||||
options.activeRange,
|
||||
options.customRange?.start,
|
||||
options.customRange?.end,
|
||||
activeTrendRange.value,
|
||||
activeDepartmentRange.value
|
||||
],
|
||||
() => {
|
||||
void loadFinanceDashboard()
|
||||
}
|
||||
)
|
||||
|
||||
watch(activeRiskWindowDays, () => {
|
||||
void loadRiskDashboard()
|
||||
})
|
||||
|
||||
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: activeRiskWindowDays.value,
|
||||
totalObservations: 0,
|
||||
pendingCount: 0,
|
||||
highOrAboveCount: 0,
|
||||
confirmedCount: 0,
|
||||
falsePositiveCount: 0,
|
||||
totalAmount: 0,
|
||||
averageScore: 0,
|
||||
confirmationRate: 0,
|
||||
falsePositiveRate: 0,
|
||||
candidateRuleCount: 0,
|
||||
levelDistribution: {},
|
||||
statusDistribution: {},
|
||||
signalDistribution: {},
|
||||
sourceDistribution: {},
|
||||
automationDistribution: {},
|
||||
departmentDistribution: {},
|
||||
expenseTypeDistribution: {},
|
||||
riskTypeDistribution: {},
|
||||
supplierDistribution: {},
|
||||
employeeGradeDistribution: {},
|
||||
dailyTrend: [],
|
||||
topRiskSignals: [],
|
||||
topDepartments: [],
|
||||
topEmployees: [],
|
||||
topSuppliers: [],
|
||||
topExpenseTypes: [],
|
||||
topRules: [],
|
||||
recentHighObservations: []
|
||||
}
|
||||
))
|
||||
const financeDashboardTotals = computed(() => (
|
||||
financeDashboardPayload.value?.totals || demoTotals
|
||||
))
|
||||
const financeMetricMeta = computed(() => (
|
||||
financeDashboardPayload.value?.metricMeta || {}
|
||||
))
|
||||
const financeTrend = computed(() => (
|
||||
financeDashboardPayload.value?.trend || trendSeries[activeTrendRange.value]
|
||||
))
|
||||
const financeSpendByCategory = computed(() => (
|
||||
financeDashboardPayload.value?.spendByCategory || fallbackSpendByCategory
|
||||
))
|
||||
const financeExceptionMix = computed(() => (
|
||||
financeDashboardPayload.value?.exceptionMix || fallbackExceptionMix
|
||||
))
|
||||
const financeDepartmentRanking = computed(() => (
|
||||
financeDashboardPayload.value?.departmentRanking || demoDepartments
|
||||
))
|
||||
const financeBottlenecks = computed(() => (
|
||||
financeDashboardPayload.value?.bottlenecks || fallbackBottlenecks
|
||||
))
|
||||
const financeBudgetSummary = computed(() => (
|
||||
financeDashboardPayload.value?.budgetSummary || fallbackBudgetSummary
|
||||
))
|
||||
|
||||
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: metric.change,
|
||||
delta: metric.delta,
|
||||
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 = demoTotals[metric.key]
|
||||
const rawValue = Number(financeDashboardTotals.value[metric.key] || 0)
|
||||
const displayValue = formatMetricValue(metric, rawValue)
|
||||
const metricMeta = resolveFinanceMetricMeta(metric)
|
||||
|
||||
return {
|
||||
...metric,
|
||||
...metricMeta,
|
||||
displayValue,
|
||||
changeText: metric.change,
|
||||
delay: index * 55
|
||||
}
|
||||
}))
|
||||
|
||||
const activeTrend = computed(() => trendSeries[activeTrendRange.value])
|
||||
const spendTotal = computed(() => spendByCategory.reduce((sum, item) => sum + item.value, 0))
|
||||
const riskTotal = computed(() => exceptionMix.reduce((sum, item) => sum + item.value, 0))
|
||||
const systemKpiMetrics = computed(() => systemMetricBlueprints.map((metric, index) => {
|
||||
const rawValue = systemDashboardTotals.value[metric.key]
|
||||
const displayValue = formatSystemMetricValue(metric, rawValue)
|
||||
const metricMeta = resolveSystemMetricMeta(metric)
|
||||
|
||||
const spendLegend = computed(() => spendByCategory.map((item) => ({
|
||||
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 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 spendCenterValue = computed(() => formatCurrency(Math.round(spendTotal.value)))
|
||||
|
||||
const spendLegend = computed(() => financeSpendByCategory.value.map((item) => ({
|
||||
...item,
|
||||
display: `${Math.round((item.value / spendTotal.value) * 100)}%`
|
||||
display: spendTotal.value ? `${Math.round((Number(item.value || 0) / spendTotal.value) * 100)}%` : '0%'
|
||||
})))
|
||||
|
||||
const riskLegend = computed(() => exceptionMix.map((item) => ({
|
||||
const riskLegend = computed(() => financeExceptionMix.value.map((item) => ({
|
||||
...item,
|
||||
display: `${item.value} 单`
|
||||
})))
|
||||
|
||||
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 = demoDepartments
|
||||
const rows = financeDepartmentRanking.value.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) => ({
|
||||
@@ -88,25 +479,215 @@ export function useOverviewView() {
|
||||
}))
|
||||
})
|
||||
|
||||
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 riskSourceLegend = computed(() => buildRiskDistributionLegend(
|
||||
riskDashboard.value.sourceDistribution,
|
||||
{
|
||||
financial_risk_graph: '风险图谱',
|
||||
rule_center: '规则中心',
|
||||
unknown: '未知来源'
|
||||
},
|
||||
{
|
||||
financial_risk_graph: 'var(--theme-primary)',
|
||||
rule_center: '#0f766e',
|
||||
unknown: '#94a3b8'
|
||||
}
|
||||
))
|
||||
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)}%`
|
||||
}))
|
||||
})
|
||||
|
||||
function buildRiskDistributionLegend(distribution, labels, colors) {
|
||||
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]) => ({
|
||||
name: labels[key] || formatRiskSignalName(key),
|
||||
value: Number(value || 0),
|
||||
display: `${Number(value || 0)}项`,
|
||||
color: colors[key] || 'var(--theme-primary)'
|
||||
}))
|
||||
}
|
||||
|
||||
function formatRiskSignalName(value) {
|
||||
const text = String(value || '').trim()
|
||||
const labels = {
|
||||
duplicate_invoice: '重复发票',
|
||||
split_billing: '拆分报销',
|
||||
frequent_small_claims: '高频小额',
|
||||
location_mismatch: '地点不一致',
|
||||
amount_outlier: '金额异常',
|
||||
preapproval_absent: '缺少事前申请'
|
||||
}
|
||||
return labels[text] || text.replace(/_/g, ' ') || '未知风险'
|
||||
}
|
||||
|
||||
const bottlenecks = financeBottlenecks
|
||||
const budgetSummary = financeBudgetSummary
|
||||
const spendByCategory = financeSpendByCategory
|
||||
const exceptionMix = financeExceptionMix
|
||||
|
||||
return {
|
||||
activeDepartmentRange,
|
||||
activeRiskWindowDays,
|
||||
activeTrend,
|
||||
activeTrendRange,
|
||||
bottlenecks,
|
||||
budgetSummary,
|
||||
departmentRangeOptions,
|
||||
exceptionMix,
|
||||
financeDashboardError,
|
||||
financeDashboardLoading,
|
||||
formatCompact,
|
||||
formatCurrency,
|
||||
formatMetricValue,
|
||||
formatNumberCompact,
|
||||
formatSystemMetricValue,
|
||||
kpiMetrics,
|
||||
metricBlueprints,
|
||||
rankedDepartments,
|
||||
riskDashboard,
|
||||
riskDashboardError,
|
||||
riskDashboardLoading,
|
||||
riskDailyTrendRows,
|
||||
riskLegend,
|
||||
riskKpiMetrics,
|
||||
riskLevelLegend,
|
||||
riskSignalRanking,
|
||||
riskSourceLegend,
|
||||
riskTotal,
|
||||
riskWindowOptions,
|
||||
setRiskWindowDays,
|
||||
spendByCategory,
|
||||
spendCenterValue,
|
||||
spendLegend,
|
||||
spendTotal,
|
||||
systemDashboardTotals,
|
||||
systemDashboardError,
|
||||
systemDashboardLoading,
|
||||
systemAgentDailyRatio,
|
||||
systemLoginWave,
|
||||
systemTokenDailyWave,
|
||||
systemUsageDurationRows,
|
||||
systemUsageDurationSummary,
|
||||
systemUserTokenUsage,
|
||||
systemAccuracyComparison,
|
||||
systemExecutionLegend,
|
||||
systemExecutionMix,
|
||||
systemExecutionTotal,
|
||||
systemFeedbackSummary,
|
||||
systemKpiMetrics,
|
||||
systemLoadHeatmap,
|
||||
systemExecutionRows,
|
||||
systemMetricBlueprints,
|
||||
systemModelUsageRows,
|
||||
systemToolDetailItems,
|
||||
systemToolCallLegend,
|
||||
systemToolCallMix,
|
||||
systemToolRankingItems,
|
||||
systemToolRankings,
|
||||
systemToolTotal,
|
||||
systemTrendSeries,
|
||||
trendRanges,
|
||||
trendSeries
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user