feat(dashboard): reorganize budget and risk cards
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user