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>
|