463 lines
17 KiB
Vue
463 lines
17 KiB
Vue
<template>
|
||
<section class="dashboard" :class="[`dashboard-${activeDashboard}`, { 'is-loading': activeDashboardLoading }]">
|
||
<div v-if="activeDashboardLoading" class="dashboard-loading-overlay" role="status" aria-live="polite">
|
||
<i class="mdi mdi-loading mdi-spin"></i>
|
||
<span>{{ activeDashboardLoadingText }}</span>
|
||
</div>
|
||
<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>
|
||
<EnterpriseSelect
|
||
v-model="activeDepartmentRange"
|
||
class="card-select"
|
||
:options="departmentRangeOptions"
|
||
aria-label="个人排行时间范围"
|
||
size="small"
|
||
/>
|
||
</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>
|
||
<span class="card-range-chip">{{ activeDepartmentRange }}</span>
|
||
</div>
|
||
|
||
<div class="top-claim-split">
|
||
<div class="department-employee-mix">
|
||
<DonutChart
|
||
:items="departmentEmployeeLegend"
|
||
:center-value="departmentEmployeeCenterValue"
|
||
center-label="人员占比"
|
||
/>
|
||
</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>
|
||
</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"
|
||
:last-updated-at="riskDashboardLastUpdatedAt"
|
||
: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,
|
||
departmentEmployeeCenterValue,
|
||
departmentEmployeeLegend,
|
||
departmentRangeOptions,
|
||
digitalEmployeeCategoryRows,
|
||
digitalEmployeeDashboard,
|
||
digitalEmployeeDashboardError,
|
||
digitalEmployeeDashboardLoaded,
|
||
digitalEmployeeDashboardLoading,
|
||
digitalEmployeeDailyRows,
|
||
digitalEmployeeKpiMetrics,
|
||
digitalEmployeeTaskRanking,
|
||
financeDashboardLoading,
|
||
financeDashboardLoaded,
|
||
kpiMetrics,
|
||
rankedDepartments,
|
||
rankedEmployees,
|
||
riskDashboard,
|
||
riskDashboardError,
|
||
riskDashboardLoaded,
|
||
riskDashboardLastUpdatedAt,
|
||
riskDashboardLoading,
|
||
riskDailyTrendRows,
|
||
riskKpiMetrics,
|
||
riskLevelLegend,
|
||
riskSignalRanking,
|
||
riskSourceLegend,
|
||
riskWindowOptions,
|
||
setRiskWindowDays,
|
||
spendCenterValue,
|
||
spendLegend,
|
||
systemDashboardLoading,
|
||
systemDashboardLoaded,
|
||
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
|
||
})
|
||
const activeDashboardLoading = computed(() => {
|
||
if (activeDashboard.value === 'system') {
|
||
return systemDashboardLoading.value && !systemDashboardLoaded.value
|
||
}
|
||
if (activeDashboard.value === 'digitalEmployee') {
|
||
return digitalEmployeeDashboardLoading.value && !digitalEmployeeDashboardLoaded.value
|
||
}
|
||
if (activeDashboard.value === 'risk') {
|
||
return riskDashboardLoading.value && !riskDashboardLoaded.value
|
||
}
|
||
return financeDashboardLoading.value && !financeDashboardLoaded.value
|
||
})
|
||
const activeDashboardLoadingText = computed(() => {
|
||
if (activeDashboard.value === 'system') return '正在加载系统看板数据'
|
||
if (activeDashboard.value === 'digitalEmployee') return '正在加载数字员工看板数据'
|
||
if (activeDashboard.value === 'risk') return '正在加载风险看板数据'
|
||
return '正在加载财务看板数据'
|
||
})
|
||
</script>
|
||
|
||
<style scoped src="../assets/styles/views/overview-view.css"></style>
|