feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -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>
|
||||
|
||||
154
web/src/components/charts/DigitalEmployeeDailyWorkChart.vue
Normal file
154
web/src/components/charts/DigitalEmployeeDailyWorkChart.vue
Normal 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>
|
||||
Reference in New Issue
Block a user