DSDSWeb/src/views/statistics/components/BarChart.vue

436 lines
12 KiB
Vue
Raw Normal View History

2025-10-31 16:15:28 +08:00
<template>
<div ref="chartContainer" style="width: 100%; height: 300px;"></div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'AdvancedMixedChart',
props: {
title: {
type: String,
default: '图表标题'
},
chartData: {
type: [Array, Object],
default: () => []
},
categories: {
type: Array,
default: () => []
},
chartType: {
type: String,
default: 'single', // 'single' | 'multiple' | 'mixed' | 'stacked'
validator: (value) => ['single', 'multiple', 'mixed', 'stacked'].includes(value)
},
colors: {
type: Array,
default: () => ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272']
},
// 新增是否显示双Y轴
dualYAxis: {
type: Boolean,
default: false
},
// 新增堆叠组名称用于stacked模式
stackName: {
type: String,
default: '总量'
}
},
data() {
return {
myChart: null
};
},
mounted() {
this.initChart();
},
beforeDestroy() {
if (this.myChart) {
this.myChart.dispose();
}
window.removeEventListener('resize', this.handleResize);
},
methods: {
initChart() {
this.$nextTick(() => {
this.myChart = echarts.init(this.$refs.chartContainer);
this.renderChart();
window.addEventListener('resize', this.handleResize);
});
},
// 处理数据格式
processChartData() {
if (this.chartType === 'single') {
// 单系列柱状图
const data = Array.isArray(this.chartData) && this.chartData.length > 0
? this.chartData
: new Array(this.categories.length).fill(0);
return [{
name: '数量',
type: 'bar',
data: data
}];
} else if (this.chartType === 'multiple') {
// 多系列柱状图
if (Array.isArray(this.chartData) && this.chartData.every(item => typeof item === 'number')) {
return [{
name: '系列1',
type: 'bar',
data: this.chartData
}];
} else if (Array.isArray(this.chartData)) {
return this.chartData.map((series, index) => ({
name: series.name || `系列${index + 1}`,
type: series.type || 'bar',
data: series.data || [],
yAxisIndex: series.yAxisIndex || 0
}));
}
} else if (this.chartType === 'mixed') {
// 柱状图和折线图组合
if (Array.isArray(this.chartData)) {
return this.chartData.map((series, index) => ({
name: series.name || (series.type === 'line' ? '折线' : '柱状') + (index + 1),
type: series.type || (index === 0 ? 'bar' : 'line'),
data: series.data || [],
yAxisIndex: series.yAxisIndex || (this.dualYAxis && index > 0 ? 1 : 0)
}));
}
} else if (this.chartType === 'stacked') {
// 堆叠柱状图 [2,3](@ref)
if (Array.isArray(this.chartData) && this.chartData.every(item => typeof item === 'number')) {
// 如果是数字数组,转换为单个堆叠系列
return [{
name: '数量',
type: 'bar',
data: this.chartData,
stack: this.stackName // 设置堆叠属性 [2](@ref)
}];
} else if (Array.isArray(this.chartData)) {
// 处理多系列堆叠柱状图
return this.chartData.map((series, index) => ({
name: series.name || `系列${index + 1}`,
type: 'bar',
data: series.data || [],
yAxisIndex: series.yAxisIndex || 0,
stack: series.stack || this.stackName // 设置堆叠属性相同stack值的系列会堆叠 [2,3](@ref)
}));
}
}
return [{
name: '数据',
type: 'bar',
data: new Array(this.categories.length).fill(0)
}];
},
// 获取Y轴配置
getYAxisConfig() {
if (!this.dualYAxis) {
// 单Y轴配置
return [{
type: 'value',
axisLine: {
show: true,
lineStyle: {
color: '#999'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'solid'
}
}
}];
}
// 双Y轴配置
return [
{
type: 'value',
name: '左轴',
position: 'left',
axisLine: {
show: true,
lineStyle: {
color: this.colors[0]
}
},
axisLabel: {
color: '#666',
formatter: '{value}'
},
splitLine: {
show: false
}
},
{
type: 'value',
name: '右轴',
position: 'right',
axisLine: {
show: true,
lineStyle: {
color: this.colors[1]
}
},
axisLabel: {
color: '#666',
formatter: '{value}'
},
splitLine: {
show: false
}
}
];
},
// 获取系列配置
getSeriesConfig(processedSeries) {
return processedSeries.map((series, index) => {
const isLine = series.type === 'line';
const isStacked = this.chartType === 'stacked';
// 基础配置,包含堆叠属性 [2](@ref)
const baseConfig = {
name: series.name,
type: series.type,
data: series.data,
yAxisIndex: series.yAxisIndex || 0,
stack: series.stack // 保留堆叠属性,如果存在
};
if (isLine) {
// 折线图配置
return {
...baseConfig,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 3,
color: this.colors[index % this.colors.length]
},
itemStyle: {
color: this.colors[index % this.colors.length]
}
};
} else {
// 柱状图配置
const barConfig = {
...baseConfig,
barWidth: this.chartType === 'single' ? '60%' : '40%',
itemStyle: {
color: this.colors[index % this.colors.length]
},
emphasis: {
itemStyle: {
color: this.colors[index % this.colors.length],
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
};
// 为堆叠柱状图调整柱宽和样式 [3](@ref)
if (isStacked) {
barConfig.barWidth = '50%';
barConfig.itemStyle.borderRadius = [2, 2, 0, 0]; // 顶部圆角
}
return barConfig;
}
});
},
// 渲染图表
renderChart() {
const processedSeries = this.processChartData();
const seriesConfig = this.getSeriesConfig(processedSeries);
const yAxisConfig = this.getYAxisConfig();
// 计算图例显示条件:单系列时不显示图例
const showLegend = this.chartType !== 'single';
// 堆叠柱状图的tooltip配置 [2](@ref)
const tooltipFormatter = this.chartType === 'stacked'
? this.getStackedTooltipFormatter
: null;
const option = {
title: {
text: this.title,
left: 'left',
top: 10,
textStyle: {
color: '#333',
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: this.chartType === 'stacked' ? 'shadow' : 'line' // 堆叠模式使用阴影指针 [2](@ref)
},
formatter: tooltipFormatter || this.getDefaultTooltipFormatter(),
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
legend: {
show: showLegend,
data: seriesConfig.map(s => s.name),
right: 10,
top: 10,
orient: 'horizontal',
type: 'scroll',
textStyle: {
color: '#666'
}
},
grid: {
left: '3%',
right: this.dualYAxis ? '8%' : '3%',
bottom: '8%',
top: showLegend ? 60 : 40,
containLabel: true
},
xAxis: {
type: 'category',
data: this.categories,
axisLine: {
lineStyle: {
color: '#999'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#666',
interval: 0,
rotate: this.categories.length > 6 ? 45 : 0,
margin: 10
}
},
yAxis: yAxisConfig,
series: seriesConfig
};
this.myChart.setOption(option);
},
// 默认tooltip格式化器
getDefaultTooltipFormatter() {
return (params) => {
let result = `<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">${params[0].name}</div>`;
params.forEach(param => {
const color = param.color;
const seriesName = param.seriesName;
const value = param.value;
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${color};"></span>`;
result += `
<div style="display: flex; justify-content: space-between; align-items: center; margin: 4px 0; min-width: 120px;">
<div style="display: flex; align-items: center;">
${marker}
<span style="margin-right: 15px;">${seriesName}</span>
</div>
<span style="font-weight: bold; font-size: 13px;">${value}</span>
</div>
`;
});
return result;
};
},
// 堆叠柱状图tooltip格式化器 [2](@ref)
getStackedTooltipFormatter(params) {
let result = `<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">${params[0].name}</div>`;
let total = 0;
// 计算总和
params.forEach(param => {
total += param.value || 0;
});
params.forEach(param => {
const color = param.color;
const seriesName = param.seriesName;
const value = param.value;
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${color};"></span>`;
result += `
<div style="display: flex; justify-content: space-between; align-items: center; margin: 4px 0; min-width: 150px;">
<div style="display: flex; align-items: center;">
${marker}
<span style="margin-right: 15px;">${seriesName}</span>
</div>
<div style="text-align: right;">
<span style="font-weight: bold; font-size: 13px;">${value}</span>
</div>
</div>
`;
});
return result;
},
handleResize() {
if (this.myChart) {
this.myChart.resize();
}
}
},
watch: {
chartData: {
handler() {
if (this.myChart) {
this.renderChart();
}
},
deep: true
},
categories() {
if (this.myChart) {
this.renderChart();
}
},
chartType() {
if (this.myChart) {
this.renderChart();
}
},
dualYAxis() {
if (this.myChart) {
this.renderChart();
}
},
stackName() {
if (this.myChart && this.chartType === 'stacked') {
this.renderChart();
}
}
}
};
</script>