feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -5,14 +5,14 @@
<script setup>
import { computed, shallowRef } from 'vue'
import { BarChart as EChartsBarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { useEcharts } from '../../composables/useEcharts.js'
import { useThemeColors } from '../../composables/useThemeColors.js'
use([GridComponent, TooltipComponent, EChartsBarChart, CanvasRenderer])
use([GridComponent, LegendComponent, TooltipComponent, EChartsBarChart, CanvasRenderer])
const props = defineProps({
labels: { type: Array, required: true },
@@ -60,17 +60,13 @@ const availablePercent = computed(() =>
props.budget.map((total, index) => percent(availableAmountSeries.value[index], total))
)
const yAxisMax = computed(() => {
const maxUsage = Math.max(
100,
...usedPercent.value.map((value, index) => value + Number(occupiedPercent.value[index] || 0))
)
return Math.ceil(maxUsage / 20) * 20
})
const usagePercent = computed(() =>
usedPercent.value.map((value, index) => Number((value + Number(occupiedPercent.value[index] || 0)).toFixed(2)))
)
const ariaLabel = computed(() =>
props.labels.map((label, index) => (
`${label}预算${currency(props.budget[index])},已使用${usedPercent.value[index] || 0}%,已占用${occupiedPercent.value[index] || 0}%`
`${label}预算${currency(props.budget[index])},已发生${usedPercent.value[index] || 0}%,已占用${occupiedPercent.value[index] || 0}%,剩余${availablePercent.value[index] || 0}%`
)).join('')
)
@@ -84,27 +80,43 @@ function buildSeriesData(percentValues, amountValues) {
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: {
duration: prefersReducedMotion() ? 0 : 760,
easing: 'easeOutQuart'
duration: prefersReducedMotion() ? 0 : 820,
easing: 'cubicOut'
},
legend: {
top: 0,
left: 0,
itemWidth: 9,
itemHeight: 9,
itemGap: 16,
textStyle: {
color: '#475569',
fontSize: 12,
fontWeight: 700
}
},
grid: {
top: 12,
right: 16,
bottom: 24,
left: 34,
containLabel: true
top: 38,
right: 88,
bottom: 14,
left: 58
},
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
axisPointer: { type: 'shadow' },
backgroundColor: '#ffffff',
borderColor: '#e2e8f0',
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(58, 124, 165, 0.06)'
}
},
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.24)',
borderWidth: 1,
padding: [10, 12],
padding: [9, 10],
textStyle: {
color: '#475569',
color: '#334155',
fontSize: 12,
fontWeight: 700
},
@@ -117,45 +129,59 @@ const chartOptions = computed(() => ({
const amount = currency(item?.data?.amount || 0)
return `${item.marker}${item.seriesName}: ${percentValue}%(¥${amount}`
})
return [`${items[0]?.axisValue || ''}`, ...lines, `预算总额: ¥${currency(props.budget[index])}`].join('<br/>')
return [
`${items[0]?.axisValue || ''}`,
...lines,
`总占用率: ${usagePercent.value[index] || 0}%`,
`预算总额: ¥${currency(props.budget[index])}`
].join('<br/>')
}
},
xAxis: {
type: 'category',
data: props.labels,
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: '#64748b',
fontSize: 12,
fontWeight: 700
}
},
yAxis: {
type: 'value',
min: 0,
max: yAxisMax.value,
splitNumber: Math.max(1, Math.ceil(yAxisMax.value / 20)),
max: 100,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: '#64748b',
fontSize: 12,
fontSize: 11,
fontWeight: 700,
formatter: (value) => `${Number(value)}%`
},
splitLine: { lineStyle: { color: '#edf2f7' } }
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.72)' } }
},
yAxis: {
type: 'category',
data: props.labels,
inverse: true,
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: '#334155',
fontSize: 12,
fontWeight: 800
}
},
series: [
{
name: '已使用',
name: '已发生',
type: 'bar',
stack: 'budgetUsage',
data: buildSeriesData(usedPercent.value, props.used),
barWidth: 16,
barWidth: 20,
emphasis: { focus: 'series' },
label: {
show: true,
position: 'inside',
color: '#ffffff',
fontSize: 11,
fontWeight: 800,
formatter: ({ value }) => Number(value) >= 10 ? `${Number(value).toFixed(1)}%` : ''
},
itemStyle: {
color: themeColors.value.chartPrimary,
borderRadius: [4, 4, 0, 0]
borderRadius: [4, 0, 0, 4]
}
},
{
@@ -163,10 +189,20 @@ const chartOptions = computed(() => ({
type: 'bar',
stack: 'budgetUsage',
data: buildSeriesData(occupiedPercent.value, props.occupied),
barWidth: 16,
barWidth: 20,
emphasis: { focus: 'series' },
label: {
show: true,
position: 'inside',
color: '#ffffff',
fontSize: 11,
fontWeight: 800,
formatter: ({ value }) => Number(value) >= 10 ? `${Number(value).toFixed(1)}%` : ''
},
itemStyle: {
color: themeColors.value.warning,
borderRadius: [4, 4, 0, 0]
color: themeColors.value.chartAmber || themeColors.value.warning,
borderColor: '#ffffff',
borderWidth: 1
}
},
{
@@ -174,10 +210,22 @@ const chartOptions = computed(() => ({
type: 'bar',
stack: 'budgetUsage',
data: buildSeriesData(availablePercent.value, availableAmountSeries.value),
barWidth: 16,
barWidth: 20,
emphasis: { focus: 'series' },
label: {
show: true,
position: 'right',
distance: 10,
color: '#334155',
fontSize: 12,
fontWeight: 850,
formatter: ({ dataIndex }) => `${usagePercent.value[dataIndex] || 0}%`
},
itemStyle: {
color: '#e5edf3',
borderRadius: [4, 4, 0, 0]
color: '#e8eef5',
borderColor: '#ffffff',
borderWidth: 1,
borderRadius: [0, 4, 4, 0]
}
}
]
@@ -190,6 +238,6 @@ useEcharts(chartElement, chartOptions)
.budget-trend-chart {
position: relative;
width: 100%;
height: 220px;
height: 252px;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<div ref="chartElement" class="digital-employee-daily-work-chart" role="img" :aria-label="ariaLabel"></div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { BarChart as EChartsBarChart, LineChart as EChartsLineChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { useEcharts } from '../../composables/useEcharts.js'
import { useThemeColors } from '../../composables/useThemeColors.js'
use([
GridComponent,
LegendComponent,
TooltipComponent,
EChartsBarChart,
EChartsLineChart,
CanvasRenderer
])
const props = defineProps({
rows: { type: Array, default: () => [] }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const labels = computed(() => props.rows.map((item) => item.date))
const totals = computed(() => props.rows.map((item) => Number(item.total || 0)))
const failed = computed(() => props.rows.map((item) => Number(item.failed || 0)))
const outputs = computed(() => props.rows.map((item) => Number(item.businessOutputs || 0)))
const maxValue = computed(() => Math.max(...totals.value, ...failed.value, ...outputs.value, 1))
const axisMax = computed(() => Math.max(5, Math.ceil(maxValue.value * 1.2)))
const ariaLabel = computed(() =>
props.rows.map((item) => (
`${item.date}工作${item.total || 0}次,失败${item.failed || 0}次,产出${item.businessOutputs || 0}`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 900,
animationDurationUpdate: 700,
grid: {
top: 34,
right: 18,
bottom: 24,
left: 30,
containLabel: true
},
legend: {
top: 0,
right: 4,
itemWidth: 8,
itemHeight: 8,
textStyle: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
}
},
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255,255,255,.98)',
borderColor: 'rgba(148,163,184,.24)',
borderWidth: 1,
padding: [9, 10],
textStyle: {
color: '#334155',
fontSize: 12,
fontWeight: 700
},
extraCssText: 'border-radius:4px;box-shadow:0 12px 28px rgba(15,23,42,.12);'
},
xAxis: {
type: 'category',
data: labels.value,
boundaryGap: true,
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(148,163,184,.26)' } },
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
}
},
yAxis: {
type: 'value',
min: 0,
max: axisMax.value,
splitNumber: 4,
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
},
splitLine: { lineStyle: { color: 'rgba(226,232,240,.74)' } }
},
series: [
{
name: '工作次数',
type: 'bar',
data: totals.value,
barWidth: 18,
itemStyle: {
color: themeColors.value.chartPrimary,
borderRadius: [4, 4, 0, 0]
}
},
{
name: '失败次数',
type: 'bar',
data: failed.value,
barWidth: 18,
itemStyle: {
color: '#ef4444',
borderRadius: [4, 4, 0, 0]
}
},
{
name: '业务产出',
type: 'line',
data: outputs.value,
smooth: true,
symbol: 'circle',
symbolSize: 7,
lineStyle: {
width: 2.5,
color: '#0f766e'
},
itemStyle: {
color: '#ffffff',
borderColor: '#0f766e',
borderWidth: 2.5
}
}
]
}))
useEcharts(chartElement, chartOptions)
</script>
<style scoped>
.digital-employee-daily-work-chart {
width: 100%;
height: 250px;
}
</style>