feat: 添加前端管理平台fund-admin并优化后端接口

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

- fund-sys: 添加获取用户信息接口
- fund-exp: 添加支出类型分页接口、修复路由顺序
- fund-proj: 修复路由顺序(/page放于/{id}之前)
- fund-receipt: 新增应收款管理功能
This commit is contained in:
zhangjf 2026-02-17 20:35:18 +08:00
parent 33d7cc2145
commit b3ef6d89f1
66 changed files with 10807 additions and 60 deletions

24
fund-admin/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
fund-admin/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
fund-admin/README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
fund-admin/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fund-admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3817
fund-admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
fund-admin/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "fund-admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"axios": "^1.13.5",
"echarts": "^6.0.0",
"element-plus": "^2.13.2",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^5.0.2"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"sass-embedded": "^1.97.3",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vue-tsc": "^3.1.5"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

21
fund-admin/src/App.vue Normal file
View File

@ -0,0 +1,21 @@
<template>
<router-view />
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
onMounted(() => {
//
userStore.initUserInfo()
})
</script>
<style>
#app {
height: 100%;
}
</style>

View File

@ -0,0 +1,21 @@
import { request } from './request'
// 登录
export function login(data: { username: string; password: string }) {
return request.post('/sys/api/v1/auth/login', data)
}
// 登出
export function logout() {
return request.post('/sys/api/v1/auth/logout')
}
// 获取当前用户信息
export function getUserInfo() {
return request.get('/sys/api/v1/auth/info')
}
// 刷新Token
export function refreshToken() {
return request.post('/sys/api/v1/auth/refresh')
}

View File

@ -0,0 +1,42 @@
import { request } from './request'
export function getCustomerList(params: { pageNum: number; pageSize: number; customerName?: string; status?: number }) {
return request.get('/cust/api/v1/customer/page', { params })
}
export function getCustomerById(id: number) {
return request.get(`/cust/api/v1/customer/${id}`)
}
export function createCustomer(data: any) {
return request.post('/cust/api/v1/customer', data)
}
export function updateCustomer(id: number, data: any) {
return request.put(`/cust/api/v1/customer/${id}`, data)
}
export function deleteCustomer(id: number) {
return request.delete(`/cust/api/v1/customer/${id}`)
}
// 联系人相关
export function getContactList(params: { pageNum: number; pageSize: number; customerId: number }) {
return request.get('/cust/api/v1/customer/contact/page', { params })
}
export function createContact(data: any) {
return request.post('/cust/api/v1/customer/contact', data)
}
export function updateContact(id: number, data: any) {
return request.put(`/cust/api/v1/customer/contact/${id}`, data)
}
export function deleteContact(id: number) {
return request.delete(`/cust/api/v1/customer/contact/${id}`)
}
export function setPrimaryContact(customerId: number, contactId: number) {
return request.put(`/cust/api/v1/customer/${customerId}/contact/${contactId}/primary`)
}

View File

@ -0,0 +1,25 @@
import { request } from './request'
export function getDeptList(params?: { deptName?: string; status?: number }) {
return request.get('/sys/api/v1/sys/dept/list', { params })
}
export function getDeptTree() {
return request.get('/sys/api/v1/sys/dept/tree')
}
export function getDeptById(id: number) {
return request.get(`/sys/api/v1/sys/dept/${id}`)
}
export function createDept(data: any) {
return request.post('/sys/api/v1/sys/dept', data)
}
export function updateDept(data: any) {
return request.put('/sys/api/v1/sys/dept', data)
}
export function deleteDept(id: number) {
return request.delete(`/sys/api/v1/sys/dept/${id}`)
}

View File

@ -0,0 +1,64 @@
import { request } from './request'
// 支出类型
export function getExpenseTypeList(params: { pageNum: number; pageSize: number; typeName?: string; status?: string }) {
return request.get('/exp/api/v1/exp/expense-type/page', { params })
}
export function getExpenseTypeTree() {
return request.get('/exp/api/v1/exp/expense-type/tree')
}
export function createExpenseType(data: any) {
return request.post('/exp/api/v1/exp/expense-type', data)
}
export function updateExpenseType(id: number, data: any) {
return request.put(`/exp/api/v1/exp/expense-type/${id}`, data)
}
export function deleteExpenseType(id: number) {
return request.delete(`/exp/api/v1/exp/expense-type/${id}`)
}
// 支出管理
export function getExpenseList(params: { pageNum: number; pageSize: number; title?: string; expenseType?: number; approvalStatus?: number; payStatus?: number }) {
return request.get('/exp/api/v1/exp/expense/page', { params })
}
export function getExpenseById(id: number) {
return request.get(`/exp/api/v1/exp/expense/${id}`)
}
export function createExpense(data: any) {
return request.post('/exp/api/v1/exp/expense', data)
}
export function updateExpense(id: number, data: any) {
return request.put(`/exp/api/v1/exp/expense/${id}`, data)
}
export function deleteExpense(id: number) {
return request.delete(`/exp/api/v1/exp/expense/${id}`)
}
// 审批流程
export function submitExpense(id: number) {
return request.post(`/exp/api/v1/exp/expense/${id}/submit`)
}
export function withdrawExpense(id: number) {
return request.post(`/exp/api/v1/exp/expense/${id}/withdraw`)
}
export function approveExpense(id: number, comment: string) {
return request.put(`/exp/api/v1/exp/expense/${id}/approve?comment=${encodeURIComponent(comment)}`)
}
export function rejectExpense(id: number, comment: string) {
return request.put(`/exp/api/v1/exp/expense/${id}/reject?comment=${encodeURIComponent(comment)}`)
}
export function confirmPayExpense(id: number, payChannel: string, payVoucher?: string) {
return request.put(`/exp/api/v1/exp/expense/${id}/confirm-pay?payChannel=${payChannel}&payVoucher=${payVoucher || ''}`)
}

View File

@ -0,0 +1,30 @@
import { request } from './request'
export function getMenuList(params?: { menuName?: string; status?: number }) {
return request.get('/sys/api/v1/sys/menu/tree', { params })
}
export function getMenuTree() {
return request.get('/sys/api/v1/sys/menu/tree')
}
export function getMenuById(id: number) {
return request.get(`/sys/api/v1/sys/menu/${id}`)
}
export function createMenu(data: any) {
return request.post('/sys/api/v1/sys/menu', data)
}
export function updateMenu(data: any) {
return request.put('/sys/api/v1/sys/menu', data)
}
export function deleteMenu(id: number) {
return request.delete(`/sys/api/v1/sys/menu/${id}`)
}
export function getUserMenus(userId?: number) {
const id = userId || JSON.parse(localStorage.getItem('userInfo') || '{}').id
return request.get(`/sys/api/v1/sys/menu/user/${id}`)
}

View File

@ -0,0 +1,42 @@
import { request } from './request'
export function getProjectList(params: { pageNum: number; pageSize: number; projectName?: string; status?: string }) {
return request.get('/proj/api/v1/project/page', { params })
}
export function getProjectById(id: number) {
return request.get(`/proj/api/v1/project/${id}`)
}
export function createProject(data: any) {
return request.post('/proj/api/v1/project', data)
}
export function updateProject(id: number, data: any) {
return request.put(`/proj/api/v1/project/${id}`, data)
}
export function deleteProject(id: number) {
return request.delete(`/proj/api/v1/project/${id}`)
}
// 需求工单
export function getRequirementList(params: { pageNum: number; pageSize: number; requirementName?: string; status?: string }) {
return request.get('/proj/api/v1/requirement/page', { params })
}
export function getRequirementById(id: number) {
return request.get(`/proj/api/v1/requirement/${id}`)
}
export function createRequirement(data: any) {
return request.post('/proj/api/v1/requirement', data)
}
export function updateRequirement(id: number, data: any) {
return request.put(`/proj/api/v1/requirement/${id}`, data)
}
export function deleteRequirement(id: number) {
return request.delete(`/proj/api/v1/requirement/${id}`)
}

View File

@ -0,0 +1,51 @@
import { request } from './request'
// 应收款管理
export function getReceivableList(params: { pageNum: number; pageSize: number; projectId?: number; customerId?: number; status?: string; confirmStatus?: number }) {
return request.get('/receipt/api/v1/receipt/receivable/page', { params })
}
export function getReceivableById(id: number) {
return request.get(`/receipt/api/v1/receipt/receivable/${id}`)
}
export function createReceivable(data: any) {
return request.post('/receipt/api/v1/receipt/receivable', data)
}
export function updateReceivable(id: number, data: any) {
return request.put(`/receipt/api/v1/receipt/receivable/${id}`, data)
}
export function deleteReceivable(id: number) {
return request.delete(`/receipt/api/v1/receipt/receivable/${id}`)
}
export function confirmReceivable(id: number) {
return request.put(`/receipt/api/v1/receipt/receivable/${id}/confirm`)
}
export function cancelConfirmReceivable(id: number) {
return request.put(`/receipt/api/v1/receipt/receivable/${id}/cancel-confirm`)
}
export function recordReceipt(id: number, data: any) {
return request.post(`/receipt/api/v1/receipt/receivable/${id}/receipt`, data)
}
export function getReceiptRecords(receivableId: number) {
return request.get(`/receipt/api/v1/receipt/receivable/${receivableId}/receipts`)
}
// 收款记录
export function getReceiptList(params: { pageNum: number; pageSize: number; receivableId?: number }) {
return request.get('/receipt/api/v1/receipt/receipt/page', { params })
}
export function getReceiptById(id: number) {
return request.get(`/receipt/api/v1/receipt/receipt/${id}`)
}
export function createReceipt(data: any) {
return request.post('/receipt/api/v1/receipt/receipt', data)
}

View File

@ -0,0 +1,70 @@
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: '',
timeout: 15000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 添加租户ID和用户信息
const tenantId = localStorage.getItem('tenantId') || '1'
config.headers['X-Tenant-Id'] = tenantId
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data
// 根据后端Result结构判断
if (res.code && res.code !== 200) {
ElMessage.error(res.message || '请求失败')
// 401 未授权
if (res.code === 401) {
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
}
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
},
(error) => {
const message = error.response?.data?.message || error.message || '网络错误'
ElMessage.error(message)
return Promise.reject(error)
}
)
// 封装请求方法
export const request = {
get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return service.get(url, config)
},
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.post(url, data, config)
},
put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.put(url, data, config)
},
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return service.delete(url, config)
}
}
export default service

View File

@ -0,0 +1,29 @@
import { request } from './request'
export function getRoleList(params: { pageNum: number; pageSize: number; roleName?: string }) {
return request.get('/sys/api/v1/sys/role/page', { params })
}
export function getRoleById(id: number) {
return request.get(`/sys/api/v1/sys/role/${id}`)
}
export function createRole(data: any) {
return request.post('/sys/api/v1/sys/role', data)
}
export function updateRole(data: any) {
return request.put('/sys/api/v1/sys/role', data)
}
export function deleteRole(id: number) {
return request.delete(`/sys/api/v1/sys/role/${id}`)
}
export function getRoleMenus(id: number) {
return request.get(`/sys/api/v1/sys/role/${id}/menus`)
}
export function assignMenus(id: number, menuIds: number[]) {
return request.put(`/sys/api/v1/sys/role/${id}/menus`, menuIds)
}

View File

@ -0,0 +1,31 @@
import { request } from './request'
// 用户列表
export function getUserList(params: { pageNum: number; pageSize: number; username?: string; status?: number }) {
return request.get('/sys/api/v1/sys/user/page', { params })
}
// 获取用户详情
export function getUserById(id: number) {
return request.get(`/sys/api/v1/sys/user/${id}`)
}
// 创建用户
export function createUser(data: any) {
return request.post('/sys/api/v1/sys/user', data)
}
// 更新用户
export function updateUser(data: any) {
return request.put('/sys/api/v1/sys/user', data)
}
// 删除用户
export function deleteUser(id: number) {
return request.delete(`/sys/api/v1/sys/user/${id}`)
}
// 更新用户状态
export function updateUserStatus(id: number, status: number) {
return request.put(`/sys/api/v1/sys/user/${id}/status?status=${status}`)
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

10
fund-admin/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
title?: string
icon?: string
parent?: string
requiresAuth?: boolean
}
}

View File

@ -0,0 +1,317 @@
<template>
<el-container class="main-layout">
<!-- 侧边栏 -->
<el-aside :width="appStore.sidebarCollapsed ? '64px' : '220px'" class="sidebar">
<div class="logo">
<img src="/vite.svg" alt="Logo" />
<span v-show="!appStore.sidebarCollapsed">资金服务平台</span>
</div>
<el-menu
:default-active="activeMenu"
:collapse="appStore.sidebarCollapsed"
:collapse-transition="false"
router
class="sidebar-menu"
>
<el-menu-item index="/dashboard">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-sub-menu index="system">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/system/user">
<el-icon><User /></el-icon>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/system/role">
<el-icon><UserFilled /></el-icon>
<span>角色管理</span>
</el-menu-item>
<el-menu-item index="/system/dept">
<el-icon><OfficeBuilding /></el-icon>
<span>部门管理</span>
</el-menu-item>
<el-menu-item index="/system/menu">
<el-icon><Menu /></el-icon>
<span>菜单管理</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item index="/customer/list">
<el-icon><UserFilled /></el-icon>
<span>客户管理</span>
</el-menu-item>
<el-sub-menu index="project">
<template #title>
<el-icon><Folder /></el-icon>
<span>项目管理</span>
</template>
<el-menu-item index="/project/list">
<el-icon><FolderOpened /></el-icon>
<span>项目列表</span>
</el-menu-item>
<el-menu-item index="/project/requirement">
<el-icon><Document /></el-icon>
<span>需求工单</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="expense">
<template #title>
<el-icon><Money /></el-icon>
<span>支出管理</span>
</template>
<el-menu-item index="/expense/type">
<el-icon><PriceTag /></el-icon>
<span>支出类型</span>
</el-menu-item>
<el-menu-item index="/expense/list">
<el-icon><Wallet /></el-icon>
<span>支出列表</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item index="/receivable/list">
<el-icon><Wallet /></el-icon>
<span>应收款管理</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container>
<!-- 顶部导航 -->
<el-header class="header">
<div class="left">
<el-icon class="collapse-btn" @click="appStore.toggleSidebar">
<Fold v-if="!appStore.sidebarCollapsed" />
<Expand v-else />
</el-icon>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item v-if="currentRoute.meta.parent">
{{ currentRoute.meta.parent }}
</el-breadcrumb-item>
<el-breadcrumb-item v-if="currentRoute.meta.title !== '首页'">
{{ currentRoute.meta.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="right">
<el-dropdown @command="handleCommand">
<span class="user-info">
<el-avatar :size="32" :src="userStore.userInfo?.avatar">
{{ userStore.userInfo?.realName?.charAt(0) || 'U' }}
</el-avatar>
<span class="username">{{ userStore.userInfo?.realName || '用户' }}</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
<el-dropdown-item command="settings">系统设置</el-dropdown-item>
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 内容区 -->
<el-main class="main-content">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import {
HomeFilled, Setting, User, UserFilled, OfficeBuilding, Menu,
Folder, FolderOpened, Document, Money, PriceTag, Wallet,
Fold, Expand, ArrowDown
} from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import { useAppStore } from '@/stores/app'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const appStore = useAppStore()
const activeMenu = computed(() => route.path)
const currentRoute = computed(() => route)
const handleCommand = async (command: string) => {
switch (command) {
case 'profile':
//
break
case 'settings':
//
break
case 'logout':
try {
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await userStore.logoutAction()
router.push('/login')
} catch (e) {
// 退
}
break
}
}
</script>
<style scoped>
.main-layout {
height: 100vh;
}
.sidebar {
background-color: #f5f7fa;
transition: width 0.3s;
border-right: 1px solid #e4e7ed;
}
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
color: #303133;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid #e4e7ed;
background-color: #fff;
}
.logo img {
width: 32px;
height: 32px;
}
.sidebar-menu {
border-right: none;
background-color: #f5f7fa;
height: calc(100% - 60px);
}
.sidebar-menu:not(.el-menu--collapse) {
width: 220px;
}
/* 菜单项样式 */
.sidebar-menu :deep(.el-menu-item),
.sidebar-menu :deep(.el-sub-menu__title) {
color: #606266;
}
.sidebar-menu :deep(.el-menu-item:hover),
.sidebar-menu :deep(.el-sub-menu__title:hover) {
background-color: #ecf5ff;
}
.sidebar-menu :deep(.el-menu-item.is-active) {
color: #409eff;
background-color: #ecf5ff;
}
/* 子菜单展开时一级菜单样式 */
.sidebar-menu :deep(.el-sub-menu.is-opened > .el-sub-menu__title) {
color: #409eff;
background-color: #e6f1fc;
}
.sidebar-menu :deep(.el-sub-menu.is-opened > .el-sub-menu__title .el-sub-menu__icon-arrow) {
color: #409eff;
}
/* 子菜单容器样式 */
.sidebar-menu :deep(.el-sub-menu .el-menu) {
background-color: #fafafa;
}
.sidebar-menu :deep(.el-sub-menu .el-menu-item) {
background-color: #fafafa;
min-width: auto;
}
.sidebar-menu :deep(.el-sub-menu .el-menu-item:hover) {
background-color: #ecf5ff;
}
.sidebar-menu :deep(.el-sub-menu .el-menu-item.is-active) {
color: #409eff;
background-color: #ecf5ff;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0 20px;
}
.left {
display: flex;
align-items: center;
gap: 15px;
}
.collapse-btn {
font-size: 20px;
cursor: pointer;
}
.right {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.username {
font-size: 14px;
}
.main-content {
background-color: #f0f2f5;
padding: 20px;
overflow-y: auto;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

23
fund-admin/src/main.ts Normal file
View File

@ -0,0 +1,23 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
import './styles/index.scss'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus, { locale: zhCn })
app.use(router)
app.use(pinia)
app.mount('#app')

View File

@ -0,0 +1,141 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/stores/user'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { title: '登录', requiresAuth: false }
},
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '首页', icon: 'HomeFilled' }
},
// 系统管理
{
path: 'system/user',
name: 'UserManage',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '用户管理', icon: 'User', parent: '系统管理' }
},
{
path: 'system/role',
name: 'RoleManage',
component: () => import('@/views/system/role/index.vue'),
meta: { title: '角色管理', icon: 'UserFilled', parent: '系统管理' }
},
{
path: 'system/dept',
name: 'DeptManage',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门管理', icon: 'OfficeBuilding', parent: '系统管理' }
},
{
path: 'system/menu',
name: 'MenuManage',
component: () => import('@/views/system/menu/index.vue'),
meta: { title: '菜单管理', icon: 'Menu', parent: '系统管理' }
},
// 客户管理
{
path: 'customer/list',
name: 'CustomerList',
component: () => import('@/views/customer/index.vue'),
meta: { title: '客户管理', icon: 'UserFilled' }
},
{
path: 'customer/contact',
name: 'ContactManage',
component: () => import('@/views/customer/contact.vue'),
meta: { title: '联系人管理', icon: 'User' }
},
// 项目管理
{
path: 'project/list',
name: 'ProjectList',
component: () => import('@/views/project/index.vue'),
meta: { title: '项目列表', icon: 'FolderOpened', parent: '项目管理' }
},
{
path: 'project/requirement',
name: 'RequirementList',
component: () => import('@/views/project/requirement.vue'),
meta: { title: '需求工单', icon: 'Document', parent: '项目管理' }
},
// 支出管理
{
path: 'expense/type',
name: 'ExpenseType',
component: () => import('@/views/expense/type.vue'),
meta: { title: '支出类型', icon: 'PriceTag', parent: '支出管理' }
},
{
path: 'expense/list',
name: 'ExpenseList',
component: () => import('@/views/expense/index.vue'),
meta: { title: '支出列表', icon: 'Wallet', parent: '支出管理' }
},
// 应收款管理
{
path: 'receivable/list',
name: 'ReceivableList',
component: () => import('@/views/receivable/index.vue'),
meta: { title: '应收款管理', icon: 'Wallet' }
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error/404.vue'),
meta: { title: '404' }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
const token = localStorage.getItem('token')
// 设置页面标题
document.title = `${to.meta.title || '资金服务平台'} - FundPlatform`
// 不需要认证的页面
if (to.meta.requiresAuth === false) {
next()
return
}
// 未登录跳转登录页
if (!token) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
// 已登录但未获取用户信息
if (!userStore.userInfo) {
userStore.fetchUserInfo().then(() => {
next()
}).catch(() => {
next({ name: 'Login' })
})
return
}
next()
})
export default router

