|
|
@@ -131,14 +131,15 @@ import {
|
|
|
FireOutlined as FireIconOutlined,
|
|
|
ThunderboltOutlined as ThunderboltIconOutlined,
|
|
|
} from '@ant-design/icons';
|
|
|
-import { Button, Input, Select, Checkbox, Tabs, Modal, Dropdown, Popover, message } from 'antd';
|
|
|
+import { Button, Input, Select, Checkbox, Tabs, Modal, Dropdown, Popover, message, Card, Alert, InputNumber, Radio, DatePicker, Form as AntdForm, Cascader, Upload, Switch } from 'antd';
|
|
|
import type { MenuProps } from 'antd';
|
|
|
import { toast } from 'sonner';
|
|
|
import { workflowDesignApi, WorkflowDesignVO } from '../api/WorkflowDesign';
|
|
|
import { userApi } from '../api/user';
|
|
|
import { UserVO } from '../types';
|
|
|
import { segregationPointApi, SegregationPointVO } from '../api/spm';
|
|
|
-import { getFormPage, FormVO } from '../api/bpm/form';
|
|
|
+import { getFormPage, getForm, FormVO } from '../api/bpm/form';
|
|
|
+import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
|
|
|
|
|
|
// 节点配置
|
|
|
const nodeConfigs = [
|
|
|
@@ -694,6 +695,29 @@ export default function ProcessDesigner() {
|
|
|
const [workflowDetail, setWorkflowDetail] = useState<WorkflowDesignVO | null>(null);
|
|
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
|
|
|
|
+ // 表单预览相关状态
|
|
|
+ const [formPreviewVisible, setFormPreviewVisible] = useState(false);
|
|
|
+ const [formPreviewData, setFormPreviewData] = useState<FormVO | null>(null);
|
|
|
+ const [formPreviewLoading, setFormPreviewLoading] = useState(false);
|
|
|
+ const [formPreviewDetailData, setFormPreviewDetailData] = useState<FormCreateData>({
|
|
|
+ rule: [],
|
|
|
+ option: {}
|
|
|
+ });
|
|
|
+ const formPreviewForm = AntdForm.useForm()[0];
|
|
|
+
|
|
|
+ const defaultFormConfig = {
|
|
|
+ name: '',
|
|
|
+ labelPosition: 'right',
|
|
|
+ formSize: 'middle',
|
|
|
+ labelSuffix: '',
|
|
|
+ labelWidth: 100,
|
|
|
+ hideRequiredMark: false,
|
|
|
+ showValidationError: true,
|
|
|
+ inlineValidation: false,
|
|
|
+ showSubmitButton: false,
|
|
|
+ showResetButton: false,
|
|
|
+ };
|
|
|
+
|
|
|
// 角色用户列表
|
|
|
const [drawerUsers, setDrawerUsers] = useState<UserVO[]>([]); // 负责人(jtdrawer)
|
|
|
const [lockerUsers, setLockerUsers] = useState<UserVO[]>([]); // 上锁人(jtlocker)
|
|
|
@@ -1476,7 +1500,7 @@ export default function ProcessDesigner() {
|
|
|
const handleSave = async () => {
|
|
|
try {
|
|
|
// 构建导出数据(与导出JSON逻辑相同)
|
|
|
- const adjacency: Record<string, { incoming: string[]; outgoing: string[] }> = {};
|
|
|
+ const adjacency: Record<string, { parentUuid: string[]; childrenUuid: string[] }> = {};
|
|
|
const nodeIdMap = new Map<string, string>();
|
|
|
nodes.forEach(n => {
|
|
|
nodeIdMap.set(n.id, n.id);
|
|
|
@@ -1484,15 +1508,15 @@ export default function ProcessDesigner() {
|
|
|
|
|
|
nodes.forEach(n => {
|
|
|
const uuid = nodeIdMap.get(n.id) || n.id;
|
|
|
- adjacency[uuid] = adjacency[uuid] || { incoming: [], outgoing: [] };
|
|
|
+ adjacency[uuid] = adjacency[uuid] || { parentUuid: [], childrenUuid: [] };
|
|
|
});
|
|
|
edges.forEach(e => {
|
|
|
const sourceUuid = nodeIdMap.get(e.source) || e.source;
|
|
|
const targetUuid = nodeIdMap.get(e.target) || e.target;
|
|
|
- if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { incoming: [], outgoing: [] };
|
|
|
- if (!adjacency[targetUuid]) adjacency[targetUuid] = { incoming: [], outgoing: [] };
|
|
|
- adjacency[sourceUuid].outgoing.push(targetUuid);
|
|
|
- adjacency[targetUuid].incoming.push(sourceUuid);
|
|
|
+ if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] };
|
|
|
+ if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] };
|
|
|
+ adjacency[sourceUuid].childrenUuid.push(targetUuid);
|
|
|
+ adjacency[targetUuid].parentUuid.push(sourceUuid);
|
|
|
});
|
|
|
|
|
|
const exportData = {
|
|
|
@@ -1587,18 +1611,18 @@ export default function ProcessDesigner() {
|
|
|
});
|
|
|
|
|
|
// adjacency使用uuid作为key
|
|
|
- const adjacency: Record<string, { incoming: string[]; outgoing: string[] }> = {};
|
|
|
+ const adjacency: Record<string, { parentUuid: string[]; childrenUuid: string[] }> = {};
|
|
|
nodes.forEach(n => {
|
|
|
const uuid = nodeIdMap.get(n.id) || n.id;
|
|
|
- adjacency[uuid] = adjacency[uuid] || { incoming: [], outgoing: [] };
|
|
|
+ adjacency[uuid] = adjacency[uuid] || { parentUuid: [], childrenUuid: [] };
|
|
|
});
|
|
|
edges.forEach(e => {
|
|
|
const sourceUuid = nodeIdMap.get(e.source) || e.source;
|
|
|
const targetUuid = nodeIdMap.get(e.target) || e.target;
|
|
|
- if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { incoming: [], outgoing: [] };
|
|
|
- if (!adjacency[targetUuid]) adjacency[targetUuid] = { incoming: [], outgoing: [] };
|
|
|
- adjacency[sourceUuid].outgoing.push(targetUuid);
|
|
|
- adjacency[targetUuid].incoming.push(sourceUuid);
|
|
|
+ if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] };
|
|
|
+ if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] };
|
|
|
+ adjacency[sourceUuid].childrenUuid.push(targetUuid);
|
|
|
+ adjacency[targetUuid].parentUuid.push(sourceUuid);
|
|
|
});
|
|
|
|
|
|
const exportData = {
|
|
|
@@ -2301,19 +2325,47 @@ export default function ProcessDesigner() {
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
提交表单 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
|
|
|
</label>
|
|
|
- <Select
|
|
|
- value={nodeConfig.submitForm || undefined}
|
|
|
- onChange={(value) =>
|
|
|
- setNodeConfig({ ...nodeConfig, submitForm: value || '' })
|
|
|
- }
|
|
|
- placeholder="请选择提交表单"
|
|
|
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
- allowClear
|
|
|
- >
|
|
|
- {formList.map(form => (
|
|
|
- <Select.Option key={form.id} value={form.id}>{form.name}</Select.Option>
|
|
|
- ))}
|
|
|
- </Select>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Select
|
|
|
+ value={nodeConfig.submitForm || undefined}
|
|
|
+ onChange={(value) =>
|
|
|
+ setNodeConfig({ ...nodeConfig, submitForm: value || '' })
|
|
|
+ }
|
|
|
+ placeholder="请选择提交表单"
|
|
|
+ className="flex-1 [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
+ allowClear
|
|
|
+ >
|
|
|
+ {formList.map(form => (
|
|
|
+ <Select.Option key={form.id} value={form.id}>{form.name}</Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <Button
|
|
|
+ icon={<EyeOutlined />}
|
|
|
+ onClick={async () => {
|
|
|
+ if (!nodeConfig.submitForm) {
|
|
|
+ message.warning('请先选择表单');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setFormPreviewLoading(true);
|
|
|
+ try {
|
|
|
+ const formData = await getForm(Number(nodeConfig.submitForm));
|
|
|
+ setFormPreviewData(formData);
|
|
|
+ // 解析表单配置和字段
|
|
|
+ setConfAndFields2(setFormPreviewDetailData, formData.conf, formData.fields);
|
|
|
+ setFormPreviewVisible(true);
|
|
|
+ } catch (error: any) {
|
|
|
+ message.error(error?.message || '获取表单详情失败');
|
|
|
+ } finally {
|
|
|
+ setFormPreviewLoading(false);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ disabled={!nodeConfig.submitForm}
|
|
|
+ loading={formPreviewLoading}
|
|
|
+ className="flex-shrink-0"
|
|
|
+ >
|
|
|
+ 预览
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
{/* 隔离/方案 和 解除隔离 节点特有的字段 */}
|
|
|
@@ -2834,6 +2886,239 @@ export default function ProcessDesigner() {
|
|
|
className="font-mono text-xs"
|
|
|
/>
|
|
|
</div>
|
|
|
+ </Modal>
|
|
|
+
|
|
|
+ {/* 表单预览Modal */}
|
|
|
+ <Modal
|
|
|
+ open={formPreviewVisible}
|
|
|
+ title={`预览表单 - ${formPreviewData?.name || ''}`}
|
|
|
+ onCancel={() => {
|
|
|
+ setFormPreviewVisible(false);
|
|
|
+ setFormPreviewData(null);
|
|
|
+ setFormPreviewDetailData({ rule: [], option: {} });
|
|
|
+ }}
|
|
|
+ footer={[
|
|
|
+ <Button key="close" onClick={() => {
|
|
|
+ setFormPreviewVisible(false);
|
|
|
+ setFormPreviewData(null);
|
|
|
+ setFormPreviewDetailData({ rule: [], option: {} });
|
|
|
+ }}>
|
|
|
+ 关闭
|
|
|
+ </Button>
|
|
|
+ ]}
|
|
|
+ width={800}
|
|
|
+ style={{ top: 20 }}
|
|
|
+ styles={{ body: { maxHeight: 'calc(90vh - 120px)', overflowY: 'auto', overflowX: 'hidden' } }}
|
|
|
+ >
|
|
|
+ <div className="p-4">
|
|
|
+ {(() => {
|
|
|
+ const formConfig = formPreviewDetailData.option?.formConfig || defaultFormConfig;
|
|
|
+ const layoutColumns = formConfig.layoutColumns || 1;
|
|
|
+
|
|
|
+ // 渲染字段预览(支持嵌套结构)
|
|
|
+ const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
|
|
|
+ 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') {
|
|
|
+ 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}
|
|
|
+ />
|
|
|
+ </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}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Input.TextArea
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
|
|
|
+ rows={4}
|
|
|
+ disabled
|
|
|
+ />
|
|
|
+ </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}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <InputNumber style={{ width: '100%' }} placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'} disabled />
|
|
|
+ </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}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Select placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'} disabled>
|
|
|
+ {(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}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <DatePicker style={{ width: '100%' }} placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'} disabled />
|
|
|
+ </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}
|
|
|
+ valuePropName="checked"
|
|
|
+ >
|
|
|
+ <Switch 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}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Radio.Group 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}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Checkbox.Group disabled>
|
|
|
+ {(field.options || []).map((opt: any, idx: number) => (
|
|
|
+ <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
|
|
|
+ ))}
|
|
|
+ </Checkbox.Group>
|
|
|
+ </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}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Input
|
|
|
+ type={field.inputType || 'text'}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
|
|
|
+ disabled
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <AntdForm
|
|
|
+ form={formPreviewForm}
|
|
|
+ layout={formConfig.labelPosition === 'top' ? 'vertical' : 'horizontal'}
|
|
|
+ size={formConfig.formSize || 'middle'}
|
|
|
+ labelCol={formConfig.labelPosition !== 'top' ? { span: formConfig.labelWidth ? Math.floor(formConfig.labelWidth / 8) : 6 } : undefined}
|
|
|
+ wrapperCol={formConfig.labelPosition !== 'top' ? { span: 24 - (formConfig.labelWidth ? Math.floor(formConfig.labelWidth / 8) : 6) } : undefined}
|
|
|
+ >
|
|
|
+ <div style={layoutColumns > 1 ? { display: 'grid', gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`, gap: '16px' } : {}}>
|
|
|
+ {(formPreviewDetailData.rule || []).map((field: any) => renderFieldPreview(field))}
|
|
|
+ </div>
|
|
|
+ </AntdForm>
|
|
|
+ );
|
|
|
+ })()}
|
|
|
+ </div>
|
|
|
</Modal>
|
|
|
</div>
|
|
|
);
|