225 lines
5.7 KiB
Vue
225 lines
5.7 KiB
Vue
<script setup lang="ts">
|
||
import type { ECharts, EChartsOption } from 'echarts';
|
||
import * as echarts from 'echarts';
|
||
|
||
// ---------- 接收父组件传递的数据 ----------
|
||
interface TaskSeriesData {
|
||
name: string;
|
||
data: number[];
|
||
}
|
||
interface ChartData {
|
||
xList: string[];
|
||
seriesList: TaskSeriesData[];
|
||
}
|
||
const props = defineProps({
|
||
chartData: {
|
||
type: Object as () => ChartData,
|
||
required: true,
|
||
default: () => ({ xList: [], seriesList: [] })
|
||
}
|
||
});
|
||
|
||
// 定义Tab类型
|
||
type TabType = 'day' | 'month';
|
||
const activeTab = ref<TabType>('day');
|
||
|
||
// 图表相关引用
|
||
const chartRef = ref<HTMLDivElement | null>(null);
|
||
let myChart: ECharts | null = null;
|
||
let resizeHandler: () => void;
|
||
|
||
// ---------- 渐变颜色配置(适配多任务) ----------
|
||
const colorMap = [
|
||
{ line: '#52c4fa', area: ['rgba(82, 196, 250, 0.6)', 'rgba(82, 196, 250, 0.1)'] },
|
||
{ line: '#99c741', area: ['rgba(153, 199, 65, 0.6)', 'rgba(153, 199, 65, 0.1)'] },
|
||
{ line: '#f56c6c', area: ['rgba(245, 108, 108, 0.6)', 'rgba(245, 108, 108, 0.1)'] },
|
||
{ line: '#e6a23c', area: ['rgba(230, 162, 60, 0.6)', 'rgba(230, 162, 60, 0.1)'] },
|
||
{ line: '#67c23a', area: ['rgba(103, 194, 58, 0.6)', 'rgba(103, 194, 58, 0.1)'] },
|
||
];
|
||
|
||
// ---------- 初始化图表 ----------
|
||
function initChart() {
|
||
if (!chartRef.value || !props.chartData.xList.length) return;
|
||
|
||
// 销毁原有实例
|
||
if (myChart) {
|
||
myChart.dispose();
|
||
}
|
||
|
||
// 创建新实例
|
||
myChart = echarts.init(chartRef.value);
|
||
|
||
// 构建图表配置
|
||
const option: EChartsOption = {
|
||
animation: false, // 全局关闭所有动画
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'line', lineStyle: { color: '#ccc' } },
|
||
textStyle: { color: '#333' },
|
||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||
padding: 10,
|
||
formatter: (params: any) => {
|
||
let res = `<div style="font-weight:bold;">${params[0].name}</div>`;
|
||
params.forEach((item: any) => {
|
||
res += `<div>${item.seriesName}: ${item.value}</div>`;
|
||
});
|
||
return res;
|
||
},
|
||
},
|
||
legend: {
|
||
top: 20,
|
||
left: 'center',
|
||
textStyle: { color: '#ade6ee' },
|
||
data: props.chartData.seriesList.map(item => item.name), // 动态渲染任务名
|
||
icon: 'circle',
|
||
itemWidth: 10,
|
||
itemHeight: 10,
|
||
},
|
||
grid: {
|
||
left: '0%',
|
||
right: '0%',
|
||
bottom: '0%',
|
||
top: '15%',
|
||
containLabel: true,
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: props.chartData.xList, // 父组件传递的x轴日期
|
||
axisLine: { lineStyle: { color: '#416273' } },
|
||
axisLabel: {
|
||
color: '#ade6ee',
|
||
fontSize: 12,
|
||
formatter(value, index) {
|
||
const dataLen = props.chartData.xList.length;
|
||
if (index === dataLen - 1) {
|
||
return `${value}\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0`;
|
||
}
|
||
return value;
|
||
},
|
||
},
|
||
splitLine: { show: false },
|
||
axisTick: { show: false },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
// 动态计算Y轴最大值(向上取整到千位)
|
||
max: () => {
|
||
const allValues = props.chartData.seriesList.flatMap(item => item.data);
|
||
const maxVal = Math.max(...allValues);
|
||
return maxVal > 0 ? Math.ceil(maxVal / 1000) * 1000 : 1000;
|
||
},
|
||
axisLabel: { color: '#ade6ee', fontSize: 12 },
|
||
splitLine: { lineStyle: { color: '#77B5D533', type: 'solid' } },
|
||
show: true,
|
||
axisLine: { show: true, lineStyle: { color: '#416273' } },
|
||
},
|
||
series: props.chartData.seriesList.map((item, index) => ({
|
||
name: item.name,
|
||
type: 'line',
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [
|
||
{ offset: 0, color: colorMap[index % colorMap.length].area[0] },
|
||
{ offset: 1, color: colorMap[index % colorMap.length].area[1] },
|
||
],
|
||
},
|
||
},
|
||
lineStyle: { color: colorMap[index % colorMap.length].line },
|
||
data: item.data, // 父组件传递的任务数据
|
||
symbol: 'none',
|
||
})),
|
||
};
|
||
|
||
myChart.setOption(option);
|
||
|
||
// 窗口大小监听
|
||
resizeHandler = () => {
|
||
myChart?.resize();
|
||
};
|
||
window.addEventListener('resize', resizeHandler);
|
||
}
|
||
|
||
// ---------- 监听数据变化,重新渲染图表 ----------
|
||
watch([() => props.chartData, () => activeTab], () => {
|
||
initChart();
|
||
}, { deep: true });
|
||
|
||
// ---------- 生命周期 ----------
|
||
onMounted(() => {
|
||
initChart();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (myChart) {
|
||
myChart.dispose();
|
||
myChart = null;
|
||
}
|
||
if (resizeHandler) {
|
||
window.removeEventListener('resize', resizeHandler);
|
||
}
|
||
});
|
||
|
||
// ---------- 暴露activeTab给父组件 ----------
|
||
defineExpose({
|
||
activeTab
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="task-area-chart-container" style="position: relative;">
|
||
<!-- 右上角Tab切换 -->
|
||
<div class="tab-bar">
|
||
<span :class="{ active: activeTab === 'day' }" @click="activeTab = 'day'">天</span>
|
||
<span :class="{ active: activeTab === 'month' }" @click="activeTab = 'month'">月</span>
|
||
</div>
|
||
<!-- 图表容器 -->
|
||
<div ref="chartRef" class="chart" />
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 保留原有样式,省略 */
|
||
.task-area-chart-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.chart {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.tab-bar {
|
||
position: absolute;
|
||
top: 15px;
|
||
right: 0;
|
||
display: flex;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
z-index: 10;
|
||
background: rgb(7, 39, 43);
|
||
border: 1px solid #0b8c82;
|
||
/* user-select: none; */
|
||
}
|
||
|
||
.tab-bar span {
|
||
width: 50px;
|
||
height: 30px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 2px 8px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.tab-bar span.active {
|
||
background-color: #1397a3;
|
||
}
|
||
</style> |