Procházet zdrojové kódy

作业管理页面流程管理中内容与心拆分后的节点内容保持一致

wyn před 1 týdnem
rodič
revize
79e9c61a16
2 změnil soubory, kde provedl 518 přidání a 338 odebrání
  1. 516 175
      src/components/IsolationWork.tsx
  2. 2 163
      src/components/ProcessDesigner.tsx

+ 516 - 175
src/components/IsolationWork.tsx

@@ -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>
   );
 }

+ 2 - 163
src/components/ProcessDesigner.tsx

@@ -158,6 +158,7 @@ import {
   isWorkflowUnlockSchemeType,
   resolveWorkflowPaletteType,
 } from '../utils/workflowNodeTypes';
+import { generateIconPaths, getIconPathByFileName, getWorkflowNodeDescription } from '../utils/workflowNodePanelUi';
 
 /** 抄送部门/人员 ID:支持数组、逗号分隔字符串、JSON 数组字符串,兼容旧 cc* 单选 */
 function parseWorkflowCopyIds(multi: unknown, legacySingle: unknown): string[] {
@@ -890,149 +891,6 @@ const edgeTypes: EdgeTypes = {
 };
 
 
-// 节点类型到图标分类的映射
-const getIconCategoryByNodeType = (nodeType: string): string => {
-  const typeMap: Record<string, string> = {
-    'createJob': '开始',
-    'confirm': '确认',
-    'review': '审核',
-    'inputInfo': '录入',
-    'lock': '能量隔离',
-    'unlock': '解除隔离',
-    'coLock': '能量隔离',
-    'unlockCoLock': '解除隔离',
-    'isolation': '能量隔离',
-    'releaseIsolation': '解除隔离',
-    'returnLock': '结束',
-    'complete': '结束',
-  };
-  return typeMap[nodeType] || '开始';
-};
-
-// 图标分类配置(分类名称 -> 图标文件列表)
-const iconCategories: Record<string, { start: number; end: number }> = {
-  '审核': { start: 1000, end: 1011 },
-  '开始': { start: 2000, end: 2016 },
-  '录入': { start: 3000, end: 3028 },
-  '确认': { start: 4000, end: 4024 },
-  '结束': { start: 5000, end: 5018 },
-  '能量隔离': { start: 6000, end: 6027 },
-  '解除隔离': { start: 7000, end: 7021 },
-};
-
-// 使用 import.meta.glob 动态导入所有图标
-const iconModules = import.meta.glob('../assets/节点图标/**/*.png', { eager: true, as: 'url' });
-
-// 调试:打印加载的图标模块信息
-const allIconKeys = Object.keys(iconModules);
-console.log('ProcessDesigner: 图标模块加载情况');
-console.log('总数量:', allIconKeys.length);
-if (allIconKeys.length > 0) {
-  console.log('前5个路径示例:', allIconKeys.slice(0, 5));
-  // 打印一个完整的路径示例
-  const firstKey = allIconKeys[0];
-  console.log('第一个路径完整信息:', {
-    key: firstKey,
-    value: iconModules[firstKey],
-    normalized: firstKey.replace(/\\/g, '/')
-  });
-} else {
-  console.error('ProcessDesigner: 未加载到任何图标模块!');
-  console.error('请检查路径: ../assets/节点图标/**/*.png');
-  console.error('提示: 如果路径正确,可能需要重启开发服务器');
-}
-
-// 生成图标路径列表(返回文件名和路径的映射)
-const generateIconPaths = (category: string): Array<{ id: string; fileName: string; path: string }> => {
-  const config = iconCategories[category];
-  if (!config) {
-    console.warn(`ProcessDesigner: 分类 ${category} 没有配置`);
-    return [];
-  }
-  
-  const paths: Array<{ id: string; fileName: string; path: string }> = [];
-  
-  // 如果 import.meta.glob 加载成功,使用它
-  const allKeys = Object.keys(iconModules);
-  
-  if (allKeys.length > 0) {
-    // 使用 import.meta.glob 的结果
-    for (let i = config.start; i <= config.end; i++) {
-      const fileName = `${i}.png`;
-      const matchingKey = allKeys.find(k => {
-        const normalizedKey = k.replace(/\\/g, '/').toLowerCase();
-        const normalizedCategory = category.toLowerCase();
-        const normalizedFileName = fileName.toLowerCase();
-        return normalizedKey.includes(normalizedCategory) && normalizedKey.endsWith(normalizedFileName);
-      });
-      
-      if (matchingKey) {
-        const iconPath = iconModules[matchingKey] as string;
-        paths.push({ id: `${category}_${i}`, fileName: fileName, path: iconPath });
-      }
-    }
-  } else {
-    // Fallback: 使用 new URL 动态构建路径(适用于开发环境)
-    console.warn('ProcessDesigner: import.meta.glob 未加载成功,使用 fallback 方式');
-    for (let i = config.start; i <= config.end; i++) {
-      try {
-        // 使用 new URL 构建路径
-        const fileName = `${i}.png`;
-        const iconPath = new URL(`../assets/节点图标/${category}/${fileName}`, import.meta.url).href;
-        paths.push({ id: `${category}_${i}`, fileName: fileName, path: iconPath });
-      } catch (e) {
-        console.warn(`ProcessDesigner: 无法构建图标路径 ${category}/${i}.png:`, e);
-      }
-    }
-  }
-  
-  return paths;
-};
-
-// 根据文件名获取图标路径(用于节点显示)
-const getIconPathByFileName = (fileName: string | undefined): string | null => {
-  if (!fileName) return null;
-  
-  // 如果已经是完整路径,尝试提取文件名
-  let actualFileName = fileName;
-  if (fileName.includes('/') || fileName.includes('\\')) {
-    // 从路径中提取文件名
-    const pathParts = fileName.replace(/\\/g, '/').split('/');
-    actualFileName = pathParts[pathParts.length - 1];
-  }
-  
-  // 从文件名中提取数字和分类
-  const match = actualFileName.match(/^(\d+)\.png$/);
-  if (!match) return null;
-  
-  const iconNumber = parseInt(match[1], 10);
-  
-  // 根据数字范围判断分类
-  let category = '';
-  if (iconNumber >= 1000 && iconNumber <= 1011) category = '审核';
-  else if (iconNumber >= 2000 && iconNumber <= 2016) category = '开始';
-  else if (iconNumber >= 3000 && iconNumber <= 3028) category = '录入';
-  else if (iconNumber >= 4000 && iconNumber <= 4024) category = '确认';
-  else if (iconNumber >= 5000 && iconNumber <= 5018) category = '结束';
-  else if (iconNumber >= 6000 && iconNumber <= 6027) category = '能量隔离';
-  else if (iconNumber >= 7000 && iconNumber <= 7021) category = '解除隔离';
-  
-  if (!category) return null;
-  
-  // 查找对应的路径
-  const allKeys = Object.keys(iconModules);
-  const matchingKey = allKeys.find(k => {
-    const normalizedKey = k.replace(/\\/g, '/').toLowerCase();
-    return normalizedKey.includes(category.toLowerCase()) && normalizedKey.endsWith(actualFileName.toLowerCase());
-  });
-  
-  if (matchingKey) {
-    return iconModules[matchingKey] as string;
-  }
-  
-  return null;
-};
-
 // 从图标路径或文件名中提取文件名(用于保存JSON)
 const extractIconFileName = (icon: string | undefined): string | undefined => {
   if (!icon) return undefined;
@@ -3331,25 +3189,6 @@ export default function ProcessDesigner() {
     }
   }, [importJson, setNodes, setEdges, saveNodeCache, loadNodeCache, selectedNode, nodeConfigs, setNodeConfig, setSelectedNode, history, setHistory, setHistoryIndex]);
 
