Files
td_official/src/views/largeScreen/components/leftPage.vue

328 lines
10 KiB
Vue
Raw Normal View History

2025-08-20 10:28:23 +08:00
<template>
2025-08-21 14:18:21 +08:00
<div class="leftPage">
<div class="kpi_box">
2025-08-22 18:25:54 +08:00
<TitleComponent :title="'支付KPI'" style="margin-bottom: 20px" />
2025-08-22 19:01:55 +08:00
<ProgressComponent title="应收账款" :value="formatCurrency(incomeData.planAmount)"
2025-08-22 18:25:54 +08:00
:percentageChange="getPercentageChange(incomeData.planAmount, incomeData.actualAmount)"
:progressPercentage="getProgressPercentage(incomeData.planAmount, incomeData.actualAmount)"
2025-08-22 19:01:55 +08:00
progressColor="rgba(255, 77, 79, 1)" />
<ProgressComponent title="应付账款" :value="formatCurrency(expensesData.planAmount)"
2025-08-22 18:25:54 +08:00
:percentageChange="getPercentageChange(expensesData.planAmount, expensesData.actualAmount)"
:progressPercentage="getProgressPercentage(expensesData.planAmount, expensesData.actualAmount)"
2025-08-22 19:01:55 +08:00
progressColor="rgba(29, 214, 255, 1)" />
<ProgressComponent title="本月付款" :value="formatCurrency(expensesData.actualAmount)"
2025-08-22 18:25:54 +08:00
:percentageChange="getPercentageChange(expensesData.planAmount, expensesData.actualAmount)"
:progressPercentage="getProgressPercentage(expensesData.planAmount, expensesData.actualAmount)"
2025-08-22 19:01:55 +08:00
progressColor="rgba(0, 227, 150, 1)" />
<ProgressComponent title="本月收款" :value="formatCurrency(incomeData.actualAmount)"
2025-08-22 18:25:54 +08:00
:percentageChange="getPercentageChange(incomeData.planAmount, incomeData.actualAmount)"
:progressPercentage="getProgressPercentage(incomeData.planAmount, incomeData.actualAmount)"
2025-08-22 19:01:55 +08:00
progressColor="rgba(255, 147, 42, 1)" />
2025-08-21 14:18:21 +08:00
</div>
2025-08-21 17:34:02 +08:00
<div class="contract_box">
2025-08-22 19:01:55 +08:00
<div style="height: 60px;">
<TitleComponent :title="'收支合同分析'" style="margin-bottom: 20px" />
</div>
2025-08-22 18:25:54 +08:00
<!-- 切换按钮 -->
2025-08-22 19:01:55 +08:00
2025-08-22 18:25:54 +08:00
<div style="margin-bottom: 10px; text-align: center">
2025-08-22 19:01:55 +08:00
<button @click="switchChart('income')"
:style="activeChart === 'income' ? activeBtnStyle : defaultBtnStyle">收入合同</button>
<button @click="switchChart('expenses')" :style="activeChart === 'expenses' ? activeBtnStyle : defaultBtnStyle"
style="margin-left: 15px">
2025-08-22 18:25:54 +08:00
支出合同
</button>
</div>
<!-- 环形图容器固定高度确保图表不拉伸添加flex居中 -->
<div class="chart-container">
<EchartBox :option="pieOption" />
</div>
2025-08-21 17:34:02 +08:00
</div>
2025-08-21 14:18:21 +08:00
</div>
2025-08-20 10:28:23 +08:00
</template>
2025-08-21 14:18:21 +08:00
<script setup>
2025-08-22 18:25:54 +08:00
import { ref, onMounted, watch } from 'vue';
import TitleComponent from './TitleComponent.vue';
2025-08-21 14:18:21 +08:00
import ProgressComponent from './ProgressComponent.vue';
2025-08-21 18:45:51 +08:00
import EchartBox from '@/components/EchartBox/index.vue';
2025-08-22 18:25:54 +08:00
import { incomePay, expensesPay, incomeAnalyze, expensesAnalyze } from '@/api/largeScreen/index';
// 初始化数据容器
2025-08-22 19:01:55 +08:00
const incomeData = ref({ planAmount: '0.00', actualAmount: '0.00' }); // 收入相关数据(应收/收款)
2025-08-22 18:25:54 +08:00
const expensesData = ref({ planAmount: '0.00', actualAmount: '0.00' }); // 支出相关数据(应付/付款)
const pieOption = ref({}); // 环形图配置
const activeChart = ref('income'); // 当前激活的图表类型income-收入合同expenses-支出合同
// 新增:存储合同数据及对应总数(避免重复计算)
const chartData = ref({
income: { firstCount: 0, secondCount: 0, thirdCount: 0, fourthCount: 0, total: 0 }, // 收入合同+总数
expenses: { firstCount: 0, secondCount: 0, thirdCount: 0, fourthCount: 0, total: 0 } // 支出合同+总数
});
// 按钮样式
const defaultBtnStyle = {
padding: '4px 16px',
border: '1px solid rgba(29, 214, 255, 0.5)',
backgroundColor: 'transparent',
color: 'rgba(29, 214, 255, 1)',
cursor: 'pointer',
borderRadius: '4px'
};
const activeBtnStyle = {
...defaultBtnStyle,
backgroundColor: 'rgba(29, 214, 255, 0.2)',
borderColor: 'rgba(29, 214, 255, 1)'
};
/**
* 格式化金额为带千分位的格式
* @param {string} amount 金额字符串
* @returns {string} 格式化后的金额
*/
const formatCurrency = (amount) => {
if (!amount) return '0.00';
return Number(amount).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
};
/**
* 计算百分比变化保留两位小数
* @param {string} plan 计划金额
* @param {string} actual 实际金额
* @returns {string} 带正负号的百分比字符串
*/
const getPercentageChange = (plan, actual) => {
const planNum = Number(plan);
const actualNum = Number(actual);
if (planNum === 0) {
return planNum === actualNum ? '0.00%' : '+100.00%';
}
const change = ((actualNum - planNum) / planNum) * 100;
return `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`;
};
/**
* 计算进度百分比保留整数
* @param {string} plan 计划金额
* @param {string} actual 实际金额
* @returns {number} 进度百分比0-100
*/
const getProgressPercentage = (plan, actual) => {
const planNum = Number(plan);
const actualNum = Number(actual);
if (planNum === 0) {
return actualNum === 0 ? 0 : 100;
}
const percentage = (actualNum / planNum) * 100;
return Math.min(Math.round(percentage), 100); // 限制最大为100
};
/**
* 获取并处理资金数据原逻辑保留
*/
const getCapitalData = async () => {
try {
const [incomeRes, expensesRes] = await Promise.all([incomePay(), expensesPay()]);
if (incomeRes.code === 200) {
incomeData.value = incomeRes.data;
}
if (expensesRes.code === 200) {
expensesData.value = expensesRes.data;
}
} catch (error) {
console.error('获取资金数据失败:', error);
}
2025-08-21 18:45:51 +08:00
};
2025-08-22 18:25:54 +08:00
/**
* 计算单类合同总数复用方法
* @param {Object} data 单类合同数据收入/支出
* @returns {number} 合同总数
*/
const calculateTotalCount = (data) => {
return data.firstCount + data.secondCount + data.thirdCount + data.fourthCount;
};
// 新增:标记合同图表数据是否加载完成
const isChartDataLoaded = ref(false);
// 修改 getContractChartData 方法:请求成功后标记加载完成
const getContractChartData = async () => {
try {
const [incomeChartRes, expensesChartRes] = await Promise.all([incomeAnalyze(), expensesAnalyze()]);
// 收入合同数据处理(原逻辑保留)
if (incomeChartRes.code === 200) {
const incomeRaw = incomeChartRes.data;
chartData.value.income = {
...incomeRaw,
total: calculateTotalCount(incomeRaw)
};
}
// 支出合同数据处理(原逻辑保留)
if (expensesChartRes.code === 200) {
const expensesRaw = expensesChartRes.data;
chartData.value.expenses = {
...expensesRaw,
total: calculateTotalCount(expensesRaw)
};
}
// ✅ 关键:所有数据请求完成后,标记为“已加载”
isChartDataLoaded.value = true;
} catch (error) {
console.error('获取合同图表数据失败:', error);
// 可选:失败时也标记加载完成,避免一直显示“加载中”(可替换为“加载失败”提示)
isChartDataLoaded.value = true;
}
};
const generatePieOption = (data) => {
// 1. 未加载:返回“加载中”配置
if (!isChartDataLoaded.value) {
return {
tooltip: { trigger: 'none' }, // 未加载时不需要tooltip
legend: { show: false }, // 隐藏图例(无数据可展示)
series: [
{
name: '合同数量',
type: 'pie',
2025-08-22 19:01:55 +08:00
radius: ['30%', '60%'],
center: ['50%', '80%'],
2025-08-22 18:25:54 +08:00
data: [], // 空数据,避免显示圆环
itemStyle: { borderColor: '#000', borderWidth: 1 },
// 中心显示“加载中”提示
label: {
show: true,
position: 'center',
formatter: '合同数据加载中...',
textStyle: { color: 'rgba(255, 255, 255, 0.9)', fontSize: 16 }
},
emphasis: { label: { show: false } },
labelLine: { show: false }
}
],
grid: { top: '15%', bottom: '5%', left: '5%', right: '5%' }
};
}
// 2. 已加载:返回真实数据配置(原逻辑保留,仅优化中心文本)
const pieData = [
{ name: '100万以下', value: data.firstCount },
{ name: '100-500万', value: data.secondCount },
{ name: '500-1000万', value: data.thirdCount },
{ name: '1000万以上', value: data.fourthCount }
];
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} 份'
},
legend: {
top: '5%',
left: 'center',
textStyle: { color: 'rgba(255, 255, 255, 0.8)' },
itemWidth: 12,
itemHeight: 12
},
series: [
{
name: '合同数量',
type: 'pie',
radius: ['70%', '80%'], // 恢复原半径原代码中70%/80%可能过窄,可根据需求调整)
center: ['50%', '60%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 8,
borderColor: '#000',
borderWidth: 1
},
// 中心文本显示真实总数此时data.total已可靠
label: {
show: true,
position: 'center',
formatter: `合同总数\n{total|${data.total} 份}`,
rich: {
total: {
fontSize: 20,
fontWeight: 'bold',
color: 'rgba(29, 214, 255, 1)',
marginTop: 8
}
},
textStyle: { color: 'rgba(255, 255, 255, 0.9)', fontSize: 14 }
},
emphasis: { label: { show: false } },
labelLine: { show: false },
data: pieData,
color: ['rgba(255, 77, 79, 1)', 'rgba(29, 214, 255, 1)', 'rgba(0, 227, 150, 1)', 'rgba(255, 147, 42, 1)']
}
],
grid: {
top: '15%',
bottom: '5%',
left: '5%',
right: '5%'
}
};
};
/**
* 切换图表类型收入/支出
* @param {string} type 目标类型income/expenses
*/
const switchChart = (type) => {
activeChart.value = type;
};
watch(
[activeChart, isChartDataLoaded], // 监听两个变量:图表类型 + 加载状态
([newType]) => {
pieOption.value = generatePieOption(chartData.value[newType]);
},
{ immediate: true }
);
// 组件挂载后初始化数据(并行请求,提升速度)
onMounted(async () => {
await Promise.all([getCapitalData(), getContractChartData()]);
2025-08-21 18:45:51 +08:00
});
2025-08-21 14:18:21 +08:00
</script>
2025-08-20 10:28:23 +08:00
2025-08-21 14:18:21 +08:00
<style lang="scss">
2025-08-20 10:28:23 +08:00
.leftPage {
width: 100%;
height: 100%;
2025-08-22 19:01:55 +08:00
2025-08-22 18:25:54 +08:00
.kpi_box {
2025-08-21 17:34:02 +08:00
margin-bottom: 10px;
}
2025-08-22 19:01:55 +08:00
2025-08-22 18:25:54 +08:00
.contract_box {
2025-08-22 19:01:55 +08:00
height: 33vh;
2025-08-22 18:25:54 +08:00
display: flex;
flex-direction: column; // 按钮区和图表区垂直排列
}
2025-08-22 19:01:55 +08:00
2025-08-22 18:25:54 +08:00
.chart-container {
2025-08-22 19:01:55 +08:00
height: 28vh;
2025-08-21 18:45:51 +08:00
}
2025-08-22 19:01:55 +08:00
2025-08-22 18:25:54 +08:00
.kpi_box,
.contract_box {
2025-08-21 14:18:21 +08:00
padding: 10px;
box-sizing: border-box;
2025-08-21 17:34:02 +08:00
border: 1px solid rgba(29, 214, 255, 0.3);
2025-08-21 14:18:21 +08:00
}
2025-08-20 10:28:23 +08:00
}
</style>