feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -29,7 +29,10 @@ import { resolveCssColor, useThemeColors } from '../../composables/useThemeColor
use([GridComponent, TooltipComponent, EChartsBarChart, CanvasRenderer])
const props = defineProps({
items: { type: Array, required: true }
items: { type: Array, required: true },
valuePrefix: { type: String, default: '¥' },
valueSuffix: { type: String, default: '' },
compact: { type: Boolean, default: true }
})
const chartElement = shallowRef(null)
@@ -51,7 +54,13 @@ const ariaLabel = computed(() =>
)
const chartMaxValue = computed(() => Math.max(...resolvedItems.value.map((item) => item.value), 1))
const chartAxisMax = computed(() => Math.ceil((chartMaxValue.value * 1.1) / 10000) * 10000)
const axisStep = computed(() => {
if (chartMaxValue.value <= 100) return 10
if (chartMaxValue.value <= 1000) return 100
if (chartMaxValue.value <= 10000) return 1000
return 10000
})
const chartAxisMax = computed(() => Math.ceil((chartMaxValue.value * 1.1) / axisStep.value) * axisStep.value)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
@@ -115,7 +124,7 @@ const chartOptions = computed(() => ({
value: item.value,
itemStyle: { color: item.resolvedColor }
})),
barWidth: 14,
barWidth: 18,
showBackground: true,
backgroundStyle: {
color: 'rgba(226, 232, 240, 0.42)',
@@ -155,9 +164,11 @@ const medalFill = (idx) => {
const formatValue = (value) => {
const number = Number(value || 0)
if (number >= 1_000_000) return `¥${(number / 1_000_000).toFixed(1)}M`
if (number >= 1_000) return `¥${(number / 1_000).toFixed(1)}K`
return `¥${number}`
const prefix = props.valuePrefix
const suffix = props.valueSuffix
if (props.compact && number >= 1_000_000) return `${prefix}${(number / 1_000_000).toFixed(1)}M${suffix}`
if (props.compact && number >= 1_000) return `${prefix}${(number / 1_000).toFixed(1)}K${suffix}`
return `${prefix}${number}${suffix}`
}
</script>
@@ -180,7 +191,7 @@ const formatValue = (value) => {
display: flex;
align-items: center;
gap: 6px;
height: 34px;
height: 38px;
white-space: nowrap;
}

View File

@@ -2,6 +2,10 @@
<div class="gauge-chart">
<div class="gauge-body">
<div ref="chartElement" class="gauge-canvas" role="img" :aria-label="ariaLabel"></div>
<div class="gauge-center-value" aria-hidden="true">
<strong>{{ normalizedRatio }}%</strong>
<span>已执行</span>
</div>
</div>
<div class="gauge-summary">
<div>
@@ -82,22 +86,8 @@ const chartOptions = computed(() => {
splitLine: { show: false },
axisLabel: { show: false },
anchor: { show: false },
detail: {
show: true,
valueAnimation: true,
offsetCenter: [0, '22%'],
formatter: '{value}%',
color: primary,
fontSize: 24,
fontWeight: 850
},
title: {
show: true,
offsetCenter: [0, '46%'],
color: '#64748b',
fontSize: 11,
fontWeight: 700
},
detail: { show: false },
title: { show: false },
data: [{ value: normalizedRatio.value, name: '已执行' }]
}
]
@@ -127,6 +117,36 @@ useEcharts(chartElement, chartOptions)
height: 100%;
}
.gauge-center-value {
position: absolute;
left: 0;
right: 0;
top: 60%;
display: grid;
justify-items: center;
gap: 4px;
min-width: 0;
transform: translateY(-50%);
text-align: center;
pointer-events: none;
}
.gauge-center-value strong {
color: var(--theme-primary);
font-size: 24px;
font-weight: 850;
line-height: 1;
font-variant-numeric: tabular-nums;
letter-spacing: 0;
}
.gauge-center-value span {
color: #64748b;
font-size: 11px;
font-weight: 700;
line-height: 1;
}
.gauge-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));

