style(@vben/web-antd): 优化保洁工单看板图表样式和命名

- 热力图:改用对数变换+RGB线性插值,0单与有单颜色自适应平滑过渡
- 工单趋势:双线改为蓝色系同色深浅,当月深蓝实线+上月浅蓝虚线
- 工牌队列统计改名为工单排队趋势(近7天)
- 功能类型排行改名为区域类型排行,按工单数从高到低排序
- 第四行卡片统一使用chart-card样式,补充右上角Tooltip说明

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-09 12:59:35 +08:00
parent 2a18c8ce04
commit f4c6c4437c

View File

@@ -113,9 +113,7 @@ const { renderEcharts: renderStatusChart } = useEcharts(statusChartRef);
const { renderEcharts: renderFunctionTypeRankingChart } = useEcharts(
functionTypeRankingChartRef,
);
const { renderEcharts: renderBadgeQueueChart } = useEcharts(
badgeQueueChartRef,
);
const { renderEcharts: renderBadgeQueueChart } = useEcharts(badgeQueueChartRef);
const statsData = ref<DashboardStats>({
pendingCount: 0,
@@ -175,11 +173,62 @@ function getTrendChartOptions(): ECOption {
const { trendData } = statsData.value;
return {
tooltip: { trigger: 'axis' },
legend: { data: ['新增工单'], top: '5%', textStyle: { color: '#595959' } },
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [{ type: 'category', boundaryGap: false, data: trendData.dates, axisLine: { lineStyle: { color: '#d9d9d9' } }, axisLabel: { color: '#8c8c8c' } }],
yAxis: [{ type: 'value', name: '工单数量', nameTextStyle: { color: '#8c8c8c' }, axisLine: { show: false }, axisLabel: { color: '#8c8c8c' }, splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } } }],
series: [{ name: '新增工单', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: trendData.createdData, itemStyle: { color: '#1677ff' }, areaStyle: { opacity: 0.15, color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: 'rgba(22, 119, 255, 0.4)' }, { offset: 1, color: 'rgba(22, 119, 255, 0.05)' }] } } }],
legend: {
data: ['新增工单'],
top: '5%',
textStyle: { color: '#595959' },
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: trendData.dates,
axisLine: { lineStyle: { color: '#d9d9d9' } },
axisLabel: { color: '#8c8c8c' },
},
],
yAxis: [
{
type: 'value',
name: '工单数量',
nameTextStyle: { color: '#8c8c8c' },
axisLine: { show: false },
axisLabel: { color: '#8c8c8c' },
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
},
],
series: [
{
name: '新增工单',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
data: trendData.createdData,
itemStyle: { color: '#1677ff' },
areaStyle: {
opacity: 0.15,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(22, 119, 255, 0.4)' },
{ offset: 1, color: 'rgba(22, 119, 255, 0.05)' },
],
},
},
},
],
};
}
@@ -193,7 +242,8 @@ function getTrendChartOptions(): ECOption {
return parts[2] || d;
});
// 取最长的日期轴
const xData = currentDates.length >= lastDates.length ? currentDates : lastDates;
const xData =
currentDates.length >= lastDates.length ? currentDates : lastDates;
return {
tooltip: {
@@ -205,22 +255,32 @@ function getTrendChartOptions(): ECOption {
top: '5%',
textStyle: { color: '#595959' },
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [{
type: 'category',
boundaryGap: false,
data: xData,
axisLine: { lineStyle: { color: '#d9d9d9' } },
axisLabel: { color: '#8c8c8c', formatter: (val: string) => `${val}` },
}],
yAxis: [{
type: 'value',
name: '工单数量',
nameTextStyle: { color: '#8c8c8c' },
axisLine: { show: false },
axisLabel: { color: '#8c8c8c' },
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
}],
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: xData,
axisLine: { lineStyle: { color: '#d9d9d9' } },
axisLabel: { color: '#8c8c8c', formatter: (val: string) => `${val}` },
},
],
yAxis: [
{
type: 'value',
name: '工单数量',
nameTextStyle: { color: '#8c8c8c' },
axisLine: { show: false },
axisLabel: { color: '#8c8c8c' },
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
},
],
series: [
{
name: '当月新增',
@@ -231,37 +291,46 @@ function getTrendChartOptions(): ECOption {
areaStyle: {
opacity: 0.15,
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(22, 119, 255, 0.4)' },
{ offset: 1, color: 'rgba(22, 119, 255, 0.05)' },
{ offset: 0, color: 'rgba(22, 119, 255, 0.35)' },
{ offset: 1, color: 'rgba(22, 119, 255, 0.03)' },
],
},
},
emphasis: { focus: 'series' },
data: monthly.currentMonth.createdData,
itemStyle: { color: '#1677ff' },
lineStyle: { width: 2.5 },
},
{
name: '上月新增',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { type: 'dashed', opacity: 0.6 },
symbolSize: 5,
lineStyle: { type: 'dashed', width: 1.5 },
areaStyle: {
opacity: 0.06,
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(82, 196, 26, 0.3)' },
{ offset: 1, color: 'rgba(82, 196, 26, 0.05)' },
{ offset: 0, color: 'rgba(147, 196, 255, 0.25)' },
{ offset: 1, color: 'rgba(147, 196, 255, 0.03)' },
],
},
},
emphasis: { focus: 'series' },
data: monthly.lastMonth.createdData,
itemStyle: { color: '#52c41a', opacity: 0.6 },
itemStyle: { color: '#93c4fd' },
},
],
};
@@ -488,13 +557,13 @@ function getStatusDistributionChartOptions(): ECOption {
/**
* 时段热力图图表配置7天x24小时
* 对数变换 + RGB线性插值低量区间色彩差异大高量区间自动压缩
*/
function getHeatmapChartOptions(): ECOption {
const { heatmapData } = statsData.value;
const xAxisData = heatmapData.hours.map((h, i) => i);
const yAxisData = heatmapData.days.map((d, i) => i);
const xAxisData = heatmapData.hours.map((_h, i) => i);
const yAxisData = heatmapData.days.map((_d, i) => i);
// 将二维数据转换为ECharts热力图格式 [x, y, value]
const data: number[][] = [];
for (let day = 0; day < heatmapData.days.length; day++) {
for (let hour = 0; hour < 24; hour++) {
@@ -503,6 +572,36 @@ function getHeatmapChartOptions(): ECOption {
}
}
const maxVal = Math.max(1, ...data.map((d: number[]) => d[2] || 0));
// RGB线性插值生成颜色0单灰色 → 最大值主蓝,基于对数比例
const COLOR_ZERO: [number, number, number] = [245, 245, 245]; // #f5f5f5
const COLOR_MAX: [number, number, number] = [22, 119, 255]; // #1677ff
const logMax = Math.log1p(maxVal);
function valueToColor(val: number): string {
if (val <= 0) return `rgb(${COLOR_ZERO.join(',')})`;
const t = Math.log1p(val) / logMax; // 0~1 对数比例
const r = Math.round(COLOR_ZERO[0] + (COLOR_MAX[0] - COLOR_ZERO[0]) * t);
const g = Math.round(COLOR_ZERO[1] + (COLOR_MAX[1] - COLOR_ZERO[1]) * t);
const b = Math.round(COLOR_ZERO[2] + (COLOR_MAX[2] - COLOR_ZERO[2]) * t);
return `rgb(${r},${g},${b})`;
}
// 为每个数据点计算精确颜色
const seriesData = data.map((d) => ({
value: d,
itemStyle: { color: valueToColor(d[2] || 0) },
}));
// 图例色带:采样生成渐变色阶
const legendSteps = 10;
const legendColors: string[] = [];
for (let i = 0; i <= legendSteps; i++) {
const val = (maxVal * i) / legendSteps;
legendColors.push(valueToColor(val));
}
return {
tooltip: {
position: 'top',
@@ -514,85 +613,56 @@ function getHeatmapChartOptions(): ECOption {
},
},
grid: {
height: '75%',
height: '70%',
top: '5%',
left: '8%',
right: '3%',
bottom: '15%',
bottom: '20%',
},
xAxis: {
type: 'category',
data: xAxisData,
splitArea: {
show: true,
},
splitArea: { show: true },
axisLabel: {
fontSize: 9,
color: '#8c8c8c',
formatter: (i: number) => (i % 3 === 0 ? `${i}:00` : ''),
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLine: { show: false },
axisTick: { show: false },
},
yAxis: {
type: 'category',
data: yAxisData,
splitArea: {
show: true,
},
splitArea: { show: true },
axisLabel: {
fontSize: 11,
color: '#595959',
formatter: (i: number) => heatmapData.days[i] || '',
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLine: { show: false },
axisTick: { show: false },
},
visualMap: {
min: 0,
max: Math.max(10, ...data.map((d: number[]) => d[2] || 0)),
max: maxVal,
calculable: false,
orient: 'horizontal',
left: 'center',
bottom: '2%',
textStyle: {
fontSize: 10,
color: '#8c8c8c',
},
itemWidth: 8,
itemHeight: 50,
inRange: {
color: [
'#f0f5ff',
'#d6e4ff',
'#adc6ff',
'#85a5ff',
'#597ef7',
'#2f54eb',
'#1d39c4',
'#10239e',
'#061178',
],
},
splitNumber: 8,
show: false,
text: [`${maxVal}`, '0'],
textStyle: { fontSize: 10, color: '#8c8c8c' },
itemWidth: 10,
itemHeight: 80,
inRange: { color: legendColors },
show: true,
},
series: [
{
name: '工单数量',
type: 'heatmap',
data,
label: {
show: false,
},
data: seriesData,
label: { show: false },
emphasis: {
itemStyle: {
shadowBlur: 10,
@@ -605,10 +675,13 @@ function getHeatmapChartOptions(): ECOption {
}
/**
* 功能类型排行图表配置
* 区域类型排行图表配置
*/
function getFunctionTypeRankingChartOptions(): ECOption {
const { functionTypeRanking } = statsData.value;
// 按工单数从低到高排序ECharts水平柱状图yAxis从下往上所以升序=视觉上从高到低)
const sorted = [...statsData.value.functionTypeRanking].toSorted(
(a, b) => a.count - b.count,
);
return {
grid: {
left: '3%',
@@ -629,7 +702,7 @@ function getFunctionTypeRankingChartOptions(): ECOption {
},
yAxis: {
type: 'category',
data: functionTypeRanking.map((item) => item.functionType),
data: sorted.map((item) => item.functionType),
axisLabel: {
fontSize: 13,
color: '#595959',
@@ -644,7 +717,7 @@ function getFunctionTypeRankingChartOptions(): ECOption {
series: [
{
type: 'bar',
data: functionTypeRanking.map((item) => ({
data: sorted.map((item) => ({
value: item.count,
itemStyle: {
color: getCompletionRateColor(item.rate),
@@ -701,61 +774,73 @@ function getBadgeQueueChartOptions(): ECOption {
top: '15%',
containLabel: true,
},
xAxis: [{
type: 'category',
data: queue.dates,
axisLine: { lineStyle: { color: '#f0f0f0' } },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
}],
yAxis: [{
type: 'value',
name: '队列数',
nameTextStyle: { color: '#8c8c8c', fontSize: 12 },
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c' },
splitLine: { lineStyle: { color: '#f5f5f5', type: 'dashed' } },
}],
series: [{
name: '队列数量',
type: 'bar',
barWidth: '40%',
data: queue.queueData.map((val) => ({
value: val,
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: val === maxVal ? '#0958d9' : '#1677ff' },
{ offset: 1, color: val === maxVal ? '#4096ff' : '#91caff' },
],
},
borderRadius: [6, 6, 0, 0],
},
})),
label: {
show: true,
position: 'top',
fontSize: 11,
fontWeight: '600',
color: '#1677ff',
formatter: (params: any) => params.value > 0 ? params.value : '',
xAxis: [
{
type: 'category',
data: queue.dates,
axisLine: { lineStyle: { color: '#f0f0f0' } },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
},
emphasis: {
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: '#003eb3' },
{ offset: 1, color: '#0958d9' },
],
],
yAxis: [
{
type: 'value',
name: '队列数',
nameTextStyle: { color: '#8c8c8c', fontSize: 12 },
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c' },
splitLine: { lineStyle: { color: '#f5f5f5', type: 'dashed' } },
},
],
series: [
{
name: '队列数量',
type: 'bar',
barWidth: '40%',
data: queue.queueData.map((val) => ({
value: val,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: val === maxVal ? '#0958d9' : '#1677ff' },
{ offset: 1, color: val === maxVal ? '#4096ff' : '#91caff' },
],
},
borderRadius: [6, 6, 0, 0],
},
})),
label: {
show: true,
position: 'top',
fontSize: 11,
fontWeight: '600',
color: '#1677ff',
formatter: (params: any) => (params.value > 0 ? params.value : ''),
},
emphasis: {
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: '#003eb3' },
{ offset: 1, color: '#0958d9' },
],
},
},
},
},
}],
],
};
}
@@ -991,30 +1076,40 @@ onUnmounted(stopPolling);
</Col>
</Row>
<!-- 第四行牌队列统计近7天 + 功能类型排行 -->
<!-- 第四行单排队趋势近7天 + 区域类型排行 -->
<Row :gutter="[12, 12]">
<!-- 牌队列统计近7天 -->
<!-- 单排队趋势近7天 -->
<Col :xs="24" :md="12" :lg="12">
<Card class="modern-card modern-card--heatmap">
<div class="modern-header">
<span class="modern-title">工牌队列统计近7天</span>
</div>
<Card class="chart-card" title="工单排队趋势近7天">
<template #extra>
<Tooltip title="展示近7天每日排队等待派单的工单数量变化">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
/>
</Tooltip>
</template>
<Spin :spinning="chartLoading">
<EchartsUI ref="badgeQueueChartRef" class="modern-chart" />
<EchartsUI ref="badgeQueueChartRef" class="chart-container" />
</Spin>
</Card>
</Col>
<!-- 功能类型排行 -->
<!-- 区域类型排行 -->
<Col :xs="24" :md="12" :lg="12">
<Card class="modern-card modern-card--ranking">
<div class="modern-header">
<span class="modern-title">功能类型排行</span>
</div>
<Card class="chart-card" title="区域类型排行">
<template #extra>
<Tooltip title="按区域类型统计工单数量排行">
<IconifyIcon
icon="solar:info-circle-bold-duotone"
class="info-icon"
/>
</Tooltip>
</template>
<Spin :spinning="chartLoading">
<EchartsUI
ref="functionTypeRankingChartRef"
class="modern-chart"
class="chart-container"
/>
</Spin>
</Card>