200 lines
6.1 KiB
JavaScript
200 lines
6.1 KiB
JavaScript
|
|
import { formatRiskSignalLabel } from '../utils/riskLabels.js'
|
||
|
|
|
||
|
|
export const emptyFinanceTotals = {
|
||
|
|
reimbursementAmount: 0,
|
||
|
|
reimbursementCount: 0,
|
||
|
|
pendingPaymentAmount: 0,
|
||
|
|
avgClaimAmount: 0,
|
||
|
|
budgetUsageRate: 0,
|
||
|
|
paymentClearanceRate: 0
|
||
|
|
}
|
||
|
|
|
||
|
|
export const emptyFinanceTrend = {
|
||
|
|
labels: [],
|
||
|
|
claimCount: [],
|
||
|
|
claimAmount: [],
|
||
|
|
categoryAmountSeries: [],
|
||
|
|
applications: [],
|
||
|
|
approved: [],
|
||
|
|
avgHours: []
|
||
|
|
}
|
||
|
|
|
||
|
|
export const emptyFinanceDonut = [
|
||
|
|
{ name: '暂无数据', value: 0, color: '#cbd5e1' }
|
||
|
|
]
|
||
|
|
|
||
|
|
export const emptyFinanceBudgetSummary = {
|
||
|
|
ratio: 0,
|
||
|
|
total: '¥0',
|
||
|
|
used: '¥0',
|
||
|
|
left: '¥0'
|
||
|
|
}
|
||
|
|
|
||
|
|
export const emptyFinanceBudgetMetrics = [
|
||
|
|
{ label: '预算池数量', value: '0 个', detail: '年度有效预算池', tone: 'neutral', icon: 'mdi mdi-database-outline' },
|
||
|
|
{ label: '总预算', value: '¥0', detail: '原始预算 + 调整', tone: 'neutral', icon: 'mdi mdi-cash-register' },
|
||
|
|
{ label: '已用预算', value: '¥0', detail: '使用率 0.0%', tone: 'success', icon: 'mdi mdi-chart-arc' },
|
||
|
|
{ label: '预占预算', value: '¥0', detail: '待流转单据占用', tone: 'success', icon: 'mdi mdi-lock-outline' },
|
||
|
|
{ label: '可用预算', value: '¥0', detail: '可继续使用额度', tone: 'success', icon: 'mdi mdi-wallet-outline' },
|
||
|
|
{ label: '预警预算池', value: '0 个', detail: '超支 0 个', tone: 'success', icon: 'mdi mdi-alert-outline' }
|
||
|
|
]
|
||
|
|
|
||
|
|
export const emptySystemDashboardTotals = {
|
||
|
|
toolCalls: 0,
|
||
|
|
modelTokens: 0,
|
||
|
|
onlineUsers: 0,
|
||
|
|
avgOnlineMinutes: 0,
|
||
|
|
executionSuccessRate: 0,
|
||
|
|
positiveFeedback: 0,
|
||
|
|
negativeFeedback: 0,
|
||
|
|
failedRuns: 0,
|
||
|
|
toolCallsChange: 0,
|
||
|
|
modelTokensChange: 0
|
||
|
|
}
|
||
|
|
|
||
|
|
export 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)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function 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}`
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatCurrency(value) {
|
||
|
|
return formatCompact(value)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function 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)}`
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatPercent(value) {
|
||
|
|
return `${Math.round(Number(value || 0) * 100)}%`
|
||
|
|
}
|
||
|
|
|
||
|
|
export function 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)}`
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatSystemMetricValue(metric, value, totals = emptySystemDashboardTotals) {
|
||
|
|
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(totals.negativeFeedback || 0))
|
||
|
|
return `${Math.round(numericValue)} / ${negativeFeedback}`
|
||
|
|
}
|
||
|
|
if (metric.unit) return `${formatNumberCompact(numericValue)} ${metric.unit}`
|
||
|
|
return formatNumberCompact(numericValue)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveSystemMetricMeta(metric, totals = emptySystemDashboardTotals, realDashboardLoaded = false) {
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveFinanceMetricMeta({
|
||
|
|
metric,
|
||
|
|
meta,
|
||
|
|
dashboardLoaded,
|
||
|
|
loading,
|
||
|
|
error
|
||
|
|
}) {
|
||
|
|
if (!dashboardLoaded || !meta) {
|
||
|
|
return {
|
||
|
|
changeText: loading ? '加载中' : '实时',
|
||
|
|
delta: error ? '真实数据加载失败' : '等待真实数据',
|
||
|
|
trend: metric.trend
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
changeText: meta.changeText || metric.change,
|
||
|
|
delta: meta.delta || metric.delta,
|
||
|
|
trend: meta.trend || metric.trend
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export 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]
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatRiskSignalName(value) {
|
||
|
|
return formatRiskSignalLabel(value)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isMissingDimension(value) {
|
||
|
|
const text = String(value || '').trim()
|
||
|
|
return !text || ['待补充', '待确认', '未归属部门', '未归属', 'N/A', 'n/a', '-'].includes(text)
|
||
|
|
}
|