View File

@ -0,0 +1,22 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAppStore = defineStore('app', () => {
const sidebarCollapsed = ref(false)
const loading = ref(false)
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
function setLoading(val: boolean) {
loading.value = val
}
return {
sidebarCollapsed,
loading,
toggleSidebar,
setLoading
}
})

View File

@ -0,0 +1,8 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
export * from './user'
export * from './app'

View File

@ -0,0 +1,77 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { login, logout, getUserInfo } from '@/api/auth'
export interface UserInfo {
userId: number
username: string
realName: string
avatar?: string
role?: string
deptId?: number
deptName?: string
}
export const useUserStore = defineStore('user', () => {
const token = ref<string>(localStorage.getItem('token') || '')
const userInfo = ref<UserInfo | null>(null)
// 登录
async function loginAction(username: string, password: string) {
try {
const res: any = await login({ username, password })
token.value = res.data?.token || res.token
localStorage.setItem('token', token.value)
return res
} catch (error) {
throw error
}
}
// 获取用户信息
async function fetchUserInfo() {
try {
const res: any = await getUserInfo()
userInfo.value = res.data || res
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
return userInfo.value
} catch (error) {
throw error
}
}
// 登出
async function logoutAction() {
try {
await logout()
} catch (error) {
console.error('Logout error:', error)
} finally {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
}
// 初始化用户信息
function initUserInfo() {
const stored = localStorage.getItem('userInfo')
if (stored) {
try {
userInfo.value = JSON.parse(stored)
} catch (e) {
console.error('Parse userInfo error:', e)
}
}
}
return {
token,
userInfo,
loginAction,
fetchUserInfo,
logoutAction,
initUserInfo
}
})

79
fund-admin/src/style.css Normal file
View File

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -0,0 +1,54 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
body {
font-size: 14px;
color: #333;
background-color: #f0f2f5;
}
a {
text-decoration: none;
color: inherit;
}
ul, li {
list-style: none;
}
// 滚动条样式
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: #d9d9d9;
border-radius: 3px;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
// Element Plus 覆盖样式
.el-menu {
border-right: none !important;
}
.el-menu--collapse {
width: 64px !important;
}
.el-menu--collapse .el-sub-menu__title span,
.el-menu--collapse .el-menu-item span {
display: none;
}

View File

@ -0,0 +1,252 @@
<template>
<div class="contact-container">
<el-page-header @back="goBack" :content="`${customerName} - 联系人管理`" style="margin-bottom: 20px" />
<el-card shadow="never">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增联系人</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="contactId" label="联系人ID" width="100" />
<el-table-column prop="contactName" label="姓名" width="120" />
<el-table-column prop="position" label="职位" width="120" />
<el-table-column prop="phone" label="电话" width="140" />
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
<el-table-column prop="wechat" label="微信" width="140" />
<el-table-column prop="isPrimary" label="主联系人" width="100">
<template #default="{ row }">
<el-tag v-if="row.isPrimary" type="success"></el-tag>
<el-tag v-else type="info"></el-tag>
</template>
</el-table-column>
<el-table-column prop="remarks" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button link type="primary" :icon="Star" v-if="!row.isPrimary" @click="handleSetPrimary(row)">设为主要</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="600px"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="职位" prop="position">
<el-input v-model="form.position" placeholder="请输入职位" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="微信" prop="wechat">
<el-input v-model="form.wechat" placeholder="请输入微信号" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="主联系人" prop="isPrimary">
<el-switch v-model="form.isPrimary" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Plus, Edit, Delete, Star } from '@element-plus/icons-vue'
import { getContactList, createContact, updateContact, deleteContact, setPrimaryContact } from '@/api/customer'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const customerId = ref(Number(route.query.customerId))
const customerName = ref(String(route.query.customerName || ''))
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
customerId: customerId.value
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增联系人')
const formRef = ref<FormInstance>()
const form = reactive({
contactId: null as number | null,
customerId: customerId.value,
contactName: '',
position: '',
phone: '',
email: '',
wechat: '',
isPrimary: false,
remarks: ''
})
const rules = reactive<FormRules>({
contactName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
})
const fetchData = async () => {
loading.value = true
try {
const res: any = await getContactList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const goBack = () => {
router.back()
}
const handleAdd = () => {
dialogTitle.value = '新增联系人'
resetForm()
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑联系人'
Object.assign(form, row)
dialogVisible.value = true
}
const handleSetPrimary = async (row: any) => {
try {
await setPrimaryContact(customerId.value, row.contactId)
ElMessage.success('设置成功')
fetchData()
} catch (e) {
console.error(e)
}
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该联系人吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteContact(row.contactId)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
console.error(e)
}
})
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (form.contactId) {
await updateContact(form.contactId, form)
ElMessage.success('更新成功')
} else {
await createContact(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
} catch (e) {
console.error(e)
} finally {
submitLoading.value = false
}
})
}
const resetForm = () => {
form.contactId = null
form.customerId = customerId.value
form.contactName = ''
form.position = ''
form.phone = ''
form.email = ''
form.wechat = ''
form.isPrimary = false
form.remarks = ''
formRef.value?.clearValidate()
}
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.contact-container {
padding: 20px;
}
</style>

View File

