Files
td_official/src/views/largeScreen/components/leftPage.vue
2025-08-22 19:01:55 +08:00

328 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="leftPage">
<div class="kpi_box">
<TitleComponent :title="'支付KPI'" style="margin-bottom: 20px" />
<ProgressComponent title="应收账款" :value="formatCurrency(incomeData.planAmount)"
:percentageChange="getPercentageChange(incomeData.planAmount, incomeData.actualAmount)"
:progressPercentage="getProgressPercentage(incomeData.planAmount, incomeData.actualAmount)"
progressColor="rgba(255, 77, 79, 1)" />
<ProgressComponent title="应付账款" :value="formatCurrency(expensesData.planAmount)"
:percentageChange="getPercentageChange(expensesData.planAmount, expensesData.actualAmount)"
:progressPercentage="getProgressPercentage(expensesData.planAmount, expensesData.actualAmount)"
progressColor="rgba(29, 214, 255, 1)" />
<ProgressComponent title="本月付款" :value="formatCurrency(expensesData.actualAmount)"
:percentageChange="getPercentageChange(expensesData.planAmount, expensesData.actualAmount)"
:progressPercentage="getProgressPercentage(expensesData.planAmount, expensesData.actualAmount)"
progressColor="rgba(0, 227, 150, 1)" />
<ProgressComponent title="本月收款" :value="formatCurrency(incomeData.actualAmount)"
:percentageChange="getPercentageChange(incomeData.planAmount, incomeData.actualAmount)"
:progressPercentage="getProgressPercentage(incomeData.planAmount, incomeData.actualAmount)"
progressColor="rgba(255, 147, 42, 1)" />
</div>
<div class="contract_box">
<div style="height: 60px;">
<TitleComponent :title="'收支合同分析'" style="margin-bottom: 20px" />
</div>
<!-- 切换按钮 -->
<div style="margin-bottom: 10px; text-align: center">
<button @click="switchChart('income')"
:style="activeChart === 'income' ? activeBtnStyle : defaultBtnStyle">收入合同</button>
<button @click="switchChart('expenses')" :style="activeChart === 'expenses' ? activeBtnStyle : defaultBtnStyle"
style="margin-left: 15px">
支出合同
</button>
</div>
<!-- 环形图容器固定高度确保图表不拉伸添加flex居中 -->
<div class="chart-container">
<EchartBox :option="pieOption" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import TitleComponent from './TitleComponent.vue';
import ProgressComponent from './ProgressComponent.vue';
import EchartBox from '@/components/EchartBox/index.vue';
import { incomePay, expensesPay, incomeAnalyze, expensesAnalyze } from '@/api/largeScreen/index';
// 初始化数据容器
const incomeData = ref({ planAmount: '0.00', actualAmount: '0.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);
}
};
/**
* 计算单类合同总数(复用方法)
* @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',
radius: ['30%', '60%'],
center: ['50%', '80%'],
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()]);
});
</script>
<style lang="scss">
.leftPage {
width: 100%;
height: 100%;
.kpi_box {
margin-bottom: 10px;
}
.contract_box {
height: 33vh;
display: flex;
flex-direction: column; // 按钮区和图表区垂直排列
}
.chart-container {
height: 28vh;
}
.kpi_box,
.contract_box {
padding: 10px;
box-sizing: border-box;
border: 1px solid rgba(29, 214, 255, 0.3);
}
}
</style>