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:
parent
33d7cc2145
commit
b3ef6d89f1
24
fund-admin/.gitignore
vendored
Normal file
24
fund-admin/.gitignore
vendored
Normal 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
3
fund-admin/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
fund-admin/README.md
Normal file
5
fund-admin/README.md
Normal 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
13
fund-admin/index.html
Normal 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
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
29
fund-admin/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
fund-admin/public/vite.svg
Normal file
1
fund-admin/public/vite.svg
Normal 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
21
fund-admin/src/App.vue
Normal 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>
|
||||
21
fund-admin/src/api/auth.ts
Normal file
21
fund-admin/src/api/auth.ts
Normal 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')
|
||||
}
|
||||
42
fund-admin/src/api/customer.ts
Normal file
42
fund-admin/src/api/customer.ts
Normal 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`)
|
||||
}
|
||||
25
fund-admin/src/api/dept.ts
Normal file
25
fund-admin/src/api/dept.ts
Normal 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}`)
|
||||
}
|
||||
64
fund-admin/src/api/expense.ts
Normal file
64
fund-admin/src/api/expense.ts
Normal 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 || ''}`)
|
||||
}
|
||||
30
fund-admin/src/api/menu.ts
Normal file
30
fund-admin/src/api/menu.ts
Normal 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}`)
|
||||
}
|
||||
42
fund-admin/src/api/project.ts
Normal file
42
fund-admin/src/api/project.ts
Normal 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}`)
|
||||
}
|
||||
51
fund-admin/src/api/receivable.ts
Normal file
51
fund-admin/src/api/receivable.ts
Normal 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)
|
||||
}
|
||||
70
fund-admin/src/api/request.ts
Normal file
70
fund-admin/src/api/request.ts
Normal 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
|
||||
29
fund-admin/src/api/role.ts
Normal file
29
fund-admin/src/api/role.ts
Normal 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)
|
||||
}
|
||||
31
fund-admin/src/api/user.ts
Normal file
31
fund-admin/src/api/user.ts
Normal 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}`)
|
||||
}
|
||||
1
fund-admin/src/assets/vue.svg
Normal file
1
fund-admin/src/assets/vue.svg
Normal 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 |
41
fund-admin/src/components/HelloWorld.vue
Normal file
41
fund-admin/src/components/HelloWorld.vue
Normal 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
10
fund-admin/src/env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title?: string
|
||||
icon?: string
|
||||
parent?: string
|
||||
requiresAuth?: boolean
|
||||
}
|
||||
}
|
||||
317
fund-admin/src/layouts/MainLayout.vue
Normal file
317
fund-admin/src/layouts/MainLayout.vue
Normal 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
23
fund-admin/src/main.ts
Normal 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')
|
||||
141
fund-admin/src/router/index.ts
Normal file
141
fund-admin/src/router/index.ts
Normal 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
|
||||
22
fund-admin/src/stores/app.ts
Normal file
22
fund-admin/src/stores/app.ts
Normal 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
|
||||
}
|
||||
})
|
||||
8
fund-admin/src/stores/index.ts
Normal file
8
fund-admin/src/stores/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
export default pinia
|
||||
|
||||
export * from './user'
|
||||
export * from './app'
|
||||
77
fund-admin/src/stores/user.ts
Normal file
77
fund-admin/src/stores/user.ts
Normal 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
79
fund-admin/src/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
54
fund-admin/src/styles/index.scss
Normal file
54
fund-admin/src/styles/index.scss
Normal 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;
|
||||
}
|
||||
252
fund-admin/src/views/customer/contact.vue
Normal file
252
fund-admin/src/views/customer/contact.vue
Normal 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>
|
||||
335
fund-admin/src/views/customer/index.vue
Normal file
335
fund-admin/src/views/customer/index.vue
Normal 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>
|
||||
480
fund-admin/src/views/dashboard/index.vue
Normal file
480
fund-admin/src/views/dashboard/index.vue
Normal 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>
|
||||
45
fund-admin/src/views/error/404.vue
Normal file
45
fund-admin/src/views/error/404.vue
Normal 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>
|
||||
492
fund-admin/src/views/expense/index.vue
Normal file
492
fund-admin/src/views/expense/index.vue
Normal 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>
|
||||
279
fund-admin/src/views/expense/type.vue
Normal file
279
fund-admin/src/views/expense/type.vue
Normal 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>
|
||||
143
fund-admin/src/views/login/index.vue
Normal file
143
fund-admin/src/views/login/index.vue
Normal 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>
|
||||
361
fund-admin/src/views/project/index.vue
Normal file
361
fund-admin/src/views/project/index.vue
Normal 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>
|
||||
464
fund-admin/src/views/project/requirement.vue
Normal file
464
fund-admin/src/views/project/requirement.vue
Normal 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>
|
||||
520
fund-admin/src/views/receivable/index.vue
Normal file
520
fund-admin/src/views/receivable/index.vue
Normal 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>
|
||||
176
fund-admin/src/views/system/dept/index.vue
Normal file
176
fund-admin/src/views/system/dept/index.vue
Normal 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>
|
||||
202
fund-admin/src/views/system/menu/index.vue
Normal file
202
fund-admin/src/views/system/menu/index.vue
Normal 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>
|
||||
200
fund-admin/src/views/system/role/index.vue
Normal file
200
fund-admin/src/views/system/role/index.vue
Normal 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>
|
||||
137
fund-admin/src/views/system/user/index.vue
Normal file
137
fund-admin/src/views/system/user/index.vue
Normal 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>
|
||||
16
fund-admin/tsconfig.app.json
Normal file
16
fund-admin/tsconfig.app.json
Normal 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
7
fund-admin/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
fund-admin/tsconfig.node.json
Normal file
26
fund-admin/tsconfig.node.json
Normal 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
46
fund-admin/vite.config.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定父级的子类型列表
|
||||
*/
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
/**
|
||||
* 创建支出类型
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 -> "";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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> {
|
||||
}
|
||||
@ -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> {
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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 -> "";
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user