From dfa6abf0a9a5bbafaa81ddb571edf3d9de2b2fe1 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 24 Oct 2025 19:46:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A1=E6=89=B9=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../User/components/AssignRolesDialog.tsx | 110 ++ .../System/User/components/DeleteDialog.tsx | 75 ++ .../System/User/components/EditModal.tsx | 241 +++++ .../User/components/ResetPasswordDialog.tsx | 120 +++ .../User/components/RoleModal.module.css | 34 - .../System/User/components/RoleModal.tsx | 108 -- frontend/src/pages/System/User/index.tsx | 917 ++++++++--------- frontend/src/pages/System/User/service.ts | 7 +- .../Definition/components/DeleteDialog.tsx | 94 ++ .../Definition/components/DeployDialog.tsx | 56 + .../src/pages/Workflow/Definition/index.tsx | 955 +++++++++--------- .../src/pages/Workflow/Instance/index.tsx | 437 +++++--- frontend/src/pages/Workflow/Instance/types.ts | 29 +- 13 files changed, 1965 insertions(+), 1218 deletions(-) create mode 100644 frontend/src/pages/System/User/components/AssignRolesDialog.tsx create mode 100644 frontend/src/pages/System/User/components/DeleteDialog.tsx create mode 100644 frontend/src/pages/System/User/components/EditModal.tsx create mode 100644 frontend/src/pages/System/User/components/ResetPasswordDialog.tsx delete mode 100644 frontend/src/pages/System/User/components/RoleModal.module.css delete mode 100644 frontend/src/pages/System/User/components/RoleModal.tsx create mode 100644 frontend/src/pages/Workflow/Definition/components/DeleteDialog.tsx create mode 100644 frontend/src/pages/Workflow/Definition/components/DeployDialog.tsx diff --git a/frontend/src/pages/System/User/components/AssignRolesDialog.tsx b/frontend/src/pages/System/User/components/AssignRolesDialog.tsx new file mode 100644 index 00000000..074558f4 --- /dev/null +++ b/frontend/src/pages/System/User/components/AssignRolesDialog.tsx @@ -0,0 +1,110 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Users } from 'lucide-react'; +import type { UserResponse, Role } from '../types'; + +interface AssignRolesDialogProps { + open: boolean; + record: UserResponse | null; + allRoles: Role[]; + onOpenChange: (open: boolean) => void; + onConfirm: (roleIds: number[]) => Promise; +} + +/** + * 分配角色对话框 + */ +const AssignRolesDialog: React.FC = ({ + open, + record, + allRoles, + onOpenChange, + onConfirm, +}) => { + const [selectedRoles, setSelectedRoles] = useState([]); + + useEffect(() => { + if (open && record) { + setSelectedRoles(record.roles?.map(r => r.id) || []); + } + }, [open, record]); + + const handleToggle = (roleId: number) => { + setSelectedRoles(prev => + prev.includes(roleId) + ? prev.filter(id => id !== roleId) + : [...prev, roleId] + ); + }; + + const handleSubmit = async () => { + await onConfirm(selectedRoles); + }; + + if (!record) return null; + + return ( + + + + + 分配角色 + + +
+
+ 为用户 "{record.username}" 分配角色 +
+ +
+ +
+ {allRoles.length > 0 ? ( + allRoles.map(role => ( +
+ handleToggle(role.id)} + /> + +
+ )) + ) : ( +
+ 暂无可用角色 +
+ )} +
+
+
+ + + + +
+
+ ); +}; + +export default AssignRolesDialog; + diff --git a/frontend/src/pages/System/User/components/DeleteDialog.tsx b/frontend/src/pages/System/User/components/DeleteDialog.tsx new file mode 100644 index 00000000..11b30192 --- /dev/null +++ b/frontend/src/pages/System/User/components/DeleteDialog.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { AlertCircle } from 'lucide-react'; +import type { UserResponse } from '../types'; + +interface DeleteDialogProps { + open: boolean; + record: UserResponse | null; + onOpenChange: (open: boolean) => void; + onConfirm: () => Promise; +} + +/** + * 删除确认对话框 + */ +const DeleteDialog: React.FC = ({ + open, + record, + onOpenChange, + onConfirm, +}) => { + if (!record) return null; + + return ( + + + + + 确认删除用户? + + + 您确定要删除用户 "{record.username}" 吗? + 此操作不可逆。 + + +
+ {record.nickname && ( +

+ 昵称: {record.nickname} +

+ )} + {record.email && ( +

+ 邮箱: {record.email} +

+ )} + {record.departmentName && ( +

+ 部门: {record.departmentName} +

+ )} +
+ + + + +
+
+ ); +}; + +export default DeleteDialog; + diff --git a/frontend/src/pages/System/User/components/EditModal.tsx b/frontend/src/pages/System/User/components/EditModal.tsx new file mode 100644 index 00000000..d629c678 --- /dev/null +++ b/frontend/src/pages/System/User/components/EditModal.tsx @@ -0,0 +1,241 @@ +import React, { useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { useToast } from '@/components/ui/use-toast'; +import { createUser, updateUser } from '../service'; +import type { UserResponse, UserRequest } from '../types'; +import type { DepartmentResponse } from '../../Department/types'; + +interface EditModalProps { + open: boolean; + record?: UserResponse; + departments: DepartmentResponse[]; + onOpenChange: (open: boolean) => void; + onSuccess: () => void; +} + +/** + * 用户编辑弹窗 + */ +const EditModal: React.FC = ({ + open, + record, + departments, + onOpenChange, + onSuccess, +}) => { + const { toast } = useToast(); + const [formData, setFormData] = React.useState>({ + enabled: true, + }); + + useEffect(() => { + if (open) { + if (record) { + setFormData({ + username: record.username, + nickname: record.nickname, + email: record.email, + phone: record.phone, + departmentId: record.departmentId, + enabled: record.enabled, + }); + } else { + setFormData({ enabled: true }); + } + } + }, [open, record]); + + const handleSubmit = async () => { + try { + // 验证 + if (!formData.username?.trim()) { + toast({ + title: '提示', + description: '请输入用户名', + variant: 'destructive' + }); + return; + } + + if (!record && !formData.password) { + toast({ + title: '提示', + description: '请输入密码', + variant: 'destructive' + }); + return; + } + + if (!record && formData.password && formData.password.length < 6) { + toast({ + title: '提示', + description: '密码长度不能小于6位', + variant: 'destructive' + }); + return; + } + + if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + toast({ + title: '提示', + description: '请输入正确的邮箱格式', + variant: 'destructive' + }); + return; + } + + if (record) { + await updateUser(record.id, formData as UserRequest); + toast({ + title: '更新成功', + description: `用户 "${formData.username}" 已更新`, + }); + } else { + await createUser(formData as UserRequest); + toast({ + title: '创建成功', + description: `用户 "${formData.username}" 已创建`, + }); + } + + onSuccess(); + onOpenChange(false); + } catch (error) { + console.error('保存失败:', error); + toast({ + title: '保存失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + }; + + // 扁平化部门列表 + const flattenDepartments = (depts: DepartmentResponse[]): DepartmentResponse[] => { + const result: DepartmentResponse[] = []; + const traverse = (dept: DepartmentResponse, level: number = 0) => { + result.push({ ...dept, name: ' '.repeat(level) + dept.name }); + if (dept.children) { + dept.children.forEach(child => traverse(child, level + 1)); + } + }; + depts.forEach(dept => traverse(dept)); + return result; + }; + + const flatDepartments = flattenDepartments(departments); + + return ( + + + + {record ? '编辑用户' : '新增用户'} + +
+
+ + setFormData({ ...formData, username: e.target.value })} + placeholder="请输入用户名" + disabled={!!record} + /> +
+ + {!record && ( +
+ + setFormData({ ...formData, password: e.target.value })} + placeholder="请输入密码(至少6位)" + /> +
+ )} + +
+ + setFormData({ ...formData, nickname: e.target.value })} + placeholder="请输入昵称" + /> +
+ +
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="请输入邮箱" + /> +
+ +
+ + setFormData({ ...formData, phone: e.target.value })} + placeholder="请输入手机号" + /> +
+ +
+ + +
+ +
+ + setFormData({ ...formData, enabled: checked })} + /> +
+
+ + + + +
+
+ ); +}; + +export default EditModal; + diff --git a/frontend/src/pages/System/User/components/ResetPasswordDialog.tsx b/frontend/src/pages/System/User/components/ResetPasswordDialog.tsx new file mode 100644 index 00000000..a4973a36 --- /dev/null +++ b/frontend/src/pages/System/User/components/ResetPasswordDialog.tsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { KeyRound } from 'lucide-react'; +import type { UserResponse } from '../types'; + +interface ResetPasswordDialogProps { + open: boolean; + record: UserResponse | null; + onOpenChange: (open: boolean) => void; + onConfirm: (password: string) => Promise; +} + +/** + * 重置密码对话框 + */ +const ResetPasswordDialog: React.FC = ({ + open, + record, + onOpenChange, + onConfirm, +}) => { + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + + useEffect(() => { + if (open) { + setPassword(''); + setConfirmPassword(''); + setError(''); + } + }, [open]); + + const handleSubmit = async () => { + // 验证 + if (!password) { + setError('请输入新密码'); + return; + } + if (password.length < 6) { + setError('密码长度不能小于6位'); + return; + } + if (password !== confirmPassword) { + setError('两次输入的密码不一致'); + return; + } + + await onConfirm(password); + }; + + if (!record) return null; + + return ( + + + + + 重置密码 + + +
+
+ 为用户 "{record.username}" 重置密码 +
+ +
+ + { + setPassword(e.target.value); + setError(''); + }} + placeholder="请输入新密码(至少6位)" + /> +
+ +
+ + { + setConfirmPassword(e.target.value); + setError(''); + }} + placeholder="请再次输入新密码" + /> +
+ + {error && ( +
{error}
+ )} +
+ + + + +
+
+ ); +}; + +export default ResetPasswordDialog; + diff --git a/frontend/src/pages/System/User/components/RoleModal.module.css b/frontend/src/pages/System/User/components/RoleModal.module.css deleted file mode 100644 index 37e86487..00000000 --- a/frontend/src/pages/System/User/components/RoleModal.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.role-modal { - min-width: 520px; -} - -.role-list { - max-height: 400px; - overflow-y: auto; -} - -.role-item { - display: flex; - align-items: center; - padding: 8px 0; - border-bottom: 1px solid #f0f0f0; -} - -.role-item:last-child { - border-bottom: none; -} - -.role-info { - flex: 1; - margin-left: 8px; -} - -.role-name { - font-weight: 500; - margin-bottom: 4px; -} - -.role-description { - color: #666; - font-size: 12px; -} \ No newline at end of file diff --git a/frontend/src/pages/System/User/components/RoleModal.tsx b/frontend/src/pages/System/User/components/RoleModal.tsx deleted file mode 100644 index add307e0..00000000 --- a/frontend/src/pages/System/User/components/RoleModal.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Modal, Transfer, message, Spin } from 'antd'; -import type { TransferDirection, TransferProps } from 'antd/es/transfer'; -import { getRoles } from '../../Role/service'; -import { getUserRoleIds, updateUserRoles } from '../service'; -import type { RoleDTO } from '../../Role/types'; - -interface RoleModalProps { - userId: number; - visible: boolean; - onCancel: () => void; - onSuccess: () => void; -} - -interface RoleTransferItem { - key: string; - title: string; - description: string; - disabled?: boolean; -} - -const RoleModal: React.FC = ({ - userId, - visible, - onCancel, - onSuccess -}) => { - const [roles, setRoles] = useState([]); - const [selectedKeys, setSelectedKeys] = useState([]); - const [loading, setLoading] = useState(false); - - const fetchData = async () => { - try { - setLoading(true); - const [allRoles, userRoleIds] = await Promise.all([ - getRoles(), - getUserRoleIds(userId) - ]); - setRoles(allRoles); - setSelectedKeys(userRoleIds.map(String)); - } catch (error) { - message.error('获取角色数据失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (visible) { - fetchData(); - } - }, [visible, userId]); - - const handleChange: TransferProps['onChange'] = (nextTargetKeys) => { - setSelectedKeys(nextTargetKeys as string[]); - }; - - const handleOk = async () => { - try { - setLoading(true); - await updateUserRoles(userId, selectedKeys.map(Number)); - message.success('角色分配成功'); - onSuccess(); - } catch (error) { - message.error('角色分配失败'); - } finally { - setLoading(false); - } - }; - - return ( - - - - dataSource={roles.map(role => ({ - key: role.id.toString(), - title: role.name, - description: role.code, - disabled: role.code === 'ROLE_ADMIN' - }))} - titles={['未选择', '已选择']} - targetKeys={selectedKeys} - onChange={handleChange} - render={item => item.title} - listStyle={{ - width: 250, - height: 400, - }} - showSearch - filterOption={(inputValue, item) => - (item.title?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1) || - (item.description?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1) - } - /> - - - ); -}; - -export default RoleModal; \ No newline at end of file diff --git a/frontend/src/pages/System/User/index.tsx b/frontend/src/pages/System/User/index.tsx index e9e67360..a40dfcb9 100644 --- a/frontend/src/pages/System/User/index.tsx +++ b/frontend/src/pages/System/User/index.tsx @@ -1,468 +1,483 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Modal, Form, Input, Space, message, Switch, TreeSelect, Select, Tag, Dropdown } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined, MoreOutlined } from '@ant-design/icons'; -import type { MenuProps } from 'antd'; -import { useTableData } from '@/hooks/useTableData'; -import * as service from './service'; -import type { UserResponse, UserRequest, UserQuery, Role } from './types'; -import type { DepartmentResponse } from '../Department/types'; -import { getDepartmentTree } from '../Department/service'; +import React, { useState, useEffect, useMemo } from 'react'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { DataTablePagination } from '@/components/ui/pagination'; import { - Table, - TableHeader, - TableBody, - TableHead, - TableRow, - TableCell, -} from "@/components/ui/table"; - -interface Column { - accessorKey?: keyof UserResponse; - id?: string; - header: string; - size: number; - cell?: (props: { row: { original: UserResponse } }) => React.ReactNode; -} - -interface TreeNode { - title: string; - value: number; - children?: TreeNode[]; -} + Loader2, Plus, Search, Edit, Trash2, KeyRound, Users as UsersIcon, + UserCheck, UserX, Activity +} from 'lucide-react'; +import { useToast } from '@/components/ui/use-toast'; +import { getUsers, deleteUser, resetPassword, assignRoles, getAllRoles } from './service'; +import { getDepartmentTree } from '../Department/service'; +import type { UserResponse, UserQuery, Role } from './types'; +import type { DepartmentResponse } from '../Department/types'; +import type { Page } from '@/types/base'; +import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; +import EditModal from './components/EditModal'; +import ResetPasswordDialog from './components/ResetPasswordDialog'; +import AssignRolesDialog from './components/AssignRolesDialog'; +import DeleteDialog from './components/DeleteDialog'; +import dayjs from 'dayjs'; +/** + * 用户管理页面 + */ const UserPage: React.FC = () => { - const [form] = Form.useForm(); - const [passwordForm] = Form.useForm(); - const [modalVisible, setModalVisible] = useState(false); - const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false); - const [editingUser, setEditingUser] = useState(null); - const [departments, setDepartments] = useState([]); - const [roleModalVisible, setRoleModalVisible] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); - const [selectedRoles, setSelectedRoles] = useState([]); - const [allRoles, setAllRoles] = useState([]); + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [departments, setDepartments] = useState([]); + const [allRoles, setAllRoles] = useState([]); + const [editModalOpen, setEditModalOpen] = useState(false); + const [editRecord, setEditRecord] = useState(); + const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false); + const [resetPasswordRecord, setResetPasswordRecord] = useState(null); + const [assignRolesDialogOpen, setAssignRolesDialogOpen] = useState(false); + const [assignRolesRecord, setAssignRolesRecord] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteRecord, setDeleteRecord] = useState(null); + const [query, setQuery] = useState({ + pageNum: DEFAULT_CURRENT - 1, + pageSize: DEFAULT_PAGE_SIZE, + username: '', + email: '', + enabled: undefined, + }); - const { - list, - loading, - pagination, - handleTableChange, - handleCreate, - handleUpdate, - handleDelete, - refresh - } = useTableData({ - service: { - list: service.getUsers, - create: service.createUser, - update: service.updateUser, - delete: service.deleteUser - }, - defaultParams: { - sortField: 'createTime', - sortOrder: 'desc' - }, - config: { - message: { - createSuccess: '创建用户成功', - updateSuccess: '更新用户成功', - deleteSuccess: '删除用户成功' - } - } + // 加载数据 + const loadData = async () => { + setLoading(true); + try { + const result = await getUsers(query); + setData(result); + } catch (error) { + console.error('加载用户列表失败:', error); + } finally { + setLoading(false); + } + }; + + // 加载部门和角色 + const loadDepartmentsAndRoles = async () => { + try { + const [deptData, roleData] = await Promise.all([ + getDepartmentTree(), + getAllRoles() + ]); + setDepartments(deptData); + setAllRoles(roleData); + } catch (error) { + console.error('加载部门和角色失败:', error); + } + }; + + useEffect(() => { + loadDepartmentsAndRoles(); + }, []); + + useEffect(() => { + loadData(); + }, [query]); + + // 搜索 + const handleSearch = () => { + setQuery(prev => ({ ...prev, pageNum: 0 })); + }; + + // 重置 + const handleReset = () => { + setQuery({ + pageNum: 0, + pageSize: DEFAULT_PAGE_SIZE, + username: '', + email: '', + enabled: undefined, }); + }; - useEffect(() => { - service.getAllRoles().then(roles => setAllRoles(roles)); - loadDepartmentTree(); - }, []); + // 新建 + const handleCreate = () => { + setEditRecord(undefined); + setEditModalOpen(true); + }; - const loadDepartmentTree = async () => { - try { - const data = await getDepartmentTree(); - setDepartments(data); - } catch (error) { - message.error('加载部门数据失败'); - } - }; + // 编辑 + const handleEdit = (record: UserResponse) => { + setEditRecord(record); + setEditModalOpen(true); + }; - const getTreeData = (departments: DepartmentResponse[]): TreeNode[] => { - return departments.map(dept => ({ - title: dept.name, - value: dept.id, - children: dept.children ? getTreeData(dept.children) : undefined - })); - }; + // 重置密码 + const handleResetPassword = (record: UserResponse) => { + setResetPasswordRecord(record); + setResetPasswordDialogOpen(true); + }; - const handleAdd = () => { - setEditingUser(null); - form.resetFields(); - form.setFieldsValue({ - enabled: true - }); - setModalVisible(true); - }; + const confirmResetPassword = async (password: string) => { + if (!resetPasswordRecord) return; + try { + await resetPassword(resetPasswordRecord.id, password); + toast({ + title: '重置成功', + description: `用户 "${resetPasswordRecord.username}" 的密码已重置`, + }); + setResetPasswordDialogOpen(false); + setResetPasswordRecord(null); + } catch (error) { + console.error('重置密码失败:', error); + toast({ + title: '重置失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + }; - const handleEdit = (record: UserResponse) => { - setEditingUser(record); - form.setFieldsValue({ - ...record, - password: undefined // 不显示密码 - }); - setModalVisible(true); - }; + // 分配角色 + const handleAssignRoles = (record: UserResponse) => { + setAssignRolesRecord(record); + setAssignRolesDialogOpen(true); + }; - const handleResetPassword = (record: UserResponse) => { - setEditingUser(record); - passwordForm.resetFields(); - setResetPasswordModalVisible(true); - }; + const confirmAssignRoles = async (roleIds: number[]) => { + if (!assignRolesRecord) return; + try { + await assignRoles(assignRolesRecord.id, roleIds); + toast({ + title: '分配成功', + description: `已为用户 "${assignRolesRecord.username}" 分配角色`, + }); + loadData(); + setAssignRolesDialogOpen(false); + setAssignRolesRecord(null); + } catch (error) { + console.error('分配角色失败:', error); + toast({ + title: '分配失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + }; - const handleResetPasswordSubmit = async () => { - try { - const values = await passwordForm.validateFields(); - if (editingUser) { - await service.resetPassword(editingUser.id, values.password); - message.success('密码重置成功'); - setResetPasswordModalVisible(false); - refresh(); - } - } catch (error: any) { - message.error(error.message || '密码重置失败'); - } - }; + // 删除 + const handleDelete = (record: UserResponse) => { + setDeleteRecord(record); + setDeleteDialogOpen(true); + }; - const handleSubmit = async () => { - try { - const values = await form.validateFields(); - if (editingUser) { - await handleUpdate(editingUser.id, values); - } else { - await handleCreate(values); - } - setModalVisible(false); - refresh(); - } catch (error: any) { - message.error(error.message || '操作失败'); - } - }; + const confirmDelete = async () => { + if (!deleteRecord) return; + try { + await deleteUser(deleteRecord.id); + toast({ + title: '删除成功', + description: `用户 "${deleteRecord.username}" 已删除`, + }); + loadData(); + setDeleteDialogOpen(false); + setDeleteRecord(null); + } catch (error) { + console.error('删除失败:', error); + toast({ + title: '删除失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + }; - const handleAssignRoles = (record: UserResponse) => { - setSelectedUser(record); - setSelectedRoles(record.roles?.map(role => role.id) || []); - setRoleModalVisible(true); - }; - - const handleAssignRoleSubmit = async () => { - if (selectedUser) { - try { - await service.assignRoles(selectedUser.id, selectedRoles); - message.success('角色分配成功'); - setRoleModalVisible(false); - refresh(); - } catch (error) { - message.error('角色分配失败'); - } - } - }; - - const columns: Column[] = [ - { - accessorKey: 'id', - header: 'ID', - size: 60, - }, - { - accessorKey: 'username', - header: '用户名', - size: 100, - }, - { - accessorKey: 'nickname', - header: '昵称', - size: 100, - }, - { - accessorKey: 'email', - header: '邮箱', - size: 200, - }, - { - accessorKey: 'departmentName', - header: '部门', - size: 150, - }, - { - accessorKey: 'enabled', - header: '状态', - size: 100, - cell: ({ row }) => ( - - {row.original.enabled ? '启用' : '禁用'} - - ), - }, - { - accessorKey: 'phone', - header: '手机号', - size: 120, - }, - { - header: '角色', - size: 120, - cell: ({ row }) => row.original.roles?.map(role => role.name).join(', ') || '-', - }, - { - accessorKey: 'createTime', - header: '创建时间', - size: 150, - }, - { - accessorKey: 'updateTime', - header: '更新时间', - size: 150, - }, - { - id: 'actions', - header: '操作', - size: 180, - cell: ({ row }) => { - const record = row.original; - const items: MenuProps['items'] = [ - { - key: 'resetPassword', - icon: , - label: '重置密码', - onClick: () => handleResetPassword(record) - }, - { - key: 'assignRoles', - icon: , - label: '分配角色', - onClick: () => handleAssignRoles(record), - disabled: record.username === 'admin' - } - ]; - - if (record.username !== 'admin') { - items.push({ - key: 'delete', - icon: , - label: '删除', - danger: true, - onClick: () => handleDelete(record.id) - }); - } - - return ( - - - - - - -
- - - - {columns.map((column) => ( - - {column.header} - - ))} - - - - {list.map((row) => ( - - {columns.map((column) => ( - - {column.cell - ? column.cell({ row: { original: row } }) - : column.accessorKey - ? String(row[column.accessorKey]) - : null} - - ))} - - ))} - -
-
- - setModalVisible(false)} - width={600} - > -
- - - - - {!editingUser && ( - - - - )} - - - - - - - - - - - - - - - - - - - - -
-
- - setResetPasswordModalVisible(false)} - > -
- - - - - ({ - validator(_, value) { - if (!value || getFieldValue('password') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('两次输入的密码不一致')); - }, - }), - ]} - > - - -
-
- - setRoleModalVisible(false)} - width={600} - > -
- - - -
-
- + // 状态徽章 + const getStatusBadge = (enabled: boolean) => { + return enabled ? ( + + + 启用 + + ) : ( + + + 禁用 + ); + }; + + // 统计数据 + const stats = useMemo(() => { + const total = data?.totalElements || 0; + const enabledCount = data?.content?.filter(d => d.enabled).length || 0; + const disabledCount = data?.content?.filter(d => !d.enabled).length || 0; + return { total, enabledCount, disabledCount }; + }, [data]); + + const pageCount = data?.totalElements ? Math.ceil(data.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0; + + return ( +
+
+