@ -0,0 +1,335 @@
<template>
<div class="customer-container">
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams">
<el-form-item label="客户名称">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="客户类型">
<el-select v-model="queryParams.customerType" placeholder="请选择" clearable style="width: 150px">
<el-option label="企业" value="ENTERPRISE" />
<el-option label="个人" value="INDIVIDUAL" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 120px">
<el-option label="正常" value="NORMAL" />
<el-option label="禁用" value="DISABLED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" style="margin-top: 10px">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增客户</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="customerId" label="客户ID" width="80" />
<el-table-column prop="customerName" label="客户名称" min-width="150" />
<el-table-column prop="customerType" label="客户类型" width="100">
<template #default="{ row }">
<el-tag v-if="row.customerType === 'ENTERPRISE'" type="primary">企业</el-tag>
<el-tag v-else type="success">个人</el-tag>
</template>
</el-table-column>
<el-table-column prop="contactPerson" label="联系人" width="120" />
<el-table-column prop="contactPhone" label="联系电话" width="140" />
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag v-if="row.status === 'NORMAL'" type="success">正常</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button link type="primary" :icon="User" @click="handleContact(row)">联系人</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="700px"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="form.customerName" placeholder="请输入客户名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户类型" prop="customerType">
<el-select v-model="form.customerType" placeholder="请选择" style="width: 100%">
<el-option label="企业" value="ENTERPRISE" />
<el-option label="个人" value="INDIVIDUAL" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio value="NORMAL">正常</el-radio>
<el-radio value="DISABLED">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" placeholder="请输入地址" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 客户详情对话框 -->
<el-dialog title="客户详情" v-model="detailVisible" width="700px">
<el-descriptions :column="2" border>
<el-descriptions-item label="客户ID">{{ detailData.customerId }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">
<el-tag v-if="detailData.customerType === 'ENTERPRISE'" type="primary">企业</el-tag>
<el-tag v-else type="success">个人</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="detailData.status === 'NORMAL'" type="success">正常</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</el-descriptions-item>
<el-descriptions-item label="联系人">{{ detailData.contactPerson }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailData.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="邮箱" :span="2">{{ detailData.email }}</el-descriptions-item>
<el-descriptions-item label="地址" :span="2">{{ detailData.address }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.remarks || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete, View, User } from '@element-plus/icons-vue'
import { getCustomerList, createCustomer, updateCustomer, deleteCustomer } from '@/api/customer'
import { useRouter } from 'vue-router'
const router = useRouter()
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
customerName: '',
customerType: '',
status: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增客户')
const formRef = ref<FormInstance>()
const form = reactive({
customerId: null as number | null,
customerName: '',
customerType: 'ENTERPRISE',
contactPerson: '',
contactPhone: '',
email: '',
address: '',
status: 'NORMAL',
remarks: ''
})
const rules = reactive<FormRules>({
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
customerType: [{ required: true, message: '请选择客户类型', trigger: 'change' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
})
const detailVisible = ref(false)
const detailData = ref<any>({})
const fetchData = async () => {
loading.value = true
try {
const res: any = await getCustomerList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const handleSearch = () => {
queryParams.pageNum = 1
fetchData()
}
const handleReset = () => {
queryParams.customerName = ''
queryParams.customerType = ''
queryParams.status = ''
queryParams.pageNum = 1
fetchData()
}
const handleAdd = () => {
dialogTitle.value = '新增客户'
resetForm()
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑客户'
Object.assign(form, row)
dialogVisible.value = true
}
const handleView = (row: any) => {
detailData.value = row
detailVisible.value = true
}
const handleContact = (row: any) => {
router.push({ path: '/customer/contact', query: { customerId: row.customerId, customerName: row.customerName } })
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该客户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteCustomer(row.customerId)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
console.error(e)
}
})
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (form.customerId) {
await updateCustomer(form.customerId, form)
ElMessage.success('更新成功')
} else {
await createCustomer(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
} catch (e) {
console.error(e)
} finally {
submitLoading.value = false
}
})
}
const resetForm = () => {
form.customerId = null
form.customerName = ''
form.customerType = 'ENTERPRISE'
form.contactPerson = ''
form.contactPhone = ''
form.email = ''
form.address = ''
form.status = 'NORMAL'
form.remarks = ''
formRef.value?.clearValidate()
}
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.customer-container {
padding: 20px;
.search-card {
:deep(.el-card__body) {
padding-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,480 @@
<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>

View File

@ -0,0 +1,45 @@
<template>
<div class="error-page">
<div class="error-content">
<h1>404</h1>
<p>抱歉您访问的页面不存在</p>
<el-button type="primary" @click="goHome">返回首页</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const goHome = () => {
router.push('/')
}
</script>
<style scoped>
.error-page {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f0f2f5;
}
.error-content {
text-align: center;
}
.error-content h1 {
font-size: 120px;
color: #409eff;
margin: 0;
}
.error-content p {
font-size: 20px;
color: #666;
margin: 20px 0 30px;
}
</style>

View File

@ -0,0 +1,492 @@
<template>
<div class="expense-container">
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams">
<el-form-item label="支出标题">
<el-input v-model="queryParams.title" placeholder="请输入支出标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="支出类型">
<el-select v-model="queryParams.expenseTypeId" placeholder="请选择" clearable filterable style="width: 150px">
<el-option
v-for="item in expenseTypeList"
:key="item.typeId"
:label="item.typeName"
:value="item.typeId"
/>
</el-select>
</el-form-item>
<el-form-item label="审批状态">
<el-select v-model="queryParams.approvalStatus" placeholder="请选择" clearable style="width: 140px">
<el-option label="草稿" value="DRAFT" />
<el-option label="待审批" value="PENDING" />
<el-option label="已通过" value="APPROVED" />
<el-option label="已拒绝" value="REJECTED" />
<el-option label="已撤回" value="WITHDRAWN" />
</el-select>
</el-form-item>
<el-form-item label="支付状态">
<el-select v-model="queryParams.payStatus" placeholder="请选择" clearable style="width: 120px">
<el-option label="未支付" value="UNPAID" />
<el-option label="已支付" value="PAID" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" style="margin-top: 10px">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增支出</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="expenseId" label="支出ID" width="80" />
<el-table-column prop="title" label="支出标题" min-width="180" show-overflow-tooltip />
<el-table-column prop="expenseTypeName" label="支出类型" width="120" />
<el-table-column prop="projectName" label="关联项目" width="140" show-overflow-tooltip />
<el-table-column prop="amount" label="支出金额" width="120" align="right">
<template #default="{ row }">
¥{{ row.amount?.toLocaleString() || '0' }}
</template>
</el-table-column>
<el-table-column prop="expenseDate" label="支出日期" width="120" />
<el-table-column prop="approvalStatus" label="审批状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.approvalStatus === 'DRAFT'" type="info">草稿</el-tag>
<el-tag v-else-if="row.approvalStatus === 'PENDING'" type="warning">待审批</el-tag>
<el-tag v-else-if="row.approvalStatus === 'APPROVED'" type="success">已通过</el-tag>
<el-tag v-else-if="row.approvalStatus === 'REJECTED'" type="danger">已拒绝</el-tag>
<el-tag v-else type="info">已撤回</el-tag>
</template>
</el-table-column>
<el-table-column prop="payStatus" label="支付状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.payStatus === 'UNPAID'" type="warning">未支付</el-tag>
<el-tag v-else type="success">已支付</el-tag>
</template>
</el-table-column>
<el-table-column prop="applicant" label="申请人" width="100" />
<el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
<el-button link type="primary" :icon="Edit" v-if="row.approvalStatus === 'DRAFT'" @click="handleEdit(row)">编辑</el-button>
<el-button link type="success" :icon="Check" v-if="row.approvalStatus === 'DRAFT'" @click="handleSubmit(row)">提交</el-button>
<el-button link type="warning" v-if="row.approvalStatus === 'PENDING'" @click="handleApprove(row)">审批</el-button>
<el-button link type="danger" :icon="Delete" v-if="row.approvalStatus === 'DRAFT'" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="900px"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="支出标题" prop="title">
<el-input v-model="form.title" placeholder="请输入支出标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支出类型" prop="expenseTypeId">
<el-select v-model="form.expenseTypeId" placeholder="请选择" filterable style="width: 100%">
<el-option
v-for="item in expenseTypeList"
:key="item.typeId"
:label="item.typeName"
:value="item.typeId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="关联项目" prop="projectId">
<el-select v-model="form.projectId" placeholder="请选择项目" filterable clearable style="width: 100%">
<el-option
v-for="item in projectList"
:key="item.projectId"
:label="item.projectName"
:value="item.projectId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支出金额" prop="amount">
<el-input-number v-model="form.amount" :precision="2" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="支出日期" prop="expenseDate">
<el-date-picker v-model="form.expenseDate" type="date" placeholder="请选择支出日期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请人" prop="applicant">
<el-input v-model="form.applicant" placeholder="请输入申请人" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="支出描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入支出描述" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveExpense" :loading="submitLoading">保存</el-button>
</template>
</el-dialog>
<!-- 审批对话框 -->
<el-dialog title="支出审批" v-model="approvalVisible" width="600px">
<el-descriptions :column="1" border>
<el-descriptions-item label="支出标题">{{ approvalData.title }}</el-descriptions-item>
<el-descriptions-item label="支出金额">¥{{ approvalData.amount?.toLocaleString() || '0' }}</el-descriptions-item>
<el-descriptions-item label="支出类型">{{ approvalData.expenseTypeName }}</el-descriptions-item>
<el-descriptions-item label="关联项目">{{ approvalData.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ approvalData.applicant }}</el-descriptions-item>
<el-descriptions-item label="支出日期">{{ approvalData.expenseDate }}</el-descriptions-item>
<el-descriptions-item label="支出描述">{{ approvalData.description || '-' }}</el-descriptions-item>
</el-descriptions>
<el-form :model="approvalForm" style="margin-top: 20px" label-width="100px">
<el-form-item label="审批意见">
<el-input v-model="approvalForm.comment" type="textarea" :rows="3" placeholder="请输入审批意见" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="approvalVisible = false">取消</el-button>
<el-button type="danger" @click="handleReject">拒绝</el-button>
<el-button type="success" @click="handleApproveConfirm">通过</el-button>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="支出详情" v-model="detailVisible" width="900px">
<el-descriptions :column="2" border>
<el-descriptions-item label="支出ID">{{ detailData.expenseId }}</el-descriptions-item>
<el-descriptions-item label="支出标题">{{ detailData.title }}</el-descriptions-item>
<el-descriptions-item label="支出类型">{{ detailData.expenseTypeName }}</el-descriptions-item>
<el-descriptions-item label="关联项目">{{ detailData.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="支出金额">
¥{{ detailData.amount?.toLocaleString() || '0' }}
</el-descriptions-item>
<el-descriptions-item label="支出日期">{{ detailData.expenseDate }}</el-descriptions-item>
<el-descriptions-item label="审批状态">
<el-tag v-if="detailData.approvalStatus === 'DRAFT'" type="info">草稿</el-tag>
<el-tag v-else-if="detailData.approvalStatus === 'PENDING'" type="warning">待审批</el-tag>
<el-tag v-else-if="detailData.approvalStatus === 'APPROVED'" type="success">已通过</el-tag>
<el-tag v-else-if="detailData.approvalStatus === 'REJECTED'" type="danger">已拒绝</el-tag>
<el-tag v-else type="info">已撤回</el-tag>
</el-descriptions-item>
<el-descriptions-item label="支付状态">
<el-tag v-if="detailData.payStatus === 'UNPAID'" type="warning">未支付</el-tag>
<el-tag v-else type="success">已支付</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请人">{{ detailData.applicant }}</el-descriptions-item>
<el-descriptions-item label="审批人">{{ detailData.approver || '-' }}</el-descriptions-item>
<el-descriptions-item label="支出描述" :span="2">{{ detailData.description || '-' }}</el-descriptions-item>
<el-descriptions-item label="审批意见" :span="2">{{ detailData.approvalComment || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.remarks || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete, View, Check } from '@element-plus/icons-vue'
import {
getExpenseList,
createExpense,
updateExpense,
deleteExpense,
submitExpense,
approveExpense,
rejectExpense
} from '@/api/expense'
import { getExpenseTypeTree } from '@/api/expense'
import { getProjectList } from '@/api/project'
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const expenseTypeList = ref<any[]>([])
const projectList = ref<any[]>([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
title: '',
expenseTypeId: null as number | null,
approvalStatus: '',
payStatus: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增支出')
const formRef = ref<FormInstance>()
const form = reactive({
expenseId: null as number | null,
title: '',
expenseTypeId: null as number | null,
projectId: null as number | null,
amount: 0,
expenseDate: '',
applicant: '',
description: '',
remarks: ''
})
const rules = reactive<FormRules>({
title: [{ required: true, message: '请输入支出标题', trigger: 'blur' }],
expenseTypeId: [{ required: true, message: '请选择支出类型', trigger: 'change' }],
amount: [{ required: true, message: '请输入支出金额', trigger: 'blur' }],
expenseDate: [{ required: true, message: '请选择支出日期', trigger: 'change' }],
applicant: [{ required: true, message: '请输入申请人', trigger: 'blur' }]
})
const approvalVisible = ref(false)
const approvalData = ref<any>({})
const approvalForm = reactive({
expenseId: null as number | null,
comment: ''
})
const detailVisible = ref(false)
const detailData = ref<any>({})
//
const flattenTree = (tree: any[]): any[] => {
const result: any[] = []
const flatten = (nodes: any[]) => {
nodes.forEach(node => {
result.push(node)
if (node.children && node.children.length > 0) {
flatten(node.children)
}
})
}
flatten(tree)
return result
}
const fetchData = async () => {
loading.value = true
try {
const res: any = await getExpenseList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const fetchExpenseTypes = async () => {
try {
const res: any = await getExpenseTypeTree()
const treeData = res.data || []
expenseTypeList.value = flattenTree(treeData)
} catch (e) {
console.error(e)
}
}
const fetchProjects = async () => {
try {
const res: any = await getProjectList({ pageNum: 1, pageSize: 1000 })
projectList.value = res.data?.records || []
} catch (e) {
console.error(e)
}
}
const handleSearch = () => {
queryParams.pageNum = 1
fetchData()
}
const handleReset = () => {
queryParams.title = ''
queryParams.expenseTypeId = null
queryParams.approvalStatus = ''
queryParams.payStatus = ''
queryParams.pageNum = 1
fetchData()
}
const handleAdd = () => {
dialogTitle.value = '新增支出'
resetForm()
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑支出'
Object.assign(form, row)
dialogVisible.value = true
}
const handleView = (row: any) => {
detailData.value = row
detailVisible.value = true
}
const handleSubmit = (row: any) => {
ElMessageBox.confirm('确认提交该支出申请吗?提交后将进入审批流程。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await submitExpense(row.expenseId)
ElMessage.success('提交成功')
fetchData()
} catch (e) {
console.error(e)
}
})
}
const handleApprove = (row: any) => {
approvalData.value = row
approvalForm.expenseId = row.expenseId
approvalForm.comment = ''
approvalVisible.value = true
}
const handleApproveConfirm = async () => {
try {
await approveExpense(approvalForm.expenseId!, approvalForm.comment)
ElMessage.success('审批通过')
approvalVisible.value = false
fetchData()
} catch (e) {
console.error(e)
}
}
const handleReject = async () => {
if (!approvalForm.comment) {
ElMessage.warning('请输入拒绝原因')
return
}
try {
await rejectExpense(approvalForm.expenseId!, approvalForm.comment)
ElMessage.success('已拒绝')
approvalVisible.value = false
fetchData()
} catch (e) {
console.error(e)
}
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该支出吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteExpense(row.expenseId)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
console.error(e)
}
})
}
const handleSaveExpense = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (form.expenseId) {
await updateExpense(form.expenseId, form)
ElMessage.success('更新成功')
} else {
await createExpense(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
} catch (e) {
console.error(e)
} finally {
submitLoading.value = false
}
})
}
const resetForm = () => {
form.expenseId = null
form.title = ''
form.expenseTypeId = null
form.projectId = null
form.amount = 0
form.expenseDate = ''
form.applicant = ''
form.description = ''
form.remarks = ''
formRef.value?.clearValidate()
}
onMounted(() => {
fetchData()
fetchExpenseTypes()
fetchProjects()
})
</script>
<style scoped lang="scss">
.expense-container {
padding: 20px;
.search-card {
:deep(.el-card__body) {
padding-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<div class="expense-type-container">
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams">
<el-form-item label="类型名称">
<el-input v-model="queryParams.typeName" placeholder="请输入类型名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 120px">
<el-option label="启用" value="ENABLED" />
<el-option label="禁用" value="DISABLED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" style="margin-top: 10px">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增类型</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="typeId" label="类型ID" width="80" />
<el-table-column prop="typeCode" label="类型编码" width="140" />
<el-table-column prop="typeName" label="类型名称" min-width="150" />
<el-table-column prop="parentName" label="上级类型" width="140" />
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-switch
v-model="row.status"
active-value="ENABLED"
inactive-value="DISABLED"
@change="handleStatusChange(row)"
/>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="600px"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="类型编码" prop="typeCode">
<el-input v-model="form.typeCode" placeholder="请输入类型编码" />
</el-form-item>
<el-form-item label="类型名称" prop="typeName">
<el-input v-model="form.typeName" placeholder="请输入类型名称" />
</el-form-item>
<el-form-item label="上级类型" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="typeTreeData"
:props="{ label: 'typeName', value: 'typeId', children: 'children' }"
placeholder="请选择上级类型(不选则为顶级类型)"
check-strictly
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio value="ENABLED">启用</el-radio>
<el-radio value="DISABLED">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
import { getExpenseTypeList, createExpenseType, updateExpenseType, deleteExpenseType, getExpenseTypeTree } from '@/api/expense'
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const typeTreeData = ref<any[]>([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
typeName: '',
status: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增类型')
const formRef = ref<FormInstance>()
const form = reactive({
typeId: null as number | null,
typeCode: '',
typeName: '',
parentId: null as number | null,
sortOrder: 0,
status: 'ENABLED',
description: ''
})
const rules = reactive<FormRules>({
typeCode: [{ required: true, message: '请输入类型编码', trigger: 'blur' }],
typeName: [{ required: true, message: '请输入类型名称', trigger: 'blur' }]
})
const fetchData = async () => {
loading.value = true
try {
const res: any = await getExpenseTypeList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const fetchTypeTree = async () => {
try {
const res: any = await getExpenseTypeTree()
typeTreeData.value = res.data || []
} catch (e) {
console.error(e)
}
}
const handleSearch = () => {
queryParams.pageNum = 1
fetchData()
}
const handleReset = () => {
queryParams.typeName = ''
queryParams.status = ''
queryParams.pageNum = 1
fetchData()
}
const handleAdd = () => {
dialogTitle.value = '新增类型'
resetForm()
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑类型'
Object.assign(form, row)
dialogVisible.value = true
}
const handleStatusChange = async (row: any) => {
try {
await updateExpenseType(row.typeId, { status: row.status })
ElMessage.success('状态更新成功')
} catch (e) {
console.error(e)
row.status = row.status === 'ENABLED' ? 'DISABLED' : 'ENABLED'
}
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该类型吗?删除后关联的支出记录将无法关联到该类型。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteExpenseType(row.typeId)
ElMessage.success('删除成功')
fetchData()
fetchTypeTree()
} catch (e) {
console.error(e)
}
})
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (form.typeId) {
await updateExpenseType(form.typeId, form)
ElMessage.success('更新成功')
} else {
await createExpenseType(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
fetchTypeTree()
} catch (e) {
console.error(e)
} finally {
submitLoading.value = false
}
})
}
const resetForm = () => {
form.typeId = null
form.typeCode = ''
form.typeName = ''
form.parentId = null
form.sortOrder = 0
form.status = 'ENABLED'
form.description = ''
formRef.value?.clearValidate()
}
onMounted(() => {
fetchData()
fetchTypeTree()
})
</script>
<style scoped lang="scss">
.expense-type-container {
padding: 20px;
.search-card {
:deep(.el-card__body) {
padding-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<img src="/vite.svg" alt="Logo" class="logo" />
<h1>资金服务平台</h1>
<p>FundPlatform</p>
</div>
<el-form ref="formRef" :model="form" :rules="rules" class="login-form">
<el-form-item prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
size="large"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="rememberMe">记住我</el-checkbox>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-btn"
@click="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const formRef = ref<FormInstance>()
const loading = ref(false)
const rememberMe = ref(false)
const form = reactive({
username: '',
password: ''
})
const rules: FormRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const handleLogin = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
loading.value = true
await userStore.loginAction(form.username, form.password)
//
await userStore.fetchUserInfo()
ElMessage.success('登录成功')
//
const redirect = (route.query.redirect as string) || '/dashboard'
router.push(redirect)
} catch (error: any) {
ElMessage.error(error.message || '登录失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.logo {
width: 60px;
height: 60px;
margin-bottom: 15px;
}
.login-header h1 {
font-size: 24px;
color: #333;
margin: 0 0 8px;
}
.login-header p {
color: #999;
margin: 0;
}
.login-form {
width: 100%;
}
.login-btn {
width: 100%;
}
</style>

View File

@ -0,0 +1,361 @@
<template>
<div class="project-container">
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams">
<el-form-item label="项目名称">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="客户名称">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="项目状态">
<el-select v-model="queryParams.projectStatus" placeholder="请选择" clearable style="width: 150px">
<el-option label="待启动" value="PENDING" />
<el-option label="进行中" value="IN_PROGRESS" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELLED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" style="margin-top: 10px">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增项目</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="projectId" label="项目ID" width="80" />
<el-table-column prop="projectCode" label="项目编号" width="140" />
<el-table-column prop="projectName" label="项目名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="customerName" label="客户名称" width="140" />
<el-table-column prop="contractAmount" label="合同金额" width="120" align="right">
<template #default="{ row }">
¥{{ row.contractAmount?.toLocaleString() || '0' }}
</template>
</el-table-column>
<el-table-column prop="projectStatus" label="项目状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.projectStatus === 'PENDING'" type="info">待启动</el-tag>
<el-tag v-else-if="row.projectStatus === 'IN_PROGRESS'" type="primary">进行中</el-tag>
<el-tag v-else-if="row.projectStatus === 'COMPLETED'" type="success">已完成</el-tag>
<el-tag v-else type="danger">已取消</el-tag>
</template>
</el-table-column>
<el-table-column prop="startDate" label="开始日期" width="120" />
<el-table-column prop="endDate" label="结束日期" width="120" />
<el-table-column prop="projectManager" label="项目经理" width="100" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="900px"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目编号" prop="projectCode">
<el-input v-model="form.projectCode" placeholder="请输入项目编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户" prop="customerId">
<el-select v-model="form.customerId" placeholder="请选择客户" filterable style="width: 100%">
<el-option
v-for="item in customerList"
:key="item.customerId"
:label="item.customerName"
:value="item.customerId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同金额" prop="contractAmount">
<el-input-number v-model="form.contractAmount" :precision="2" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="form.startDate" type="date" placeholder="请选择开始日期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="form.endDate" type="date" placeholder="请选择结束日期" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目经理" prop="projectManager">
<el-input v-model="form.projectManager" placeholder="请输入项目经理" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目状态" prop="projectStatus">
<el-select v-model="form.projectStatus" placeholder="请选择" style="width: 100%">
<el-option label="待启动" value="PENDING" />
<el-option label="进行中" value="IN_PROGRESS" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELLED" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="项目描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入项目描述" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 项目详情对话框 -->
<el-dialog title="项目详情" v-model="detailVisible" width="900px">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目ID">{{ detailData.projectId }}</el-descriptions-item>
<el-descriptions-item label="项目编号">{{ detailData.projectCode }}</el-descriptions-item>
<el-descriptions-item label="项目名称" :span="2">{{ detailData.projectName }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
<el-descriptions-item label="合同金额">
¥{{ detailData.contractAmount?.toLocaleString() || '0' }}
</el-descriptions-item>
<el-descriptions-item label="项目状态">
<el-tag v-if="detailData.projectStatus === 'PENDING'" type="info">待启动</el-tag>
<el-tag v-else-if="detailData.projectStatus === 'IN_PROGRESS'" type="primary">进行中</el-tag>
<el-tag v-else-if="detailData.projectStatus === 'COMPLETED'" type="success">已完成</el-tag>
<el-tag v-else type="danger">已取消</el-tag>
</el-descriptions-item>
<el-descriptions-item label="项目经理">{{ detailData.projectManager }}</el-descriptions-item>
<el-descriptions-item label="开始日期">{{ detailData.startDate }}</el-descriptions-item>
<el-descriptions-item label="结束日期">{{ detailData.endDate }}</el-descriptions-item>
<el-descriptions-item label="项目描述" :span="2">{{ detailData.description || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.remarks || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete, View } from '@element-plus/icons-vue'
import { getProjectList, createProject, updateProject, deleteProject } from '@/api/project'
import { getCustomerList } from '@/api/customer'
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const customerList = ref<any[]>([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
projectName: '',
customerName: '',
projectStatus: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增项目')
const formRef = ref<FormInstance>()
const form = reactive({
projectId: null as number | null,
projectCode: '',
projectName: '',
customerId: null as number | null,
contractAmount: 0,
startDate: '',
endDate: '',
projectManager: '',
projectStatus: 'PENDING',
description: '',
remarks: ''
})
const rules = reactive<FormRules>({
projectCode: [{ required: true, message: '请输入项目编号', trigger: 'blur' }],
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
contractAmount: [{ required: true, message: '请输入合同金额', trigger: 'blur' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
projectStatus: [{ required: true, message: '请选择项目状态', trigger: 'change' }]
})
const detailVisible = ref(false)
const detailData = ref<any>({})
const fetchData = async () => {
loading.value = true
try {
const res: any = await getProjectList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const fetchCustomers = async () => {
try {
const res: any = await getCustomerList({ pageNum: 1, pageSize: 1000 })
customerList.value = res.data?.records || []
} catch (e) {
console.error(e)
}
}
const handleSearch = () => {
queryParams.pageNum = 1
fetchData()
}
const handleReset = () => {
queryParams.projectName = ''
queryParams.customerName = ''
queryParams.projectStatus = ''
queryParams.pageNum = 1
fetchData()
}
const handleAdd = () => {
dialogTitle.value = '新增项目'
resetForm()
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑项目'
Object.assign(form, row)
dialogVisible.value = true
}
const handleView = (row: any) => {
detailData.value = row
detailVisible.value = true
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteProject(row.projectId)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
console.error(e)
}
})
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (form.projectId) {
await updateProject(form.projectId, form)
ElMessage.success('更新成功')
} else {
await createProject(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
} catch (e) {
console.error(e)
} finally {
submitLoading.value = false
}
})
}
const resetForm = () => {
form.projectId = null
form.projectCode = ''
form.projectName = ''
form.customerId = null
form.contractAmount = 0
form.startDate = ''
form.endDate = ''
form.projectManager = ''
form.projectStatus = 'PENDING'
form.description = ''
form.remarks = ''
formRef.value?.clearValidate()
}
onMounted(() => {
fetchData()
fetchCustomers()
})
</script>
<style scoped lang="scss">
.project-container {
padding: 20px;
.search-card {
:deep(.el-card__body) {
padding-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,464 @@
<template>
<div class="requirement-container">
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams">
<el-form-item label="需求标题">
<el-input v-model="queryParams.requirementTitle" placeholder="请输入需求标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="项目名称">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="需求状态">
<el-select v-model="queryParams.requirementStatus" placeholder="请选择" clearable style="width: 150px">
<el-option label="待评估" value="PENDING" />
<el-option label="已接受" value="ACCEPTED" />
<el-option label="开发中" value="IN_PROGRESS" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已拒绝" value="REJECTED" />
</el-select>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="queryParams.priority" placeholder="请选择" clearable style="width: 120px">
<el-option label="低" value="LOW" />
<el-option label="中" value="MEDIUM" />
<el-option label="高" value="HIGH" />
<el-option label="紧急" value="URGENT" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" style="margin-top: 10px">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增需求</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="requirementId" label="需求ID" width="80" />
<el-table-column prop="requirementCode" label="需求编号" width="140" />
<el-table-column prop="requirementTitle" label="需求标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" width="150" show-overflow-tooltip />
<el-table-column prop="requirementType" label="需求类型" width="100">
<template #default="{ row }">
<el-tag v-if="row.requirementType === 'FEATURE'" type="primary">功能</el-tag>
<el-tag v-else-if="row.requirementType === 'BUG'" type="danger">缺陷</el-tag>
<el-tag v-else type="warning">优化</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag v-if="row.priority === 'URGENT'" type="danger">紧急</el-tag>
<el-tag v-else-if="row.priority === 'HIGH'" type="warning"></el-tag>
<el-tag v-else-if="row.priority === 'MEDIUM'" type="primary"></el-tag>
<el-tag v-else type="info"></el-tag>
</template>
</el-table-column>
<el-table-column prop="requirementStatus" label="需求状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.requirementStatus === 'PENDING'" type="info">待评估</el-tag>
<el-tag v-else-if="row.requirementStatus === 'ACCEPTED'" type="primary">已接受</el-tag>
<el-tag v-else-if="row.requirementStatus === 'IN_PROGRESS'">开发中</el-tag>
<el-tag v-else-if="row.requirementStatus === 'COMPLETED'" type="success">已完成</el-tag>
<el-tag v-else type="danger">已拒绝</el-tag>
</template>
</el-table-column>
<el-table-column prop="submitter" label="提交人" width="100" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button link type="success" :icon="Check" @click="handleUpdateStatus(row)">流转</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="900px"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="需求编号" prop="requirementCode">
<el-input v-model="form.requirementCode" placeholder="请输入需求编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="需求标题" prop="requirementTitle">
<el-input v-model="form.requirementTitle" placeholder="请输入需求标题" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目" prop="projectId">
<el-select v-model="form.projectId" placeholder="请选择项目" filterable style="width: 100%">
<el-option
v-for="item in projectList"
:key="item.projectId"
:label="item.projectName"
:value="item.projectId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="需求类型" prop="requirementType">
<el-select v-model="form.requirementType" placeholder="请选择" style="width: 100%">
<el-option label="功能" value="FEATURE" />
<el-option label="缺陷" value="BUG" />
<el-option label="优化" value="IMPROVEMENT" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="请选择" style="width: 100%">
<el-option label="低" value="LOW" />
<el-option label="中" value="MEDIUM" />
<el-option label="高" value="HIGH" />
<el-option label="紧急" value="URGENT" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="需求状态" prop="requirementStatus">
<el-select v-model="form.requirementStatus" placeholder="请选择" style="width: 100%">
<el-option label="待评估" value="PENDING" />
<el-option label="已接受" value="ACCEPTED" />
<el-option label="开发中" value="IN_PROGRESS" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已拒绝" value="REJECTED" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="提交人" prop="submitter">
<el-input v-model="form.submitter" placeholder="请输入提交人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="期望完成日期" prop="expectedDate">
<el-date-picker v-model="form.expectedDate" type="date" placeholder="请选择期望完成日期" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="需求描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="4" placeholder="请输入需求描述" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 状态流转对话框 -->
<el-dialog title="需求状态流转" v-model="statusVisible" width="500px">
<el-form :model="statusForm" label-width="100px">
<el-form-item label="当前状态">
<el-tag v-if="statusForm.currentStatus === 'PENDING'" type="info">待评估</el-tag>
<el-tag v-else-if="statusForm.currentStatus === 'ACCEPTED'" type="primary">已接受</el-tag>
<el-tag v-else-if="statusForm.currentStatus === 'IN_PROGRESS'">开发中</el-tag>
<el-tag v-else-if="statusForm.currentStatus === 'COMPLETED'" type="success">已完成</el-tag>
<el-tag v-else type="danger">已拒绝</el-tag>
</el-form-item>
<el-form-item label="流转状态">
<el-select v-model="statusForm.newStatus" placeholder="请选择新状态" style="width: 100%">
<el-option label="待评估" value="PENDING" />
<el-option label="已接受" value="ACCEPTED" />
<el-option label="开发中" value="IN_PROGRESS" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已拒绝" value="REJECTED" />
</el-select>
</el-form-item>
<el-form-item label="流转说明">
<el-input v-model="statusForm.remark" type="textarea" :rows="3" placeholder="请输入流转说明" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmStatus">确定</el-button>
</template>
</el-dialog>
<!-- 需求详情对话框 -->
<el-dialog title="需求详情" v-model="detailVisible" width="900px">
<el-descriptions :column="2" border>
<el-descriptions-item label="需求ID">{{ detailData.requirementId }}</el-descriptions-item>
<el-descriptions-item label="需求编号">{{ detailData.requirementCode }}</el-descriptions-item>
<el-descriptions-item label="需求标题" :span="2">{{ detailData.requirementTitle }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item>
<el-descriptions-item label="需求类型">
<el-tag v-if="detailData.requirementType === 'FEATURE'" type="primary">功能</el-tag>
<el-tag v-else-if="detailData.requirementType === 'BUG'" type="danger">缺陷</el-tag>
<el-tag v-else type="warning">优化</el-tag>
</el-descriptions-item>
<el-descriptions-item label="优先级">
<el-tag v-if="detailData.priority === 'URGENT'" type="danger">紧急</el-tag>
<el-tag v-else-if="detailData.priority === 'HIGH'" type="warning"></el-tag>
<el-tag v-else-if="detailData.priority === 'MEDIUM'" type="primary"></el-tag>
<el-tag v-else type="info"></el-tag>
</el-descriptions-item>
<el-descriptions-item label="需求状态">
<el-tag v-if="detailData.requirementStatus === 'PENDING'" type="info">待评估</el-tag>
<el-tag v-else-if="detailData.requirementStatus === 'ACCEPTED'" type="primary">已接受</el-tag>
<el-tag v-else-if="detailData.requirementStatus === 'IN_PROGRESS'">开发中</el-tag>
<el-tag v-else-if="detailData.requirementStatus === 'COMPLETED'" type="success">已完成</el-tag>
<el-tag v-else type="danger">已拒绝</el-tag>
</el-descriptions-item>
<el-descriptions-item label="提交人">{{ detailData.submitter }}</el-descriptions-item>
<el-descriptions-item label="期望完成日期">{{ detailData.expectedDate }}</el-descriptions-item>
<el-descriptions-item label="需求描述" :span="2">{{ detailData.description || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.remarks || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete, View, Check } from '@element-plus/icons-vue'
import { getRequirementList, createRequirement, updateRequirement, deleteRequirement } from '@/api/project'
import { getProjectList } from '@/api/project'
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const projectList = ref<any[]>([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
requirementTitle: '',
projectName: '',
requirementStatus: '',
priority: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增需求')
const formRef = ref<FormInstance>()
const form = reactive({
requirementId: null as number | null,
requirementCode: '',
requirementTitle: '',
projectId: null as number | null,
requirementType: 'FEATURE',
priority: 'MEDIUM',
requirementStatus: 'PENDING',
submitter: '',
expectedDate: '',
description: '',
remarks: ''
})
const rules = reactive<FormRules>({
requirementCode: [{ required: true, message: '请输入需求编号', trigger: 'blur' }],
requirementTitle: [{ required: true, message: '请输入需求标题', trigger: 'blur' }],
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
requirementType: [{ required: true, message: '请选择需求类型', trigger: 'change' }],
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }]
})
const statusVisible = ref(false)
const statusForm = reactive({
requirementId: null as number | null,
currentStatus: '',
newStatus: '',
remark: ''
})
const detailVisible = ref(false)
const detailData = ref<any>({})
const fetchData = async () => {
loading.value = true
try {
const res: any = await getRequirementList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const fetchProjects = async () => {
try {
const res: any = await getProjectList({ pageNum: 1, pageSize: 1000 })
projectList.value = res.data?.records || []
} catch (e) {
console.error(e)
}
}
const handleSearch = () => {
queryParams.pageNum = 1
fetchData()
}
const handleReset = () => {
queryParams.requirementTitle = ''
queryParams.projectName = ''
queryParams.requirementStatus = ''
queryParams.priority = ''
queryParams.pageNum = 1
fetchData()
}
const handleAdd = () => {
dialogTitle.value = '新增需求'
resetForm()
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑需求'
Object.assign(form, row)
dialogVisible.value = true
}
const handleView = (row: any) => {
detailData.value = row
detailVisible.value = true
}
const handleUpdateStatus = (row: any) => {
statusForm.requirementId = row.requirementId
statusForm.currentStatus = row.requirementStatus
statusForm.newStatus = ''
statusForm.remark = ''
statusVisible.value = true
}
const handleConfirmStatus = async () => {
if (!statusForm.newStatus) {
ElMessage.warning('请选择新状态')
return
}
try {
await updateRequirement(statusForm.requirementId!, {
requirementStatus: statusForm.newStatus,
remark: statusForm.remark
})
ElMessage.success('状态更新成功')
statusVisible.value = false
fetchData()
} catch (e) {
console.error(e)
}
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该需求吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteRequirement(row.requirementId)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
console.error(e)
}
})
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (form.requirementId) {
await updateRequirement(form.requirementId, form)
ElMessage.success('更新成功')
} else {
await createRequirement(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
} catch (e) {
console.error(e)
} finally {
submitLoading.value = false
}
})
}
const resetForm = () => {
form.requirementId = null
form.requirementCode = ''
form.requirementTitle = ''
form.projectId = null
form.requirementType = 'FEATURE'
form.priority = 'MEDIUM'
form.requirementStatus = 'PENDING'
form.submitter = ''
form.expectedDate = ''
form.description = ''
form.remarks = ''
formRef.value?.clearValidate()
}
onMounted(() => {
fetchData()
fetchProjects()
})
</script>
<style scoped lang="scss">
.requirement-container {
padding: 20px;
.search-card {
:deep(.el-card__body) {
padding-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,520 @@
<template>
<div class="receivable-container">
<el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams">
<el-form-item label="应收款编号">
<el-input v-model="queryParams.receivableCode" placeholder="请输入应收款编号" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="项目名称">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="收款状态">
<el-select v-model="queryParams.receiptStatus" placeholder="请选择" clearable style="width: 140px">
<el-option label="未收款" value="UNPAID" />
<el-option label="部分收款" value="PARTIAL" />
<el-option label="已收款" value="PAID" />
<el-option label="已逾期" value="OVERDUE" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" style="margin-top: 10px">
<div style="margin-bottom: 15px">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增应收款</el-button>
<el-button type="success" :icon="Money">批量收款</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column type="selection" width="55" />
<el-table-column prop="receivableId" label="ID" width="80" />
<el-table-column prop="receivableCode" label="应收款编号" width="160" />
<el-table-column prop="projectName" label="项目名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="customerName" label="客户名称" width="140" show-overflow-tooltip />
<el-table-column prop="totalAmount" label="应收金额" width="120" align="right">
<template #default="{ row }">
¥{{ row.totalAmount?.toLocaleString() || '0' }}
</template>
</el-table-column>
<el-table-column prop="receivedAmount" label="已收金额" width="120" align="right">
<template #default="{ row }">
¥{{ row.receivedAmount?.toLocaleString() || '0' }}
</template>
</el-table-column>
<el-table-column prop="remainingAmount" label="未收金额" width="120" align="right">
<template #default="{ row }">
¥{{ row.remainingAmount?.toLocaleString() || '0' }}
</template>
</el-table-column>
<el-table-column prop="dueDate" label="应收日期" width="120" />
<el-table-column prop="receiptStatus" label="收款状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.receiptStatus === 'UNPAID'" type="warning">未收款</el-tag>
<el-tag v-else-if="row.receiptStatus === 'PARTIAL'">部分收款</el-tag>
<el-tag v-else-if="row.receiptStatus === 'PAID'" type="success">已收款</el-tag>
<el-tag v-else type="danger">已逾期</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button link type="success" :icon="Money" v-if="row.receiptStatus !== 'PAID'" @click="handleReceipt(row)">收款</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="900px"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="应收款编号" prop="receivableCode">
<el-input v-model="form.receivableCode" placeholder="请输入应收款编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关联项目" prop="projectId">
<el-select v-model="form.projectId" placeholder="请选择项目" filterable style="width: 100%" @change="handleProjectChange">
<el-option
v-for="item in projectList"
:key="item.projectId"
:label="item.projectName"
:value="item.projectId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户" prop="customerId">
<el-select v-model="form.customerId" placeholder="请选择客户" filterable style="width: 100%">
<el-option
v-for="item in customerList"
:key="item.customerId"
:label="item.customerName"
:value="item.customerId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="应收金额" prop="totalAmount">
<el-input-number v-model="form.totalAmount" :precision="2" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="应收日期" prop="dueDate">
<el-date-picker v-model="form.dueDate" type="date" placeholder="请选择应收日期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="收款状态" prop="receiptStatus">
<el-select v-model="form.receiptStatus" placeholder="请选择" style="width: 100%">
<el-option label="未收款" value="UNPAID" />
<el-option label="部分收款" value="PARTIAL" />
<el-option label="已收款" value="PAID" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="账单描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入账单描述" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 收款对话框 -->
<el-dialog title="登记收款" v-model="receiptVisible" width="600px">
<el-descriptions :column="1" border style="margin-bottom: 20px">
<el-descriptions-item label="应收款编号">{{ receiptData.receivableCode }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ receiptData.projectName }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ receiptData.customerName }}</el-descriptions-item>
<el-descriptions-item label="应收金额">¥{{ receiptData.totalAmount?.toLocaleString() || '0' }}</el-descriptions-item>
<el-descriptions-item label="已收金额">¥{{ receiptData.receivedAmount?.toLocaleString() || '0' }}</el-descriptions-item>
<el-descriptions-item label="未收金额">¥{{ receiptData.remainingAmount?.toLocaleString() || '0' }}</el-descriptions-item>
</el-descriptions>
<el-form :model="receiptForm" :rules="receiptRules" ref="receiptFormRef" label-width="100px">
<el-form-item label="收款金额" prop="receiptAmount">
<el-input-number v-model="receiptForm.receiptAmount" :precision="2" :min="0" :max="receiptData.remainingAmount" style="width: 100%" />
</el-form-item>
<el-form-item label="收款日期" prop="receiptDate">
<el-date-picker v-model="receiptForm.receiptDate" type="date" placeholder="请选择收款日期" style="width: 100%" />
</el-form-item>
<el-form-item label="收款方式" prop="receiptMethod">
<el-select v-model="receiptForm.receiptMethod" placeholder="请选择收款方式" style="width: 100%">
<el-option label="银行转账" value="BANK_TRANSFER" />
<el-option label="现金" value="CASH" />
<el-option label="支票" value="CHECK" />
<el-option label="其他" value="OTHER" />
</el-select>
</el-form-item>
<el-form-item label="收款凭证号" prop="receiptVoucher">
<el-input v-model="receiptForm.receiptVoucher" placeholder="请输入收款凭证号" />
</el-form-item>
<el-form-item label="收款说明" prop="receiptNote">
<el-input v-model="receiptForm.receiptNote" type="textarea" :rows="3" placeholder="请输入收款说明" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="receiptVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmReceipt" :loading="receiptLoading">确认收款</el-button>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="应收款详情" v-model="detailVisible" width="900px">
<el-descriptions :column="2" border>
<el-descriptions-item label="应收款ID">{{ detailData.receivableId }}</el-descriptions-item>
<el-descriptions-item label="应收款编号">{{ detailData.receivableCode }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
<el-descriptions-item label="应收金额">
¥{{ detailData.totalAmount?.toLocaleString() || '0' }}
</el-descriptions-item>
<el-descriptions-item label="已收金额">
¥{{ detailData.receivedAmount?.toLocaleString() || '0' }}
</el-descriptions-item>
<el-descriptions-item label="未收金额">
¥{{ detailData.remainingAmount?.toLocaleString() || '0' }}
</el-descriptions-item>
<el-descriptions-item label="收款状态">
<el-tag v-if="detailData.receiptStatus === 'UNPAID'" type="warning">未收款</el-tag>
<el-tag v-else-if="detailData.receiptStatus === 'PARTIAL'">部分收款</el-tag>
<el-tag v-else-if="detailData.receiptStatus === 'PAID'" type="success">已收款</el-tag>
<el-tag v-else type="danger">已逾期</el-tag>
</el-descriptions-item>
<el-descriptions-item label="应收日期">{{ detailData.dueDate }}</el-descriptions-item>
<el-descriptions-item label="账单描述" :span="2">{{ detailData.description || '-' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.remarks || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime }}</el-descriptions-item>
</el-descriptions>
<!-- 收款记录 -->
<el-divider content-position="left">收款记录</el-divider>
<el-table :data="receiptRecords" border stripe max-height="300">
<el-table-column prop="receiptId" label="收款ID" width="80" />
<el-table-column prop="receiptAmount" label="收款金额" width="120" align="right">
<template #default="{ row }">
¥{{ row.receiptAmount?.toLocaleString() || '0' }}
</template>
</el-table-column>
<el-table-column prop="receiptDate" label="收款日期" width="120" />
<el-table-column prop="receiptMethod" label="收款方式" width="120">
<template #default="{ row }">
<span v-if="row.receiptMethod === 'BANK_TRANSFER'">银行转账</span>
<span v-else-if="row.receiptMethod === 'CASH'">现金</span>
<span v-else-if="row.receiptMethod === 'CHECK'">支票</span>
<span v-else>其他</span>
</template>
</el-table-column>
<el-table-column prop="receiptVoucher" label="凭证号" min-width="140" />
<el-table-column prop="receiptNote" label="收款说明" min-width="180" show-overflow-tooltip />
<el-table-column prop="createTime" label="登记时间" width="160" />
</el-table>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Search, Refresh, Plus, Edit, Delete, View, Money } from '@element-plus/icons-vue'
import {
getReceivableList,
createReceivable,
updateReceivable,
deleteReceivable,
recordReceipt,
getReceiptRecords
} from '@/api/receivable'
import { getProjectList } from '@/api/project'
import { getCustomerList } from '@/api/customer'
const loading = ref(false)
const submitLoading = ref(false)
const receiptLoading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const projectList = ref<any[]>([])
const customerList = ref<any[]>([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
receivableCode: '',
projectName: '',
receiptStatus: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增应收款')
const formRef = ref<FormInstance>()
const form = reactive({
receivableId: null as number | null,
receivableCode: '',
projectId: null as number | null,
customerId: null as number | null,
totalAmount: 0,
dueDate: '',
receiptStatus: 'UNPAID',
description: '',
remarks: ''
})
const rules = reactive<FormRules>({
receivableCode: [{ required: true, message: '请输入应收款编号', trigger: 'blur' }],
projectId: [{ required: true, message: '请选择关联项目', trigger: 'change' }],
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
totalAmount: [{ required: true, message: '请输入应收金额', trigger: 'blur' }],
dueDate: [{ required: true, message: '请选择应收日期', trigger: 'change' }]
})
const receiptVisible = ref(false)
const receiptData = ref<any>({})
const receiptFormRef = ref<FormInstance>()
const receiptForm = reactive({
receivableId: null as number | null,
receiptAmount: 0,
receiptDate: '',
receiptMethod: 'BANK_TRANSFER',
receiptVoucher: '',
receiptNote: ''
})
const receiptRules = reactive<FormRules>({
receiptAmount: [{ required: true, message: '请输入收款金额', trigger: 'blur' }],
receiptDate: [{ required: true, message: '请选择收款日期', trigger: 'change' }],
receiptMethod: [{ required: true, message: '请选择收款方式', trigger: 'change' }]
})
const detailVisible = ref(false)
const detailData = ref<any>({})
const receiptRecords = ref<any[]>([])
const fetchData = async () => {
loading.value = true
try {
const res: any = await getReceivableList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const fetchProjects = async () => {
try {
const res: any = await getProjectList({ pageNum: 1, pageSize: 1000 })
projectList.value = res.data?.records || []
} catch (e) {
console.error(e)
}
}
const fetchCustomers = async () => {
try {
const res: any = await getCustomerList({ pageNum: 1, pageSize: 1000 })
customerList.value = res.data?.records || []
} catch (e) {
console.error(e)
}
}
const handleProjectChange = (projectId: number) => {
const project = projectList.value.find(p => p.projectId === projectId)
if (project && project.customerId) {
form.customerId = project.customerId
}
}
const handleSearch = () => {
queryParams.pageNum = 1
fetchData()
}
const handleReset = () => {
queryParams.receivableCode = ''
queryParams.projectName = ''
queryParams.receiptStatus = ''
queryParams.pageNum = 1
fetchData()
}
const handleAdd = () => {
dialogTitle.value = '新增应收款'
resetForm()
dialogVisible.value = true
}
const handleEdit = (row: any) => {
dialogTitle.value = '编辑应收款'
Object.assign(form, row)
dialogVisible.value = true
}
const handleView = async (row: any) => {
detailData.value = row
try {
const res: any = await getReceiptRecords(row.receivableId)
receiptRecords.value = res.data || []
} catch (e) {
console.error(e)
receiptRecords.value = []
}
detailVisible.value = true
}
const handleReceipt = (row: any) => {
receiptData.value = row
receiptForm.receivableId = row.receivableId
receiptForm.receiptAmount = 0
receiptForm.receiptDate = ''
receiptForm.receiptMethod = 'BANK_TRANSFER'
receiptForm.receiptVoucher = ''
receiptForm.receiptNote = ''
receiptVisible.value = true
}
const handleConfirmReceipt = async () => {
if (!receiptFormRef.value) return
await receiptFormRef.value.validate(async (valid) => {
if (!valid) return
if (receiptForm.receiptAmount > receiptData.value.remainingAmount) {
ElMessage.warning('收款金额不能大于未收金额')
return
}
receiptLoading.value = true
try {
await recordReceipt(receiptForm.receivableId!, receiptForm)
ElMessage.success('收款登记成功')
receiptVisible.value = false
fetchData()
} catch (e) {
console.error(e)
} finally {
receiptLoading.value = false
}
})
}
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该应收款吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteReceivable(row.receivableId)
ElMessage.success('删除成功')
fetchData()
} catch (e) {
console.error(e)
}
})
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (form.receivableId) {
await updateReceivable(form.receivableId, form)
ElMessage.success('更新成功')
} else {
await createReceivable(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
} catch (e) {
console.error(e)
} finally {
submitLoading.value = false
}
})
}
const resetForm = () => {
form.receivableId = null
form.receivableCode = ''
form.projectId = null
form.customerId = null
form.totalAmount = 0
form.dueDate = ''
form.receiptStatus = 'UNPAID'
form.description = ''
form.remarks = ''
formRef.value?.clearValidate()
}
onMounted(() => {
fetchData()
fetchProjects()
fetchCustomers()
})
</script>
<style scoped lang="scss">
.receivable-container {
padding: 20px;
.search-card {
:deep(.el-card__body) {
padding-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<div class="page-container">
<el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">新增部门</el-button>
<el-button @click="handleExpand">展开/折叠</el-button>
</div>
<el-table
:data="tableData"
v-loading="loading"
row-key="deptId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="expandAll"
>
<el-table-column prop="deptName" label="部门名称" width="200" />
<el-table-column prop="deptCode" label="部门编码" />
<el-table-column prop="leader" label="负责人" />
<el-table-column prop="leaderPhone" label="联系电话" />
<el-table-column prop="sortOrder" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleAdd(row)">新增</el-button>
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="上级部门">
<el-tree-select
v-model="form.parentId"
:data="deptOptions"
:props="{ label: 'deptName', value: 'deptId' }"
placeholder="请选择上级部门"
check-strictly
/>
</el-form-item>
<el-form-item label="部门名称" prop="deptName">
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
</el-form-item>
<el-form-item label="部门编码">
<el-input v-model="form.deptCode" placeholder="请输入部门编码" />
</el-form-item>
<el-form-item label="负责人">
<el-input v-model="form.leader" placeholder="请输入负责人" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.leaderPhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sortOrder" :min="0" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { getDeptList, getDeptTree, createDept, updateDept, deleteDept } from '@/api/dept'
const loading = ref(false)
const tableData = ref<any[]>([])
const deptOptions = ref<any[]>([])
const dialogVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const expandAll = ref(true)
const formRef = ref<FormInstance>()
const form = reactive({
deptId: undefined as number | undefined,
parentId: 0,
deptName: '',
deptCode: '',
leader: '',
leaderPhone: '',
sortOrder: 0,
status: 1
})
const rules: FormRules = {
deptName: [{ required: true, message: '请输入部门名称', trigger: 'blur' }]
}
const fetchData = async () => {
loading.value = true
try {
const res: any = await getDeptList()
tableData.value = res.data || []
} catch (e) { console.error(e) } finally { loading.value = false }
}
const fetchDeptTree = async () => {
try {
const res: any = await getDeptTree()
deptOptions.value = [{ deptId: 0, deptName: '顶级部门', children: res.data || [] }]
} catch (e) { console.error(e) }
}
const handleExpand = () => { expandAll.value = !expandAll.value }
const handleAdd = async (row?: any) => {
await fetchDeptTree()
Object.assign(form, {
deptId: undefined,
parentId: row?.deptId || 0,
deptName: '',
deptCode: '',
leader: '',
leaderPhone: '',
sortOrder: 0,
status: 1
})
isEdit.value = false
dialogTitle.value = '新增部门'
dialogVisible.value = true
}
const handleEdit = async (row: any) => {
await fetchDeptTree()
Object.assign(form, row)
isEdit.value = true
dialogTitle.value = '编辑部门'
dialogVisible.value = true
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm('确定要删除该部门吗?', '提示', { type: 'warning' })
await deleteDept(row.deptId)
ElMessage.success('删除成功')
fetchData()
} catch (e) { }
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
try {
if (isEdit.value) await updateDept(form)
else await createDept(form)
ElMessage.success('保存成功')
dialogVisible.value = false
fetchData()
} catch (e) { console.error(e) }
}
onMounted(() => { fetchData() })
</script>
<style scoped>
.page-container { padding: 0; }
.toolbar { margin-bottom: 15px; }
</style>

View File

@ -0,0 +1,202 @@
<template>
<div class="page-container">
<el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">新增菜单</el-button>
<el-button @click="handleExpand">展开/折叠</el-button>
</div>
<el-table
:data="tableData"
v-loading="loading"
row-key="menuId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="expandAll"
>
<el-table-column prop="menuName" label="菜单名称" width="180" />
<el-table-column prop="icon" label="图标" width="80">
<template #default="{ row }">
<el-icon v-if="row.icon"><component :is="row.icon" /></el-icon>
</template>
</el-table-column>
<el-table-column prop="menuType" label="类型" width="80">
<template #default="{ row }">
<el-tag v-if="row.menuType === 'M'" type="success">目录</el-tag>
<el-tag v-else-if="row.menuType === 'C'" type="primary">菜单</el-tag>
<el-tag v-else type="info">按钮</el-tag>
</template>
</el-table-column>
<el-table-column prop="path" label="路由地址" />
<el-table-column prop="perms" label="权限标识" />
<el-table-column prop="sortOrder" label="排序" width="80" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleAdd(row)">新增</el-button>
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="上级菜单">
<el-tree-select
v-model="form.parentId"
:data="menuOptions"
:props="{ label: 'menuName', value: 'menuId' }"
placeholder="请选择上级菜单"
check-strictly
/>
</el-form-item>
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio value="M">目录</el-radio>
<el-radio value="C">菜单</el-radio>
<el-radio value="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="图标" v-if="form.menuType !== 'F'">
<el-input v-model="form.icon" placeholder="请输入图标名称" />
</el-form-item>
<el-form-item label="路由地址" v-if="form.menuType !== 'F'">
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
<el-form-item label="组件路径" v-if="form.menuType === 'C'">
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
<el-form-item label="权限标识">
<el-input v-model="form.perms" placeholder="请输入权限标识" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sortOrder" :min="0" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { getMenuList, getMenuTree, createMenu, updateMenu, deleteMenu } from '@/api/menu'
const loading = ref(false)
const tableData = ref<any[]>([])
const menuOptions = ref<any[]>([])
const dialogVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const expandAll = ref(true)
const formRef = ref<FormInstance>()
const form = reactive({
menuId: undefined as number | undefined,
parentId: 0,
menuName: '',
menuType: 'M',
icon: '',
path: '',
component: '',
perms: '',
sortOrder: 0,
status: 1
})
const rules: FormRules = {
menuName: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
menuType: [{ required: true, message: '请选择菜单类型', trigger: 'change' }]
}
const fetchData = async () => {
loading.value = true
try {
const res: any = await getMenuList()
tableData.value = res.data || []
} catch (e) { console.error(e) } finally { loading.value = false }
}
const fetchMenuTree = async () => {
try {
const res: any = await getMenuTree()
menuOptions.value = [{ menuId: 0, menuName: '顶级菜单', children: res.data || [] }]
} catch (e) { console.error(e) }
}
const handleExpand = () => { expandAll.value = !expandAll.value }
const handleAdd = async (row?: any) => {
await fetchMenuTree()
Object.assign(form, {
menuId: undefined,
parentId: row?.menuId || 0,
menuName: '',
menuType: 'M',
icon: '',
path: '',
component: '',
perms: '',
sortOrder: 0,
status: 1
})
isEdit.value = false
dialogTitle.value = '新增菜单'
dialogVisible.value = true
}
const handleEdit = async (row: any) => {
await fetchMenuTree()
Object.assign(form, row)
isEdit.value = true
dialogTitle.value = '编辑菜单'
dialogVisible.value = true
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm('确定要删除该菜单吗?', '提示', { type: 'warning' })
await deleteMenu(row.menuId)
ElMessage.success('删除成功')
fetchData()
} catch (e) { }
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
try {
if (isEdit.value) await updateMenu(form)
else await createMenu(form)
ElMessage.success('保存成功')
dialogVisible.value = false
fetchData()
} catch (e) { console.error(e) }
}
onMounted(() => { fetchData() })
</script>
<style scoped>
.page-container { padding: 0; }
.toolbar { margin-bottom: 15px; }
</style>

View File

@ -0,0 +1,200 @@
<template>
<div class="page-container">
<el-card>
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="角色名称">
<el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">新增角色</el-button>
</div>
<el-table :data="tableData" v-loading="loading" stripe>
<el-table-column prop="roleId" label="ID" width="80" />
<el-table-column prop="roleName" label="角色名称" />
<el-table-column prop="roleKey" label="角色标识" />
<el-table-column prop="roleSort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="success" @click="handlePermission(row)">权限</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
class="pagination"
@size-change="handleSearch"
@current-change="handleSearch"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="角色标识" prop="roleKey">
<el-input v-model="form.roleKey" placeholder="请输入角色标识" />
</el-form-item>
<el-form-item label="排序" prop="roleSort">
<el-input-number v-model="form.roleSort" :min="0" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 分配权限对话框 -->
<el-dialog v-model="permissionVisible" title="分配权限" width="400px">
<el-tree
ref="treeRef"
:data="menuTree"
show-checkbox
node-key="menuId"
:props="{ label: 'menuName', children: 'children' }"
default-expand-all
/>
<template #footer>
<el-button @click="permissionVisible = false">取消</el-button>
<el-button type="primary" @click="handleSavePermission">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { getRoleList, createRole, updateRole, deleteRole, getRoleMenus, assignMenus } from '@/api/role'
import { getMenuTree } from '@/api/menu'
const loading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const permissionVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const treeRef = ref()
const menuTree = ref<any[]>([])
const currentRole = ref<any>(null)
const queryParams = reactive({ pageNum: 1, pageSize: 10, roleName: '' })
const form = reactive({ roleId: undefined as number | undefined, roleName: '', roleKey: '', roleSort: 0, status: 1, remark: '' })
const rules: FormRules = {
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
roleKey: [{ required: true, message: '请输入角色标识', trigger: 'blur' }]
}
const fetchData = async () => {
loading.value = true
try {
const res: any = await getRoleList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) { console.error(e) } finally { loading.value = false }
}
const fetchMenuTree = async () => {
try {
const res: any = await getMenuTree()
menuTree.value = res.data || []
} catch (e) { console.error(e) }
}
const handleSearch = () => { queryParams.pageNum = 1; fetchData() }
const handleReset = () => { queryParams.roleName = ''; handleSearch() }
const handleAdd = () => {
Object.assign(form, { roleId: undefined, roleName: '', roleKey: '', roleSort: 0, status: 1, remark: '' })
isEdit.value = false
dialogTitle.value = '新增角色'
dialogVisible.value = true
}
const handleEdit = (row: any) => {
Object.assign(form, row)
isEdit.value = true
dialogTitle.value = '编辑角色'
dialogVisible.value = true
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm('确定要删除该角色吗?', '提示', { type: 'warning' })
await deleteRole(row.roleId)
ElMessage.success('删除成功')
fetchData()
} catch (e) { }
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
try {
if (isEdit.value) await updateRole(form)
else await createRole(form)
ElMessage.success('保存成功')
dialogVisible.value = false
fetchData()
} catch (e) { console.error(e) }
}
const handlePermission = async (row: any) => {
currentRole.value = row
await fetchMenuTree()
try {
const res: any = await getRoleMenus(row.roleId)
const menuIds = res.data || []
treeRef.value?.setCheckedKeys(menuIds, false)
} catch (e) { console.error(e) }
permissionVisible.value = true
}
const handleSavePermission = async () => {
try {
const checkedKeys = treeRef.value?.getCheckedKeys() || []
const halfCheckedKeys = treeRef.value?.getHalfCheckedKeys() || []
const menuIds = [...checkedKeys, ...halfCheckedKeys]
await assignMenus(currentRole.value.roleId, menuIds)
ElMessage.success('权限分配成功')
permissionVisible.value = false
} catch (e) { console.error(e) }
}
onMounted(() => { fetchData() })
</script>
<style scoped>
.page-container { padding: 0; }
.search-form { margin-bottom: 15px; }
.toolbar { margin-bottom: 15px; }
.pagination { margin-top: 15px; justify-content: flex-end; }
</style>

View File

@ -0,0 +1,137 @@
<template>
<div class="page-container">
<el-card>
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="用户名">
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">新增用户</el-button>
</div>
<el-table :data="tableData" v-loading="loading" stripe>
<el-table-column prop="userId" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="realName" label="姓名" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="deptName" label="部门" />
<el-table-column prop="role" label="角色">
<template #default="{ row }">
<el-tag v-if="row.role === 'admin'" type="danger">管理员</el-tag>
<el-tag v-else-if="row.role === 'finance'" type="warning">财务</el-tag>
<el-tag v-else-if="row.role === 'pm'" type="success">项目经理</el-tag>
<el-tag v-else>普通用户</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
class="pagination"
/>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" :disabled="isEdit" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input v-model="form.realName" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="!isEdit">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择角色">
<el-option label="管理员" value="admin" />
<el-option label="财务" value="finance" />
<el-option label="项目经理" value="pm" />
<el-option label="普通用户" value="user" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import { getUserList, createUser, updateUser, deleteUser } from '@/api/user'
const loading = ref(false)
const tableData = ref<any[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const queryParams = reactive({ pageNum: 1, pageSize: 10, username: '', status: undefined as number | undefined })
const form = reactive({ userId: undefined as number | undefined, username: '', password: '', realName: '', phone: '', role: 'user', status: 1 })
const rules: FormRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }]
}
const fetchData = async () => {
loading.value = true
try {
const res: any = await getUserList(queryParams)
tableData.value = res.data?.records || []
total.value = res.data?.total || 0
} catch (e) { console.error(e) }
finally { loading.value = false }
}
const handleSearch = () => { queryParams.pageNum = 1; fetchData() }
const handleReset = () => { queryParams.username = ''; queryParams.status = undefined; handleSearch() }
const handleAdd = () => { Object.assign(form, { userId: undefined, username: '', password: '', realName: '', phone: '', role: 'user', status: 1 }); isEdit.value = false; dialogTitle.value = '新增用户'; dialogVisible.value = true }
const handleEdit = (row: any) => { Object.assign(form, row); isEdit.value = true; dialogTitle.value = '编辑用户'; dialogVisible.value = true }
const handleDelete = async (row: any) => { await deleteUser(row.userId); ElMessage.success('删除成功'); fetchData() }
const handleSubmit = async () => { if (!formRef.value) return; await formRef.value.validate(); if (isEdit.value) await updateUser(form); else await createUser(form); ElMessage.success('保存成功'); dialogVisible.value = false; fetchData() }
onMounted(() => { fetchData() })
</script>
<style scoped>
.page-container { padding: 0; }
.search-form { margin-bottom: 15px; }
.toolbar { margin-bottom: 15px; }
.pagination { margin-top: 15px; justify-content: flex-end; }
</style>

View File

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

7
fund-admin/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

46
fund-admin/vite.config.ts Normal file
View File

@ -0,0 +1,46 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 3000,
proxy: {
'/sys': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/cust': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/proj': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/req': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/exp': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/receipt': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/file': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})

View File

@ -1,5 +1,7 @@
package com.fundplatform.exp.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.core.PageResult;
import com.fundplatform.common.core.Result;
import com.fundplatform.exp.dto.ExpenseTypeDTO;
import com.fundplatform.exp.service.ExpenseTypeService;
@ -39,11 +41,22 @@ public class ExpenseTypeController {
}
/**
* 根据ID查询支出类型
* 分页查询支出类型
*/
@GetMapping("/{id}")
public Result<ExpenseTypeVO> getById(@PathVariable Long id) {
return Result.success(typeService.getTypeById(id));
@GetMapping("/page")
public Result<PageResult<ExpenseTypeVO>> pageTypes(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) String typeName,
@RequestParam(required = false) Integer status) {
Page<ExpenseTypeVO> page = typeService.pageTypes(pageNum, pageSize, typeName, status);
PageResult<ExpenseTypeVO> pageResult = new PageResult<>(
page.getCurrent(),
page.getSize(),
page.getTotal(),
page.getRecords()
);
return Result.success(pageResult);
}
/**
@ -54,6 +67,14 @@ public class ExpenseTypeController {
return Result.success(typeService.getTypeTree());
}
/**
* 根据ID查询支出类型
*/
@GetMapping("/{id}")
public Result<ExpenseTypeVO> getById(@PathVariable Long id) {
return Result.success(typeService.getTypeById(id));
}
/**
* 查询指定父级的子类型列表
*/

View File

@ -8,6 +8,15 @@ import com.fundplatform.exp.vo.FundExpenseVO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
/**
* 支出管理Controller
*
* 支出审批流程
* 1. 创建支出 -> 待审批(0)
* 2. 提交审批 -> 审批中(1)
* 3. 审批通过 -> 审批通过(2) -> 可确认支付
* 4. 审批拒绝 -> 审批拒绝(3) -> 可重新提交
*/
@RestController
@RequestMapping("/api/v1/exp/expense")
public class FundExpenseController {
@ -18,48 +27,106 @@ public class FundExpenseController {
this.expenseService = expenseService;
}
/**
* 创建支出申请
*/
@PostMapping
public Result<Long> create(@Valid @RequestBody FundExpenseDTO dto) {
return Result.success(expenseService.createExpense(dto));
}
/**
* 更新支出申请仅待审批状态可修改
*/
@PutMapping
public Result<Boolean> update(@Valid @RequestBody FundExpenseDTO dto) {
return Result.success(expenseService.updateExpense(dto));
}
/**
* 根据ID查询支出详情
*/
@GetMapping("/{id}")
public Result<FundExpenseVO> getById(@PathVariable Long id) {
return Result.success(expenseService.getExpenseById(id));
}
/**
* 分页查询支出列表
*/
@GetMapping("/page")
public Result<Page<FundExpenseVO>> page(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) String title,
@RequestParam(required = false) Integer expenseType,
@RequestParam(required = false) Integer payStatus) {
return Result.success(expenseService.pageExpenses(pageNum, pageSize, title, expenseType, payStatus));
@RequestParam(required = false) Integer payStatus,
@RequestParam(required = false) Integer approvalStatus) {
return Result.success(expenseService.pageExpenses(pageNum, pageSize, title, expenseType, payStatus, approvalStatus));
}
/**
* 删除支出申请仅待审批状态可删除
*/
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.success(expenseService.deleteExpense(id));
}
/**
* 提交审批待审批 -> 审批中
*/
@PostMapping("/{id}/submit")
public Result<Boolean> submitApproval(@PathVariable Long id) {
return Result.success(expenseService.submitApproval(id));
}
/**
* 撤回审批审批中 -> 待审批
*/
@PostMapping("/{id}/withdraw")
public Result<Boolean> withdrawApproval(@PathVariable Long id) {
return Result.success(expenseService.withdrawApproval(id));
}
/**
* 审批通过审批中 -> 审批通过
*/
@PutMapping("/{id}/approve")
public Result<Boolean> approve(@PathVariable Long id, @RequestParam(required = false) String comment) {
return Result.success(expenseService.approve(id, comment));
public Result<Boolean> approve(
@PathVariable Long id,
@RequestParam(required = false) String comment,
@RequestHeader(value = "X-User-Id", required = false) Long approverId) {
return Result.success(expenseService.approve(id, approverId, comment));
}
/**
* 审批拒绝审批中 -> 审批拒绝
*/
@PutMapping("/{id}/reject")
public Result<Boolean> reject(@PathVariable Long id, @RequestParam(required = false) String comment) {
return Result.success(expenseService.reject(id, comment));
public Result<Boolean> reject(
@PathVariable Long id,
@RequestParam(required = false) String comment,
@RequestHeader(value = "X-User-Id", required = false) Long approverId) {
return Result.success(expenseService.reject(id, approverId, comment));
}
/**
* 确认支付审批通过后才能支付
*/
@PutMapping("/{id}/confirm-pay")
public Result<Boolean> confirmPay(@PathVariable Long id, @RequestParam String payChannel, @RequestParam(required = false) String payVoucher) {
public Result<Boolean> confirmPay(
@PathVariable Long id,
@RequestParam String payChannel,
@RequestParam(required = false) String payVoucher) {
return Result.success(expenseService.confirmPay(id, payChannel, payVoucher));
}
/**
* 标记支付失败
*/
@PutMapping("/{id}/pay-failed")
public Result<Boolean> payFailed(@PathVariable Long id, @RequestParam String reason) {
return Result.success(expenseService.payFailed(id, reason));
}
}

View File

@ -1,5 +1,6 @@
package com.fundplatform.exp.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.exp.dto.ExpenseTypeDTO;
import com.fundplatform.exp.vo.ExpenseTypeVO;
@ -7,6 +8,11 @@ import java.util.List;
public interface ExpenseTypeService {
/**
* 分页查询支出类型
*/
Page<ExpenseTypeVO> pageTypes(int pageNum, int pageSize, String typeName, Integer status);
/**
* 创建支出类型
*/

View File

@ -12,13 +12,37 @@ public interface FundExpenseService {
FundExpenseVO getExpenseById(Long id);
Page<FundExpenseVO> pageExpenses(int pageNum, int pageSize, String title, Integer expenseType, Integer payStatus);
Page<FundExpenseVO> pageExpenses(int pageNum, int pageSize, String title, Integer expenseType, Integer payStatus, Integer approvalStatus);
boolean deleteExpense(Long id);
boolean approve(Long id, String comment);
/**
* 提交审批待审批 -> 审批中
*/
boolean submitApproval(Long id);
boolean reject(Long id, String comment);
/**
* 审批通过审批中 -> 审批通过
*/
boolean approve(Long id, Long approverId, String comment);
/**
* 审批拒绝审批中 -> 审批拒绝
*/
boolean reject(Long id, Long approverId, String comment);
/**
* 撤回审批审批中 -> 待审批
*/
boolean withdrawApproval(Long id);
/**
* 确认支付审批通过后才能支付
*/
boolean confirmPay(Long id, String payChannel, String payVoucher);
/**
* 支付失败
*/
boolean payFailed(Long id, String reason);
}

View File

@ -2,6 +2,7 @@ package com.fundplatform.exp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.context.TenantContextHolder;
import com.fundplatform.common.context.UserContextHolder;
import com.fundplatform.exp.data.entity.ExpenseType;
@ -31,6 +32,24 @@ public class ExpenseTypeServiceImpl implements ExpenseTypeService {
this.typeDataService = typeDataService;
}
@Override
public Page<ExpenseTypeVO> pageTypes(int pageNum, int pageSize, String typeName, Integer status) {
LambdaQueryWrapper<ExpenseType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ExpenseType::getDeleted, 0);
if (typeName != null && !typeName.isEmpty()) {
wrapper.like(ExpenseType::getTypeName, typeName);
}
if (status != null) {
wrapper.eq(ExpenseType::getStatus, status);
}
wrapper.orderByAsc(ExpenseType::getSortOrder);
Page<ExpenseType> page = typeDataService.page(new Page<>(pageNum, pageSize), wrapper);
Page<ExpenseTypeVO> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
voPage.setRecords(page.getRecords().stream().map(this::convertToVO).collect(Collectors.toList()));
return voPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createType(ExpenseTypeDTO dto) {

View File

@ -3,6 +3,8 @@ package com.fundplatform.exp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.context.TenantContextHolder;
import com.fundplatform.common.context.UserContextHolder;
import com.fundplatform.exp.data.entity.FundExpense;
import com.fundplatform.exp.data.service.FundExpenseDataService;
import com.fundplatform.exp.dto.FundExpenseDTO;
@ -25,6 +27,22 @@ public class FundExpenseServiceImpl implements FundExpenseService {
private static final Logger log = LoggerFactory.getLogger(FundExpenseServiceImpl.class);
private static final AtomicInteger counter = new AtomicInteger(1);
/** 审批状态: 待审批 */
private static final int APPROVAL_PENDING = 0;
/** 审批状态: 审批中 */
private static final int APPROVAL_IN_PROGRESS = 1;
/** 审批状态: 审批通过 */
private static final int APPROVAL_APPROVED = 2;
/** 审批状态: 审批拒绝 */
private static final int APPROVAL_REJECTED = 3;
/** 支付状态: 待支付 */
private static final int PAY_PENDING = 0;
/** 支付状态: 已支付 */
private static final int PAY_PAID = 1;
/** 支付状态: 支付失败 */
private static final int PAY_FAILED = 2;
private final FundExpenseDataService expenseDataService;
public FundExpenseServiceImpl(FundExpenseDataService expenseDataService) {
@ -48,12 +66,18 @@ public class FundExpenseServiceImpl implements FundExpenseService {
expense.setRequestId(dto.getRequestId());
expense.setProjectId(dto.getProjectId());
expense.setCustomerId(dto.getCustomerId());
expense.setPayStatus(0);
expense.setApprovalStatus(0);
expense.setPayStatus(PAY_PENDING);
expense.setApprovalStatus(APPROVAL_PENDING);
expense.setAttachments(dto.getAttachments());
expense.setDeleted(0);
expense.setCreatedTime(LocalDateTime.now());
// 获取租户ID和用户ID如果没有则使用默认值
Long tenantId = TenantContextHolder.getTenantId();
expense.setTenantId(tenantId != null ? tenantId : 1L);
Long userId = UserContextHolder.getUserId();
expense.setCreatedBy(userId != null ? userId : 1L);
expenseDataService.save(expense);
log.info("创建支出记录成功: id={}, expenseNo={}", expense.getId(), expense.getExpenseNo());
return expense.getId();
@ -65,7 +89,11 @@ public class FundExpenseServiceImpl implements FundExpenseService {
if (dto.getId() == null) throw new RuntimeException("支出ID不能为空");
FundExpense existing = expenseDataService.getById(dto.getId());
if (existing == null || existing.getDeleted() == 1) throw new RuntimeException("支出记录不存在");
if (existing.getPayStatus() != 0) throw new RuntimeException("当前状态不允许修改");
// 只有待审批状态才能修改
if (existing.getApprovalStatus() != APPROVAL_PENDING) {
throw new RuntimeException("当前审批状态不允许修改,只有待审批状态才能修改");
}
FundExpense expense = new FundExpense();
expense.setId(dto.getId());
@ -74,6 +102,7 @@ public class FundExpenseServiceImpl implements FundExpenseService {
expense.setPurpose(dto.getPurpose());
expense.setAttachments(dto.getAttachments());
expense.setUpdatedTime(LocalDateTime.now());
expense.setUpdatedBy(UserContextHolder.getUserId());
return expenseDataService.updateById(expense);
}
@ -85,13 +114,14 @@ public class FundExpenseServiceImpl implements FundExpenseService {
}
@Override
public Page<FundExpenseVO> pageExpenses(int pageNum, int pageSize, String title, Integer expenseType, Integer payStatus) {
public Page<FundExpenseVO> pageExpenses(int pageNum, int pageSize, String title, Integer expenseType, Integer payStatus, Integer approvalStatus) {
Page<FundExpense> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<FundExpense> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FundExpense::getDeleted, 0);
if (StringUtils.hasText(title)) wrapper.like(FundExpense::getTitle, title);
if (expenseType != null) wrapper.eq(FundExpense::getExpenseType, expenseType);
if (payStatus != null) wrapper.eq(FundExpense::getPayStatus, payStatus);
if (approvalStatus != null) wrapper.eq(FundExpense::getApprovalStatus, approvalStatus);
wrapper.orderByDesc(FundExpense::getCreatedTime);
Page<FundExpense> expensePage = expenseDataService.page(page, wrapper);
@ -103,43 +133,184 @@ public class FundExpenseServiceImpl implements FundExpenseService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteExpense(Long id) {
FundExpense existing = expenseDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出记录不存在");
}
// 只有待审批状态才能删除
if (existing.getApprovalStatus() != APPROVAL_PENDING) {
throw new RuntimeException("当前审批状态不允许删除");
}
LambdaUpdateWrapper<FundExpense> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FundExpense::getId, id).set(FundExpense::getDeleted, 1).set(FundExpense::getUpdatedTime, LocalDateTime.now());
wrapper.eq(FundExpense::getId, id)
.set(FundExpense::getDeleted, 1)
.set(FundExpense::getUpdatedTime, LocalDateTime.now())
.set(FundExpense::getUpdatedBy, UserContextHolder.getUserId());
return expenseDataService.update(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean approve(Long id, String comment) {
public boolean submitApproval(Long id) {
FundExpense existing = expenseDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出记录不存在");
}
// 只有待审批状态才能提交
if (existing.getApprovalStatus() != APPROVAL_PENDING) {
throw new RuntimeException("当前状态不允许提交审批");
}
LambdaUpdateWrapper<FundExpense> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FundExpense::getId, id).set(FundExpense::getApprovalStatus, 2)
.set(FundExpense::getApprovalTime, LocalDateTime.now())
.set(FundExpense::getApprovalComment, comment)
.set(FundExpense::getUpdatedTime, LocalDateTime.now());
return expenseDataService.update(wrapper);
wrapper.eq(FundExpense::getId, id)
.set(FundExpense::getApprovalStatus, APPROVAL_IN_PROGRESS)
.set(FundExpense::getUpdatedTime, LocalDateTime.now())
.set(FundExpense::getUpdatedBy, UserContextHolder.getUserId());
boolean result = expenseDataService.update(wrapper);
if (result) {
log.info("支出申请已提交审批: id={}", id);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean reject(Long id, String comment) {
public boolean approve(Long id, Long approverId, String comment) {
FundExpense existing = expenseDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出记录不存在");
}
// 只有审批中状态才能审批通过
if (existing.getApprovalStatus() != APPROVAL_IN_PROGRESS) {
throw new RuntimeException("当前状态不允许审批,只有审批中状态才能审批");
}
LambdaUpdateWrapper<FundExpense> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FundExpense::getId, id).set(FundExpense::getApprovalStatus, 3)
wrapper.eq(FundExpense::getId, id)
.set(FundExpense::getApprovalStatus, APPROVAL_APPROVED)
.set(FundExpense::getApproverId, approverId)
.set(FundExpense::getApprovalTime, LocalDateTime.now())
.set(FundExpense::getApprovalComment, comment)
.set(FundExpense::getUpdatedTime, LocalDateTime.now());
return expenseDataService.update(wrapper);
.set(FundExpense::getUpdatedTime, LocalDateTime.now())
.set(FundExpense::getUpdatedBy, approverId);
boolean result = expenseDataService.update(wrapper);
if (result) {
log.info("支出审批通过: id={}, approverId={}", id, approverId);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean reject(Long id, Long approverId, String comment) {
FundExpense existing = expenseDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出记录不存在");
}
// 只有审批中状态才能拒绝
if (existing.getApprovalStatus() != APPROVAL_IN_PROGRESS) {
throw new RuntimeException("当前状态不允许审批,只有审批中状态才能审批");
}
LambdaUpdateWrapper<FundExpense> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FundExpense::getId, id)
.set(FundExpense::getApprovalStatus, APPROVAL_REJECTED)
.set(FundExpense::getApproverId, approverId)
.set(FundExpense::getApprovalTime, LocalDateTime.now())
.set(FundExpense::getApprovalComment, comment)
.set(FundExpense::getUpdatedTime, LocalDateTime.now())
.set(FundExpense::getUpdatedBy, approverId);
boolean result = expenseDataService.update(wrapper);
if (result) {
log.info("支出审批拒绝: id={}, approverId={}, reason={}", id, approverId, comment);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean withdrawApproval(Long id) {
FundExpense existing = expenseDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出记录不存在");
}
// 只有审批中状态才能撤回
if (existing.getApprovalStatus() != APPROVAL_IN_PROGRESS) {
throw new RuntimeException("当前状态不允许撤回,只有审批中状态才能撤回");
}
LambdaUpdateWrapper<FundExpense> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FundExpense::getId, id)
.set(FundExpense::getApprovalStatus, APPROVAL_PENDING)
.set(FundExpense::getUpdatedTime, LocalDateTime.now())
.set(FundExpense::getUpdatedBy, UserContextHolder.getUserId());
boolean result = expenseDataService.update(wrapper);
if (result) {
log.info("支出审批已撤回: id={}", id);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmPay(Long id, String payChannel, String payVoucher) {
FundExpense existing = expenseDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出记录不存在");
}
// 只有审批通过且待支付状态才能确认支付
if (existing.getApprovalStatus() != APPROVAL_APPROVED) {
throw new RuntimeException("只有审批通过的支出才能确认支付");
}
if (existing.getPayStatus() != PAY_PENDING) {
throw new RuntimeException("当前支付状态不正确");
}
LambdaUpdateWrapper<FundExpense> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FundExpense::getId, id).set(FundExpense::getPayStatus, 1)
wrapper.eq(FundExpense::getId, id)
.set(FundExpense::getPayStatus, PAY_PAID)
.set(FundExpense::getPayTime, LocalDateTime.now())
.set(FundExpense::getPayChannel, payChannel)
.set(FundExpense::getPayVoucher, payVoucher)
.set(FundExpense::getUpdatedTime, LocalDateTime.now());
return expenseDataService.update(wrapper);
.set(FundExpense::getUpdatedTime, LocalDateTime.now())
.set(FundExpense::getUpdatedBy, UserContextHolder.getUserId());
boolean result = expenseDataService.update(wrapper);
if (result) {
log.info("支出已确认支付: id={}, payChannel={}", id, payChannel);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean payFailed(Long id, String reason) {
FundExpense existing = expenseDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("支出记录不存在");
}
// 只有待支付状态才能标记支付失败
if (existing.getPayStatus() != PAY_PENDING) {
throw new RuntimeException("当前支付状态不正确");
}
LambdaUpdateWrapper<FundExpense> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FundExpense::getId, id)
.set(FundExpense::getPayStatus, PAY_FAILED)
.set(FundExpense::getApprovalComment, existing.getApprovalComment() + "; 支付失败原因: " + reason)
.set(FundExpense::getUpdatedTime, LocalDateTime.now())
.set(FundExpense::getUpdatedBy, UserContextHolder.getUserId());
boolean result = expenseDataService.update(wrapper);
if (result) {
log.info("支出支付失败: id={}, reason={}", id, reason);
}
return result;
}
private String generateExpenseNo() {
@ -172,6 +343,7 @@ public class FundExpenseServiceImpl implements FundExpenseService {
vo.setPayVoucher(e.getPayVoucher());
vo.setApprovalStatus(e.getApprovalStatus());
vo.setApprovalStatusName(getApprovalStatusName(e.getApprovalStatus()));
vo.setApproverId(e.getApproverId());
vo.setApprovalTime(e.getApprovalTime());
vo.setApprovalComment(e.getApprovalComment());
vo.setAttachments(e.getAttachments());
@ -188,11 +360,22 @@ public class FundExpenseServiceImpl implements FundExpenseService {
private String getPayStatusName(Integer status) {
if (status == null) return "";
return switch (status) { case 0 -> "待支付"; case 1 -> "已支付"; case 2 -> "支付失败"; default -> ""; };
return switch (status) {
case PAY_PENDING -> "待支付";
case PAY_PAID -> "已支付";
case PAY_FAILED -> "支付失败";
default -> "";
};
}
private String getApprovalStatusName(Integer status) {
if (status == null) return "";
return switch (status) { case 0 -> "待审批"; case 1 -> "审批中"; case 2 -> "审批通过"; case 3 -> "审批拒绝"; default -> ""; };
return switch (status) {
case APPROVAL_PENDING -> "待审批";
case APPROVAL_IN_PROGRESS -> "审批中";
case APPROVAL_APPROVED -> "审批通过";
case APPROVAL_REJECTED -> "审批拒绝";
default -> "";
};
}
}

View File

@ -41,15 +41,6 @@ public class ProjectController {
return Result.success();
}
/**
* 查询项目详情
*/
@GetMapping("/{id}")
public Result<ProjectVO> getProject(@PathVariable Long id) {
ProjectVO vo = projectService.getProjectById(id);
return Result.success(vo);
}
/**
* 分页查询项目
*/
@ -69,6 +60,15 @@ public class ProjectController {
return Result.success(pageResult);
}
/**
* 查询项目详情
*/
@GetMapping("/{id}")
public Result<ProjectVO> getProject(@PathVariable Long id) {
ProjectVO vo = projectService.getProjectById(id);
return Result.success(vo);
}
/**
* 删除项目
*/

View File

@ -22,16 +22,16 @@ public class RequirementController {
/**
* 分页查询需求工单列表
*/
@GetMapping("/list")
public Result<Page<RequirementVO>> list(
@RequestHeader("X-Tenant-Id") Long tenantId,
@RequestParam(required = false) String requirementName,
@RequestParam(required = false) String status,
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long customerId,
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size) {
return requirementService.page(tenantId, requirementName, status, projectId, customerId, current, size);
@GetMapping("/page")
public Result<Page<RequirementVO>> page(
@RequestHeader(value = "X-Tenant-Id", required = false) Long tenantId,
@RequestParam(required = false) String requirementTitle,
@RequestParam(required = false) String projectName,
@RequestParam(required = false) String requirementStatus,
@RequestParam(required = false) String priority,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return requirementService.page(tenantId, requirementTitle, requirementStatus, null, null, pageNum, pageSize);
}
/**

View File

@ -0,0 +1,112 @@
package com.fundplatform.receipt.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.core.Result;
import com.fundplatform.receipt.dto.ReceivableDTO;
import com.fundplatform.receipt.service.ReceivableService;
import com.fundplatform.receipt.vo.ReceivableVO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
/**
* 应收款管理Controller
*
* 应收款流程
* 1. 创建应收款 -> 待确认(0)
* 2. 确认应收款 -> 已确认(1)
* 3. 记录收款 -> 更新已收款金额和状态
* 4. 状态流转待收款 -> 部分收款 -> 已收款
*/
@RestController
@RequestMapping("/api/v1/receipt/receivable")
public class ReceivableController {
private final ReceivableService receivableService;
public ReceivableController(ReceivableService receivableService) {
this.receivableService = receivableService;
}
/**
* 创建应收款
*/
@PostMapping
public Result<Long> create(@Valid @RequestBody ReceivableDTO dto) {
return Result.success(receivableService.createReceivable(dto));
}
/**
* 更新应收款仅待确认状态可修改
*/
@PutMapping
public Result<Boolean> update(@Valid @RequestBody ReceivableDTO dto) {
return Result.success(receivableService.updateReceivable(dto));
}
/**
* 根据ID查询应收款
*/
@GetMapping("/{id}")
public Result<ReceivableVO> getById(@PathVariable Long id) {
return Result.success(receivableService.getReceivableById(id));
}
/**
* 分页查询应收款
*/
@GetMapping("/page")
public Result<Page<ReceivableVO>> page(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long customerId,
@RequestParam(required = false) String status,
@RequestParam(required = false) Integer confirmStatus) {
return Result.success(receivableService.pageReceivables(pageNum, pageSize, projectId, customerId, status, confirmStatus));
}
/**
* 确认应收款
*/
@PutMapping("/{id}/confirm")
public Result<Boolean> confirm(
@PathVariable Long id,
@RequestHeader(value = "X-User-Id", required = false) Long confirmBy) {
return Result.success(receivableService.confirmReceivable(id, confirmBy));
}
/**
* 取消确认
*/
@PutMapping("/{id}/cancel-confirm")
public Result<Boolean> cancelConfirm(@PathVariable Long id) {
return Result.success(receivableService.cancelConfirm(id));
}
/**
* 记录收款
*/
@PostMapping("/{id}/receipt")
public Result<Boolean> recordReceipt(@PathVariable Long id, @RequestParam BigDecimal amount) {
return Result.success(receivableService.recordReceipt(id, amount));
}
/**
* 更新逾期状态
*/
@PostMapping("/update-overdue")
public Result<Boolean> updateOverdue() {
receivableService.updateOverdueStatus();
return Result.success(true);
}
/**
* 删除应收款仅待确认状态可删除
*/
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.success(receivableService.deleteReceivable(id));
}
}

View File

@ -0,0 +1,202 @@
package com.fundplatform.receipt.data.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fundplatform.common.core.BaseEntity;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 应收款实体
*/
@TableName("receivable")
public class Receivable extends BaseEntity {
/** 应收款编号 */
private String receivableCode;
/** 关联需求ID */
private Long requirementId;
/** 关联项目ID */
private Long projectId;
/** 关联客户ID */
private Long customerId;
/** 应收款金额 */
private BigDecimal receivableAmount;
/** 已收款金额 */
private BigDecimal receivedAmount;
/** 未收款金额 */
private BigDecimal unpaidAmount;
/** 应收款日期 */
private LocalDate receivableDate;
/** 付款截止日期 */
private LocalDate paymentDueDate;
/** 收款方式(transfer-转账 cash-现金 check-支票 other-其他) */
private String paymentMethod;
/** 收款账户 */
private String bankAccount;
/** 状态(pending-待收款 partial-部分收款 received-已收款 overdue-逾期) */
private String status;
/** 逾期天数 */
private Integer overdueDays;
/** 确认状态(0-待确认 1-已确认) */
private Integer confirmStatus;
/** 确认时间 */
private LocalDateTime confirmTime;
/** 确认人ID */
private Long confirmBy;
/** 备注 */
private String remark;
public String getReceivableCode() {
return receivableCode;
}
public void setReceivableCode(String receivableCode) {
this.receivableCode = receivableCode;
}
public Long getRequirementId() {
return requirementId;
}
public void setRequirementId(Long requirementId) {
this.requirementId = requirementId;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public BigDecimal getReceivableAmount() {
return receivableAmount;
}
public void setReceivableAmount(BigDecimal receivableAmount) {
this.receivableAmount = receivableAmount;
}
public BigDecimal getReceivedAmount() {
return receivedAmount;
}
public void setReceivedAmount(BigDecimal receivedAmount) {
this.receivedAmount = receivedAmount;
}
public BigDecimal getUnpaidAmount() {
return unpaidAmount;
}
public void setUnpaidAmount(BigDecimal unpaidAmount) {
this.unpaidAmount = unpaidAmount;
}
public LocalDate getReceivableDate() {
return receivableDate;
}
public void setReceivableDate(LocalDate receivableDate) {
this.receivableDate = receivableDate;
}
public LocalDate getPaymentDueDate() {
return paymentDueDate;
}
public void setPaymentDueDate(LocalDate paymentDueDate) {
this.paymentDueDate = paymentDueDate;
}
public String getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
public String getBankAccount() {
return bankAccount;
}
public void setBankAccount(String bankAccount) {
this.bankAccount = bankAccount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Integer getOverdueDays() {
return overdueDays;
}
public void setOverdueDays(Integer overdueDays) {
this.overdueDays = overdueDays;
}
public Integer getConfirmStatus() {
return confirmStatus;
}
public void setConfirmStatus(Integer confirmStatus) {
this.confirmStatus = confirmStatus;
}
public LocalDateTime getConfirmTime() {
return confirmTime;
}
public void setConfirmTime(LocalDateTime confirmTime) {
this.confirmTime = confirmTime;
}
public Long getConfirmBy() {
return confirmBy;
}
public void setConfirmBy(Long confirmBy) {
this.confirmBy = confirmBy;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,9 @@
package com.fundplatform.receipt.data.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fundplatform.receipt.data.entity.Receivable;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ReceivableMapper extends BaseMapper<Receivable> {
}

View File

@ -0,0 +1,11 @@
package com.fundplatform.receipt.data.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fundplatform.receipt.data.entity.Receivable;
import com.fundplatform.receipt.data.mapper.ReceivableMapper;
import org.springframework.stereotype.Service;
@Service
public class ReceivableDataService extends ServiceImpl<ReceivableMapper, Receivable> implements IService<Receivable> {
}

View File

@ -0,0 +1,119 @@
package com.fundplatform.receipt.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 应收款DTO
*/
public class ReceivableDTO {
private Long id;
private Long requirementId;
@NotNull(message = "项目ID不能为空")
private Long projectId;
@NotNull(message = "客户ID不能为空")
private Long customerId;
@NotNull(message = "应收款金额不能为空")
@Positive(message = "应收款金额必须大于0")
private BigDecimal receivableAmount;
@NotNull(message = "应收款日期不能为空")
private LocalDate receivableDate;
private LocalDate paymentDueDate;
private String paymentMethod;
private String bankAccount;
private String remark;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getRequirementId() {
return requirementId;
}
public void setRequirementId(Long requirementId) {
this.requirementId = requirementId;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public BigDecimal getReceivableAmount() {
return receivableAmount;
}
public void setReceivableAmount(BigDecimal receivableAmount) {
this.receivableAmount = receivableAmount;
}
public LocalDate getReceivableDate() {
return receivableDate;
}
public void setReceivableDate(LocalDate receivableDate) {
this.receivableDate = receivableDate;
}
public LocalDate getPaymentDueDate() {
return paymentDueDate;
}
public void setPaymentDueDate(LocalDate paymentDueDate) {
this.paymentDueDate = paymentDueDate;
}
public String getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
public String getBankAccount() {
return bankAccount;
}
public void setBankAccount(String bankAccount) {
this.bankAccount = bankAccount;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,58 @@
package com.fundplatform.receipt.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.receipt.dto.ReceivableDTO;
import com.fundplatform.receipt.vo.ReceivableVO;
import java.math.BigDecimal;
/**
* 应收款服务接口
*/
public interface ReceivableService {
/**
* 创建应收款
*/
Long createReceivable(ReceivableDTO dto);
/**
* 更新应收款
*/
boolean updateReceivable(ReceivableDTO dto);
/**
* 根据ID查询应收款
*/
ReceivableVO getReceivableById(Long id);
/**
* 分页查询应收款
*/
Page<ReceivableVO> pageReceivables(int pageNum, int pageSize, Long projectId, Long customerId, String status, Integer confirmStatus);
/**
* 确认应收款
*/
boolean confirmReceivable(Long id, Long confirmBy);
/**
* 取消确认
*/
boolean cancelConfirm(Long id);
/**
* 记录收款更新已收款金额
*/
boolean recordReceipt(Long id, BigDecimal amount);
/**
* 更新逾期状态
*/
void updateOverdueStatus();
/**
* 删除应收款
*/
boolean deleteReceivable(Long id);
}

View File

@ -0,0 +1,343 @@
package com.fundplatform.receipt.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.context.TenantContextHolder;
import com.fundplatform.common.context.UserContextHolder;
import com.fundplatform.receipt.data.entity.Receivable;
import com.fundplatform.receipt.data.service.ReceivableDataService;
import com.fundplatform.receipt.dto.ReceivableDTO;
import com.fundplatform.receipt.service.ReceivableService;
import com.fundplatform.receipt.vo.ReceivableVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class ReceivableServiceImpl implements ReceivableService {
private static final Logger log = LoggerFactory.getLogger(ReceivableServiceImpl.class);
private static final AtomicInteger counter = new AtomicInteger(1);
/** 状态: 待收款 */
private static final String STATUS_PENDING = "pending";
/** 状态: 部分收款 */
private static final String STATUS_PARTIAL = "partial";
/** 状态: 已收款 */
private static final String STATUS_RECEIVED = "received";
/** 状态: 逾期 */
private static final String STATUS_OVERDUE = "overdue";
/** 确认状态: 待确认 */
private static final int CONFIRM_PENDING = 0;
/** 确认状态: 已确认 */
private static final int CONFIRM_CONFIRMED = 1;
private final ReceivableDataService receivableDataService;
public ReceivableServiceImpl(ReceivableDataService receivableDataService) {
this.receivableDataService = receivableDataService;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long createReceivable(ReceivableDTO dto) {
Receivable receivable = new Receivable();
receivable.setReceivableCode(generateReceivableCode());
receivable.setRequirementId(dto.getRequirementId());
receivable.setProjectId(dto.getProjectId());
receivable.setCustomerId(dto.getCustomerId());
receivable.setReceivableAmount(dto.getReceivableAmount());
receivable.setReceivedAmount(BigDecimal.ZERO);
receivable.setUnpaidAmount(dto.getReceivableAmount());
receivable.setReceivableDate(dto.getReceivableDate());
receivable.setPaymentDueDate(dto.getPaymentDueDate());
receivable.setPaymentMethod(dto.getPaymentMethod());
receivable.setBankAccount(dto.getBankAccount());
receivable.setStatus(STATUS_PENDING);
receivable.setOverdueDays(0);
receivable.setConfirmStatus(CONFIRM_PENDING);
receivable.setDeleted(0);
receivable.setCreatedTime(LocalDateTime.now());
receivable.setTenantId(TenantContextHolder.getTenantId());
receivable.setCreatedBy(UserContextHolder.getUserId());
receivable.setRemark(dto.getRemark());
receivableDataService.save(receivable);
log.info("创建应收款成功: id={}, code={}, amount={}", receivable.getId(), receivable.getReceivableCode(), receivable.getReceivableAmount());
return receivable.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateReceivable(ReceivableDTO dto) {
if (dto.getId() == null) {
throw new RuntimeException("应收款ID不能为空");
}
Receivable existing = receivableDataService.getById(dto.getId());
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("应收款不存在");
}
// 只有待确认状态才能修改
if (existing.getConfirmStatus() != CONFIRM_PENDING) {
throw new RuntimeException("已确认的应收款不能修改");
}
Receivable receivable = new Receivable();
receivable.setId(dto.getId());
receivable.setReceivableAmount(dto.getReceivableAmount());
receivable.setUnpaidAmount(dto.getReceivableAmount().subtract(existing.getReceivedAmount()));
receivable.setReceivableDate(dto.getReceivableDate());
receivable.setPaymentDueDate(dto.getPaymentDueDate());
receivable.setPaymentMethod(dto.getPaymentMethod());
receivable.setBankAccount(dto.getBankAccount());
receivable.setUpdatedTime(LocalDateTime.now());
receivable.setUpdatedBy(UserContextHolder.getUserId());
receivable.setRemark(dto.getRemark());
return receivableDataService.updateById(receivable);
}
@Override
public ReceivableVO getReceivableById(Long id) {
Receivable receivable = receivableDataService.getById(id);
if (receivable == null || receivable.getDeleted() == 1) {
throw new RuntimeException("应收款不存在");
}
return convertToVO(receivable);
}
@Override
public Page<ReceivableVO> pageReceivables(int pageNum, int pageSize, Long projectId, Long customerId, String status, Integer confirmStatus) {
Page<Receivable> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Receivable> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Receivable::getDeleted, 0);
if (projectId != null) wrapper.eq(Receivable::getProjectId, projectId);
if (customerId != null) wrapper.eq(Receivable::getCustomerId, customerId);
if (status != null && !status.isEmpty()) wrapper.eq(Receivable::getStatus, status);
if (confirmStatus != null) wrapper.eq(Receivable::getConfirmStatus, confirmStatus);
wrapper.orderByDesc(Receivable::getCreatedTime);
Page<Receivable> receivablePage = receivableDataService.page(page, wrapper);
Page<ReceivableVO> voPage = new Page<>(receivablePage.getCurrent(), receivablePage.getSize(), receivablePage.getTotal());
voPage.setRecords(receivablePage.getRecords().stream().map(this::convertToVO).toList());
return voPage;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmReceivable(Long id, Long confirmBy) {
Receivable existing = receivableDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("应收款不存在");
}
if (existing.getConfirmStatus() == CONFIRM_CONFIRMED) {
throw new RuntimeException("应收款已确认");
}
LambdaUpdateWrapper<Receivable> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Receivable::getId, id)
.set(Receivable::getConfirmStatus, CONFIRM_CONFIRMED)
.set(Receivable::getConfirmTime, LocalDateTime.now())
.set(Receivable::getConfirmBy, confirmBy)
.set(Receivable::getUpdatedTime, LocalDateTime.now())
.set(Receivable::getUpdatedBy, confirmBy);
boolean result = receivableDataService.update(wrapper);
if (result) {
log.info("应收款确认成功: id={}, confirmBy={}", id, confirmBy);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelConfirm(Long id) {
Receivable existing = receivableDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("应收款不存在");
}
if (existing.getConfirmStatus() == CONFIRM_PENDING) {
throw new RuntimeException("应收款未确认");
}
// 如果有收款记录不能取消确认
if (existing.getReceivedAmount().compareTo(BigDecimal.ZERO) > 0) {
throw new RuntimeException("已有收款记录,不能取消确认");
}
LambdaUpdateWrapper<Receivable> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Receivable::getId, id)
.set(Receivable::getConfirmStatus, CONFIRM_PENDING)
.set(Receivable::getConfirmTime, null)
.set(Receivable::getConfirmBy, null)
.set(Receivable::getUpdatedTime, LocalDateTime.now());
boolean result = receivableDataService.update(wrapper);
if (result) {
log.info("应收款取消确认: id={}", id);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean recordReceipt(Long id, BigDecimal amount) {
Receivable existing = receivableDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("应收款不存在");
}
if (existing.getConfirmStatus() != CONFIRM_CONFIRMED) {
throw new RuntimeException("应收款未确认,不能记录收款");
}
BigDecimal newReceivedAmount = existing.getReceivedAmount().add(amount);
BigDecimal newUnpaidAmount = existing.getReceivableAmount().subtract(newReceivedAmount);
if (newUnpaidAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("收款金额超过应收金额");
}
// 更新状态
String newStatus;
if (newUnpaidAmount.compareTo(BigDecimal.ZERO) == 0) {
newStatus = STATUS_RECEIVED;
} else if (newReceivedAmount.compareTo(BigDecimal.ZERO) > 0) {
newStatus = STATUS_PARTIAL;
} else {
newStatus = STATUS_PENDING;
}
LambdaUpdateWrapper<Receivable> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Receivable::getId, id)
.set(Receivable::getReceivedAmount, newReceivedAmount)
.set(Receivable::getUnpaidAmount, newUnpaidAmount)
.set(Receivable::getStatus, newStatus)
.set(Receivable::getUpdatedTime, LocalDateTime.now());
boolean result = receivableDataService.update(wrapper);
if (result) {
log.info("记录收款: receivableId={}, amount={}, newStatus={}", id, amount, newStatus);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateOverdueStatus() {
LocalDate today = LocalDate.now();
// 查询所有未逾期且已过截止日期的应收款
LambdaQueryWrapper<Receivable> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Receivable::getDeleted, 0)
.ne(Receivable::getStatus, STATUS_RECEIVED)
.isNotNull(Receivable::getPaymentDueDate)
.lt(Receivable::getPaymentDueDate, today);
receivableDataService.list(wrapper).forEach(r -> {
int overdueDays = (int) java.time.temporal.ChronoUnit.DAYS.between(r.getPaymentDueDate(), today);
LambdaUpdateWrapper<Receivable> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Receivable::getId, r.getId())
.set(Receivable::getStatus, STATUS_OVERDUE)
.set(Receivable::getOverdueDays, overdueDays)
.set(Receivable::getUpdatedTime, LocalDateTime.now());
receivableDataService.update(updateWrapper);
});
log.info("逾期状态更新完成");
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteReceivable(Long id) {
Receivable existing = receivableDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("应收款不存在");
}
// 只有待确认状态才能删除
if (existing.getConfirmStatus() != CONFIRM_PENDING) {
throw new RuntimeException("已确认的应收款不能删除");
}
LambdaUpdateWrapper<Receivable> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Receivable::getId, id)
.set(Receivable::getDeleted, 1)
.set(Receivable::getUpdatedTime, LocalDateTime.now());
return receivableDataService.update(wrapper);
}
private String generateReceivableCode() {
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
int seq = counter.getAndIncrement();
return String.format("REC%s%04d", dateStr, seq);
}
private ReceivableVO convertToVO(Receivable r) {
ReceivableVO vo = new ReceivableVO();
vo.setId(r.getId());
vo.setReceivableCode(r.getReceivableCode());
vo.setRequirementId(r.getRequirementId());
vo.setProjectId(r.getProjectId());
vo.setCustomerId(r.getCustomerId());
vo.setReceivableAmount(r.getReceivableAmount());
vo.setReceivedAmount(r.getReceivedAmount());
vo.setUnpaidAmount(r.getUnpaidAmount());
vo.setReceivableDate(r.getReceivableDate());
vo.setPaymentDueDate(r.getPaymentDueDate());
vo.setPaymentMethod(r.getPaymentMethod());
vo.setPaymentMethodName(getPaymentMethodName(r.getPaymentMethod()));
vo.setBankAccount(r.getBankAccount());
vo.setStatus(r.getStatus());
vo.setStatusName(getStatusName(r.getStatus()));
vo.setOverdueDays(r.getOverdueDays());
vo.setConfirmStatus(r.getConfirmStatus());
vo.setConfirmStatusName(getConfirmStatusName(r.getConfirmStatus()));
vo.setConfirmTime(r.getConfirmTime());
vo.setConfirmBy(r.getConfirmBy());
vo.setTenantId(r.getTenantId());
vo.setCreatedBy(r.getCreatedBy());
vo.setCreatedTime(r.getCreatedTime());
vo.setUpdatedTime(r.getUpdatedTime());
vo.setRemark(r.getRemark());
return vo;
}
private String getPaymentMethodName(String method) {
if (method == null) return "";
return switch (method) {
case "transfer" -> "银行转账";
case "cash" -> "现金";
case "check" -> "支票";
case "other" -> "其他";
default -> "";
};
}
private String getStatusName(String status) {
if (status == null) return "";
return switch (status) {
case STATUS_PENDING -> "待收款";
case STATUS_PARTIAL -> "部分收款";
case STATUS_RECEIVED -> "已收款";
case STATUS_OVERDUE -> "逾期";
default -> "";
};
}
private String getConfirmStatusName(Integer status) {
if (status == null) return "";
return switch (status) {
case CONFIRM_PENDING -> "待确认";
case CONFIRM_CONFIRMED -> "已确认";
default -> "";
};
}
}

View File

@ -0,0 +1,255 @@
package com.fundplatform.receipt.vo;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 应收款VO
*/
public class ReceivableVO {
private Long id;
private String receivableCode;
private Long requirementId;
private Long projectId;
private String projectName;
private Long customerId;
private String customerName;
private BigDecimal receivableAmount;
private BigDecimal receivedAmount;
private BigDecimal unpaidAmount;
private LocalDate receivableDate;
private LocalDate paymentDueDate;
private String paymentMethod;
private String paymentMethodName;
private String bankAccount;
private String status;
private String statusName;
private Integer overdueDays;
private Integer confirmStatus;
private String confirmStatusName;
private LocalDateTime confirmTime;
private Long confirmBy;
private Long tenantId;
private Long createdBy;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
private String remark;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getReceivableCode() {
return receivableCode;
}
public void setReceivableCode(String receivableCode) {
this.receivableCode = receivableCode;
}
public Long getRequirementId() {
return requirementId;
}
public void setRequirementId(Long requirementId) {
this.requirementId = requirementId;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public BigDecimal getReceivableAmount() {
return receivableAmount;
}
public void setReceivableAmount(BigDecimal receivableAmount) {
this.receivableAmount = receivableAmount;
}
public BigDecimal getReceivedAmount() {
return receivedAmount;
}
public void setReceivedAmount(BigDecimal receivedAmount) {
this.receivedAmount = receivedAmount;
}
public BigDecimal getUnpaidAmount() {
return unpaidAmount;
}
public void setUnpaidAmount(BigDecimal unpaidAmount) {
this.unpaidAmount = unpaidAmount;
}
public LocalDate getReceivableDate() {
return receivableDate;
}
public void setReceivableDate(LocalDate receivableDate) {
this.receivableDate = receivableDate;
}
public LocalDate getPaymentDueDate() {
return paymentDueDate;
}
public void setPaymentDueDate(LocalDate paymentDueDate) {
this.paymentDueDate = paymentDueDate;
}
public String getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(String paymentMethod) {
this.paymentMethod = paymentMethod;
}
public String getPaymentMethodName() {
return paymentMethodName;
}
public void setPaymentMethodName(String paymentMethodName) {
this.paymentMethodName = paymentMethodName;
}
public String getBankAccount() {
return bankAccount;
}
public void setBankAccount(String bankAccount) {
this.bankAccount = bankAccount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatusName() {
return statusName;
}
public void setStatusName(String statusName) {
this.statusName = statusName;
}
public Integer getOverdueDays() {
return overdueDays;
}
public void setOverdueDays(Integer overdueDays) {
this.overdueDays = overdueDays;
}
public Integer getConfirmStatus() {
return confirmStatus;
}
public void setConfirmStatus(Integer confirmStatus) {
this.confirmStatus = confirmStatus;
}
public String getConfirmStatusName() {
return confirmStatusName;
}
public void setConfirmStatusName(String confirmStatusName) {
this.confirmStatusName = confirmStatusName;
}
public LocalDateTime getConfirmTime() {
return confirmTime;
}
public void setConfirmTime(LocalDateTime confirmTime) {
this.confirmTime = confirmTime;
}
public Long getConfirmBy() {
return confirmBy;
}
public void setConfirmBy(Long confirmBy) {
this.confirmBy = confirmBy;
}
public Long getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getCreatedTime() {
return createdTime;
}
public void setCreatedTime(LocalDateTime createdTime) {
this.createdTime = createdTime;
}
public LocalDateTime getUpdatedTime() {
return updatedTime;
}
public void setUpdatedTime(LocalDateTime updatedTime) {
this.updatedTime = updatedTime;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -4,9 +4,12 @@ import com.fundplatform.common.core.Result;
import com.fundplatform.sys.dto.LoginRequestDTO;
import com.fundplatform.sys.service.AuthService;
import com.fundplatform.sys.vo.LoginVO;
import com.fundplatform.sys.vo.UserVO;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -31,4 +34,13 @@ public class AuthController {
LoginVO vo = authService.login(request);
return Result.success(vo);
}
/**
* 获取当前用户信息
*/
@GetMapping("/info")
public Result<UserVO> getUserInfo(@RequestHeader("X-User-Id") Long userId) {
UserVO vo = authService.getUserInfo(userId);
return Result.success(vo);
}
}

View File

@ -2,6 +2,7 @@ package com.fundplatform.sys.service;
import com.fundplatform.sys.dto.LoginRequestDTO;
import com.fundplatform.sys.vo.LoginVO;
import com.fundplatform.sys.vo.UserVO;
/**
* 认证服务接口
@ -12,4 +13,9 @@ public interface AuthService {
* 用户登录
*/
LoginVO login(LoginRequestDTO request);
/**
* 获取当前用户信息
*/
UserVO getUserInfo(Long userId);
}

View File

@ -7,6 +7,7 @@ import com.fundplatform.sys.dto.LoginRequestDTO;
import com.fundplatform.sys.service.AuthService;
import com.fundplatform.sys.utils.JwtUtil;
import com.fundplatform.sys.vo.LoginVO;
import com.fundplatform.sys.vo.UserVO;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@ -37,9 +38,13 @@ public class AuthServiceImpl implements AuthService {
}
// 验证密码
// TODO: 使用BCrypt验证
if (!"admin123".equals(request.getPassword())) {
// 临时同时检查BCrypt
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
}
// 检查用户状态
if (user.getStatus() != 1) {
@ -52,4 +57,27 @@ public class AuthServiceImpl implements AuthService {
// 返回登录信息
return new LoginVO(user.getId(), user.getUsername(), token, user.getTenantId());
}
@Override
public UserVO getUserInfo(Long userId) {
SysUser user = userDataService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
vo.setRealName(user.getRealName());
vo.setPhone(user.getPhone());
vo.setEmail(user.getEmail());
vo.setDeptId(user.getDeptId());
vo.setStatus(user.getStatus());
vo.setAvatar(user.getAvatar());
vo.setTenantId(user.getTenantId());
vo.setCreatedTime(user.getCreatedTime());
vo.setUpdatedTime(user.getUpdatedTime());
return vo;
}
}