|
|
@@ -3,7 +3,7 @@ import { flushSync } from 'react-dom';
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import { Plus, Search, Edit2, Trash2, MoreVertical, FileText, Eye, Play, CheckCircle, RefreshCw, Workflow, Hand } from 'lucide-react';
|
|
|
-import { Button, Input, Space, Select, TreeSelect, Table as AntdTable, message, Modal, Form, Row, Col, Tabs, Radio, DatePicker, Checkbox, Tooltip, Switch as AntdSwitch } from 'antd';
|
|
|
+import { Button, Input, Space, Select, TreeSelect, Table as AntdTable, message, Modal, Form, Row, Col, Tabs, Radio, DatePicker, Checkbox, Tooltip, Switch as AntdSwitch, Popover } from 'antd';
|
|
|
import { Button as UIButton } from './ui/button';
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
import { workflowDesignApi, WorkflowDesignVO } from '../api/WorkflowDesign';
|
|
|
@@ -41,11 +41,15 @@ import {
|
|
|
ClockCircleOutlined,
|
|
|
FireOutlined,
|
|
|
WarningOutlined,
|
|
|
+ SaveOutlined,
|
|
|
+ EyeOutlined,
|
|
|
} from '@ant-design/icons';
|
|
|
import { userApi, UserVO } from '../api/user';
|
|
|
import { deptApi, type DeptVO } from '../api/dept';
|
|
|
import { segregationPointApi, SegregationPointVO } from '../api/spm';
|
|
|
import { getFormPage, getForm, FormVO } from '../api/bpm/form';
|
|
|
+import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
|
|
|
+import { generateIconPaths, getIconPathByFileName, getWorkflowNodeDescription } from '../utils/workflowNodePanelUi';
|
|
|
import {
|
|
|
isWorkflowCoLockType,
|
|
|
isWorkflowIsolationSchemePanelType,
|
|
|
@@ -275,6 +279,59 @@ const getIconPathByFileName = (fileName: string | undefined): string | null => {
|
|
|
return null;
|
|
|
};
|
|
|
|
|
|
+/** 作业流程管理:节点配置是否完整(供初始化「已配置」与保存前校验共用) */
|
|
|
+function validateWorkflowJobNodeConfig(config: any, nodeType: string): boolean {
|
|
|
+ if (!config.nodeName || String(config.nodeName).trim() === '') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const formRequiredNodeTypes = ['confirm', 'review', 'inputInfo'];
|
|
|
+ if (formRequiredNodeTypes.includes(nodeType) && !config.submitForm) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ nodeType !== 'createJob' &&
|
|
|
+ !isWorkflowLockLikeValidationType(nodeType) &&
|
|
|
+ !isWorkflowUnlockLikeValidationType(nodeType)
|
|
|
+ ) {
|
|
|
+ if (!config.responsible) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isWorkflowCoLockType(nodeType)) {
|
|
|
+ if (!config.isolationNodeUuid || config.isolationNodeUuid === '') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!config.coLockPersons || !Array.isArray(config.coLockPersons) || config.coLockPersons.length === 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (isWorkflowLockSchemeType(nodeType)) {
|
|
|
+ if (!config.isolationType || config.isolationType === '') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!config.isolationPoints || !Array.isArray(config.isolationPoints) || config.isolationPoints.length === 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (config.isolationType === '0' || config.isolationType === '2') {
|
|
|
+ if (!config.responsible) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (config.isolationType === '1') {
|
|
|
+ if (!config.lockPerson) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isWorkflowUnlockLikeValidationType(nodeType)) {
|
|
|
+ if (!config.isolationNodeUuid || config.isolationNodeUuid === '') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
// 自定义节点组件
|
|
|
function CustomNode({ data, selected, id, nodeSavedStatusCache }: any) {
|
|
|
const paletteType = resolveWorkflowPaletteType(data.type);
|
|
|
@@ -476,7 +533,11 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
const [workflowNodes, setWorkflowNodes, onWorkflowNodesChange] = useNodesState([]);
|
|
|
const [workflowEdges, setWorkflowEdges, onWorkflowEdgesChange] = useEdgesState([]);
|
|
|
const [selectedWorkflowNode, setSelectedWorkflowNode] = useState<Node | null>(null);
|
|
|
- const [workflowActiveTabKey, setWorkflowActiveTabKey] = useState<string>('info');
|
|
|
+ const [workflowIconPickerOpen, setWorkflowIconPickerOpen] = useState(false);
|
|
|
+ const [workflowFormPreviewVisible, setWorkflowFormPreviewVisible] = useState(false);
|
|
|
+ const [workflowFormPreviewLoading, setWorkflowFormPreviewLoading] = useState(false);
|
|
|
+ const [workflowFormPreviewData, setWorkflowFormPreviewData] = useState<FormVO | null>(null);
|
|
|
+ const [workflowFormPreviewDetail, setWorkflowFormPreviewDetail] = useState<FormCreateData>({ rule: [], option: {} });
|
|
|
const workflowReactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
|
const [workflowReactFlowInstance, setWorkflowReactFlowInstance] = useState<any>(null);
|
|
|
/** 当前 workflowNodeConfig 对应的节点 id,用于切换节点时避免把上一节点的 config 应用到画布 */
|
|
|
@@ -1731,19 +1792,23 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
// 将节点配置存入缓存
|
|
|
newCache.set(nodeDO.uuid, nodeConfig);
|
|
|
|
|
|
- // 检查节点是否已保存(如果不在未保存列表中,说明已保存)
|
|
|
- // 通过检查未保存消息中是否包含该节点的信息来判断
|
|
|
- const isNodeUnsaved = unsavedNodeMessages.some((msg: string) => {
|
|
|
- // 如果消息中包含节点名称或UUID,说明该节点未保存
|
|
|
- return msg.includes(nodeDO.nodeName || '') || msg.includes(nodeDO.uuid || '');
|
|
|
+ // 是否被 check 接口提示为未保存(消息里含节点名或 UUID)
|
|
|
+ const messageImpliesUnsaved = unsavedNodeMessages.some((msg: string) => {
|
|
|
+ const m = String(msg ?? '');
|
|
|
+ return (
|
|
|
+ (nodeDO.nodeName && m.includes(String(nodeDO.nodeName))) ||
|
|
|
+ (nodeDO.uuid && m.includes(String(nodeDO.uuid)))
|
|
|
+ );
|
|
|
});
|
|
|
-
|
|
|
- if (!isNodeUnsaved) {
|
|
|
- // 节点已保存,标记为已完成
|
|
|
+ const paletteType = resolveWorkflowPaletteType(String(nodeDO.type || nodeData.type));
|
|
|
+ const configLooksComplete = validateWorkflowJobNodeConfig(nodeConfig, paletteType);
|
|
|
+ // 有持久化 id 且(校验通过 或 check 未点名未保存)→ 已配置;共锁/解除共锁等以校验为准避免漏判
|
|
|
+ const isSavedNode = !!nodeDO.id && (configLooksComplete || !messageImpliesUnsaved);
|
|
|
+
|
|
|
+ if (isSavedNode) {
|
|
|
newCompleted.add(nodeDO.uuid);
|
|
|
newSavedStatusCache.set(nodeDO.uuid, true);
|
|
|
} else {
|
|
|
- // 节点未保存
|
|
|
newSavedStatusCache.set(nodeDO.uuid, false);
|
|
|
}
|
|
|
});
|
|
|
@@ -1993,68 +2058,7 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
}
|
|
|
}, [workJobStep, workflowWorkId]);
|
|
|
|
|
|
- // 验证节点配置是否完整
|
|
|
- const validateNodeConfig = useCallback((config: any, nodeType: string): boolean => {
|
|
|
- // 节点名称必填
|
|
|
- if (!config.nodeName || config.nodeName.trim() === '') {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // 业务表单必填(只有确认节点、审核节点、录入信息节点需要必填)
|
|
|
- const formRequiredNodeTypes = ['confirm', 'review', 'inputInfo'];
|
|
|
- if (formRequiredNodeTypes.includes(nodeType) && !config.submitForm) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // 负责人必填(创建作业、上锁/解锁/共锁类方案节点除外);方案节点负责人在隔离方式规则中校验
|
|
|
- if (
|
|
|
- nodeType !== 'createJob' &&
|
|
|
- !isWorkflowLockLikeValidationType(nodeType) &&
|
|
|
- !isWorkflowUnlockLikeValidationType(nodeType)
|
|
|
- ) {
|
|
|
- if (!config.responsible) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 共锁节点:仅校验关联的上锁任务(隔离方式等随上锁任务同步)
|
|
|
- if (isWorkflowCoLockType(nodeType)) {
|
|
|
- if (!config.isolationNodeUuid || config.isolationNodeUuid === '') {
|
|
|
- return false;
|
|
|
- }
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // 上锁节点特殊验证(不含共锁节点)
|
|
|
- if (isWorkflowLockSchemeType(nodeType)) {
|
|
|
- if (!config.isolationType || config.isolationType === '') {
|
|
|
- return false;
|
|
|
- }
|
|
|
- if (!config.isolationPoints || !Array.isArray(config.isolationPoints) || config.isolationPoints.length === 0) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- // 字典值:0=盲板,1=上锁挂牌,2=拆除
|
|
|
- if (config.isolationType === '0' || config.isolationType === '2') {
|
|
|
- if (!config.responsible) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- if (config.isolationType === '1') {
|
|
|
- if (!config.lockPerson) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 解锁/解除共锁节点特殊验证
|
|
|
- if (isWorkflowUnlockLikeValidationType(nodeType)) {
|
|
|
- if (!config.isolationNodeUuid || config.isolationNodeUuid === '') {
|
|
|
- return false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return true;
|
|
|
- }, []);
|
|
|
+ const validateNodeConfig = useCallback((config: any, nodeType: string) => validateWorkflowJobNodeConfig(config, nodeType), []);
|
|
|
|
|
|
// 获取下一个未完成的节点(基于 workflowJson 中的 adjacency 信息)
|
|
|
// 规则:
|
|
|
@@ -2223,13 +2227,47 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
}
|
|
|
}
|
|
|
const source = nodeData || node.data || {};
|
|
|
- // 解除类节点自身的 data(仅用于 nodeName、submitForm 保持本节点)
|
|
|
+ // 解除类节点自身的 data(节点名称、表单、共锁人等以本节点为准;隔离方式等仍从关联方案源取)
|
|
|
let releaseNodeData: any = {};
|
|
|
- if (isUnlockBranch && isolationNodeDO && nodeDO.data) {
|
|
|
+ if (isUnlockBranch && nodeDO.data) {
|
|
|
try {
|
|
|
releaseNodeData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
|
|
|
} catch (_) {}
|
|
|
}
|
|
|
+
|
|
|
+ const parseCoLockPersonIdsFromField = (raw: unknown): number[] => {
|
|
|
+ if (raw == null) return [];
|
|
|
+ let arr: unknown[] = [];
|
|
|
+ if (Array.isArray(raw)) arr = raw;
|
|
|
+ else if (typeof raw === 'string' && raw.trim()) {
|
|
|
+ try {
|
|
|
+ const p = JSON.parse(raw);
|
|
|
+ arr = Array.isArray(p) ? p : [];
|
|
|
+ } catch {
|
|
|
+ arr = [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return arr
|
|
|
+ .map((id) => (typeof id === 'number' ? id : Number(id)))
|
|
|
+ .filter((n) => !Number.isNaN(n));
|
|
|
+ };
|
|
|
+ const coLockPersonIdsFromOwnData = parseCoLockPersonIdsFromField(releaseNodeData?.coLockPersons);
|
|
|
+
|
|
|
+ const pickIsolationTypeString = (v: unknown): string => {
|
|
|
+ if (v === null || v === undefined || v === '') return '';
|
|
|
+ return String(v);
|
|
|
+ };
|
|
|
+ /** 父节点:接口顶层 isolationType 与 data JSON 内字段可能只填一处 */
|
|
|
+ const isolationTypeFromParent =
|
|
|
+ pickIsolationTypeString(dataSource.isolationType) || pickIsolationTypeString(source.isolationType);
|
|
|
+ /** 解锁/解除共锁本节点 data 里落库的隔离方式(优先于父节点,便于回显) */
|
|
|
+ const isolationTypeFromOwn = pickIsolationTypeString(releaseNodeData?.isolationType);
|
|
|
+
|
|
|
+ const parseLockPersonId = (raw: unknown): number | undefined => {
|
|
|
+ if (raw === null || raw === undefined || raw === '') return undefined;
|
|
|
+ const n = typeof raw === 'number' ? raw : Number(raw);
|
|
|
+ return Number.isNaN(n) ? undefined : n;
|
|
|
+ };
|
|
|
|
|
|
let lockPersonId: string | number = '';
|
|
|
const coLockPersonIds: (string | number)[] = [];
|
|
|
@@ -2261,16 +2299,40 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
}
|
|
|
|
|
|
const fromIsolationOnly = isUnlockBranch && isolationNodeDO;
|
|
|
- // 解除类节点且有关联方案源时:表单项仅从方案源(dataSource)取数,不兜底 source/node.data
|
|
|
- const isolationOnlyIsolationPoints = fromIsolationOnly && dataSource.isolationPoints
|
|
|
+
|
|
|
+ const parseIsolationPointsArray = (raw: unknown): string[] => {
|
|
|
+ if (raw == null) return [];
|
|
|
+ if (Array.isArray(raw)) return raw.map((x) => String(x));
|
|
|
+ if (typeof raw === 'string' && raw.trim()) {
|
|
|
+ try {
|
|
|
+ const p = JSON.parse(raw);
|
|
|
+ return Array.isArray(p) ? p.map((x: unknown) => String(x)) : [];
|
|
|
+ } catch {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ };
|
|
|
+
|
|
|
+ // 解除类且有关联方案源:隔离点优先父节点接口字段,否则父 data,否则本节点 data
|
|
|
+ const isolationOnlyIsolationPoints = fromIsolationOnly
|
|
|
? (() => {
|
|
|
- try {
|
|
|
- return typeof dataSource.isolationPoints === 'string'
|
|
|
- ? JSON.parse(dataSource.isolationPoints)
|
|
|
- : (Array.isArray(dataSource.isolationPoints) ? dataSource.isolationPoints : []);
|
|
|
- } catch (_) { return []; }
|
|
|
+ if (dataSource.isolationPoints) {
|
|
|
+ try {
|
|
|
+ return typeof dataSource.isolationPoints === 'string'
|
|
|
+ ? JSON.parse(dataSource.isolationPoints)
|
|
|
+ : Array.isArray(dataSource.isolationPoints)
|
|
|
+ ? dataSource.isolationPoints
|
|
|
+ : [];
|
|
|
+ } catch (_) {
|
|
|
+ return parseIsolationPointsArray(source.isolationPoints);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const fromParentData = parseIsolationPointsArray(source.isolationPoints);
|
|
|
+ if (fromParentData.length > 0) return fromParentData;
|
|
|
+ return parseIsolationPointsArray(releaseNodeData?.isolationPoints);
|
|
|
})()
|
|
|
- : (fromIsolationOnly ? [] : isolationPoints);
|
|
|
+ : isolationPoints;
|
|
|
const nodeConfig = {
|
|
|
nodeName: fromIsolationOnly
|
|
|
? (nodeDO.nodeName || releaseNodeData.label || node.data?.label || config?.label || '')
|
|
|
@@ -2285,12 +2347,33 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
submitForm: fromIsolationOnly
|
|
|
? (nodeDO.formId != null && nodeDO.formId !== '' ? (typeof nodeDO.formId === 'number' ? nodeDO.formId : Number(nodeDO.formId)) : (releaseNodeData.submitForm != null ? (typeof releaseNodeData.submitForm === 'number' ? releaseNodeData.submitForm : Number(releaseNodeData.submitForm)) : undefined))
|
|
|
: (dataSource.formId ? (typeof dataSource.formId === 'number' ? dataSource.formId : Number(dataSource.formId)) : (source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined)),
|
|
|
- isolationType: fromIsolationOnly ? (dataSource.isolationType != null && dataSource.isolationType !== undefined ? String(dataSource.isolationType) : '') : (dataSource.isolationType !== null && dataSource.isolationType !== undefined ? String(dataSource.isolationType) : (source.isolationType || '')),
|
|
|
+ isolationType: fromIsolationOnly
|
|
|
+ ? isolationTypeFromOwn || isolationTypeFromParent
|
|
|
+ : dataSource.isolationType !== null && dataSource.isolationType !== undefined
|
|
|
+ ? String(dataSource.isolationType)
|
|
|
+ : source.isolationType || '',
|
|
|
isolationPoints: fromIsolationOnly ? isolationOnlyIsolationPoints : isolationPoints,
|
|
|
isolationNode: source.isolationNode || [],
|
|
|
isolationNodeUuid: nodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
|
|
|
- lockPerson: fromIsolationOnly ? (lockPersonId || undefined) : (lockPersonId || (source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined)),
|
|
|
- coLockPersons: fromIsolationOnly ? coLockPersonIds : (coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [])),
|
|
|
+ lockPerson: fromIsolationOnly
|
|
|
+ ? parseLockPersonId(releaseNodeData?.lockPerson) ??
|
|
|
+ parseLockPersonId(lockPersonId) ??
|
|
|
+ parseLockPersonId(source.lockPerson)
|
|
|
+ : parseLockPersonId(lockPersonId) ??
|
|
|
+ parseLockPersonId(source.lockPerson) ??
|
|
|
+ parseLockPersonId(releaseNodeData?.lockPerson),
|
|
|
+ // 解除共锁/解锁:本节点 data 里的 coLockPersons(接口落库)优先于关联节点 nodeUserList,避免不回显
|
|
|
+ coLockPersons: fromIsolationOnly
|
|
|
+ ? coLockPersonIdsFromOwnData.length > 0
|
|
|
+ ? coLockPersonIdsFromOwnData
|
|
|
+ : coLockPersonIds
|
|
|
+ : coLockPersonIds.length > 0
|
|
|
+ ? coLockPersonIds
|
|
|
+ : coLockPersonIdsFromOwnData.length > 0
|
|
|
+ ? coLockPersonIdsFromOwnData
|
|
|
+ : source.coLockPersons && Array.isArray(source.coLockPersons)
|
|
|
+ ? source.coLockPersons.map((id: any) => (typeof id === 'number' ? id : Number(id)))
|
|
|
+ : [],
|
|
|
notificationMethods: resolveNotificationMethods(source, dataSource),
|
|
|
notificationPerson: source.notificationPerson || '',
|
|
|
notificationTime: fromIsolationOnly ? (dataSource.notifyTime ?? '') : (dataSource.notifyTime || source.notificationTime || ''),
|
|
|
@@ -2353,13 +2436,17 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
flushSync(() => {
|
|
|
setSelectedWorkflowNode(node);
|
|
|
});
|
|
|
- setWorkflowActiveTabKey('info');
|
|
|
}, [workflowNodeConfigCache, workflowWorkNodeDOList, nodeConfigs]);
|
|
|
|
|
|
// 画布点击事件(取消选择)
|
|
|
const onWorkflowPaneClick = useCallback(() => {
|
|
|
setSelectedWorkflowNode(null);
|
|
|
}, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!selectedWorkflowNode) return;
|
|
|
+ setWorkflowIconPickerOpen(false);
|
|
|
+ }, [selectedWorkflowNode?.id]);
|
|
|
|
|
|
// 实时更新节点显示(仅当当前 config 属于当前选中节点时才写回画布,避免切换节点时把上一节点的 config 误写到新节点)
|
|
|
useEffect(() => {
|
|
|
@@ -4854,16 +4941,13 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
)}
|
|
|
</div>
|
|
|
|
|
|
- {/* 右侧:配置面板;key 随选中节点变化以强制重挂载,避免切换节点后面板内容未刷新 */}
|
|
|
- <div className="w-80 border border-gray-200 rounded-lg bg-white overflow-y-auto flex-shrink-0">
|
|
|
+ {/* 右侧:配置面板纵向滚动(节点信息 / 提交表单 / 通知消息) */}
|
|
|
+ <div className="w-80 bg-gray-50 border-l border-gray-200 overflow-y-auto flex-shrink-0">
|
|
|
{selectedWorkflowNode ? (
|
|
|
<div key={selectedWorkflowNode.id} className="h-full flex flex-col">
|
|
|
- {/* 头部 */}
|
|
|
- <div className="px-4 py-2.5 bg-white border-b border-gray-200 flex items-center justify-between sticky top-0 z-10 shadow-sm">
|
|
|
- <h3 className="text-sm font-medium text-gray-900">
|
|
|
- {t('isolationWork.nodeSettingsTitle', {
|
|
|
- nodeName: workflowNodeConfig.nodeName || selectedWorkflowNode.data?.label || t('isolationWork.nodeDefaultName'),
|
|
|
- })}
|
|
|
+ <div className="p-4 bg-white border-b border-gray-200 flex items-center justify-between sticky top-0 z-10 shadow-sm">
|
|
|
+ <h3 className="text-base font-semibold text-gray-900">
|
|
|
+ {workflowNodeConfig.nodeName || selectedWorkflowNode.data?.label || t('isolationWork.nodeDefaultName')} 设置
|
|
|
</h3>
|
|
|
<button
|
|
|
onClick={() => setSelectedWorkflowNode(null)}
|
|
|
@@ -4874,7 +4958,6 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
- {/* 内容 */}
|
|
|
<div className="flex-1 p-4 overflow-y-auto space-y-6">
|
|
|
{/* 节点信息 */}
|
|
|
<div>
|
|
|
@@ -4883,70 +4966,258 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
{t('isolationWork.nodeInfo')}
|
|
|
</h4>
|
|
|
<div className="space-y-5">
|
|
|
- {/* 节点名称 */}
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
- {t('isolationWork.nodeName')} <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
|
|
|
- </label>
|
|
|
- <Input
|
|
|
- value={workflowNodeConfig.nodeName || ''}
|
|
|
- onChange={(e) =>
|
|
|
- setWorkflowNodeConfig({ ...workflowNodeConfig, nodeName: e.target.value })
|
|
|
- }
|
|
|
- placeholder={t('isolationWork.nodeNamePlaceholder')}
|
|
|
- className="rounded-lg border-gray-200"
|
|
|
- disabled={isViewMode}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 负责人 */}
|
|
|
- {selectedWorkflowNode.data?.type !== 'createJob' && !workflowSchemePanelOpen && (
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
- {t('isolationWork.responsible')} <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
|
|
|
- </label>
|
|
|
- <Select
|
|
|
- value={workflowNodeConfig.responsible || undefined}
|
|
|
- onChange={(value) =>
|
|
|
- setWorkflowNodeConfig({ ...workflowNodeConfig, responsible: value || undefined })
|
|
|
- }
|
|
|
- placeholder={t('isolationWork.responsiblePlaceholder')}
|
|
|
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
- allowClear
|
|
|
- disabled={isViewMode}
|
|
|
- >
|
|
|
- {workflowDrawerUsers.map(user => (
|
|
|
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
|
|
|
- ))}
|
|
|
- </Select>
|
|
|
- <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">
|
|
|
- {t('isolationWork.responsibleHelp')}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {/* 备注 */}
|
|
|
- {selectedWorkflowNode.data?.type !== 'createJob' && selectedWorkflowNode.data?.type !== 'confirm' && selectedWorkflowNode.data?.type !== 'review' && selectedWorkflowNode.data?.type !== 'inputInfo' && !workflowSchemePanelOpen && selectedWorkflowNode.data?.type !== 'returnLock' && selectedWorkflowNode.data?.type !== 'complete' && (
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
- 备注
|
|
|
- </label>
|
|
|
- <Input.TextArea
|
|
|
- value={workflowNodeConfig.remark || ''}
|
|
|
- onChange={(e) =>
|
|
|
- setWorkflowNodeConfig({ ...workflowNodeConfig, remark: e.target.value })
|
|
|
- }
|
|
|
- placeholder="请输入备注"
|
|
|
- rows={3}
|
|
|
- className="rounded-lg border-gray-200"
|
|
|
- disabled={isViewMode}
|
|
|
- />
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ {(selectedWorkflowNode.data?.type === 'createJob' ||
|
|
|
+ selectedWorkflowNode.data?.type === 'confirm' ||
|
|
|
+ selectedWorkflowNode.data?.type === 'review' ||
|
|
|
+ selectedWorkflowNode.data?.type === 'inputInfo' ||
|
|
|
+ workflowSchemePanelOpen ||
|
|
|
+ selectedWorkflowNode.data?.type === 'returnLock' ||
|
|
|
+ selectedWorkflowNode.data?.type === 'complete') && (
|
|
|
+ <div className="text-xs text-gray-500 leading-relaxed mb-2">
|
|
|
+ {getWorkflowNodeDescription(selectedWorkflowNode.data?.type || '')}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
+ {t('isolationWork.nodeName')}{' '}
|
|
|
+ <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
|
|
|
+ {selectedWorkflowNode.data?.type && (
|
|
|
+ <span className="text-gray-500 font-normal ml-2">({selectedWorkflowNode.data.type})</span>
|
|
|
+ )}
|
|
|
+ </label>
|
|
|
+ {selectedWorkflowNode.data?.type !== 'createJob' &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'confirm' && (
|
|
|
+ <p className="text-xs text-gray-500 mb-2.5 leading-relaxed">
|
|
|
+ (默认名称:{' '}
|
|
|
+ {nodeConfigs.find((c) => c.type === resolveWorkflowPaletteType(selectedWorkflowNode.data?.type))
|
|
|
+ ?.label || '节点名称'}
|
|
|
+ ,可根据需求调整)
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ {selectedWorkflowNode.data?.type === 'createJob' && (
|
|
|
+ <p className="text-xs text-gray-500 mb-2.5 leading-relaxed">
|
|
|
+ (默认名称:{' '}
|
|
|
+ {nodeConfigs.find((c) => c.type === resolveWorkflowPaletteType(selectedWorkflowNode.data?.type))
|
|
|
+ ?.label || '节点名称'}
|
|
|
+ ,可根据需求调整)
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ <Input
|
|
|
+ value={workflowNodeConfig.nodeName || ''}
|
|
|
+ onChange={(e) =>
|
|
|
+ setWorkflowNodeConfig({ ...workflowNodeConfig, nodeName: e.target.value })
|
|
|
+ }
|
|
|
+ placeholder={t('isolationWork.nodeNamePlaceholder')}
|
|
|
+ className="rounded-lg border-gray-200 h-10"
|
|
|
+ disabled={isViewMode}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-2">显示图标</label>
|
|
|
+ <Popover
|
|
|
+ content={
|
|
|
+ <div style={{ width: '400px', maxHeight: '600px', overflowY: 'auto', padding: '12px' }}>
|
|
|
+ {['开始', '确认', '审核', '录入', '能量隔离', '解除隔离', '结束'].map((category) => {
|
|
|
+ const iconPaths = generateIconPaths(category);
|
|
|
+ return (
|
|
|
+ <div key={category} className="mb-6">
|
|
|
+ <div className="flex items-center mb-3">
|
|
|
+ <div className="w-1 h-5 bg-blue-500 rounded-full" style={{ minWidth: '4px', marginRight: '8px' }} />
|
|
|
+ <span className="text-sm font-bold text-gray-800">{category}</span>
|
|
|
+ </div>
|
|
|
+ {iconPaths.length > 0 ? (
|
|
|
+ <div className="grid grid-cols-5 gap-3" style={{ gridTemplateColumns: 'repeat(5, 1fr)' }}>
|
|
|
+ {iconPaths.map((iconItem) => {
|
|
|
+ const isSelected = workflowNodeConfig.nodeIcon === iconItem.fileName;
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ key={iconItem.id}
|
|
|
+ onClick={() => {
|
|
|
+ if (isViewMode) return;
|
|
|
+ setWorkflowNodeConfig({ ...workflowNodeConfig, nodeIcon: iconItem.fileName });
|
|
|
+ setWorkflowIconPickerOpen(false);
|
|
|
+ setWorkflowNodes((nds) =>
|
|
|
+ nds.map((node) =>
|
|
|
+ node.id === selectedWorkflowNode.id
|
|
|
+ ? {
|
|
|
+ ...node,
|
|
|
+ data: { ...node.data, icon: iconItem.fileName },
|
|
|
+ }
|
|
|
+ : node
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ className={`rounded-lg border-2 cursor-pointer transition-all flex items-center justify-center overflow-hidden hover:border-blue-400 hover:bg-blue-50 ${
|
|
|
+ isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white'
|
|
|
+ }`}
|
|
|
+ style={{ width: '25px', height: '25px' }}
|
|
|
+ title={category}
|
|
|
+ >
|
|
|
+ <img src={iconItem.path} alt="" className="w-6 h-6 object-contain" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="text-sm text-gray-500 py-4 text-center">该分类暂无图标</div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ trigger="click"
|
|
|
+ open={workflowIconPickerOpen}
|
|
|
+ onOpenChange={setWorkflowIconPickerOpen}
|
|
|
+ placement="left"
|
|
|
+ >
|
|
|
+ <div className="border border-gray-200 rounded-lg p-3 bg-white flex items-center gap-3 cursor-pointer hover:border-blue-400 transition-colors shadow-sm">
|
|
|
+ {(() => {
|
|
|
+ if (workflowNodeConfig.nodeIcon && /^\d+\.png$/.test(workflowNodeConfig.nodeIcon)) {
|
|
|
+ const iconPath = getIconPathByFileName(workflowNodeConfig.nodeIcon);
|
|
|
+ if (iconPath) {
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <div className="w-12 h-12 rounded-lg border border-gray-200 bg-white flex items-center justify-center overflow-hidden" style={{ borderRadius: '12px' }}>
|
|
|
+ <img src={iconPath} alt="" className="w-full h-full object-contain" />
|
|
|
+ </div>
|
|
|
+ <span className="text-sm text-gray-600">选择图标</span>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const iconType = selectedWorkflowNode.data?.type;
|
|
|
+ const cfg = nodeConfigs.find((c) => c.type === resolveWorkflowPaletteType(iconType));
|
|
|
+ const IconComp = cfg?.icon || FileTextOutlined;
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className={`${cfg?.bgColor || 'bg-gray-50'} ${cfg?.borderColor || 'border-gray-100'} border p-3 rounded-xl shadow-sm flex items-center justify-center`}
|
|
|
+ style={{ borderRadius: '12px' }}
|
|
|
+ >
|
|
|
+ <IconComp className={`${cfg?.iconColor || 'text-gray-600'} text-lg`} style={{ color: cfg?.iconColorCustom || undefined }} />
|
|
|
+ </div>
|
|
|
+ <span className="text-sm text-gray-600">选择图标</span>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ })()}
|
|
|
+ </div>
|
|
|
+ </Popover>
|
|
|
+ </div>
|
|
|
+ {selectedWorkflowNode.data?.type === 'confirm' && (
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-2">{t('isolationWork.responsible')}</label>
|
|
|
+ <Select
|
|
|
+ value={
|
|
|
+ workflowNodeConfig.responsible !== undefined && workflowNodeConfig.responsible !== null && workflowNodeConfig.responsible !== ''
|
|
|
+ ? String(workflowNodeConfig.responsible)
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ onChange={(value) =>
|
|
|
+ setWorkflowNodeConfig({
|
|
|
+ ...workflowNodeConfig,
|
|
|
+ responsible: value !== undefined && value !== null && value !== '' ? Number(value) : undefined,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ placeholder={t('isolationWork.responsiblePlaceholder')}
|
|
|
+ className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
+ allowClear
|
|
|
+ disabled={isViewMode}
|
|
|
+ >
|
|
|
+ {workflowDrawerUsers.map((user) => (
|
|
|
+ <Select.Option key={user.id} value={String(user.id)}>
|
|
|
+ {user.nickname || user.username}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">{t('isolationWork.responsibleHelp')}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {selectedWorkflowNode.data?.type === 'review' && (
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-2">{t('isolationWork.responsible')}</label>
|
|
|
+ <Select
|
|
|
+ value={
|
|
|
+ workflowNodeConfig.responsible !== undefined && workflowNodeConfig.responsible !== null && workflowNodeConfig.responsible !== ''
|
|
|
+ ? String(workflowNodeConfig.responsible)
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ onChange={(value) =>
|
|
|
+ setWorkflowNodeConfig({
|
|
|
+ ...workflowNodeConfig,
|
|
|
+ responsible: value !== undefined && value !== null && value !== '' ? Number(value) : undefined,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ placeholder={t('isolationWork.responsiblePlaceholder')}
|
|
|
+ className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
+ allowClear
|
|
|
+ disabled={isViewMode}
|
|
|
+ >
|
|
|
+ {workflowDrawerUsers.map((user) => (
|
|
|
+ <Select.Option key={user.id} value={String(user.id)}>
|
|
|
+ {user.nickname || user.username}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">{t('isolationWork.responsibleHelp')}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {selectedWorkflowNode.data?.type !== 'createJob' &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'confirm' &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'review' &&
|
|
|
+ !workflowSchemePanelOpen && (
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-2">{t('isolationWork.responsible')}</label>
|
|
|
+ <Select
|
|
|
+ value={
|
|
|
+ workflowNodeConfig.responsible !== undefined && workflowNodeConfig.responsible !== null && workflowNodeConfig.responsible !== ''
|
|
|
+ ? String(workflowNodeConfig.responsible)
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ onChange={(value) =>
|
|
|
+ setWorkflowNodeConfig({
|
|
|
+ ...workflowNodeConfig,
|
|
|
+ responsible: value !== undefined && value !== null && value !== '' ? Number(value) : undefined,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ placeholder={t('isolationWork.responsiblePlaceholder')}
|
|
|
+ className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
+ allowClear
|
|
|
+ disabled={isViewMode}
|
|
|
+ >
|
|
|
+ {workflowDrawerUsers.map((user) => (
|
|
|
+ <Select.Option key={user.id} value={String(user.id)}>
|
|
|
+ {user.nickname || user.username}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">{t('isolationWork.responsibleHelp')}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {selectedWorkflowNode.data?.type !== 'createJob' &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'confirm' &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'review' &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'inputInfo' &&
|
|
|
+ !workflowSchemePanelOpen &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'returnLock' &&
|
|
|
+ selectedWorkflowNode.data?.type !== 'complete' && (
|
|
|
+ <div>
|
|
|
+ <label className="block text-sm font-medium text-gray-700 mb-2">备注</label>
|
|
|
+ <Input.TextArea
|
|
|
+ value={workflowNodeConfig.remark || ''}
|
|
|
+ onChange={(e) =>
|
|
|
+ setWorkflowNodeConfig({ ...workflowNodeConfig, remark: e.target.value })
|
|
|
+ }
|
|
|
+ placeholder="请输入备注"
|
|
|
+ rows={3}
|
|
|
+ className="rounded-lg border-gray-200"
|
|
|
+ disabled={isViewMode}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- {/* 提交表单 - 创建作业节点不显示 */}
|
|
|
{selectedWorkflowNode.data?.type !== 'createJob' && (
|
|
|
<div>
|
|
|
<h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
|
|
|
@@ -4956,26 +5227,58 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
<div className="space-y-5">
|
|
|
<div>
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
- {t('isolationWork.businessForm')}
|
|
|
- {/* 只有确认节点、审核节点、录入信息节点显示必填标记 */}
|
|
|
+ {t('isolationWork.businessForm')}
|
|
|
{['confirm', 'review', 'inputInfo'].includes(selectedWorkflowNode.data?.type || '') && (
|
|
|
<span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
|
|
|
)}
|
|
|
</label>
|
|
|
- <Select
|
|
|
- value={workflowNodeConfig.submitForm || undefined}
|
|
|
- onChange={(value) =>
|
|
|
- setWorkflowNodeConfig({ ...workflowNodeConfig, submitForm: value || undefined })
|
|
|
- }
|
|
|
- placeholder={t('isolationWork.businessFormPlaceholder')}
|
|
|
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
- allowClear
|
|
|
- disabled={isViewMode || workflowUnlockSideReadonly}
|
|
|
- >
|
|
|
- {workflowFormList.map(form => (
|
|
|
- <Select.Option key={form.id} value={form.id}>{form.name}</Select.Option>
|
|
|
- ))}
|
|
|
- </Select>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Select
|
|
|
+ value={workflowNodeConfig.submitForm || undefined}
|
|
|
+ onChange={(value) =>
|
|
|
+ setWorkflowNodeConfig({ ...workflowNodeConfig, submitForm: value || undefined })
|
|
|
+ }
|
|
|
+ placeholder={t('isolationWork.businessFormPlaceholder')}
|
|
|
+ className="flex-1 [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
|
|
|
+ allowClear
|
|
|
+ disabled={isViewMode || workflowUnlockSideReadonly}
|
|
|
+ >
|
|
|
+ {workflowFormList.map((form) => (
|
|
|
+ <Select.Option key={form.id} value={form.id}>
|
|
|
+ {form.name}
|
|
|
+ </Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ <Button
|
|
|
+ icon={<EyeOutlined />}
|
|
|
+ className="flex-shrink-0"
|
|
|
+ loading={workflowFormPreviewLoading}
|
|
|
+ disabled={!workflowNodeConfig.submitForm || isViewMode}
|
|
|
+ onClick={async () => {
|
|
|
+ if (!workflowNodeConfig.submitForm) {
|
|
|
+ message.warning('请先选择表单');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setWorkflowFormPreviewLoading(true);
|
|
|
+ try {
|
|
|
+ const formResponse = await getForm(Number(workflowNodeConfig.submitForm));
|
|
|
+ let formDataObj: any = formResponse;
|
|
|
+ if (formResponse && typeof formResponse === 'object' && 'data' in formResponse) {
|
|
|
+ formDataObj = (formResponse as any).data || formResponse;
|
|
|
+ }
|
|
|
+ setWorkflowFormPreviewData(formDataObj);
|
|
|
+ setConfAndFields2(setWorkflowFormPreviewDetail, formDataObj.conf, formDataObj.fields);
|
|
|
+ setWorkflowFormPreviewVisible(true);
|
|
|
+ } catch (error: any) {
|
|
|
+ message.error(error?.message || '获取表单详情失败');
|
|
|
+ } finally {
|
|
|
+ setWorkflowFormPreviewLoading(false);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 预览
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
{/* 上锁/解锁/共锁/解除共锁 节点特有的字段 */}
|
|
|
@@ -5541,7 +5844,6 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {/* 通知消息 - 创建作业节点不显示 */}
|
|
|
{selectedWorkflowNode.data?.type !== 'createJob' && (
|
|
|
<div>
|
|
|
<h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
|
|
|
@@ -5751,7 +6053,10 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
}
|
|
|
|
|
|
// 验证当前节点配置
|
|
|
- const isValid = validateNodeConfig(workflowNodeConfig, selectedWorkflowNode.data?.type || '');
|
|
|
+ const isValid = validateNodeConfig(
|
|
|
+ workflowNodeConfig,
|
|
|
+ resolveWorkflowPaletteType(selectedWorkflowNode.data?.type || '')
|
|
|
+ );
|
|
|
if (!isValid) {
|
|
|
message.warning(t('common.pleaseCompleteRequiredNodeConfig'));
|
|
|
return;
|
|
|
@@ -7474,6 +7779,42 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
|
|
|
</div>
|
|
|
</div>
|
|
|
</Modal>
|
|
|
+
|
|
|
+ <Modal
|
|
|
+ open={workflowFormPreviewVisible}
|
|
|
+ title={`预览表单 - ${workflowFormPreviewData?.name || ''}`}
|
|
|
+ onCancel={() => {
|
|
|
+ setWorkflowFormPreviewVisible(false);
|
|
|
+ setWorkflowFormPreviewData(null);
|
|
|
+ setWorkflowFormPreviewDetail({ rule: [], option: {} });
|
|
|
+ }}
|
|
|
+ footer={[
|
|
|
+ <Button
|
|
|
+ key="close"
|
|
|
+ onClick={() => {
|
|
|
+ setWorkflowFormPreviewVisible(false);
|
|
|
+ setWorkflowFormPreviewData(null);
|
|
|
+ setWorkflowFormPreviewDetail({ rule: [], option: {} });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 关闭
|
|
|
+ </Button>,
|
|
|
+ ]}
|
|
|
+ width={720}
|
|
|
+ style={{ top: 20 }}
|
|
|
+ >
|
|
|
+ <div className="max-h-[70vh] overflow-y-auto text-sm text-gray-700 space-y-1 p-1">
|
|
|
+ {(workflowFormPreviewDetail.rule || []).length === 0 ? (
|
|
|
+ <div className="text-gray-500">暂无字段</div>
|
|
|
+ ) : (
|
|
|
+ (workflowFormPreviewDetail.rule || []).map((field: any, idx: number) => (
|
|
|
+ <div key={String(field.id ?? field.name ?? field.field ?? `f-${idx}`)}>
|
|
|
+ {field.label || field.title || field.name || field.field || field.type}
|
|
|
+ </div>
|
|
|
+ ))
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
</div>
|
|
|
);
|
|
|
}
|