-  // 获取节点描述
-  const getNodeDescription = (type: string) => {
-    const descriptions: { [key: string]: string } = {
-      createJob: '该节点为作业创建人员创建作业录入信息开始节点。',
-      confirm: '该节点为作业确认,为"上一节点"操作分配确认模式及确认人员权限',
-      review: '该节点为作业审核,为"上一节点"操作分配审核模式及审核人员权限',
-      inputInfo: '该节点为作业录入提交,可提交信息或图片,主要为"信息确认"',
-      lock: '该节点为作业隔离类型选择,主要包括盲板,上锁挂牌,拆除等。',
-      unlock: '该节点在解锁时与所选上锁节点保持隔离方式等信息一致。',
-      coLock: '该节点需选择流程中的上锁任务并配置共锁人;隔离信息随所选上锁任务一致。',
-      unlockCoLock: '该节点在解除共锁时与所选共锁节点保持隔离方式等信息一致。',
-      isolation: '该节点为作业隔离类型选择,主要包括盲板,上锁挂牌,拆除等。',
-      releaseIsolation: '该节点为作业隔离类型选择,主要包括盲板,上锁挂牌,拆除等。',
-      returnLock: '该节点为还锁操作,归还钥匙,确认隔离操作完成',
-      complete: '该节点为流程结束点',
-    };
-    return descriptions[type] || '该节点的功能描述。';
-  };
-
   const selectedDataType = selectedNode?.data?.type || '';
   const isUnlockDesignerReadOnly =
     isWorkflowUnlockSchemeType(selectedDataType) || isWorkflowUnlockCoLockType(selectedDataType);
@@ -3590,7 +3429,7 @@ export default function ProcessDesigner() {
                             {/* 描述 - 创建作业、确认、审核、录入信息、上锁/解锁/共锁类节点、还锁与结束节点显示在节点名称顶部 */}
                             {(selectedNode.data?.type === 'createJob' || selectedNode.data?.type === 'confirm' || selectedNode.data?.type === 'review' || selectedNode.data?.type === 'inputInfo' || isSchemeDesignerPanel || selectedNode.data?.type === 'returnLock' || selectedNode.data?.type === 'complete') && (
                               <div className="text-xs text-gray-500 leading-relaxed mb-2">
-                                {getNodeDescription(selectedNode.data?.type || '')}
+                                {getWorkflowNodeDescription(selectedNode.data?.type || '')}
                               </div>
                             )}
                             <label className="block text-sm font-medium text-gray-700 mb-2">