用户管理

+

+ 管理系统用户账号,设置权限和角色。 +

+
+ + {/* 统计卡片 */} +
+ + + 总用户数 + + + +
{stats.total}
+

所有系统用户

+
+
+ + + 启用用户 + + + +
{stats.enabledCount}
+

可正常登录的用户

+
+
+ + + 禁用用户 + + + +
{stats.disabledCount}
+

已停用的用户

+
+
+
+ + + + 用户列表 + + + + {/* 搜索栏 */} +
+
+ setQuery(prev => ({ ...prev, username: e.target.value }))} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="h-9" + /> +
+
+ setQuery(prev => ({ ...prev, email: e.target.value }))} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="h-9" + /> +
+ + + +
+ + {/* 表格 */} +
+ + + + 用户名 + 昵称 + 邮箱 + 手机号 + 部门 + 角色 + 状态 + 创建时间 + 操作 + + + + {loading ? ( + + +
+ + 加载中... +
+
+
+ ) : data?.content && data.content.length > 0 ? ( + data.content.map((record) => { + const isAdmin = record.username === 'admin'; + return ( + + + {record.username} + + {record.nickname || '-'} + {record.email || '-'} + {record.phone || '-'} + {record.departmentName || '-'} + +
+ {record.roles && record.roles.length > 0 ? ( + record.roles.map(role => ( + + {role.name} + + )) + ) : ( + - + )} +
+
+ {getStatusBadge(record.enabled)} + + {record.createTime ? dayjs(record.createTime).format('YYYY-MM-DD HH:mm') : '-'} + + +
+ + + + {!isAdmin && ( + + )} +
+
+
+ ); + }) + ) : ( + + +
+ +
暂无用户数据
+
点击右上角"新增用户"开始创建用户。
+
+
+
+ )} +
+
+
+ + {/* 分页 */} + {pageCount > 1 && ( + setQuery(prev => ({ + ...prev, + pageNum: page - 1 + }))} + /> + )} +
+
+ + {/* 编辑弹窗 */} + + + {/* 重置密码对话框 */} + + + {/* 分配角色对话框 */} + + + {/* 删除确认对话框 */} + +
+ ); }; -export default UserPage; \ No newline at end of file +export default UserPage; diff --git a/frontend/src/pages/System/User/service.ts b/frontend/src/pages/System/User/service.ts index af91596d..49cd7287 100644 --- a/frontend/src/pages/System/User/service.ts +++ b/frontend/src/pages/System/User/service.ts @@ -1,7 +1,6 @@ import request from '@/utils/request'; -import type { UserResponse, UserRequest, UserQuery } from './types'; -import {Page} from "@/types/base.ts"; -import {RoleResponse} from "@/pages/System/Role/types"; +import type { UserResponse, UserRequest, UserQuery, Role } from './types'; +import { Page } from '@/types/base'; const BASE_URL = '/api/v1/user'; const ROLE_BASE_URL = '/api/v1/role'; @@ -32,4 +31,4 @@ export const assignRoles = (userId: number, roleIds: number[]) => // 获取所有角色列表(不分页) export const getAllRoles = () => - request.get(`${ROLE_BASE_URL}/list`); + request.get(`${ROLE_BASE_URL}/list`); diff --git a/frontend/src/pages/Workflow/Definition/components/DeleteDialog.tsx b/frontend/src/pages/Workflow/Definition/components/DeleteDialog.tsx new file mode 100644 index 00000000..71de457e --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/components/DeleteDialog.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { AlertCircle, Clock, CheckCircle2 } from 'lucide-react'; +import type { WorkflowDefinition } from '../types'; + +interface DeleteDialogProps { + open: boolean; + record: WorkflowDefinition | null; + onOpenChange: (open: boolean) => void; + onConfirm: () => Promise; +} + +/** + * 删除确认对话框 + */ +const DeleteDialog: React.FC = ({ + open, + record, + onOpenChange, + onConfirm, +}) => { + if (!record) return null; + + // 状态徽章 + const getStatusBadge = (status: string) => { + const statusMap: Record = { + DRAFT: { variant: 'outline', text: '草稿', icon: Clock }, + PUBLISHED: { variant: 'success', text: '已发布', icon: CheckCircle2 }, + }; + const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock }; + const Icon = statusInfo.icon; + return ( + + + {statusInfo.text} + + ); + }; + + return ( + + + + + 确认删除工作流? + + + 您确定要删除工作流 "{record.name}" 吗? + 此操作不可逆。 + + +
+

