328 lines
10 KiB
Vue
328 lines
10 KiB
Vue
<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>
|