442 lines
13 KiB
Vue
442 lines
13 KiB
Vue
<template>
|
|
<div>
|
|
<AppHeader />
|
|
<div class="finance-dashboard">
|
|
<!-- 总体统计 -->
|
|
<el-card class="summary-card">
|
|
<template #header>
|
|
<span>财务概览</span>
|
|
</template>
|
|
<el-row :gutter="20">
|
|
<el-col :span="6">
|
|
<el-statistic title="总收入" :value="totalIncome" :precision="2" suffix="元">
|
|
<template #prefix>
|
|
<el-icon style="vertical-align: middle;"><TrendCharts /></el-icon>
|
|
</template>
|
|
</el-statistic>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-statistic title="总支出" :value="totalExpense" :precision="2" suffix="元">
|
|
<template #prefix>
|
|
<el-icon style="vertical-align: middle;"><Money /></el-icon>
|
|
</template>
|
|
</el-statistic>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-statistic title="总剩余资金" :value="netProfit" :precision="2" suffix="元"
|
|
:value-style="netProfit >= 0 ? 'color: #67c23a' : 'color: #f56c6c'">
|
|
<template #prefix>
|
|
<el-icon style="vertical-align: middle;"><Wallet /></el-icon>
|
|
</template>
|
|
</el-statistic>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-statistic title="记录总数" :value="totalRecords">
|
|
<template #prefix>
|
|
<el-icon style="vertical-align: middle;"><Document /></el-icon>
|
|
</template>
|
|
</el-statistic>
|
|
</el-col>
|
|
</el-row>
|
|
</el-card>
|
|
|
|
<!-- 本月统计 -->
|
|
<el-card class="monthly-card">
|
|
<template #header>
|
|
<span>{{ currentMonth }}月统计</span>
|
|
</template>
|
|
<el-row :gutter="20">
|
|
<el-col :span="8">
|
|
<el-statistic title="本月收入" :value="monthlyIncome" :precision="2" suffix="元"
|
|
value-style="color: #67c23a">
|
|
</el-statistic>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<el-statistic title="本月支出" :value="monthlyExpense" :precision="2" suffix="元"
|
|
value-style="color: #f56c6c">
|
|
</el-statistic>
|
|
</el-col>
|
|
<el-col :span="8">
|
|
<el-statistic title="流水总结" :value="monthlyNet" :precision="2" suffix="元"
|
|
:value-style="monthlyNet >= 0 ? 'color: #67c23a' : 'color: #f56c6c'">
|
|
</el-statistic>
|
|
</el-col>
|
|
</el-row>
|
|
</el-card>
|
|
|
|
<!-- 部门财务统计 -->
|
|
<el-card class="department-card">
|
|
<template #header>
|
|
<span>重点部门财务统计</span>
|
|
</template>
|
|
<el-row :gutter="20">
|
|
<el-col :span="8" v-for="dept in keyDepartments" :key="dept.name">
|
|
<el-card class="dept-stat-card" :class="dept.name">
|
|
<h3>{{ dept.name }}</h3>
|
|
<div class="dept-stats">
|
|
<div class="stat-item">
|
|
<span class="label">收入:</span>
|
|
<span class="value income">+¥{{ dept.income.toFixed(2) }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="label">支出:</span>
|
|
<span class="value expense">-¥{{ dept.expense.toFixed(2) }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="label">余额:</span>
|
|
<span class="value" :class="dept.balance >= 0 ? 'positive' : 'negative'">
|
|
¥{{ dept.balance.toFixed(2) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
</el-card>
|
|
|
|
<!-- 收支流水图 -->
|
|
<el-card class="chart-card">
|
|
<template #header>
|
|
<span>收支流水图</span>
|
|
</template>
|
|
<div ref="chart" style="height: 400px;"></div>
|
|
</el-card>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import {financeService} from '@/services/api';
|
|
import * as echarts from 'echarts';
|
|
import {Document, Money, TrendCharts, Wallet} from '@element-plus/icons-vue';
|
|
import AppHeader from '@/components/AppHeader.vue';
|
|
|
|
export default {
|
|
name: 'FinanceDashboard',
|
|
components: {
|
|
AppHeader,
|
|
TrendCharts,
|
|
Money,
|
|
Wallet,
|
|
Document
|
|
},
|
|
data() {
|
|
return {
|
|
records: [],
|
|
chart: null,
|
|
currentMonth: new Date().getMonth() + 1,
|
|
keyDepartmentNames: ['爱特工作室本部', 'UI部', '游戏部']
|
|
};
|
|
},
|
|
computed: {
|
|
totalIncome() {
|
|
return this.records
|
|
.filter(r => r.record_type === 'income')
|
|
.reduce((sum, r) => sum + parseFloat(r.amount), 0);
|
|
},
|
|
totalExpense() {
|
|
return this.records
|
|
.filter(r => r.record_type === 'expense')
|
|
.reduce((sum, r) => sum + parseFloat(r.amount), 0);
|
|
},
|
|
netProfit() {
|
|
return this.totalIncome - this.totalExpense;
|
|
},
|
|
totalRecords() {
|
|
return this.records.length;
|
|
},
|
|
monthlyIncome() {
|
|
const currentMonth = new Date().getMonth() + 1;
|
|
const currentYear = new Date().getFullYear();
|
|
return this.records
|
|
.filter(r => {
|
|
const recordDate = new Date(r.transaction_date);
|
|
return r.record_type === 'income' &&
|
|
recordDate.getMonth() + 1 === currentMonth &&
|
|
recordDate.getFullYear() === currentYear;
|
|
})
|
|
.reduce((sum, r) => sum + parseFloat(r.amount), 0);
|
|
},
|
|
monthlyExpense() {
|
|
const currentMonth = new Date().getMonth() + 1;
|
|
const currentYear = new Date().getFullYear();
|
|
return this.records
|
|
.filter(r => {
|
|
const recordDate = new Date(r.transaction_date);
|
|
return r.record_type === 'expense' &&
|
|
recordDate.getMonth() + 1 === currentMonth &&
|
|
recordDate.getFullYear() === currentYear;
|
|
})
|
|
.reduce((sum, r) => sum + parseFloat(r.amount), 0);
|
|
},
|
|
monthlyNet() {
|
|
return this.monthlyIncome - this.monthlyExpense;
|
|
},
|
|
keyDepartments() {
|
|
return this.keyDepartmentNames.map(deptName => {
|
|
const deptRecords = this.records.filter(r =>
|
|
r.department && r.department.name === deptName
|
|
);
|
|
|
|
const income = deptRecords
|
|
.filter(r => r.record_type === 'income')
|
|
.reduce((sum, r) => sum + parseFloat(r.amount), 0);
|
|
|
|
const expense = deptRecords
|
|
.filter(r => r.record_type === 'expense')
|
|
.reduce((sum, r) => sum + parseFloat(r.amount), 0);
|
|
|
|
return {
|
|
name: deptName,
|
|
income,
|
|
expense,
|
|
balance: income - expense
|
|
};
|
|
});
|
|
}
|
|
},
|
|
async created() {
|
|
await this.fetchRecords();
|
|
this.$nextTick(() => {
|
|
this.initChart();
|
|
});
|
|
},
|
|
methods: {
|
|
async fetchRecords() {
|
|
try {
|
|
const response = await financeService.getAllFinanceRecords();
|
|
this.records = response.data;
|
|
} catch (error) {
|
|
this.$message.error('获取财务记录失败');
|
|
}
|
|
},
|
|
initChart() {
|
|
if (!this.$refs.chart) return;
|
|
|
|
this.chart = echarts.init(this.$refs.chart);
|
|
const dates = [...new Set(this.records.map(r => r.transaction_date))].sort();
|
|
|
|
const option = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
formatter: function(params) {
|
|
let result = params[0].axisValueLabel + '<br/>';
|
|
params.forEach(param => {
|
|
result += `${param.seriesName}: ¥${param.value}<br/>`;
|
|
});
|
|
return result;
|
|
}
|
|
},
|
|
legend: {
|
|
data: ['收入', '支出']
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: dates,
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLabel: {
|
|
formatter: '¥{value}'
|
|
}
|
|
},
|
|
series: [
|
|
{
|
|
name: '收入',
|
|
type: 'line',
|
|
smooth: true,
|
|
itemStyle: { color: '#67c23a' },
|
|
data: this.getChartData('income', dates),
|
|
label: {
|
|
show: false,
|
|
position: 'top',
|
|
formatter: '¥{c}',
|
|
color: '#67c23a',
|
|
fontWeight: 'bold'
|
|
},
|
|
emphasis: {
|
|
label: {
|
|
show: true
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: '支出',
|
|
type: 'line',
|
|
smooth: true,
|
|
itemStyle: { color: '#f56c6c' },
|
|
data: this.getChartData('expense', dates),
|
|
label: {
|
|
show: false,
|
|
position: 'top',
|
|
formatter: '¥{c}',
|
|
color: '#f56c6c',
|
|
fontWeight: 'bold'
|
|
},
|
|
emphasis: {
|
|
label: {
|
|
show: true
|
|
}
|
|
}
|
|
},
|
|
],
|
|
};
|
|
this.chart.setOption(option);
|
|
|
|
// 添加点击事件监听器
|
|
this.chart.on('click', (params) => {
|
|
const { seriesName, value, name: date, dataIndex } = params;
|
|
|
|
// 获取该日期的详细记录
|
|
const dayRecords = this.records.filter(r => r.transaction_date === date);
|
|
const typeRecords = dayRecords.filter(r =>
|
|
(seriesName === '收入' && r.record_type === 'income') ||
|
|
(seriesName === '支出' && r.record_type === 'expense')
|
|
);
|
|
|
|
// 构建详细信息
|
|
let detailInfo = `<div style="max-width: 400px;">
|
|
<h3 style="margin: 0 0 15px 0; color: #409eff;">${date} - ${seriesName}</h3>
|
|
<p style="margin: 0 0 10px 0; font-size: 18px; font-weight: bold; color: ${seriesName === '收入' ? '#67c23a' : '#f56c6c'};">
|
|
总计: ¥${value.toFixed(2)}
|
|
</p>
|
|
<div style="max-height: 300px; overflow-y: auto;">`;
|
|
|
|
if (typeRecords.length > 0) {
|
|
detailInfo += `<p style="margin: 0 0 10px 0; font-weight: bold;">具体记录 (${typeRecords.length}条):</p>`;
|
|
typeRecords.forEach((record, index) => {
|
|
detailInfo += `
|
|
<div style="padding: 8px; margin-bottom: 8px; background-color: #f5f7fa; border-radius: 4px; border-left: 3px solid ${seriesName === '收入' ? '#67c23a' : '#f56c6c'};">
|
|
<div style="font-weight: bold; color: #303133;">${record.title}</div>
|
|
<div style="display: flex; justify-content: space-between; margin-top: 4px;">
|
|
<span style="color: #606266;">¥${parseFloat(record.amount).toFixed(2)}</span>
|
|
${record.department ? `<span style="color: #909399; font-size: 12px;">${record.department.name}</span>` : ''}
|
|
</div>
|
|
${record.description ? `<div style="color: #909399; font-size: 12px; margin-top: 4px;">${record.description}</div>` : ''}
|
|
</div>`;
|
|
});
|
|
} else {
|
|
detailInfo += '<p style="color: #909399;">该日期无相关记录</p>';
|
|
}
|
|
|
|
detailInfo += '</div></div>';
|
|
|
|
// 显示详细信息对话框
|
|
this.$alert(detailInfo, '财务详情', {
|
|
dangerouslyUseHTMLString: true,
|
|
customClass: 'finance-detail-alert',
|
|
showClose: true,
|
|
closeOnClickModal: true,
|
|
closeOnPressEscape: true
|
|
});
|
|
});
|
|
|
|
// 添加鼠标悬停时显示数据标签
|
|
this.chart.on('mouseover', (params) => {
|
|
const option = this.chart.getOption();
|
|
option.series[params.seriesIndex].label.show = true;
|
|
this.chart.setOption(option);
|
|
});
|
|
|
|
this.chart.on('mouseout', (params) => {
|
|
const option = this.chart.getOption();
|
|
option.series[params.seriesIndex].label.show = false;
|
|
this.chart.setOption(option);
|
|
});
|
|
},
|
|
getChartData(type, dates) {
|
|
const data = {};
|
|
this.records
|
|
.filter(r => r.record_type === type)
|
|
.forEach(r => {
|
|
if (data[r.transaction_date]) {
|
|
data[r.transaction_date] += parseFloat(r.amount);
|
|
} else {
|
|
data[r.transaction_date] = parseFloat(r.amount);
|
|
}
|
|
});
|
|
|
|
return dates.map(date => data[date] || 0);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.finance-dashboard {
|
|
padding: 20px;
|
|
}
|
|
|
|
.summary-card, .monthly-card, .department-card, .chart-card {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.dept-stat-card {
|
|
height: 180px;
|
|
text-align: center;
|
|
transition: transform 0.3s, box-shadow 0.3s;
|
|
}
|
|
|
|
.dept-stat-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.dept-stat-card h3 {
|
|
margin: 0 0 15px 0;
|
|
font-size: 18px;
|
|
color: #409eff;
|
|
}
|
|
|
|
.dept-stats {
|
|
text-align: left;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
padding: 5px 0;
|
|
}
|
|
|
|
.stat-item .label {
|
|
font-weight: bold;
|
|
color: #666;
|
|
}
|
|
|
|
.stat-item .value {
|
|
font-weight: bold;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.value.income {
|
|
color: #67c23a;
|
|
}
|
|
|
|
.value.expense {
|
|
color: #f56c6c;
|
|
}
|
|
|
|
.value.positive {
|
|
color: #67c23a;
|
|
}
|
|
|
|
.value.negative {
|
|
color: #f56c6c;
|
|
}
|
|
|
|
/* 部门特色样式 */
|
|
.dept-stat-card.爱特工作室本部 {
|
|
border-left: 4px solid #409eff;
|
|
}
|
|
|
|
.dept-stat-card.UI部 {
|
|
border-left: 4px solid #67c23a;
|
|
}
|
|
|
|
.dept-stat-card.游戏部 {
|
|
border-left: 4px solid #e6a23c;
|
|
}
|
|
</style>
|