Files
udmin/frontend/src/pages/FlowList.tsx
ayou 65764a2cbc feat(FlowList): 为删除操作添加确认弹窗并改进错误处理
添加 Popconfirm 组件以防止误删流程,同时优化删除操作的错误提示
2025-09-15 23:22:26 +08:00

321 lines
12 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import { Button, Modal, Space, Table, message, Typography, Input, Form, Tooltip, Popconfirm } from 'antd'
import { PlusOutlined, ReloadOutlined, DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import api, { type ApiResp } from '../utils/axios'
import dayjs from 'dayjs'
interface FlowSummary { id: string; name: string; code?: string; remark?: string; created_at: string; updated_at: string; last_modified_by?: string }
// 新增:扩展 Doc 以便复用
interface FlowDoc { id: string; yaml: string }
// 后端创建/更新入参
interface FlowCreateReq { name?: string; yaml?: string; design_json?: any; code?: string; remark?: string }
interface FlowUpdateReq { name?: string; yaml?: string; design_json?: any; code?: string; remark?: string }
interface PageResp<T> { items: T[]; total: number; page: number; page_size: number }
export default function FlowList() {
const [loading, setLoading] = useState(false)
const [list, setList] = useState<FlowSummary[]>([])
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [total, setTotal] = useState(0)
const [keyword, setKeyword] = useState('')
const [searchForm] = Form.useForm()
const navigate = useNavigate()
// 编辑基础信息弹窗状态
const [editOpen, setEditOpen] = useState(false)
const [editing, setEditing] = useState(false)
const [editRow, setEditRow] = useState<FlowSummary | null>(null)
const [editName, setEditName] = useState('')
// 新建弹窗状态
const [createOpen, setCreateOpen] = useState(false)
const [createForm] = Form.useForm()
// 编辑表单
const [editForm] = Form.useForm()
const fetchList = async (p: number = page, ps: number = pageSize, kw: string = keyword) => {
setLoading(true)
try {
const { data } = await api.get<ApiResp<PageResp<FlowSummary>>>('/flows', { params: { page: p, page_size: ps, keyword: kw || undefined } })
if (data?.code === 0) {
setList(data.data?.items || [])
setTotal(data.data?.total || 0)
setPage(data.data?.page || p)
setPageSize(data.data?.page_size || ps)
}
} catch (e: any) {
message.error(e?.message || '加载失败')
} finally {
setLoading(false)
}
}
const openEdit = (row: FlowSummary) => {
setEditRow(row)
setEditName(row.name || '')
// 先重置表单,避免上一次的 code/remark 残留
editForm.resetFields()
// 预填表单为当前行已有数据(若无则为空)
editForm.setFieldsValue({ name: row.name || '', code: row.code || undefined, remark: row.remark || undefined })
setEditOpen(true)
// 尝试拉取详情以补充 code/remark
;(async () => {
try {
const { data } = await api.get(`/flows/${row.id}`)
// 兼容后端仅返回 yaml 的场景
const detail: any = data?.data || {}
const patch: any = {}
if (detail?.name !== undefined) patch.name = detail.name
if (detail?.code !== undefined) patch.code = detail.code
if (detail?.remark !== undefined) patch.remark = detail.remark
if (Object.keys(patch).length) editForm.setFieldsValue(patch)
} catch {}
})()
}
const handleEditOk = async () => {
if (!editRow) return
try {
const values = await editForm.validateFields()
const payload: FlowUpdateReq = {
name: (values.name || '').trim(),
code: values.code ? String(values.code).trim() : undefined,
remark: values.remark ? String(values.remark).trim() : undefined,
}
setEditing(true)
const { data } = await api.put(`/flows/${editRow.id}`, payload)
if (data?.code === 0) {
message.success('已保存')
setEditOpen(false)
// 保存成功后刷新列表,保证时间与最近修改人同步更新
fetchList(page, pageSize, keyword)
// 重置编辑状态
editForm.resetFields()
setEditRow(null)
setEditName('')
} else {
throw new Error(data?.message || '保存失败')
}
} catch (e: any) {
if (e?.errorFields) return // 表单校验错误
message.error(e?.message || '保存失败')
} finally {
setEditing(false)
}
}
const columns = useMemo(() => [
{ title: '流程编号', dataIndex: 'code', width: 160 },
{
title: '名称',
dataIndex: 'name',
width: 280,
ellipsis: { showTitle: false },
render: (text: string) => (
<Tooltip placement="topLeft" title={text || ''}>
<span>{text || '-'}</span>
</Tooltip>
)
},
{
title: '备注',
dataIndex: 'remark',
width: 220,
ellipsis: { showTitle: false },
render: (text?: string) => (
<Tooltip placement="topLeft" title={text || ''}>
<span>{text || '-'}</span>
</Tooltip>
)
},
{ title: '新建时间', dataIndex: 'created_at', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '' },
{ title: '最近修改时间', dataIndex: 'updated_at', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '' },
{ title: '最近修改人', dataIndex: 'last_modified_by', width: 140 },
{
title: '操作',
key: 'action',
width: 320,
render: (_: any, row: FlowSummary) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(row)}></Button>
<Button type="link" onClick={() => navigate(`/flows/editor?id=${encodeURIComponent(row.id)}`)}></Button>
<Popconfirm title={`确认删除流程「${row.name}」?`} onConfirm={() => onDelete(row)}>
<a className="action-link action-danger">
<DeleteOutlined />
<span></span>
</a>
</Popconfirm>
<Button type="link" icon={<EyeOutlined />} onClick={() => onView(row)}></Button>
</Space>
)
}
], [])
useEffect(() => { fetchList(1, 10, '') }, [])
const onCreate = async () => {
// 打开新建弹窗,让用户填写名称/编号/备注
createForm.resetFields()
setCreateOpen(true)
}
const handleCreateOk = async () => {
try{
const values = await createForm.validateFields()
const payload: FlowCreateReq = {
name: (values.name || '').trim(),
code: values.code ? String(values.code).trim() : undefined,
remark: values.remark ? String(values.remark).trim() : undefined
}
const { data } = await api.post('/flows', payload)
if(data?.code === 0){
message.success('创建成功')
setCreateOpen(false)
fetchList(page, pageSize, keyword)
}else{
throw new Error(data?.message || '创建失败')
}
}catch(e: any){
if(e?.errorFields) return; // 表单校验未通过
message.error(e?.message || '创建失败')
}
}
const onDelete = async (row: FlowSummary) => {
try {
const { data } = await api.delete<ApiResp<boolean>>(`/flows/${row.id}`)
if (data?.code === 0) {
message.success('删除成功')
fetchList(page, pageSize, keyword)
} else {
throw new Error(data?.message || '删除失败')
}
} catch (e: any) {
message.error(e?.message || '删除失败')
}
}
const onView = async (row: FlowSummary) => {
try {
const { data } = await api.get<ApiResp<FlowDoc>>(`/flows/${row.id}`)
if (data?.code === 0) {
Modal.info({
title: `流程内容 - ${row.name}`,
width: 720,
content: (
<pre style={{ maxHeight: 400, overflow: 'auto' }}>
{data.data?.yaml}
</pre>
)
})
}
} catch (e: any) {
message.error(e?.message || '加载失败')
}
}
return (
<div>
<div style={{ marginBottom: 12 }}>
<Form
form={searchForm}
layout="inline"
onFinish={(vals: any) => {
const kw = String(vals?.keyword ?? '').trim();
setKeyword(kw)
fetchList(1, pageSize, kw)
}}
>
<Form.Item name="keyword">
<Input allowClear placeholder="搜索流程名称" style={{ width: 320 }} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit"></Button>
<Button onClick={() => { searchForm.resetFields(); setKeyword(''); fetchList(1, pageSize, '') }}></Button>
<Button type="primary" icon={<PlusOutlined />} onClick={onCreate}></Button>
<Button icon={<ReloadOutlined />} onClick={() => fetchList(page, pageSize, keyword)}></Button>
</Space>
</Form.Item>
</Form>
</div>
<Table
rowKey="id"
loading={loading}
dataSource={list}
columns={columns as any}
pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p: number, ps?: number) => fetchList(p, ps ?? pageSize, keyword) }}
/>
{/* 编辑基础信息弹窗 */}
<Modal
title={`编辑流程${(editRow?.name || editName) ? ' - ' + (editRow?.name || editName) : ''}`}
open={editOpen}
onOk={handleEditOk}
confirmLoading={editing}
onCancel={() => { setEditOpen(false); setEditRow(null); setEditName(''); editForm.resetFields() }}
okText="保存"
destroyOnHidden
maskClosable={false}
>
<Form form={editForm} layout="vertical" preserve={false} initialValues={{ name: editName }}>
<Form.Item name="name" label="流程名称" rules={[{ required: true, message: '请输入流程名称' }, { max: 50, message: '最多50个字符' }]}>
<Input placeholder="请输入流程名称" allowClear />
</Form.Item>
<Form.Item name="code" label="流程编号" rules={[{ required: true, message: '请输入流程编号' }, { max: 50, message: '最多50个字符' }]}>
<Input placeholder="必填,唯一标识建议使用字母数字与-或_" allowClear />
</Form.Item>
<Form.Item name="remark" label="备注" rules={[{ max: 255, message: '最多255个字符' }]}>
<Input.TextArea rows={3} placeholder="可选,备注信息" allowClear />
</Form.Item>
</Form>
</Modal>
{/* 新建弹窗 */}
<Modal
title="新建流程"
open={createOpen}
onOk={handleCreateOk}
onCancel={() => setCreateOpen(false)}
destroyOnHidden
maskClosable={false}
>
<Form form={createForm} layout="vertical" preserve={false}>
<Form.Item name="name" label="流程名称" rules={[{ required: true, message: '请输入流程名称' }, { max: 50, message: '最多50个字符' }]}>
<Input placeholder="请输入流程名称" allowClear />
</Form.Item>
<Form.Item name="code" label="流程编号" rules={[{ required: true, message: '请输入流程编号' }, { max: 50, message: '最多50个字符' }]}>
<Input placeholder="必填,唯一标识建议使用字母数字与-或_" allowClear />
</Form.Item>
<Form.Item name="remark" label="备注" rules={[{ max: 255, message: '最多255个字符' }]}>
<Input.TextArea placeholder="可选,备注信息" rows={3} allowClear />
</Form.Item>
</Form>
</Modal>
{/* 编辑弹窗(冗余,禁用) */}
<Modal
title="编辑流程"
open={false}
onOk={handleEditOk}
confirmLoading={editing}
onCancel={() => setEditOpen(false)}
okText="保存"
>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ width: 64, textAlign: 'right' }}></span>
<Input
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="请输入流程名称"
maxLength={50}
showCount
/>
</div>
</Modal>
</div>
)
}