feat(flows): 新增流程编辑器基础功能与相关组件
feat(backend): 添加流程模型与服务支持 feat(frontend): 实现流程编辑器UI与交互 feat(assets): 添加流程节点图标资源 feat(plugins): 实现上下文菜单和运行时插件 feat(components): 新增基础节点和侧边栏组件 feat(routes): 添加流程相关路由配置 feat(models): 创建流程和运行日志数据模型 feat(services): 实现流程服务层逻辑 feat(migration): 添加流程相关数据库迁移 feat(config): 更新前端配置支持流程编辑器 feat(utils): 增强axios错误处理和工具函数
This commit is contained in:
314
frontend/src/pages/FlowList.tsx
Normal file
314
frontend/src/pages/FlowList.tsx
Normal file
@ -0,0 +1,314 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Modal, Space, Table, message, Typography, Input, Form, Tooltip } 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>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => onDelete(row)}>删除</Button>
|
||||
<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)
|
||||
}
|
||||
} 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="保存"
|
||||
destroyOnClose
|
||||
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)}
|
||||
destroyOnClose
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user