321 lines
12 KiB
TypeScript
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>
|
|
)
|
|
} |