新增预警页面

This commit is contained in:
2025-12-10 23:27:11 +08:00
parent ee34f3bbf6
commit a2f4e1c31f

View File

@@ -18,9 +18,9 @@ import { Card } from 'ant-design-vue';
import { BasicForm, FormSchema } from '@jeesite/core/components/Form'; import { BasicForm, FormSchema } from '@jeesite/core/components/Form';
import { useMessage } from '@jeesite/core/hooks/web/useMessage'; import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { ErpSummaryAll, erpSummaryAllListAll } from '@jeesite/erp/api/erp/summaryAll'; import { ErpSummaryAll, erpSummaryAllListAll } from '@jeesite/erp/api/erp/summaryAll';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
// 表单默认值
const defaultFormValues = { const defaultFormValues = {
ctype: '1', ctype: '1',
fcycle: 'M' fcycle: 'M'
@@ -67,18 +67,31 @@ const chartDom = ref<HTMLDivElement | null>(null);
let myChart: echarts.ECharts | null = null; let myChart: echarts.ECharts | null = null;
const { createMessage } = useMessage(); const { createMessage } = useMessage();
// 保留2位小数的工具函数 // ========== 核心工具函数 ==========
const formatNumber = (num: number | undefined, decimal = 2): number => { // 保留2位小数增强容错
if (num === undefined || isNaN(num)) return 0; const formatNumber = (num: number | string | undefined, decimal = 2): number => {
return Number(num.toFixed(decimal)); const parsed = Number(num);
if (isNaN(parsed)) return 0;
return Number(parsed.toFixed(decimal));
}; };
// 转换为万元保留2位小数 // 元转万元(仅用于柱状图/Y轴展示
const toTenThousandYuan = (num: number | undefined): number => { const toTenThousandYuan = (num: number | string | undefined): number => {
return formatNumber((num || 0) / 10000); const rawNum = formatNumber(num);
return formatNumber(rawNum / 10000);
}; };
// 柱状图标签配置(显示万元 // 千分位格式化Tooltip显示原始元时使用
const formatWithThousandsSeparator = (num: number | string | undefined): string => {
const parsed = Number(num);
if (isNaN(parsed)) return '0.00';
return parsed.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
};
// ========== 柱状图标签仅显示数值(无单位) ==========
const barLabelConfig = { const barLabelConfig = {
show: true, show: true,
position: 'top', position: 'top',
@@ -88,10 +101,10 @@ const barLabelConfig = {
color: '#333', color: '#333',
fontWeight: '500' fontWeight: '500'
}, },
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)} 万元` formatter: (params: any) => `${formatNumber(params.value).toFixed(2)}`
}; };
// 折线图标签配置保留2位小数 // 折线图标签配置:百分比格式(保留单位
const lineLabelConfig = { const lineLabelConfig = {
show: true, show: true,
position: 'top', position: 'top',
@@ -104,16 +117,34 @@ const lineLabelConfig = {
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)} %` formatter: (params: any) => `${formatNumber(params.value).toFixed(2)} %`
}; };
// 表单提交处理
const handleFormSubmit = (values: Record<string, any>) => { const handleFormSubmit = (values: Record<string, any>) => {
currentFormValues.value = { ...values }; currentFormValues.value = { ...values };
fetchList(currentFormValues.value); fetchList(currentFormValues.value);
}; };
// 日期格式化(统一格式)
const formatDate = (date: string | undefined): string => {
if (!date) return '';
return date.replace(/\//g, '-').trim();
};
// 获取数据列表 // 获取数据列表
const fetchList = async (params: Record<string, any>) => { const fetchList = async (params: Record<string, any>) => {
try { try {
const result = await erpSummaryAllListAll(params); const result = await erpSummaryAllListAll(params);
listSummary.value = result || []; console.log('接口原始数据(元):', result);
// 过滤空数据 + 统一日期格式 + 排序
const validData = (result || [])
.filter(item => item.cdate)
.map(item => ({
...item,
cdate: formatDate(item.cdate)
}))
.sort((a, b) => a.cdate.localeCompare(b.cdate));
listSummary.value = validData;
initChart(); initChart();
} catch (error) { } catch (error) {
console.error('获取数据列表失败:', error); console.error('获取数据列表失败:', error);
@@ -122,27 +153,27 @@ const fetchList = async (params: Record<string, any>) => {
} }
}; };
// 计算Y轴极值适配负数显示并确保0点对齐 // 计算Y轴极值适配万元展示层)
const calculateYAxisExtent = (data: number[], isRate = false) => { const calculateYAxisExtent = (data: number[], isRate = false) => {
if (data.length === 0) return isRate ? [-10, 10] : [0, 100]; if (data.length === 0) return isRate ? [-10, 10] : [0, 10]; // 万元默认范围
// 格式化数据保留2位小数
const formattedData = data.map(num => formatNumber(num)); const formattedData = data.map(num => formatNumber(num));
const min = Math.min(...formattedData); const min = Math.min(...formattedData);
const max = Math.max(...formattedData); const max = Math.max(...formattedData);
// 计算内边距(百分比轴内边距更大,确保负数显示清晰) // 万元轴内边距10% 或最小1万元
const padding = isRate ? Math.max(Math.abs(max) * 0.2, Math.abs(min) * 0.2, 5) const padding = isRate
: (max - min) * 0.1 || 10; ? Math.max(Math.abs(max) * 0.2, Math.abs(min) * 0.2, 5)
: Math.max((max - min) * 0.1, 1);
let minExtent = min - padding; let minExtent = min - padding;
let maxExtent = max + padding; let maxExtent = max + padding;
// 强制包含0点确保X轴对齐 // 强制包含0点
if (minExtent > 0) minExtent = 0; if (minExtent > 0) minExtent = 0;
if (maxExtent < 0) maxExtent = 0; if (maxExtent < 0) maxExtent = 0;
// 百分比轴确保正负对称(可选,提升视觉效果) // 百分比轴正负对称
if (isRate) { if (isRate) {
const absMax = Math.max(Math.abs(minExtent), Math.abs(maxExtent)); const absMax = Math.max(Math.abs(minExtent), Math.abs(maxExtent));
minExtent = -absMax; minExtent = -absMax;
@@ -152,37 +183,51 @@ const calculateYAxisExtent = (data: number[], isRate = false) => {
return [minExtent, maxExtent]; return [minExtent, maxExtent];
}; };
// 初始化图表 // 初始化图表核心修复Tooltip兼容处理
const initChart = () => { const initChart = () => {
if (!chartDom.value) return; if (!chartDom.value) return;
// 如果图表实例已存在,先销毁
if (myChart) { if (myChart) {
myChart.dispose(); myChart.dispose();
} }
// 创建新的图表实例
myChart = echarts.init(chartDom.value); myChart = echarts.init(chartDom.value);
// 处理空数据 // 提取X轴类目
const xAxisData = listSummary.value.length const xAxisData = listSummary.value.map(item => item.cdate).filter(Boolean);
? listSummary.value.map(item => item.cdate || '')
: [];
// 提取金额数据转换为万元用于计算Y轴范围 // ========== 核心分离:展示数据(万元)和原始数据(元) ==========
const thisValueData = listSummary.value.map(item => toTenThousandYuan(item.thisValue)); // 1. 展示层数据(万元):用于柱状图/Y轴/标签
const prevValueData = listSummary.value.map(item => toTenThousandYuan(item.prevValue)); const thisValueShow = xAxisData.map(date => {
const amountData = [...thisValueData, ...prevValueData]; const item = listSummary.value.find(i => i.cdate === date);
return toTenThousandYuan(item?.thisValue);
});
const prevValueShow = xAxisData.map(date => {
const item = listSummary.value.find(i => i.cdate === date);
return toTenThousandYuan(item?.prevValue);
});
// 2. 原始数据直接从接口数据提取不做任何换算用于Tooltip
const rawDataMap = new Map<string, { thisValue: any; prevValue: any; momRate: any }>();
listSummary.value.forEach(item => {
rawDataMap.set(item.cdate, {
thisValue: item.thisValue, // 接口原始值(元)
prevValue: item.prevValue, // 接口原始值(元)
momRate: item.momRate // 接口原始值(%
});
});
// 环比数据(不变)
const rateData = xAxisData.map(date => {
const item = listSummary.value.find(i => i.cdate === date);
return formatNumber(item?.momRate);
});
// 计算Y轴范围基于万元展示数据
const amountData = [...thisValueShow, ...prevValueShow];
const [amountMin, amountMax] = calculateYAxisExtent(amountData); const [amountMin, amountMax] = calculateYAxisExtent(amountData);
// 提取环比数据用于计算百分比轴范围保留2位小数
const rateData = listSummary.value.map(item => formatNumber(item.momRate));
const [rateMin, rateMax] = calculateYAxisExtent(rateData, true); const [rateMin, rateMax] = calculateYAxisExtent(rateData, true);
// 原始金额数据用于tooltip显示元
const rawThisValueData = listSummary.value.map(item => formatNumber(item.thisValue));
const rawPrevValueData = listSummary.value.map(item => formatNumber(item.prevValue));
const option = { const option = {
title: { title: {
left: 'center', left: 'center',
@@ -192,23 +237,45 @@ const initChart = () => {
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'shadow' }, axisPointer: { type: 'shadow' },
textStyle: { fontSize: 12 }, textStyle: { fontSize: 12 },
padding: 10,
// ========== 核心修复兼容params长度不固定 + 空值容错 ==========
formatter: (params: any[]) => { formatter: (params: any[]) => {
let res = params[0].axisValue; if (!params || params.length === 0) return '暂无数据';
params.forEach((param, index) => {
// 区分金额和环比:金额显示原始元,环比显示百分比
let value = param.value;
let unit = ' 元';
if (param.seriesName === '环比') { const date = params[0]?.axisValue || '';
value = formatNumber(param.value); const rawData = rawDataMap.get(date) || { thisValue: 0, prevValue: 0, momRate: 0 };
unit = ' %';
} else { let res = `<div style="font-weight: 500; margin-bottom: 4px;">${date || '未知日期'}</div>`;
// 还原为原始元数值图表展示的是万元需要乘10000
value = formatNumber(param.value * 10000); // 动态遍历params不依赖固定索引
params.forEach((param) => {
// 跳过无效参数
if (!param || !param.seriesName) return;
let displayText = '';
const seriesName = param.seriesName;
// 根据系列名称匹配原始数据避免依赖param.value
if (seriesName === '本期金额') {
displayText = `${param.marker || ''}本期金额:<span style="font-weight: 500;">${
formatWithThousandsSeparator(rawData.thisValue)
}</span> 元`;
} else if (seriesName === '上期金额') {
displayText = `${param.marker || ''}上期金额:<span style="font-weight: 500;">${
formatWithThousandsSeparator(rawData.prevValue)
}</span> 元`;
} else if (seriesName === '环比') {
displayText = `${param.marker || ''}环比:<span style="font-weight: 500;">${
formatNumber(rawData.momRate).toFixed(2)
}</span> %`;
} }
res += `<br/>${param.marker}${param.seriesName}${value.toFixed(2)}${unit}`; // 只有有效文本才添加到Tooltip
if (displayText) {
res += `<div style="margin: 2px 0;">${displayText}</div>`;
}
}); });
return res; return res;
} }
}, },
@@ -219,7 +286,7 @@ const initChart = () => {
}, },
grid: { grid: {
left: '8%', left: '8%',
right: '12%', // 增加右侧边距适配百分比轴 right: '12%',
bottom: '10%', bottom: '10%',
top: '20%', top: '20%',
containLabel: true containLabel: true
@@ -228,34 +295,32 @@ const initChart = () => {
type: 'category', type: 'category',
data: xAxisData, data: xAxisData,
axisLabel: { fontSize: 12 }, axisLabel: { fontSize: 12 },
// 确保X轴在0点位置 axisLine: { onZero: true }
axisLine: {
onZero: true
}
}, },
yAxis: [ yAxis: [
{ {
type: 'value', type: 'value',
name: '交易金额(万元)', // Y轴名称改为万元 name: '交易金额(万元)', // Y轴仍保留万元单位
nameTextStyle: { fontSize: 12 },
axisLabel: { axisLabel: {
fontSize: 12, fontSize: 11,
formatter: (value: number) => formatNumber(value).toFixed(2) formatter: (value: number) => formatNumber(value).toFixed(2) // 万元数值
}, },
min: amountMin, min: amountMin,
max: amountMax, max: amountMax,
axisLine: { onZero: true }, axisLine: { onZero: true },
splitLine: { splitLine: { lineStyle: { color: '#e8e8e8' } },
lineStyle: { color: '#e8e8e8' }
},
axisTick: { alignWithLabel: true }, axisTick: { alignWithLabel: true },
splitNumber: 4 // 控制刻度数量 splitNumber: 5,
scale: true
}, },
{ {
type: 'value', type: 'value',
name: '环比(%', name: '环比(%',
nameTextStyle: { fontSize: 12 },
axisLabel: { axisLabel: {
fontSize: 12, fontSize: 11,
formatter: (value: number) => formatNumber(value).toFixed(2) + ' %' formatter: (value: number) => `${formatNumber(value).toFixed(2)} %`
}, },
min: rateMin, min: rateMin,
max: rateMax, max: rateMax,
@@ -267,47 +332,60 @@ const initChart = () => {
splitNumber: 4 splitNumber: 4
} }
], ],
// 空数据提示
noDataLoadingOption: { noDataLoadingOption: {
text: '暂无数据', text: '暂无数据',
textStyle: { fontSize: 14, color: '#999' }, textStyle: { fontSize: 14, color: '#999' },
effect: 'bubble', effect: 'bubble',
effectOption: { effectOption: { effect: { n: 0 } }
effect: {
n: 0
}
}
}, },
series: [ series: [
{ {
name: '本期金额', name: '本期金额',
type: 'bar', type: 'bar',
data: thisValueData, // 展示万元数据 data: thisValueShow, // 万元展示数据
itemStyle: { itemStyle: {
color: '#1890ff', color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#1890ff' },
{ offset: 1, color: '#096dd9' }
]),
borderRadius: [8, 8, 0, 0] borderRadius: [8, 8, 0, 0]
}, },
barWidth: 25, barWidth: 25,
barBorderRadius: [8, 8, 0, 0], barBorderRadius: [8, 8, 0, 0],
label: barLabelConfig, label: barLabelConfig, // 仅显示数值的标签
yAxisIndex: 0, yAxisIndex: 0,
// 存储原始元数据用于tooltip emphasis: {
rawData: rawThisValueData itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#40a9ff' },
{ offset: 1, color: '#1890ff' }
])
}
}
}, },
{ {
name: '上期金额', name: '上期金额',
type: 'bar', type: 'bar',
data: prevValueData, // 展示万元数据 data: prevValueShow, // 万元展示数据
itemStyle: { itemStyle: {
color: '#52c41a', color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#52c41a' },
{ offset: 1, color: '#389e0d' }
]),
borderRadius: [8, 8, 0, 0] borderRadius: [8, 8, 0, 0]
}, },
barWidth: 25, barWidth: 25,
barBorderRadius: [8, 8, 0, 0], barBorderRadius: [8, 8, 0, 0],
label: barLabelConfig, label: barLabelConfig, // 仅显示数值的标签
yAxisIndex: 0, yAxisIndex: 0,
// 存储原始元数据用于tooltip emphasis: {
rawData: rawPrevValueData itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#73d13d' },
{ offset: 1, color: '#52c41a' }
])
}
}
}, },
{ {
name: '环比', name: '环比',
@@ -316,30 +394,19 @@ const initChart = () => {
smooth: true, smooth: true,
symbol: 'circle', symbol: 'circle',
symbolSize: 8, symbolSize: 8,
lineStyle: { lineStyle: { width: 3, color: '#f5222d' },
width: 3,
color: '#f5222d'
},
itemStyle: { itemStyle: {
color: '#f5222d', color: '#f5222d',
borderColor: '#fff', borderColor: '#fff',
borderWidth: 2 borderWidth: 2
}, },
label: lineLabelConfig, label: lineLabelConfig,
emphasis: { emphasis: { itemStyle: { symbolSize: 12 } },
itemStyle: {
symbolSize: 12
}
},
yAxisIndex: 1, yAxisIndex: 1,
// 0%基准线精确对齐X轴
markLine: { markLine: {
silent: true, silent: true,
lineStyle: { color: '#ccc', type: 'dashed', width: 1 }, lineStyle: { color: '#ccc', type: 'dashed', width: 1 },
data: [{ data: [{ yAxis: 0, lineStyle: { color: '#999', type: 'dashed' } }]
yAxis: 0,
lineStyle: { color: '#999', type: 'dashed' }
}]
} }
} }
] ]
@@ -348,7 +415,7 @@ const initChart = () => {
myChart.setOption(option); myChart.setOption(option);
}; };
// 调整图表大小 // 调整图表大小(防抖+动画)
const resizeChart = () => { const resizeChart = () => {
if (myChart) { if (myChart) {
myChart.resize({ myChart.resize({
@@ -360,7 +427,7 @@ const resizeChart = () => {
} }
}; };
// 监听窗口大小变化(防抖处理 // 窗口 resize 防抖处理
let resizeTimer: number; let resizeTimer: number;
const debounceResize = () => { const debounceResize = () => {
clearTimeout(resizeTimer); clearTimeout(resizeTimer);
@@ -400,15 +467,12 @@ watch(listSummary, () => {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
} }
/* 优化图表样式 */ /* 优化图表tooltip样式 */
:deep(.echarts-label) {
font-family: 'Microsoft YaHei', sans-serif;
}
:deep(.echarts-tooltip) { :deep(.echarts-tooltip) {
border-radius: 4px; border-radius: 6px;
padding: 8px; padding: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
border: none;
} }
/* 优化表单样式 */ /* 优化表单样式 */
@@ -420,14 +484,14 @@ watch(listSummary, () => {
width: 100%; width: 100%;
} }
/* 优化双Y轴标签样式 */ /* 优化柱状图标签溢出处理 */
:deep(.echarts-yaxis-label) { :deep(.echarts-label) {
font-size: 12px; font-family: 'Microsoft YaHei', sans-serif;
color: #666; white-space: nowrap;
} }
/* 优化0%基准线样式 */ /* 优化Y轴名称间距 */
:deep(.echarts-mark-line) { :deep(.echarts-yaxis-name) {
z-index: 1; margin-right: 5px;
} }
</style> </style>