feat(frontend): 添加用户服务和数据导出功能
- 添加 user.ts 用户管理服务 - 添加 useDataExport.ts 数据导出composable - 增强审计日志页面筛选功能
This commit is contained in:
@@ -3,39 +3,44 @@
|
|||||||
## Task Info
|
## Task Info
|
||||||
- **Task**: 实施蚊子系统管理后台权限管理系统
|
- **Task**: 实施蚊子系统管理后台权限管理系统
|
||||||
- **Start Time**: 2026-03-04
|
- **Start Time**: 2026-03-04
|
||||||
- **Max Iterations**: 100
|
- **Iterations**: 9
|
||||||
|
|
||||||
## Current State
|
|
||||||
- **Iteration**: 8
|
|
||||||
- **Status**: In Progress
|
|
||||||
- **Current Phase**: Phase 2 & 3 进行中
|
|
||||||
|
|
||||||
## Progress Summary
|
## Progress Summary
|
||||||
- [x] Phase 1: 数据库表创建(10张表)100%
|
|
||||||
- [x] Phase 2: 后端权限核心模块 100%
|
### Phase 1: 数据库层 ✅ 100%
|
||||||
- 实体: SysRole, SysPermission, SysDepartment, SysUserRole, SysRolePermission
|
- 10张权限相关数据库表 (Flyway)
|
||||||
- Repositories: 完整的JPA查询
|
|
||||||
- Services: RoleService, PermissionService, DepartmentService, PermissionCheckService
|
### Phase 2: 后端权限核心 ✅ 100%
|
||||||
- Controllers: RoleController, PermissionController, ApprovalController, UserController
|
- 实体: SysRole, SysPermission, SysDepartment, SysUserRole, SysRolePermission
|
||||||
- [x] Phase 2: 前端权限组件 100%
|
- Repositories: 完整的JPA查询
|
||||||
- 角色权限类型定义 (13角色, 40+权限)
|
- Services: RoleService, PermissionService, DepartmentService, PermissionCheckService, ApprovalFlowService
|
||||||
- 权限服务 (permission.ts, role.ts, approval.ts)
|
- Controllers: RoleController, PermissionController, ApprovalController, UserController
|
||||||
- 权限组件 (PermissionButton, PermissionDialog)
|
|
||||||
- 权限 composable (usePermission)
|
### Phase 2: 前端权限 ✅ 100%
|
||||||
- 路由守卫 (permissionGuard)
|
- 角色权限类型: 13角色, 40+权限
|
||||||
- 角色管理页面
|
- 服务: permission.ts, role.ts, approval.ts, department.ts
|
||||||
- [ ] Phase 3: 审批流引擎 30%
|
- 组件: PermissionButton.vue, PermissionDialog.vue
|
||||||
- [ ] Phase 4: 业务模块开发 0%
|
- Composable: usePermission.ts
|
||||||
|
- 路由守卫: permissionGuard.ts
|
||||||
|
- 页面: RoleManagementView.vue, DepartmentManagementView.vue, SystemConfigView.vue
|
||||||
|
|
||||||
|
### Phase 3: 审批流 ⏳ 40%
|
||||||
|
- 前端服务 approval.ts
|
||||||
|
- 后端审批控制器
|
||||||
|
- 审批流Service
|
||||||
|
|
||||||
|
### Phase 4: 业务模块 ⏳ 10%
|
||||||
|
- 现有页面完善
|
||||||
|
|
||||||
## Recent Commits
|
## Recent Commits
|
||||||
- ddae043: 修复 JPA 查询兼容性问题
|
- ce258c3: 部门管理和系统配置页面
|
||||||
- 64bae7c: 前端权限系统完善
|
|
||||||
- 62b1eef: 权限核心模块后端
|
|
||||||
- c621af0: 角色管理功能
|
|
||||||
- 061328e: 审批流服务
|
|
||||||
- e08192b: 权限和审批控制器
|
- e08192b: 权限和审批控制器
|
||||||
|
- 061328e: 审批流服务
|
||||||
|
- c621af0: 角色管理功能
|
||||||
|
- 64bae7c: 前端权限系统
|
||||||
|
- 62b1eef: 权限核心模块
|
||||||
|
|
||||||
## Next Steps
|
## Next
|
||||||
1. 完成审批流后端 Service 实现
|
1. 完善审批流Service实现
|
||||||
2. 创建审批流前端页面
|
2. 添加更多业务模块页面
|
||||||
3. 继续 Phase 4 业务模块
|
3. 完善测试覆盖
|
||||||
|
|||||||
166
frontend/admin/src/composables/useDataExport.ts
Normal file
166
frontend/admin/src/composables/useDataExport.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* 数据导出 composable
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface ExportColumn {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
width?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportOptions {
|
||||||
|
filename: string
|
||||||
|
columns: ExportColumn[]
|
||||||
|
data: Record<string, any>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDataExport() {
|
||||||
|
const exporting = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出为CSV
|
||||||
|
*/
|
||||||
|
const exportToCsv = async (options: ExportOptions) => {
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
const { filename, columns, data } = options
|
||||||
|
|
||||||
|
// 构建CSV内容
|
||||||
|
const header = columns.map(col => `"${col.label}"`).join(',')
|
||||||
|
const rows = data.map(item =>
|
||||||
|
columns.map(col => {
|
||||||
|
const value = item[col.key]
|
||||||
|
if (value === null || value === undefined) return ''
|
||||||
|
return `"${String(value).replace(/"/g, '""')}"`
|
||||||
|
}).join(',')
|
||||||
|
)
|
||||||
|
|
||||||
|
const csv = [header, ...rows].join('\n')
|
||||||
|
|
||||||
|
// 添加BOM以支持中文
|
||||||
|
const BOM = '\uFEFF'
|
||||||
|
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' })
|
||||||
|
|
||||||
|
// 下载
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `${filename}_${formatDate(new Date())}.csv`
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出为JSON
|
||||||
|
*/
|
||||||
|
const exportToJson = async (options: ExportOptions) => {
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
const { filename, data } = options
|
||||||
|
|
||||||
|
const json = JSON.stringify(data, null, 2)
|
||||||
|
const blob = new Blob([json], { type: 'application/json' })
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `${filename}_${formatDate(new Date())}.json`
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出为Excel (使用HTML table方式)
|
||||||
|
*/
|
||||||
|
const exportToExcel = async (options: ExportOptions) => {
|
||||||
|
exporting.value = true
|
||||||
|
try {
|
||||||
|
const { filename, columns, data } = options
|
||||||
|
|
||||||
|
const tableHtml = `
|
||||||
|
<table border="1">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
${columns.map(col => `<th>${col.label}</th>`).join('')}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${data.map(item =>
|
||||||
|
`<tr>${columns.map(col => `<td>${item[col.key] ?? ''}</td>`).join('')}</tr>`
|
||||||
|
).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`
|
||||||
|
|
||||||
|
const blob = new Blob([tableHtml], { type: 'application/vnd.ms-excel' })
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `${filename}_${formatDate(new Date())}.xls`
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
} finally {
|
||||||
|
exporting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印数据
|
||||||
|
*/
|
||||||
|
const printData = (options: ExportOptions) => {
|
||||||
|
const { columns, data } = options
|
||||||
|
|
||||||
|
const tableHtml = `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
table { border-collapse: collapse; width: 100%; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||||
|
th { background-color: #f2f2f2; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
${columns.map(col => `<th>${col.label}</th>`).join('')}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${data.map(item =>
|
||||||
|
`<tr>${columns.map(col => `<td>${item[col.key] ?? ''}</td>`).join('')}</tr>`
|
||||||
|
).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
const printWindow = window.open('', '_blank')
|
||||||
|
if (printWindow) {
|
||||||
|
printWindow.document.write(tableHtml)
|
||||||
|
printWindow.document.close()
|
||||||
|
printWindow.print()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exporting,
|
||||||
|
exportToCsv,
|
||||||
|
exportToJson,
|
||||||
|
exportToExcel,
|
||||||
|
printData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date: Date): string {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}${month}${day}`
|
||||||
|
}
|
||||||
127
frontend/admin/src/services/user.ts
Normal file
127
frontend/admin/src/services/user.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* 用户管理服务
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
status: number
|
||||||
|
roles?: string[]
|
||||||
|
departmentId?: number
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserService {
|
||||||
|
private baseUrl = '/api'
|
||||||
|
|
||||||
|
async getUsers(params?: { page?: number; size?: number; keyword?: string }): Promise<User[]> {
|
||||||
|
const searchParams = new URLSearchParams()
|
||||||
|
if (params?.page) searchParams.set('page', String(params.page))
|
||||||
|
if (params?.size) searchParams.set('size', String(params.size))
|
||||||
|
if (params?.keyword) searchParams.set('keyword', params.keyword)
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/users?${searchParams}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<User[]>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
throw new Error(result.message || '获取用户列表失败')
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserById(id: number): Promise<User | null> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/users/${id}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<User>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(data: Partial<User>): Promise<number> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/users`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<number>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
throw new Error(result.message || '创建用户失败')
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser(id: number, data: Partial<User>): Promise<void> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/users/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<void>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
throw new Error(result.message || '更新用户失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(id: number): Promise<void> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/users/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<void>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
throw new Error(result.message || '删除用户失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async freezeUser(id: number): Promise<void> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/users/${id}/freeze`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<void>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
throw new Error(result.message || '冻结用户失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unfreezeUser(id: number): Promise<void> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/users/${id}/unfreeze`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<void>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
throw new Error(result.message || '解冻用户失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignRoles(userId: number, roleIds: number[]): Promise<void> {
|
||||||
|
const response = await fetch(`${this.baseUrl}/users/${userId}/roles`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ roleIds })
|
||||||
|
})
|
||||||
|
const result = await response.json() as ApiResponse<void>
|
||||||
|
if (result.code !== 200) {
|
||||||
|
throw new Error(result.message || '分配角色失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userService = new UserService()
|
||||||
|
export default userService
|
||||||
Reference in New Issue
Block a user