275 lines
4.9 KiB
Vue
275 lines
4.9 KiB
Vue
<template>
|
|
<view>
|
|
<!-- ECharts图表 -->
|
|
<view class="chart-container">
|
|
<l-echart ref="chart" @finished="initChart" />
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
|
|
import * as echarts from 'echarts';
|
|
import { formatDate, getDateAfterDays, getDateAfterMonths } from '@/utils/dateTime.js';
|
|
const props = defineProps({
|
|
dataList: {
|
|
type: Array,
|
|
default: () => [],
|
|
required: true
|
|
},
|
|
xField: {
|
|
type: String,
|
|
default: 'rqDate'
|
|
},
|
|
yField: {
|
|
type: String,
|
|
default: 'rq'
|
|
},
|
|
legendField: {
|
|
type: String,
|
|
default: 'unit'
|
|
},
|
|
referenceValue: {
|
|
type: Number,
|
|
default: 0
|
|
}
|
|
});
|
|
const chart = ref(null);
|
|
const chartOption = ref({});
|
|
const chartTitle = ref('');
|
|
|
|
//、数据处理
|
|
const processSeriesData = () => {
|
|
const legendItems = [...new Set(props.dataList.map((item) => item[props.legendField]?.trim() || '未命名'))];
|
|
|
|
return legendItems.map((legend) => {
|
|
const items = props.dataList
|
|
.filter((item) => item[props.legendField] === legend)
|
|
.sort((a, b) => new Date(a[props.xField]) - new Date(b[props.xField]))
|
|
.map((item) => ({
|
|
name: item[props.xField],
|
|
value: [
|
|
formatDate(item[props.xField]), // X轴时间戳
|
|
parseFloat(item[props.yField]) || 0 // Y轴数值
|
|
]
|
|
}));
|
|
|
|
return {
|
|
name: legend,
|
|
type: 'line',
|
|
showSymbol: true,
|
|
smooth: true,
|
|
data: items
|
|
};
|
|
});
|
|
};
|
|
// 监听数据变化
|
|
watch(
|
|
() => props.dataList,
|
|
() => {
|
|
chartTitle.value = '历史趋势';
|
|
updateChart();
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// 生命周期管理
|
|
onMounted(() => {});
|
|
|
|
onUnmounted(() => {
|
|
if (chart.value) {
|
|
chart.value.dispose();
|
|
chart.value = null;
|
|
}
|
|
});
|
|
// 更新图表 formatDate
|
|
const updateChart = () => {
|
|
if (!chart.value) return;
|
|
chartOption.value = generateOptions();
|
|
chart.value.setOption(chartOption.value, true);
|
|
};
|
|
// 生成图表配置
|
|
const generateOptions = () => ({
|
|
title: {
|
|
text: chartTitle.value,
|
|
padding: [0, 0, 0, 30]
|
|
},
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(255, 255, 255, 0.96)',
|
|
borderColor: '#eee',
|
|
borderWidth: 1,
|
|
textStyle: {
|
|
color: '#333',
|
|
fontSize: 14
|
|
},
|
|
axisPointer: {
|
|
type: 'line',
|
|
lineStyle: {
|
|
color: '#FF6B6B',
|
|
width: 1,
|
|
type: 'dashed'
|
|
}
|
|
},
|
|
confine: true // 将 tooltip 限制在图表区域内
|
|
},
|
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
textStyle: {
|
|
color: '#666',
|
|
fontSize: 12
|
|
},
|
|
xAxis: {
|
|
type: 'time',
|
|
axisLine: {
|
|
lineStyle: {
|
|
color: '#ddd'
|
|
}
|
|
},
|
|
axisLabel: {
|
|
color: '#666',
|
|
formatter: '{MM}-{dd}',
|
|
rotate: 30
|
|
},
|
|
splitLine: {
|
|
show: true,
|
|
lineStyle: {
|
|
color: '#f5f5f5'
|
|
}
|
|
}
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLine: {
|
|
show: true,
|
|
lineStyle: {
|
|
color: '#ddd'
|
|
}
|
|
},
|
|
splitLine: {
|
|
lineStyle: {
|
|
color: '#f5f5f5'
|
|
}
|
|
},
|
|
axisLabel: {
|
|
color: '#666',
|
|
formatter: (value) => `${value.toFixed(2)}`
|
|
}
|
|
},
|
|
series: [
|
|
...processSeriesData().map((series) => ({
|
|
...series,
|
|
lineStyle: {
|
|
width: 2,
|
|
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
|
shadowBlur: 6,
|
|
shadowOffsetY: 3
|
|
},
|
|
itemStyle: {
|
|
color: '#00007f', // 建议改为动态颜色
|
|
borderColor: '#fff',
|
|
borderWidth: 1
|
|
}
|
|
})),
|
|
{
|
|
type: 'line',
|
|
markLine: {
|
|
silent: true,
|
|
lineStyle: {
|
|
type: 'dashed',
|
|
color: '#FF4757',
|
|
width: 2
|
|
},
|
|
data: [{ yAxis: props.referenceValue }],
|
|
label: {
|
|
show: true,
|
|
position: 'end',
|
|
formatter: `参考值: ${props.referenceValue}`,
|
|
fontSize: 12
|
|
}
|
|
},
|
|
data: [] // 空数据系列仅用于显示参考线
|
|
}
|
|
],
|
|
legend: {
|
|
type: 'scroll',
|
|
bottom: 5,
|
|
textStyle: {
|
|
color: '#666',
|
|
fontSize: 12
|
|
},
|
|
pageIconColor: '#FF6B6B',
|
|
pageTextStyle: {
|
|
color: '#999'
|
|
}
|
|
},
|
|
grid: {
|
|
top: 30,
|
|
right: 30,
|
|
bottom: 40,
|
|
left: 20,
|
|
containLabel: true
|
|
},
|
|
dataZoom: [
|
|
{
|
|
type: 'inside',
|
|
start: 0,
|
|
end: 100,
|
|
minValueSpan: 3600 * 24 * 1000 * 7 // 最小缩放范围7天
|
|
}
|
|
]
|
|
});
|
|
|
|
// 初始化图表
|
|
const initChart = () => {
|
|
setTimeout(async () => {
|
|
if (!chart.value) return;
|
|
const myChart = await chart.value.init(echarts);
|
|
myChart.setOption(chartOption.value);
|
|
}, 300);
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 容器样式 */
|
|
.chart-container {
|
|
width: 100%;
|
|
height: 30vh;
|
|
min-height: 200px;
|
|
padding: 20rpx;
|
|
background: linear-gradient(145deg, #f8f9fa 0%, #ffffff 100%);
|
|
border-radius: 16rpx;
|
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 图表加载占位 */
|
|
.chart-container::before {
|
|
/* content: '数据加载中...'; */
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: #666;
|
|
font-size: 28rpx;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* 图表主体样式 */
|
|
.chart-wrapper {
|
|
width: 100%;
|
|
height: 100%;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
/* 移动端优化 */
|
|
@media screen and (max-width: 768px) {
|
|
.chart-container {
|
|
height: 30vh;
|
|
min-height: 200px;
|
|
padding: 10rpx;
|
|
border-radius: 12rpx;
|
|
}
|
|
}
|
|
</style>
|