chore: 删除开发规则清单文档
按照规范,不应主动创建文档文件
This commit is contained in:
parent
32abc57338
commit
297ea8e259
@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
@ -1,24 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export const login = (data) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/auth/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const refreshToken = (refreshToken) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/auth/refresh',
|
||||
method: 'post',
|
||||
data: { refreshToken }
|
||||
})
|
||||
}
|
||||
|
||||
export const logout = () => {
|
||||
return request({
|
||||
url: '/sys/api/v1/auth/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取合同列表(分页)
|
||||
*/
|
||||
export const getContractList = (params) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/contract/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取合同详情
|
||||
*/
|
||||
export const getContractById = (id) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/contract/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建合同
|
||||
*/
|
||||
export const createContract = (data) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/contract',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新合同
|
||||
*/
|
||||
export const updateContract = (id, data) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/contract/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除合同
|
||||
*/
|
||||
export const deleteContract = (id) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/contract/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新合同状态
|
||||
*/
|
||||
export const updateContractStatus = (id, status) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/contract/${id}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export const getCustomerList = (params) => {
|
||||
return request({
|
||||
url: '/cust/api/v1/customer/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getCustomerById = (id) => {
|
||||
return request({
|
||||
url: `/cust/api/v1/customer/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const createCustomer = (data) => {
|
||||
return request({
|
||||
url: '/cust/api/v1/customer',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const updateCustomer = (id, data) => {
|
||||
return request({
|
||||
url: `/cust/api/v1/customer/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteCustomer = (id) => {
|
||||
return request({
|
||||
url: `/cust/api/v1/customer/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export const getContactList = (params) => {
|
||||
return request({
|
||||
url: '/cust/api/v1/contact/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const createContact = (data) => {
|
||||
return request({
|
||||
url: '/cust/api/v1/contact',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const updateContact = (id, data) => {
|
||||
return request({
|
||||
url: `/cust/api/v1/contact/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteContact = (id) => {
|
||||
return request({
|
||||
url: `/cust/api/v1/contact/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
export const getDashboardData = (params) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/dashboard',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取部门树
|
||||
*/
|
||||
export const getDeptTree = () => {
|
||||
return request({
|
||||
url: '/sys/api/v1/dept/tree',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门列表
|
||||
*/
|
||||
export const getDeptList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/dept/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门详情
|
||||
*/
|
||||
export const getDeptById = (id) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/dept/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建部门
|
||||
*/
|
||||
export const createDept = (data) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/dept',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新部门
|
||||
*/
|
||||
export const updateDept = (id, data) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/dept/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
*/
|
||||
export const deleteDept = (id) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/dept/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取支出列表(分页)
|
||||
*/
|
||||
export const getExpenseList = (params) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/expense/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支出详情
|
||||
*/
|
||||
export const getExpenseById = (expenseId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense/${expenseId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支出
|
||||
*/
|
||||
export const createExpense = (data) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/expense',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支出
|
||||
*/
|
||||
export const updateExpense = (expenseId, data) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense/${expenseId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支出
|
||||
*/
|
||||
export const deleteExpense = (expenseId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense/${expenseId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支出状态
|
||||
*/
|
||||
export const updateExpenseStatus = (expenseId, status) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense/${expenseId}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取支出类型树(仅启用)
|
||||
*/
|
||||
export const getExpenseTypeTree = () => {
|
||||
return request({
|
||||
url: '/proj/api/v1/expense-type/tree',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支出类型树(包含禁用)
|
||||
*/
|
||||
export const getAllExpenseTypeTree = () => {
|
||||
return request({
|
||||
url: '/proj/api/v1/expense-type/tree/all',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支出类型详情
|
||||
*/
|
||||
export const getExpenseTypeById = (typeId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense-type/${typeId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支出类型
|
||||
*/
|
||||
export const createExpenseType = (data) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/expense-type',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支出类型
|
||||
*/
|
||||
export const updateExpenseType = (typeId, data) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense-type/${typeId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支出类型
|
||||
*/
|
||||
export const deleteExpenseType = (typeId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense-type/${typeId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支出类型状态
|
||||
*/
|
||||
export const updateExpenseTypeStatus = (typeId, status) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/expense-type/${typeId}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
export const uploadFile = (data) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/file/upload',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表(分页)
|
||||
*/
|
||||
export const getFileList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/file/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务类型和ID查询文件列表
|
||||
*/
|
||||
export const getFileListByBusiness = (businessType, businessId) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/file/list/business',
|
||||
method: 'get',
|
||||
params: { businessType, businessId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件详情
|
||||
*/
|
||||
export const getFileById = (fileId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/file/${fileId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
export const deleteFile = (fileId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/file/${fileId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
*/
|
||||
export const getFileUrl = (fileId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/file/${fileId}/url`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export const getMenuTree = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/menu/tree',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getMenuList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/menu/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getMenuDetail = (menuId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/menu/${menuId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const createMenu = (data) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/menu',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const updateMenu = (menuId, data) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/menu/${menuId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteMenu = (menuId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/menu/${menuId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取操作日志列表(分页)
|
||||
*/
|
||||
export const getOperationLogList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/operation-log/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志详情
|
||||
*/
|
||||
export const getOperationLogById = (logId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/operation-log/${logId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除操作日志
|
||||
*/
|
||||
export const deleteOperationLog = (logId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/operation-log/${logId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除操作日志(删除指定天数之前的日志)
|
||||
*/
|
||||
export const batchDeleteOperationLog = (days) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/operation-log/batch',
|
||||
method: 'delete',
|
||||
params: { days }
|
||||
})
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取岗位列表(分页)
|
||||
*/
|
||||
export const getPostList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/post/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门ID获取岗位列表
|
||||
*/
|
||||
export const getPostListByDept = (deptId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/post/list/dept/${deptId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的岗位列表
|
||||
*/
|
||||
export const getPostListEnabled = () => {
|
||||
return request({
|
||||
url: '/sys/api/v1/post/list/enabled',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位详情
|
||||
*/
|
||||
export const getPostById = (postId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/post/${postId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建岗位
|
||||
*/
|
||||
export const createPost = (data) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/post',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新岗位
|
||||
*/
|
||||
export const updatePost = (postId, data) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/post/${postId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除岗位
|
||||
*/
|
||||
export const deletePost = (postId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/post/${postId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新岗位状态
|
||||
*/
|
||||
export const updatePostStatus = (postId, status) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/post/${postId}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取项目列表(分页)
|
||||
*/
|
||||
export const getProjectList = (params) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/project/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目详情
|
||||
*/
|
||||
export const getProjectById = (id) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建项目
|
||||
*/
|
||||
export const createProject = (data) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/project',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目
|
||||
*/
|
||||
export const updateProject = (id, data) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*/
|
||||
export const deleteProject = (id) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目状态
|
||||
*/
|
||||
export const updateProjectStatus = (id, status) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project/${id}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取项目成员列表(按项目ID)
|
||||
*/
|
||||
export const getMemberListByProject = (projectId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project-member/list/project/${projectId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的项目列表(按用户ID)
|
||||
*/
|
||||
export const getMemberListByUser = (userId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project-member/list/user/${userId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目成员详情
|
||||
*/
|
||||
export const getMemberById = (memberId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project-member/${memberId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加项目成员
|
||||
*/
|
||||
export const addMember = (data) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/project-member',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目成员
|
||||
*/
|
||||
export const updateMember = (memberId, data) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project-member/${memberId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除项目成员
|
||||
*/
|
||||
export const removeMember = (memberId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project-member/${memberId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新成员状态
|
||||
*/
|
||||
export const updateMemberStatus = (memberId, status) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/project-member/${memberId}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 收款记录管理API
|
||||
|
||||
export function getReceiptPage(params) {
|
||||
return request({
|
||||
url: '/receipt/page',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getReceiptById(receiptId) {
|
||||
return request({
|
||||
url: `/receipt/${receiptId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createReceipt(data) {
|
||||
return request({
|
||||
url: '/receipt',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateReceipt(receiptId, data) {
|
||||
return request({
|
||||
url: `/receipt/${receiptId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteReceipt(receiptId) {
|
||||
return request({
|
||||
url: `/receipt/${receiptId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function batchDeleteReceipt(receiptIds) {
|
||||
return request({
|
||||
url: '/receipt/batch',
|
||||
method: 'delete',
|
||||
params: { receiptIds }
|
||||
})
|
||||
}
|
||||
|
||||
export function getReceiptStatistics() {
|
||||
return request({
|
||||
url: '/receipt/statistics',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getReceiptsByProject(projectId) {
|
||||
return request({
|
||||
url: '/receipt/project/' + projectId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getReceiptsByCustomer(customerId) {
|
||||
return request({
|
||||
url: '/receipt/customer/' + customerId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取应收款列表(分页)
|
||||
*/
|
||||
export const getReceivableList = (params) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/receivable/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应收款详情
|
||||
*/
|
||||
export const getReceivableById = (receivableId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/receivable/${receivableId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应收款
|
||||
*/
|
||||
export const createReceivable = (data) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/receivable',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新应收款
|
||||
*/
|
||||
export const updateReceivable = (receivableId, data) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/receivable/${receivableId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除应收款
|
||||
*/
|
||||
export const deleteReceivable = (receivableId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/receivable/${receivableId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录收款
|
||||
*/
|
||||
export const recordPayment = (receivableId, amount) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/receivable/${receivableId}/payment`,
|
||||
method: 'post',
|
||||
params: { amount }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新应收款状态
|
||||
*/
|
||||
export const updateReceivableStatus = (receivableId, status) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/receivable/${receivableId}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取需求工单列表
|
||||
*/
|
||||
export const getRequirementList = (params) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/requirement/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需求工单详情
|
||||
*/
|
||||
export const getRequirementById = (requirementId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/requirement/${requirementId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建需求工单
|
||||
*/
|
||||
export const createRequirement = (data) => {
|
||||
return request({
|
||||
url: '/proj/api/v1/requirement',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新需求工单
|
||||
*/
|
||||
export const updateRequirement = (requirementId, data) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/requirement/${requirementId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除需求工单
|
||||
*/
|
||||
export const deleteRequirement = (requirementId) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/requirement/${requirementId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新需求状态
|
||||
*/
|
||||
export const updateRequirementStatus = (requirementId, status) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/requirement/${requirementId}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新需求进度
|
||||
*/
|
||||
export const updateRequirementProgress = (requirementId, progress) => {
|
||||
return request({
|
||||
url: `/proj/api/v1/requirement/${requirementId}/progress`,
|
||||
method: 'put',
|
||||
params: { progress }
|
||||
})
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export const getRoleList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/role/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getRoleDetail = (roleId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/role/${roleId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const createRole = (data) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/role',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const updateRole = (roleId, data) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/role/${roleId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteRole = (roleId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/role/${roleId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export const getRoleMenuIds = (roleId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/role/${roleId}/menus`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const assignRoleMenus = (roleId, menuIds) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/role/${roleId}/menus`,
|
||||
method: 'post',
|
||||
data: { menuIds }
|
||||
})
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 系统配置管理API
|
||||
|
||||
export function getConfigPage(params) {
|
||||
return request({
|
||||
url: '/sys/config/page',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getConfigById(configId) {
|
||||
return request({
|
||||
url: `/sys/config/${configId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getConfigValue(configKey) {
|
||||
return request({
|
||||
url: `/sys/config/value/${configKey}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getConfigByGroup(configGroup) {
|
||||
return request({
|
||||
url: `/sys/config/group/${configGroup}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createConfig(data) {
|
||||
return request({
|
||||
url: '/sys/config',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateConfig(configId, data) {
|
||||
return request({
|
||||
url: `/sys/config/${configId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteConfig(configId) {
|
||||
return request({
|
||||
url: `/sys/config/${configId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function batchDeleteConfig(configIds) {
|
||||
return request({
|
||||
url: '/sys/config/batch',
|
||||
method: 'delete',
|
||||
params: { configIds }
|
||||
})
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 数据字典管理API
|
||||
|
||||
export function getDictPage(params) {
|
||||
return request({
|
||||
url: '/sys/dict/page',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getDictById(dictId) {
|
||||
return request({
|
||||
url: `/sys/dict/${dictId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getDictByType(dictType) {
|
||||
return request({
|
||||
url: `/sys/dict/type/${dictType}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getAllDictTypes() {
|
||||
return request({
|
||||
url: '/sys/dict/types',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getDictMap(dictType) {
|
||||
return request({
|
||||
url: `/sys/dict/map/${dictType}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getDictLabel(dictType, dictCode) {
|
||||
return request({
|
||||
url: '/sys/dict/label',
|
||||
method: 'get',
|
||||
params: { dictType, dictCode }
|
||||
})
|
||||
}
|
||||
|
||||
export function createDict(data) {
|
||||
return request({
|
||||
url: '/sys/dict',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateDict(dictId, data) {
|
||||
return request({
|
||||
url: `/sys/dict/${dictId}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteDict(dictId) {
|
||||
return request({
|
||||
url: `/sys/dict/${dictId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function batchDeleteDict(dictIds) {
|
||||
return request({
|
||||
url: '/sys/dict/batch',
|
||||
method: 'delete',
|
||||
params: { dictIds }
|
||||
})
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export const getUserList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/user/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getUserById = (id) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export const createUser = (data) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/user',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUser = (id, data) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteUser = (id) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export const resetPassword = (id, newPassword) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user/${id}/reset-password`,
|
||||
method: 'put',
|
||||
params: { newPassword }
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUserStatus = (id, status) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user/${id}/status`,
|
||||
method: 'put',
|
||||
params: { status }
|
||||
})
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
/**
|
||||
* 获取用户分配列表(分页)
|
||||
*/
|
||||
export const getUserAssignmentList = (params) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/user-assignment/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门ID获取用户列表
|
||||
*/
|
||||
export const getUserListByDept = (deptId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user-assignment/list/dept/${deptId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配用户到部门和岗位
|
||||
*/
|
||||
export const assignUser = (userId, data) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user-assignment/${userId}`,
|
||||
method: 'put',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量分配用户
|
||||
*/
|
||||
export const batchAssignUsers = (userIds, deptId, postId) => {
|
||||
return request({
|
||||
url: '/sys/api/v1/user-assignment/batch',
|
||||
method: 'put',
|
||||
data: userIds,
|
||||
params: { deptId, postId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户分配
|
||||
*/
|
||||
export const removeUserAssignment = (userId) => {
|
||||
return request({
|
||||
url: `/sys/api/v1/user-assignment/${userId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 496 B |
@ -1,43 +0,0 @@
|
||||
<script setup>
|
||||
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>
|
||||
@ -1,185 +0,0 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside width="200px" class="sidebar">
|
||||
<div class="logo">
|
||||
<h3>资金服务平台</h3>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="el-menu-vertical"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409EFF"
|
||||
router
|
||||
>
|
||||
<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-menu-item>
|
||||
<el-menu-item index="/system/dept">部门管理</el-menu-item>
|
||||
<el-menu-item index="/system/role">角色管理</el-menu-item>
|
||||
<el-menu-item index="/system/menu">菜单管理</el-menu-item>
|
||||
<el-menu-item index="/system/post">岗位管理</el-menu-item>
|
||||
<el-menu-item index="/system/user-assignment">人员分配</el-menu-item>
|
||||
<el-menu-item index="/system/file">文件管理</el-menu-item>
|
||||
<el-menu-item index="/system/operation-log">操作日志</el-menu-item>
|
||||
<el-menu-item index="/system/sys-config">系统配置</el-menu-item>
|
||||
<el-menu-item index="/system/sys-dict">数据字典</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="/customer">
|
||||
<template #title>
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<span>客户中心</span>
|
||||
</template>
|
||||
<el-menu-item index="/customer/list">客户管理</el-menu-item>
|
||||
<el-menu-item index="/customer/contact">联系人管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="/project">
|
||||
<template #title>
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<span>项目管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/project/list">项目管理</el-menu-item>
|
||||
<el-menu-item index="/project/contract">合同管理</el-menu-item>
|
||||
<el-menu-item index="/project/requirement">需求工单</el-menu-item>
|
||||
<el-menu-item index="/project/member">项目成员</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-sub-menu index="/finance">
|
||||
<template #title>
|
||||
<el-icon><Money /></el-icon>
|
||||
<span>财务管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/finance/expense-type">支出类型</el-menu-item>
|
||||
<el-menu-item index="/finance/expense">支出管理</el-menu-item>
|
||||
<el-menu-item index="/finance/receivable">应收款管理</el-menu-item>
|
||||
<el-menu-item index="/finance/receipt">收款记录</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<div class="header-right">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="user-info">
|
||||
{{ userStore.realName || userStore.username }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></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 />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { HomeFilled, Setting, UserFilled, FolderOpened, Money, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
const handleCommand = (command) => {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
router.push('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
router.push('/settings')
|
||||
break
|
||||
case 'logout':
|
||||
handleLogout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userStore.logout()
|
||||
ElMessage.success('退出成功')
|
||||
router.push('/login')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #304156;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #1f2d3d;
|
||||
}
|
||||
|
||||
.logo h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.el-menu-vertical {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,178 +0,0 @@
|
||||
<template>
|
||||
<div class="mobile-sidebar-wrapper">
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
class="mobile-sidebar-mask"
|
||||
:class="{ show: modelValue }"
|
||||
@click="handleClose"
|
||||
/>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div
|
||||
class="mobile-sidebar"
|
||||
:class="{ open: modelValue }"
|
||||
>
|
||||
<div class="mobile-sidebar-header">
|
||||
<div class="logo">
|
||||
<img src="@/assets/logo.png" alt="logo" v-if="false">
|
||||
<span>资金服务平台</span>
|
||||
</div>
|
||||
<el-button link @click="handleClose">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="mobile-sidebar-content">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="false"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in routes"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
</el-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import SidebarItem from '@/layout/components/Sidebar/SidebarItem.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 当前激活的菜单
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
return path
|
||||
})
|
||||
|
||||
// 路由列表
|
||||
const routes = computed(() => {
|
||||
// 从路由配置中获取菜单
|
||||
const allRoutes = route.matched[0]?.children || []
|
||||
return allRoutes.filter(item => !item.hidden)
|
||||
})
|
||||
|
||||
// 关闭侧边栏
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mobile-sidebar-wrapper {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-sidebar-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 260px;
|
||||
background: #304156;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&-header {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
background: #2b3a4d;
|
||||
|
||||
.logo {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
:deep(.el-menu) {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: #bfcbd9;
|
||||
|
||||
&:hover {
|
||||
background: #263445;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: #409eff;
|
||||
background: #263445;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,209 +0,0 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 断点定义
|
||||
const BREAKPOINTS = {
|
||||
xs: 768, // 手机
|
||||
sm: 992, // 平板
|
||||
md: 1200, // 小桌面
|
||||
lg: 1920 // 大桌面
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端检测组合式函数
|
||||
*/
|
||||
export function useMobile() {
|
||||
// 视口宽度
|
||||
const width = ref(window.innerWidth)
|
||||
const height = ref(window.innerHeight)
|
||||
|
||||
// 是否移动端(< 768px)
|
||||
const isMobile = computed(() => width.value < BREAKPOINTS.xs)
|
||||
|
||||
// 是否平板(768px - 992px)
|
||||
const isTablet = computed(() => width.value >= BREAKPOINTS.xs && width.value < BREAKPOINTS.sm)
|
||||
|
||||
// 是否桌面(>= 992px)
|
||||
const isDesktop = computed(() => width.value >= BREAKPOINTS.sm)
|
||||
|
||||
// 是否小屏幕(< 992px)
|
||||
const isSmallScreen = computed(() => width.value < BREAKPOINTS.sm)
|
||||
|
||||
// 当前断点
|
||||
const breakpoint = computed(() => {
|
||||
if (width.value < BREAKPOINTS.xs) return 'xs'
|
||||
if (width.value < BREAKPOINTS.sm) return 'sm'
|
||||
if (width.value < BREAKPOINTS.md) return 'md'
|
||||
if (width.value < BREAKPOINTS.lg) return 'lg'
|
||||
return 'xl'
|
||||
})
|
||||
|
||||
// 侧边栏是否折叠
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
// 移动端侧边栏是否打开
|
||||
const mobileSidebarOpen = ref(false)
|
||||
|
||||
// 切换侧边栏
|
||||
const toggleSidebar = () => {
|
||||
if (isMobile.value) {
|
||||
mobileSidebarOpen.value = !mobileSidebarOpen.value
|
||||
} else {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭移动端侧边栏
|
||||
const closeMobileSidebar = () => {
|
||||
mobileSidebarOpen.value = false
|
||||
}
|
||||
|
||||
// 打开移动端侧边栏
|
||||
const openMobileSidebar = () => {
|
||||
mobileSidebarOpen.value = true
|
||||
}
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
width.value = window.innerWidth
|
||||
height.value = window.innerHeight
|
||||
|
||||
// 切换到桌面端时关闭移动端侧边栏
|
||||
if (!isMobile.value) {
|
||||
mobileSidebarOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
handleResize()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
isMobile,
|
||||
isTablet,
|
||||
isDesktop,
|
||||
isSmallScreen,
|
||||
breakpoint,
|
||||
sidebarCollapsed,
|
||||
mobileSidebarOpen,
|
||||
toggleSidebar,
|
||||
closeMobileSidebar,
|
||||
openMobileSidebar
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式表格组合式函数
|
||||
*/
|
||||
export function useResponsiveTable() {
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
// 表格高度
|
||||
const tableHeight = computed(() => {
|
||||
if (isMobile.value) {
|
||||
return 'auto'
|
||||
}
|
||||
// 桌面端:视口高度 - 头部(50) - 搜索区域(80) - 分页(50) - 边距(40)
|
||||
return window.innerHeight - 220
|
||||
})
|
||||
|
||||
// 分页大小选项
|
||||
const pageSizeOptions = computed(() => {
|
||||
return isMobile.value ? [10, 20] : [10, 20, 50, 100]
|
||||
})
|
||||
|
||||
// 默认分页大小
|
||||
const defaultPageSize = computed(() => {
|
||||
return isMobile.value ? 10 : 20
|
||||
})
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
tableHeight,
|
||||
pageSizeOptions,
|
||||
defaultPageSize
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸手势支持
|
||||
*/
|
||||
export function useTouchGesture() {
|
||||
const touchStartX = ref(0)
|
||||
const touchEndX = ref(0)
|
||||
const touchStartY = ref(0)
|
||||
const touchEndY = ref(0)
|
||||
|
||||
// 最小滑动距离
|
||||
const MIN_SWIPE_DISTANCE = 50
|
||||
|
||||
// 处理触摸开始
|
||||
const handleTouchStart = (e) => {
|
||||
touchStartX.value = e.touches[0].clientX
|
||||
touchStartY.value = e.touches[0].clientY
|
||||
}
|
||||
|
||||
// 处理触摸结束
|
||||
const handleTouchEnd = (e) => {
|
||||
touchEndX.value = e.changedTouches[0].clientX
|
||||
touchEndY.value = e.changedTouches[0].clientY
|
||||
|
||||
return {
|
||||
direction: getSwipeDirection(),
|
||||
distance: Math.abs(touchEndX.value - touchStartX.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取滑动方向
|
||||
const getSwipeDirection = () => {
|
||||
const diffX = touchEndX.value - touchStartX.value
|
||||
const diffY = touchEndY.value - touchStartY.value
|
||||
|
||||
// 判断是水平滑动还是垂直滑动
|
||||
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||
if (Math.abs(diffX) < MIN_SWIPE_DISTANCE) return null
|
||||
return diffX > 0 ? 'right' : 'left'
|
||||
} else {
|
||||
if (Math.abs(diffY) < MIN_SWIPE_DISTANCE) return null
|
||||
return diffY > 0 ? 'down' : 'up'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleTouchStart,
|
||||
handleTouchEnd,
|
||||
getSwipeDirection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟键盘检测
|
||||
*/
|
||||
export function useVirtualKeyboard() {
|
||||
const isKeyboardOpen = ref(false)
|
||||
const windowHeight = ref(window.innerHeight)
|
||||
|
||||
const handleResize = () => {
|
||||
const currentHeight = window.innerHeight
|
||||
// 如果窗口高度变小超过150px,认为是虚拟键盘打开
|
||||
isKeyboardOpen.value = windowHeight.value - currentHeight > 150
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
return {
|
||||
isKeyboardOpen
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
@ -1,197 +0,0 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/login/index.vue'),
|
||||
meta: { public: true }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: () => import('../components/Layout.vue'),
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/dashboard/index.vue'),
|
||||
meta: { title: '首页', icon: 'HomeFilled' }
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
name: 'System',
|
||||
meta: { title: '系统管理', icon: 'Setting' },
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
component: () => import('../views/system/user.vue'),
|
||||
meta: { title: '用户管理' }
|
||||
},
|
||||
{
|
||||
path: 'dept',
|
||||
name: 'Dept',
|
||||
component: () => import('../views/system/dept.vue'),
|
||||
meta: { title: '部门管理' }
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'Role',
|
||||
component: () => import('../views/system/role.vue'),
|
||||
meta: { title: '角色管理' }
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
name: 'Menu',
|
||||
component: () => import('../views/system/menu.vue'),
|
||||
meta: { title: '菜单管理' }
|
||||
},
|
||||
{
|
||||
path: 'post',
|
||||
name: 'Post',
|
||||
component: () => import('../views/system/post.vue'),
|
||||
meta: { title: '岗位管理' }
|
||||
},
|
||||
{
|
||||
path: 'user-assignment',
|
||||
name: 'UserAssignment',
|
||||
component: () => import('../views/system/userAssignment.vue'),
|
||||
meta: { title: '人员分配' }
|
||||
},
|
||||
{
|
||||
path: 'file',
|
||||
name: 'File',
|
||||
component: () => import('../views/system/file.vue'),
|
||||
meta: { title: '文件管理' }
|
||||
},
|
||||
{
|
||||
path: 'operation-log',
|
||||
name: 'OperationLog',
|
||||
component: () => import('../views/system/operationLog.vue'),
|
||||
meta: { title: '操作日志' }
|
||||
},
|
||||
{
|
||||
path: 'sys-config',
|
||||
name: 'SysConfig',
|
||||
component: () => import('../views/system/sysConfig.vue'),
|
||||
meta: { title: '系统配置' }
|
||||
},
|
||||
{
|
||||
path: 'sys-dict',
|
||||
name: 'SysDict',
|
||||
component: () => import('../views/system/sysDict.vue'),
|
||||
meta: { title: '数据字典' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'customer',
|
||||
name: 'Customer',
|
||||
meta: { title: '客户中心', icon: 'UserFilled' },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'CustomerList',
|
||||
component: () => import('../views/customer/list.vue'),
|
||||
meta: { title: '客户管理' }
|
||||
},
|
||||
{
|
||||
path: 'contact',
|
||||
name: 'CustomerContact',
|
||||
component: () => import('../views/customer/contact.vue'),
|
||||
meta: { title: '联系人管理' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'project',
|
||||
name: 'Project',
|
||||
meta: { title: '项目管理', icon: 'FolderOpened' },
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
name: 'ProjectList',
|
||||
component: () => import('../views/project/list.vue'),
|
||||
meta: { title: '项目管理' }
|
||||
},
|
||||
{
|
||||
path: 'contract',
|
||||
name: 'Contract',
|
||||
component: () => import('../views/project/contract.vue'),
|
||||
meta: { title: '合同管理' }
|
||||
},
|
||||
{
|
||||
path: 'requirement',
|
||||
name: 'Requirement',
|
||||
component: () => import('../views/project/requirement.vue'),
|
||||
meta: { title: '需求工单' }
|
||||
},
|
||||
{
|
||||
path: 'member',
|
||||
name: 'ProjectMember',
|
||||
component: () => import('../views/project/projectMember.vue'),
|
||||
meta: { title: '项目成员' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'finance',
|
||||
name: 'Finance',
|
||||
meta: { title: '财务管理', icon: 'Money' },
|
||||
children: [
|
||||
{
|
||||
path: 'expense-type',
|
||||
name: 'ExpenseType',
|
||||
component: () => import('../views/finance/expenseType.vue'),
|
||||
meta: { title: '支出类型' }
|
||||
},
|
||||
{
|
||||
path: 'expense',
|
||||
name: 'Expense',
|
||||
component: () => import('../views/finance/expense.vue'),
|
||||
meta: { title: '支出管理' }
|
||||
},
|
||||
{
|
||||
path: 'receivable',
|
||||
name: 'Receivable',
|
||||
component: () => import('../views/finance/receivable.vue'),
|
||||
meta: { title: '应收款管理' }
|
||||
},
|
||||
{
|
||||
path: 'receipt',
|
||||
name: 'Receipt',
|
||||
component: () => import('../views/finance/receipt.vue'),
|
||||
meta: { title: '收款记录' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (to.meta.public) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!userStore.token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -1,72 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
|
||||
const permissions = ref(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||||
const roles = ref(JSON.parse(localStorage.getItem('roles') || '[]'))
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
const username = computed(() => userInfo.value?.username || '')
|
||||
const realName = computed(() => userInfo.value?.realName || '')
|
||||
|
||||
// Actions
|
||||
const setToken = (newToken) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('token', newToken)
|
||||
}
|
||||
|
||||
const setUserInfo = (info) => {
|
||||
userInfo.value = info
|
||||
localStorage.setItem('userInfo', JSON.stringify(info))
|
||||
}
|
||||
|
||||
const setPermissions = (perms) => {
|
||||
permissions.value = perms
|
||||
localStorage.setItem('permissions', JSON.stringify(perms))
|
||||
}
|
||||
|
||||
const setRoles = (roleList) => {
|
||||
roles.value = roleList
|
||||
localStorage.setItem('roles', JSON.stringify(roleList))
|
||||
}
|
||||
|
||||
const hasPermission = (perm) => {
|
||||
return permissions.value.includes(perm)
|
||||
}
|
||||
|
||||
const hasRole = (role) => {
|
||||
return roles.value.includes(role)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
token.value = ''
|
||||
userInfo.value = {}
|
||||
permissions.value = []
|
||||
roles.value = []
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('permissions')
|
||||
localStorage.removeItem('roles')
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
permissions,
|
||||
roles,
|
||||
isLoggedIn,
|
||||
username,
|
||||
realName,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
setPermissions,
|
||||
setRoles,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
logout
|
||||
}
|
||||
})
|
||||
@ -1,79 +0,0 @@
|
||||
: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;
|
||||
}
|
||||
}
|
||||
@ -1,296 +0,0 @@
|
||||
// 移动端适配样式
|
||||
// 断点定义:xs < 768px < sm < 992px < md < 1200px < lg
|
||||
|
||||
// 移动端基础样式
|
||||
@media screen and (max-width: 768px) {
|
||||
// 隐藏侧边栏
|
||||
.sidebar-container {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s;
|
||||
|
||||
&.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 主内容区域全宽
|
||||
.main-container {
|
||||
margin-left: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
// 顶部导航栏适配
|
||||
.navbar {
|
||||
.hamburger-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
.right-menu-item {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索表单改为垂直布局
|
||||
.search-form {
|
||||
.el-form-item {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
|
||||
.el-input,
|
||||
.el-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
margin-left: 0 !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格操作按钮
|
||||
.el-table {
|
||||
.el-button--link {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页器适配
|
||||
.el-pagination {
|
||||
.el-pagination__sizes,
|
||||
.el-pagination__jump {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-prev,
|
||||
.btn-next {
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.el-pager li {
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框全屏
|
||||
.el-dialog {
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
height: 100vh;
|
||||
|
||||
.el-dialog__body {
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 抽屉全屏
|
||||
.el-drawer {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
// 表单单行显示
|
||||
.el-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.el-form-item__label {
|
||||
float: none;
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片内边距减小
|
||||
.el-card {
|
||||
.el-card__header {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片
|
||||
.dashboard-card {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.card-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部固定按钮
|
||||
.fixed-bottom-buttons {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 15px;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.el-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部留白(避免被固定按钮遮挡)
|
||||
.has-fixed-bottom {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
// 平板适配
|
||||
@media screen and (min-width: 769px) and (max-width: 992px) {
|
||||
.sidebar-container {
|
||||
width: 54px !important;
|
||||
|
||||
.el-menu {
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
padding: 0 15px !important;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 54px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端菜单按钮
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端遮罩层
|
||||
.mobile-sidebar-mask {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式工具类
|
||||
.hide-on-mobile {
|
||||
@media screen and (max-width: 768px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.show-on-mobile {
|
||||
display: none !important;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式表格
|
||||
.responsive-table {
|
||||
@media screen and (max-width: 768px) {
|
||||
.el-table {
|
||||
&__header-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__body-wrapper {
|
||||
.el-table__row {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: none;
|
||||
padding: 8px 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: attr(data-label);
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸优化
|
||||
@media (hover: none) {
|
||||
.el-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.el-table__row:hover > td {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const userStore = useUserStore()
|
||||
if (userStore.token) {
|
||||
config.headers.Authorization = `Bearer ${userStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const res = response.data
|
||||
|
||||
// 业务成功
|
||||
if (res.code === 200) {
|
||||
return res.data
|
||||
}
|
||||
|
||||
// 处理401未认证
|
||||
if (res.code === 401) {
|
||||
ElMessage.error(res.message || '登录已过期,请重新登录')
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(new Error(res.message || '未认证'))
|
||||
}
|
||||
|
||||
// 其他业务失败
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
return Promise.reject(new Error(res.message || '请求失败'))
|
||||
},
|
||||
(error) => {
|
||||
const { response } = error
|
||||
|
||||
if (response) {
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
const userStore = useUserStore()
|
||||
userStore.logout()
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('没有权限访问')
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(response.data?.message || '请求失败')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
@ -1,332 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>联系人管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增联系人</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="客户">
|
||||
<el-select v-model="searchForm.customerId" placeholder="请选择客户" clearable style="width: 200px" @change="handleSearch">
|
||||
<el-option
|
||||
v-for="customer in customerList"
|
||||
:key="customer.customerId"
|
||||
:label="customer.customerName"
|
||||
:value="customer.customerId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="searchForm.contactName" 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>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" stripe border>
|
||||
<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="department" label="部门" width="150" />
|
||||
<el-table-column prop="phone" label="联系电话" width="130" />
|
||||
<el-table-column prop="mobile" label="手机号码" width="130" />
|
||||
<el-table-column prop="email" label="邮箱" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="wechat" label="微信" width="120" />
|
||||
<el-table-column prop="isPrimary" label="主联系人" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.isPrimary === '1' ? 'success' : 'info'">
|
||||
{{ scope.row.isPrimary === '1' ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === '1' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.isPrimary !== '1'"
|
||||
type="warning"
|
||||
link
|
||||
@click="handleSetPrimary(scope.row)"
|
||||
>
|
||||
设为主联系人
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属客户" prop="customerId">
|
||||
<el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%">
|
||||
<el-option
|
||||
v-for="customer in customerList"
|
||||
:key="customer.customerId"
|
||||
:label="customer.customerName"
|
||||
:value="customer.customerId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系人姓名" prop="contactName">
|
||||
<el-input v-model="form.contactName" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="职位" prop="position">
|
||||
<el-input v-model="form.position" placeholder="请输入职位" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" 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="mobile">
|
||||
<el-input v-model="form.mobile" 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="wechat">
|
||||
<el-input v-model="form.wechat" placeholder="请输入微信号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="QQ" prop="qq">
|
||||
<el-input v-model="form.qq" placeholder="请输入QQ号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="主联系人" prop="isPrimary">
|
||||
<el-switch v-model="form.isPrimary" active-value="1" inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" 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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getContactList, createContact, updateContact, deleteContact } from '../../api/customer'
|
||||
import { getCustomerList } from '../../api/customer'
|
||||
|
||||
const searchForm = reactive({
|
||||
customerId: null,
|
||||
contactName: ''
|
||||
})
|
||||
|
||||
const customerList = ref([])
|
||||
const tableData = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增联系人')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
contactId: null,
|
||||
customerId: null,
|
||||
contactName: '',
|
||||
position: '',
|
||||
department: '',
|
||||
phone: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
wechat: '',
|
||||
qq: '',
|
||||
isPrimary: '0',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
contactName: [{ required: true, message: '请输入联系人姓名', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const loadCustomerList = async () => {
|
||||
try {
|
||||
const res = await getCustomerList({ current: 1, size: 1000 })
|
||||
customerList.value = res.records
|
||||
} catch (error) {
|
||||
ElMessage.error('加载客户列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadTableData = async () => {
|
||||
try {
|
||||
const params = {
|
||||
current: 1,
|
||||
size: 1000,
|
||||
customerId: searchForm.customerId || undefined,
|
||||
contactName: searchForm.contactName || undefined
|
||||
}
|
||||
const res = await getContactList(params)
|
||||
tableData.value = res.records || []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
loadTableData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.customerId = null
|
||||
searchForm.contactName = ''
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增联系人'
|
||||
Object.assign(form, {
|
||||
contactId: null,
|
||||
customerId: searchForm.customerId || null,
|
||||
contactName: '',
|
||||
position: '',
|
||||
department: '',
|
||||
phone: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
wechat: '',
|
||||
qq: '',
|
||||
isPrimary: '0',
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑联系人'
|
||||
Object.assign(form, {
|
||||
contactId: row.contactId,
|
||||
customerId: row.customerId,
|
||||
contactName: row.contactName,
|
||||
position: row.position,
|
||||
department: row.department,
|
||||
phone: row.phone,
|
||||
mobile: row.mobile,
|
||||
email: row.email,
|
||||
wechat: row.wechat,
|
||||
qq: row.qq,
|
||||
isPrimary: row.isPrimary,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value.validate()
|
||||
try {
|
||||
if (form.contactId) {
|
||||
await updateContact(form.contactId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createContact(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadTableData()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该联系人吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteContact(row.contactId)
|
||||
ElMessage.success('删除成功')
|
||||
loadTableData()
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSetPrimary = (row) => {
|
||||
ElMessageBox.confirm('确定要将此联系人设置为主联系人吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const updatedContact = { ...row, isPrimary: '1' }
|
||||
await updateContact(row.contactId, updatedContact)
|
||||
ElMessage.success('设置成功')
|
||||
loadTableData()
|
||||
} catch (error) {
|
||||
ElMessage.error('设置失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCustomerList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,298 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>客户管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增客户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="客户名称">
|
||||
<el-input v-model="searchForm.customerName" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户类型">
|
||||
<el-select v-model="searchForm.customerType" placeholder="请选择">
|
||||
<el-option label="企业客户" value="ENTERPRISE" />
|
||||
<el-option label="个人客户" value="PERSONAL" />
|
||||
</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>
|
||||
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="customerCode" label="客户编码" width="120" />
|
||||
<el-table-column prop="customerName" label="客户名称" />
|
||||
<el-table-column prop="customerType" label="客户类型" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.customerType === 'ENTERPRISE' ? '企业' : '个人' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="industry" label="所属行业" />
|
||||
<el-table-column prop="legalPerson" label="联系人" />
|
||||
<el-table-column prop="phone" label="联系电话" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户编码" prop="customerCode">
|
||||
<el-input v-model="form.customerCode" placeholder="请输入客户编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户名称" prop="customerName">
|
||||
<el-input v-model="form.customerName" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<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="PERSONAL" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属行业">
|
||||
<el-input v-model="form.industry" placeholder="请输入所属行业" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="form.legalPerson" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="地址">
|
||||
<el-input v-model="form.address" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" 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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getCustomerList, createCustomer, updateCustomer, deleteCustomer } from '../../api/customer'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
customerName: '',
|
||||
customerType: ''
|
||||
})
|
||||
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getCustomerList({
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
customerName: searchForm.customerName,
|
||||
customerType: searchForm.customerType
|
||||
})
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('获取客户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.customerName = ''
|
||||
searchForm.customerType = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
customerId: null,
|
||||
customerCode: '',
|
||||
customerName: '',
|
||||
customerType: 'ENTERPRISE',
|
||||
industry: '',
|
||||
legalPerson: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
customerCode: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
|
||||
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
||||
customerType: [{ required: true, message: '请选择客户类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增客户'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑客户'
|
||||
resetForm()
|
||||
Object.assign(form, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.customerId = null
|
||||
form.customerCode = ''
|
||||
form.customerName = ''
|
||||
form.customerType = 'ENTERPRISE'
|
||||
form.industry = ''
|
||||
form.legalPerson = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.address = ''
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (form.customerId) {
|
||||
await updateCustomer(form.customerId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createCustomer(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该客户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteCustomer(row.customerId)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
page.size = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.current = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -1,638 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<!-- 概览卡片 -->
|
||||
<el-row :gutter="20" class="overview-cards">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<el-icon size="28"><Folder /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ data.projectCount || 0 }}</div>
|
||||
<div class="stat-label">项目总数</div>
|
||||
</div>
|
||||
<div class="stat-extra">本月新增 +{{ data.monthNewProjects || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<el-icon size="28"><User /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ data.customerCount || 0 }}</div>
|
||||
<div class="stat-label">客户总数</div>
|
||||
</div>
|
||||
<div class="stat-extra">本月新增 +{{ data.monthNewCustomers || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<el-icon size="28"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ data.contractCount || 0 }}</div>
|
||||
<div class="stat-label">合同总数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<el-icon size="28"><Tickets /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ data.requirementCount || 0 }}</div>
|
||||
<div class="stat-label">需求工单</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 收支概览 -->
|
||||
<el-row :gutter="20" class="finance-cards">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="finance-card income">
|
||||
<div class="finance-title">总收入</div>
|
||||
<div class="finance-value">¥ {{ formatMoney(data.totalIncome) }}</div>
|
||||
<div class="finance-sub">本月收入:¥ {{ formatMoney(data.monthIncome) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="finance-card expense">
|
||||
<div class="finance-title">总支出</div>
|
||||
<div class="finance-value">¥ {{ formatMoney(data.totalExpense) }}</div>
|
||||
<div class="finance-sub">本月支出:¥ {{ formatMoney(data.monthExpense) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="finance-card profit">
|
||||
<div class="finance-title">净利润</div>
|
||||
<div class="finance-value" :class="{ negative: (data.netProfit || 0) < 0 }">
|
||||
¥ {{ formatMoney(data.netProfit) }}
|
||||
</div>
|
||||
<div class="finance-sub">利润率:{{ getProfitRate() }}%</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 应收款概览 -->
|
||||
<el-row :gutter="20" class="receivable-cards">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="receivable-card">
|
||||
<div class="receivable-title">应收款总额</div>
|
||||
<div class="receivable-value">¥ {{ formatMoney(data.totalReceivable) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="receivable-card warning">
|
||||
<div class="receivable-title">待收款金额</div>
|
||||
<div class="receivable-value">¥ {{ formatMoney(data.totalUnpaid) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="receivable-card danger">
|
||||
<div class="receivable-title">逾期金额</div>
|
||||
<div class="receivable-value">¥ {{ formatMoney(data.overdueAmount) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>收支趋势(最近12个月)</span>
|
||||
</template>
|
||||
<div ref="trendChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>项目状态分布</span>
|
||||
</template>
|
||||
<div ref="projectPieRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>支出类型分布</span>
|
||||
</template>
|
||||
<div ref="expensePieRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<span>应收款状态分布</span>
|
||||
</template>
|
||||
<div ref="receivablePieRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { Folder, User, Document, Tickets } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
import { getDashboardData } from '../../api/dashboard'
|
||||
|
||||
// 统计数据
|
||||
const data = reactive({
|
||||
projectCount: 0,
|
||||
customerCount: 0,
|
||||
contractCount: 0,
|
||||
requirementCount: 0,
|
||||
totalIncome: 0,
|
||||
totalExpense: 0,
|
||||
netProfit: 0,
|
||||
totalReceivable: 0,
|
||||
totalUnpaid: 0,
|
||||
overdueAmount: 0,
|
||||
monthIncome: 0,
|
||||
monthExpense: 0,
|
||||
monthNewProjects: 0,
|
||||
monthNewCustomers: 0,
|
||||
incomeTrend: [],
|
||||
expenseTrend: [],
|
||||
projectStatusDistribution: [],
|
||||
expenseTypeDistribution: [],
|
||||
receivableStatusDistribution: []
|
||||
})
|
||||
|
||||
// 图表引用
|
||||
const trendChartRef = ref(null)
|
||||
const projectPieRef = ref(null)
|
||||
const expensePieRef = ref(null)
|
||||
const receivablePieRef = ref(null)
|
||||
|
||||
// 图表实例
|
||||
let trendChart = null
|
||||
let projectPieChart = null
|
||||
let expensePieChart = null
|
||||
let receivablePieChart = null
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value) => {
|
||||
if (value === null || value === undefined) return '0.00'
|
||||
return Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
// 计算利润率
|
||||
const getProfitRate = () => {
|
||||
const income = data.totalIncome || 0
|
||||
if (income === 0) return '0.00'
|
||||
return ((data.netProfit / income) * 100).toFixed(2)
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await getDashboardData()
|
||||
Object.assign(data, res)
|
||||
|
||||
// 渲染图表
|
||||
await nextTick()
|
||||
renderTrendChart()
|
||||
renderProjectPieChart()
|
||||
renderExpensePieChart()
|
||||
renderReceivablePieChart()
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染收支趋势图
|
||||
const renderTrendChart = () => {
|
||||
if (!trendChartRef.value) return
|
||||
|
||||
if (trendChart) {
|
||||
trendChart.dispose()
|
||||
}
|
||||
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
|
||||
const months = data.incomeTrend?.map(item => item.month) || []
|
||||
const incomeData = data.incomeTrend?.map(item => item.amount || 0) || []
|
||||
const expenseData = data.expenseTrend?.map(item => item.amount || 0) || []
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params) => {
|
||||
let result = params[0].axisValue + '<br/>'
|
||||
params.forEach(param => {
|
||||
result += `${param.marker} ${param.seriesName}: ¥${formatMoney(param.value)}<br/>`
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['收入', '支出'],
|
||||
bottom: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: months,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: (value) => {
|
||||
if (value >= 10000) return (value / 10000) + 'w'
|
||||
return value
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: incomeData,
|
||||
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.05)' }
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: expenseData,
|
||||
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.05)' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
trendChart.setOption(option)
|
||||
}
|
||||
|
||||
// 渲染项目状态分布图
|
||||
const renderProjectPieChart = () => {
|
||||
if (!projectPieRef.value) return
|
||||
|
||||
if (projectPieChart) {
|
||||
projectPieChart.dispose()
|
||||
}
|
||||
|
||||
projectPieChart = echarts.init(projectPieRef.value)
|
||||
|
||||
const pieData = data.projectStatusDistribution?.map(item => ({
|
||||
name: item.name,
|
||||
value: item.count || 0
|
||||
})) || []
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
projectPieChart.setOption(option)
|
||||
}
|
||||
|
||||
// 渲染支出类型分布图
|
||||
const renderExpensePieChart = () => {
|
||||
if (!expensePieRef.value) return
|
||||
|
||||
if (expensePieChart) {
|
||||
expensePieChart.dispose()
|
||||
}
|
||||
|
||||
expensePieChart = echarts.init(expensePieRef.value)
|
||||
|
||||
const pieData = data.expenseTypeDistribution?.map(item => ({
|
||||
name: item.name,
|
||||
value: item.amount || 0
|
||||
})) || []
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params) => {
|
||||
return `${params.name}: ¥${formatMoney(params.value)} (${params.percent}%)`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expensePieChart.setOption(option)
|
||||
}
|
||||
|
||||
// 渲染应收款状态分布图
|
||||
const renderReceivablePieChart = () => {
|
||||
if (!receivablePieRef.value) return
|
||||
|
||||
if (receivablePieChart) {
|
||||
receivablePieChart.dispose()
|
||||
}
|
||||
|
||||
receivablePieChart = echarts.init(receivablePieRef.value)
|
||||
|
||||
const pieData = data.receivableStatusDistribution?.map(item => ({
|
||||
name: item.name,
|
||||
value: item.amount || 0
|
||||
})) || []
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params) => {
|
||||
return `${params.name}: ¥${formatMoney(params.value)} (${params.percent}%)`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
receivablePieChart.setOption(option)
|
||||
}
|
||||
|
||||
// 窗口大小变化时重绘图表
|
||||
const handleResize = () => {
|
||||
trendChart?.resize()
|
||||
projectPieChart?.resize()
|
||||
expensePieChart?.resize()
|
||||
receivablePieChart?.resize()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
trendChart?.dispose()
|
||||
projectPieChart?.dispose()
|
||||
expensePieChart?.dispose()
|
||||
receivablePieChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* 概览卡片 */
|
||||
.overview-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-card :deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-extra {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
font-size: 12px;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
/* 收支卡片 */
|
||||
.finance-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.finance-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.finance-card :deep(.el-card__body) {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.finance-title {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.finance-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.finance-value.negative {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.finance-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.finance-card.income .finance-value {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.finance-card.expense .finance-value {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.finance-card.profit .finance-value {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
/* 应收款卡片 */
|
||||
.receivable-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.receivable-card {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.receivable-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.receivable-title {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.receivable-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.receivable-card.warning .receivable-value {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.receivable-card.danger .receivable-value {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 图表区域 */
|
||||
.chart-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 350px;
|
||||
}
|
||||
</style>
|
||||
@ -1,506 +0,0 @@
|
||||
<template>
|
||||
<div class="expense-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="支出编号">
|
||||
<el-input v-model="searchForm.expenseCode" placeholder="请输入支出编号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="支出类型">
|
||||
<el-tree-select
|
||||
v-model="searchForm.expenseTypeId"
|
||||
:data="expenseTypeOptions"
|
||||
:props="{ label: 'typeName', value: 'typeId', children: 'children' }"
|
||||
placeholder="请选择支出类型"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目">
|
||||
<el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable>
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="待付款" value="pending" />
|
||||
<el-option label="已付款" value="paid" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleAdd">新增支出</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="expenseCode" label="支出编号" width="150" />
|
||||
<el-table-column prop="expenseTypeId" label="支出类型" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getExpenseTypeName(row.expenseTypeId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expenseAmount" label="支出金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.expenseAmount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expenseDate" label="支出日期" width="120" />
|
||||
<el-table-column prop="expenseReason" label="支出事由" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="projectId" label="所属项目" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getProjectName(row.projectId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="paymentMethod" label="付款方式" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getPaymentMethodText(row.paymentMethod) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'pending'" type="warning">待付款</el-tag>
|
||||
<el-tag v-else-if="row.status === 'paid'" type="primary">已付款</el-tag>
|
||||
<el-tag v-else-if="row.status === 'completed'" type="success">已完成</el-tag>
|
||||
<el-tag v-else type="info">已作废</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="info" size="small" @click="handleUpdateStatus(row)">更新状态</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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
|
||||
v-model="dialogVisible"
|
||||
:title="form.expenseId ? '编辑支出' : '新增支出'"
|
||||
width="700px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支出编号" prop="expenseCode">
|
||||
<el-input v-model="form.expenseCode" placeholder="请输入支出编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支出类型" prop="expenseTypeId">
|
||||
<el-tree-select
|
||||
v-model="form.expenseTypeId"
|
||||
:data="expenseTypeOptions"
|
||||
:props="{ label: 'typeName', value: 'typeId', children: 'children' }"
|
||||
placeholder="请选择支出类型"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支出金额" prop="expenseAmount">
|
||||
<el-input-number v-model="form.expenseAmount" :min="0" :precision="2" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<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-row>
|
||||
|
||||
<el-form-item label="支出事由" prop="expenseReason">
|
||||
<el-input
|
||||
v-model="form.expenseReason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入支出事由"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属项目" prop="projectId">
|
||||
<el-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;" clearable>
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="付款方式" prop="paymentMethod">
|
||||
<el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%;">
|
||||
<el-option label="转账" value="transfer" />
|
||||
<el-option label="现金" value="cash" />
|
||||
<el-option label="支票" value="check" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="付款账户" prop="paymentAccount">
|
||||
<el-input v-model="form.paymentAccount" placeholder="请输入付款账户" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注说明" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 更新状态对话框 -->
|
||||
<el-dialog v-model="statusDialogVisible" title="更新状态" width="400px">
|
||||
<el-form>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="statusForm.status" placeholder="请选择状态" style="width: 100%;">
|
||||
<el-option label="待付款" value="pending" />
|
||||
<el-option label="已付款" value="paid" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitStatus">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getExpenseList,
|
||||
createExpense,
|
||||
updateExpense,
|
||||
deleteExpense,
|
||||
updateExpenseStatus
|
||||
} from '../../api/expense'
|
||||
import { getExpenseTypeTree } from '../../api/expenseType'
|
||||
import { getProjectList } from '../../api/project'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
expenseCode: '',
|
||||
expenseTypeId: null,
|
||||
projectId: null,
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 支出类型选项
|
||||
const expenseTypeOptions = ref([])
|
||||
// 项目选项
|
||||
const projectOptions = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
expenseId: null,
|
||||
expenseCode: '',
|
||||
expenseTypeId: null,
|
||||
expenseAmount: 0,
|
||||
expenseDate: null,
|
||||
expenseReason: '',
|
||||
projectId: null,
|
||||
paymentMethod: 'transfer',
|
||||
paymentAccount: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
expenseCode: [
|
||||
{ required: true, message: '请输入支出编号', trigger: 'blur' }
|
||||
],
|
||||
expenseTypeId: [
|
||||
{ required: true, message: '请选择支出类型', trigger: 'change' }
|
||||
],
|
||||
expenseAmount: [
|
||||
{ required: true, message: '请输入支出金额', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态对话框
|
||||
const statusDialogVisible = ref(false)
|
||||
const statusForm = reactive({
|
||||
expenseId: null,
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 加载支出类型
|
||||
const loadExpenseTypes = async () => {
|
||||
try {
|
||||
const res = await getExpenseTypeTree()
|
||||
expenseTypeOptions.value = res
|
||||
} catch (error) {
|
||||
console.error('加载支出类型失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载项目列表
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectList({ current: 1, size: 1000 })
|
||||
projectOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载项目列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取支出类型名称
|
||||
const getExpenseTypeName = (typeId) => {
|
||||
const findType = (types) => {
|
||||
for (const type of types) {
|
||||
if (type.typeId === typeId) return type.typeName
|
||||
if (type.children) {
|
||||
const found = findType(type.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findType(expenseTypeOptions.value) || '-'
|
||||
}
|
||||
|
||||
// 获取项目名称
|
||||
const getProjectName = (projectId) => {
|
||||
const project = projectOptions.value.find(p => p.projectId === projectId)
|
||||
return project ? project.projectName : '-'
|
||||
}
|
||||
|
||||
// 获取付款方式文本
|
||||
const getPaymentMethodText = (method) => {
|
||||
const map = {
|
||||
transfer: '转账',
|
||||
cash: '现金',
|
||||
check: '支票',
|
||||
other: '其他'
|
||||
}
|
||||
return map[method] || method
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
expenseCode: searchForm.expenseCode || undefined,
|
||||
expenseTypeId: searchForm.expenseTypeId || undefined,
|
||||
projectId: searchForm.projectId || undefined,
|
||||
status: searchForm.status || undefined
|
||||
}
|
||||
const res = await getExpenseList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.expenseCode = ''
|
||||
searchForm.expenseTypeId = null
|
||||
searchForm.projectId = null
|
||||
searchForm.status = ''
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, {
|
||||
expenseId: null,
|
||||
expenseCode: '',
|
||||
expenseTypeId: null,
|
||||
expenseAmount: 0,
|
||||
expenseDate: null,
|
||||
expenseReason: '',
|
||||
projectId: null,
|
||||
paymentMethod: 'transfer',
|
||||
paymentAccount: '',
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, {
|
||||
expenseId: row.expenseId,
|
||||
expenseCode: row.expenseCode,
|
||||
expenseTypeId: row.expenseTypeId,
|
||||
expenseAmount: row.expenseAmount,
|
||||
expenseDate: row.expenseDate,
|
||||
expenseReason: row.expenseReason,
|
||||
projectId: row.projectId,
|
||||
paymentMethod: row.paymentMethod,
|
||||
paymentAccount: row.paymentAccount,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.expenseId) {
|
||||
await updateExpense(form.expenseId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createExpense(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const handleUpdateStatus = (row) => {
|
||||
statusForm.expenseId = row.expenseId
|
||||
statusForm.status = row.status
|
||||
statusDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交状态
|
||||
const handleSubmitStatus = async () => {
|
||||
try {
|
||||
await updateExpenseStatus(statusForm.expenseId, statusForm.status)
|
||||
ElMessage.success('状态更新成功')
|
||||
statusDialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error(error.message || '状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该支出吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteExpense(row.expenseId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果是最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadExpenseTypes()
|
||||
loadProjects()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.expense-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,295 +0,0 @@
|
||||
<template>
|
||||
<div class="expense-type-container">
|
||||
<el-card>
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleAdd">新增类型</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 树形表格 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
row-key="typeId"
|
||||
border
|
||||
v-loading="loading"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
default-expand-all
|
||||
>
|
||||
<el-table-column prop="typeName" label="类型名称" width="250" />
|
||||
<el-table-column prop="typeCode" label="类型编码" width="150" />
|
||||
<el-table-column prop="typeLevel" label="层级" width="80" align="center" />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleAddChild(row)">添加子类型</el-button>
|
||||
<el-button type="info" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
:type="row.status === 1 ? 'warning' : 'success'"
|
||||
size="small"
|
||||
@click="handleToggleStatus(row)"
|
||||
>
|
||||
{{ row.status === 1 ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="form.typeId ? '编辑支出类型' : '新增支出类型'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="上级类型" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="typeTreeOptions"
|
||||
:props="{ label: 'typeName', value: 'typeId', children: 'children' }"
|
||||
placeholder="请选择上级类型(留空为一级类型)"
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="类型名称" prop="typeName">
|
||||
<el-input v-model="form.typeName" placeholder="请输入类型名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="类型编码" prop="typeCode">
|
||||
<el-input v-model="form.typeCode" placeholder="请输入类型编码" />
|
||||
</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="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getAllExpenseTypeTree,
|
||||
createExpenseType,
|
||||
updateExpenseType,
|
||||
deleteExpenseType,
|
||||
updateExpenseTypeStatus
|
||||
} from '../../api/expenseType'
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 类型树选项(用于选择上级类型)
|
||||
const typeTreeOptions = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
typeId: null,
|
||||
parentId: null,
|
||||
typeName: '',
|
||||
typeCode: '',
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
typeName: [
|
||||
{ required: true, message: '请输入类型名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getAllExpenseTypeTree()
|
||||
tableData.value = res
|
||||
// 更新类型树选项(用于选择上级)
|
||||
typeTreeOptions.value = [
|
||||
{
|
||||
typeId: 0,
|
||||
typeName: '无(一级类型)',
|
||||
children: res
|
||||
}
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, {
|
||||
typeId: null,
|
||||
parentId: null,
|
||||
typeName: '',
|
||||
typeCode: '',
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
description: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 添加子类型
|
||||
const handleAddChild = (row) => {
|
||||
Object.assign(form, {
|
||||
typeId: null,
|
||||
parentId: row.typeId,
|
||||
typeName: '',
|
||||
typeCode: '',
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
description: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, {
|
||||
typeId: row.typeId,
|
||||
parentId: row.parentId === 0 ? null : row.parentId,
|
||||
typeName: row.typeName,
|
||||
typeCode: row.typeCode,
|
||||
sortOrder: row.sortOrder,
|
||||
status: row.status,
|
||||
description: row.description
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果 parentId 为 null 或 0,设置为 0
|
||||
const submitData = {
|
||||
...form,
|
||||
parentId: form.parentId || 0
|
||||
}
|
||||
|
||||
if (form.typeId) {
|
||||
await updateExpenseType(form.typeId, submitData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createExpenseType(submitData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const handleToggleStatus = async (row) => {
|
||||
try {
|
||||
const newStatus = row.status === 1 ? 0 : 1
|
||||
const statusText = newStatus === 1 ? '启用' : '禁用'
|
||||
|
||||
await ElMessageBox.confirm(`确定要${statusText}该类型吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await updateExpenseTypeStatus(row.typeId, newStatus)
|
||||
ElMessage.success(`${statusText}成功`)
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error(error.message || '状态更新失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该类型及其所有子类型吗?此操作不可恢复!', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteExpenseType(row.typeId)
|
||||
ElMessage.success('删除成功')
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.expense-type-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,391 +0,0 @@
|
||||
<template>
|
||||
<div class="receipt-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="收款编号">
|
||||
<el-input v-model="searchForm.receiptCode" placeholder="请输入编号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="应收款">
|
||||
<el-select v-model="searchForm.receivableId" placeholder="请选择应收款" clearable>
|
||||
<el-option
|
||||
v-for="receivable in receivableOptions"
|
||||
:key="receivable.receivableId"
|
||||
:label="receivable.receivableCode"
|
||||
:value="receivable.receivableId"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleAdd">新增收款记录</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="receiptCode" label="收款编号" width="150" />
|
||||
<el-table-column prop="receivableId" label="应收款" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getReceivableName(row.receivableId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receiptAmount" label="收款金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.receiptAmount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receiptDate" label="收款日期" width="120" />
|
||||
<el-table-column prop="receiptMethod" label="收款方式" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getReceiptMethodText(row.receiptMethod) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payerName" label="付款方" width="150" />
|
||||
<el-table-column prop="receiptAccount" label="收款账户" width="180" />
|
||||
<el-table-column prop="receiptVoucher" label="收款凭证" width="120" />
|
||||
<el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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
|
||||
v-model="dialogVisible"
|
||||
:title="form.receiptId ? '编辑收款记录' : '新增收款记录'"
|
||||
width="700px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款编号" prop="receiptCode">
|
||||
<el-input v-model="form.receiptCode" placeholder="请输入编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="应收款" prop="receivableId">
|
||||
<el-select v-model="form.receivableId" placeholder="请选择应收款" style="width: 100%;" clearable>
|
||||
<el-option
|
||||
v-for="receivable in receivableOptions"
|
||||
:key="receivable.receivableId"
|
||||
:label="receivable.receivableCode"
|
||||
:value="receivable.receivableId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款金额" prop="receiptAmount">
|
||||
<el-input-number v-model="form.receiptAmount" :min="0" :precision="2" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款日期" prop="receiptDate">
|
||||
<el-date-picker
|
||||
v-model="form.receiptDate"
|
||||
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="receiptMethod">
|
||||
<el-select v-model="form.receiptMethod" placeholder="请选择收款方式" style="width: 100%;">
|
||||
<el-option label="转账" value="transfer" />
|
||||
<el-option label="现金" value="cash" />
|
||||
<el-option label="支票" value="check" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款账户" prop="receiptAccount">
|
||||
<el-input v-model="form.receiptAccount" placeholder="请输入收款账户" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="付款方名称" prop="payerName">
|
||||
<el-input v-model="form.payerName" placeholder="请输入付款方名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款凭证" prop="receiptVoucher">
|
||||
<el-input v-model="form.receiptVoucher" placeholder="请输入凭证编号或URL" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注说明" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getReceiptList,
|
||||
createReceipt,
|
||||
updateReceipt,
|
||||
deleteReceipt
|
||||
} from '../../api/receipt'
|
||||
import { getReceivableList } from '../../api/receivable'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
receiptCode: '',
|
||||
receivableId: null
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 应收款选项
|
||||
const receivableOptions = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
receiptId: null,
|
||||
receiptCode: '',
|
||||
receivableId: null,
|
||||
receiptAmount: 0,
|
||||
receiptDate: null,
|
||||
receiptMethod: 'transfer',
|
||||
receiptAccount: '',
|
||||
payerName: '',
|
||||
receiptVoucher: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
receiptCode: [
|
||||
{ required: true, message: '请输入收款编号', trigger: 'blur' }
|
||||
],
|
||||
receivableId: [
|
||||
{ required: true, message: '请选择应收款', trigger: 'change' }
|
||||
],
|
||||
receiptAmount: [
|
||||
{ required: true, message: '请输入收款金额', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 加载应收款列表
|
||||
const loadReceivables = async () => {
|
||||
try {
|
||||
const res = await getReceivableList({ current: 1, size: 1000 })
|
||||
receivableOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载应收款列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取应收款名称
|
||||
const getReceivableName = (receivableId) => {
|
||||
const receivable = receivableOptions.value.find(r => r.receivableId === receivableId)
|
||||
return receivable ? receivable.receivableCode : '-'
|
||||
}
|
||||
|
||||
// 获取收款方式文本
|
||||
const getReceiptMethodText = (method) => {
|
||||
const methodMap = {
|
||||
'transfer': '转账',
|
||||
'cash': '现金',
|
||||
'check': '支票',
|
||||
'other': '其他'
|
||||
}
|
||||
return methodMap[method] || method
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
receiptCode: searchForm.receiptCode || undefined,
|
||||
receivableId: searchForm.receivableId || undefined
|
||||
}
|
||||
const res = await getReceiptList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.receiptCode = ''
|
||||
searchForm.receivableId = null
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, {
|
||||
receiptId: null,
|
||||
receiptCode: '',
|
||||
receivableId: null,
|
||||
receiptAmount: 0,
|
||||
receiptDate: null,
|
||||
receiptMethod: 'transfer',
|
||||
receiptAccount: '',
|
||||
payerName: '',
|
||||
receiptVoucher: '',
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, {
|
||||
receiptId: row.receiptId,
|
||||
receiptCode: row.receiptCode,
|
||||
receivableId: row.receivableId,
|
||||
receiptAmount: row.receiptAmount,
|
||||
receiptDate: row.receiptDate,
|
||||
receiptMethod: row.receiptMethod,
|
||||
receiptAccount: row.receiptAccount,
|
||||
payerName: row.payerName,
|
||||
receiptVoucher: row.receiptVoucher,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.receiptId) {
|
||||
await updateReceipt(form.receiptId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createReceipt(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该收款记录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteReceipt(row.receiptId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果是最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadReceivables()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.receipt-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,540 +0,0 @@
|
||||
<template>
|
||||
<div class="receivable-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="应收款编号">
|
||||
<el-input v-model="searchForm.receivableCode" placeholder="请输入编号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目">
|
||||
<el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable>
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="待收款" value="pending" />
|
||||
<el-option label="部分收款" value="partial" />
|
||||
<el-option label="已收款" value="received" />
|
||||
<el-option label="逾期" value="overdue" />
|
||||
</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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleAdd">新增应收款</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="receivableCode" label="应收款编号" width="150" />
|
||||
<el-table-column prop="requirementId" label="需求工单" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getRequirementName(row.requirementId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="customerId" label="客户" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getCustomerName(row.customerId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectId" label="项目" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getProjectName(row.projectId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receivableAmount" label="应收款金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.receivableAmount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receivedAmount" label="已收款金额" width="120">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.receivedAmount }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unpaidAmount" label="未收款金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.unpaidAmount > 0 ? 'red' : 'green' }">
|
||||
¥{{ row.unpaidAmount }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receivableDate" label="应收款日期" width="120" />
|
||||
<el-table-column prop="paymentDueDate" label="付款截止日期" width="130" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'pending'" type="warning">待收款</el-tag>
|
||||
<el-tag v-else-if="row.status === 'partial'" type="primary">部分收款</el-tag>
|
||||
<el-tag v-else-if="row.status === 'received'" type="success">已收款</el-tag>
|
||||
<el-tag v-else type="danger">逾期</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="overdueDays" label="逾期天数" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.overdueDays > 0" style="color: red;">{{ row.overdueDays }}天</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="success" size="small" @click="handleRecordPayment(row)">记录收款</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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
|
||||
v-model="dialogVisible"
|
||||
:title="form.receivableId ? '编辑应收款' : '新增应收款'"
|
||||
width="700px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="130px">
|
||||
<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="requirementId">
|
||||
<el-select v-model="form.requirementId" placeholder="请选择需求工单" style="width: 100%;" clearable>
|
||||
<el-option
|
||||
v-for="req in requirementOptions"
|
||||
:key="req.requirementId"
|
||||
:label="req.requirementName"
|
||||
:value="req.requirementId"
|
||||
/>
|
||||
</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="请选择项目" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户" prop="customerId">
|
||||
<el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%;" clearable>
|
||||
<el-option
|
||||
v-for="customer in customerOptions"
|
||||
:key="customer.customerId"
|
||||
:label="customer.customerName"
|
||||
:value="customer.customerId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="应收款金额" prop="receivableAmount">
|
||||
<el-input-number v-model="form.receivableAmount" :min="0" :precision="2" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="应收款日期" prop="receivableDate">
|
||||
<el-date-picker
|
||||
v-model="form.receivableDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="付款截止日期" prop="paymentDueDate">
|
||||
<el-date-picker
|
||||
v-model="form.paymentDueDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="付款方式" prop="paymentMethod">
|
||||
<el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%;">
|
||||
<el-option label="转账" value="transfer" />
|
||||
<el-option label="现金" value="cash" />
|
||||
<el-option label="支票" value="check" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收款账户" prop="bankAccount">
|
||||
<el-input v-model="form.bankAccount" placeholder="请输入收款账户" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注说明" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 记录收款对话框 -->
|
||||
<el-dialog v-model="paymentDialogVisible" title="记录收款" width="400px">
|
||||
<el-form>
|
||||
<el-form-item label="收款金额">
|
||||
<el-input-number v-model="paymentForm.amount" :min="0" :precision="2" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="paymentDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitPayment">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getReceivableList,
|
||||
createReceivable,
|
||||
updateReceivable,
|
||||
deleteReceivable,
|
||||
recordPayment
|
||||
} from '../../api/receivable'
|
||||
import { getProjectList } from '../../api/project'
|
||||
import { getRequirementList } from '../../api/requirement'
|
||||
import { getCustomerList } from '../../api/customer'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
receivableCode: '',
|
||||
projectId: null,
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 项目选项
|
||||
const projectOptions = ref([])
|
||||
|
||||
// 需求选项
|
||||
const requirementOptions = ref([])
|
||||
|
||||
// 客户选项
|
||||
const customerOptions = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
receivableId: null,
|
||||
receivableCode: '',
|
||||
requirementId: null,
|
||||
projectId: null,
|
||||
customerId: null,
|
||||
receivableAmount: 0,
|
||||
receivableDate: null,
|
||||
paymentDueDate: null,
|
||||
paymentMethod: 'transfer',
|
||||
bankAccount: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
receivableCode: [
|
||||
{ required: true, message: '请输入应收款编号', trigger: 'blur' }
|
||||
],
|
||||
requirementId: [
|
||||
{ required: true, message: '请选择需求工单', trigger: 'change' }
|
||||
],
|
||||
projectId: [
|
||||
{ required: true, message: '请选择项目', trigger: 'change' }
|
||||
],
|
||||
customerId: [
|
||||
{ required: true, message: '请选择客户', trigger: 'change' }
|
||||
],
|
||||
receivableAmount: [
|
||||
{ required: true, message: '请输入应收款金额', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 记录收款对话框
|
||||
const paymentDialogVisible = ref(false)
|
||||
const paymentForm = reactive({
|
||||
receivableId: null,
|
||||
amount: 0
|
||||
})
|
||||
|
||||
// 加载项目列表
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectList({ current: 1, size: 1000 })
|
||||
projectOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载项目列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载需求列表
|
||||
const loadRequirements = async () => {
|
||||
try {
|
||||
const res = await getRequirementList({ current: 1, size: 1000 })
|
||||
requirementOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载需求列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载客户列表
|
||||
const loadCustomers = async () => {
|
||||
try {
|
||||
const res = await getCustomerList({ current: 1, size: 1000 })
|
||||
customerOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载客户列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项目名称
|
||||
const getProjectName = (projectId) => {
|
||||
const project = projectOptions.value.find(p => p.projectId === projectId)
|
||||
return project ? project.projectName : '-'
|
||||
}
|
||||
|
||||
// 获取需求名称
|
||||
const getRequirementName = (requirementId) => {
|
||||
const requirement = requirementOptions.value.find(r => r.requirementId === requirementId)
|
||||
return requirement ? requirement.requirementName : '-'
|
||||
}
|
||||
|
||||
// 获取客户名称
|
||||
const getCustomerName = (customerId) => {
|
||||
const customer = customerOptions.value.find(c => c.customerId === customerId)
|
||||
return customer ? customer.customerName : '-'
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
receivableCode: searchForm.receivableCode || undefined,
|
||||
projectId: searchForm.projectId || undefined,
|
||||
status: searchForm.status || undefined
|
||||
}
|
||||
const res = await getReceivableList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.receivableCode = ''
|
||||
searchForm.projectId = null
|
||||
searchForm.status = ''
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, {
|
||||
receivableId: null,
|
||||
receivableCode: '',
|
||||
requirementId: null,
|
||||
projectId: null,
|
||||
customerId: null,
|
||||
receivableAmount: 0,
|
||||
receivableDate: null,
|
||||
paymentDueDate: null,
|
||||
paymentMethod: 'transfer',
|
||||
bankAccount: '',
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, {
|
||||
receivableId: row.receivableId,
|
||||
receivableCode: row.receivableCode,
|
||||
requirementId: row.requirementId,
|
||||
projectId: row.projectId,
|
||||
customerId: row.customerId,
|
||||
receivableAmount: row.receivableAmount,
|
||||
receivableDate: row.receivableDate,
|
||||
paymentDueDate: row.paymentDueDate,
|
||||
paymentMethod: row.paymentMethod,
|
||||
bankAccount: row.bankAccount,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.receivableId) {
|
||||
await updateReceivable(form.receivableId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createReceivable(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 记录收款
|
||||
const handleRecordPayment = (row) => {
|
||||
paymentForm.receivableId = row.receivableId
|
||||
paymentForm.amount = 0
|
||||
paymentDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交收款
|
||||
const handleSubmitPayment = async () => {
|
||||
if (paymentForm.amount <= 0) {
|
||||
ElMessage.warning('请输入有效的收款金额')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await recordPayment(paymentForm.receivableId, paymentForm.amount)
|
||||
ElMessage.success('收款记录成功')
|
||||
paymentDialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('收款记录失败:', error)
|
||||
ElMessage.error(error.message || '收款记录失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该应收款吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteReceivable(row.receivableId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果是最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadProjects()
|
||||
loadRequirements()
|
||||
loadCustomers()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.receivable-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,139 +0,0 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-box">
|
||||
<template #header>
|
||||
<div class="login-header">
|
||||
<h2>资金服务平台</h2>
|
||||
<p>Fund Service Platform</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
class="login-form"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
:prefix-icon="User"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
:prefix-icon="Lock"
|
||||
size="large"
|
||||
show-password
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="login-btn"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import { login } from '../../api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const loginFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
tenantCode: 'default'
|
||||
})
|
||||
|
||||
const loginRules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
await loginFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login(loginForm)
|
||||
userStore.setToken(res.accessToken)
|
||||
userStore.setUserInfo(res.userInfo)
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 400px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -1,343 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>合同管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增合同</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="合同名称">
|
||||
<el-input v-model="searchForm.contractName" placeholder="请输入合同名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同状态">
|
||||
<el-select v-model="searchForm.contractStatus" placeholder="请选择" clearable>
|
||||
<el-option label="草稿" value="DRAFT" />
|
||||
<el-option label="已签署" value="SIGNED" />
|
||||
<el-option label="执行中" value="EXECUTING" />
|
||||
<el-option label="已完成" value="COMPLETED" />
|
||||
<el-option label="已终止" value="TERMINATED" />
|
||||
</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>
|
||||
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="contractCode" label="合同编码" width="120" />
|
||||
<el-table-column prop="contractName" label="合同名称" />
|
||||
<el-table-column prop="contractStatus" label="合同状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.contractStatus)">
|
||||
{{ getStatusText(scope.row.contractStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contractAmount" label="合同金额" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.contractAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="signDate" label="签署日期" width="120" />
|
||||
<el-table-column prop="effectiveDate" label="生效日期" width="120" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="合同编码" prop="contractCode">
|
||||
<el-input v-model="form.contractCode" placeholder="请输入合同编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
<el-input v-model="form.contractName" placeholder="请输入合同名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<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-col :span="12">
|
||||
<el-form-item label="合同类型">
|
||||
<el-select v-model="form.contractType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="销售合同" value="SALES" />
|
||||
<el-option label="采购合同" value="PURCHASE" />
|
||||
<el-option label="服务合同" value="SERVICE" />
|
||||
<el-option label="其他" value="OTHER" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="签署日期">
|
||||
<el-date-picker v-model="form.signDate" type="date" placeholder="选择日期" style="width: 100%" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生效日期">
|
||||
<el-date-picker v-model="form.effectiveDate" type="date" placeholder="选择日期" style="width: 100%" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="到期日期">
|
||||
<el-date-picker v-model="form.expiryDate" type="date" placeholder="选择日期" style="width: 100%" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="签署地点">
|
||||
<el-input v-model="form.signLocation" placeholder="请输入签署地点" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="付款条款">
|
||||
<el-input v-model="form.paymentTerms" type="textarea" rows="2" placeholder="请输入付款条款" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" 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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getContractList, createContract, updateContract, deleteContract } from '../../api/contract'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
contractName: '',
|
||||
contractStatus: ''
|
||||
})
|
||||
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
contractId: null,
|
||||
contractCode: '',
|
||||
contractName: '',
|
||||
contractType: 'SALES',
|
||||
contractAmount: 0,
|
||||
signDate: '',
|
||||
effectiveDate: '',
|
||||
expiryDate: '',
|
||||
signLocation: '',
|
||||
paymentTerms: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
contractCode: [{ required: true, message: '请输入合同编码', trigger: 'blur' }],
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractAmount: [{ required: true, message: '请输入合同金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getContractList({
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
contractName: searchForm.contractName,
|
||||
contractStatus: searchForm.contractStatus
|
||||
})
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('获取合同列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.contractName = ''
|
||||
searchForm.contractStatus = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增合同'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑合同'
|
||||
resetForm()
|
||||
Object.assign(form, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.contractId = null
|
||||
form.contractCode = ''
|
||||
form.contractName = ''
|
||||
form.contractType = 'SALES'
|
||||
form.contractAmount = 0
|
||||
form.signDate = ''
|
||||
form.effectiveDate = ''
|
||||
form.expiryDate = ''
|
||||
form.signLocation = ''
|
||||
form.paymentTerms = ''
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.contractId) {
|
||||
await updateContract(form.contractId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createContract(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该合同吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteContract(row.contractId)
|
||||
ElMessage.success('删除成功')
|
||||
// 如果删除的是当前页最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
page.size = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.current = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const map = {
|
||||
'DRAFT': 'info',
|
||||
'SIGNED': 'warning',
|
||||
'EXECUTING': 'primary',
|
||||
'COMPLETED': 'success',
|
||||
'TERMINATED': 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const map = {
|
||||
'DRAFT': '草稿',
|
||||
'SIGNED': '已签署',
|
||||
'EXECUTING': '执行中',
|
||||
'COMPLETED': '已完成',
|
||||
'TERMINATED': '已终止'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '¥0.00'
|
||||
return '¥' + parseFloat(amount).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -1,392 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>项目管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增项目</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="项目名称">
|
||||
<el-input v-model="searchForm.projectName" placeholder="请输入项目名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目状态">
|
||||
<el-select v-model="searchForm.projectStatus" placeholder="请选择" clearable>
|
||||
<el-option label="草稿" value="DRAFT" />
|
||||
<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" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="projectCode" label="项目编码" width="120" />
|
||||
<el-table-column prop="projectName" label="项目名称" />
|
||||
<el-table-column prop="projectStatus" label="项目状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.projectStatus)">
|
||||
{{ getStatusText(scope.row.projectStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="budgetAmount" label="预算金额" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.budgetAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contractAmount" label="合同金额" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.contractAmount) }}
|
||||
</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="120" />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="success" link @click="handleUpdateStatus(scope.row)">状态</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" 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="projectShort">
|
||||
<el-input v-model="form.projectShort" placeholder="请输入项目简称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目类型">
|
||||
<el-select v-model="form.projectType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="开发项目" value="development" />
|
||||
<el-option label="维护项目" value="maintenance" />
|
||||
<el-option label="咨询项目" value="consulting" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预算金额" prop="budgetAmount">
|
||||
<el-input-number v-model="form.budgetAmount" :precision="2" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="合同金额">
|
||||
<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%" value-format="YYYY-MM-DD" />
|
||||
</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%" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="项目描述">
|
||||
<el-input v-model="form.description" type="textarea" rows="3" placeholder="请输入项目描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" 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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 状态更新对话框 -->
|
||||
<el-dialog v-model="statusDialogVisible" title="更新项目状态" width="400px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="项目状态">
|
||||
<el-select v-model="statusForm.projectStatus" placeholder="请选择状态" style="width: 100%">
|
||||
<el-option label="草稿" value="DRAFT" />
|
||||
<el-option label="进行中" value="IN_PROGRESS" />
|
||||
<el-option label="已完成" value="COMPLETED" />
|
||||
<el-option label="已取消" value="CANCELLED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleStatusSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getProjectList, createProject, updateProject, deleteProject, updateProjectStatus } from '../../api/project'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
projectName: '',
|
||||
projectStatus: ''
|
||||
})
|
||||
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
projectId: null,
|
||||
projectCode: '',
|
||||
projectName: '',
|
||||
projectShort: '',
|
||||
customerId: null,
|
||||
projectType: 'development',
|
||||
projectManagerId: null,
|
||||
budgetAmount: 0,
|
||||
contractAmount: 0,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
description: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
projectCode: [{ required: true, message: '请输入项目编码', trigger: 'blur' }],
|
||||
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
|
||||
budgetAmount: [{ required: true, message: '请输入项目预算', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const statusDialogVisible = ref(false)
|
||||
const statusForm = reactive({
|
||||
projectId: null,
|
||||
projectStatus: ''
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProjectList({
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
projectName: searchForm.projectName,
|
||||
projectStatus: searchForm.projectStatus
|
||||
})
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('获取项目列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.projectName = ''
|
||||
searchForm.projectStatus = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增项目'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑项目'
|
||||
resetForm()
|
||||
Object.assign(form, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.projectId = null
|
||||
form.projectCode = ''
|
||||
form.projectName = ''
|
||||
form.projectShort = ''
|
||||
form.customerId = null
|
||||
form.projectType = 'development'
|
||||
form.projectManagerId = null
|
||||
form.budgetAmount = 0
|
||||
form.contractAmount = 0
|
||||
form.startDate = ''
|
||||
form.endDate = ''
|
||||
form.description = ''
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.projectId) {
|
||||
await updateProject(form.projectId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createProject(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateStatus = (row) => {
|
||||
statusForm.projectId = row.projectId
|
||||
statusForm.projectStatus = row.projectStatus
|
||||
statusDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleStatusSubmit = async () => {
|
||||
try {
|
||||
await updateProjectStatus(statusForm.projectId, statusForm.projectStatus)
|
||||
ElMessage.success('状态更新成功')
|
||||
statusDialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error(error.message || '状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该项目吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteProject(row.projectId)
|
||||
ElMessage.success('删除成功')
|
||||
// 如果删除的是当前页最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
page.size = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.current = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const map = {
|
||||
'DRAFT': 'info',
|
||||
'IN_PROGRESS': 'primary',
|
||||
'COMPLETED': 'success',
|
||||
'CANCELLED': 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const map = {
|
||||
'DRAFT': '草稿',
|
||||
'IN_PROGRESS': '进行中',
|
||||
'COMPLETED': '已完成',
|
||||
'CANCELLED': '已取消'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '¥0.00'
|
||||
return '¥' + parseFloat(amount).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -1,393 +0,0 @@
|
||||
<template>
|
||||
<div class="member-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="项目">
|
||||
<el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable @change="handleSearch">
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable @change="handleSearch">
|
||||
<el-option label="项目经理" value="pm" />
|
||||
<el-option label="开发" value="dev" />
|
||||
<el-option label="测试" value="test" />
|
||||
<el-option label="财务" value="finance" />
|
||||
<el-option label="普通成员" value="member" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable @change="handleSearch">
|
||||
<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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleAdd" :disabled="!searchForm.projectId">添加成员</el-button>
|
||||
<el-alert
|
||||
v-if="!searchForm.projectId"
|
||||
title="请先选择项目"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-top: 10px;"
|
||||
/>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="userId" label="用户ID" width="100" />
|
||||
<el-table-column prop="role" label="项目角色" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getRoleText(row.role) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="joinDate" label="加入日期" width="120" />
|
||||
<el-table-column prop="leaveDate" label="离开日期" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.leaveDate || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="workload" label="工作量占比" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.workload }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">在职</el-tag>
|
||||
<el-tag v-else type="info">已离开</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleUpdateStatus(row, 0)"
|
||||
>标记离开</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleUpdateStatus(row, 1)"
|
||||
>恢复在职</el-button>
|
||||
<el-button type="danger" size="small" @click="handleRemove(row)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="form.memberId ? '编辑项目成员' : '添加项目成员'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="项目" prop="projectId">
|
||||
<el-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;" disabled>
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户ID" prop="userId">
|
||||
<el-input-number v-model="form.userId" :min="1" style="width: 100%;" placeholder="请输入用户ID" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="项目角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="请选择角色" style="width: 100%;">
|
||||
<el-option label="项目经理" value="pm" />
|
||||
<el-option label="开发" value="dev" />
|
||||
<el-option label="测试" value="test" />
|
||||
<el-option label="财务" value="finance" />
|
||||
<el-option label="普通成员" value="member" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="加入日期" prop="joinDate">
|
||||
<el-date-picker
|
||||
v-model="form.joinDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="工作量占比">
|
||||
<el-input-number v-model="form.workload" :min="0" :max="100" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注说明">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getMemberListByProject,
|
||||
addMember,
|
||||
updateMember,
|
||||
removeMember,
|
||||
updateMemberStatus
|
||||
} from '../../api/projectMember'
|
||||
import { getProjectList } from '../../api/project'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
projectId: null,
|
||||
role: '',
|
||||
status: null
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 项目选项
|
||||
const projectOptions = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
memberId: null,
|
||||
projectId: null,
|
||||
userId: null,
|
||||
role: 'member',
|
||||
joinDate: null,
|
||||
workload: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
projectId: [
|
||||
{ required: true, message: '请选择项目', trigger: 'change' }
|
||||
],
|
||||
userId: [
|
||||
{ required: true, message: '请输入用户ID', trigger: 'blur' }
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: '请选择项目角色', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 加载项目列表
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectList({ current: 1, size: 1000 })
|
||||
projectOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载项目列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色文本
|
||||
const getRoleText = (role) => {
|
||||
const roleMap = {
|
||||
'pm': '项目经理',
|
||||
'dev': '开发',
|
||||
'test': '测试',
|
||||
'finance': '财务',
|
||||
'member': '普通成员'
|
||||
}
|
||||
return roleMap[role] || role
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
if (!searchForm.projectId) {
|
||||
tableData.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
let data = await getMemberListByProject(searchForm.projectId)
|
||||
|
||||
// 前端过滤
|
||||
if (searchForm.role) {
|
||||
data = data.filter(item => item.role === searchForm.role)
|
||||
}
|
||||
if (searchForm.status !== null && searchForm.status !== '') {
|
||||
data = data.filter(item => item.status === searchForm.status)
|
||||
}
|
||||
|
||||
tableData.value = data
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.projectId = null
|
||||
searchForm.role = ''
|
||||
searchForm.status = null
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
if (!searchForm.projectId) {
|
||||
ElMessage.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(form, {
|
||||
memberId: null,
|
||||
projectId: searchForm.projectId,
|
||||
userId: null,
|
||||
role: 'member',
|
||||
joinDate: null,
|
||||
workload: 0,
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, {
|
||||
memberId: row.memberId,
|
||||
projectId: row.projectId,
|
||||
userId: row.userId,
|
||||
role: row.role,
|
||||
joinDate: row.joinDate,
|
||||
workload: row.workload,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.memberId) {
|
||||
await updateMember(form.memberId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await addMember(form)
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const handleUpdateStatus = async (row, status) => {
|
||||
const statusText = status === 1 ? '恢复在职' : '标记离开'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要${statusText}吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await updateMemberStatus(row.memberId, status)
|
||||
ElMessage.success(`${statusText}成功`)
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error(error.message || '状态更新失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除
|
||||
const handleRemove = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要移除该成员吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await removeMember(row.memberId)
|
||||
ElMessage.success('移除成功')
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('移除失败:', error)
|
||||
ElMessage.error(error.message || '移除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.member-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,641 +0,0 @@
|
||||
<template>
|
||||
<div class="requirement-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="需求名称">
|
||||
<el-input v-model="searchForm.requirementName" placeholder="请输入需求名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目">
|
||||
<el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable>
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户">
|
||||
<el-select v-model="searchForm.customerId" placeholder="请选择客户" clearable>
|
||||
<el-option
|
||||
v-for="customer in customerOptions"
|
||||
:key="customer.customerId"
|
||||
:label="customer.customerName"
|
||||
:value="customer.customerId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="待开发" value="pending" />
|
||||
<el-option label="开发中" value="developing" />
|
||||
<el-option label="已交付" value="delivered" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleAdd">新增需求</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="requirementCode" label="需求编号" width="150" />
|
||||
<el-table-column prop="requirementName" label="需求名称" width="200" />
|
||||
<el-table-column prop="projectId" label="项目" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getProjectName(row.projectId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="customerName" label="客户" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getCustomerName(row.customerId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="priority" label="优先级" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.priority === 'high'" type="danger">高</el-tag>
|
||||
<el-tag v-else-if="row.priority === 'normal'" type="warning">中</el-tag>
|
||||
<el-tag v-else type="info">低</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'pending'" type="info">待开发</el-tag>
|
||||
<el-tag v-else-if="row.status === 'developing'" type="primary">开发中</el-tag>
|
||||
<el-tag v-else-if="row.status === 'delivered'" type="warning">已交付</el-tag>
|
||||
<el-tag v-else-if="row.status === 'completed'" type="success">已完成</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress" label="进度" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.progress" :color="getProgressColor(row.progress)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="estimatedHours" label="预估工时(h)" width="120" />
|
||||
<el-table-column prop="actualHours" label="实际工时(h)" width="120" />
|
||||
<el-table-column prop="receivableAmount" label="应收款" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.receivableAmount ? '¥' + row.receivableAmount : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="info" size="small" @click="handleUpdateStatus(row)">更新状态</el-button>
|
||||
<el-button type="warning" size="small" @click="handleUpdateProgress(row)">更新进度</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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
|
||||
v-model="dialogVisible"
|
||||
:title="form.requirementId ? '编辑需求' : '新增需求'"
|
||||
width="800px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<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="requirementName">
|
||||
<el-input v-model="form.requirementName" 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="请选择项目" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="project in projectOptions"
|
||||
:key="project.projectId"
|
||||
:label="project.projectName"
|
||||
:value="project.projectId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户" prop="customerId">
|
||||
<el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="customer in customerOptions"
|
||||
:key="customer.customerId"
|
||||
:label="customer.customerName"
|
||||
:value="customer.customerId"
|
||||
/>
|
||||
</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="high" />
|
||||
<el-option label="中" value="normal" />
|
||||
<el-option label="低" value="low" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%;">
|
||||
<el-option label="待开发" value="pending" />
|
||||
<el-option label="开发中" value="developing" />
|
||||
<el-option label="已交付" value="delivered" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</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-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预估工时(h)" prop="estimatedHours">
|
||||
<el-input-number v-model="form.estimatedHours" :min="0" :precision="2" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实际工时(h)" prop="actualHours">
|
||||
<el-input-number v-model="form.actualHours" :min="0" :precision="2" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划开始日期" prop="plannedStart">
|
||||
<el-date-picker
|
||||
v-model="form.plannedStart"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计划结束日期" prop="plannedEnd">
|
||||
<el-date-picker
|
||||
v-model="form.plannedEnd"
|
||||
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="actualStart">
|
||||
<el-date-picker
|
||||
v-model="form.actualStart"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实际结束日期" prop="actualEnd">
|
||||
<el-date-picker
|
||||
v-model="form.actualEnd"
|
||||
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="receivableAmount">
|
||||
<el-input-number v-model="form.receivableAmount" :min="0" :precision="2" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="应收款日期" prop="receivableDate">
|
||||
<el-date-picker
|
||||
v-model="form.receivableDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="交付日期" prop="deliveryDate">
|
||||
<el-date-picker
|
||||
v-model="form.deliveryDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</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="statusDialogVisible" title="更新状态" width="400px">
|
||||
<el-form>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="statusForm.status" placeholder="请选择状态" style="width: 100%;">
|
||||
<el-option label="待开发" value="pending" />
|
||||
<el-option label="开发中" value="developing" />
|
||||
<el-option label="已交付" value="delivered" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitStatus">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 更新进度对话框 -->
|
||||
<el-dialog v-model="progressDialogVisible" title="更新进度" width="400px">
|
||||
<el-form>
|
||||
<el-form-item label="进度">
|
||||
<el-slider v-model="progressForm.progress" :min="0" :max="100" show-input />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="progressDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitProgress">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getRequirementList,
|
||||
createRequirement,
|
||||
updateRequirement,
|
||||
deleteRequirement,
|
||||
updateRequirementStatus,
|
||||
updateRequirementProgress
|
||||
} from '../../api/requirement'
|
||||
import { getProjectList } from '../../api/project'
|
||||
import { getCustomerList } from '../../api/customer'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
requirementName: '',
|
||||
projectId: null,
|
||||
customerId: null,
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 项目选项
|
||||
const projectOptions = ref([])
|
||||
// 客户选项
|
||||
const customerOptions = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
requirementId: null,
|
||||
requirementCode: '',
|
||||
requirementName: '',
|
||||
description: '',
|
||||
projectId: null,
|
||||
customerId: null,
|
||||
priority: 'normal',
|
||||
status: 'pending',
|
||||
estimatedHours: 0,
|
||||
actualHours: 0,
|
||||
plannedStart: null,
|
||||
plannedEnd: null,
|
||||
actualStart: null,
|
||||
actualEnd: null,
|
||||
deliveryDate: null,
|
||||
receivableAmount: 0,
|
||||
receivableDate: null
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
requirementCode: [
|
||||
{ required: true, message: '请输入需求编号', trigger: 'blur' }
|
||||
],
|
||||
requirementName: [
|
||||
{ required: true, message: '请输入需求名称', trigger: 'blur' }
|
||||
],
|
||||
projectId: [
|
||||
{ required: true, message: '请选择项目', trigger: 'change' }
|
||||
],
|
||||
customerId: [
|
||||
{ required: true, message: '请选择客户', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 状态对话框
|
||||
const statusDialogVisible = ref(false)
|
||||
const statusForm = reactive({
|
||||
requirementId: null,
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 进度对话框
|
||||
const progressDialogVisible = ref(false)
|
||||
const progressForm = reactive({
|
||||
requirementId: null,
|
||||
progress: 0
|
||||
})
|
||||
|
||||
// 加载项目列表
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectList({ current: 1, size: 1000 })
|
||||
projectOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载项目列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载客户列表
|
||||
const loadCustomers = async () => {
|
||||
try {
|
||||
const res = await getCustomerList({ current: 1, size: 1000 })
|
||||
customerOptions.value = res.records || []
|
||||
} catch (error) {
|
||||
console.error('加载客户列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项目名称
|
||||
const getProjectName = (projectId) => {
|
||||
const project = projectOptions.value.find(p => p.projectId === projectId)
|
||||
return project ? project.projectName : '-'
|
||||
}
|
||||
|
||||
// 获取客户名称
|
||||
const getCustomerName = (customerId) => {
|
||||
const customer = customerOptions.value.find(c => c.customerId === customerId)
|
||||
return customer ? customer.customerName : '-'
|
||||
}
|
||||
|
||||
// 进度颜色
|
||||
const getProgressColor = (percentage) => {
|
||||
if (percentage < 30) return '#f56c6c'
|
||||
if (percentage < 70) return '#e6a23c'
|
||||
return '#67c23a'
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
requirementName: searchForm.requirementName || undefined,
|
||||
projectId: searchForm.projectId || undefined,
|
||||
customerId: searchForm.customerId || undefined,
|
||||
status: searchForm.status || undefined
|
||||
}
|
||||
const res = await getRequirementList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.requirementName = ''
|
||||
searchForm.projectId = null
|
||||
searchForm.customerId = null
|
||||
searchForm.status = ''
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, {
|
||||
requirementId: null,
|
||||
requirementCode: '',
|
||||
requirementName: '',
|
||||
description: '',
|
||||
projectId: null,
|
||||
customerId: null,
|
||||
priority: 'normal',
|
||||
status: 'pending',
|
||||
estimatedHours: 0,
|
||||
actualHours: 0,
|
||||
plannedStart: null,
|
||||
plannedEnd: null,
|
||||
actualStart: null,
|
||||
actualEnd: null,
|
||||
deliveryDate: null,
|
||||
receivableAmount: 0,
|
||||
receivableDate: null
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, {
|
||||
requirementId: row.requirementId,
|
||||
requirementCode: row.requirementCode,
|
||||
requirementName: row.requirementName,
|
||||
description: row.description,
|
||||
projectId: row.projectId,
|
||||
customerId: row.customerId,
|
||||
priority: row.priority,
|
||||
status: row.status,
|
||||
estimatedHours: row.estimatedHours,
|
||||
actualHours: row.actualHours,
|
||||
plannedStart: row.plannedStart,
|
||||
plannedEnd: row.plannedEnd,
|
||||
actualStart: row.actualStart,
|
||||
actualEnd: row.actualEnd,
|
||||
deliveryDate: row.deliveryDate,
|
||||
receivableAmount: row.receivableAmount,
|
||||
receivableDate: row.receivableDate
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.requirementId) {
|
||||
await updateRequirement(form.requirementId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createRequirement(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const handleUpdateStatus = (row) => {
|
||||
statusForm.requirementId = row.requirementId
|
||||
statusForm.status = row.status
|
||||
statusDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交状态
|
||||
const handleSubmitStatus = async () => {
|
||||
try {
|
||||
await updateRequirementStatus(statusForm.requirementId, statusForm.status)
|
||||
ElMessage.success('状态更新成功')
|
||||
statusDialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error(error.message || '状态更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
const handleUpdateProgress = (row) => {
|
||||
progressForm.requirementId = row.requirementId
|
||||
progressForm.progress = row.progress
|
||||
progressDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交进度
|
||||
const handleSubmitProgress = async () => {
|
||||
try {
|
||||
await updateRequirementProgress(progressForm.requirementId, progressForm.progress)
|
||||
ElMessage.success('进度更新成功')
|
||||
progressDialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('进度更新失败:', error)
|
||||
ElMessage.error(error.message || '进度更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该需求吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteRequirement(row.requirementId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果是最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadProjects()
|
||||
loadCustomers()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.requirement-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,257 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>部门管理</span>
|
||||
<el-button type="primary" @click="handleAdd(null)">新增部门</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
row-key="deptId"
|
||||
border
|
||||
v-loading="loading"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
default-expand-all
|
||||
>
|
||||
<el-table-column prop="deptName" label="部门名称" width="250" />
|
||||
<el-table-column prop="deptCode" label="部门编码" width="150" />
|
||||
<el-table-column prop="leader" label="负责人" width="120" />
|
||||
<el-table-column prop="phone" label="联系电话" width="140" />
|
||||
<el-table-column prop="email" label="邮箱" width="200" />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleAdd(scope.row)">添加子部门</el-button>
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.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-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="上级部门">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="deptTreeOptions"
|
||||
:props="{ label: 'deptName', value: 'deptId', children: 'children' }"
|
||||
placeholder="请选择上级部门"
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门名称" prop="deptName">
|
||||
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门编码" prop="deptCode">
|
||||
<el-input v-model="form.deptCode" placeholder="请输入部门编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="负责人">
|
||||
<el-input v-model="form.leader" placeholder="请输入负责人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" 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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getDeptTree, createDept, updateDept, deleteDept } from '../../api/dept'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const deptTreeOptions = ref([])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
deptId: null,
|
||||
parentId: 0,
|
||||
deptName: '',
|
||||
deptCode: '',
|
||||
leader: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
sortOrder: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
deptName: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
|
||||
deptCode: [{ required: true, message: '请输入部门编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 加载部门树
|
||||
const loadDeptTree = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDeptTree()
|
||||
tableData.value = res || []
|
||||
|
||||
// 构建部门树选项(用于上级部门选择)
|
||||
deptTreeOptions.value = [
|
||||
{ deptId: 0, deptName: '顶级部门', children: res || [] }
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('加载部门树失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 新增部门
|
||||
const handleAdd = (row) => {
|
||||
dialogTitle.value = row ? '添加子部门' : '新增部门'
|
||||
resetForm()
|
||||
if (row) {
|
||||
form.parentId = row.deptId
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑部门
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑部门'
|
||||
resetForm()
|
||||
Object.assign(form, {
|
||||
deptId: row.deptId,
|
||||
parentId: row.parentId,
|
||||
deptName: row.deptName,
|
||||
deptCode: row.deptCode,
|
||||
leader: row.leader,
|
||||
phone: row.phone,
|
||||
email: row.email,
|
||||
sortOrder: row.sortOrder,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.deptId = null
|
||||
form.parentId = 0
|
||||
form.deptName = ''
|
||||
form.deptCode = ''
|
||||
form.leader = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.sortOrder = 0
|
||||
form.remark = ''
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.deptId) {
|
||||
await updateDept(form.deptId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createDept(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await loadDeptTree()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除部门
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该部门吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteDept(row.deptId)
|
||||
ElMessage.success('删除成功')
|
||||
await loadDeptTree()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDeptTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,402 +0,0 @@
|
||||
<template>
|
||||
<div class="file-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="文件名">
|
||||
<el-input v-model="searchForm.fileName" placeholder="请输入文件名" clearable style="width: 200px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="searchForm.businessType" placeholder="请选择业务类型" clearable style="width: 150px;">
|
||||
<el-option label="合同附件" value="contract" />
|
||||
<el-option label="收款凭证" value="receipt" />
|
||||
<el-option label="支出凭证" value="expense" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联业务ID">
|
||||
<el-input-number v-model="searchForm.businessId" :min="1" controls-position="right" style="width: 150px;" />
|
||||
</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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<el-button type="primary">上传文件</el-button>
|
||||
</el-upload>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading" stripe>
|
||||
<el-table-column prop="fileId" label="文件ID" width="80" />
|
||||
<el-table-column prop="originalName" label="文件名" width="250" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handlePreview(row)">
|
||||
<el-icon><Document /></el-icon>
|
||||
{{ row.originalName }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="businessType" label="业务类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getBusinessTypeTag(row.businessType)">
|
||||
{{ getBusinessTypeText(row.businessType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="businessId" label="关联业务ID" width="120" />
|
||||
<el-table-column prop="fileSize" label="文件大小" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.fileSize) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileType" label="文件类型" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="uploadName" label="上传人" width="120" />
|
||||
<el-table-column prop="createdTime" label="上传时间" width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleDownload(row)">下载</el-button>
|
||||
<el-button type="success" size="small" @click="handlePreview(row)">预览</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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 v-model="uploadDialogVisible" title="上传文件" width="500px">
|
||||
<el-form :model="uploadForm" label-width="100px">
|
||||
<el-form-item label="选择文件">
|
||||
<div v-if="selectedFile" class="selected-file">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ selectedFile.name }}</span>
|
||||
<span class="file-size">({{ formatFileSize(selectedFile.size) }})</span>
|
||||
</div>
|
||||
<el-button v-else @click="triggerFileSelect">选择文件</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="uploadForm.businessType" placeholder="请选择业务类型" style="width: 100%;">
|
||||
<el-option label="合同附件" value="contract" />
|
||||
<el-option label="收款凭证" value="receipt" />
|
||||
<el-option label="支出凭证" value="expense" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联业务ID">
|
||||
<el-input-number v-model="uploadForm.businessId" :min="1" style="width: 100%;" placeholder="请输入关联的业务ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="uploadForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleUpload" :disabled="!selectedFile">上传</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<el-dialog v-model="previewDialogVisible" title="文件预览" width="800px">
|
||||
<div class="preview-content">
|
||||
<!-- 图片预览 -->
|
||||
<img v-if="isImage(currentFile.fileType)" :src="currentFile.fileUrl" style="max-width: 100%;" />
|
||||
|
||||
<!-- PDF预览 -->
|
||||
<iframe v-else-if="isPDF(currentFile.fileType)" :src="currentFile.fileUrl" style="width: 100%; height: 600px;"></iframe>
|
||||
|
||||
<!-- 其他文件 -->
|
||||
<div v-else class="unsupported-preview">
|
||||
<el-icon :size="64"><Document /></el-icon>
|
||||
<p>该文件类型暂不支持预览</p>
|
||||
<p>文件名:{{ currentFile.originalName }}</p>
|
||||
<p>文件大小:{{ formatFileSize(currentFile.fileSize) }}</p>
|
||||
<el-button type="primary" @click="handleDownload(currentFile)">下载查看</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getFileList,
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
getFileUrl
|
||||
} from '../../api/file'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
fileName: '',
|
||||
businessType: '',
|
||||
businessId: null
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 上传相关
|
||||
const uploadRef = ref(null)
|
||||
const uploadDialogVisible = ref(false)
|
||||
const selectedFile = ref(null)
|
||||
const uploadForm = reactive({
|
||||
businessType: '',
|
||||
businessId: null,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 预览相关
|
||||
const previewDialogVisible = ref(false)
|
||||
const currentFile = ref({})
|
||||
|
||||
// 获取业务类型标签
|
||||
const getBusinessTypeTag = (type) => {
|
||||
const tagMap = {
|
||||
'contract': 'primary',
|
||||
'receipt': 'success',
|
||||
'expense': 'warning',
|
||||
'other': 'info'
|
||||
}
|
||||
return tagMap[type] || 'info'
|
||||
}
|
||||
|
||||
// 获取业务类型文本
|
||||
const getBusinessTypeText = (type) => {
|
||||
const textMap = {
|
||||
'contract': '合同附件',
|
||||
'receipt': '收款凭证',
|
||||
'expense': '支出凭证',
|
||||
'other': '其他'
|
||||
}
|
||||
return textMap[type] || type
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size) => {
|
||||
if (size < 1024) {
|
||||
return size + ' B'
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + ' KB'
|
||||
} else if (size < 1024 * 1024 * 1024) {
|
||||
return (size / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
} else {
|
||||
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为图片
|
||||
const isImage = (fileType) => {
|
||||
return fileType && fileType.startsWith('image/')
|
||||
}
|
||||
|
||||
// 判断是否为PDF
|
||||
const isPDF = (fileType) => {
|
||||
return fileType === 'application/pdf'
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
fileName: searchForm.fileName || undefined,
|
||||
businessType: searchForm.businessType || undefined,
|
||||
businessId: searchForm.businessId || undefined
|
||||
}
|
||||
const res = await getFileList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.fileName = ''
|
||||
searchForm.businessType = ''
|
||||
searchForm.businessId = null
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 文件选择变化
|
||||
const handleFileChange = (file) => {
|
||||
selectedFile.value = file.raw
|
||||
uploadDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 上传前验证
|
||||
const beforeUpload = (file) => {
|
||||
const isLt50M = file.size / 1024 / 1024 < 50
|
||||
if (!isLt50M) {
|
||||
ElMessage.error('文件大小不能超过 50MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 触发文件选择
|
||||
const triggerFileSelect = () => {
|
||||
uploadRef.value.$refs.input.click()
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', selectedFile.value)
|
||||
formData.append('businessType', uploadForm.businessType || 'other')
|
||||
if (uploadForm.businessId) {
|
||||
formData.append('businessId', uploadForm.businessId)
|
||||
}
|
||||
formData.append('uploadBy', 1) // 当前用户ID
|
||||
formData.append('uploadName', '管理员') // 当前用户名
|
||||
|
||||
try {
|
||||
await uploadFile(formData)
|
||||
ElMessage.success('上传成功')
|
||||
uploadDialogVisible.value = false
|
||||
selectedFile.value = null
|
||||
uploadForm.businessType = ''
|
||||
uploadForm.businessId = null
|
||||
uploadForm.remark = ''
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error(error.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = async (row) => {
|
||||
try {
|
||||
const res = await getFileUrl(row.fileId)
|
||||
window.open(res, '_blank')
|
||||
} catch (error) {
|
||||
console.error('获取文件URL失败:', error)
|
||||
ElMessage.error('下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
const handlePreview = (row) => {
|
||||
currentFile.value = row
|
||||
previewDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除文件 "${row.originalName}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteFile(row.fileId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果是最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.selected-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.selected-file .file-size {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.unsupported-preview {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.unsupported-preview p {
|
||||
margin: 10px 0;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
@ -1,288 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>菜单管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增菜单</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" stripe border row-key="menuId" default-expand-all>
|
||||
<el-table-column prop="menuId" label="菜单ID" width="80" />
|
||||
<el-table-column prop="menuName" label="菜单名称" width="200" />
|
||||
<el-table-column prop="menuType" label="类型" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getMenuTypeColor(scope.row.menuType)">
|
||||
{{ getMenuTypeLabel(scope.row.menuType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="icon" label="图标" width="100" />
|
||||
<el-table-column prop="path" label="路由路径" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="component" label="组件路径" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="permission" label="权限标识" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="success" link @click="handleAdd(scope.row)">添加子菜单</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单类型" prop="menuType">
|
||||
<el-select v-model="form.menuType" placeholder="请选择菜单类型" style="width: 100%">
|
||||
<el-option label="目录" value="dir" />
|
||||
<el-option label="菜单" value="menu" />
|
||||
<el-option label="按钮" value="button" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单名称" prop="menuName">
|
||||
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input v-model="form.icon" placeholder="请输入图标" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="路由路径" prop="path">
|
||||
<el-input v-model="form.path" placeholder="请输入路由路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="组件路径" prop="component">
|
||||
<el-input v-model="form.component" placeholder="请输入组件路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="权限标识" prop="permission">
|
||||
<el-input v-model="form.permission" placeholder="如: user:add" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单层级" prop="menuLevel">
|
||||
<el-input-number v-model="form.menuLevel" :min="1" :max="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" 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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getMenuList, createMenu, updateMenu, deleteMenu } from '../../api/menu'
|
||||
|
||||
const tableData = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增菜单')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
menuId: null,
|
||||
parentId: 0,
|
||||
menuName: '',
|
||||
menuType: 'menu',
|
||||
icon: '',
|
||||
path: '',
|
||||
component: '',
|
||||
permission: '',
|
||||
sortOrder: 0,
|
||||
menuLevel: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
menuName: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
menuType: [{ required: true, message: '请选择菜单类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const getMenuTypeLabel = (type) => {
|
||||
const map = {
|
||||
'dir': '目录',
|
||||
'menu': '菜单',
|
||||
'button': '按钮'
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
const getMenuTypeColor = (type) => {
|
||||
const map = {
|
||||
'dir': 'warning',
|
||||
'menu': 'primary',
|
||||
'button': 'success'
|
||||
}
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
const buildMenuTree = (menus) => {
|
||||
const tree = []
|
||||
const map = {}
|
||||
|
||||
menus.forEach(menu => {
|
||||
map[menu.menuId] = { ...menu, children: [] }
|
||||
})
|
||||
|
||||
menus.forEach(menu => {
|
||||
if (menu.parentId === 0) {
|
||||
tree.push(map[menu.menuId])
|
||||
} else if (map[menu.parentId]) {
|
||||
map[menu.parentId].children.push(map[menu.menuId])
|
||||
}
|
||||
})
|
||||
|
||||
// 清理空 children
|
||||
const clearEmptyChildren = (nodes) => {
|
||||
nodes.forEach(node => {
|
||||
if (node.children.length === 0) {
|
||||
delete node.children
|
||||
} else {
|
||||
clearEmptyChildren(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
clearEmptyChildren(tree)
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const loadTableData = async () => {
|
||||
try {
|
||||
const res = await getMenuList()
|
||||
tableData.value = buildMenuTree(res)
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = (row) => {
|
||||
dialogTitle.value = row ? '添加子菜单' : '新增菜单'
|
||||
Object.assign(form, {
|
||||
menuId: null,
|
||||
parentId: row ? row.menuId : 0,
|
||||
menuName: '',
|
||||
menuType: 'menu',
|
||||
icon: '',
|
||||
path: '',
|
||||
component: '',
|
||||
permission: '',
|
||||
sortOrder: 0,
|
||||
menuLevel: row ? row.menuLevel + 1 : 1,
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑菜单'
|
||||
Object.assign(form, {
|
||||
menuId: row.menuId,
|
||||
parentId: row.parentId,
|
||||
menuName: row.menuName,
|
||||
menuType: row.menuType,
|
||||
icon: row.icon,
|
||||
path: row.path,
|
||||
component: row.component,
|
||||
permission: row.permission,
|
||||
sortOrder: row.sortOrder,
|
||||
menuLevel: row.menuLevel,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.menuId) {
|
||||
await updateMenu(form.menuId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createMenu(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadTableData()
|
||||
} catch (error) {
|
||||
console.error('菜单操作失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该菜单吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteMenu(row.menuId)
|
||||
ElMessage.success('删除成功')
|
||||
loadTableData()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,350 +0,0 @@
|
||||
<template>
|
||||
<div class="log-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="模块">
|
||||
<el-input v-model="searchForm.module" placeholder="请输入模块名称" clearable style="width: 200px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="searchForm.operationType" placeholder="请选择" clearable style="width: 150px;">
|
||||
<el-option label="查询" value="SELECT" />
|
||||
<el-option label="新增" value="INSERT" />
|
||||
<el-option label="更新" value="UPDATE" />
|
||||
<el-option label="删除" value="DELETE" />
|
||||
<el-option label="导入" value="IMPORT" />
|
||||
<el-option label="导出" value="EXPORT" />
|
||||
<el-option label="登录" value="LOGIN" />
|
||||
<el-option label="登出" value="LOGOUT" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人ID">
|
||||
<el-input-number v-model="searchForm.operatorId" :min="1" controls-position="right" style="width: 150px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 380px;"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="danger" @click="handleBatchDelete">批量清理</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading" stripe>
|
||||
<el-table-column prop="logId" label="日志ID" width="80" />
|
||||
<el-table-column prop="module" label="模块" width="120" />
|
||||
<el-table-column prop="operationType" label="操作类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOperationTypeTag(row.operationType)">
|
||||
{{ getOperationTypeText(row.operationType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="操作描述" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="requestMethod" label="请求方式" width="100" />
|
||||
<el-table-column prop="requestUrl" label="请求URL" width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="executionTime" label="执行时长" width="100">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.executionTime > 1000 ? 'red' : 'inherit' }">
|
||||
{{ row.executionTime }}ms
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operatorName" label="操作人" width="120" />
|
||||
<el-table-column prop="operatorIp" label="IP地址" width="140" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">成功</el-tag>
|
||||
<el-tag v-else type="danger">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdTime" label="操作时间" width="180" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleViewDetail(row)">查看详情</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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 v-model="detailVisible" title="操作日志详情" width="800px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="日志ID">{{ detail.logId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="模块">{{ detail.module }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作类型">
|
||||
<el-tag :type="getOperationTypeTag(detail.operationType)">
|
||||
{{ getOperationTypeText(detail.operationType) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag v-if="detail.status === 1" type="success">成功</el-tag>
|
||||
<el-tag v-else type="danger">失败</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作描述" :span="2">{{ detail.description }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方式">{{ detail.requestMethod }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行时长">{{ detail.executionTime }}ms</el-descriptions-item>
|
||||
<el-descriptions-item label="请求URL" :span="2">{{ detail.requestUrl }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求参数" :span="2">
|
||||
<pre style="max-height: 200px; overflow-y: auto;">{{ detail.requestParams }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="响应结果" :span="2">
|
||||
<pre style="max-height: 200px; overflow-y: auto;">{{ detail.responseResult }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ detail.operatorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人ID">{{ detail.operatorId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">{{ detail.operatorIp }}</el-descriptions-item>
|
||||
<el-descriptions-item label="位置">{{ detail.operatorLocation }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间" :span="2">{{ detail.createdTime }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detail.errorMsg" label="错误信息" :span="2">
|
||||
<pre style="max-height: 200px; overflow-y: auto; color: red;">{{ detail.errorMsg }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量删除对话框 -->
|
||||
<el-dialog v-model="batchDeleteVisible" title="批量清理日志" width="400px">
|
||||
<el-form>
|
||||
<el-form-item label="清理天数">
|
||||
<el-input-number v-model="batchDeleteDays" :min="7" :max="365" style="width: 100%;" />
|
||||
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
|
||||
将删除 {{ batchDeleteDays }} 天前的所有操作日志
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchDeleteVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="handleConfirmBatchDelete">确定删除</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getOperationLogList,
|
||||
getOperationLogById,
|
||||
deleteOperationLog,
|
||||
batchDeleteOperationLog
|
||||
} from '../../api/operationLog'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
module: '',
|
||||
operationType: '',
|
||||
operatorId: null
|
||||
})
|
||||
|
||||
// 日期范围
|
||||
const dateRange = ref([])
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 详情对话框
|
||||
const detailVisible = ref(false)
|
||||
const detail = ref({})
|
||||
|
||||
// 批量删除对话框
|
||||
const batchDeleteVisible = ref(false)
|
||||
const batchDeleteDays = ref(30)
|
||||
|
||||
// 获取操作类型文本
|
||||
const getOperationTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'SELECT': '查询',
|
||||
'INSERT': '新增',
|
||||
'UPDATE': '更新',
|
||||
'DELETE': '删除',
|
||||
'IMPORT': '导入',
|
||||
'EXPORT': '导出',
|
||||
'LOGIN': '登录',
|
||||
'LOGOUT': '登出'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取操作类型标签颜色
|
||||
const getOperationTypeTag = (type) => {
|
||||
const tagMap = {
|
||||
'SELECT': '',
|
||||
'INSERT': 'success',
|
||||
'UPDATE': 'warning',
|
||||
'DELETE': 'danger',
|
||||
'IMPORT': 'info',
|
||||
'EXPORT': 'info',
|
||||
'LOGIN': 'success',
|
||||
'LOGOUT': 'info'
|
||||
}
|
||||
return tagMap[type] || ''
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
module: searchForm.module || undefined,
|
||||
operationType: searchForm.operationType || undefined,
|
||||
operatorId: searchForm.operatorId || undefined,
|
||||
startTime: dateRange.value && dateRange.value[0] ? dateRange.value[0] : undefined,
|
||||
endTime: dateRange.value && dateRange.value[1] ? dateRange.value[1] : undefined
|
||||
}
|
||||
const res = await getOperationLogList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.module = ''
|
||||
searchForm.operationType = ''
|
||||
searchForm.operatorId = null
|
||||
dateRange.value = []
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = async (row) => {
|
||||
try {
|
||||
const res = await getOperationLogById(row.logId)
|
||||
detail.value = res
|
||||
detailVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('加载详情失败:', error)
|
||||
ElMessage.error(error.message || '加载详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该操作日志吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deleteOperationLog(row.logId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果是最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
batchDeleteVisible.value = true
|
||||
}
|
||||
|
||||
// 确认批量删除
|
||||
const handleConfirmBatchDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除 ${batchDeleteDays.value} 天前的所有操作日志吗?此操作不可恢复!`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await batchDeleteOperationLog(batchDeleteDays.value)
|
||||
ElMessage.success('批量删除成功')
|
||||
batchDeleteVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error(error.message || '批量删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,415 +0,0 @@
|
||||
<template>
|
||||
<div class="post-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="岗位编码">
|
||||
<el-input v-model="searchForm.postCode" placeholder="请输入岗位编码" clearable style="width: 150px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位名称">
|
||||
<el-input v-model="searchForm.postName" placeholder="请输入岗位名称" clearable style="width: 150px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属部门">
|
||||
<el-tree-select
|
||||
v-model="searchForm.deptId"
|
||||
:data="deptTreeOptions"
|
||||
placeholder="请选择部门"
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 120px;">
|
||||
<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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleAdd">新增岗位</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" border v-loading="loading" stripe>
|
||||
<el-table-column prop="postId" label="岗位ID" width="80" />
|
||||
<el-table-column prop="postCode" label="岗位编码" width="120" />
|
||||
<el-table-column prop="postName" label="岗位名称" width="150" />
|
||||
<el-table-column prop="deptId" label="所属部门" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getDeptName(row.deptId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="postDuty" label="岗位职责" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleStatusChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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
|
||||
v-model="dialogVisible"
|
||||
:title="form.postId ? '编辑岗位' : '新增岗位'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="岗位编码" prop="postCode">
|
||||
<el-input v-model="form.postCode" placeholder="请输入岗位编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="岗位名称" prop="postName">
|
||||
<el-input v-model="form.postName" placeholder="请输入岗位名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="所属部门" prop="deptId">
|
||||
<el-tree-select
|
||||
v-model="form.deptId"
|
||||
:data="deptTreeOptions"
|
||||
placeholder="请选择部门"
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="岗位职责">
|
||||
<el-input
|
||||
v-model="form.postDuty"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入岗位职责描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="岗位要求">
|
||||
<el-input
|
||||
v-model="form.postRequirement"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入岗位要求"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序号">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getPostList,
|
||||
createPost,
|
||||
updatePost,
|
||||
deletePost,
|
||||
updatePostStatus
|
||||
} from '../../api/post'
|
||||
import { getDeptTree } from '../../api/dept'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
postCode: '',
|
||||
postName: '',
|
||||
deptId: null,
|
||||
status: null
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
postId: null,
|
||||
postCode: '',
|
||||
postName: '',
|
||||
postDuty: '',
|
||||
postRequirement: '',
|
||||
deptId: null,
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
postCode: [
|
||||
{ required: true, message: '请输入岗位编码', trigger: 'blur' }
|
||||
],
|
||||
postName: [
|
||||
{ required: true, message: '请输入岗位名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 部门树选项
|
||||
const deptTreeOptions = ref([])
|
||||
|
||||
// 部门映射(用于显示部门名称)
|
||||
const deptMap = ref({})
|
||||
|
||||
// 加载部门树
|
||||
const loadDeptTree = async () => {
|
||||
try {
|
||||
const data = await getDeptTree()
|
||||
deptTreeOptions.value = convertToTreeSelect(data)
|
||||
buildDeptMap(data)
|
||||
} catch (error) {
|
||||
console.error('加载部门树失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为TreeSelect格式
|
||||
const convertToTreeSelect = (data) => {
|
||||
return data.map(item => ({
|
||||
value: item.deptId,
|
||||
label: item.deptName,
|
||||
children: item.children ? convertToTreeSelect(item.children) : []
|
||||
}))
|
||||
}
|
||||
|
||||
// 构建部门映射
|
||||
const buildDeptMap = (data) => {
|
||||
data.forEach(item => {
|
||||
deptMap.value[item.deptId] = item.deptName
|
||||
if (item.children && item.children.length > 0) {
|
||||
buildDeptMap(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取部门名称
|
||||
const getDeptName = (deptId) => {
|
||||
return deptMap.value[deptId] || '-'
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
postCode: searchForm.postCode || undefined,
|
||||
postName: searchForm.postName || undefined,
|
||||
deptId: searchForm.deptId || undefined,
|
||||
status: searchForm.status !== null && searchForm.status !== '' ? searchForm.status : undefined
|
||||
}
|
||||
const res = await getPostList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.postCode = ''
|
||||
searchForm.postName = ''
|
||||
searchForm.deptId = null
|
||||
searchForm.status = null
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, {
|
||||
postId: null,
|
||||
postCode: '',
|
||||
postName: '',
|
||||
postDuty: '',
|
||||
postRequirement: '',
|
||||
deptId: null,
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, {
|
||||
postId: row.postId,
|
||||
postCode: row.postCode,
|
||||
postName: row.postName,
|
||||
postDuty: row.postDuty,
|
||||
postRequirement: row.postRequirement,
|
||||
deptId: row.deptId,
|
||||
sortOrder: row.sortOrder,
|
||||
status: row.status,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.postId) {
|
||||
await updatePost(form.postId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createPost(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
const handleStatusChange = async (row) => {
|
||||
try {
|
||||
await updatePostStatus(row.postId, row.status)
|
||||
ElMessage.success('状态更新成功')
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error(error.message || '状态更新失败')
|
||||
// 回滚状态
|
||||
row.status = row.status === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该岗位吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await deletePost(row.postId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果是最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadDeptTree()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.post-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,361 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>角色管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增角色</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="searchForm.roleName" placeholder="请输入角色名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.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>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" stripe border>
|
||||
<el-table-column prop="roleId" label="角色ID" width="80" />
|
||||
<el-table-column prop="roleCode" label="角色编码" width="150" />
|
||||
<el-table-column prop="roleName" label="角色名称" width="150" />
|
||||
<el-table-column prop="roleType" label="角色类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.roleType === 'system' ? 'danger' : 'success'">
|
||||
{{ scope.row.roleType === 'system' ? '系统角色' : '自定义角色' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dataScope" label="数据范围" width="120">
|
||||
<template #default="scope">
|
||||
{{ getDataScopeLabel(scope.row.dataScope) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="warning" link @click="handleAssignMenu(scope.row)">分配菜单</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</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="角色编码" prop="roleCode">
|
||||
<el-input v-model="form.roleCode" placeholder="请输入角色编码" :disabled="!!form.roleId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色类型" prop="roleType">
|
||||
<el-select v-model="form.roleType" placeholder="请选择角色类型" style="width: 100%">
|
||||
<el-option label="系统角色" value="system" />
|
||||
<el-option label="自定义角色" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据范围" prop="dataScope">
|
||||
<el-select v-model="form.dataScope" placeholder="请选择数据范围" style="width: 100%">
|
||||
<el-option label="全部数据" value="all" />
|
||||
<el-option label="本部门" value="dept" />
|
||||
<el-option label="仅本人" value="self" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" 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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分配菜单对话框 -->
|
||||
<el-dialog v-model="menuDialogVisible" title="分配菜单" width="600px">
|
||||
<el-tree
|
||||
ref="menuTreeRef"
|
||||
:data="menuTreeData"
|
||||
:props="{ label: 'menuName', children: 'children' }"
|
||||
node-key="menuId"
|
||||
show-checkbox
|
||||
default-expand-all
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="menuDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitMenus">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getRoleList, createRole, updateRole, deleteRole, getRoleMenuIds, assignRoleMenus } from '../../api/role'
|
||||
import { getMenuTree } from '../../api/menu'
|
||||
|
||||
const searchForm = reactive({
|
||||
roleName: '',
|
||||
status: null
|
||||
})
|
||||
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const tableData = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增角色')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
roleId: null,
|
||||
roleCode: '',
|
||||
roleName: '',
|
||||
roleType: 'custom',
|
||||
dataScope: 'self',
|
||||
sortOrder: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
roleCode: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
roleType: [{ required: true, message: '请选择角色类型', trigger: 'change' }],
|
||||
dataScope: [{ required: true, message: '请选择数据范围', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const menuDialogVisible = ref(false)
|
||||
const menuTreeRef = ref(null)
|
||||
const menuTreeData = ref([])
|
||||
const currentRoleId = ref(null)
|
||||
|
||||
const getDataScopeLabel = (dataScope) => {
|
||||
const map = {
|
||||
'all': '全部数据',
|
||||
'dept': '本部门',
|
||||
'self': '仅本人'
|
||||
}
|
||||
return map[dataScope] || dataScope
|
||||
}
|
||||
|
||||
const loadTableData = async () => {
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
roleName: searchForm.roleName || undefined,
|
||||
status: searchForm.status
|
||||
}
|
||||
const res = await getRoleList(params)
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
ElMessage.error('加载数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
loadTableData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.roleName = ''
|
||||
searchForm.status = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增角色'
|
||||
Object.assign(form, {
|
||||
roleId: null,
|
||||
roleCode: '',
|
||||
roleName: '',
|
||||
roleType: 'custom',
|
||||
dataScope: 'self',
|
||||
sortOrder: 0,
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑角色'
|
||||
Object.assign(form, {
|
||||
roleId: row.roleId,
|
||||
roleCode: row.roleCode,
|
||||
roleName: row.roleName,
|
||||
roleType: row.roleType,
|
||||
dataScope: row.dataScope,
|
||||
sortOrder: row.sortOrder,
|
||||
remark: row.remark
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.roleId) {
|
||||
await updateRole(form.roleId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createRole(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadTableData()
|
||||
} catch (error) {
|
||||
console.error('角色操作失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该角色吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteRole(row.roleId)
|
||||
ElMessage.success('删除成功')
|
||||
loadTableData()
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleAssignMenu = async (row) => {
|
||||
currentRoleId.value = row.roleId
|
||||
try {
|
||||
// 加载菜单树
|
||||
const menuRes = await getMenuTree()
|
||||
menuTreeData.value = buildMenuTree(menuRes)
|
||||
|
||||
// 加载角色已有菜单
|
||||
const menuIdsRes = await getRoleMenuIds(row.roleId)
|
||||
menuDialogVisible.value = true
|
||||
|
||||
// 等待对话框渲染完成后设置选中
|
||||
setTimeout(() => {
|
||||
menuTreeRef.value.setCheckedKeys(menuIdsRes || [])
|
||||
}, 100)
|
||||
} catch (error) {
|
||||
console.error('加载菜单失败:', error)
|
||||
ElMessage.error('加载菜单失败')
|
||||
}
|
||||
}
|
||||
|
||||
const buildMenuTree = (menus) => {
|
||||
const tree = []
|
||||
const map = {}
|
||||
|
||||
menus.forEach(menu => {
|
||||
map[menu.menuId] = { ...menu, children: [] }
|
||||
})
|
||||
|
||||
menus.forEach(menu => {
|
||||
if (menu.parentId === 0) {
|
||||
tree.push(map[menu.menuId])
|
||||
} else if (map[menu.parentId]) {
|
||||
map[menu.parentId].children.push(map[menu.menuId])
|
||||
}
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const handleSubmitMenus = async () => {
|
||||
try {
|
||||
const checkedKeys = menuTreeRef.value.getCheckedKeys()
|
||||
const halfCheckedKeys = menuTreeRef.value.getHalfCheckedKeys()
|
||||
const menuIds = [...checkedKeys, ...halfCheckedKeys]
|
||||
|
||||
await assignRoleMenus(currentRoleId.value, menuIds)
|
||||
ElMessage.success('分配成功')
|
||||
menuDialogVisible.value = false
|
||||
} catch (error) {
|
||||
ElMessage.error('分配失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
loadTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = () => {
|
||||
loadTableData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -1,404 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="配置键">
|
||||
<el-input v-model="searchForm.configKey" placeholder="请输入配置键" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置名称">
|
||||
<el-input v-model="searchForm.configName" placeholder="请输入配置名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置分组">
|
||||
<el-select v-model="searchForm.configGroup" placeholder="请选择分组" clearable>
|
||||
<el-option label="系统设置" value="system" />
|
||||
<el-option label="邮件设置" value="email" />
|
||||
<el-option label="短信设置" value="sms" />
|
||||
<el-option label="文件设置" value="file" />
|
||||
<el-option label="安全设置" value="security" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-card class="table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>系统配置列表</span>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增配置
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="configId" label="ID" width="80" />
|
||||
<el-table-column prop="configKey" label="配置键" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="configName" label="配置名称" min-width="150" />
|
||||
<el-table-column prop="configValue" label="配置值" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="configType" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTagType(row.configType)">
|
||||
{{ getTypeLabel(row.configType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="configGroup" label="分组" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getGroupLabel(row.configGroup) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="isSystem" label="系统内置" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isSystem === 1 ? 'danger' : 'info'">
|
||||
{{ row.isSystem === 1 ? '是' : '否' }}
|
||||
</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"
|
||||
@change="handleStatusChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="配置键" prop="configKey">
|
||||
<el-input v-model="formData.configKey" placeholder="请输入配置键" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置名称" prop="configName">
|
||||
<el-input v-model="formData.configName" placeholder="请输入配置名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置值" prop="configValue">
|
||||
<el-input
|
||||
v-model="formData.configValue"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置类型" prop="configType">
|
||||
<el-select v-model="formData.configType" placeholder="请选择配置类型" style="width: 100%">
|
||||
<el-option label="字符串" value="string" />
|
||||
<el-option label="整数" value="int" />
|
||||
<el-option label="布尔值" value="boolean" />
|
||||
<el-option label="JSON对象" value="json" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置分组" prop="configGroup">
|
||||
<el-select v-model="formData.configGroup" placeholder="请选择配置分组" style="width: 100%">
|
||||
<el-option label="系统设置" value="system" />
|
||||
<el-option label="邮件设置" value="email" />
|
||||
<el-option label="短信设置" value="sms" />
|
||||
<el-option label="文件设置" value="file" />
|
||||
<el-option label="安全设置" value="security" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系统内置">
|
||||
<el-radio-group v-model="formData.isSystem">
|
||||
<el-radio :label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getConfigPage,
|
||||
createConfig,
|
||||
updateConfig,
|
||||
deleteConfig
|
||||
} from '@/api/sysConfig'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
configKey: '',
|
||||
configName: '',
|
||||
configGroup: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const selectedRows = ref([])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
const formData = reactive({
|
||||
configId: null,
|
||||
configKey: '',
|
||||
configName: '',
|
||||
configValue: '',
|
||||
configType: 'string',
|
||||
configGroup: 'system',
|
||||
isSystem: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
configKey: [{ required: true, message: '请输入配置键', trigger: 'blur' }],
|
||||
configName: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
||||
configValue: [{ required: true, message: '请输入配置值', trigger: 'blur' }],
|
||||
configType: [{ required: true, message: '请选择配置类型', trigger: 'change' }],
|
||||
configGroup: [{ required: true, message: '请选择配置分组', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 类型标签
|
||||
const getTypeTagType = (type) => {
|
||||
const map = {
|
||||
string: '',
|
||||
int: 'success',
|
||||
boolean: 'warning',
|
||||
json: 'info'
|
||||
}
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
const map = {
|
||||
string: '字符串',
|
||||
int: '整数',
|
||||
boolean: '布尔值',
|
||||
json: 'JSON'
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
const getGroupLabel = (group) => {
|
||||
const map = {
|
||||
system: '系统设置',
|
||||
email: '邮件设置',
|
||||
sms: '短信设置',
|
||||
file: '文件设置',
|
||||
security: '安全设置'
|
||||
}
|
||||
return map[group] || group
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getConfigPage({
|
||||
current: pagination.current,
|
||||
size: pagination.size,
|
||||
...searchForm
|
||||
})
|
||||
tableData.value = res.data.records
|
||||
pagination.total = res.data.total
|
||||
} catch (error) {
|
||||
console.error('加载配置列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.configKey = ''
|
||||
searchForm.configName = ''
|
||||
searchForm.configGroup = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (rows) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (current) => {
|
||||
pagination.current = current
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增配置'
|
||||
Object.assign(formData, {
|
||||
configId: null,
|
||||
configKey: '',
|
||||
configName: '',
|
||||
configValue: '',
|
||||
configType: 'string',
|
||||
configGroup: 'system',
|
||||
isSystem: 0,
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑配置'
|
||||
Object.assign(formData, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确认删除该配置吗?', '提示', {
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteConfig(row.configId)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
const handleStatusChange = async (row) => {
|
||||
try {
|
||||
await updateConfig(row.configId, { status: row.status })
|
||||
ElMessage.success('状态更新成功')
|
||||
} catch (error) {
|
||||
row.status = row.status === 1 ? 0 : 1
|
||||
console.error('状态更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateConfig(formData.configId, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createConfig(formData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -1,393 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="字典类型">
|
||||
<el-select v-model="searchForm.dictType" placeholder="请选择字典类型" clearable style="width: 200px">
|
||||
<el-option
|
||||
v-for="type in dictTypeList"
|
||||
:key="type"
|
||||
:label="type"
|
||||
:value="type"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签">
|
||||
<el-input v-model="searchForm.dictLabel" placeholder="请输入字典标签" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-card class="table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据字典列表</span>
|
||||
<div>
|
||||
<el-button type="success" @click="handleRefreshCache" style="margin-right: 10px">
|
||||
<el-icon><Refresh /></el-icon>刷新缓存
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增字典
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="dictId" label="ID" width="80" />
|
||||
<el-table-column prop="dictType" label="字典类型" min-width="150" />
|
||||
<el-table-column prop="dictTypeName" label="类型名称" min-width="150" />
|
||||
<el-table-column prop="dictCode" label="字典编码" width="120" />
|
||||
<el-table-column prop="dictLabel" label="字典标签" min-width="150" />
|
||||
<el-table-column prop="dictValue" label="字典值" min-width="120" />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||
<el-table-column prop="isDefault" label="默认" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isDefault === 1 ? 'success' : 'info'">
|
||||
{{ row.isDefault === 1 ? '是' : '否' }}
|
||||
</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"
|
||||
@change="handleStatusChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="dictType">
|
||||
<el-input v-model="formData.dictType" placeholder="请输入字典类型编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型名称" prop="dictTypeName">
|
||||
<el-input v-model="formData.dictTypeName" placeholder="请输入字典类型名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典编码" prop="dictCode">
|
||||
<el-input v-model="formData.dictCode" placeholder="请输入字典编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签" prop="dictLabel">
|
||||
<el-input v-model="formData.dictLabel" placeholder="请输入字典标签" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dictValue">
|
||||
<el-input v-model="formData.dictValue" placeholder="请输入字典值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序号" prop="sortOrder">
|
||||
<el-input-number v-model="formData.sortOrder" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否默认">
|
||||
<el-radio-group v-model="formData.isDefault">
|
||||
<el-radio :label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="样式类名">
|
||||
<el-input v-model="formData.cssClass" placeholder="请输入CSS样式类名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="回显样式">
|
||||
<el-select v-model="formData.listClass" placeholder="请选择回显样式" style="width: 100%">
|
||||
<el-option label="默认" value="" />
|
||||
<el-option label="主要" value="primary" />
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="警告" value="warning" />
|
||||
<el-option label="危险" value="danger" />
|
||||
<el-option label="信息" value="info" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
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">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictPage,
|
||||
getAllDictTypes,
|
||||
createDict,
|
||||
updateDict,
|
||||
deleteDict
|
||||
} from '@/api/sysDict'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
dictType: '',
|
||||
dictLabel: ''
|
||||
})
|
||||
|
||||
// 字典类型列表
|
||||
const dictTypeList = ref([])
|
||||
|
||||
// 表格数据
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const selectedRows = ref([])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
const formData = reactive({
|
||||
dictId: null,
|
||||
dictType: '',
|
||||
dictTypeName: '',
|
||||
dictCode: '',
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sortOrder: 0,
|
||||
isDefault: 0,
|
||||
cssClass: '',
|
||||
listClass: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
dictType: [{ required: true, message: '请输入字典类型', trigger: 'blur' }],
|
||||
dictTypeName: [{ required: true, message: '请输入类型名称', trigger: 'blur' }],
|
||||
dictCode: [{ required: true, message: '请输入字典编码', trigger: 'blur' }],
|
||||
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 加载字典类型
|
||||
const loadDictTypes = async () => {
|
||||
try {
|
||||
const res = await getAllDictTypes()
|
||||
dictTypeList.value = res.data || []
|
||||
} catch (error) {
|
||||
console.error('加载字典类型失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDictPage({
|
||||
current: pagination.current,
|
||||
size: pagination.size,
|
||||
...searchForm
|
||||
})
|
||||
tableData.value = res.data.records
|
||||
pagination.total = res.data.total
|
||||
} catch (error) {
|
||||
console.error('加载字典列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.dictType = ''
|
||||
searchForm.dictLabel = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (rows) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (current) => {
|
||||
pagination.current = current
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
const handleRefreshCache = () => {
|
||||
loadDictTypes()
|
||||
loadData()
|
||||
ElMessage.success('缓存刷新成功')
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增字典'
|
||||
Object.assign(formData, {
|
||||
dictId: null,
|
||||
dictType: '',
|
||||
dictTypeName: '',
|
||||
dictCode: '',
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sortOrder: 0,
|
||||
isDefault: 0,
|
||||
cssClass: '',
|
||||
listClass: '',
|
||||
remark: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑字典'
|
||||
Object.assign(formData, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确认删除该字典吗?', '提示', {
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteDict(row.dictId)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
const handleStatusChange = async (row) => {
|
||||
try {
|
||||
await updateDict(row.dictId, { status: row.status })
|
||||
ElMessage.success('状态更新成功')
|
||||
} catch (error) {
|
||||
row.status = row.status === 1 ? 0 : 1
|
||||
console.error('状态更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateDict(formData.dictId, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createDict(formData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
loadDictTypes()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDictTypes()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -1,361 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="handleAdd">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :inline="true" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="searchForm.realName" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.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>
|
||||
|
||||
<el-table :data="tableData" border v-loading="loading">
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="realName" label="姓名" width="120" />
|
||||
<el-table-column prop="phone" label="手机号" width="140" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="position" label="职位" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="warning" link @click="handleResetPassword(scope.row)">重置密码</el-button>
|
||||
<el-button
|
||||
:type="scope.row.status === 1 ? 'danger' : 'success'"
|
||||
link
|
||||
@click="handleToggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 1 ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" :disabled="!!form.userId" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" prop="realName">
|
||||
<el-input v-model="form.realName" 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="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="性别">
|
||||
<el-select v-model="form.gender" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="男" :value="1" />
|
||||
<el-option label="女" :value="2" />
|
||||
<el-option label="保密" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="职位">
|
||||
<el-input v-model="form.position" placeholder="请输入职位" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item v-if="!form.userId" label="密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
|
||||
</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="passwordDialogVisible" title="重置密码" width="400px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input v-model="passwordForm.newPassword" type="password" placeholder="请输入新密码" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="passwordDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handlePasswordSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getUserList, createUser, updateUser, deleteUser, resetPassword, updateUserStatus } from '../../api/user'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
realName: '',
|
||||
status: null
|
||||
})
|
||||
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
userId: null,
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
gender: 1,
|
||||
position: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const passwordDialogVisible = ref(false)
|
||||
const passwordForm = reactive({
|
||||
userId: null,
|
||||
newPassword: ''
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserList({
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
username: searchForm.username,
|
||||
realName: searchForm.realName,
|
||||
status: searchForm.status
|
||||
})
|
||||
tableData.value = res.records
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.username = ''
|
||||
searchForm.realName = ''
|
||||
searchForm.status = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增用户'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑用户'
|
||||
resetForm()
|
||||
Object.assign(form, row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.userId = null
|
||||
form.username = ''
|
||||
form.password = ''
|
||||
form.realName = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.gender = 1
|
||||
form.position = ''
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
ElMessage.warning('请检查表单填写是否完整')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (form.userId) {
|
||||
await updateUser(form.userId, form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createUser(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetPassword = (row) => {
|
||||
passwordForm.userId = row.userId
|
||||
passwordForm.newPassword = ''
|
||||
passwordDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
if (!passwordForm.newPassword) {
|
||||
ElMessage.warning('请输入新密码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await resetPassword(passwordForm.userId, passwordForm.newPassword)
|
||||
ElMessage.success('密码重置成功')
|
||||
passwordDialogVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('密码重置失败:', error)
|
||||
ElMessage.error(error.message || '密码重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleStatus = (row) => {
|
||||
const newStatus = row.status === 1 ? 0 : 1
|
||||
const actionText = newStatus === 1 ? '启用' : '禁用'
|
||||
ElMessageBox.confirm(`确定要${actionText}该用户吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await updateUserStatus(row.userId, newStatus)
|
||||
ElMessage.success(`${actionText}成功`)
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error(error.message || '操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteUser(row.userId)
|
||||
ElMessage.success('删除成功')
|
||||
// 如果删除的是当前页最后一条且不是第一页,返回上一页
|
||||
if (tableData.value.length === 1 && page.current > 1) {
|
||||
page.current--
|
||||
}
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
page.size = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.current = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@ -1,407 +0,0 @@
|
||||
<template>
|
||||
<div class="assignment-container">
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable style="width: 150px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="真实姓名">
|
||||
<el-input v-model="searchForm.realName" placeholder="请输入真实姓名" clearable style="width: 150px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属部门">
|
||||
<el-tree-select
|
||||
v-model="searchForm.deptId"
|
||||
:data="deptTreeOptions"
|
||||
placeholder="请选择部门"
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位">
|
||||
<el-select v-model="searchForm.postId" placeholder="请选择岗位" clearable style="width: 150px;">
|
||||
<el-option
|
||||
v-for="post in postOptions"
|
||||
:key="post.postId"
|
||||
:label="post.postName"
|
||||
:value="post.postId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 120px;">
|
||||
<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>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-row style="margin-bottom: 15px;">
|
||||
<el-button type="primary" @click="handleBatchAssign" :disabled="selectedUsers.length === 0">
|
||||
批量分配
|
||||
</el-button>
|
||||
<el-button @click="handleShowUnassigned" :type="showUnassigned ? 'primary' : ''">
|
||||
{{ showUnassigned ? '显示全部' : '仅显示未分配' }}
|
||||
</el-button>
|
||||
<span style="margin-left: 15px; color: #909399;">
|
||||
已选择 {{ selectedUsers.length }} 人
|
||||
</span>
|
||||
</el-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
border
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="userId" label="用户ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="realName" label="真实姓名" width="120" />
|
||||
<el-table-column prop="gender" label="性别" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.gender === 1 ? '男' : row.gender === 2 ? '女' : '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="手机号" width="140" />
|
||||
<el-table-column prop="deptName" label="所属部门" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.deptName && row.deptName !== '-'" type="info">{{ row.deptName }}</el-tag>
|
||||
<el-tag v-else type="warning">未分配</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="postName" label="岗位" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.postName && row.postName !== '-'" type="success">{{ row.postName }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="position" label="职位" width="150" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 1" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleAssign(row)">分配</el-button>
|
||||
<el-button
|
||||
v-if="row.deptId || row.postId"
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleRemove(row)"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
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
|
||||
v-model="dialogVisible"
|
||||
:title="isBatch ? '批量分配用户' : '分配用户'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="选择用户" v-if="isBatch">
|
||||
<el-tag
|
||||
v-for="user in selectedUsers"
|
||||
:key="user.userId"
|
||||
style="margin-right: 5px; margin-bottom: 5px;"
|
||||
>
|
||||
{{ user.realName || user.username }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户" v-else>
|
||||
<span>{{ form.userName }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属部门">
|
||||
<el-tree-select
|
||||
v-model="form.deptId"
|
||||
:data="deptTreeOptions"
|
||||
placeholder="请选择部门"
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="岗位">
|
||||
<el-select v-model="form.postId" placeholder="请选择岗位" clearable style="width: 100%;">
|
||||
<el-option
|
||||
v-for="post in postOptions"
|
||||
:key="post.postId"
|
||||
:label="post.postName"
|
||||
:value="post.postId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职位">
|
||||
<el-input v-model="form.position" 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getUserAssignmentList,
|
||||
assignUser,
|
||||
batchAssignUsers,
|
||||
removeUserAssignment
|
||||
} from '../../api/userAssignment'
|
||||
import { getDeptTree } from '../../api/dept'
|
||||
import { getPostListEnabled } from '../../api/post'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: '',
|
||||
realName: '',
|
||||
deptId: null,
|
||||
postId: null,
|
||||
status: null
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
const showUnassigned = ref(false)
|
||||
|
||||
// 分页
|
||||
const page = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 选中的用户
|
||||
const selectedUsers = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const isBatch = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
userId: null,
|
||||
userName: '',
|
||||
deptId: null,
|
||||
postId: null,
|
||||
position: ''
|
||||
})
|
||||
|
||||
// 部门树选项
|
||||
const deptTreeOptions = ref([])
|
||||
|
||||
// 岗位选项
|
||||
const postOptions = ref([])
|
||||
|
||||
// 加载部门树
|
||||
const loadDeptTree = async () => {
|
||||
try {
|
||||
const data = await getDeptTree()
|
||||
deptTreeOptions.value = convertToTreeSelect(data)
|
||||
} catch (error) {
|
||||
console.error('加载部门树失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载岗位列表
|
||||
const loadPostOptions = async () => {
|
||||
try {
|
||||
const data = await getPostListEnabled()
|
||||
postOptions.value = data
|
||||
} catch (error) {
|
||||
console.error('加载岗位列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为TreeSelect格式
|
||||
const convertToTreeSelect = (data) => {
|
||||
return data.map(item => ({
|
||||
value: item.deptId,
|
||||
label: item.deptName,
|
||||
children: item.children ? convertToTreeSelect(item.children) : []
|
||||
}))
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
username: searchForm.username || undefined,
|
||||
realName: searchForm.realName || undefined,
|
||||
deptId: searchForm.deptId || undefined,
|
||||
postId: searchForm.postId || undefined,
|
||||
status: searchForm.status !== null && searchForm.status !== '' ? searchForm.status : undefined
|
||||
}
|
||||
const res = await getUserAssignmentList(params)
|
||||
|
||||
let data = res.records
|
||||
|
||||
// 如果显示未分配,过滤掉已分配的
|
||||
if (showUnassigned.value) {
|
||||
data = data.filter(item => !item.deptId && !item.postId)
|
||||
}
|
||||
|
||||
tableData.value = data
|
||||
page.total = res.total
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error(error.message || '加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.username = ''
|
||||
searchForm.realName = ''
|
||||
searchForm.deptId = null
|
||||
searchForm.postId = null
|
||||
searchForm.status = null
|
||||
showUnassigned.value = false
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 显示未分配
|
||||
const handleShowUnassigned = () => {
|
||||
showUnassigned.value = !showUnassigned.value
|
||||
page.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedUsers.value = selection
|
||||
}
|
||||
|
||||
// 单个分配
|
||||
const handleAssign = (row) => {
|
||||
isBatch.value = false
|
||||
form.userId = row.userId
|
||||
form.userName = row.realName || row.username
|
||||
form.deptId = row.deptId
|
||||
form.postId = row.postId
|
||||
form.position = row.position
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 批量分配
|
||||
const handleBatchAssign = () => {
|
||||
if (selectedUsers.value.length === 0) {
|
||||
ElMessage.warning('请选择要分配的用户')
|
||||
return
|
||||
}
|
||||
isBatch.value = true
|
||||
form.deptId = null
|
||||
form.postId = null
|
||||
form.position = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交分配
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isBatch.value) {
|
||||
// 批量分配
|
||||
const userIds = selectedUsers.value.map(u => u.userId)
|
||||
await batchAssignUsers(userIds, form.deptId, form.postId)
|
||||
ElMessage.success(`成功分配 ${userIds.length} 个用户`)
|
||||
} else {
|
||||
// 单个分配
|
||||
await assignUser(form.userId, {
|
||||
deptId: form.deptId,
|
||||
postId: form.postId,
|
||||
position: form.position
|
||||
})
|
||||
ElMessage.success('分配成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
selectedUsers.value = []
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('分配失败:', error)
|
||||
ElMessage.error(error.message || '分配失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 移除分配
|
||||
const handleRemove = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要移除 ${row.realName || row.username} 的部门/岗位分配吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await removeUserAssignment(row.userId)
|
||||
ElMessage.success('移除分配成功')
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('移除分配失败:', error)
|
||||
ElMessage.error(error.message || '移除分配失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadDeptTree()
|
||||
loadPostOptions()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.assignment-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1 +1 @@
|
||||
Subproject commit 85528d3f3bcefbcea1a237175ee93b18f7b8feb2
|
||||
Subproject commit b2e2535d6798d027d15320c5cab163907c2fb920
|
||||
429
开发规则清单.md
429
开发规则清单.md
@ -1,429 +0,0 @@
|
||||
# 资金服务平台 - 开发规则清单
|
||||
|
||||
> **创建时间**: 2026-02-17
|
||||
> **用途**: 重新生成代码前的规范和约束
|
||||
> **版本**: v1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. 技术栈规范
|
||||
|
||||
### 1.1 核心框架
|
||||
- **Java**: 21
|
||||
- **Spring Boot**: 3.2.0
|
||||
- **Spring Cloud**: 2023.0.0
|
||||
- **认证框架**: Apache Shiro 2.0.0(必须,不使用 Spring Security)
|
||||
- **数据库连接池**: HikariCP
|
||||
- **ORM框架**: MyBatis-Plus 3.5.6
|
||||
|
||||
### 1.2 依赖版本锁定
|
||||
```xml
|
||||
<lombok.version>1.18.30</lombok.version>
|
||||
<shiro.version>2.0.0</shiro.version>
|
||||
<jwt.version>4.4.0</jwt.version>
|
||||
<hutool.version>5.8.23</hutool.version>
|
||||
<mybatis-plus.version>3.5.6</mybatis-plus.version>
|
||||
```
|
||||
|
||||
### 1.3 禁用的依赖
|
||||
- ❌ `spring-boot-starter-security`
|
||||
- ❌ Shiro 1.x(不兼容 Spring Boot 3)
|
||||
|
||||
---
|
||||
|
||||
## 2. Maven 多模块配置规范
|
||||
|
||||
### 2.1 父 POM 配置
|
||||
```xml
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
```
|
||||
|
||||
### 2.2 子模块 POM 配置
|
||||
**每个子模块都必须显式激活 maven-compiler-plugin**:
|
||||
```xml
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
### 2.3 依赖冲突检查
|
||||
- ⚠️ 避免在 pom.xml 中重复声明同一依赖
|
||||
- ⚠️ 使用 `mvn dependency:tree` 检查依赖冲突
|
||||
|
||||
---
|
||||
|
||||
## 3. Lombok 使用规范
|
||||
|
||||
### 3.1 禁止在继承 ServiceImpl 的类上使用 @Slf4j
|
||||
|
||||
**❌ 错误示例**:
|
||||
```java
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DeptService extends ServiceImpl<DeptMapper, Dept> {
|
||||
public void saveDept(Dept dept) {
|
||||
log.info("保存部门"); // 编译错误!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**✅ 正确示例**:
|
||||
```java
|
||||
@Service
|
||||
public class DeptService extends ServiceImpl<DeptMapper, Dept> {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DeptService.class);
|
||||
|
||||
public void saveDept(Dept dept) {
|
||||
logger.info("保存部门");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原因**: MyBatis-Plus 的 ServiceImpl 基类已定义 `protected Log log`(org.apache.ibatis.logging.Log),与 Lombok @Slf4j 生成的字段冲突。
|
||||
|
||||
### 3.2 实体类 Lombok 注解规范
|
||||
```java
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_dept")
|
||||
public class Dept extends BaseEntity {
|
||||
// 必须确保 import lombok.Data 和 import lombok.EqualsAndHashCode 存在
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Controller/Component Lombok 注解
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dept")
|
||||
@RequiredArgsConstructor // 推荐用于依赖注入
|
||||
public class DeptController {
|
||||
private final DeptService deptService;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Shiro 认证框架规范
|
||||
|
||||
### 4.1 核心组件清单
|
||||
1. **JwtToken.java** - 自定义 Token 类
|
||||
2. **JwtRealm.java** - 认证授权域
|
||||
3. **JwtFilter.java** - JWT 过滤器
|
||||
4. **ShiroConfig.java** - Shiro 配置类
|
||||
5. **PasswordEncoderConfig.java** - 密码编码器(使用 BCrypt)
|
||||
|
||||
### 4.2 权限注解规范
|
||||
- ✅ 使用 `@RequiresRoles("ADMIN")`
|
||||
- ✅ 使用 `@RequiresPermissions("system:user:view")`
|
||||
- ❌ 不使用 Spring Security 注解(@PreAuthorize)
|
||||
|
||||
### 4.3 认证流程
|
||||
1. 用户登录 → 查询数据库
|
||||
2. BCrypt 验证密码
|
||||
3. 生成 JWT Token(包含 userId, username, tenantId)
|
||||
4. 每次请求通过 JwtFilter 验证 Token
|
||||
5. JwtRealm 加载用户权限
|
||||
|
||||
---
|
||||
|
||||
## 5. 代码编写规范
|
||||
|
||||
### 5.1 分层架构
|
||||
```
|
||||
Controller → Service → Mapper → Database
|
||||
↓ ↓
|
||||
DTO Entity
|
||||
```
|
||||
|
||||
### 5.2 包结构规范
|
||||
```
|
||||
com.fundplatform.sys
|
||||
├── controller # 控制器层
|
||||
├── service # 业务逻辑层
|
||||
├── mapper # 数据访问层
|
||||
├── entity # 实体类
|
||||
├── dto # 数据传输对象
|
||||
├── vo # 视图对象
|
||||
├── security # 安全相关(Shiro配置)
|
||||
├── config # 配置类
|
||||
└── util # 工具类
|
||||
```
|
||||
|
||||
### 5.3 命名规范
|
||||
- **Entity**: 单数名词,如 `User.java`、`Dept.java`
|
||||
- **Mapper**: 实体名 + Mapper,如 `UserMapper.java`
|
||||
- **Service**: 实体名 + Service,如 `UserService.java`
|
||||
- **Controller**: 实体名 + Controller,如 `UserController.java`
|
||||
|
||||
### 5.4 日志规范
|
||||
```java
|
||||
// 业务操作日志
|
||||
logger.info("[ServiceName] 业务操作描述: {}, 参数: {}", entity.getName(), param);
|
||||
|
||||
// 错误日志
|
||||
logger.error("[ServiceName] 操作失败: {}", errorMessage, exception);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据库规范
|
||||
|
||||
### 6.1 表命名规范
|
||||
- 系统表前缀: `sys_`(如 `sys_user`, `sys_role`)
|
||||
- 业务表前缀: `biz_`(如 `biz_project`, `biz_fund`)
|
||||
|
||||
### 6.2 字段规范
|
||||
- 主键: `主表名_id`(如 `user_id`, `dept_id`)
|
||||
- 租户ID: `tenant_id`(必须字段,默认值 1)
|
||||
- 软删除: `deleted`(0-未删除,1-已删除)
|
||||
- 状态: `status`(1-启用,0-禁用)
|
||||
- 时间: `created_time`, `updated_time`
|
||||
|
||||
### 6.3 BaseEntity 基类
|
||||
```java
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
private Long tenantId; // 租户ID
|
||||
private Integer deleted; // 删除标记
|
||||
private LocalDateTime createdTime; // 创建时间
|
||||
private Long createdBy; // 创建人
|
||||
private LocalDateTime updatedTime; // 更新时间
|
||||
private Long updatedBy; // 更新人
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. API 规范
|
||||
|
||||
### 7.1 RESTful 路径规范
|
||||
```
|
||||
GET /api/v1/dept # 查询列表
|
||||
GET /api/v1/dept/{id} # 查询单个
|
||||
POST /api/v1/dept # 创建
|
||||
PUT /api/v1/dept/{id} # 更新
|
||||
DELETE /api/v1/dept/{id} # 删除
|
||||
```
|
||||
|
||||
### 7.2 统一响应格式
|
||||
```java
|
||||
public class Result<T> {
|
||||
private Integer code; // 200-成功,其他-失败
|
||||
private String message;
|
||||
private T data;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 认证路径规范
|
||||
```
|
||||
POST /api/v1/auth/login # 登录(不需要认证)
|
||||
POST /api/v1/auth/logout # 登出
|
||||
POST /api/v1/auth/refresh # 刷新Token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 多租户规范
|
||||
|
||||
### 8.1 租户隔离
|
||||
- 所有业务表必须包含 `tenant_id` 字段
|
||||
- Service 层插入数据时自动设置 `tenant_id`
|
||||
- 查询时自动过滤 `tenant_id`
|
||||
|
||||
### 8.2 默认租户
|
||||
- 系统租户: `tenant_id = 0`
|
||||
- 业务租户: `tenant_id >= 1`
|
||||
|
||||
---
|
||||
|
||||
## 9. 编译和测试规范
|
||||
|
||||
### 9.1 编译命令
|
||||
```bash
|
||||
# 清理编译
|
||||
cd /home/along/MyCode/wanjiabuluo/fundplatform/fundplatform
|
||||
mvn clean compile -DskipTests
|
||||
|
||||
# 安装到本地仓库
|
||||
mvn clean install -DskipTests
|
||||
|
||||
# 编译单个模块
|
||||
mvn clean compile -pl fund-sys -am -DskipTests
|
||||
```
|
||||
|
||||
### 9.2 禁止的操作
|
||||
- ❌ 不要在 run_in_terminal 中重复 `cd` 到同一目录
|
||||
- ❌ 不要使用 `mvn clean` 后立即执行其他命令(应分开执行)
|
||||
|
||||
---
|
||||
|
||||
## 10. Git 规范
|
||||
|
||||
### 10.1 分支策略
|
||||
- `main` - 生产分支
|
||||
- `develop` - 开发分支
|
||||
- `feature/*` - 功能分支
|
||||
- `bugfix/*` - 修复分支
|
||||
|
||||
### 10.2 提交规范
|
||||
```
|
||||
feat: 新功能
|
||||
fix: 修复bug
|
||||
refactor: 重构
|
||||
docs: 文档更新
|
||||
style: 代码格式调整
|
||||
test: 测试相关
|
||||
chore: 构建/工具链相关
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 常见陷阱清单
|
||||
|
||||
### 11.1 Lombok 相关
|
||||
- ✓ 每个子模块必须显式配置 maven-compiler-plugin
|
||||
- ✓ 继承 ServiceImpl 的类不能使用 @Slf4j
|
||||
- ✓ 确保所有 import lombok.* 语句存在
|
||||
|
||||
### 11.2 Shiro 相关
|
||||
- ✓ 使用 Shiro 2.0.0(支持 jakarta.servlet)
|
||||
- ✓ 保留 spring-security-crypto(用于 BCrypt)
|
||||
- ✓ 登录接口路径配置为匿名访问
|
||||
|
||||
### 11.3 Maven 相关
|
||||
- ✓ 避免在 pom.xml 中重复声明依赖
|
||||
- ✓ 父 pom 中未创建的子模块需注释掉
|
||||
- ✓ 使用 `-U` 参数强制更新依赖
|
||||
|
||||
### 11.4 数据库相关
|
||||
- ✓ 初始化 SQL 主键必须与字段定义一致
|
||||
- ✓ tenant_id 必须有默认值保障机制
|
||||
- ✓ 软删除字段 deleted 默认为 0
|
||||
|
||||
---
|
||||
|
||||
## 12. 重新生成代码的策略
|
||||
|
||||
### 12.1 代码生成工具选择
|
||||
**选项 A**: MyBatis-Plus 代码生成器
|
||||
- 优点: 自动生成 Entity、Mapper、Service、Controller
|
||||
- 缺点: 需要手动调整生成的代码
|
||||
|
||||
**选项 B**: 手动编写
|
||||
- 优点: 完全可控,符合规范
|
||||
- 缺点: 工作量大
|
||||
|
||||
**选项 C**: 模板化生成
|
||||
- 优点: 可重复使用,质量稳定
|
||||
- 缺点: 需要先建立模板
|
||||
|
||||
### 12.2 推荐策略
|
||||
1. **第一步**: 手动编写核心基础类(BaseEntity、Result、自定义异常)
|
||||
2. **第二步**: 使用 MyBatis-Plus 生成器生成基础 CRUD
|
||||
3. **第三步**: 手动完善业务逻辑和复杂查询
|
||||
4. **第四步**: 编写单元测试验证
|
||||
|
||||
### 12.3 生成顺序
|
||||
```
|
||||
1. fund-common (基础类)
|
||||
├── BaseEntity
|
||||
├── Result
|
||||
└── 工具类
|
||||
|
||||
2. fund-sys (系统模块)
|
||||
├── 实体类 (Entity)
|
||||
├── Mapper 接口
|
||||
├── Service 实现
|
||||
├── Controller
|
||||
└── Shiro 配置
|
||||
|
||||
3. fund-gateway (网关)
|
||||
└── 路由配置
|
||||
|
||||
4. fund-cust (客户模块)
|
||||
5. fund-proj (项目模块)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 架构约束
|
||||
|
||||
### 13.1 必须遵守的架构要求
|
||||
1. ✅ 使用 Shiro 作为认证框架(架构文档要求)
|
||||
2. ✅ 实现多租户数据隔离
|
||||
3. ✅ Header 透传(X-Uid, X-Uname)
|
||||
4. ✅ 全链路日志追踪
|
||||
|
||||
### 13.2 性能要求
|
||||
- 接口响应时间 < 500ms
|
||||
- 数据库查询使用索引
|
||||
- 使用 Redis 缓存热点数据
|
||||
|
||||
---
|
||||
|
||||
## 14. 下一步行动
|
||||
|
||||
### 14.1 准备工作
|
||||
- [ ] 备份当前代码
|
||||
- [ ] 清理所有 target 目录
|
||||
- [ ] 确认数据库表结构
|
||||
- [ ] 准备初始化 SQL 脚本
|
||||
|
||||
### 14.2 代码生成
|
||||
- [ ] 生成 fund-common 基础类
|
||||
- [ ] 生成 fund-sys 实体类和 Mapper
|
||||
- [ ] 实现 Shiro 认证框架
|
||||
- [ ] 编写核心业务逻辑
|
||||
|
||||
### 14.3 验证
|
||||
- [ ] 编译通过(mvn clean compile)
|
||||
- [ ] 启动成功
|
||||
- [ ] 登录功能正常
|
||||
- [ ] 基础 CRUD 接口正常
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本规则清单将持续更新,每次遇到问题都会添加到"常见陷阱清单"中。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user