224 lines
5.8 KiB
Vue
224 lines
5.8 KiB
Vue
<template>
|
||
<div class="delivery-trend-chart">
|
||
<div class="chart-container">
|
||
<canvas ref="chartCanvas" :width="chartWidth" :height="chartHeight"></canvas>
|
||
</div>
|
||
<div class="chart-legend">
|
||
<div v-for="(item, index) in chartData" :key="index" class="legend-item">
|
||
<span class="legend-date">{{ item.date }}</span>
|
||
<span class="legend-value">{{ item.value }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'DeliveryTrendChart',
|
||
props: {
|
||
data: {
|
||
type: Array,
|
||
default: () => []
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
chartWidth: 600,
|
||
chartHeight: 200,
|
||
padding: { top: 20, right: 20, bottom: 30, left: 40 }
|
||
};
|
||
},
|
||
computed: {
|
||
chartData() {
|
||
// 如果没有数据,生成7天的空数据
|
||
if (!this.data || this.data.length === 0) {
|
||
const today = new Date();
|
||
return Array.from({ length: 7 }, (_, i) => {
|
||
const date = new Date(today);
|
||
date.setDate(date.getDate() - (6 - i));
|
||
return {
|
||
date: this.formatDate(date),
|
||
value: 0
|
||
};
|
||
});
|
||
}
|
||
return this.data;
|
||
},
|
||
maxValue() {
|
||
const values = this.chartData.map(item => item.value);
|
||
const max = Math.max(...values, 1); // 至少为1,避免除零
|
||
return Math.ceil(max * 1.2); // 增加20%的顶部空间
|
||
}
|
||
},
|
||
mounted() {
|
||
this.drawChart();
|
||
},
|
||
watch: {
|
||
chartData: {
|
||
handler() {
|
||
this.$nextTick(() => {
|
||
this.drawChart();
|
||
});
|
||
},
|
||
deep: true
|
||
}
|
||
},
|
||
methods: {
|
||
formatDate(date) {
|
||
const month = date.getMonth() + 1;
|
||
const day = date.getDate();
|
||
return `${month}/${day}`;
|
||
},
|
||
drawChart() {
|
||
const canvas = this.$refs.chartCanvas;
|
||
if (!canvas) return;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
const { width, height } = canvas;
|
||
const { padding } = this;
|
||
|
||
// 清空画布
|
||
ctx.clearRect(0, 0, width, height);
|
||
|
||
// 计算绘图区域
|
||
const chartWidth = width - padding.left - padding.right;
|
||
const chartHeight = height - padding.top - padding.bottom;
|
||
|
||
// 绘制背景
|
||
ctx.fillStyle = '#f9f9f9';
|
||
ctx.fillRect(padding.left, padding.top, chartWidth, chartHeight);
|
||
|
||
// 绘制网格线
|
||
ctx.strokeStyle = '#e0e0e0';
|
||
ctx.lineWidth = 1;
|
||
|
||
// 水平网格线(5条)
|
||
for (let i = 0; i <= 5; i++) {
|
||
const y = padding.top + (chartHeight / 5) * i;
|
||
ctx.beginPath();
|
||
ctx.moveTo(padding.left, y);
|
||
ctx.lineTo(padding.left + chartWidth, y);
|
||
ctx.stroke();
|
||
}
|
||
|
||
// 垂直网格线(7条,对应7天)
|
||
for (let i = 0; i <= 7; i++) {
|
||
const x = padding.left + (chartWidth / 7) * i;
|
||
ctx.beginPath();
|
||
ctx.moveTo(x, padding.top);
|
||
ctx.lineTo(x, padding.top + chartHeight);
|
||
ctx.stroke();
|
||
}
|
||
|
||
// 绘制数据点和折线
|
||
if (this.chartData.length > 0) {
|
||
const points = this.chartData.map((item, index) => {
|
||
const x = padding.left + (chartWidth / (this.chartData.length - 1)) * index;
|
||
const y = padding.top + chartHeight - (item.value / this.maxValue) * chartHeight;
|
||
return { x, y, value: item.value };
|
||
});
|
||
|
||
// 绘制折线
|
||
ctx.strokeStyle = '#4CAF50';
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath();
|
||
points.forEach((point, index) => {
|
||
if (index === 0) {
|
||
ctx.moveTo(point.x, point.y);
|
||
} else {
|
||
ctx.lineTo(point.x, point.y);
|
||
}
|
||
});
|
||
ctx.stroke();
|
||
|
||
// 绘制数据点
|
||
ctx.fillStyle = '#4CAF50';
|
||
points.forEach(point => {
|
||
ctx.beginPath();
|
||
ctx.arc(point.x, point.y, 4, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
// 显示数值
|
||
ctx.fillStyle = '#666';
|
||
ctx.font = '12px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(point.value.toString(), point.x, point.y - 10);
|
||
ctx.fillStyle = '#4CAF50';
|
||
});
|
||
|
||
// 绘制区域填充(渐变)
|
||
const gradient = ctx.createLinearGradient(
|
||
padding.left,
|
||
padding.top,
|
||
padding.left,
|
||
padding.top + chartHeight
|
||
);
|
||
gradient.addColorStop(0, 'rgba(76, 175, 80, 0.2)');
|
||
gradient.addColorStop(1, 'rgba(76, 175, 80, 0.05)');
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.moveTo(padding.left, padding.top + chartHeight);
|
||
points.forEach(point => {
|
||
ctx.lineTo(point.x, point.y);
|
||
});
|
||
ctx.lineTo(padding.left + chartWidth, padding.top + chartHeight);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
}
|
||
|
||
// 绘制Y轴标签
|
||
ctx.fillStyle = '#666';
|
||
ctx.font = '11px Arial';
|
||
ctx.textAlign = 'right';
|
||
for (let i = 0; i <= 5; i++) {
|
||
const value = Math.round((this.maxValue / 5) * (5 - i));
|
||
const y = padding.top + (chartHeight / 5) * i;
|
||
ctx.fillText(value.toString(), padding.left - 10, y + 4);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.delivery-trend-chart {
|
||
.chart-container {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
|
||
canvas {
|
||
display: block;
|
||
max-width: 100%;
|
||
}
|
||
}
|
||
|
||
.chart-legend {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
padding: 10px 0;
|
||
border-top: 1px solid #f0f0f0;
|
||
margin-top: 10px;
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
|
||
.legend-date {
|
||
font-size: 11px;
|
||
color: #999;
|
||
}
|
||
|
||
.legend-value {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #4CAF50;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|