- 新增fund-admin前端项目(Vue3 + TypeScript + Element Plus)
- 登录认证、用户信息获取
- 系统管理:用户、角色、部门、菜单
- 客户管理、项目管理、需求工单
- 支出管理、应收款管理
- Dashboard首页
- 浅色系侧边栏菜单、面包屑导航
- fund-sys: 添加获取用户信息接口
- fund-exp: 添加支出类型分页接口、修复路由顺序
- fund-proj: 修复路由顺序(/page放于/{id}之前)
- fund-receipt: 新增应收款管理功能
481 lines
12 KiB
Vue
481 lines
12 KiB
Vue
<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>
|