+ 标识:{' '} + + {record.key} + +

+

+ 版本:{' '} + {record.flowVersion || 1} +

+

+ 状态: {getStatusBadge(record.status || 'DRAFT')} +

+
+ + + + +
+
+ ); +}; + +export default DeleteDialog; + diff --git a/frontend/src/pages/Workflow/Definition/components/DeployDialog.tsx b/frontend/src/pages/Workflow/Definition/components/DeployDialog.tsx new file mode 100644 index 00000000..406f6be9 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/components/DeployDialog.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { CheckCircle2 } from 'lucide-react'; +import type { WorkflowDefinition } from '../types'; + +interface DeployDialogProps { + open: boolean; + record: WorkflowDefinition | null; + onOpenChange: (open: boolean) => void; + onConfirm: () => Promise; +} + +/** + * 发布确认对话框 + */ +const DeployDialog: React.FC = ({ + open, + record, + onOpenChange, + onConfirm, +}) => { + if (!record) return null; + + return ( + + + + + 确认发布工作流? + + + 您确定要发布工作流 "{record.name}" 吗? + 发布后将可以启动执行。 + + + + + + + + + ); +}; + +export default DeployDialog; + diff --git a/frontend/src/pages/Workflow/Definition/index.tsx b/frontend/src/pages/Workflow/Definition/index.tsx index 61e9191a..d7f7556e 100644 --- a/frontend/src/pages/Workflow/Definition/index.tsx +++ b/frontend/src/pages/Workflow/Definition/index.tsx @@ -6,513 +6,482 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'; import { DataTablePagination } from '@/components/ui/pagination'; import { Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2, - Clock, Activity, Workflow, MoreHorizontal, AlertCircle, Eye + Clock, Activity, Workflow, Eye, Pencil } from 'lucide-react'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { useToast } from '@/components/ui/use-toast'; -import * as service from './service'; +import { getDefinitions, getWorkflowCategoryList, deleteDefinition, publishDefinition, startWorkflowInstance } from './service'; import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategoryResponse } from './types'; +import type { Page } from '@/types/base'; import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; import EditModal from './components/EditModal'; +import DeleteDialog from './components/DeleteDialog'; +import DeployDialog from './components/DeployDialog'; +/** + * 工作流定义列表页 + */ const WorkflowDefinitionList: React.FC = () => { - const navigate = useNavigate(); - const { toast } = useToast(); - const [loading, setLoading] = useState(false); - const [pageData, setPageData] = useState<{ - content: WorkflowDefinition[]; - totalElements: number; - size: number; - number: number; - } | null>(null); - const [modalVisible, setModalVisible] = useState(false); - const [currentRecord, setCurrentRecord] = useState(); - const [categories, setCategories] = useState([]); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [deleteRecord, setDeleteRecord] = useState(null); - const [deployDialogOpen, setDeployDialogOpen] = useState(false); - const [deployRecord, setDeployRecord] = useState(null); - const [query, setQuery] = useState({ - pageNum: DEFAULT_CURRENT - 1, - pageSize: DEFAULT_PAGE_SIZE, - name: '', - categoryId: undefined, - status: undefined + const navigate = useNavigate(); + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [categories, setCategories] = useState([]); + const [editModalVisible, setEditModalVisible] = useState(false); + const [editRecord, setEditRecord] = useState(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteRecord, setDeleteRecord] = useState(null); + const [deployDialogOpen, setDeployDialogOpen] = useState(false); + const [deployRecord, setDeployRecord] = useState(null); + const [query, setQuery] = useState({ + pageNum: DEFAULT_CURRENT - 1, + pageSize: DEFAULT_PAGE_SIZE, + name: '', + categoryId: undefined, + status: undefined + }); + + // 加载数据 + const loadData = async () => { + setLoading(true); + try { + const result = await getDefinitions(query); + setData(result); + } catch (error) { + console.error('加载工作流定义失败:', error); + } finally { + setLoading(false); + } + }; + + // 加载分类 + const loadCategories = async () => { + try { + const result = await getWorkflowCategoryList(); + setCategories(result || []); + } catch (error) { + console.error('加载分类失败:', error); + } + }; + + useEffect(() => { + loadCategories(); + }, []); + + useEffect(() => { + loadData(); + }, [query]); + + // 搜索 + const handleSearch = () => { + setQuery(prev => ({ ...prev, pageNum: 0 })); + }; + + // 重置 + const handleReset = () => { + setQuery({ + pageNum: 0, + pageSize: DEFAULT_PAGE_SIZE, + name: '', + categoryId: undefined, + status: undefined }); + }; - const loadData = async (params: WorkflowDefinitionQuery) => { - setLoading(true); - try { - const data = await service.getDefinitions(params); - setPageData(data); - } catch (error) { - if (error instanceof Error) { - toast({ - title: '加载失败', - description: error.message, - variant: 'destructive' - }); - } - } finally { - setLoading(false); - } + // 新建 + const handleCreate = () => { + setEditRecord(undefined); + setEditModalVisible(true); + }; + + // 编辑 + const handleEdit = (record: WorkflowDefinition) => { + setEditRecord(record); + setEditModalVisible(true); + }; + + // 设计 + const handleDesign = (record: WorkflowDefinition) => { + navigate(`/workflow/design/${record.id}`); + }; + + // 发布 + const handleDeploy = (record: WorkflowDefinition) => { + setDeployRecord(record); + setDeployDialogOpen(true); + }; + + const confirmDeploy = async () => { + if (!deployRecord) return; + try { + await publishDefinition(deployRecord.id); + toast({ + title: '发布成功', + description: `工作流 "${deployRecord.name}" 已发布`, + }); + loadData(); + setDeployDialogOpen(false); + setDeployRecord(null); + } catch (error) { + console.error('发布失败:', error); + toast({ + title: '发布失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + }; + + // 删除 + const handleDelete = (record: WorkflowDefinition) => { + setDeleteRecord(record); + setDeleteDialogOpen(true); + }; + + const confirmDelete = async () => { + if (!deleteRecord) return; + try { + await deleteDefinition(deleteRecord.id); + toast({ + title: '删除成功', + description: `工作流 "${deleteRecord.name}" 已删除`, + }); + loadData(); + setDeleteDialogOpen(false); + setDeleteRecord(null); + } catch (error) { + console.error('删除失败:', error); + toast({ + title: '删除失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + }; + + // 启动 + const handleStart = async (record: WorkflowDefinition) => { + try { + await startWorkflowInstance(record.key, record.categoryId); + toast({ + title: '启动成功', + description: `工作流 "${record.name}" 已启动`, + }); + } catch (error) { + console.error('启动失败:', error); + toast({ + title: '启动失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive' + }); + } + }; + + // 状态徽章 + const getStatusBadge = (status: string) => { + const statusMap: Record = { + DRAFT: { variant: 'outline', text: '草稿', icon: Clock }, + PUBLISHED: { variant: 'success', text: '已发布', icon: CheckCircle2 }, + DISABLED: { variant: 'secondary', text: '已停用', icon: Clock }, }; - - const loadCategories = async () => { - try { - const data = await service.getWorkflowCategoryList(); - setCategories(data); - } catch (error) { - console.error('加载工作流分类失败:', error); - } - }; - - useEffect(() => { - loadCategories(); - }, []); - - useEffect(() => { - loadData(query); - }, [query]); - - const handleCreateFlow = () => { - setCurrentRecord(undefined); - setModalVisible(true); - }; - - const handleEditFlow = (record: WorkflowDefinition) => { - setCurrentRecord(record); - setModalVisible(true); - }; - - const handleDesignFlow = (record: WorkflowDefinition) => { - navigate(`/workflow/design/${record.id}`); - }; - - const handleModalClose = () => { - setModalVisible(false); - setCurrentRecord(undefined); - }; - - const handleSearch = () => { - setQuery(prev => ({ - ...prev, - pageNum: 0, - })); - }; - - const handleReset = () => { - setQuery({ - pageNum: 0, - pageSize: DEFAULT_PAGE_SIZE, - name: '', - categoryId: undefined, - status: undefined - }); - }; - - const handleDeploy = (record: WorkflowDefinition) => { - setDeployRecord(record); - setDeployDialogOpen(true); - }; - - const confirmDeploy = async () => { - if (!deployRecord) return; - try { - await service.publishDefinition(deployRecord.id); - toast({ - title: '发布成功', - description: `工作流 "${deployRecord.name}" 已发布`, - }); - loadData(query); - setDeployDialogOpen(false); - } catch (error) { - if (error instanceof Error) { - toast({ - title: '发布失败', - description: error.message, - variant: 'destructive' - }); - } - } - }; - - const handleDelete = (record: WorkflowDefinition) => { - setDeleteRecord(record); - setDeleteDialogOpen(true); - }; - - const confirmDelete = async () => { - if (!deleteRecord) return; - try { - await service.deleteDefinition(deleteRecord.id); - toast({ - title: '删除成功', - description: `工作流 "${deleteRecord.name}" 已删除`, - }); - loadData(query); - setDeleteDialogOpen(false); - } catch (error) { - if (error instanceof Error) { - toast({ - title: '删除失败', - description: error.message, - variant: 'destructive' - }); - } - } - }; - - const handleStartFlow = async (record: WorkflowDefinition) => { - try { - await service.startWorkflowInstance(record.key, record.categoryId); - toast({ - title: '启动成功', - description: `工作流 "${record.name}" 已启动`, - }); - } catch (error) { - if (error instanceof Error) { - toast({ - title: '启动失败', - description: error.message, - variant: 'destructive' - }); - } - } - }; - - // 状态徽章 - const getStatusBadge = (status: string) => { - const statusMap: Record = { - DRAFT: { variant: 'outline', text: '草稿', icon: Clock }, - PUBLISHED: { variant: 'success', text: '已发布', icon: CheckCircle2 }, - }; - const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock }; - const Icon = statusInfo.icon; - return ( - - - {statusInfo.text} - - ); - }; - - // 统计数据 - const stats = useMemo(() => { - const total = pageData?.totalElements || 0; - const draftCount = pageData?.content?.filter(d => d.status === 'DRAFT').length || 0; - const publishedCount = pageData?.content?.filter(d => d.status !== 'DRAFT').length || 0; - return { total, draftCount, publishedCount }; - }, [pageData]); - - const pageCount = pageData?.totalElements ? Math.ceil(pageData.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0; - + const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock }; + const Icon = statusInfo.icon; return ( -
-
-

