新增预警页面
This commit is contained in:
@@ -1,33 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card title="账户占比" style="width: 100%; height: 400px; margin: 4px 0;">
|
<Card title="账户占比" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||||
|
<template #extra>
|
||||||
|
<div class="total-amount">
|
||||||
|
总金额:<span class="amount-value">{{ totalAmountText }}</span> 元
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { Card } from 'ant-design-vue';
|
import { Card } from 'ant-design-vue';
|
||||||
import { ErpAccount, erpAccountListAll } from '@jeesite/erp/api/erp/account';
|
import { ErpAccount, erpAccountListAll } from '@jeesite/erp/api/erp/account';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import { log } from 'console';
|
import type { ECharts, EChartsOption, LegendSelectedChangedParams } from 'echarts';
|
||||||
|
|
||||||
const listAccount = ref<ErpAccount[]>([]);
|
const listAccount = ref<ErpAccount[]>([]);
|
||||||
|
|
||||||
const chartDom = ref<HTMLDivElement | null>(null);
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
let myChart: echarts.ECharts | null = null;
|
let myChart: ECharts | null = null;
|
||||||
|
|
||||||
|
// 核心修复1:新增「是否全取消」的标记 + 维护所有图例名称的集合
|
||||||
|
const allLegendNames = ref<Set<string>>(new Set()); // 存储所有账户名称(图例名)
|
||||||
|
const selectedLegends = ref<Set<string>>(new Set()); // 选中的图例
|
||||||
|
const isAllUnselected = ref(false); // 标记是否所有图例都被取消选中
|
||||||
|
|
||||||
|
// 修复后的总金额计算逻辑
|
||||||
|
const totalAmountText = computed(() => {
|
||||||
|
// 1. 所有图例都取消选中时,显示 0 元
|
||||||
|
if (isAllUnselected.value) {
|
||||||
|
return '0.00';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始状态(全选):显示全部总金额
|
||||||
|
if (selectedLegends.value.size === 0 && !isAllUnselected.value) {
|
||||||
|
const total = listAccount.value.reduce(
|
||||||
|
(sum, item) => sum + (Number(item.currentBalance) || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return total.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 部分选中:显示选中账户的合计
|
||||||
|
const total = listAccount.value
|
||||||
|
.filter(item => selectedLegends.value.has(item.accountName || '未知账户'))
|
||||||
|
.reduce((sum, item) => sum + (Number(item.currentBalance) || 0), 0);
|
||||||
|
|
||||||
|
return total.toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
// 饼图数据格式化函数
|
|
||||||
const formatPercent = (value: number) => {
|
const formatPercent = (value: number) => {
|
||||||
return `${value.toFixed(1)}%`;
|
return `${value.toFixed(1)}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机的美观颜色
|
|
||||||
* @param count 需要生成的颜色数量
|
|
||||||
* @returns 颜色数组
|
|
||||||
*/
|
|
||||||
const generateRandomColors = (count: number): string[] => {
|
const generateRandomColors = (count: number): string[] => {
|
||||||
// 预设的美观颜色库(可扩展)
|
|
||||||
const colorLibrary = [
|
const colorLibrary = [
|
||||||
'#1890ff', '#52c41a', '#f5a623', '#fa8c16', '#722ed1', '#eb2f96',
|
'#1890ff', '#52c41a', '#f5a623', '#fa8c16', '#722ed1', '#eb2f96',
|
||||||
'#13c2c2', '#2f54eb', '#f7ba1e', '#f5222d', '#8543e0', '#0fc6c2',
|
'#13c2c2', '#2f54eb', '#f7ba1e', '#f5222d', '#8543e0', '#0fc6c2',
|
||||||
@@ -42,8 +68,8 @@ const generateRandomColors = (count: number): string[] => {
|
|||||||
const colors: string[] = [];
|
const colors: string[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const hue = Math.floor(Math.random() * 360);
|
const hue = Math.floor(Math.random() * 360);
|
||||||
const saturation = 70 + Math.floor(Math.random() * 20); // 70-90%
|
const saturation = 70 + Math.floor(Math.random() * 20);
|
||||||
const lightness = 45 + Math.floor(Math.random() * 15); // 45-60%
|
const lightness = 45 + Math.floor(Math.random() * 15);
|
||||||
colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
|
colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
|
||||||
}
|
}
|
||||||
return colors;
|
return colors;
|
||||||
@@ -52,37 +78,64 @@ const generateRandomColors = (count: number): string[] => {
|
|||||||
const fetchList = async () => {
|
const fetchList = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await erpAccountListAll();
|
const result = await erpAccountListAll();
|
||||||
listAccount.value = result || [];
|
listAccount.value = result || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取账号列表失败:', error);
|
console.error('获取账号列表失败:', error);
|
||||||
listAccount.value = []; // 异常时置空列表,显示空状态
|
listAccount.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initChart = () => {
|
// 核心修复2:重构图例选中事件处理逻辑
|
||||||
if (!chartDom.value) return;
|
const handleLegendSelect = (params: LegendSelectedChangedParams) => {
|
||||||
myChart = echarts.init(chartDom.value);
|
const { name, selected } = params;
|
||||||
const pieData = listAccount.value.map(item => ({
|
|
||||||
name: item.accountName || '未知账户',
|
|
||||||
value: Number(item.currentBalance) || 0
|
|
||||||
}));
|
|
||||||
|
|
||||||
const colors = generateRandomColors(pieData.length);
|
// 1. 更新选中集合
|
||||||
|
if (selected[name]) {
|
||||||
const pieDataWithColor = pieData.map((item, index) => ({
|
selectedLegends.value.add(name);
|
||||||
...item,
|
} else {
|
||||||
color: colors[index]
|
selectedLegends.value.delete(name);
|
||||||
}));
|
}
|
||||||
|
|
||||||
const total = pieDataWithColor.reduce((sum, item) => sum + item.value, 0);
|
|
||||||
const formattedData = pieDataWithColor.map(item => ({
|
|
||||||
name: item.name,
|
|
||||||
value: item.value,
|
|
||||||
percent: (item.value / total) * 100,
|
|
||||||
color: item.color
|
|
||||||
}));
|
|
||||||
|
|
||||||
const option = {
|
// 2. 判断是否所有图例都被取消选中
|
||||||
|
isAllUnselected.value = selectedLegends.value.size === 0 && allLegendNames.value.size > 0;
|
||||||
|
|
||||||
|
// 3. 强制更新图表(确保饼图和总金额同步)
|
||||||
|
myChart?.setOption({
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '账户金额',
|
||||||
|
data: listAccount.value.map(item => ({
|
||||||
|
name: item.accountName || '未知账户',
|
||||||
|
value: Number(item.currentBalance) || 0
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!chartDom.value || listAccount.value.length === 0) return;
|
||||||
|
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
myChart = echarts.init(chartDom.value);
|
||||||
|
|
||||||
|
const pieData = listAccount.value.map(item => ({
|
||||||
|
name: item.accountName || '未知账户',
|
||||||
|
value: Number(item.currentBalance) || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
const validData = pieData.filter(item => item.value > 0);
|
||||||
|
const colors = generateRandomColors(validData.length);
|
||||||
|
|
||||||
|
// 核心修复3:初始化所有图例名称集合
|
||||||
|
allLegendNames.value = new Set(validData.map(item => item.name));
|
||||||
|
|
||||||
|
const total = validData.reduce((sum, item) => sum + item.value, 0);
|
||||||
|
|
||||||
|
const option: EChartsOption = {
|
||||||
title: {
|
title: {
|
||||||
left: 'center',
|
left: 'center',
|
||||||
top: 10,
|
top: 10,
|
||||||
@@ -94,83 +147,93 @@ const initChart = () => {
|
|||||||
textStyle: { fontSize: 12 }
|
textStyle: { fontSize: 12 }
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal', // 水平布局
|
orient: 'horizontal',
|
||||||
bottom: 5, // 图例在底部
|
bottom: 5,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
textStyle: { fontSize: 12, color: '#666' },
|
textStyle: { fontSize: 12, color: '#666' },
|
||||||
itemWidth: 12,
|
itemWidth: 12,
|
||||||
itemHeight: 12,
|
itemHeight: 12,
|
||||||
itemGap: 30, // 图例项之间的间距(默认10),调大到30
|
itemGap: 30,
|
||||||
padding: [10, 20], // 图例内边距 [上, 右, 下, 左]
|
padding: [10, 20],
|
||||||
spacing: [20, 10], // 图例组件之间的间距
|
spacing: [20, 10],
|
||||||
},
|
},
|
||||||
// 饼图系列配置
|
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '账户金额',
|
name: '账户金额',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'], // 内环和外环半径,实现环形饼图效果
|
radius: ['40%', '70%'],
|
||||||
center: ['50%', '45%'], // 微调饼图位置,给图例留出更多空间
|
center: ['50%', '45%'],
|
||||||
avoidLabelOverlap: true, // 开启避免标签重叠
|
avoidLabelOverlap: true,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 8, // 扇形圆角
|
borderRadius: 8,
|
||||||
borderColor: '#fff', // 扇形边框白色
|
borderColor: '#fff',
|
||||||
borderWidth: 2 // 边框宽度
|
borderWidth: 2
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true, // 显示标签
|
show: true,
|
||||||
position: 'outside', // 标签在饼图外部
|
position: 'outside',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
formatter: (params: any) => {
|
formatter: (params: any) => {
|
||||||
return `${params.name} ${formatPercent(params.percent)}`;
|
return `${params.name} ${formatPercent((params.value / total) * 100)}`;
|
||||||
},
|
},
|
||||||
color: '#333',
|
color: '#333',
|
||||||
distance: 20, // 标签离饼图的距离,避免和图例重叠
|
distance: 20,
|
||||||
},
|
},
|
||||||
labelLine: {
|
labelLine: {
|
||||||
show: true, // 显示标签连接线
|
show: true,
|
||||||
length: 20, // 第一段线长度(加长避免重叠)
|
length: 20,
|
||||||
length2: 15, // 第二段线长度
|
length2: 15,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1,
|
width: 1,
|
||||||
color: '#999'
|
color: '#999'
|
||||||
},
|
},
|
||||||
smooth: 0.2, // 连接线轻微平滑,避免生硬
|
smooth: 0.2,
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
// 悬浮高亮效果
|
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
shadowBlur: 10,
|
shadowBlur: 10,
|
||||||
shadowOffsetX: 0,
|
shadowOffsetX: 0,
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.2)'
|
shadowColor: 'rgba(0, 0, 0, 0.2)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 数据项(指定随机颜色)
|
data: validData.map((item, index) => ({
|
||||||
data: formattedData.map(item => ({
|
|
||||||
name: item.name,
|
name: item.name,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
itemStyle: { color: item.color }
|
itemStyle: { color: colors[index] }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
myChart.setOption(option);
|
myChart.setOption(option);
|
||||||
|
|
||||||
|
myChart.on('legendselectchanged', handleLegendSelect);
|
||||||
|
|
||||||
|
// 初始化选中状态:全选 + 标记非全取消
|
||||||
|
selectedLegends.value = new Set(validData.map(item => item.name));
|
||||||
|
isAllUnselected.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 窗口缩放自适应
|
const resizeChart = () => {
|
||||||
const resizeChart = () => myChart?.resize();
|
myChart?.resize();
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async() => {
|
onMounted(async () => {
|
||||||
await fetchList()
|
await fetchList();
|
||||||
initChart();
|
initChart();
|
||||||
window.addEventListener('resize', resizeChart);
|
window.addEventListener('resize', resizeChart);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', resizeChart);
|
window.removeEventListener('resize', resizeChart);
|
||||||
myChart?.dispose();
|
if (myChart) {
|
||||||
myChart = null;
|
myChart.off('legendselectchanged', handleLegendSelect);
|
||||||
|
myChart.dispose();
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
selectedLegends.value.clear();
|
||||||
|
allLegendNames.value.clear();
|
||||||
|
isAllUnselected.value = false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -180,27 +243,39 @@ onUnmounted(() => {
|
|||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 优化饼图图例样式 - 增强间距效果 */
|
.total-amount {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-value {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-card .echarts-legend-item) {
|
:deep(.ant-card .echarts-legend-item) {
|
||||||
margin: 0 8px !important; /* 增加左右外边距 */
|
margin: 0 8px !important;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 优化图例文字和图标对齐 */
|
|
||||||
:deep(.echarts-legend-text) {
|
:deep(.echarts-legend-text) {
|
||||||
margin-left: 6px; /* 文字和图例图标之间的间距 */
|
margin-left: 6px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 优化饼图标签字体 */
|
|
||||||
:deep(.echarts-text) {
|
:deep(.echarts-text) {
|
||||||
font-family: 'Microsoft YaHei', sans-serif;
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 悬浮高亮效果优化 */
|
|
||||||
:deep(.echarts-tooltip) {
|
:deep(.echarts-tooltip) {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
:deep(.echarts-empty) {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user