<template>
|
<div style="padding: 20px;">
|
<!-- 页面标题和月份筛选 -->
|
<div class="w-full md:w-auto flex items-center gap-3" style="margin-bottom: 20px;">
|
<el-date-picker
|
v-model="dateRange"
|
type="monthrange"
|
format="YYYY-MM"
|
value-format="YYYY-MM"
|
range-separator="至"
|
start-placeholder="开始月份"
|
end-placeholder="结束月份"
|
:disabled-date="disabledDate"
|
@change="handleDateChange"
|
class="w-full md:w-auto"
|
style="margin-right: 30px;"
|
/>
|
|
<el-button
|
type="primary"
|
icon="Refresh"
|
@click="resetDateRange"
|
size="default"
|
>
|
重置
|
</el-button>
|
</div>
|
|
<main class="container mx-auto px-4 pb-10">
|
<!-- 财务指标卡片 -->
|
<div class="grid-container">
|
<!-- 总收入 -->
|
<el-card class="bg1">
|
<p>总收入</p>
|
<h3>
|
¥{{ pageInfo.totalIncome }}
|
</h3>
|
</el-card>
|
|
<!-- 收入笔数 -->
|
<el-card class="bg2">
|
<p>收入笔数</p>
|
<h3>
|
{{ pageInfo.incomeNumber }}
|
</h3>
|
</el-card>
|
|
<!-- 总支出 -->
|
<el-card class="bg3">
|
<p>总支出</p>
|
<h3>
|
¥{{ pageInfo.totalExpense }}
|
</h3>
|
</el-card>
|
|
<!-- 支出笔数 -->
|
<el-card class="bg4">
|
<p>支出笔数</p>
|
<h3>
|
{{ pageInfo.expenseNumber }}
|
</h3>
|
</el-card>
|
|
<!-- 净收入 -->
|
<el-card class="bg5">
|
<p>净收入</p>
|
<h3>
|
¥{{ pageInfo.netRevenue }}
|
</h3>
|
</el-card>
|
</div>
|
|
<!-- 收入统计图表 -->
|
<div class="grid-layout">
|
<el-card style="margin-bottom: 20px;">
|
<h2 class="section-title">收入统计(元)</h2>
|
<div class="echarts">
|
<Echarts :legend="pieLegend0" :chartStyle="chartStylePie"
|
:series="materialPieSeries0"
|
:tooltip="pieTooltip" style="height: 260px;width: 35%;">
|
<div class="chart-num">
|
<span style="font-size: 22px;">收入</span>
|
<span style="font-size: 36px;
|
font-weight: 500;
|
font-family: 'MyCustomFont', sans-serif;">{{ pageInfo.totalIncome }}</span>
|
</div>
|
</Echarts>
|
<Echarts ref="chart"
|
:chartStyle="chartStyle"
|
:grid="grid"
|
:legend="lineLegend"
|
:series="lineSeries0"
|
:tooltip="tooltip"
|
:xAxis="xAxis0"
|
:yAxis="yAxis0"
|
style="height: 260px;width: 64%;"></Echarts>
|
</div>
|
</el-card>
|
|
<!-- 支出统计图表 -->
|
<el-card>
|
<h2 class="section-title">支出统计(元)</h2>
|
<div class="echarts">
|
<Echarts ref="chart"
|
:legend="pieLegend1"
|
:chartStyle="chartStylePie"
|
:series="materialPieSeries1"
|
:tooltip="pieTooltip"
|
style="height: 260px;width: 35%;">
|
<div class="chart-num">
|
<span style="font-size: 22px;">支出</span>
|
<span style="font-size: 36px;
|
font-weight: 500;
|
font-family: 'MyCustomFont', sans-serif;">{{ pageInfo.totalExpense }}</span>
|
</div></Echarts>
|
<Echarts ref="chart"
|
:chartStyle="chartStyle"
|
:grid="grid"
|
:legend="lineLegend"
|
:series="lineSeries1"
|
:tooltip="tooltip"
|
:xAxis="xAxis1"
|
:yAxis="yAxis1"
|
style="height: 260px;width: 64%;"></Echarts>
|
</div>
|
</el-card>
|
</div>
|
</main>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, computed, onMounted, reactive, nextTick, getCurrentInstance } from 'vue';
|
import 'element-plus/dist/index.css';
|
import Echarts from "@/components/Echarts/echarts.vue";
|
import { reportForms,reportIncome,reportExpense } from "@/api/financialManagement/financialStatements";
|
import dayjs from "dayjs";
|
|
// 日期范围
|
const dateRange = ref(null);
|
const { proxy } = getCurrentInstance();
|
const chartStyle = {
|
width: '100%',
|
height: '100%', // 设置图表容器的高度
|
position:'relative',
|
}
|
const grid = {
|
left: '3%',
|
right: '4%',
|
bottom: '3%',
|
containLabel: true
|
}
|
const lineLegend = {
|
show: false,
|
}
|
// 折线图提示框
|
const tooltip = reactive({
|
trigger: 'axis',
|
axisPointer: {
|
type: 'line',
|
lineStyle: { color: '#aaa' }
|
},
|
// 自定义内容
|
formatter: function (params) {
|
if (!params || !params.length) return ''
|
const axisLabel = params[0].axisValueLabel || params[0].axisValue || ''
|
const rows = params
|
.map(p => {
|
const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>`
|
return `${colorDot}${p.seriesName}: ${p.value}`
|
})
|
.join('<br/>')
|
return `<div>${axisLabel}</div><div>${rows}</div>`
|
}
|
})
|
const lineSeries0 = ref([])
|
const lineSeries1 = ref([])
|
|
// 根据月份范围生成 x 轴数据
|
const generateMonthLabels = (startMonth, endMonth) => {
|
const labels = [];
|
let current = dayjs(startMonth);
|
const end = dayjs(endMonth);
|
|
while (current.isBefore(end) || current.isSame(end, 'month')) {
|
labels.push(`${current.month() + 1}月`);
|
current = current.add(1, 'month');
|
}
|
|
return labels;
|
};
|
|
const xAxis0 = ref([
|
{
|
type: 'category',
|
axisTick: { show: true, alignWithLabel: true },
|
data: [],
|
},
|
]);
|
const xAxis1 = ref([
|
{
|
type: 'category',
|
axisTick: { show: true, alignWithLabel: true },
|
data: [],
|
},
|
]);
|
const yAxis0 = [
|
{
|
type: 'value',
|
name: '收入统计', // 左侧y轴
|
position: 'left',
|
min: 0,
|
// 坐标轴名称样式
|
nameTextStyle: {
|
color: '#000',
|
fontSize: 14,
|
},
|
}
|
]
|
|
const yAxis1 = [
|
{
|
type: 'value',
|
name: '支出统计', // 左侧y轴
|
position: 'left',
|
min: 0,
|
// 坐标轴名称样式
|
nameTextStyle: {
|
color: '#000',
|
fontSize: 14,
|
},
|
}
|
]
|
|
const chartStylePie = {
|
width: '100%',
|
height: '100%' // 设置图表容器的高度
|
}
|
const pieColors = ['#F04864','#FACC14', '#8543E0', '#1890FF', '#13C2C2','#2FC25B']; // 可根据实际调整
|
const pieData0 = ref([]);
|
const pieData1 = ref([]);
|
|
const pieLegend0 = computed(() => ({
|
show: true,
|
top: 'center',
|
left: '60%',
|
orient: 'vertical',
|
icon: 'circle',
|
data: (pieData0.value || []).filter(item => item && item.name).map(item => item.name),
|
formatter: function(name) {
|
if (!name) return '';
|
const item = pieData0.value.find(i => i && i.name === name);
|
if (!item) return name;
|
return `${name} | ${item.percent} ${item.amount}`;
|
},
|
textStyle: {
|
color: '#333',
|
fontSize: 14,
|
lineHeight: 26,
|
}
|
}));
|
const pieLegend1 = computed(() => ({
|
show: true,
|
top: 'center',
|
left: '60%',
|
orient: 'vertical',
|
icon: 'circle',
|
data: (pieData1.value || []).filter(item => item && item.name).map(item => item.name),
|
formatter: function(name) {
|
if (!name) return '';
|
const item = pieData1.value.find(i => i && i.name === name);
|
if (!item) return name;
|
return `${name} | ${item.percent} ${item.amount}`;
|
},
|
textStyle: {
|
color: '#333',
|
fontSize: 14,
|
lineHeight: 26,
|
}
|
}));
|
|
const materialPieSeries0 = computed(() => [
|
{
|
type: 'pie',
|
radius: ['50%', '65%'],
|
center: ['25%', '50%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
show: false
|
},
|
data: (pieData0.value || []).filter(item => item && item.name),
|
color: pieColors
|
}
|
]);
|
const materialPieSeries1 = computed(() => [
|
{
|
type: 'pie',
|
radius: ['50%', '65%'],
|
center: ['25%', '50%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderColor: '#fff',
|
borderWidth: 2
|
},
|
label: {
|
show: false
|
},
|
data: (pieData1.value || []).filter(item => item && item.name),
|
color: pieColors
|
}
|
]);
|
const pieTooltip = reactive({
|
trigger: 'item',
|
formatter: function(params) {
|
// 检查数据是否存在
|
if (!params.data) return params.name;
|
// 拼接完整内容
|
return `
|
<div>
|
<div style="color:${params.color};font-size:16px;">●</div>
|
<div>${params.name}</div>
|
<div>占比:${params.data.percent}</div>
|
<div>金额:${params.data.amount}</div>
|
</div>
|
`;
|
}
|
})
|
|
|
const pageInfo = ref({
|
})
|
|
// 获取最近六个月的范围
|
const getLastSixMonths = () => {
|
const endMonth = dayjs().format('YYYY-MM');
|
const startMonth = dayjs().subtract(5, 'month').format('YYYY-MM');
|
return [startMonth, endMonth];
|
};
|
|
const getData = async () => {
|
if (!dateRange.value || !Array.isArray(dateRange.value) || dateRange.value.length !== 2) {
|
return;
|
}
|
const startDateStr = dateRange.value[0];
|
const endDateStr = dateRange.value[1];
|
if (!startDateStr || !endDateStr) {
|
return;
|
}
|
|
// 验证日期格式并转换为完整日期
|
const startDate = dayjs(startDateStr);
|
const endDate = dayjs(endDateStr);
|
if (!startDate.isValid() || !endDate.isValid()) {
|
console.error('无效的日期格式');
|
return;
|
}
|
|
// 更新 x 轴数据
|
const monthLabels = generateMonthLabels(startDateStr, endDateStr);
|
xAxis0.value[0].data = monthLabels;
|
xAxis1.value[0].data = monthLabels;
|
|
// 开始月份拼接第一天,结束月份拼接最后一天
|
const entryDateStart = startDate.startOf('month').format('YYYY-MM-DD');
|
const entryDateEnd = endDate.endOf('month').format('YYYY-MM-DD');
|
|
try {
|
const {code,data} = await reportForms({entryDateStart, entryDateEnd});
|
if(code === 200 && data) {
|
pageInfo.value = data || {};
|
// 安全处理数据,过滤掉 null 或 undefined
|
pieData0.value = (data.incomeType || []).filter(item => item && item.typeName).map(item=>({
|
name:item.typeName || '',
|
value:item.account || 0,
|
percent:`${((item.proportion || 0) * 100).toFixed(2)}%`,
|
amount:`¥${(item.account || 0).toFixed(2)}`
|
}))
|
pieData1.value = (data.expenseType || []).filter(item => item && item.typeName).map(item=>({
|
name:item.typeName || '',
|
value:item.account || 0,
|
percent:`${((item.proportion || 0) * 100).toFixed(2)}%`,
|
amount:`¥${(item.account || 0).toFixed(2)}`
|
}))
|
}
|
} catch (error) {
|
console.error('获取财务指标数据失败:', error);
|
}
|
try{
|
const {code,data} = await reportIncome({entryDateStart, entryDateEnd});
|
if(code==200 && data && Array.isArray(data)){
|
lineSeries0.value = data.filter(item => item && item.typeName).map(item=>({
|
name:item.typeName || '',
|
type: 'line',
|
data:(item.account || []).map(val => Number(val) || 0)
|
}))
|
}
|
}catch (error) {
|
console.error('获取财务指标数据失败:', error);
|
}
|
try{
|
const {code,data} = await reportExpense({entryDateStart, entryDateEnd});
|
if(code==200 && data && Array.isArray(data)){
|
lineSeries1.value = data.filter(item => item && item.typeName).map(item=>({
|
name:item.typeName || '',
|
type: 'line',
|
data:(item.account || []).map(val => Number(val) || 0)
|
}))
|
}
|
}catch (error) {
|
console.error('获取财务指标数据失败:', error);
|
}
|
};
|
|
|
// 初始化
|
onMounted(() => {
|
// 设置默认值为最近六个月
|
const defaultRange = getLastSixMonths();
|
dateRange.value = defaultRange;
|
// 使用 nextTick 确保组件完全渲染后再调用
|
nextTick(() => {
|
getData();
|
});
|
});
|
|
// 限制月份选择范围(最多12个月)
|
const disabledDate = (time) => {
|
// 如果没有选择开始月份,不禁用任何日期
|
if (!dateRange.value || !Array.isArray(dateRange.value) || !dateRange.value[0]) {
|
return false;
|
}
|
|
const startMonth = dayjs(dateRange.value[0]);
|
const currentMonth = dayjs(time);
|
|
// 如果当前月份在开始月份之前,禁用
|
if (currentMonth.isBefore(startMonth, 'month')) {
|
return true;
|
}
|
|
// 计算最大允许的月份(开始月份 + 11个月 = 12个月)
|
const maxMonth = startMonth.add(11, 'month');
|
|
// 禁用超过12个月的月份
|
return currentMonth.isAfter(maxMonth, 'month');
|
};
|
|
// 处理月份范围变化
|
const handleDateChange = (newRange) => {
|
if (!newRange || !Array.isArray(newRange) || newRange.length !== 2) {
|
return;
|
}
|
|
// 验证月份范围不超过12个月
|
const startDate = dayjs(newRange[0]);
|
const endDate = dayjs(newRange[1]);
|
const monthDiff = endDate.diff(startDate, 'month');
|
|
if (monthDiff > 11) {
|
proxy.$modal.msgWarning('最多只能选择12个月份');
|
// 自动调整为12个月
|
const adjustedEnd = startDate.add(11, 'month').format('YYYY-MM');
|
dateRange.value = [newRange[0], adjustedEnd];
|
getData();
|
return;
|
}
|
|
dateRange.value = newRange;
|
getData();
|
};
|
|
// 重置月份范围
|
const resetDateRange = () => {
|
// 重置为最近六个月
|
dateRange.value = getLastSixMonths();
|
getData();
|
};
|
|
</script>
|
|
<style scoped lang="scss">
|
/* 基础样式补充 */
|
:root {
|
--el-color-primary: #4f46e5;
|
}
|
.el-card{
|
position: relative;
|
border-radius: 12px;
|
padding: 14px 10px 10px 10px;
|
box-shadow: 0 2px 8px #eee;
|
:deep(.el-card__body){
|
padding: 10px 20px !important;
|
}
|
&.bg1{
|
background: url(@/assets/icons/png/1.png) no-repeat 100% 100% !important;
|
}
|
&.bg2{
|
background: url(@/assets/icons/png/2.png) no-repeat 100% 100% !important;
|
}
|
&.bg3{
|
background: url(@/assets/icons/png/3.png) no-repeat 100% 100% !important;
|
}
|
&.bg4{
|
background: url(@/assets/icons/png/4.png) no-repeat 100% 100% !important;
|
}
|
&.bg5{
|
background: url(@/assets/icons/png/5.png) no-repeat 100% 100% !important;
|
}
|
}
|
|
.grid-container {
|
/* grid 容器基础样式 */
|
display: grid;
|
gap: 1rem; /* gap-4 对应 1rem (16px) */
|
margin-bottom: 2rem; /* mb-8 对应 2rem (32px) */
|
|
p{
|
font-size: 22px;
|
margin-top: 0px;
|
color: #fff;
|
}
|
h3{
|
font-size: 36px;
|
font-weight: 500;
|
font-family: 'MyCustomFont', sans-serif;
|
margin: 10px 0;
|
color: #fff;
|
}
|
|
}
|
|
/* 移动端默认样式 (grid-cols-1) */
|
.grid-container {
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
}
|
|
/* 小屏幕及以上 (sm:grid-cols-2) */
|
@media (min-width: 640px) {
|
.grid-container {
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
}
|
}
|
|
/* 大屏幕及以上 (lg:grid-cols-5) */
|
@media (min-width: 1024px) {
|
.grid-container {
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
}
|
}
|
|
/* 卡片悬停效果增强 */
|
.el-card:hover {
|
transform: translateY(-2px);
|
}
|
.echarts{
|
display: flex;
|
justify-content: space-between;
|
}
|
|
/* 图表容器样式 */
|
.el-chart {
|
width: 100%;
|
height: 100%;
|
}
|
.section-title {
|
position: relative;
|
font-size: 18px;
|
color: #333;
|
padding-left: 10px;
|
margin-bottom: 10px;
|
font-weight: 700;
|
}
|
|
.section-title::before {
|
position: absolute;
|
left: 0;
|
top: 0px;
|
content: '';
|
width: 4px;
|
height: 18px;
|
background-color: #002FA7;
|
border-radius: 2px;
|
}
|
.chart-num{
|
position: absolute;
|
z-index: 3;
|
top: 92px;
|
left: 92px;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
}
|
</style>
|