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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user