| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825 |
- import React, { useState, useEffect, useMemo } from 'react';
- import { useNavigate, useLocation } from 'react-router-dom';
- import { Plus, Edit2, Trash2, Copy, Eye, X, Search, RotateCcw } from 'lucide-react';
- import { Button, Space, Table as AntdTable, Modal, message, Input, Form as AntdForm, Select, DatePicker, TimePicker, InputNumber, Switch, Radio, Checkbox, Card, Alert, Cascader, Upload, Tooltip } from 'antd';
- import { UploadOutlined } from '@ant-design/icons';
- import type { ColumnsType } from 'antd/es/table';
- import { toast } from 'sonner';
- import * as FormApi from '../api/bpm/form';
- import { DICT_TYPE, getDictLabel } from '../utils/dict';
- import { dateFormatter } from '../utils/formatTime';
- import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
- export default function FormManagement() {
- const navigate = useNavigate();
- const location = useLocation();
-
- // 调试信息
- useEffect(() => {
- console.log('FormManagement 组件已加载');
- }, []);
-
- const [loading, setLoading] = useState(true);
- const [list, setList] = useState<FormApi.FormVO[]>([]);
- const [total, setTotal] = useState(0);
- const [queryParams, setQueryParams] = useState<FormApi.FormPageParams>({
- pageNo: 1,
- pageSize: 10,
- name: '',
- });
- const [searchName, setSearchName] = useState('');
- const [detailForm] = AntdForm.useForm();
-
- // 详情弹窗
- const [detailVisible, setDetailVisible] = useState(false);
- const [detailData, setDetailData] = useState<FormCreateData>({
- rule: [],
- option: {}
- });
- const defaultFormConfig = {
- name: '',
- labelPosition: 'right',
- formSize: 'middle',
- labelSuffix: '',
- labelWidth: 100,
- hideRequiredMark: false,
- showValidationError: true,
- inlineValidation: false,
- showSubmitButton: false,
- showResetButton: false,
- };
- // 渲染字段预览(支持嵌套结构)
- const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
- const formConfig = detailData.option?.formConfig || defaultFormConfig;
- const layoutColumns = formConfig.layoutColumns || 1;
- const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
- // 处理容器类型(card 和 grid)
- if (field.type === 'card') {
- const children = field.children || [];
- // 优先使用 label(字段名称),如果没有则使用 cardTitle,最后使用默认值
- const cardTitle = field.label || field.cardTitle || '卡片容器';
- return (
- <div key={field.id} style={spanStyle} className="mb-4">
- <Card title={cardTitle} className="w-full">
- <div className="space-y-4">
- {children.map((child: any) => renderFieldPreview(child))}
- </div>
- </Card>
- </div>
- );
- }
- if (field.type === 'grid') {
- const gridColumns = field.gridColumns || 2;
- const children = field.children || [];
- return (
- <div key={field.id} style={spanStyle} className="mb-4">
- <div
- style={{
- display: 'grid',
- gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
- gap: '12px',
- rowGap: '16px',
- }}
- >
- {children.map((child: any) => {
- const childSpanStyle = gridColumns > 1
- ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
- : undefined;
- return (
- <div key={child.id} style={childSpanStyle}>
- {renderFieldPreview(child)}
- </div>
- );
- })}
- </div>
- </div>
- );
- }
- // 处理 alert 类型
- if (field.type === 'alert') {
- const themeClass =
- field.alertTheme === 'dark'
- ? 'bg-gray-800 text-white border-gray-700'
- : '';
- return (
- <div key={field.id} className="mb-4" style={spanStyle}>
- <Alert
- message={field.alertTitle || field.label}
- description={field.alertDescription}
- type={field.alertType || 'info'}
- showIcon={field.alertShowIcon !== false}
- closable={field.alertClosable}
- closeText={field.alertCloseText}
- className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
- banner
- style={field.style}
- />
- {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
- </div>
- );
- }
- // 处理普通字段
- switch (field.type) {
- case 'textarea':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- >
- <Input.TextArea
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- rows={4}
- allowClear={field.showClear}
- maxLength={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'password':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- >
- <Input.Password
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- allowClear={field.showClear}
- maxLength={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'number':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- >
- <InputNumber
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- max={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'select':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- >
- <Select
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- >
- {(field.options || []).map((opt: any, idx: number) => (
- <Select.Option key={idx} value={opt.value}>{opt.label}</Select.Option>
- ))}
- </Select>
- </AntdForm.Item>
- </div>
- );
- case 'date':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
- help={field.hint}
- >
- <DatePicker
- className="w-full"
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'daterange':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期范围' }] : []}
- help={field.hint}
- >
- <DatePicker.RangePicker
- className="w-full"
- placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'datetime':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
- help={field.hint}
- >
- <DatePicker
- className="w-full"
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- showTime={{ format: 'HH:mm:ss' }}
- format="YYYY-MM-DD HH:mm:ss"
- />
- </AntdForm.Item>
- </div>
- );
- case 'timepicker':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
- help={field.hint}
- >
- <TimePicker
- className="w-full"
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- format="HH:mm:ss"
- />
- </AntdForm.Item>
- </div>
- );
- case 'switch':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- valuePropName="checked"
- >
- <Switch disabled={field.disabled} />
- </AntdForm.Item>
- </div>
- );
- case 'radio':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- >
- <Radio.Group disabled={field.disabled}>
- {(field.options || []).map((opt: any, idx: number) => (
- <Radio key={idx} value={opt.value}>{opt.label}</Radio>
- ))}
- </Radio.Group>
- </AntdForm.Item>
- </div>
- );
- case 'checkbox':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- >
- <Checkbox.Group disabled={field.disabled}>
- {(field.options || []).map((opt: any, idx: number) => (
- <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
- ))}
- </Checkbox.Group>
- </AntdForm.Item>
- </div>
- );
- case 'cascader':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- >
- <Cascader
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
- options={field.cascaderOptions || []}
- className="w-full"
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'upload':
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请上传' }] : []}
- help={field.hint}
- >
- <Upload
- disabled={field.disabled}
- maxCount={field.uploadType === 'single-image' ? 1 : field.maxCount || (field.uploadType === 'multiple-image' ? 9 : undefined)}
- accept={field.accept || (field.uploadType === 'single-image' || field.uploadType === 'multiple-image' ? 'image/*' : undefined)}
- listType={(field.uploadType === 'file' ? 'text' : 'picture-card') as 'text' | 'picture-card' | 'picture'}
- multiple={field.uploadType !== 'single-image'}
- >
- {field.uploadType === 'file' ? (
- <Button icon={<UploadOutlined />}>上传文件</Button>
- ) : (
- <div>
- <UploadOutlined />
- <div style={{ marginTop: 8 }}>上传</div>
- </div>
- )}
- </Upload>
- </AntdForm.Item>
- </div>
- );
- default:
- return (
- <div key={field.id} style={spanStyle}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- >
- <Input
- type={field.inputType || 'text'}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- allowClear={field.showClear}
- maxLength={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- }
- };
- /** 查询列表 */
- const getList = async (params?: FormApi.FormPageParams) => {
- const searchParams = params || queryParams;
- setLoading(true);
- try {
- console.log('FormManagement: 开始获取表单列表', searchParams);
- const data = await FormApi.getFormPage(searchParams);
- console.log('FormManagement: 获取到数据', data);
- setList(data.list || []);
- setTotal(data.total || 0);
- } catch (error: any) {
- console.error('FormManagement: 获取表单列表失败', error);
- toast.error(error.message || '获取表单列表失败');
- // 确保即使失败也显示空列表
- setList([]);
- setTotal(0);
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- getList();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [queryParams.pageNo, queryParams.pageSize, queryParams.name]);
- // 监听路径变化,当从表单编辑器返回时自动刷新列表
- useEffect(() => {
- // 当路径包含 /form 时,刷新列表以确保显示最新数据
- if (location.pathname.includes('/form')) {
- getList();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [location.pathname]);
- /** 添加/修改操作 */
- const openForm = (type: string, id?: number) => {
- const query: Record<string, string> = { type };
- if (id !== undefined && (typeof id === 'number' || typeof id === 'string')) {
- query.id = String(id);
- }
- // 跳转到表单编辑器(这里需要根据实际路由配置)
- navigate(`/bpm/form/editor?${new URLSearchParams(query).toString()}`);
- };
- /** 删除按钮操作 */
- const handleDelete = async (id: number) => {
- Modal.confirm({
- title: '确认删除',
- content: '确定要删除这条表单数据吗?',
- okText: '确定',
- cancelText: '取消',
- onOk: async () => {
- try {
- await FormApi.deleteForm(id);
- toast.success('删除成功');
- await getList();
- } catch (error: any) {
- toast.error(error.message || '删除失败');
- }
- }
- });
- };
- /** 详情操作 */
- const openDetail = async (rowId: number) => {
- try {
- const data = await FormApi.getForm(rowId);
- setConfAndFields2(setDetailData, data.conf, data.fields);
- setDetailVisible(true);
- } catch (error: any) {
- toast.error(error.message || '获取表单详情失败');
- }
- };
- // 搜索
- const handleSearch = () => {
- setQueryParams({
- ...queryParams,
- pageNo: 1,
- name: searchName.trim() || undefined,
- });
- };
- // 重置
- const handleReset = () => {
- setSearchName('');
- setQueryParams({
- ...queryParams,
- pageNo: 1,
- name: undefined,
- });
- };
- // 表格列配置
- const columns: ColumnsType<FormApi.FormVO> = [
- {
- title: '编号',
- dataIndex: 'id',
- width: 90,
- align: 'center',
- },
- {
- title: '表单名',
- dataIndex: 'name',
- width: 220,
- align: 'center',
- render: (name: string, record: FormApi.FormVO) => {
- if (!name || name === '-') return <span>{name || '-'}</span>;
- return (
- <span
- className="text-blue-600 cursor-pointer hover:text-blue-800 hover:underline"
- onClick={(e) => {
- e.stopPropagation();
- openDetail(record.id!);
- }}
- >
- {name}
- </span>
- );
- },
- },
- {
- title: '状态',
- dataIndex: 'status',
- width: 110,
- align: 'center',
- render: (status: number) => {
- const isOpen = Number(status) === 0;
- return (
- <span
- className={`inline-flex px-3 py-1 rounded-lg text-xs ${
- isOpen ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
- }`}
- >
- {isOpen ? '开启' : '关闭'}
- </span>
- );
- },
- },
- {
- title: '备注',
- dataIndex: 'remark',
- width: 260,
- align: 'center',
- render: (text: string) => {
- const remarkText = text || '-';
- const maxLength = 20;
- const shouldTruncate = remarkText.length > maxLength;
- const displayText = shouldTruncate ? remarkText.slice(0, maxLength) + '...' : remarkText;
-
- return (
- <Tooltip placement="topLeft" title={remarkText}>
- <span>{displayText}</span>
- </Tooltip>
- );
- },
- },
- {
- title: '创建时间',
- dataIndex: 'createTime',
- width: 180,
- align: 'center',
- render: (text: string) => dateFormatter(text),
- },
- {
- title: '操作',
- width: 260,
- align: 'center',
- fixed: 'right',
- render: (_: any, record: FormApi.FormVO) => (
- <Space size="small">
- <Button
- type="link"
- size="small"
- icon={<Copy className="w-4 h-4" style={{ color: '#000000' }} />}
- onClick={() => openForm('copy', record.id)}
- style={{ color: '#000000' }}
- className="transition-colors"
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- 复制
- </Button>
- <Button
- type="link"
- size="small"
- icon={<Edit2 className="w-4 h-4" style={{ color: '#000000' }} />}
- onClick={() => openForm('update', record.id)}
- style={{ color: '#000000' }}
- className="transition-colors"
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- 编辑
- </Button>
- <Button
- type="link"
- size="small"
- icon={<Eye className="w-4 h-4" style={{ color: '#000000' }} />}
- onClick={() => openDetail(record.id!)}
- style={{ color: '#000000' }}
- className="transition-colors"
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- 预览
- </Button>
- <Button
- type="link"
- size="small"
- icon={<Trash2 className="w-4 h-4" style={{ color: '#000000' }} />}
- onClick={() => handleDelete(record.id!)}
- style={{ color: '#000000' }}
- className="transition-colors"
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- 删除
- </Button>
- </Space>
- ),
- },
- ];
- // 计算分页数据
- const totalPages = Math.ceil(total / queryParams.pageSize) || 1;
- return (
- <div className="space-y-6">
- {/* 操作栏和表格容器 */}
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
- {/* 查询与操作栏 */}
- <div className="p-4 lg:p-5 border-b border-gray-200/50">
- <div className="flex flex-wrap items-center justify-between gap-3">
- <div className="flex items-center gap-3">
- <span className="text-sm text-gray-700">表单名:</span>
- <Input
- value={searchName}
- onChange={(e) => setSearchName(e.target.value)}
- placeholder="请输入表单名"
- allowClear
- style={{ width: 240 }}
- />
- </div>
- <Space size="small">
- <Button type="primary" icon={<Search className="w-4 h-4" />} onClick={handleSearch}>
- 搜索
- </Button>
- <Button icon={<RotateCcw className="w-4 h-4" />} onClick={handleReset}>
- 重置
- </Button>
- <Button
- type="primary"
- icon={<Plus className="w-4 h-4" />}
- onClick={() => openForm('create')}
- >
- 新建
- </Button>
- </Space>
- </div>
- </div>
- {/* 表格容器 */}
- <div className="overflow-hidden min-w-0">
- <AntdTable
- loading={loading}
- columns={columns}
- dataSource={list}
- rowKey="id"
- pagination={false}
- scroll={{ x: 'max-content' }}
- />
- </div>
- </div>
- {/* 分页 */}
- {total > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
- <div className="flex items-center justify-between">
- <div className="text-sm text-gray-600">
- 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
- </div>
- <div className="flex gap-2">
- <Button
- onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
- disabled={queryParams.pageNo <= 1}
- >
- 上一页
- </Button>
- <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
- {queryParams.pageNo} / {totalPages}
- </span>
- <Button
- onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
- disabled={queryParams.pageNo >= totalPages}
- >
- 下一页
- </Button>
- </div>
- </div>
- </div>
- )}
- {/* 表单详情的弹窗 */}
- <Modal
- title="表单详情"
- open={detailVisible}
- onCancel={() => setDetailVisible(false)}
- footer={[
- <div key="tip" style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '12px', width: '100%' }}>
- <span style={{ color: '#ff4d4f', fontSize: '14px' }}>仅为表单预览,点击提交无效</span>
- <Button
- key="submit"
- type="primary"
- onClick={() => {
- // 详情预览提交按钮不执行任何操作,也不关闭弹窗
- return;
- }}
- >
- 提交
- </Button>
- </div>,
- ]}
- width={800}
- >
- <div className="p-2">
- {(() => {
- const formConfig = detailData.option?.formConfig || defaultFormConfig;
- const layoutColumns = formConfig.layoutColumns || 1;
- const gridStyle = layoutColumns > 1 ? {
- display: 'grid',
- gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
- gap: '12px',
- rowGap: '16px',
- } : undefined;
- return (
- <AntdForm
- form={detailForm}
- layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
- size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
- requiredMark={formConfig.hideRequiredMark ? false : undefined}
- labelCol={formConfig.labelWidth ? {
- style: {
- width: `${formConfig.labelWidth}px`,
- textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
- }
- } : undefined}
- >
- <div
- style={gridStyle}
- className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
- >
- <style>{`
- .form-detail-grid .ant-form-item {
- margin-bottom: 12px;
- }
- `}</style>
- {(detailData.rule || []).map((field: any) => renderFieldPreview(field))}
- </div>
- </AntdForm>
- );
- })()}
- </div>
- </Modal>
- </div>
- );
- }
|