feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
515
web/src/components/dashboard/DigitalEmployeeDashboard.vue
Normal file
515
web/src/components/dashboard/DigitalEmployeeDashboard.vue
Normal file
@@ -0,0 +1,515 @@
|
||||
<template>
|
||||
<section class="digital-employee-dashboard">
|
||||
<article class="panel dashboard-card digital-work-trend-panel">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>每日工作趋势 <i class="mdi mdi-information-outline"></i></h3>
|
||||
<p class="card-subtitle">按天统计后台分析、整理、积累和评估任务的执行次数与业务产出。</p>
|
||||
</div>
|
||||
<span class="dashboard-window">近 {{ dashboard.windowDays }} 天</span>
|
||||
</div>
|
||||
<div v-if="loading" class="digital-dashboard-state">
|
||||
<i class="mdi mdi-loading mdi-spin"></i>
|
||||
<span>正在加载数字员工看板数据</span>
|
||||
</div>
|
||||
<div v-else-if="error" class="digital-dashboard-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
<DigitalEmployeeDailyWorkChart v-else :rows="dailyRows" />
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card digital-work-day-panel">
|
||||
<div class="card-head">
|
||||
<h3>每日工作摘要 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<div class="digital-day-list">
|
||||
<div
|
||||
v-for="row in dailyRows"
|
||||
:key="row.date"
|
||||
class="digital-day-row"
|
||||
>
|
||||
<span class="digital-day-date">{{ row.date }}</span>
|
||||
<div class="digital-day-main">
|
||||
<strong>{{ row.total }} 次工作</strong>
|
||||
<small>成功 {{ row.success }},失败 {{ row.failed }},产出 {{ row.businessOutputs }} 项</small>
|
||||
</div>
|
||||
<span class="digital-day-output">{{ row.businessOutputs }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card digital-category-panel">
|
||||
<div class="card-head">
|
||||
<h3>技能类型分布 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<DonutChart
|
||||
:items="categoryLegend"
|
||||
:center-value="String(dashboard.totals.totalRuns || 0)"
|
||||
center-label="工作次数"
|
||||
/>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card digital-task-panel">
|
||||
<div class="card-head">
|
||||
<h3>工作模块排行 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<BarChart
|
||||
v-if="taskRanking.length"
|
||||
:items="taskRanking"
|
||||
value-prefix=""
|
||||
value-suffix="次"
|
||||
/>
|
||||
<div v-else class="digital-dashboard-empty">
|
||||
<i class="mdi mdi-clipboard-check-outline"></i>
|
||||
<span>当前周期暂无工作记录</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card digital-output-panel">
|
||||
<div class="card-head">
|
||||
<h3>业务产出 <i class="mdi mdi-information-outline"></i></h3>
|
||||
</div>
|
||||
<div class="digital-output-grid">
|
||||
<div v-for="item in outputItems" :key="item.label" class="digital-output-item">
|
||||
<span :style="{ color: item.color }"><i :class="item.icon"></i></span>
|
||||
<div>
|
||||
<strong>{{ item.value }}</strong>
|
||||
<small>{{ item.label }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel dashboard-card digital-recent-panel">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>最近工作记录 <i class="mdi mdi-information-outline"></i></h3>
|
||||
<p class="card-subtitle">展示最近完成或正在执行的后台任务,具体详情仍在数字员工工作记录中处理。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="recentRuns.length" class="digital-recent-table">
|
||||
<div class="digital-recent-head">
|
||||
<span>时间</span>
|
||||
<span>工作模块</span>
|
||||
<span>技能类型</span>
|
||||
<span>状态</span>
|
||||
<span>摘要</span>
|
||||
<span>产出</span>
|
||||
</div>
|
||||
<div v-for="run in recentRuns" :key="run.runId" class="digital-recent-row">
|
||||
<span>{{ formatDateTime(run.startedAt) }}</span>
|
||||
<strong>{{ run.taskLabel }}</strong>
|
||||
<span>{{ run.category }}</span>
|
||||
<span class="digital-status-pill" :class="run.statusTone">{{ run.statusLabel }}</span>
|
||||
<span class="digital-recent-summary">{{ run.summary }}</span>
|
||||
<span>{{ formatRunMetrics(run.metrics) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="digital-dashboard-empty compact">
|
||||
<i class="mdi mdi-clipboard-text-clock-outline"></i>
|
||||
<span>暂无最近工作记录</span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
import BarChart from '../charts/BarChart.vue'
|
||||
import DigitalEmployeeDailyWorkChart from '../charts/DigitalEmployeeDailyWorkChart.vue'
|
||||
import DonutChart from '../charts/DonutChart.vue'
|
||||
|
||||
const props = defineProps({
|
||||
dashboard: { type: Object, required: true },
|
||||
loading: { type: Boolean, default: false },
|
||||
error: { type: Object, default: () => null },
|
||||
dailyRows: { type: Array, default: () => [] },
|
||||
taskRanking: { type: Array, default: () => [] },
|
||||
categoryRows: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const errorMessage = computed(() => props.error?.message || '数字员工看板数据加载失败')
|
||||
const recentRuns = computed(() => props.dashboard.recentRuns || [])
|
||||
const categoryLegend = computed(() => {
|
||||
const rows = props.categoryRows
|
||||
.filter((item) => Number(item.value || item.count || 0) > 0)
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
value: Number(item.value || item.count || 0),
|
||||
color: item.color,
|
||||
display: `${Number(item.value || item.count || 0)}次`
|
||||
}))
|
||||
|
||||
if (rows.length) {
|
||||
return rows
|
||||
}
|
||||
|
||||
return [{ name: '暂无数据', value: 1, display: '0次', color: '#cbd5e1' }]
|
||||
})
|
||||
|
||||
const outputItems = computed(() => {
|
||||
const totals = props.dashboard.totals || {}
|
||||
return [
|
||||
{
|
||||
label: '风险观察',
|
||||
value: Number(totals.riskObservations || 0),
|
||||
icon: 'mdi mdi-shield-search',
|
||||
color: 'var(--theme-primary)'
|
||||
},
|
||||
{
|
||||
label: '风险线索',
|
||||
value: Number(totals.riskClues || 0),
|
||||
icon: 'mdi mdi-alert-decagram-outline',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
label: '画像快照',
|
||||
value: Number(totals.profileSnapshots || 0),
|
||||
icon: 'mdi mdi-account-search-outline',
|
||||
color: '#2563eb'
|
||||
},
|
||||
{
|
||||
label: '知识文档',
|
||||
value: Number(totals.knowledgeDocuments || 0),
|
||||
icon: 'mdi mdi-book-open-page-variant-outline',
|
||||
color: '#0f766e'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return '-'
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value)
|
||||
}
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
|
||||
function formatRunMetrics(metrics = {}) {
|
||||
const rows = [
|
||||
['观察', metrics.risk_observations],
|
||||
['线索', metrics.risk_clues],
|
||||
['快照', metrics.profile_snapshots],
|
||||
['文档', metrics.knowledge_documents]
|
||||
].filter(([, value]) => Number(value || 0) > 0)
|
||||
|
||||
if (!rows.length) {
|
||||
return '0项'
|
||||
}
|
||||
|
||||
return rows.map(([label, value]) => `${label}${Number(value || 0)}`).join(' / ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.digital-employee-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
min-width: 0;
|
||||
padding: 18px;
|
||||
border: 1px solid #edf2f7;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-head h3 {
|
||||
min-width: 0;
|
||||
color: #1e293b;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-head .mdi {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dashboard-window {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.digital-work-trend-panel,
|
||||
.digital-recent-panel {
|
||||
grid-column: span 8;
|
||||
}
|
||||
|
||||
.digital-work-day-panel {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.digital-category-panel,
|
||||
.digital-task-panel,
|
||||
.digital-output-panel {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.digital-dashboard-state,
|
||||
.digital-dashboard-empty {
|
||||
min-height: 220px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.digital-dashboard-empty.compact {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.digital-dashboard-state i,
|
||||
.digital-dashboard-empty i {
|
||||
color: #94a3b8;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.digital-dashboard-state.error {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.digital-day-list {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.digital-day-row {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr) 36px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.digital-day-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.digital-day-date,
|
||||
.digital-day-output {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.digital-day-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.digital-day-main strong,
|
||||
.digital-day-main small {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.digital-day-main strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.digital-day-main small {
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.digital-day-output {
|
||||
text-align: right;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.digital-output-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.digital-output-item {
|
||||
min-width: 0;
|
||||
min-height: 92px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 4px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.digital-output-item > span {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 18px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.digital-output-item div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.digital-output-item strong {
|
||||
display: block;
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
font-weight: 850;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.digital-output-item small {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.digital-recent-table {
|
||||
display: grid;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.digital-recent-head,
|
||||
.digital-recent-row {
|
||||
display: grid;
|
||||
grid-template-columns: 116px 150px 84px 76px minmax(0, 1fr) 150px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.digital-recent-head {
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.digital-recent-row {
|
||||
min-height: 50px;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid #edf2f7;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.digital-recent-row strong,
|
||||
.digital-recent-row span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.digital-recent-row strong {
|
||||
color: #0f172a;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.digital-recent-summary {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.digital-status-pill {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
background: #eef2f7;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.digital-status-pill.success {
|
||||
background: rgba(var(--success-rgb), .10);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.digital-status-pill.danger {
|
||||
background: rgba(239, 68, 68, .10);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.digital-status-pill.warning {
|
||||
background: rgba(245, 158, 11, .12);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.digital-work-trend-panel,
|
||||
.digital-recent-panel,
|
||||
.digital-work-day-panel,
|
||||
.digital-category-panel,
|
||||
.digital-task-panel,
|
||||
.digital-output-panel {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.digital-recent-head,
|
||||
.digital-recent-row {
|
||||
grid-template-columns: 108px 140px 76px 72px minmax(180px, 1fr) 130px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -204,9 +204,10 @@ const effectItems = computed(() => {
|
||||
return [
|
||||
{ label: '规则命中', value: sourceDistribution.rule_center || 0 },
|
||||
{ label: '图谱异常', value: sourceDistribution.financial_risk_graph || 0 },
|
||||
{ label: '待复核线索', value: props.dashboard.riskClueCount || pending },
|
||||
{ label: '反馈样本', value: props.dashboard.feedbackSampleCount || 0 },
|
||||
{ label: '确认率', value: formatPercent(props.dashboard.confirmationRate) },
|
||||
{ label: '误报率', value: formatPercent(props.dashboard.falsePositiveRate) },
|
||||
{ label: '候选规则', value: props.dashboard.candidateRuleCount || 0 },
|
||||
{ label: '完成率', value: formatPercent(processedRate) }
|
||||
]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user