工作流定义管理

-
- - {/* 统计卡片 */} -
- - - 总工作流 - - - -
{stats.total}
-

全部工作流定义

-
-
- - - 草稿 - - - -
{stats.draftCount}
-

待发布的工作流

-
-
- - - 已发布 - - - -
{stats.publishedCount}
-

正在使用的工作流

-
-
-
- - - - 工作流列表 - - - - {/* 搜索栏 */} -
-
- setQuery(prev => ({ ...prev, name: e.target.value }))} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - className="h-9" - /> -
- - - - -
- - {/* 表格 */} -
- - - - 流程名称 - 流程标识 - 流程分类 - 版本 - 状态 - 描述 - 操作 - - - - {loading ? ( - - -
- - 加载中... -
-
-
- ) : pageData?.content && pageData.content.length > 0 ? ( - pageData.content.map((record) => { - const categoryInfo = categories.find(c => c.id === record.categoryId); - return ( - - {record.name} - - - {record.key} - - - - {categoryInfo ? ( - - {categoryInfo.name} - - ) : ( - 未分类 - )} - - - {record.flowVersion || 1} - - {getStatusBadge(record.status || 'DRAFT')} - - {record.description || '-'} - - - - - - - - {record.status === 'DRAFT' && ( - <> - handleEditFlow(record)}> - 编辑 - - handleDesignFlow(record)}> - 设计 - - handleDeploy(record)}> - 发布 - - - )} - {record.status !== 'DRAFT' && ( - <> - handleStartFlow(record)}> - 启动 - - handleDesignFlow(record)}> - 查看 - - - )} - - handleDelete(record)} - className="text-red-600 focus:text-red-600" - > - 删除 - - - - - - ); - }) - ) : ( - - -
- -
暂无工作流定义
-
点击右上角"新建工作流"开始设计您的第一个工作流。
-
-
-
- )} -
-
-
- - {/* 分页 */} - {pageCount > 1 && ( - setQuery(prev => ({ - ...prev, - pageNum: page - 1 - }))} - /> - )} -
-
- - {/* EditModal */} - loadData(query)} - record={currentRecord} - /> - - {/* 发布确认对话框 */} - {deployRecord && ( - - - - - 确认发布工作流? - - - 您确定要发布工作流 "{deployRecord.name}" 吗? - 发布后将可以启动执行。 - - - - - - - - - )} - - {/* 删除确认对话框 */} - {deleteRecord && ( - - - - - 确认删除工作流? - - - 您确定要删除工作流 "{deleteRecord.name}" 吗? - 此操作不可逆。 - - -
-

- 标识: {deleteRecord.key} -

-

- 版本: {deleteRecord.flowVersion || 1} -

-

- 状态: {getStatusBadge(deleteRecord.status || 'DRAFT')} -

-
- - - - -
-
- )} -
+ + + {statusInfo.text} + ); + }; + + // 统计数据 + const stats = useMemo(() => { + const total = data?.totalElements || 0; + const draftCount = data?.content?.filter(d => d.status === 'DRAFT').length || 0; + const publishedCount = data?.content?.filter(d => d.status === 'PUBLISHED').length || 0; + return { total, draftCount, publishedCount }; + }, [data]); + + const pageCount = data?.totalElements ? Math.ceil(data.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0; + + return ( +
+
+

