|
|
|
@ -1,165 +1,332 @@
|
|
|
|
|
<template>
|
|
|
|
|
<el-form style="margin: 20px 0px" label-width="80px" size="default">
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button style="padding: 0px 50px" @click="charts('0')">日统计</el-button>
|
|
|
|
|
<el-button @click="charts('1')" style="padding: 0px 50px" type="primary" plain>月统计</el-button>
|
|
|
|
|
<el-button @click="charts('2')" style="padding: 0px 50px" type="success" plain>年统计</el-button>
|
|
|
|
|
<el-button style="padding: 0px 50px" @click="charts('0')" :type="activeChart === '0' ? 'primary' : ''">
|
|
|
|
|
日统计
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button @click="charts('1')" style="padding: 0px 50px" :type="activeChart === '1' ? 'primary' : ''">
|
|
|
|
|
月统计
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button @click="charts('2')" style="padding: 0px 50px" :type="activeChart === '2' ? 'primary' : ''">
|
|
|
|
|
年统计
|
|
|
|
|
</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<div ref="myChart" :style="{ width: '100%', height: '400px' }"></div>
|
|
|
|
|
<div ref="myChart1" :style="{ width: '100%', height: '350px' }"></div>
|
|
|
|
|
|
|
|
|
|
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon style="margin-bottom: 20px;"
|
|
|
|
|
@close="errorMessage = ''" />
|
|
|
|
|
|
|
|
|
|
<el-skeleton :rows="5" animated v-if="loading" />
|
|
|
|
|
|
|
|
|
|
<!-- 确保图表容器始终存在,使用v-show -->
|
|
|
|
|
<div v-show="!loading" style="min-height: 800px;">
|
|
|
|
|
<div ref="myChart" :style="{ width: '100%', height: '400px' }"></div>
|
|
|
|
|
<div ref="myChart1" :style="{ width: '100%', height: '350px' }"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
import { onMounted, reactive, ref } from "vue";
|
|
|
|
|
import { onMounted, reactive, ref, nextTick, onUnmounted } from "vue";
|
|
|
|
|
import useInstance from "@/hooks/useInstance";
|
|
|
|
|
import { getTotalApi } from '../../api/order/index'
|
|
|
|
|
|
|
|
|
|
// 定义API响应类型
|
|
|
|
|
interface ApiResponse {
|
|
|
|
|
code: number;
|
|
|
|
|
data: {
|
|
|
|
|
names: string[];
|
|
|
|
|
values: number[];
|
|
|
|
|
};
|
|
|
|
|
message?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { global } = useInstance();
|
|
|
|
|
const myChart = ref<HTMLElement>();
|
|
|
|
|
const myChart1 = ref<HTMLElement>();
|
|
|
|
|
const myChart = ref<HTMLElement | null>(null);
|
|
|
|
|
const myChart1 = ref<HTMLElement | null>(null);
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const errorMessage = ref("");
|
|
|
|
|
const activeChart = ref("0");
|
|
|
|
|
|
|
|
|
|
// 清理图表实例
|
|
|
|
|
let echartInstance: any = null;
|
|
|
|
|
let echartInstance1: any = null;
|
|
|
|
|
let resizeObserver: ResizeObserver | null = null;
|
|
|
|
|
|
|
|
|
|
// 完全避免使用 Promise 构造函数
|
|
|
|
|
// 使用回调函数替代 Promise
|
|
|
|
|
const wait = (ms: number, callback: () => void): void => {
|
|
|
|
|
setTimeout(callback, ms);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 确保DOM元素存在的函数 - 使用回调而不是 Promise
|
|
|
|
|
const ensureElementExists = (callback: () => void): void => {
|
|
|
|
|
let attempts = 0;
|
|
|
|
|
|
|
|
|
|
const checkElement = () => {
|
|
|
|
|
attempts++;
|
|
|
|
|
if (myChart.value && myChart1.value) {
|
|
|
|
|
callback();
|
|
|
|
|
} else if (attempts < 20) {
|
|
|
|
|
wait(50, checkElement);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn("图表容器未找到,但继续执行");
|
|
|
|
|
callback();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
checkElement();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//柱状图
|
|
|
|
|
const charts = async (type: string) => {
|
|
|
|
|
const echartInstance = global.$echarts.init(myChart.value);
|
|
|
|
|
const echartInstance1 = global.$echarts.init(myChart1.value);
|
|
|
|
|
let option = reactive({
|
|
|
|
|
title: {
|
|
|
|
|
text: "直方图",
|
|
|
|
|
},
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: "category",
|
|
|
|
|
data: [],
|
|
|
|
|
axisLabel: {
|
|
|
|
|
//x轴文字的配置
|
|
|
|
|
show: true,
|
|
|
|
|
interval: 0, //使x轴文字显示全
|
|
|
|
|
try {
|
|
|
|
|
activeChart.value = type;
|
|
|
|
|
loading.value = true;
|
|
|
|
|
errorMessage.value = "";
|
|
|
|
|
|
|
|
|
|
// 先等待数据获取
|
|
|
|
|
const res = await getTotalApi(type) as ApiResponse;
|
|
|
|
|
|
|
|
|
|
if (!res || res.code !== 200) {
|
|
|
|
|
throw new Error(res?.message || "获取数据失败");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 数据获取成功后,再等待DOM更新
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
|
|
// 使用回调方式确保DOM元素存在
|
|
|
|
|
ensureElementExists(() => {
|
|
|
|
|
// 如果元素仍然不存在,抛出错误
|
|
|
|
|
if (!myChart.value || !myChart1.value) {
|
|
|
|
|
throw new Error("图表容器未找到");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initCharts(res, type);
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error("图表渲染错误:", error);
|
|
|
|
|
errorMessage.value = "加载图表数据失败: " + (error.message || "未知错误");
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 初始化图表的独立函数
|
|
|
|
|
const initCharts = (res: ApiResponse, type: string) => {
|
|
|
|
|
try {
|
|
|
|
|
// 清理现有图表
|
|
|
|
|
if (echartInstance) {
|
|
|
|
|
echartInstance.dispose();
|
|
|
|
|
}
|
|
|
|
|
if (echartInstance1) {
|
|
|
|
|
echartInstance1.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echartInstance = global.$echarts.init(myChart.value!);
|
|
|
|
|
echartInstance1 = global.$echarts.init(myChart1.value!);
|
|
|
|
|
|
|
|
|
|
let option = {
|
|
|
|
|
title: {
|
|
|
|
|
text: type === '0' ? "日统计直方图" : type === '1' ? "月统计直方图" : "年统计直方图",
|
|
|
|
|
left: 'center'
|
|
|
|
|
},
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: "category",
|
|
|
|
|
data: res.data.names || [],
|
|
|
|
|
axisLabel: {
|
|
|
|
|
show: true,
|
|
|
|
|
interval: 0,
|
|
|
|
|
rotate: 30 // 添加旋转防止文字重叠
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: "value",
|
|
|
|
|
name: '金额'
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: "value",
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
data: [],
|
|
|
|
|
type: "bar",
|
|
|
|
|
itemStyle: {
|
|
|
|
|
normal: {
|
|
|
|
|
//这里是颜色
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
data: res.data.values || [],
|
|
|
|
|
type: "bar",
|
|
|
|
|
itemStyle: {
|
|
|
|
|
color: function (params: any) {
|
|
|
|
|
//注意,如果颜色太少的话,后面颜色不会自动循环,最好多定义几个颜色
|
|
|
|
|
var colorList = [
|
|
|
|
|
"#00A3E0",
|
|
|
|
|
"#FFA100",
|
|
|
|
|
"#ffc0cb",
|
|
|
|
|
"#CCCCCC",
|
|
|
|
|
"#BBFFAA",
|
|
|
|
|
"#749f83",
|
|
|
|
|
"#ca8622",
|
|
|
|
|
"#00A3E0",
|
|
|
|
|
"#FFA100",
|
|
|
|
|
"#ffc0cb",
|
|
|
|
|
"#CCCCCC",
|
|
|
|
|
"#BBFFAA",
|
|
|
|
|
"#749f83",
|
|
|
|
|
"#ca8622",
|
|
|
|
|
"#00A3E0",
|
|
|
|
|
"#FFA100",
|
|
|
|
|
"#ffc0cb",
|
|
|
|
|
"#CCCCCC",
|
|
|
|
|
"#BBFFAA",
|
|
|
|
|
"#749f83",
|
|
|
|
|
"#ca8622",
|
|
|
|
|
"#00A3E0",
|
|
|
|
|
"#FFA100",
|
|
|
|
|
"#ffc0cb",
|
|
|
|
|
"#CCCCCC",
|
|
|
|
|
"#BBFFAA",
|
|
|
|
|
"#749f83",
|
|
|
|
|
"#ca8622",
|
|
|
|
|
"#00A3E0",
|
|
|
|
|
"#FFA100",
|
|
|
|
|
"#ffc0cb",
|
|
|
|
|
"#CCCCCC",
|
|
|
|
|
"#BBFFAA",
|
|
|
|
|
"#749f83",
|
|
|
|
|
"#ca8622",
|
|
|
|
|
"#00A3E0",
|
|
|
|
|
"#FFA100",
|
|
|
|
|
"#ffc0cb",
|
|
|
|
|
"#CCCCCC",
|
|
|
|
|
"#BBFFAA",
|
|
|
|
|
"#749f83",
|
|
|
|
|
"#ca8622",
|
|
|
|
|
"#00A3E0", "#FFA100", "#ffc0cb", "#CCCCCC", "#BBFFAA",
|
|
|
|
|
"#749f83", "#ca8622", "#00A3E0", "#FFA100", "#ffc0cb"
|
|
|
|
|
];
|
|
|
|
|
return colorList[params.dataIndex];
|
|
|
|
|
return colorList[params.dataIndex % colorList.length];
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: "axis",
|
|
|
|
|
backgroundColor: "rgba(32, 33, 36,.7)",
|
|
|
|
|
borderColor: "rgba(32, 33, 36,0.20)",
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
textStyle: {
|
|
|
|
|
color: "#fff",
|
|
|
|
|
fontSize: "12",
|
|
|
|
|
},
|
|
|
|
|
formatter: function (params: any) {
|
|
|
|
|
return `${params[0].name}<br/>金额: ${params[0].value}`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
tooltip: {
|
|
|
|
|
// 鼠标悬浮提示框显示 X和Y 轴数据
|
|
|
|
|
trigger: "axis",
|
|
|
|
|
backgroundColor: "rgba(32, 33, 36,.7)",
|
|
|
|
|
borderColor: "rgba(32, 33, 36,0.20)",
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
textStyle: {
|
|
|
|
|
// 文字提示样式
|
|
|
|
|
color: "#fff",
|
|
|
|
|
fontSize: "12",
|
|
|
|
|
grid: {
|
|
|
|
|
left: '3%',
|
|
|
|
|
right: '4%',
|
|
|
|
|
bottom: '10%',
|
|
|
|
|
containLabel: true
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let option1 = {
|
|
|
|
|
title: {
|
|
|
|
|
text: type === '0' ? "日统计折线图" : type === '1' ? "月统计折线图" : "年统计折线图",
|
|
|
|
|
left: 'center'
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
let option1 = reactive({
|
|
|
|
|
title: {
|
|
|
|
|
text: "折线图",
|
|
|
|
|
},
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: "category",
|
|
|
|
|
data: [],
|
|
|
|
|
axisLabel: {
|
|
|
|
|
//x轴文字的配置
|
|
|
|
|
show: true,
|
|
|
|
|
interval: 0, //使x轴文字显示全
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: "category",
|
|
|
|
|
data: res.data.names || [],
|
|
|
|
|
axisLabel: {
|
|
|
|
|
show: true,
|
|
|
|
|
interval: 0,
|
|
|
|
|
rotate: 30
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: "value",
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
data: [],
|
|
|
|
|
type: "line",
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: "value",
|
|
|
|
|
name: '金额'
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
tooltip: {
|
|
|
|
|
// 鼠标悬浮提示框显示 X和Y 轴数据
|
|
|
|
|
trigger: "axis",
|
|
|
|
|
backgroundColor: "rgba(32, 33, 36,.7)",
|
|
|
|
|
borderColor: "rgba(32, 33, 36,0.20)",
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
textStyle: {
|
|
|
|
|
// 文字提示样式
|
|
|
|
|
color: "#fff",
|
|
|
|
|
fontSize: "12",
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
data: res.data.values || [],
|
|
|
|
|
type: "line",
|
|
|
|
|
smooth: true,
|
|
|
|
|
itemStyle: {
|
|
|
|
|
color: '#00A3E0'
|
|
|
|
|
},
|
|
|
|
|
lineStyle: {
|
|
|
|
|
width: 3
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: "axis",
|
|
|
|
|
backgroundColor: "rgba(32, 33, 36,.7)",
|
|
|
|
|
borderColor: "rgba(32, 33, 36,0.20)",
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
textStyle: {
|
|
|
|
|
color: "#fff",
|
|
|
|
|
fontSize: "12",
|
|
|
|
|
},
|
|
|
|
|
formatter: function (params: any) {
|
|
|
|
|
return `${params[0].name}<br/>金额: ${params[0].value}`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
//动态获取数据
|
|
|
|
|
let res = await getTotalApi(type)
|
|
|
|
|
if (res && res.data) {
|
|
|
|
|
console.log(res)
|
|
|
|
|
option.xAxis.data = res.data.names
|
|
|
|
|
option.series[0].data = res.data.values
|
|
|
|
|
option1.xAxis.data = res.data.names
|
|
|
|
|
option1.series[0].data = res.data.values
|
|
|
|
|
grid: {
|
|
|
|
|
left: '3%',
|
|
|
|
|
right: '4%',
|
|
|
|
|
bottom: '10%',
|
|
|
|
|
containLabel: true
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
echartInstance.setOption(option);
|
|
|
|
|
echartInstance1.setOption(option1);
|
|
|
|
|
|
|
|
|
|
// 监听容器大小变化
|
|
|
|
|
if (resizeObserver) {
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
if (echartInstance) {
|
|
|
|
|
echartInstance.resize();
|
|
|
|
|
}
|
|
|
|
|
if (echartInstance1) {
|
|
|
|
|
echartInstance1.resize();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (myChart.value) {
|
|
|
|
|
resizeObserver.observe(myChart.value);
|
|
|
|
|
}
|
|
|
|
|
if (myChart1.value) {
|
|
|
|
|
resizeObserver.observe(myChart1.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加窗口调整大小时重绘图表
|
|
|
|
|
const resizeHandler = function () {
|
|
|
|
|
if (echartInstance) {
|
|
|
|
|
echartInstance.resize();
|
|
|
|
|
}
|
|
|
|
|
if (echartInstance1) {
|
|
|
|
|
echartInstance1.resize();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener('resize', resizeHandler);
|
|
|
|
|
|
|
|
|
|
// 保存resizeHandler以便卸载时移除
|
|
|
|
|
(window as any).__chartResizeHandler = resizeHandler;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error("图表初始化错误:", error);
|
|
|
|
|
errorMessage.value = "图表初始化失败: " + (error.message || "未知错误");
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
echartInstance.setOption(option);
|
|
|
|
|
echartInstance1.setOption(option1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
charts('0');
|
|
|
|
|
// 使用setTimeout确保DOM完全加载
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
charts('0');
|
|
|
|
|
}, 300); // 增加延迟时间
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清理
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (echartInstance) {
|
|
|
|
|
echartInstance.dispose();
|
|
|
|
|
}
|
|
|
|
|
if (echartInstance1) {
|
|
|
|
|
echartInstance1.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除窗口调整事件监听器
|
|
|
|
|
if ((window as any).__chartResizeHandler) {
|
|
|
|
|
window.removeEventListener('resize', (window as any).__chartResizeHandler);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped></style>
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.chart-container {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.charts-wrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
flex: 1;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-item {
|
|
|
|
|
width: 100%;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.chart-container {
|
|
|
|
|
padding: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.charts-wrapper {
|
|
|
|
|
gap: 15px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|