2025-09-22 14:13:48 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="finance-analysis-page">
|
|
|
|
|
|
<el-row :gutter="20" class="content-container">
|
|
|
|
|
|
<!-- 1. 时间选择区 - 使用封装的组件 -->
|
|
|
|
|
|
<el-col :span="24" class="time-filter">
|
2025-09-22 14:44:36 +08:00
|
|
|
|
<TimeFilter @query="fetchData" @reset="handleFilterReset" @dateChange="handleDateChange" />
|
2025-09-22 14:13:48 +08:00
|
|
|
|
</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">
|
2025-09-22 14:44:36 +08:00
|
|
|
|
<span>净收益</span>
|
2025-09-24 13:56:50 +08:00
|
|
|
|
<i class="Refresh"></i>
|
2025-09-22 14:13:48 +08:00
|
|
|
|
</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">
|
2025-09-22 14:44:36 +08:00
|
|
|
|
<el-col :span="12">
|
2025-09-22 14:13:48 +08:00
|
|
|
|
<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>
|
2025-09-22 14:44:36 +08:00
|
|
|
|
<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>
|
2025-09-22 14:13:48 +08:00
|
|
|
|
</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>
|
2025-09-22 14:44:36 +08:00
|
|
|
|
<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>
|
2025-09-22 14:13:48 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pagination-container">
|
2025-09-22 14:44:36 +08:00
|
|
|
|
<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>
|
2025-09-22 14:13:48 +08:00
|
|
|
|
</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);
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 监听窗口大小变化,调整图表
|
|
|
|
|
|
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;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
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;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 获取应付数据
|
|
|
|
|
|
const payableRes = await listPayable({ pageSize: 9999, pageNum: 1 });
|
|
|
|
|
|
payableData.value = payableRes.rows || [];
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 处理数据并更新指标
|
|
|
|
|
|
processData();
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 更新图表
|
|
|
|
|
|
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);
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 计算总支出(应付金额总和)
|
|
|
|
|
|
totalExpense.value = payableData.value.reduce((sum, item) => {
|
|
|
|
|
|
return sum + parseFloat(item.amount || 0);
|
|
|
|
|
|
}, 0);
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 计算净现金流
|
|
|
|
|
|
netCashflow.value = totalIncome.value - totalExpense.value;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 计算未结清应收总额和数量
|
|
|
|
|
|
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();
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 准备图表数据
|
|
|
|
|
|
const xAxisData = Object.keys(timeGroups);
|
|
|
|
|
|
const incomeData = xAxisData.map(key => timeGroups[key].income);
|
|
|
|
|
|
const expenseData = xAxisData.map(key => timeGroups[key].expense);
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 设置图表配置
|
|
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
trendChartInstance.value.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新按订单区分的收支图表
|
|
|
|
|
|
const updateOrderChart = () => {
|
|
|
|
|
|
// 按订单ID合并数据
|
|
|
|
|
|
const orderMap = {};
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 处理应收数据
|
|
|
|
|
|
receivableData.value.forEach(item => {
|
|
|
|
|
|
if (!orderMap[item.orderId]) {
|
|
|
|
|
|
orderMap[item.orderId] = {
|
|
|
|
|
|
income: 0,
|
|
|
|
|
|
expense: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
orderMap[item.orderId].income += parseFloat(item.amount || 0);
|
|
|
|
|
|
});
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 处理应付数据
|
|
|
|
|
|
payableData.value.forEach(item => {
|
|
|
|
|
|
if (!orderMap[item.orderId]) {
|
|
|
|
|
|
orderMap[item.orderId] = {
|
|
|
|
|
|
income: 0,
|
|
|
|
|
|
expense: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
orderMap[item.orderId].expense += parseFloat(item.amount || 0);
|
|
|
|
|
|
});
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 转换为图表数据
|
|
|
|
|
|
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个订单
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
const xAxisData = orderList.map(item => `订单 ${item.orderId.slice(-6)}`);
|
|
|
|
|
|
const incomeData = orderList.map(item => item.income);
|
|
|
|
|
|
const expenseData = orderList.map(item => item.expense);
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 设置图表配置
|
|
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
orderChartInstance.value.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新按客户区分的收入图表
|
|
|
|
|
|
const updateCustomerChart = () => {
|
|
|
|
|
|
// 按客户分组计算收入
|
|
|
|
|
|
const customerMap = {};
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
receivableData.value.forEach(item => {
|
|
|
|
|
|
if (!customerMap[item.customerId]) {
|
|
|
|
|
|
customerMap[item.customerId] = {
|
|
|
|
|
|
name: item.customerName,
|
|
|
|
|
|
amount: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
customerMap[item.customerId].amount += parseFloat(item.amount || 0);
|
|
|
|
|
|
});
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 转换为图表数据并排序
|
|
|
|
|
|
const customerList = Object.values(customerMap)
|
|
|
|
|
|
.sort((a, b) => b.amount - a.amount)
|
|
|
|
|
|
.slice(0, 10); // 只展示前10个客户
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 设置图表配置
|
|
|
|
|
|
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
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
customerChartInstance.value.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新按供应商区分的支出图表
|
|
|
|
|
|
const updateSupplierChart = () => {
|
|
|
|
|
|
// 按供应商分组计算支出
|
|
|
|
|
|
const supplierMap = {};
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
payableData.value.forEach(item => {
|
|
|
|
|
|
if (!supplierMap[item.supplierId]) {
|
|
|
|
|
|
supplierMap[item.supplierId] = {
|
|
|
|
|
|
name: item.supplierName,
|
|
|
|
|
|
amount: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
supplierMap[item.supplierId].amount += parseFloat(item.amount || 0);
|
|
|
|
|
|
});
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 转换为图表数据并排序
|
|
|
|
|
|
const supplierList = Object.values(supplierMap)
|
|
|
|
|
|
.sort((a, b) => b.amount - a.amount)
|
|
|
|
|
|
.slice(0, 10); // 只展示前10个供应商
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 设置图表配置
|
|
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
supplierChartInstance.value.setOption(option);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 按时间粒度分组数据
|
|
|
|
|
|
const groupDataByTime = () => {
|
|
|
|
|
|
const timeGroups = {};
|
|
|
|
|
|
const granularity = currentTimeParams.value.timeGranularity || 'month';
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 处理应收数据
|
|
|
|
|
|
receivableData.value.forEach(item => {
|
|
|
|
|
|
const date = new Date(item.dueDate);
|
|
|
|
|
|
const timeKey = getTimeKey(date, granularity);
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
if (!timeGroups[timeKey]) {
|
|
|
|
|
|
timeGroups[timeKey] = {
|
|
|
|
|
|
income: 0,
|
|
|
|
|
|
expense: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
timeGroups[timeKey].income += parseFloat(item.amount || 0);
|
|
|
|
|
|
});
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 处理应付数据
|
|
|
|
|
|
payableData.value.forEach(item => {
|
|
|
|
|
|
const date = new Date(item.dueDate);
|
|
|
|
|
|
const timeKey = getTimeKey(date, granularity);
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
if (!timeGroups[timeKey]) {
|
|
|
|
|
|
timeGroups[timeKey] = {
|
|
|
|
|
|
income: 0,
|
|
|
|
|
|
expense: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
timeGroups[timeKey].expense += parseFloat(item.amount || 0);
|
|
|
|
|
|
});
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
// 排序时间分组
|
|
|
|
|
|
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;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
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 {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
h1 {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
color: #1f2d3d;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
p {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #8392a5;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-container {
|
|
|
|
|
|
.time-filter {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
margin-bottom: 10px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.indicator-cards {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.indicator-card {
|
|
|
|
|
|
height: 100%;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
padding: 5px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
margin-bottom: 5px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
color: #8392a5;
|
|
|
|
|
|
font-size: 14px;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
i {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
font-size: 12px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.card-value {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
|
color: #1f2d3d;
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.card-desc {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #8392a5;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.rise {
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.drop {
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
&::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: -30px;
|
|
|
|
|
|
top: -30px;
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
height: 80px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
opacity: 0.1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.total-income {
|
|
|
|
|
|
&::after {
|
|
|
|
|
|
background-color: #4e79a7;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.total-expense {
|
|
|
|
|
|
&::after {
|
|
|
|
|
|
background-color: #e15759;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.net-cashflow {
|
|
|
|
|
|
&::after {
|
|
|
|
|
|
background-color: #59a14f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.outstanding-receivable {
|
|
|
|
|
|
&::after {
|
|
|
|
|
|
background-color: #9c755f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.charts-container {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.chart-row {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
margin-bottom: 10px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.chart-card {
|
|
|
|
|
|
height: 100%;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.chart-header {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #1f2d3d;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
padding: 5px 10px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.chart-content {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
padding: 10px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.chart-wrapper {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.table-container {
|
|
|
|
|
|
.table-header {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #1f2d3d;
|
2025-09-22 14:44:36 +08:00
|
|
|
|
padding: 5px 10px;
|
2025-09-22 14:13:48 +08:00
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.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 {
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
&:nth-child(1),
|
|
|
|
|
|
&:nth-child(2) {
|
2025-09-22 14:13:48 +08:00
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.charts-container {
|
|
|
|
|
|
.chart-row {
|
|
|
|
|
|
.el-col {
|
|
|
|
|
|
&:nth-child(1) {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.chart-wrapper {
|
|
|
|
|
|
height: 300px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.indicator-cards {
|
|
|
|
|
|
.el-col {
|
|
|
|
|
|
&:nth-child(3) {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 14:44:36 +08:00
|
|
|
|
|
2025-09-22 14:13:48 +08:00
|
|
|
|
.charts-container {
|
|
|
|
|
|
.chart-wrapper {
|
|
|
|
|
|
height: 250px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|