feat(dashboard): reorganize budget and risk cards

This commit is contained in:
caoxiaozhu
2026-06-03 10:47:11 +08:00
parent faa39e6c06
commit 27dd2f0a0d
10 changed files with 554 additions and 564 deletions

View File

@@ -6,10 +6,7 @@ import {
fetchSystemDashboard
} from '../services/analytics.js'
import { fetchRiskObservationDashboard } from '../services/riskObservations.js'
import {
formatRiskSignalLabel,
formatRiskSourceLabel
} from '../utils/riskLabels.js'
import { formatRiskSignalLabel } from '../utils/riskLabels.js'
import {
buildDigitalEmployeeCategoryRows,
buildDigitalEmployeeDailyRows,
@@ -21,8 +18,6 @@ import {
import {
metricBlueprints,
systemMetricBlueprints,
trendRanges,
departmentRangeOptions,
systemDashboardTotals as fallbackSystemDashboardTotals,
systemAgentDailyRatio as fallbackSystemAgentDailyRatio,
systemLoginWave as fallbackSystemLoginWave,
@@ -40,6 +35,9 @@ import {
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,
@@ -78,6 +76,63 @@ const emptyFinanceBudgetMetrics = [
{ 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`
}
return key || DEFAULT_OVERVIEW_RANGE
}
export function useOverviewView(options = {}) {
const activeDashboardKey = computed(() => {
const dashboard = String(options.dashboard || '').trim()
@@ -86,14 +141,17 @@ export function useOverviewView(options = {}) {
if (dashboard === 'digitalEmployee') return 'digitalEmployee'
return 'finance'
})
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 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)
@@ -154,16 +212,16 @@ export function useOverviewView(options = {}) {
}
const getFinanceRangeParams = () => {
const activeRange = String(options.activeRange || '近10日')
const customRange = options.customRange || {}
const activeRange = activeRangeValue.value
const customRange = customRangeValue.value
const isCustomRange = activeRange === 'custom'
return {
rangeKey: activeRange,
startDate: isCustomRange ? customRange.start : '',
endDate: isCustomRange ? customRange.end : '',
trendRange: activeTrendRange.value,
departmentRange: activeDepartmentRange.value
trendRange: resolveTopRangeKey(activeRange, customRange),
departmentRange: resolveTopRangeKey(activeRange, customRange)
}
}
@@ -186,7 +244,7 @@ export function useOverviewView(options = {}) {
systemDashboardError.value = null
try {
systemDashboardPayload.value = await fetchSystemDashboard({ days: 7 })
systemDashboardPayload.value = await fetchSystemDashboard({ days: topRangeDays.value })
} catch (error) {
systemDashboardPayload.value = null
systemDashboardError.value = error
@@ -202,7 +260,7 @@ export function useOverviewView(options = {}) {
try {
const payload = await fetchRiskObservationDashboard({
windowDays: activeRiskWindowDays.value,
windowDays: topRangeDays.value,
limit: 500
})
if (requestSeq !== riskDashboardRequestSeq) {
@@ -249,7 +307,7 @@ export function useOverviewView(options = {}) {
try {
digitalEmployeeDashboardPayload.value = await fetchDigitalEmployeeDashboard({
days: 7,
days: topRangeDays.value,
limit: 300
})
} catch (error) {
@@ -260,12 +318,6 @@ export function useOverviewView(options = {}) {
}
}
const setRiskWindowDays = (value) => {
const days = Number(value || 30)
const matched = riskWindowOptions.some((item) => Number(item.value) === days)
activeRiskWindowDays.value = matched ? days : 30
}
const loadActiveDashboard = () => {
if (activeDashboardKey.value === 'system') {
void loadSystemDashboard()
@@ -299,23 +351,13 @@ export function useOverviewView(options = {}) {
() => [
options.activeRange,
options.customRange?.start,
options.customRange?.end,
activeTrendRange.value,
activeDepartmentRange.value
options.customRange?.end
],
() => {
if (activeDashboardKey.value === 'finance') {
void loadFinanceDashboard()
}
loadActiveDashboard()
}
)
watch(activeRiskWindowDays, () => {
if (activeDashboardKey.value === 'risk') {
void loadRiskDashboard()
}
})
watch(activeDashboardKey, () => {
loadActiveDashboard()
})
@@ -349,7 +391,7 @@ export function useOverviewView(options = {}) {
))
const riskDashboard = computed(() => (
riskDashboardPayload.value || {
windowDays: activeRiskWindowDays.value,
windowDays: topRangeDays.value,
totalObservations: 0,
pendingCount: 0,
riskClueCount: 0,
@@ -724,20 +766,28 @@ export function useOverviewView(options = {}) {
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'
},
formatRiskSourceLabel
))
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
@@ -781,6 +831,7 @@ export function useOverviewView(options = {}) {
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)
@@ -795,11 +846,11 @@ export function useOverviewView(options = {}) {
]
}
return entries.map(([key, value]) => ({
return entries.map(([key, value], index) => ({
name: labels[key] || formatter(key),
value: Number(value || 0),
display: `${Number(value || 0)}`,
color: colors[key] || 'var(--theme-primary)'
color: colors[key] || fallbackColors[index % fallbackColors.length]
}))
}
@@ -819,16 +870,13 @@ export function useOverviewView(options = {}) {
const exceptionMix = financeExceptionMix
return {
activeDepartmentRange,
activeRiskWindowDays,
activeRangeLabel,
activeTrend,
activeTrendRange,
bottlenecks,
budgetMetrics,
budgetSummary,
departmentEmployeeCenterValue,
departmentEmployeeLegend,
departmentRangeOptions,
digitalEmployeeCategoryRows,
digitalEmployeeDashboard,
digitalEmployeeDashboardError,
@@ -860,10 +908,8 @@ export function useOverviewView(options = {}) {
riskKpiMetrics,
riskLevelLegend,
riskSignalRanking,
riskSourceLegend,
riskCompositionLegend,
riskTotal,
riskWindowOptions,
setRiskWindowDays,
spendByCategory,
spendCenterValue,
spendLegend,
@@ -895,7 +941,6 @@ export function useOverviewView(options = {}) {
systemToolRankings,
systemToolTotal,
systemTrendSeries,
topClaims,
trendRanges
topClaims
}
}