|
|
@@ -1,7 +1,8 @@
|
|
|
-import React, { useState, useEffect } from 'react';
|
|
|
+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, InputNumber, Switch, Radio, Checkbox } from 'antd';
|
|
|
+import { Button, Space, Table as AntdTable, Modal, message, Input, Form as AntdForm, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Card, Alert, Cascader, Upload } 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';
|
|
|
@@ -48,41 +49,330 @@ export default function FormManagement() {
|
|
|
showResetButton: false,
|
|
|
};
|
|
|
|
|
|
- const renderFieldPreview = (field: any) => {
|
|
|
+ // 渲染字段预览(支持嵌套结构)
|
|
|
+ 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 || [];
|
|
|
+ const cardTitle = field.cardTitle || field.label || '卡片容器';
|
|
|
+ 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: '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 <Input.TextArea placeholder={field.placeholder || '请输入'} rows={3} />;
|
|
|
+ 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 <Input.Password placeholder={field.placeholder || '请输入'} />;
|
|
|
+ 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 <InputNumber style={{ width: '100%' }} placeholder={field.placeholder || '请输入'} />;
|
|
|
+ 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 (
|
|
|
- <Select
|
|
|
- placeholder={field.placeholder || '请选择'}
|
|
|
- options={(field.options || []).map((opt: any) => ({ label: opt.label, value: opt.value }))}
|
|
|
- />
|
|
|
+ <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 <DatePicker style={{ width: '100%' }} placeholder={field.placeholder || '请选择日期'} />;
|
|
|
+ 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
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
case 'daterange':
|
|
|
- return <DatePicker.RangePicker style={{ width: '100%' }} placeholder={['开始日期', '结束日期']} />;
|
|
|
+ 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
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
case 'switch':
|
|
|
- return <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 (
|
|
|
- <Radio.Group
|
|
|
- options={(field.options || []).map((opt: any) => ({ label: opt.label, value: opt.value }))}
|
|
|
- />
|
|
|
+ <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 (
|
|
|
- <Checkbox.Group
|
|
|
- options={(field.options || []).map((opt: any) => ({ label: opt.label, value: opt.value }))}
|
|
|
- />
|
|
|
+ <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 <Input placeholder={field.placeholder || '请输入'} />;
|
|
|
+ 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>
|
|
|
+ );
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -370,36 +660,34 @@ export default function FormManagement() {
|
|
|
width={800}
|
|
|
>
|
|
|
<div className="p-2">
|
|
|
- <AntdForm
|
|
|
- form={detailForm}
|
|
|
- layout={(detailData.option?.formConfig?.labelPosition || defaultFormConfig.labelPosition) === 'top' ? 'vertical' : 'horizontal'}
|
|
|
- size={detailData.option?.formConfig?.formSize || defaultFormConfig.formSize}
|
|
|
- labelCol={{
|
|
|
- style: {
|
|
|
- width: `${detailData.option?.formConfig?.labelWidth || defaultFormConfig.labelWidth}px`,
|
|
|
- textAlign:
|
|
|
- (detailData.option?.formConfig?.labelPosition || defaultFormConfig.labelPosition) === 'left'
|
|
|
- ? 'left'
|
|
|
- : (detailData.option?.formConfig?.labelPosition || defaultFormConfig.labelPosition) === 'right'
|
|
|
- ? 'right'
|
|
|
- : 'left',
|
|
|
- },
|
|
|
- }}
|
|
|
- requiredMark={detailData.option?.formConfig?.hideRequiredMark ? false : undefined}
|
|
|
- className="space-y-3"
|
|
|
- >
|
|
|
- {(detailData.rule || []).map((field: any) => (
|
|
|
- <AntdForm.Item
|
|
|
- key={field.id || field.name}
|
|
|
- label={(field.label || field.title || '') + (detailData.option?.formConfig?.labelSuffix || '')}
|
|
|
- name={field.name || field.field}
|
|
|
- required={field.required}
|
|
|
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
|
|
|
+ {(() => {
|
|
|
+ const formConfig = detailData.option?.formConfig || defaultFormConfig;
|
|
|
+ const layoutColumns = formConfig.layoutColumns || 1;
|
|
|
+ const gridStyle = layoutColumns > 1 ? {
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
|
|
|
+ gap: '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}
|
|
|
>
|
|
|
- {renderFieldPreview(field)}
|
|
|
- </AntdForm.Item>
|
|
|
- ))}
|
|
|
- </AntdForm>
|
|
|
+ <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : ''}>
|
|
|
+ {(detailData.rule || []).map((field: any) => renderFieldPreview(field))}
|
|
|
+ </div>
|
|
|
+ </AntdForm>
|
|
|
+ );
|
|
|
+ })()}
|
|
|
</div>
|
|
|
</Modal>
|
|
|
</div>
|