- 新增本体字段注册表与字段治理审计脚本 - 重构风险规则模板执行器、DSL 验证与清单分类器 - 完善票据夹服务与差旅请求详情页交互 - 优化趋势图表与总览页数据展示 - 增强报销平台风险分级与模拟公司筛选 - 补充本体字段、风险规则生成与票据夹服务测试覆盖
965 lines
31 KiB
JavaScript
965 lines
31 KiB
JavaScript
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
|
|
}
|
|
}
|