Files
GEAR-OA/gear-ui3/src/views/finance/dashboard/index.vue
2025-09-24 13:56:50 +08:00

854 lines
21 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="finance-analysis-page">
<el-row :gutter="20" class="content-container">
<!-- 1. 时间选择区 - 使用封装的组件 -->
<el-col :span="24" class="time-filter">
<TimeFilter @query="fetchData" @reset="handleFilterReset" @dateChange="handleDateChange" />
</el-col>
<!-- 2. 指标卡区域 -->
<el-col :span="24" class="indicator-cards">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="indicator-card total-income">
<div class="card-header">
<span>总收入</span>
<i class="el-icon-arrow-up"></i>
</div>
<div class="card-value">{{ totalIncome | formatCurrency }}</div>
<!-- <div class="card-desc">较上期 <span class="rise">+5.2%</span></div> -->
</el-card>
</el-col>
<el-col :span="6">
<el-card class="indicator-card total-expense">
<div class="card-header">
<span>总支出</span>
<i class="el-icon-arrow-down"></i>
</div>
<div class="card-value">{{ totalExpense | formatCurrency }}</div>
<!-- <div class="card-desc">较上期 <span class="drop">-2.8%</span></div> -->
</el-card>
</el-col>
<el-col :span="6">
<el-card class="indicator-card net-cashflow">
<div class="card-header">
<span>净收益</span>
<i class="Refresh"></i>
</div>
<div class="card-value">{{ netCashflow | formatCurrency }}</div>
<!-- <div class="card-desc">较上期 <span class="rise">+8.0%</span></div> -->
</el-card>
</el-col>
<el-col :span="6">
<el-card class="indicator-card outstanding-receivable">
<div class="card-header">
<span>未结清应收</span>
<i class="el-icon-info"></i>
</div>
<div class="card-value">{{ outstandingReceivable | formatCurrency }}</div>
<div class="card-desc"> {{ receivableCount }} 笔未结清</div>
</el-card>
</el-col>
</el-row>
</el-col>
<!-- 3. echarts图表区域 -->
<el-col :span="24" class="charts-container">
<!-- 收入支出趋势图 -->
<el-row :gutter="20" class="chart-row">
<el-col :span="12">
<el-card class="chart-card">
<div slot="header" class="chart-header">
<span>收入支出趋势</span>
</div>
<div class="chart-content">
<div ref="trendChart" class="chart-wrapper"></div>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="chart-card">
<div slot="header" class="chart-header">
<span>按供应商区分的支出</span>
</div>
<div class="chart-content">
<div ref="supplierChart" class="chart-wrapper"></div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 按订单和客户/供应商分析 -->
<el-row :gutter="20" class="chart-row">
<el-col :span="12">
<el-card class="chart-card">
<div slot="header" class="chart-header">
<span>按订单区分的收支</span>
</div>
<div class="chart-content">
<div ref="orderChart" class="chart-wrapper"></div>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="chart-card">
<div slot="header" class="chart-header">
<span>按客户区分的收入</span>
</div>
<div class="chart-content">
<div ref="customerChart" class="chart-wrapper"></div>
</div>
</el-card>
</el-col>
</el-row>
</el-col>
<!-- 4. 回款应收未收任务信息表格 -->
<el-col :span="24" class="table-container">
<el-card>
<div slot="header" class="table-header">
<span>回款应收未收任务信息</span>
</div>
<el-table :data="receivableTasks" border style="width: 100%" v-loading="tableLoading">
<el-table-column prop="receivableId" label="应收ID" width="180" align="center"></el-table-column>
<el-table-column prop="customerName" label="客户名称" width="150" align="center"></el-table-column>
<el-table-column prop="orderId" label="订单ID" width="180" align="center"></el-table-column>
<el-table-column prop="dueDate" label="到期日期" width="150" align="center"></el-table-column>
<el-table-column prop="amount" label="总金额" width="120" align="center"
:formatter="formatCurrency"></el-table-column>
<el-table-column prop="paidAmount" label="已付金额" width="120" align="center"
:formatter="formatCurrency"></el-table-column>
<el-table-column prop="balanceAmount" label="未付金额" width="120" align="center"
:formatter="formatCurrency"></el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center"></el-table-column>
<el-table-column prop="remark" label="备注" align="center"></el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="currentPage" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="receivableTotal"></el-pagination>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import * as echarts from 'echarts';
import { listReceivable } from '@/api/finance/receivable';
import { listPayable } from '@/api/finance/payable';
import TimeFilter from './components/TimeFilter.vue'; // 引入封装的时间筛选组件
// 分页相关
const currentPage = ref(1);
const pageSize = ref(10);
// 数据相关
const receivableData = ref([]); // 应收数据
const payableData = ref([]); // 应付数据
const receivableTasks = ref([]); // 回款任务数据
const receivableTotal = ref(0); // 回款任务总数
const tableLoading = ref(false);
const currentTimeParams = ref({}); // 当前时间参数
// 指标数据
const totalIncome = ref(0);
const totalExpense = ref(0);
const netCashflow = ref(0);
const outstandingReceivable = ref(0);
const receivableCount = ref(0);
// 图表实例
const trendChart = ref(null);
const orderChart = ref(null);
const customerChart = ref(null);
const supplierChart = ref(null);
const trendChartInstance = ref(null);
const orderChartInstance = ref(null);
const customerChartInstance = ref(null);
const supplierChartInstance = ref(null);
// 初始化
onMounted(() => {
// 初始化图表
initCharts();
});
// 初始化图表
const initCharts = () => {
if (trendChart.value) {
trendChartInstance.value = echarts.init(trendChart.value);
}
if (orderChart.value) {
orderChartInstance.value = echarts.init(orderChart.value);
}
if (customerChart.value) {
customerChartInstance.value = echarts.init(customerChart.value);
}
if (supplierChart.value) {
supplierChartInstance.value = echarts.init(supplierChart.value);
}
// 监听窗口大小变化,调整图表
window.addEventListener('resize', () => {
trendChartInstance.value?.resize();
orderChartInstance.value?.resize();
customerChartInstance.value?.resize();
supplierChartInstance.value?.resize();
});
};
// 获取数据
const fetchData = async (timeParams) => {
tableLoading.value = true;
currentTimeParams.value = timeParams || currentTimeParams.value;
try {
// 获取应收数据
const receivableRes = await listReceivable({ pageSize: 9999, pageNum: 1 });
receivableData.value = receivableRes.rows || [];
receivableTasks.value = receivableData.value.filter(item => item.status === '未结清');
receivableTotal.value = receivableRes.total || 0;
// 获取应付数据
const payableRes = await listPayable({ pageSize: 9999, pageNum: 1 });
payableData.value = payableRes.rows || [];
// 处理数据并更新指标
processData();
// 更新图表
updateCharts();
} catch (error) {
console.error('获取数据失败:', error);
ElMessage.error('获取数据失败,请重试');
} finally {
tableLoading.value = false;
}
};
// 处理数据计算指标
const processData = () => {
// 计算总收入(应收金额总和)
totalIncome.value = receivableData.value.reduce((sum, item) => {
return sum + parseFloat(item.amount || 0);
}, 0);
// 计算总支出(应付金额总和)
totalExpense.value = payableData.value.reduce((sum, item) => {
return sum + parseFloat(item.amount || 0);
}, 0);
// 计算净现金流
netCashflow.value = totalIncome.value - totalExpense.value;
// 计算未结清应收总额和数量
const outstanding = receivableData.value.filter(item => item.status === '未结清');
outstandingReceivable.value = outstanding.reduce((sum, item) => {
return sum + parseFloat(item.balanceAmount || 0);
}, 0);
receivableCount.value = outstanding.length;
};
// 更新图表
const updateCharts = () => {
updateTrendChart();
updateOrderChart();
updateCustomerChart();
updateSupplierChart();
};
// 更新收入支出趋势图
const updateTrendChart = () => {
// 按时间粒度处理数据
const timeGroups = groupDataByTime();
// 准备图表数据
const xAxisData = Object.keys(timeGroups);
const incomeData = xAxisData.map(key => timeGroups[key].income);
const expenseData = xAxisData.map(key => timeGroups[key].expense);
// 设置图表配置
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: '{b}<br/>收入: {c0} 元<br/>支出: {c1} 元'
},
legend: {
data: ['收入', '支出'],
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxisData,
axisLabel: {
rotate: 45,
interval: 0
}
},
yAxis: {
type: 'value',
name: '金额 (元)',
axisLabel: {
formatter: '{value}'
}
},
series: [
{
name: '收入',
type: 'bar',
data: incomeData,
itemStyle: {
color: '#4e79a7'
},
emphasis: {
focus: 'series'
}
},
{
name: '支出',
type: 'bar',
data: expenseData,
itemStyle: {
color: '#e15759'
},
emphasis: {
focus: 'series'
}
}
]
};
trendChartInstance.value.setOption(option);
};
// 更新按订单区分的收支图表
const updateOrderChart = () => {
// 按订单ID合并数据
const orderMap = {};
// 处理应收数据
receivableData.value.forEach(item => {
if (!orderMap[item.orderId]) {
orderMap[item.orderId] = {
income: 0,
expense: 0
};
}
orderMap[item.orderId].income += parseFloat(item.amount || 0);
});
// 处理应付数据
payableData.value.forEach(item => {
if (!orderMap[item.orderId]) {
orderMap[item.orderId] = {
income: 0,
expense: 0
};
}
orderMap[item.orderId].expense += parseFloat(item.amount || 0);
});
// 转换为图表数据
const orderList = Object.entries(orderMap)
.map(([orderId, data]) => ({
orderId,
income: data.income,
expense: data.expense,
net: data.income - data.expense
}))
.sort((a, b) => b.net - a.net)
.slice(0, 10); // 只展示前10个订单
const xAxisData = orderList.map(item => `订单 ${item.orderId.slice(-6)}`);
const incomeData = orderList.map(item => item.income);
const expenseData = orderList.map(item => item.expense);
// 设置图表配置
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['收入', '支出'],
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxisData,
axisLabel: {
rotate: 45,
interval: 0
}
},
yAxis: {
type: 'value',
name: '金额 (元)'
},
series: [
{
name: '收入',
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
},
data: incomeData,
itemStyle: {
color: '#4e79a7'
}
},
{
name: '支出',
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
},
data: expenseData.map(v => -v), // 支出用负值表示
itemStyle: {
color: '#e15759'
}
}
]
};
orderChartInstance.value.setOption(option);
};
// 更新按客户区分的收入图表
const updateCustomerChart = () => {
// 按客户分组计算收入
const customerMap = {};
receivableData.value.forEach(item => {
if (!customerMap[item.customerId]) {
customerMap[item.customerId] = {
name: item.customerName,
amount: 0
};
}
customerMap[item.customerId].amount += parseFloat(item.amount || 0);
});
// 转换为图表数据并排序
const customerList = Object.values(customerMap)
.sort((a, b) => b.amount - a.amount)
.slice(0, 10); // 只展示前10个客户
// 设置图表配置
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 10,
top: 'center'
},
series: [
{
name: '客户收入',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: customerList.map(item => ({
name: item.name,
value: item.amount
}))
}
]
};
customerChartInstance.value.setOption(option);
};
// 更新按供应商区分的支出图表
const updateSupplierChart = () => {
// 按供应商分组计算支出
const supplierMap = {};
payableData.value.forEach(item => {
if (!supplierMap[item.supplierId]) {
supplierMap[item.supplierId] = {
name: item.supplierName,
amount: 0
};
}
supplierMap[item.supplierId].amount += parseFloat(item.amount || 0);
});
// 转换为图表数据并排序
const supplierList = Object.values(supplierMap)
.sort((a, b) => b.amount - a.amount)
.slice(0, 10); // 只展示前10个供应商
// 设置图表配置
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
name: '金额 (元)'
},
yAxis: {
type: 'category',
data: supplierList.map(item => item.name),
axisLabel: {
interval: 0
}
},
series: [
{
name: '支出金额',
type: 'bar',
data: supplierList.map(item => item.amount),
itemStyle: {
color: '#e15759'
},
emphasis: {
focus: 'series'
}
}
]
};
supplierChartInstance.value.setOption(option);
};
// 按时间粒度分组数据
const groupDataByTime = () => {
const timeGroups = {};
const granularity = currentTimeParams.value.timeGranularity || 'month';
// 处理应收数据
receivableData.value.forEach(item => {
const date = new Date(item.dueDate);
const timeKey = getTimeKey(date, granularity);
if (!timeGroups[timeKey]) {
timeGroups[timeKey] = {
income: 0,
expense: 0
};
}
timeGroups[timeKey].income += parseFloat(item.amount || 0);
});
// 处理应付数据
payableData.value.forEach(item => {
const date = new Date(item.dueDate);
const timeKey = getTimeKey(date, granularity);
if (!timeGroups[timeKey]) {
timeGroups[timeKey] = {
income: 0,
expense: 0
};
}
timeGroups[timeKey].expense += parseFloat(item.amount || 0);
});
// 排序时间分组
return Object.keys(timeGroups).sort().reduce((obj, key) => {
obj[key] = timeGroups[key];
return obj;
}, {});
};
// 根据时间粒度生成时间键
const getTimeKey = (date, granularity) => {
const year = date.getFullYear();
const month = date.getMonth() + 1;
if (granularity === 'year') {
return `${year}`;
} else if (granularity === 'month') {
return `${year}-${month.toString().padStart(2, '0')}`;
} else {
// 按周分组 (ISO周数)
const week = getWeekNumber(date);
return `${year}-W${week.toString().padStart(2, '0')}`;
}
};
// 获取ISO周数
const getWeekNumber = (date) => {
const temp = new Date(date.getTime());
temp.setHours(0, 0, 0, 0);
// 把周一作为一周的第一天
temp.setDate(temp.getDate() + 3 - (temp.getDay() + 6) % 7);
const firstDay = new Date(temp.getFullYear(), 0, 4);
return Math.round((temp - firstDay) / (7 * 24 * 60 * 60 * 1000)) + 1;
};
// 格式化金额
const formatCurrency = (row, column, value) => {
if (!value) return '0.00';
return parseFloat(value).toFixed(2);
};
// 事件处理函数
const handleDateChange = (timeParams) => {
currentTimeParams.value = timeParams;
};
const handleFilterReset = () => {
currentPage.value = 1;
};
</script>
<style lang="scss" scoped>
.finance-analysis-page {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.page-header {
margin-bottom: 10px;
h1 {
font-size: 24px;
color: #1f2d3d;
margin-bottom: 5px;
}
p {
font-size: 14px;
color: #8392a5;
}
}
.content-container {
.time-filter {
margin-bottom: 10px;
}
.indicator-cards {
margin-bottom: 10px;
.indicator-card {
height: 100%;
padding: 5px;
position: relative;
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
color: #8392a5;
font-size: 14px;
i {
font-size: 12px;
}
}
.card-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 5px;
color: #1f2d3d;
}
.card-desc {
font-size: 12px;
color: #8392a5;
.rise {
color: #f56c6c;
}
.drop {
color: #409eff;
}
}
&::after {
content: '';
position: absolute;
right: -30px;
top: -30px;
width: 80px;
height: 80px;
border-radius: 50%;
opacity: 0.1;
}
}
.total-income {
&::after {
background-color: #4e79a7;
}
}
.total-expense {
&::after {
background-color: #e15759;
}
}
.net-cashflow {
&::after {
background-color: #59a14f;
}
}
.outstanding-receivable {
&::after {
background-color: #9c755f;
}
}
}
.charts-container {
margin-bottom: 10px;
.chart-row {
margin-bottom: 10px;
}
.chart-card {
height: 100%;
.chart-header {
font-size: 16px;
font-weight: 500;
color: #1f2d3d;
padding: 5px 10px;
border-bottom: 1px solid #eee;
}
.chart-content {
padding: 10px;
}
.chart-wrapper {
width: 100%;
height: 400px;
}
}
}
.table-container {
.table-header {
font-size: 16px;
font-weight: 500;
color: #1f2d3d;
padding: 5px 10px;
border-bottom: 1px solid #eee;
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
}
}
// 响应式调整
@media (max-width: 1200px) {
.charts-container {
.chart-wrapper {
height: 350px;
}
}
}
@media (max-width: 992px) {
.indicator-cards {
.el-col {
&:nth-child(1),
&:nth-child(2) {
margin-bottom: 20px;
}
}
}
.charts-container {
.chart-row {
.el-col {
&:nth-child(1) {
margin-bottom: 20px;
}
}
}
.chart-wrapper {
height: 300px;
}
}
}
@media (max-width: 768px) {
.indicator-cards {
.el-col {
&:nth-child(3) {
margin-bottom: 20px;
}
}
}
.charts-container {
.chart-wrapper {
height: 250px;
}
}
}
</style>