1
This commit is contained in:
223
app/components/DeliveryTrendChart.vue
Normal file
223
app/components/DeliveryTrendChart.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user