工作流定义管理

+

+ 创建和管理工作流定义,设计流程图并发布上线。 +

+
+ + {/* 统计卡片 */} +
+ + + 总工作流 + + + +
{stats.total}
+

全部工作流定义

+
+
+ + + 草稿 + + + +
{stats.draftCount}
+

待发布的工作流

+
+
+ + + 已发布 + + + +
{stats.publishedCount}
+

正在使用的工作流

+
+
+
+ + + + 工作流列表 + + + + {/* 搜索栏 */} +
+
+ setQuery(prev => ({ ...prev, name: e.target.value }))} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="h-9" + /> +
+ + + + +
+ + {/* 表格 */} +
+ + + + 流程名称 + 流程标识 + 分类 + 版本 + 状态 + 描述 + 操作 + + + + {loading ? ( + + +
+ + 加载中... +
+
+
+ ) : data?.content && data.content.length > 0 ? ( + data.content.map((record) => { + const categoryInfo = categories.find(c => c.id === record.categoryId); + const isDraft = record.status === 'DRAFT'; + return ( + + {record.name} + + + {record.key} + + + + {categoryInfo ? ( + {categoryInfo.name} + ) : ( + 未分类 + )} + + + {record.flowVersion || 1} + + {getStatusBadge(record.status || 'DRAFT')} + + {record.description || '-'} + + +
+ {isDraft ? ( + <> + + + + + ) : ( + <> + + + + )} + +
+
+
+ ); + }) + ) : ( + + +
+ +
暂无工作流定义
+
点击右上角"新建工作流"开始设计您的第一个工作流。
+
+
+
+ )} +
+
+
+ + {/* 分页 */} + {pageCount > 1 && ( + setQuery(prev => ({ + ...prev, + pageNum: page - 1 + }))} + /> + )} +
+
+ + {/* 编辑弹窗 */} + { + setEditModalVisible(false); + setEditRecord(undefined); + }} + onSuccess={loadData} + record={editRecord} + /> + + {/* 发布确认对话框 */} + + + {/* 删除确认对话框 */} + +
+ ); }; -export default WorkflowDefinitionList; \ No newline at end of file +export default WorkflowDefinitionList; diff --git a/frontend/src/pages/Workflow/Instance/index.tsx b/frontend/src/pages/Workflow/Instance/index.tsx index f5ae33ae..fc647a7e 100644 --- a/frontend/src/pages/Workflow/Instance/index.tsx +++ b/frontend/src/pages/Workflow/Instance/index.tsx @@ -1,137 +1,322 @@ -import React, { useState, useEffect } from 'react'; -import { Card, Table, Tag, Space, Empty } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; +import React, { useState, useEffect, useMemo } from 'react'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { DataTablePagination } from '@/components/ui/pagination'; +import { + Loader2, Search, Eye, StopCircle, Activity, PlayCircle, + CheckCircle2, Clock, Workflow, XCircle, Pause +} from 'lucide-react'; import { getWorkflowInstances } from './service'; -import { WorkflowTemplateWithInstances } from './types'; -import { Page } from '@/types/base'; -// DetailModal 暂时移除,因为数据结构不匹配 (需要 WorkflowHistoricalInstance 而不是 WorkflowTemplateWithInstances) +import type { WorkflowTemplateWithInstances } from './types'; +import type { Page } from '@/types/base'; +import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; import HistoryModal from './components/HistoryModal'; +import dayjs from 'dayjs'; +/** + * 工作流实例列表页 + */ const WorkflowInstanceList: React.FC = () => { - const [loading, setLoading] = useState(false); - const [data, setData] = useState | null>(null); - const [query, setQuery] = useState({ - current: 1, - pageSize: 10, + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [historyVisible, setHistoryVisible] = useState(false); + const [selectedWorkflowDefinitionId, setSelectedWorkflowDefinitionId] = useState(); + const [query, setQuery] = useState({ + pageNum: DEFAULT_CURRENT - 1, + pageSize: DEFAULT_PAGE_SIZE, + businessKey: '', + status: undefined as string | undefined, + }); + + // 加载数据 + const loadData = async () => { + setLoading(true); + try { + const result = await getWorkflowInstances(query); + setData(result); + } catch (error) { + console.error('加载流程实例失败:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, [query]); + + // 搜索 + const handleSearch = () => { + setQuery(prev => ({ + ...prev, + pageNum: 0, + })); + }; + + // 重置搜索 + const handleReset = () => { + setQuery({ + pageNum: 0, + pageSize: DEFAULT_PAGE_SIZE, + businessKey: '', + status: undefined, }); - // 移除未使用的详情弹窗相关状态 - // const [detailVisible, setDetailVisible] = useState(false); - // const [selectedInstance, setSelectedInstance] = useState(); - const [historyVisible, setHistoryVisible] = useState(false); - const [selectedWorkflowDefinitionId, setSelectedWorkflowDefinitionId] = useState(); + }; - const loadData = async (params: any) => { - setLoading(true); - try { - const result = await getWorkflowInstances(params); - setData(result); - } catch (error) { - console.error('加载流程实例失败:', error); - } finally { - setLoading(false); - } + // 查看历史 + const handleViewHistory = (record: WorkflowTemplateWithInstances) => { + setSelectedWorkflowDefinitionId(record.id); + setHistoryVisible(true); + }; + + // 终止流程(TODO:需要后端接口) + const handleTerminate = async (record: WorkflowTemplateWithInstances) => { + if (!confirm(`确定要终止工作流 "${record.name}" 吗?`)) return; + console.log('终止流程', record); + // TODO: 调用终止接口 + }; + + // 状态徽章 + const getStatusBadge = (status: string) => { + const statusMap: Record = { + NOT_STARTED: { variant: 'outline', text: '未启动', icon: Clock }, + CREATED: { variant: 'secondary', text: '已创建', icon: PlayCircle }, + RUNNING: { variant: 'default', text: '运行中', icon: Activity }, + SUSPENDED: { variant: 'secondary', text: '已挂起', icon: Pause }, + COMPLETED: { variant: 'success', text: '已完成', icon: CheckCircle2 }, + TERMINATED: { variant: 'destructive', text: '已终止', icon: StopCircle }, + FAILED: { variant: 'destructive', text: '失败', icon: XCircle }, }; - - useEffect(() => { - loadData(query); - }, [query]); - - const handleViewHistory = (record: WorkflowTemplateWithInstances) => { - setSelectedWorkflowDefinitionId(record.id); - setHistoryVisible(true); - }; - - const columns: ColumnsType = [ - { - title: '流程名称', - dataIndex: 'name', - key: 'name', - }, - { - title: '业务标识', - dataIndex: 'businessKey', - key: 'businessKey', - }, - { - title: '最后执行时间', - dataIndex: 'lastExecutionTime', - key: 'lastExecutionTime', - render: (time: string) => time || '暂无' - }, - { - title: '最后执行状态', - dataIndex: 'lastExecutionStatus', - key: 'lastExecutionStatus', - render: (status: string) => { - if (!status) return '暂无'; - const statusMap: Record = { - TERMINATED: { color: 'warning', text: '已终止' }, - COMPLETED: { color: 'success', text: '已完成' }, - RUNNING: { color: 'processing', text: '运行中' }, - FAILED: { color: 'error', text: '失败' }, - NOT_STARTED: { color: 'default', text: '未执行' } - }; - const statusInfo = statusMap[status] || { color: 'default', text: status }; - return {statusInfo.text}; - }, - }, - { - title: '操作', - key: 'action', - fixed: 'right', - width: 200, - render: (_, record) => ( - - handleViewHistory(record)}>历史执行 - {record?.lastExecutionStatus === 'RUNNING' && ( - console.log('终止流程', record)}>终止流程 - )} - - ), - }, - ]; - - const handleTableChange = (pagination: any) => { - setQuery({ - ...query, - current: pagination.current, - pageSize: pagination.pageSize, - }); - }; - + const statusInfo = statusMap[status] || { variant: 'outline', text: status || '未知', icon: Clock }; + const Icon = statusInfo.icon; return ( - - - }} - pagination={{ - current: query.current, - pageSize: query.pageSize, - total: data?.totalElements || 0, - showSizeChanger: true, - showQuickJumper: true, - }} - onChange={handleTableChange} - /> - {/* DetailModal 暂时移除,因为数据结构不匹配 */} - {/* setDetailVisible(false)} - instanceData={selectedInstance} - /> */} - setHistoryVisible(false)} - workflowDefinitionId={selectedWorkflowDefinitionId} - /> - + + + {statusInfo.text} + ); + }; + + // 统计数据 + const stats = useMemo(() => { + const total = data?.totalElements || 0; + const runningCount = data?.content?.filter(d => d.lastExecutionStatus === 'RUNNING').length || 0; + const completedCount = data?.content?.filter(d => d.lastExecutionStatus === 'COMPLETED').length || 0; + const failedCount = data?.content?.filter(d => d.lastExecutionStatus === 'FAILED' || d.lastExecutionStatus === 'TERMINATED').length || 0; + return { total, runningCount, completedCount, failedCount }; + }, [data]); + + const pageCount = data?.totalElements ? Math.ceil(data.totalElements / query.pageSize) : 0; + + return ( +
+
+

