Files
X-Financial/web/src/views/OverviewView.vue
caoxiaozhu 0c74b4ab4a feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选
- 引入 expense_claim_status_registry 统一报销状态流转
- 完善报销草稿流程、Item Sync 与本体解析器
- 优化总览页趋势图、分页组件与请求进度步骤
- 增强报销申请快速预览、本体工具与详情展示
- 新增半年报销模拟数据种子脚本与状态审计工具
- 补充财务看板、报销状态注册与模拟数据测试覆盖
2026-06-02 16:22:59 +08:00

413 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section class="dashboard" :class="`dashboard-${activeDashboard}`">
<div class="kpi-grid">
<article
v-for="metric in activeKpiMetrics"
:key="metric.label"
class="kpi-card panel"
:style="{ '--accent': metric.accent, '--delay': `${metric.delay}ms` }"
>
<div class="kpi-head">
<span class="kpi-icon"><i :class="metric.icon"></i></span>
<span class="kpi-label">{{ metric.label }}</span>
</div>
<strong class="kpi-value">{{ metric.displayValue }}</strong>
<div class="kpi-trend">
<span class="kpi-badge" :class="metric.trend">
<i :class="metric.trend === 'down' ? 'mdi mdi-arrow-down' : 'mdi mdi-arrow-up'"></i>
{{ metric.changeText }}
</span>
<span class="kpi-delta">{{ metric.delta }}</span>
</div>
</article>
</div>
<template v-if="activeDashboard === 'finance'">
<div class="content-grid top-grid">
<article class="panel dashboard-card trend-panel">
<div class="card-head">
<h3>每日报销金额 <i class="mdi mdi-information-outline"></i></h3>
<EnterpriseSelect
v-model="activeTrendRange"
class="card-select"
:options="trendRanges"
aria-label="趋势时间范围"
size="small"
/>
</div>
<TrendChart
mode="amount"
:labels="activeTrend.labels"
:claim-count="activeTrend.claimCount"
:claim-amount="activeTrend.claimAmount"
/>
</article>
<article class="panel dashboard-card trend-count-panel">
<div class="card-head">
<h3>每日报销数量 <i class="mdi mdi-information-outline"></i></h3>
</div>
<TrendChart
mode="count"
:labels="activeTrend.labels"
:claim-count="activeTrend.claimCount"
:claim-amount="activeTrend.claimAmount"
/>
</article>
<article class="panel dashboard-card donut-panel">
<div class="card-head">
<h3>费用结构 <i class="mdi mdi-information-outline"></i></h3>
</div>
<DonutChart :items="spendLegend" :center-value="spendCenterValue" center-label="费用总额" />
<p class="panel-note">* 百分比按当前时间范围内的费用金额计算</p>
</article>
</div>
<div class="content-grid bottom-grid">
<article class="panel dashboard-card rank-panel">
<div class="card-head">
<h3>部门报销排行费用金额<i class="mdi mdi-information-outline"></i></h3>
<EnterpriseSelect
v-model="activeDepartmentRange"
class="card-select"
:options="departmentRangeOptions"
aria-label="部门排行时间范围"
size="small"
/>
</div>
<BarChart :items="rankedDepartments" />
</article>
<article class="panel dashboard-card employee-rank-panel">
<div class="card-head">
<h3>个人报销排行本月<i class="mdi mdi-information-outline"></i></h3>
</div>
<BarChart :items="rankedEmployees" />
</article>
<article class="panel dashboard-card top-claim-panel">
<div class="card-head">
<h3>本月高额单据 <i class="mdi mdi-information-outline"></i></h3>
</div>
<div class="top-claim-list">
<div
v-for="item in topClaims"
:key="item.claimNo"
class="top-claim-row"
>
<div>
<strong>{{ item.claimNo }}</strong>
<span>{{ item.employeeName }} · {{ item.departmentName || '未归属部门' }}</span>
</div>
<div>
<strong>{{ item.amountLabel }}</strong>
<span>{{ item.expenseTypeLabel }} · {{ item.statusLabel }}</span>
</div>
</div>
</div>
</article>
<article class="panel dashboard-card budget-metrics-panel">
<div class="card-head">
<h3>预算指标 <i class="mdi mdi-information-outline"></i></h3>
</div>
<div class="budget-metric-grid">
<div
v-for="(item, index) in budgetMetrics"
:key="item.label"
class="budget-metric-item"
:class="item.tone"
:style="{ '--delay': `${index * 70}ms` }"
>
<span class="budget-metric-icon"><i :class="item.icon"></i></span>
<div>
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<em>{{ item.detail }}</em>
</div>
</div>
</div>
</article>
<article class="panel dashboard-card budget-panel">
<div class="card-head">
<h3>预算执行率本月<i class="mdi mdi-information-outline"></i></h3>
</div>
<GaugeChart
:ratio="budgetSummary.ratio"
:total="budgetSummary.total"
:used="budgetSummary.used"
:left="budgetSummary.left"
/>
<button type="button" class="text-link">查看详情 <i class="mdi mdi-chevron-right"></i></button>
</article>
</div>
</template>
<RiskObservationDashboard
v-else-if="activeDashboard === 'risk'"
:dashboard="riskDashboard"
:loading="riskDashboardLoading"
:error="riskDashboardError"
:level-legend="riskLevelLegend"
:source-legend="riskSourceLegend"
:signal-ranking="riskSignalRanking"
:daily-rows="riskDailyTrendRows"
:window-options="riskWindowOptions"
:active-window-days="activeRiskWindowDays"
@update:window-days="setRiskWindowDays"
/>
<DigitalEmployeeDashboard
v-else-if="activeDashboard === 'digitalEmployee'"
:dashboard="digitalEmployeeDashboard"
:loading="digitalEmployeeDashboardLoading"
:error="digitalEmployeeDashboardError"
:daily-rows="digitalEmployeeDailyRows"
:task-ranking="digitalEmployeeTaskRanking"
:category-rows="digitalEmployeeCategoryRows"
/>
<template v-else>
<div class="system-observability-grid">
<article class="panel dashboard-card system-agent-ratio-panel">
<div class="card-head">
<h3>智能体调用占比 <i class="mdi mdi-information-outline"></i></h3>
</div>
<p class="card-subtitle">按天查看几个核心智能体的调用构成比例变化比单纯总量更容易定位偏移</p>
<SystemAgentRatioBar
:labels="systemAgentDailyRatio.labels"
:agents="systemAgentDailyRatio.agents"
:series="systemAgentDailyRatio.series"
/>
</article>
<article class="panel dashboard-card system-token-pie-panel">
<div class="card-head">
<h3>用户 Token 消耗占比 <i class="mdi mdi-information-outline"></i></h3>
</div>
<p class="card-subtitle">左侧看每日 Token 消耗波动右侧看高消耗用户便于排查重复问答或异常长上下文</p>
<div class="system-token-panel-grid">
<SystemTokenDailyWaveChart
:labels="systemTokenDailyWave.labels"
:input-tokens="systemTokenDailyWave.inputTokens"
:output-tokens="systemTokenDailyWave.outputTokens"
:total-tokens="systemTokenDailyWave.totalTokens"
/>
<SystemUserTokenPie :items="systemUserTokenUsage" />
</div>
</article>
<article class="panel dashboard-card system-accuracy-panel">
<div class="card-head">
<h3>正确 / 错误对比 <i class="mdi mdi-information-outline"></i></h3>
</div>
<p class="card-subtitle">按智能体对比正确与错误次数错误柱越靠前越需要优先追踪日志</p>
<SystemAccuracyCompareBar
:categories="systemAccuracyComparison.categories"
:correct="systemAccuracyComparison.correct"
:wrong="systemAccuracyComparison.wrong"
/>
</article>
<article class="panel dashboard-card system-tool-detail-panel">
<div class="card-head">
<h3>工具调用明细 <i class="mdi mdi-information-outline"></i></h3>
</div>
<div class="system-tool-table">
<div
v-for="item in systemToolDetailItems"
:key="item.name"
class="system-tool-row"
>
<div class="system-tool-row-head">
<strong>{{ item.name }}</strong>
<span>{{ item.callLabel }}</span>
</div>
<div class="system-tool-meter" aria-hidden="true">
<i :style="{ width: item.width, background: item.color }"></i>
</div>
<div class="system-tool-row-meta">
<span>成功率 {{ item.successRate }}%</span>
<span>平均 {{ item.avgLatency }}</span>
<span>{{ item.tokenLabel }}</span>
</div>
</div>
</div>
</article>
<aside class="system-side-stack">
<article class="panel dashboard-card system-side-card system-login-wave-panel">
<div class="card-head">
<h3>用户在线波动 <i class="mdi mdi-information-outline"></i></h3>
</div>
<p class="card-subtitle">登录人数与互动次数的时段波动</p>
<SystemLoginWaveChart
compact
:labels="systemLoginWave.labels"
:login-users="systemLoginWave.loginUsers"
:interactions="systemLoginWave.interactions"
/>
</article>
<article class="panel dashboard-card system-side-card system-duration-panel">
<div class="card-head">
<h3>用户使用时长 <i class="mdi mdi-information-outline"></i></h3>
</div>
<div class="duration-summary">
<div>
<strong>{{ systemUsageDurationSummary.average }}</strong>
<span>平均使用时长</span>
</div>
<em>{{ systemUsageDurationSummary.trend }}</em>
</div>
<div class="duration-meta">
<span>中位数 {{ systemUsageDurationSummary.median }}</span>
<span>峰值 {{ systemUsageDurationSummary.peak }}</span>
</div>
<div class="duration-bars">
<div
v-for="item in systemUsageDurationRows"
:key="item.label"
class="duration-bar-row"
>
<span>{{ item.label }}</span>
<i><b :style="{ width: item.width, background: item.color }"></b></i>
<strong>{{ item.value }} </strong>
</div>
</div>
</article>
<article class="panel dashboard-card feedback-panel system-feedback-panel">
<div class="card-head">
<h3>用户反馈概览 <i class="mdi mdi-information-outline"></i></h3>
</div>
<div class="feedback-list">
<div
v-for="item in systemFeedbackSummary"
:key="item.label"
class="feedback-row"
:class="item.tone"
>
<span class="feedback-icon"><i :class="item.icon"></i></span>
<div>
<strong>{{ item.value }}</strong>
<span>{{ item.label }}</span>
</div>
</div>
</div>
<p class="panel-note">* 反馈用于衡量智能体回答和工具执行体验</p>
</article>
</aside>
</div>
</template>
</section>
</template>
<script setup>
import { computed } from 'vue'
import TrendChart from '../components/charts/TrendChart.vue'
import DonutChart from '../components/charts/DonutChart.vue'
import BarChart from '../components/charts/BarChart.vue'
import GaugeChart from '../components/charts/GaugeChart.vue'
import SystemAccuracyCompareBar from '../components/charts/SystemAccuracyCompareBar.vue'
import SystemAgentRatioBar from '../components/charts/SystemAgentRatioBar.vue'
import SystemLoginWaveChart from '../components/charts/SystemLoginWaveChart.vue'
import SystemTokenDailyWaveChart from '../components/charts/SystemTokenDailyWaveChart.vue'
import SystemUserTokenPie from '../components/charts/SystemUserTokenPie.vue'
import DigitalEmployeeDashboard from '../components/dashboard/DigitalEmployeeDashboard.vue'
import RiskObservationDashboard from '../components/dashboard/RiskObservationDashboard.vue'
import EnterpriseSelect from '../components/shared/EnterpriseSelect.vue'
import { useOverviewView } from '../composables/useOverviewView.js'
const props = defineProps({
filteredRequests: { type: Array, required: true },
dashboard: { type: String, default: 'finance' },
activeRange: { type: String, default: '近10日' },
customRange: {
type: Object,
default: () => ({ start: '', end: '' })
}
})
const {
activeDepartmentRange,
activeRiskWindowDays,
activeTrend,
activeTrendRange,
budgetMetrics,
budgetSummary,
departmentRangeOptions,
digitalEmployeeCategoryRows,
digitalEmployeeDashboard,
digitalEmployeeDashboardError,
digitalEmployeeDashboardLoading,
digitalEmployeeDailyRows,
digitalEmployeeKpiMetrics,
digitalEmployeeTaskRanking,
kpiMetrics,
rankedDepartments,
rankedEmployees,
riskDashboard,
riskDashboardError,
riskDashboardLoading,
riskDailyTrendRows,
riskKpiMetrics,
riskLevelLegend,
riskSignalRanking,
riskSourceLegend,
riskWindowOptions,
setRiskWindowDays,
spendCenterValue,
spendLegend,
systemAccuracyComparison,
systemAgentDailyRatio,
systemFeedbackSummary,
systemKpiMetrics,
systemLoginWave,
systemTokenDailyWave,
systemToolDetailItems,
systemUsageDurationRows,
systemUsageDurationSummary,
systemUserTokenUsage,
topClaims,
trendRanges
} = useOverviewView(props)
const activeDashboard = computed(() => {
if (props.dashboard === 'system') return 'system'
if (props.dashboard === 'risk') return 'risk'
if (props.dashboard === 'digitalEmployee') return 'digitalEmployee'
return 'finance'
})
const activeKpiMetrics = computed(() => {
if (activeDashboard.value === 'system') return systemKpiMetrics.value
if (activeDashboard.value === 'digitalEmployee') return digitalEmployeeKpiMetrics.value
if (activeDashboard.value === 'risk') return riskKpiMetrics.value
return kpiMetrics.value
})
</script>
<style scoped src="../assets/styles/views/overview-view.css"></style>