feat: 引入 ECharts 统一图表并完善员工画像标签分页

后端优化员工行为画像服务和辅助函数,完善系统设置模型和
配置持久化,前端引入 ECharts 替换所有图表组件实现统一
渲染,新增员工画像标签分页器和数字员工工作记录组件,优
化工作台响应式布局和登录页过渡动画,完善预算中心和数字
员工页面样式细节。
This commit is contained in:
caoxiaozhu
2026-05-28 16:24:59 +08:00
parent 8a4a777be7
commit e384318046
53 changed files with 4698 additions and 2468 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div class="bar-chart">
<div class="rank-labels">
<div v-for="(item, idx) in items" :key="item.name" class="rank-label">
<div v-for="(item, idx) in resolvedItems" :key="item.name" class="rank-label">
<span class="rank-badge" :class="medalClass(idx)">
<svg v-if="idx < 3" width="18" height="18" viewBox="0 0 18 18">
<circle cx="9" cy="9" r="8" :fill="medalFill(idx)" />
@@ -12,39 +12,138 @@
<span class="rank-name">{{ item.name || item.shortName }}</span>
</div>
</div>
<div class="chart-area">
<Bar :data="chartData" :options="chartOptions" />
</div>
<div ref="chartElement" class="chart-area" role="img" :aria-label="ariaLabel"></div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { Bar } from 'vue-chartjs'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Tooltip
} from 'chart.js'
import { computed, shallowRef } from 'vue'
import { BarChart as EChartsBarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
import { useEcharts } from '../../composables/useEcharts.js'
import { resolveCssColor, useThemeColors } from '../../composables/useThemeColors.js'
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip)
use([GridComponent, TooltipComponent, EChartsBarChart, CanvasRenderer])
const props = defineProps({
items: { type: Array, required: true }
})
const chartElement = shallowRef(null)
const progress = useAnimationProgress([() => props.items], 980)
const themeColors = useThemeColors()
const resolvedItems = computed(() => {
const fallback = themeColors.value.chartPrimary
return props.items.map((item) => ({
...item,
value: Number(item.value || item.amount || 0),
resolvedColor: resolveCssColor(item.color, fallback)
}))
})
const ariaLabel = computed(() =>
resolvedItems.value.map((item, index) => (
`${index + 1}${item.name || item.shortName}${formatValue(item.value)}`
)).join('')
)
const chartMaxValue = computed(() => Math.max(...resolvedItems.value.map((item) => item.value), 1))
const chartAxisMax = computed(() => Math.ceil((chartMaxValue.value * 1.1) / 10000) * 10000)
const animatedItems = computed(() =>
resolvedItems.value.map((item) => ({
...item,
animatedValue: progress.value >= 0.999
? item.value
: Number((item.value * progress.value).toFixed(0))
}))
)
const chartOptions = computed(() => ({
backgroundColor: 'transparent',
animation: false,
grid: {
top: 8,
right: 62,
bottom: 8,
left: 4,
containLabel: false
},
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}: ${formatValue(params.value)}`
},
xAxis: {
type: 'value',
min: 0,
max: chartAxisMax.value,
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: { color: 'rgba(226, 232, 240, 0.72)' }
},
axisLabel: {
color: '#94a3b8',
fontSize: 11,
fontWeight: 700,
formatter: (value) => formatValue(value)
}
},
yAxis: {
type: 'category',
data: resolvedItems.value.map((item) => item.name || item.shortName),
inverse: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false }
},
series: [
{
type: 'bar',
data: animatedItems.value.map((item) => ({
name: item.name || item.shortName,
value: item.animatedValue,
itemStyle: { color: item.resolvedColor }
})),
barWidth: 14,
showBackground: true,
backgroundStyle: {
color: 'rgba(226, 232, 240, 0.42)',
borderRadius: 6
},
itemStyle: {
borderRadius: [0, 6, 6, 0]
},
label: {
show: true,
position: 'right',
color: '#64748b',
fontSize: 11,
fontWeight: 800,
formatter: ({ value }) => formatValue(value)
}
}
]
}))
useEcharts(chartElement, chartOptions)
const medalClass = (idx) => {
if (idx === 0) return 'gold'
if (idx === 1) return 'silver'
@@ -60,68 +159,10 @@ const medalFill = (idx) => {
}
const formatValue = (value) => {
if (value >= 1_000_000) return `¥${(value / 1_000_000).toFixed(1)}M`
if (value >= 1_000) return `¥${(value / 1_000).toFixed(1)}K`
return `¥${value}`
}
const chartData = computed(() => ({
labels: resolvedItems.value.map((i) => i.name || i.shortName),
datasets: [{
data: resolvedItems.value.map((i) => i.value || i.amount),
backgroundColor: resolvedItems.value.map((i) => i.resolvedColor),
borderRadius: 6,
borderSkipped: false,
barPercentage: 0.7,
categoryPercentage: 0.85
}]
}))
const chartOptions = {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
layout: {
padding: { left: 0, right: 12 }
},
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(255,255,255,0.96)',
titleColor: '#1e293b',
bodyColor: '#64748b',
borderColor: '#e2e8f0',
borderWidth: 1,
padding: 10,
boxPadding: 4,
cornerRadius: 6,
callbacks: {
title: () => '',
label: (ctx) => ` ${formatValue(ctx.parsed.x)}`
}
}
},
scales: {
x: {
beginAtZero: true,
grid: {
color: '#f1f5f9',
drawTicks: false
},
ticks: {
color: '#94a3b8',
font: { size: 11 },
padding: 4,
callback: (value) => formatValue(value)
},
border: { display: false }
},
y: {
grid: { display: false },
border: { display: false },
ticks: { display: false }
}
}
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}`
}
</script>