工作流实例管理

+

+ 查看和管理工作流实例的运行状态和执行历史。 +

+
+ + {/* 统计卡片 */} +
+ + + 总实例数 + + + +
{stats.total}
+

所有工作流实例

+
+
+ + + 运行中 + + + +
{stats.runningCount}
+

正在执行的实例

+
+
+ + + 已完成 + + + +
{stats.completedCount}
+

成功执行完成

+
+
+ + + 失败/终止 + + + +
{stats.failedCount}
+

执行失败或被终止

+
+
+
+ + + + 实例列表 + + + {/* 搜索栏 */} +
+
+ setQuery(prev => ({ ...prev, businessKey: e.target.value }))} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="h-9" + /> +
+ + + +
+ + {/* 表格 */} +
+
+ + + 流程名称 + 业务标识 + 最后执行时间 + 执行状态 + 操作 + + + + {loading ? ( + + +
+ + 加载中... +
+
+
+ ) : data?.content && data.content.length > 0 ? ( + data.content.map((record) => ( + + {record.name} + + {record.businessKey ? ( + + {record.businessKey} + + ) : ( + - + )} + + + + {record.lastExecutionTime + ? dayjs(record.lastExecutionTime).format('YYYY-MM-DD HH:mm:ss') + : '-'} + + + {getStatusBadge(record.lastExecutionStatus)} + +
+ + {record.lastExecutionStatus === 'RUNNING' && ( + + )} +
+
+
+ )) + ) : ( + + +
+ +
暂无工作流实例
+
启动工作流后,实例记录将在此显示。
+
+
+
+ )} +
+
+ + + {/* 分页 */} + {pageCount > 1 && ( + setQuery(prev => ({ + ...prev, + pageNum: page - 1 + }))} + /> + )} + +
+ + {/* 历史记录弹窗 */} + setHistoryVisible(false)} + workflowDefinitionId={selectedWorkflowDefinitionId} + /> + + ); }; -export default WorkflowInstanceList; \ No newline at end of file +export default WorkflowInstanceList; diff --git a/frontend/src/pages/Workflow/Instance/types.ts b/frontend/src/pages/Workflow/Instance/types.ts index e4ff71ad..843fc4af 100644 --- a/frontend/src/pages/Workflow/Instance/types.ts +++ b/frontend/src/pages/Workflow/Instance/types.ts @@ -1,5 +1,8 @@ import { BaseQuery } from '@/types/base'; +/** + * 工作流模板及其实例信息 + */ export interface WorkflowTemplateWithInstances { id: number; name: string; @@ -8,10 +11,17 @@ export interface WorkflowTemplateWithInstances { lastExecutionStatus: string; } -export interface WorkflowTemplateWithInstancesQuery { - +/** + * 工作流实例查询参数 + */ +export interface WorkflowTemplateWithInstancesQuery extends BaseQuery { + businessKey?: string; + status?: string; } +/** + * 工作流实例阶段 + */ export interface WorkflowInstanceStage { id: number | null; nodeId: string; @@ -22,6 +32,9 @@ export interface WorkflowInstanceStage { endTime: string | null; } +/** + * 工作流图节点 + */ export interface WorkflowGraphNode { id: string; nodeCode: string; @@ -36,6 +49,9 @@ export interface WorkflowGraphNode { outputs: any[]; } +/** + * 工作流图边 + */ export interface WorkflowGraphEdge { id: string; from: string; @@ -51,11 +67,17 @@ export interface WorkflowGraphEdge { vertices: any[]; } +/** + * 工作流图 + */ export interface WorkflowGraph { nodes: WorkflowGraphNode[]; edges: WorkflowGraphEdge[]; } +/** + * 工作流历史实例 + */ export interface WorkflowHistoricalInstance { id: number; processInstanceId: string; @@ -68,6 +90,9 @@ export interface WorkflowHistoricalInstance { graph?: WorkflowGraph; } +/** + * 工作流历史实例查询参数 + */ export interface WorkflowHistoricalInstanceQuery extends BaseQuery { workflowDefinitionId?: number; }