feat: 引入 ECharts 统一图表并完善员工画像标签分页
后端优化员工行为画像服务和辅助函数,完善系统设置模型和 配置持久化,前端引入 ECharts 替换所有图表组件实现统一 渲染,新增员工画像标签分页器和数字员工工作记录组件,优 化工作台响应式布局和登录页过渡动画,完善预算中心和数字 员工页面样式细节。
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user