zhangjf b3ef6d89f1 feat: 添加前端管理平台fund-admin并优化后端接口
- 新增fund-admin前端项目(Vue3 + TypeScript + Element Plus)
  - 登录认证、用户信息获取
  - 系统管理:用户、角色、部门、菜单
  - 客户管理、项目管理、需求工单
  - 支出管理、应收款管理
  - Dashboard首页
  - 浅色系侧边栏菜单、面包屑导航

- fund-sys: 添加获取用户信息接口
- fund-exp: 添加支出类型分页接口、修复路由顺序
- fund-proj: 修复路由顺序(/page放于/{id}之前)
- fund-receipt: 新增应收款管理功能
2026-02-17 20:35:18 +08:00

481 lines
12 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="dashboard">
<h2>欢迎使用资金服务平台</h2>
<el-row :gutter="20" class="stats-row">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon receivable">
<el-icon :size="32"><Wallet /></el-icon>
</div>
<div class="stat-info">
<p class="stat-label">待收款金额</p>
<p class="stat-value">¥ {{ stats.unpaidReceivable?.toLocaleString() || '0.00' }}</p>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon expense">
<el-icon :size="32"><Money /></el-icon>
</div>
<div class="stat-info">
<p class="stat-label">待审批支出</p>
<p class="stat-value">¥ {{ stats.pendingExpense?.toLocaleString() || '0.00' }}</p>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon today-in">
<el-icon :size="32"><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<p class="stat-label">今日收入</p>
<p class="stat-value">¥ {{ stats.todayIncome?.toLocaleString() || '0.00' }}</p>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon today-out">
<el-icon :size="32"><DataLine /></el-icon>
</div>
<div class="stat-info">
<p class="stat-label">今日支出</p>
<p class="stat-value">¥ {{ stats.todayExpense?.toLocaleString() || '0.00' }}</p>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="16">
<el-card>
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>收支趋势</span>
<el-radio-group v-model="trendPeriod" size="small" @change="fetchTrendData">
<el-radio-button value="week">近一周</el-radio-button>
<el-radio-button value="month">近一月</el-radio-button>
<el-radio-button value="quarter">近三月</el-radio-button>
</el-radio-group>
</div>
</template>
<div ref="trendChartRef" style="height: 350px;"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card style="height: 448px; overflow-y: auto;">
<template #header>
<span>待办事项</span>
</template>
<el-timeline>
<el-timeline-item
v-for="item in todoList"
:key="item.id"
:timestamp="item.time"
:type="item.type"
placement="top"
>
<div class="todo-item">
<div class="todo-title">{{ item.title }}</div>
<div class="todo-desc">{{ item.description }}</div>
</div>
</el-timeline-item>
<el-empty v-if="todoList.length === 0" description="暂无待办事项" :image-size="80" />
</el-timeline>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<el-card>
<template #header>
<span>项目状态分布</span>
</template>
<div ref="projectChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header>
<span>支出类型分布</span>
</template>
<div ref="expenseChartRef" style="height: 300px;"></div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { Wallet, Money, TrendCharts, DataLine } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
const stats = reactive({
unpaidReceivable: 0,
pendingExpense: 0,
todayIncome: 0,
todayExpense: 0
})
const trendPeriod = ref('week')
const trendChartRef = ref<HTMLElement>()
const projectChartRef = ref<HTMLElement>()
const expenseChartRef = ref<HTMLElement>()
let trendChart: echarts.ECharts | null = null
let projectChart: echarts.ECharts | null = null
let expenseChart: echarts.ECharts | null = null
const todoList = ref([
{
id: 1,
title: '待审批支出申请',
description: '有 3 笔支出申请待审批',
time: '今天 10:30',
type: 'warning'
},
{
id: 2,
title: '即将到期应收款',
description: '2 笔应收款即将到期',
time: '今天 09:15',
type: 'danger'
},
{
id: 3,
title: '项目需求更新',
description: '项目 A 有新的需求工单',
time: '昨天 16:20',
type: 'primary'
}
])
// 获取统计数据
const fetchStats = async () => {
// 这里模拟数据实际应调用API
stats.unpaidReceivable = 125680.50
stats.pendingExpense = 48920.00
stats.todayIncome = 15000.00
stats.todayExpense = 8500.00
}
// 初始化收支趋势图表
const initTrendChart = () => {
if (!trendChartRef.value) return
trendChart = echarts.init(trendChartRef.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: ['收入', '支出']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '¥{value}'
}
},
series: [
{
name: '收入',
type: 'line',
smooth: true,
data: [12000, 18000, 15000, 22000, 19000, 25000, 20000],
itemStyle: {
color: '#67C23A'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
{ offset: 1, color: 'rgba(103, 194, 58, 0.1)' }
])
}
},
{
name: '支出',
type: 'line',
smooth: true,
data: [8000, 9500, 11000, 10500, 13000, 12000, 14500],
itemStyle: {
color: '#F56C6C'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(245, 108, 108, 0.3)' },
{ offset: 1, color: 'rgba(245, 108, 108, 0.1)' }
])
}
}
]
}
trendChart.setOption(option)
}
// 初始化项目状态图表
const initProjectChart = () => {
if (!projectChartRef.value) return
projectChart = echarts.init(projectChartRef.value)
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '项目状态',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {d}%'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
data: [
{ value: 5, name: '待启动', itemStyle: { color: '#909399' } },
{ value: 12, name: '进行中', itemStyle: { color: '#409EFF' } },
{ value: 8, name: '已完成', itemStyle: { color: '#67C23A' } },
{ value: 2, name: '已取消', itemStyle: { color: '#F56C6C' } }
]
}
]
}
projectChart.setOption(option)
}
// 初始化支出类型图表
const initExpenseChart = () => {
if (!expenseChartRef.value) return
expenseChart = echarts.init(expenseChartRef.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function(params: any) {
return params[0].name + '<br/>' + params[0].marker + '¥' + params[0].value.toLocaleString()
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['办公用品', '差旅费', '设备采购', '人员工资', '营销费用', '其他'],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '¥{value}'
}
},
series: [
{
name: '支出金额',
type: 'bar',
barWidth: '60%',
data: [3200, 5400, 8900, 15000, 6200, 2800],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
}
]
}
expenseChart.setOption(option)
}
// 获取趋势数据
const fetchTrendData = () => {
// 根据时间段更新图表数据
if (!trendChart) return
let xData: string[] = []
let incomeData: number[] = []
let expenseData: number[] = []
if (trendPeriod.value === 'week') {
xData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
incomeData = [12000, 18000, 15000, 22000, 19000, 25000, 20000]
expenseData = [8000, 9500, 11000, 10500, 13000, 12000, 14500]
} else if (trendPeriod.value === 'month') {
xData = Array.from({ length: 30 }, (_, i) => `${i + 1}`)
incomeData = Array.from({ length: 30 }, () => Math.floor(Math.random() * 30000) + 10000)
expenseData = Array.from({ length: 30 }, () => Math.floor(Math.random() * 20000) + 5000)
} else {
xData = ['第1月', '第2月', '第3月']
incomeData = [450000, 520000, 480000]
expenseData = [280000, 310000, 295000]
}
trendChart.setOption({
xAxis: {
data: xData
},
series: [
{ data: incomeData },
{ data: expenseData }
]
})
}
// 窗口resize时重绘图表
const handleResize = () => {
trendChart?.resize()
projectChart?.resize()
expenseChart?.resize()
}
onMounted(async () => {
await fetchStats()
await nextTick()
initTrendChart()
initProjectChart()
initExpenseChart()
window.addEventListener('resize', handleResize)
})
</script>
<style scoped lang="scss">
.dashboard {
padding: 0;
h2 {
margin-bottom: 20px;
color: #333;
}
}
.stats-row {
margin-bottom: 20px;
}
.stat-card {
height: 120px;
}
.stat-content {
display: flex;
align-items: center;
gap: 15px;
}
.stat-icon {
width: 64px;
height: 64px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
&.receivable {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.expense {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&.today-in {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.today-out {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
}
.stat-info {
flex: 1;
}
.stat-label {
color: #999;
font-size: 14px;
margin: 0 0 8px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin: 0;
}
.todo-item {
.todo-title {
font-weight: 500;
margin-bottom: 4px;
}
.todo-desc {
font-size: 13px;
color: #666;
}
}
</style>