View File

@@ -0,0 +1,143 @@
<template>
<div ref="chartElement" class="risk-daily-trend-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 highValues = computed(() => props.rows.map((item) => Number(item.highOrAbove || 0)))
const maxValue = computed(() => Math.max(...totals.value, ...highValues.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.highOrAbove || 0}`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 900,
animationDurationUpdate: 700,
grid: {
top: 34,
right: 16,
bottom: 24,
left: 28,
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: 14,
itemStyle: {
color: themeColors.value.chartPrimary,
borderRadius: [4, 4, 0, 0]
}
},
{
name: '高风险',
type: 'line',
data: highValues.value,
smooth: true,
symbol: 'circle',
symbolSize: 7,
lineStyle: {
width: 2.5,
color: '#ef4444'
},
itemStyle: {
color: '#ffffff',
borderColor: '#ef4444',
borderWidth: 2.5
}
}
]
}))
useEcharts(chartElement, chartOptions)
</script>
<style scoped>
.risk-daily-trend-chart {
width: 100%;
height: 250px;
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<div class="system-accuracy-compare-bar">
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { BarChart as EChartsBarChart } 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, CanvasRenderer])
const props = defineProps({
categories: { type: Array, required: true },
correct: { type: Array, required: true },
wrong: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const ariaLabel = computed(() =>
props.categories.map((name, index) => (
`${name}正确${props.correct[index] || 0}次,错误${props.wrong[index] || 0}`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 900,
animationEasing: 'cubicOut',
legend: {
top: 0,
left: 0,
itemWidth: 9,
itemHeight: 9,
itemGap: 18,
textStyle: {
color: '#475569',
fontSize: 12,
fontWeight: 700
}
},
grid: {
top: 38,
right: 42,
bottom: 18,
left: 88,
containLabel: false
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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);',
valueFormatter: (value) => `${value}`
},
xAxis: {
type: 'value',
min: 0,
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
},
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.74)' } }
},
yAxis: {
type: 'category',
data: props.categories,
inverse: true,
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: '#475569',
fontSize: 12,
fontWeight: 750
}
},
series: [
{
name: '正确',
type: 'bar',
data: props.correct,
barWidth: 18,
itemStyle: {
color: themeColors.value.success,
borderRadius: [0, 4, 4, 0]
},
label: {
show: true,
position: 'right',
color: '#475569',
fontSize: 11,
fontWeight: 800
}
},
{
name: '错误',
type: 'bar',
data: props.wrong,
barWidth: 18,
itemStyle: {
color: themeColors.value.danger,
borderRadius: [0, 4, 4, 0]
},
label: {
show: true,
position: 'right',
color: '#ef4444',
fontSize: 11,
fontWeight: 800
}
}
]
}))
useEcharts(chartElement, chartOptions)
</script>
<style scoped>
.system-accuracy-compare-bar {
height: 292px;
}
.chart-body {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="system-agent-ratio-bar">
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { BarChart as EChartsBarChart } 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 { resolveCssColor, useThemeColors } from '../../composables/useThemeColors.js'
use([GridComponent, LegendComponent, TooltipComponent, EChartsBarChart, CanvasRenderer])
const props = defineProps({
labels: { type: Array, required: true },
agents: { type: Array, required: true },
series: { type: Object, required: true }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const resolvedAgents = computed(() =>
props.agents.map((agent) => ({
...agent,
resolvedColor: resolveCssColor(agent.color, themeColors.value.chartPrimary)
}))
)
const ariaLabel = computed(() =>
props.labels.map((label, dayIndex) => {
const parts = resolvedAgents.value.map((agent) => (
`${agent.name}${props.series[agent.key]?.[dayIndex] || 0}%`
))
return `${label}${parts.join('')}`
}).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 900,
animationEasing: 'cubicOut',
legend: {
top: 0,
left: 0,
itemWidth: 8,
itemHeight: 8,
itemGap: 14,
textStyle: {
color: '#475569',
fontSize: 12,
fontWeight: 700
}
},
grid: {
top: 38,
right: 16,
bottom: 24,
left: 34,
containLabel: true
},
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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);',
valueFormatter: (value) => `${value}%`
},
xAxis: {
type: 'category',
data: props.labels,
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.28)' } },
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
splitNumber: 5,
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700,
formatter: '{value}%'
},
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.75)' } }
},
series: resolvedAgents.value.map((agent, index) => ({
name: agent.name,
type: 'bar',
stack: 'agentRatio',
data: props.series[agent.key] || [],
barWidth: 34,
emphasis: { focus: 'series' },
itemStyle: {
color: agent.resolvedColor,
borderColor: '#ffffff',
borderWidth: index === resolvedAgents.value.length - 1 ? 0 : 1,
borderRadius: index === resolvedAgents.value.length - 1 ? [4, 4, 0, 0] : 0
}
}))
}))
useEcharts(chartElement, chartOptions)
</script>
<style scoped>
.system-agent-ratio-bar {
height: 292px;
}
.chart-body {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div class="system-load-heatmap">
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { HeatmapChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, VisualMapComponent } 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, VisualMapComponent, HeatmapChart, CanvasRenderer])
const props = defineProps({
hours: { type: Array, required: true },
tools: { type: Array, required: true },
values: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const maxValue = computed(() => Math.max(...props.values.map((item) => Number(item[2] || 0)), 1))
const ariaLabel = computed(() =>
props.values.map(([hourIndex, toolIndex, value]) => (
`${props.hours[hourIndex] || ''}${props.tools[toolIndex] || ''}${value || 0}`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 760,
animationEasing: 'cubicOut',
grid: {
top: 20,
right: 18,
bottom: 18,
left: 78,
containLabel: false
},
tooltip: {
position: 'top',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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);',
formatter: (params) => {
const [hourIndex, toolIndex, value] = params.value || []
return `${params.marker}${props.tools[toolIndex] || ''}<br/>${props.hours[hourIndex] || ''}: ${value || 0}`
}
},
visualMap: {
show: false,
min: 0,
max: maxValue.value,
inRange: {
color: [
'#eef6fb',
'#d7e9f3',
themeColors.value.chartPrimary,
themeColors.value.primaryActive
]
}
},
xAxis: {
type: 'category',
data: props.hours,
splitArea: { show: true },
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.28)' } },
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
}
},
yAxis: {
type: 'category',
data: props.tools,
splitArea: { show: true },
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: '#475569',
fontSize: 12,
fontWeight: 750
}
},
series: [
{
name: '调用热力',
type: 'heatmap',
data: props.values,
label: {
show: true,
color: '#ffffff',
fontSize: 10,
fontWeight: 800,
formatter: ({ value }) => value?.[2] || 0
},
itemStyle: {
borderColor: '#ffffff',
borderWidth: 2,
borderRadius: 4
},
emphasis: {
itemStyle: {
borderColor: themeColors.value.primaryActive,
borderWidth: 2
}
}
}
]
}))
useEcharts(chartElement, chartOptions)
</script>
<style scoped>
.system-load-heatmap {
height: 320px;
}
.chart-body {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<div class="system-login-wave-chart" :class="{ 'is-compact': compact }">
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { 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, EChartsLineChart, CanvasRenderer])
const props = defineProps({
labels: { type: Array, required: true },
loginUsers: { type: Array, required: true },
interactions: { type: Array, required: true },
compact: { type: Boolean, default: false }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const chartColors = computed(() => ({
primary: themeColors.value.chartPrimary,
blue: themeColors.value.chartBlue
}))
const ariaLabel = computed(() =>
props.labels.map((label, index) => (
`${label}登录${props.loginUsers[index] || 0}人,互动${props.interactions[index] || 0}`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 950,
animationEasing: 'cubicOut',
legend: {
top: 0,
left: 0,
itemWidth: 18,
itemHeight: 8,
itemGap: 16,
textStyle: {
color: '#475569',
fontSize: 12,
fontWeight: 700
}
},
grid: {
top: 38,
right: 36,
bottom: 24,
left: 32,
containLabel: true
},
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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: props.labels,
boundaryGap: false,
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.28)' } },
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
}
},
yAxis: [
{
type: 'value',
name: '登录',
min: 0,
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
},
nameTextStyle: { color: '#94a3b8', fontSize: 11, fontWeight: 700 },
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.72)' } }
},
{
type: 'value',
name: '互动',
min: 0,
axisLabel: {
color: '#94a3b8',
fontSize: 11,
fontWeight: 700
},
nameTextStyle: { color: '#94a3b8', fontSize: 11, fontWeight: 700 },
splitLine: { show: false }
}
],
series: [
{
name: '登录人数',
type: 'line',
smooth: 0.42,
symbol: 'circle',
symbolSize: 7,
data: props.loginUsers,
lineStyle: {
width: 3,
color: chartColors.value.primary
},
itemStyle: {
color: '#ffffff',
borderColor: chartColors.value.primary,
borderWidth: 2.5
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: toRgba(chartColors.value.primary, 0.18) },
{ offset: 1, color: toRgba(chartColors.value.primary, 0.02) }
]
}
}
},
{
name: '互动次数',
type: 'line',
yAxisIndex: 1,
smooth: 0.5,
symbol: 'emptyCircle',
symbolSize: 6,
data: props.interactions,
lineStyle: {
width: 2.5,
color: chartColors.value.blue,
type: 'dashed'
},
itemStyle: {
color: '#ffffff',
borderColor: chartColors.value.blue,
borderWidth: 2
}
}
]
}))
useEcharts(chartElement, chartOptions)
function toRgba(color, alpha) {
const normalized = String(color || '').trim()
const hex = normalized.replace('#', '')
if (/^[\da-f]{6}$/i.test(hex)) {
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
return `rgba(58, 124, 165, ${alpha})`
}
</script>
<style scoped>
.system-login-wave-chart {
height: 292px;
}
.system-login-wave-chart.is-compact {
height: 188px;
}
.chart-body {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<div class="system-token-daily-wave-chart">
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
</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({
labels: { type: Array, required: true },
inputTokens: { type: Array, required: true },
outputTokens: { type: Array, required: true },
totalTokens: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const chartColors = computed(() => ({
amber: themeColors.value.chartAmber,
purple: themeColors.value.chartPurple,
danger: themeColors.value.chartDanger
}))
const ariaLabel = computed(() =>
props.labels.map((label, index) => (
`${label}输入${formatTokens(props.inputTokens[index])},输出${formatTokens(props.outputTokens[index])},合计${formatTokens(props.totalTokens[index])}`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 920,
animationEasing: 'cubicOut',
legend: {
top: 0,
left: 0,
itemWidth: 9,
itemHeight: 9,
itemGap: 16,
textStyle: {
color: '#475569',
fontSize: 12,
fontWeight: 700
}
},
grid: {
top: 38,
right: 22,
bottom: 24,
left: 34,
containLabel: true
},
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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);',
valueFormatter: (value) => `${formatTokens(value)} tokens`
},
xAxis: {
type: 'category',
data: props.labels,
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.28)' } },
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
}
},
yAxis: {
type: 'value',
min: 0,
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700,
formatter: (value) => `${Math.round(value / 1000)}K`
},
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.74)' } }
},
series: [
{
name: '输入 Tokens',
type: 'bar',
stack: 'tokens',
data: props.inputTokens,
barWidth: 24,
itemStyle: {
color: chartColors.value.amber,
borderRadius: [0, 0, 3, 3]
}
},
{
name: '输出 Tokens',
type: 'bar',
stack: 'tokens',
data: props.outputTokens,
barWidth: 24,
itemStyle: {
color: chartColors.value.purple,
borderRadius: [3, 3, 0, 0]
}
},
{
name: '合计波动',
type: 'line',
data: props.totalTokens,
smooth: 0.45,
symbol: 'circle',
symbolSize: 7,
lineStyle: {
width: 2.8,
color: chartColors.value.danger
},
itemStyle: {
color: '#ffffff',
borderColor: chartColors.value.danger,
borderWidth: 2.4
},
emphasis: { focus: 'series' }
}
]
}))
useEcharts(chartElement, chartOptions)
function formatTokens(value) {
const number = Number(value || 0)
if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(1)}M`
if (number >= 1_000) return `${(number / 1_000).toFixed(1)}K`
return `${Math.round(number)}`
}
</script>
<style scoped>
.system-token-daily-wave-chart {
height: 292px;
}
.chart-body {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<div class="system-token-treemap">
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { TreemapChart } from 'echarts/charts'
import { TooltipComponent } from 'echarts/components'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { useEcharts } from '../../composables/useEcharts.js'
import { resolveCssColor, useThemeColors } from '../../composables/useThemeColors.js'
use([TooltipComponent, TreemapChart, CanvasRenderer])
const props = defineProps({
items: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const normalizedItems = computed(() =>
props.items.map((item) => ({
...item,
value: Number(item.tokens || item.value || 0),
resolvedColor: resolveCssColor(item.color, themeColors.value.chartPrimary)
}))
)
const ariaLabel = computed(() =>
normalizedItems.value.map((item) => `${item.name}${formatTokens(item.value)},占比${item.share}%`).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 760,
animationEasing: 'cubicOut',
tooltip: {
trigger: 'item',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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);',
formatter: (params) => `${params.marker}${params.name}<br/>${formatTokens(params.value)} · ${params.data?.share || 0}%`
},
series: [
{
type: 'treemap',
roam: false,
nodeClick: false,
breadcrumb: { show: false },
top: 4,
right: 4,
bottom: 4,
left: 4,
visibleMin: 300,
leafDepth: 1,
label: {
show: true,
color: '#ffffff',
fontSize: 12,
fontWeight: 800,
lineHeight: 16,
formatter: (params) => `${params.name}\n${formatTokens(params.value)}`
},
itemStyle: {
borderColor: '#ffffff',
borderWidth: 3,
borderRadius: 4,
gapWidth: 3
},
upperLabel: { show: false },
data: normalizedItems.value.map((item) => ({
name: item.name,
value: item.value,
share: item.share,
itemStyle: { color: item.resolvedColor }
}))
}
]
}))
useEcharts(chartElement, chartOptions)
function formatTokens(value) {
const number = Number(value || 0)
if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(1)}M tokens`
if (number >= 1_000) return `${(number / 1_000).toFixed(1)}K tokens`
return `${Math.round(number)} tokens`
}
</script>
<style scoped>
.system-token-treemap {
height: 268px;
}
.chart-body {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,245 @@
<template>
<div class="system-trend-chart">
<div class="chart-legend">
<span><i :style="{ background: chartColors.primary }"></i>工具调用</span>
<span><i :style="{ background: chartColors.blue }"></i>Token 消耗K</span>
<span><i :style="{ background: chartColors.purple }"></i>在线人数</span>
<span><i :style="{ background: chartColors.amber }"></i>平均在线时长分钟</span>
</div>
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { BarChart as EChartsBarChart, LineChart as EChartsLineChart } from 'echarts/charts'
import { GridComponent, 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, EChartsLineChart, CanvasRenderer])
const props = defineProps({
labels: { type: Array, required: true },
toolCalls: { type: Array, required: true },
tokens: { type: Array, required: true },
onlineUsers: { type: Array, required: true },
onlineMinutes: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const chartColors = computed(() => ({
primary: themeColors.value.chartPrimary,
blue: themeColors.value.chartBlue,
purple: themeColors.value.chartPurple,
amber: themeColors.value.chartAmber
}))
const tokenInK = computed(() => props.tokens.map((value) => Math.round(Number(value || 0) / 100) / 10))
const ariaLabel = computed(() =>
props.labels.map((label, index) => (
`${label}工具调用${props.toolCalls[index] || 0}Token消耗${tokenInK.value[index] || 0}K在线${props.onlineUsers[index] || 0}人,平均在线${props.onlineMinutes[index] || 0}分钟`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 1200,
animationDurationUpdate: 1200,
animationEasing: 'linear',
animationEasingUpdate: 'linear',
grid: {
top: 18,
right: 42,
bottom: 22,
left: 38,
containLabel: true
},
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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: props.labels,
boundaryGap: true,
axisTick: { show: false },
axisLine: { lineStyle: { color: 'rgba(148, 163, 184, 0.28)' } },
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
}
},
yAxis: [
{
type: 'value',
min: 0,
max: 360,
splitNumber: 6,
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
},
splitLine: { lineStyle: { color: 'rgba(226, 232, 240, 0.75)' } }
},
{
type: 'value',
min: 0,
max: 90,
splitNumber: 6,
axisLabel: {
color: '#64748b',
fontSize: 11,
fontWeight: 700
},
splitLine: { show: false }
}
],
series: [
{
name: '工具调用(次)',
type: 'bar',
data: props.toolCalls,
barWidth: 14,
itemStyle: {
color: chartColors.value.primary,
borderRadius: [4, 4, 0, 0]
}
},
{
name: 'Token 消耗K',
type: 'line',
yAxisIndex: 1,
data: tokenInK.value,
smooth: true,
symbol: 'circle',
symbolSize: 7,
lineStyle: {
width: 2.5,
color: chartColors.value.blue
},
itemStyle: {
color: '#ffffff',
borderColor: chartColors.value.blue,
borderWidth: 2.5
},
areaStyle: {
color: buildAreaColor(chartColors.value.blue, 0.11)
}
},
{
name: '在线人数',
type: 'line',
data: props.onlineUsers,
smooth: true,
symbol: 'circle',
symbolSize: 7,
lineStyle: {
width: 2.5,
color: chartColors.value.purple
},
itemStyle: {
color: '#ffffff',
borderColor: chartColors.value.purple,
borderWidth: 2.5
}
},
{
name: '平均在线时长(分钟)',
type: 'line',
yAxisIndex: 1,
data: props.onlineMinutes,
smooth: true,
symbol: 'circle',
symbolSize: 7,
lineStyle: {
width: 2.5,
color: chartColors.value.amber
},
itemStyle: {
color: '#ffffff',
borderColor: chartColors.value.amber,
borderWidth: 2.5
}
}
]
}))
useEcharts(chartElement, chartOptions)
function buildAreaColor(color, alpha) {
return {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: toRgba(color, alpha) },
{ offset: 1, color: toRgba(color, 0.02) }
]
}
}
function toRgba(color, alpha) {
const normalized = String(color || '').trim()
const hex = normalized.replace('#', '')
if (/^[\da-f]{6}$/i.test(hex)) {
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
return `rgba(58, 124, 165, ${alpha})`
}
</script>
<style scoped>
.system-trend-chart {
height: 280px;
display: flex;
flex-direction: column;
}
.chart-legend {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px 16px;
color: #475569;
font-size: 12px;
margin-bottom: 12px;
}
.chart-legend i {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 2px;
margin-right: 4px;
vertical-align: middle;
}
.chart-body {
flex: 1;
min-height: 0;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="system-user-token-pie">
<div ref="chartElement" class="chart-body" role="img" :aria-label="ariaLabel"></div>
<div class="token-user-list">
<div v-for="item in resolvedItems" :key="item.name" class="token-user-row">
<i :style="{ background: item.resolvedColor }"></i>
<div>
<strong>{{ item.name }}</strong>
<span>{{ item.role }}</span>
</div>
<em>{{ formatTokens(item.tokens) }}</em>
</div>
</div>
</div>
</template>
<script setup>
import { computed, shallowRef } from 'vue'
import { PieChart } from 'echarts/charts'
import { TooltipComponent } from 'echarts/components'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { useEcharts } from '../../composables/useEcharts.js'
import { resolveCssColor, useThemeColors } from '../../composables/useThemeColors.js'
use([TooltipComponent, PieChart, CanvasRenderer])
const props = defineProps({
items: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const themeColors = useThemeColors()
const totalTokens = computed(() =>
props.items.reduce((sum, item) => sum + Number(item.tokens || 0), 0)
)
const resolvedItems = computed(() =>
props.items.map((item) => ({
...item,
tokens: Number(item.tokens || 0),
resolvedColor: resolveCssColor(item.color, themeColors.value.chartPrimary)
}))
)
const ariaLabel = computed(() =>
resolvedItems.value.map((item) => (
`${item.name}${formatTokens(item.tokens)},占比${getShare(item.tokens)}%`
)).join('')
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: true,
animationDuration: 900,
animationEasing: 'cubicOut',
tooltip: {
trigger: 'item',
confine: true,
appendToBody: true,
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: 'rgba(148, 163, 184, 0.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);',
formatter: (params) => `${params.marker}${params.name}<br/>${formatTokens(params.value)} · ${params.percent}%`
},
series: [
{
type: 'pie',
radius: [0, '82%'],
center: ['47%', '50%'],
roseType: 'radius',
minAngle: 8,
avoidLabelOverlap: true,
label: {
show: true,
color: '#475569',
fontSize: 11,
fontWeight: 800,
formatter: '{b}\n{d}%'
},
labelLine: {
length: 10,
length2: 6,
lineStyle: { color: 'rgba(148, 163, 184, 0.55)' }
},
itemStyle: {
borderColor: '#ffffff',
borderWidth: 3,
borderRadius: 4
},
emphasis: {
scale: true,
scaleSize: 4
},
data: resolvedItems.value.map((item) => ({
name: item.name,
value: item.tokens,
itemStyle: { color: item.resolvedColor }
}))
}
]
}))
useEcharts(chartElement, chartOptions)
function getShare(value) {
if (!totalTokens.value) return 0
return Math.round((Number(value || 0) / totalTokens.value) * 1000) / 10
}
function formatTokens(value) {
const number = Number(value || 0)
if (number >= 1_000_000) return `${(number / 1_000_000).toFixed(1)}M`
if (number >= 1_000) return `${(number / 1_000).toFixed(1)}K`
return `${Math.round(number)}`
}
</script>
<style scoped>
.system-user-token-pie {
min-height: 292px;
display: grid;
grid-template-columns: minmax(0, 1fr) 188px;
gap: 10px;
}
.chart-body {
width: 100%;
min-width: 0;
height: 292px;
}
.token-user-list {
display: grid;
gap: 8px;
align-content: center;
}
.token-user-row {
display: grid;
grid-template-columns: 8px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
}
.token-user-row:last-child {
border-bottom: 0;
}
.token-user-row i {
width: 8px;
height: 22px;
border-radius: 2px;
}
.token-user-row strong,
.token-user-row span {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.token-user-row strong {
color: #1e293b;
font-size: 12px;
font-weight: 800;
}
.token-user-row span {
margin-top: 2px;
color: #64748b;
font-size: 11px;
}
.token-user-row em {
color: #0f172a;
font-size: 12px;
font-style: normal;
font-weight: 850;
font-variant-numeric: tabular-nums;
}
@media (max-width: 860px) {
.system-user-token-pie {
grid-template-columns: 1fr;
}
}
</style>