Ver código fonte

修改抄送人和抄送部门

pm 1 mês atrás
pai
commit
5a8b0d45c4
4 arquivos alterados com 511 adições e 74 exclusões
  1. 6 12
      src/Dashboard.tsx
  2. 20 2
      src/api/WorkJob.ts
  3. 302 58
      src/components/IsolationWork.tsx
  4. 183 2
      src/components/ProcessDesigner.tsx

+ 6 - 12
src/Dashboard.tsx

@@ -19,6 +19,7 @@ import { authApi } from './api';
 import { toast } from 'sonner';
 import { Toaster } from 'sonner';
 import { env } from './utils/env';
+import { clearAuth, stopTokenCheck } from './utils/auth';
 import { getMenus, hasMenuPathPermission, mapMenuPathToKey, getPermissionUser, hasRole } from './utils/permission';
 import { Dropdown } from 'antd';
 import type { MenuProps } from 'antd';
@@ -132,22 +133,15 @@ export default function Dashboard() {
   const handleLogout = async () => {
     try {
       await authApi.logout();
-      localStorage.removeItem('token');
-      localStorage.removeItem('tenant');
-      // 清除所有菜单相关的 sessionStorage
-      sessionStorage.removeItem('lastActiveMenu');
-      sessionStorage.removeItem('navigateToMenu');
-      sessionStorage.removeItem('cabinetDetailSource');
+      // 统一清理认证信息(包含 REFRESH_TOKEN),避免退出后继续触发 refresh-token 请求
+      clearAuth();
+      stopTokenCheck();
       // 退出登录不再弹出全局成功提示(避免频繁/停留过久影响体验)
       navigate('/login');
     } catch (error: any) {
       // 即使API失败也清除本地数据并跳转
-      localStorage.removeItem('token');
-      localStorage.removeItem('tenant');
-      // 清除所有菜单相关的 sessionStorage
-      sessionStorage.removeItem('lastActiveMenu');
-      sessionStorage.removeItem('navigateToMenu');
-      sessionStorage.removeItem('cabinetDetailSource');
+      clearAuth();
+      stopTokenCheck();
       navigate('/login');
     }
   };

+ 20 - 2
src/api/WorkJob.ts

@@ -129,9 +129,23 @@ export const workJobApi = {
     return axiosInstance.delete(`/iscs/workflow-work/deleteWorkflowWorkList?ids=${idsStr}`);
   },
   
-  // 更新作业节点
+  // 更新作业节点(copyDeptIds/copyUserIds 后端要求逗号分隔字符串,禁止 JSON 数组)
   updateWorkflowWorkNode: (data: UpdateWorkflowWorkNodeParam) => {
-    return axiosInstance.put('/iscs/workflow-work-node/updateWorkflowWorkNode', data);
+    const payload: Record<string, unknown> = { ...data };
+    const toCommaString = (v: unknown): string => {
+      if (Array.isArray(v)) {
+        return v.map((x) => String(x).trim()).filter((s) => s !== '').join(',');
+      }
+      if (v == null || v === '') return '';
+      return String(v).trim();
+    };
+    if ('copyDeptIds' in payload) {
+      payload.copyDeptIds = toCommaString(payload.copyDeptIds);
+    }
+    if ('copyUserIds' in payload) {
+      payload.copyUserIds = toCommaString(payload.copyUserIds);
+    }
+    return axiosInstance.put('/iscs/workflow-work-node/updateWorkflowWorkNode', payload as UpdateWorkflowWorkNodeParam);
   },
   
   // 发布作业
@@ -154,6 +168,10 @@ export interface UpdateWorkflowWorkNodeParam {
   lockPerson?: string;
   /** 共锁人 userId 列表(字符串),从 nodeUserDOList 中 type===jtcolocker 的项取,如 "[181,182]" */
   colockPersons?: string;
+  /** 抄送部门 ID,多选时为逗号分隔字符串,如 "1,2,3" */
+  copyDeptIds?: string;
+  /** 抄送人用户 ID,多选时为逗号分隔字符串,如 "380,381" */
+  copyUserIds?: string;
   [key: string]: any;
 }
 

+ 302 - 58
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, 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 } from 'antd';
 import { Button as UIButton } from './ui/button';
 import type { ColumnsType } from 'antd/es/table';
 import { workflowDesignApi, WorkflowDesignVO } from '../api/WorkflowDesign';
@@ -42,6 +42,7 @@ import {
   WarningOutlined,
 } 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 urgecy1Icon from '../assets/urgecy1.png';
@@ -57,6 +58,59 @@ interface IsolationWorkProps {
   subMenu: string;
 }
 
+/** 抄送部门/人员 ID:支持数组、逗号分隔字符串、JSON 数组字符串,兼容旧 cc* 单选 */
+function parseWorkflowCopyIds(multi: unknown, legacySingle: unknown): string[] {
+  const pushValid = (out: string[], x: unknown) => {
+    if (x !== undefined && x !== null && x !== '') out.push(String(x));
+  };
+  if (Array.isArray(multi)) {
+    const out: string[] = [];
+    multi.forEach((x) => pushValid(out, x));
+    return out;
+  }
+  if (typeof multi === 'string' && multi.trim()) {
+    try {
+      const parsed = JSON.parse(multi);
+      if (Array.isArray(parsed)) return parseWorkflowCopyIds(parsed, undefined);
+      if (parsed !== null && parsed !== undefined && parsed !== '') {
+        return [String(parsed)];
+      }
+    } catch {
+      /* 非 JSON:逗号分隔 ID,如 "380,381" */
+      return multi
+        .split(',')
+        .map((s) => s.trim())
+        .filter((s) => s !== '');
+    }
+  }
+  if (legacySingle !== undefined && legacySingle !== null && legacySingle !== '') {
+    return [String(legacySingle)];
+  }
+  return [];
+}
+
+function workflowCopyDeptIds(nodeData: any, nodeDO?: any): string[] {
+  return parseWorkflowCopyIds(
+    nodeData?.copyDeptIds ?? nodeDO?.copyDeptIds,
+    nodeData?.ccDeptId ?? nodeDO?.ccDeptId
+  );
+}
+
+function workflowCopyUserIds(nodeData: any, nodeDO?: any): string[] {
+  return parseWorkflowCopyIds(
+    nodeData?.copyUserIds ?? nodeDO?.copyUserIds,
+    nodeData?.ccUserId ?? nodeDO?.ccUserId
+  );
+}
+
+/** 更新作业节点接口要求 copyDeptIds/copyUserIds 为逗号分隔字符串,不能为 JSON 数组 */
+function workflowCopyIdsToApiCommaString(ids: readonly (string | number)[]): string {
+  return ids
+    .map((id) => String(id).trim())
+    .filter((s) => s !== '')
+    .join(',');
+}
+
 // 节点配置(从ProcessDesigner复制)
 const nodeConfigs = [
   {
@@ -419,6 +473,8 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
     messageTemplateCode: 'false',
     emailTemplateCode: 'false',
     appTemplateCode: 'false',
+    copyDeptIds: [] as string[],
+    copyUserIds: [] as string[],
   });
   
   // 节点配置缓存(Map<nodeId, config>)
@@ -458,8 +514,97 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
   const [workflowDrawerUsers, setWorkflowDrawerUsers] = useState<UserVO[]>([]);
   const [workflowLockerUsers, setWorkflowLockerUsers] = useState<UserVO[]>([]);
   const [workflowColockerUsers, setWorkflowColockerUsers] = useState<UserVO[]>([]);
+  const [workflowCcDeptList, setWorkflowCcDeptList] = useState<DeptVO[]>([]);
+  const [workflowCcUserList, setWorkflowCcUserList] = useState<UserVO[]>([]);
+  const workflowCcDeptTreeData = useMemo(() => {
+    const nodeMap = new Map<string, any>();
+    const roots: any[] = [];
+    workflowCcDeptList.forEach((dept) => {
+      const id = String(dept.id);
+      nodeMap.set(id, {
+        key: `dept-${id}`,
+        value: id,
+        title: dept.name,
+        children: [] as any[],
+      });
+    });
+    workflowCcDeptList.forEach((dept) => {
+      const id = String(dept.id);
+      const parentId = String(dept.parentId ?? '');
+      const node = nodeMap.get(id);
+      const parent = nodeMap.get(parentId);
+      if (parent && parentId !== id) {
+        parent.children.push(node);
+      } else {
+        roots.push(node);
+      }
+    });
+    return roots;
+  }, [workflowCcDeptList]);
+  const workflowCcUserTreeData = useMemo(() => {
+    const buildDeptNode = (node: any): any => ({
+      key: node.key,
+      value: node.value,
+      title: node.title,
+      selectable: false,
+      children: (node.children || []).map((c: any) => buildDeptNode(c)),
+    });
+    const roots = workflowCcDeptTreeData.map((n: any) => buildDeptNode(n));
+    const deptNodeMap = new Map<string, any>();
+    const walk = (nodes: any[]) => {
+      nodes.forEach((n) => {
+        deptNodeMap.set(String(n.value), n);
+        if (Array.isArray(n.children) && n.children.length > 0) walk(n.children);
+      });
+    };
+    walk(roots);
+    const unknownDeptNode = {
+      key: 'workflow-dept-unknown',
+      value: 'unknown',
+      title: '未分配部门',
+      selectable: false,
+      children: [] as any[],
+    };
+    workflowCcUserList.forEach((user) => {
+      const userNode = {
+        key: `workflow-user-${user.id}`,
+        value: String(user.id),
+        title: user.nickname || user.username,
+        isLeaf: true,
+      };
+      const deptKey = user.deptId !== undefined && user.deptId !== null ? String(user.deptId) : '';
+      const deptNode = deptNodeMap.get(deptKey);
+      if (deptNode) {
+        deptNode.children = [...(deptNode.children || []), userNode];
+      } else {
+        unknownDeptNode.children.push(userNode);
+      }
+    });
+    if (unknownDeptNode.children.length > 0) {
+      roots.push(unknownDeptNode);
+    }
+    return roots;
+  }, [workflowCcDeptTreeData, workflowCcUserList]);
   const [workflowIsolationPoints, setWorkflowIsolationPoints] = useState<SegregationPointVO[]>([]);
   const [workflowFormList, setWorkflowFormList] = useState<FormVO[]>([]);
+
+  const isTemplateEnabled = useCallback((value: any): boolean => {
+    return value === true || value === 'true' || value === 1 || value === '1';
+  }, []);
+
+  const resolveNotificationMethods = useCallback((source: any, fallback?: any) => {
+    const methods = source?.notificationMethods || {};
+    const smsCode = source?.smsTemplateCode ?? fallback?.smsTemplateCode;
+    const messageCode = source?.messageTemplateCode ?? fallback?.messageTemplateCode;
+    const emailCode = source?.emailTemplateCode ?? fallback?.emailTemplateCode;
+    const appCode = source?.appTemplateCode ?? fallback?.appTemplateCode;
+    return {
+      sms: (methods.sms === true || methods.sms === 'true') || isTemplateEnabled(smsCode),
+      message: (methods.message === true || methods.message === 'true') || isTemplateEnabled(messageCode),
+      email: (methods.email === true || methods.email === 'true') || isTemplateEnabled(emailCode),
+      app: (methods.app === true || methods.app === 'true') || isTemplateEnabled(appCode),
+    };
+  }, [isTemplateEnabled]);
   
   // 流程设计列表数据(用于流程设计页面)
   const [processDesignList, setProcessDesignList] = useState<WorkflowDesignVO[]>([]);
@@ -1273,14 +1418,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
               isolationNodeUuid: source.isolationNodeUuid || '',
               lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
               coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
-              notificationMethods: source.notificationMethods || {
-                sms: false,
-                message: false,
-                email: false,
-                app: false,
-              },
+              notificationMethods: resolveNotificationMethods(source),
               notificationPerson: source.notificationPerson || '',
               notificationTime: source.notificationTime || '',
+              smsTemplateCode: source.smsTemplateCode || 'false',
+              messageTemplateCode: source.messageTemplateCode || 'false',
+              emailTemplateCode: source.emailTemplateCode || 'false',
+              appTemplateCode: source.appTemplateCode || 'false',
+              copyDeptIds: workflowCopyDeptIds(source),
+              copyUserIds: workflowCopyUserIds(source),
             });
             // 高亮第一个节点
             setWorkflowNodes((nds) =>
@@ -1393,6 +1539,19 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
           console.error('加载表单列表失败:', error);
         }
       };
+
+      const loadCcOptions = async () => {
+        try {
+          const [depts, users] = await Promise.all([
+            deptApi.getSimpleDeptList(),
+            userApi.getSimpleUserList(),
+          ]);
+          setWorkflowCcDeptList(depts || []);
+          setWorkflowCcUserList(users || []);
+        } catch (error) {
+          console.error('加载抄送下拉数据失败:', error);
+        }
+      };
       
       // 初始化节点状态:调用 checkWorkById 和 selectWorkflowWorkById 接口
       const initializeNodeStates = async () => {
@@ -1474,18 +1633,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                   }
                   return coLockPersonIds.length > 0 ? coLockPersonIds : (nodeData.coLockPersons && Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
                 })(),
-                notificationMethods: nodeData.notificationMethods || {
-                  sms: false,
-                  message: false,
-                  email: false,
-                  app: false,
-                },
+                notificationMethods: resolveNotificationMethods(nodeData, nodeDO),
                 notificationPerson: nodeData.notificationPerson || '',
                 notificationTime: nodeDO.notifyTime || nodeData.notificationTime || '',
                 smsTemplateCode: nodeData.smsTemplateCode || (nodeData.notificationMethods?.sms ? 'true' : 'false'),
                 messageTemplateCode: nodeData.messageTemplateCode || (nodeData.notificationMethods?.message ? 'true' : 'false'),
                 emailTemplateCode: nodeData.emailTemplateCode || (nodeData.notificationMethods?.email ? 'true' : 'false'),
                 appTemplateCode: nodeData.appTemplateCode || (nodeData.notificationMethods?.app ? 'true' : 'false'),
+                copyDeptIds: workflowCopyDeptIds(nodeData, nodeDO),
+                copyUserIds: workflowCopyUserIds(nodeData, nodeDO),
               };
               
               // 将节点配置存入缓存
@@ -1689,14 +1845,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                         }
                         return coLockPersonIds.length > 0 ? coLockPersonIds : (nodeData.coLockPersons && Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
                       })(),
-                      notificationMethods: nodeData.notificationMethods || {
-                        sms: false,
-                        message: false,
-                        email: false,
-                        app: false,
-                      },
+                      notificationMethods: resolveNotificationMethods(nodeData, nodeDO),
                       notificationPerson: nodeData.notificationPerson || '',
                       notificationTime: nodeDO.notifyTime || nodeData.notificationTime || '',
+                      smsTemplateCode: nodeData.smsTemplateCode || (nodeData.notificationMethods?.sms ? 'true' : 'false'),
+                      messageTemplateCode: nodeData.messageTemplateCode || (nodeData.notificationMethods?.message ? 'true' : 'false'),
+                      emailTemplateCode: nodeData.emailTemplateCode || (nodeData.notificationMethods?.email ? 'true' : 'false'),
+                      appTemplateCode: nodeData.appTemplateCode || (nodeData.notificationMethods?.app ? 'true' : 'false'),
+                      copyDeptIds: workflowCopyDeptIds(nodeData, nodeDO),
+                      copyUserIds: workflowCopyUserIds(nodeData, nodeDO),
                     });
                   } else {
                     // 如果既没有缓存也没有 nodeDO,使用节点原始数据
@@ -1714,14 +1871,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                       isolationNodeUuid: source.isolationNodeUuid || '',
                       lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
                       coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
-                      notificationMethods: source.notificationMethods || {
-                        sms: false,
-                        message: false,
-                        email: false,
-                        app: false,
-                      },
+                      notificationMethods: resolveNotificationMethods(source),
                       notificationPerson: source.notificationPerson || '',
                       notificationTime: source.notificationTime || '',
+                      smsTemplateCode: source.smsTemplateCode || 'false',
+                      messageTemplateCode: source.messageTemplateCode || 'false',
+                      emailTemplateCode: source.emailTemplateCode || 'false',
+                      appTemplateCode: source.appTemplateCode || 'false',
+                      copyDeptIds: workflowCopyDeptIds(source),
+                      copyUserIds: workflowCopyUserIds(source),
                     });
                   }
                 }
@@ -1746,6 +1904,7 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
       loadRoleUsers();
       loadIsolationPoints();
       loadFormList();
+      loadCcOptions();
       initializeNodeStates();
     }
   }, [workJobStep, workflowWorkId]);
@@ -2033,13 +2192,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
         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)) : [])),
-        notificationMethods: source.notificationMethods || { sms: false, message: false, email: false, app: false },
+        notificationMethods: resolveNotificationMethods(source, dataSource),
         notificationPerson: source.notificationPerson || '',
         notificationTime: fromIsolationOnly ? (dataSource.notifyTime ?? '') : (dataSource.notifyTime || source.notificationTime || ''),
         smsTemplateCode: source.smsTemplateCode || 'false',
         messageTemplateCode: source.messageTemplateCode || 'false',
         emailTemplateCode: source.emailTemplateCode || 'false',
         appTemplateCode: source.appTemplateCode || 'false',
+        copyDeptIds: workflowCopyDeptIds(source, nodeDO),
+        copyUserIds: workflowCopyUserIds(source, nodeDO),
       };
       
       if (isReleaseIsolation && isolationNodeDO) {
@@ -2077,18 +2238,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
           isolationNodeUuid: source.isolationNodeUuid || '',
           lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
           coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
-          notificationMethods: source.notificationMethods || {
-            sms: false,
-            message: false,
-            email: false,
-            app: false,
-          },
+          notificationMethods: resolveNotificationMethods(source),
           notificationPerson: source.notificationPerson || '',
           notificationTime: source.notificationTime || '',
           smsTemplateCode: source.smsTemplateCode || 'false',
           messageTemplateCode: source.messageTemplateCode || 'false',
           emailTemplateCode: source.emailTemplateCode || 'false',
           appTemplateCode: source.appTemplateCode || 'false',
+          copyDeptIds: workflowCopyDeptIds(source),
+          copyUserIds: workflowCopyUserIds(source),
         });
       }
     }
@@ -2131,6 +2289,8 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
         messageTemplateCode: workflowNodeConfig.messageTemplateCode,
         emailTemplateCode: workflowNodeConfig.emailTemplateCode,
         appTemplateCode: workflowNodeConfig.appTemplateCode,
+        copyDeptIds: workflowNodeConfig.copyDeptIds,
+        copyUserIds: workflowNodeConfig.copyUserIds,
         // 保持 completed 状态从缓存中读取,不覆盖
         completed: isSaved,
       };
@@ -2414,14 +2574,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                     isolationNodeUuid: firstNodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
                     lockPerson: lockPersonId || source.lockPerson || '',
                     coLockPersons: coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons || []),
-                    notificationMethods: source.notificationMethods || {
-                      sms: false,
-                      message: false,
-                      email: false,
-                      app: false,
-                    },
+                    notificationMethods: resolveNotificationMethods(source, firstNodeDO),
                     notificationPerson: source.notificationPerson || '',
                     notificationTime: firstNodeDO.notifyTime || source.notificationTime || '',
+                    smsTemplateCode: source.smsTemplateCode || (source.notificationMethods?.sms ? 'true' : 'false'),
+                    messageTemplateCode: source.messageTemplateCode || (source.notificationMethods?.message ? 'true' : 'false'),
+                    emailTemplateCode: source.emailTemplateCode || (source.notificationMethods?.email ? 'true' : 'false'),
+                    appTemplateCode: source.appTemplateCode || (source.notificationMethods?.app ? 'true' : 'false'),
+                    copyDeptIds: workflowCopyDeptIds(source, firstNodeDO),
+                    copyUserIds: workflowCopyUserIds(source, firstNodeDO),
                   });
                 } else {
                   // 如果没有找到 nodeDO,使用原有逻辑
@@ -2439,14 +2600,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                     isolationNodeUuid: source.isolationNodeUuid || '',
                     lockPerson: source.lockPerson || '',
                     coLockPersons: source.coLockPersons || [],
-                    notificationMethods: source.notificationMethods || {
-                      sms: false,
-                      message: false,
-                      email: false,
-                      app: false,
-                    },
+                    notificationMethods: resolveNotificationMethods(source),
                     notificationPerson: source.notificationPerson || '',
                     notificationTime: source.notificationTime || '',
+                    smsTemplateCode: source.smsTemplateCode || 'false',
+                    messageTemplateCode: source.messageTemplateCode || 'false',
+                    emailTemplateCode: source.emailTemplateCode || 'false',
+                    appTemplateCode: source.appTemplateCode || 'false',
+                    copyDeptIds: workflowCopyDeptIds(source),
+                    copyUserIds: workflowCopyUserIds(source),
                   });
                 }
                 
@@ -5037,6 +5199,63 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                     </div>
                                   </div>
                                 </div>
+                                <div>
+                                  <label className="block text-sm font-medium text-gray-700 mb-3">
+                                    通知对象
+                                  </label>
+                                  <div className="mb-3 space-y-1 text-xs text-gray-500">
+                                    <div>1. 系统将默认发送消息给任务负责人(或上锁人\共锁人)</div>
+                                    <div>2. 请选择消息抄送对象:</div>
+                                  </div>
+                                  <div className="space-y-3 bg-gray-50 p-3 rounded-lg">
+                                    <div>
+                                      <label className="block text-sm font-medium text-gray-700 mb-2">
+                                        抄送给部门(部门下的所有人):
+                                      </label>
+                                      <TreeSelect
+                                        multiple
+                                        value={workflowNodeConfig.copyDeptIds.length ? workflowNodeConfig.copyDeptIds : undefined}
+                                        onChange={(value) =>
+                                          setWorkflowNodeConfig({
+                                            ...workflowNodeConfig,
+                                            copyDeptIds: Array.isArray(value) ? value.map(String) : value != null && value !== '' ? [String(value)] : [],
+                                          })
+                                        }
+                                        placeholder="请选择部门(可多选)"
+                                        treeData={workflowCcDeptTreeData}
+                                        className="w-full"
+                                        allowClear
+                                        showSearch
+                                        treeNodeFilterProp="title"
+                                        treeDefaultExpandAll
+                                        disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
+                                      />
+                                    </div>
+                                    <div>
+                                      <label className="block text-sm font-medium text-gray-700 mb-2">
+                                        抄送给人员:
+                                      </label>
+                                      <TreeSelect
+                                        multiple
+                                        value={workflowNodeConfig.copyUserIds.length ? workflowNodeConfig.copyUserIds : undefined}
+                                        onChange={(value) =>
+                                          setWorkflowNodeConfig({
+                                            ...workflowNodeConfig,
+                                            copyUserIds: Array.isArray(value) ? value.map(String) : value != null && value !== '' ? [String(value)] : [],
+                                          })
+                                        }
+                                        placeholder="请选择人员(可多选)"
+                                        treeData={workflowCcUserTreeData}
+                                        className="w-full"
+                                        allowClear
+                                        showSearch
+                                        treeNodeFilterProp="title"
+                                        treeDefaultExpandAll
+                                        disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
+                                      />
+                                    </div>
+                                  </div>
+                                </div>
                                 {/* <div>
                                   <label className="block text-sm font-medium text-gray-700 mb-2">
                                     通知人
@@ -5195,6 +5414,8 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                         .map((u: any) => Number(u.userId));
                                       // 共锁人:有则传 JSON 字符串,清空时传空字符串以便后端清空
                                       const colockPersons = colockPersonIds.length > 0 ? JSON.stringify(colockPersonIds) : '';
+                                      const copyDeptIdsStr = workflowCopyIdsToApiCommaString(workflowNodeConfig.copyDeptIds);
+                                      const copyUserIdsStr = workflowCopyIdsToApiCommaString(workflowNodeConfig.copyUserIds);
 
                                       const updateParam: UpdateWorkflowWorkNodeParam = {
                                         nodeId: nodeDO.id,
@@ -5219,6 +5440,9 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                         messageTemplateCode: workflowNodeConfig.messageTemplateCode || 'false',
                                         emailTemplateCode: workflowNodeConfig.emailTemplateCode || 'false',
                                         appTemplateCode: workflowNodeConfig.appTemplateCode || 'false',
+                                        // 后端要求逗号分隔字符串,禁止 JSON 数组(如 [380])
+                                        copyDeptIds: copyDeptIdsStr,
+                                        copyUserIds: copyUserIdsStr,
                                       };
                                       
                                       await workJobApi.updateWorkflowWorkNode(updateParam);
@@ -5292,7 +5516,7 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                               isolationNodeUuid: nodeDO.isolationNodeUuid ?? source.isolationNodeUuid ?? '',
                                               lockPerson: lockPersonVal !== undefined ? lockPersonVal : source.lockPerson,
                                               coLockPersons: coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons || []),
-                                              notificationMethods: source.notificationMethods || { sms: false, message: false, email: false, app: false },
+                                              notificationMethods: resolveNotificationMethods(source, nodeDO),
                                               notificationPerson: source.notificationPerson ?? '',
                                               notificationTime: nodeDO.notifyTime ?? source.notificationTime ?? '',
                                               completed: isCurrent ? true : (node.data?.completed ?? false),
@@ -5448,9 +5672,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                                   isolationNodeUuid: nodeDO.isolationNodeUuid ?? source.isolationNodeUuid ?? '',
                                                   lockPerson: lockPersonVal !== undefined ? lockPersonVal : source.lockPerson,
                                                   coLockPersons: coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons || []),
-                                                  notificationMethods: source.notificationMethods || { sms: false, message: false, email: false, app: false },
+                                                  notificationMethods: resolveNotificationMethods(source, nodeDO),
                                                   notificationPerson: source.notificationPerson ?? '',
                                                   notificationTime: nodeDO.notifyTime ?? source.notificationTime ?? '',
+                                                  smsTemplateCode: source.smsTemplateCode ?? (source.notificationMethods?.sms ? 'true' : 'false'),
+                                                  messageTemplateCode: source.messageTemplateCode ?? (source.notificationMethods?.message ? 'true' : 'false'),
+                                                  emailTemplateCode: source.emailTemplateCode ?? (source.notificationMethods?.email ? 'true' : 'false'),
+                                                  appTemplateCode: source.appTemplateCode ?? (source.notificationMethods?.app ? 'true' : 'false'),
+                                                  copyDeptIds: workflowCopyDeptIds(source, nodeDO),
+                                                  copyUserIds: workflowCopyUserIds(source, nodeDO),
                                                   completed: isCurrent ? true : (node.data?.completed ?? false),
                                                 };
                                                 return { ...node, data: updatedData, selected: isCurrent ? false : node.selected };
@@ -5493,6 +5723,12 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                                     notificationMethods: workflowNodeConfig.notificationMethods,
                                                     notificationPerson: workflowNodeConfig.notificationPerson,
                                                     notificationTime: workflowNodeConfig.notificationTime,
+                                                    smsTemplateCode: workflowNodeConfig.smsTemplateCode,
+                                                    messageTemplateCode: workflowNodeConfig.messageTemplateCode,
+                                                    emailTemplateCode: workflowNodeConfig.emailTemplateCode,
+                                                    appTemplateCode: workflowNodeConfig.appTemplateCode,
+                                                    copyDeptIds: workflowCopyIdsToApiCommaString(workflowNodeConfig.copyDeptIds),
+                                                    copyUserIds: workflowCopyIdsToApiCommaString(workflowNodeConfig.copyUserIds),
                                                   };
                                                 })()),
                                               };
@@ -5537,6 +5773,12 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                         notificationMethods: workflowNodeConfig.notificationMethods,
                                         notificationPerson: workflowNodeConfig.notificationPerson,
                                         notificationTime: workflowNodeConfig.notificationTime,
+                                        smsTemplateCode: workflowNodeConfig.smsTemplateCode,
+                                        messageTemplateCode: workflowNodeConfig.messageTemplateCode,
+                                        emailTemplateCode: workflowNodeConfig.emailTemplateCode,
+                                        appTemplateCode: workflowNodeConfig.appTemplateCode,
+                                        copyDeptIds: workflowNodeConfig.copyDeptIds,
+                                        copyUserIds: workflowNodeConfig.copyUserIds,
                                         completed: true,
                                       };
                                       setWorkflowNodes((nds) =>
@@ -5614,14 +5856,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                                 }
                                                 return coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
                                               })(),
-                                              notificationMethods: source.notificationMethods || {
-                                                sms: false,
-                                                message: false,
-                                                email: false,
-                                                app: false,
-                                              },
+                                              notificationMethods: resolveNotificationMethods(source, nextNodeDO),
                                               notificationPerson: source.notificationPerson || '',
                                               notificationTime: nextNodeDO.notifyTime || source.notificationTime || '',
+                                              smsTemplateCode: source.smsTemplateCode || (source.notificationMethods?.sms ? 'true' : 'false'),
+                                              messageTemplateCode: source.messageTemplateCode || (source.notificationMethods?.message ? 'true' : 'false'),
+                                              emailTemplateCode: source.emailTemplateCode || (source.notificationMethods?.email ? 'true' : 'false'),
+                                              appTemplateCode: source.appTemplateCode || (source.notificationMethods?.app ? 'true' : 'false'),
+                                              copyDeptIds: workflowCopyDeptIds(source, nextNodeDO),
+                                              copyUserIds: workflowCopyUserIds(source, nextNodeDO),
                                             });
                                           } else {
                                             const source = nextNode.data || {};
@@ -5638,14 +5881,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                               isolationNodeUuid: source.isolationNodeUuid || '',
                                               lockPerson: source.lockPerson || '',
                                               coLockPersons: source.coLockPersons || [],
-                                              notificationMethods: source.notificationMethods || {
-                                                sms: false,
-                                                message: false,
-                                                email: false,
-                                                app: false,
-                                              },
+                                              notificationMethods: resolveNotificationMethods(source),
                                               notificationPerson: source.notificationPerson || '',
                                               notificationTime: source.notificationTime || '',
+                                              smsTemplateCode: source.smsTemplateCode || 'false',
+                                              messageTemplateCode: source.messageTemplateCode || 'false',
+                                              emailTemplateCode: source.emailTemplateCode || 'false',
+                                              appTemplateCode: source.appTemplateCode || 'false',
+                                              copyDeptIds: workflowCopyDeptIds(source),
+                                              copyUserIds: workflowCopyUserIds(source),
                                             });
                                           }
                                         }

+ 183 - 2
src/components/ProcessDesigner.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useCallback, useRef, useEffect } from 'react';
+import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
 import { useNavigate, useLocation } from 'react-router-dom';
 import ReactFlow, {
   Node,
@@ -136,11 +136,12 @@ import {
   FireOutlined as FireIconOutlined,
   ThunderboltOutlined as ThunderboltIconOutlined,
 } from '@ant-design/icons';
-import { Button, Input, Select, Checkbox, Tabs, Modal, Dropdown, Popover, message, Card, Alert, InputNumber, Radio, DatePicker, TimePicker, Form as AntdForm, Cascader, Upload, Switch, Tooltip } from 'antd';
+import { Button, Input, Select, TreeSelect, Checkbox, Tabs, Modal, Dropdown, Popover, message, Card, Alert, InputNumber, Radio, DatePicker, TimePicker, Form as AntdForm, Cascader, Upload, Switch, Tooltip } from 'antd';
 import type { MenuProps } from 'antd';
 import { toast } from 'sonner';
 import { workflowDesignApi, WorkflowDesignVO } from '../api/WorkflowDesign';
 import { userApi } from '../api/user';
+import { deptApi, type DeptVO } from '../api/dept';
 import { UserVO } from '../types';
 import { segregationPointApi, SegregationPointVO } from '../api/spm';
 import { getFormPage, getForm, FormVO } from '../api/bpm/form';
@@ -148,6 +149,36 @@ import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
 import { dictDataApi, DictDataVO } from '../api/DictData';
 import FormUploadField from './FormUploadField';
 
+/** 抄送部门/人员 ID:支持数组、逗号分隔字符串、JSON 数组字符串,兼容旧 cc* 单选 */
+function parseWorkflowCopyIds(multi: unknown, legacySingle: unknown): string[] {
+  const pushValid = (out: string[], x: unknown) => {
+    if (x !== undefined && x !== null && x !== '') out.push(String(x));
+  };
+  if (Array.isArray(multi)) {
+    const out: string[] = [];
+    multi.forEach((x) => pushValid(out, x));
+    return out;
+  }
+  if (typeof multi === 'string' && multi.trim()) {
+    try {
+      const parsed = JSON.parse(multi);
+      if (Array.isArray(parsed)) return parseWorkflowCopyIds(parsed, undefined);
+      if (parsed !== null && parsed !== undefined && parsed !== '') {
+        return [String(parsed)];
+      }
+    } catch {
+      return multi
+        .split(',')
+        .map((s) => s.trim())
+        .filter((s) => s !== '');
+    }
+  }
+  if (legacySingle !== undefined && legacySingle !== null && legacySingle !== '') {
+    return [String(legacySingle)];
+  }
+  return [];
+}
+
 // 节点配置
 const nodeConfigs = [
   {
@@ -1020,6 +1051,77 @@ export default function ProcessDesigner() {
   const [drawerUsers, setDrawerUsers] = useState<UserVO[]>([]); // 负责人(jtdrawer)
   const [lockerUsers, setLockerUsers] = useState<UserVO[]>([]); // 上锁人(jtlocker)
   const [colockerUsers, setColockerUsers] = useState<UserVO[]>([]); // 共锁人(jtcolocker)
+  const [ccDeptList, setCcDeptList] = useState<DeptVO[]>([]);
+  const [ccUserList, setCcUserList] = useState<UserVO[]>([]);
+  const ccDeptTreeData = useMemo(() => {
+    const nodeMap = new Map<string, any>();
+    const roots: any[] = [];
+    ccDeptList.forEach((dept) => {
+      const id = String(dept.id);
+      nodeMap.set(id, {
+        key: `dept-${id}`,
+        value: id,
+        title: dept.name,
+        children: [] as any[],
+      });
+    });
+    ccDeptList.forEach((dept) => {
+      const id = String(dept.id);
+      const parentId = String(dept.parentId ?? '');
+      const node = nodeMap.get(id);
+      const parent = nodeMap.get(parentId);
+      if (parent && parentId !== id) {
+        parent.children.push(node);
+      } else {
+        roots.push(node);
+      }
+    });
+    return roots;
+  }, [ccDeptList]);
+  const ccUserTreeData = useMemo(() => {
+    const buildDeptNode = (node: any): any => ({
+      key: node.key,
+      value: node.value,
+      title: node.title,
+      selectable: false,
+      children: (node.children || []).map((c: any) => buildDeptNode(c)),
+    });
+    const roots = ccDeptTreeData.map((n: any) => buildDeptNode(n));
+    const deptNodeMap = new Map<string, any>();
+    const walk = (nodes: any[]) => {
+      nodes.forEach((n) => {
+        deptNodeMap.set(String(n.value), n);
+        if (Array.isArray(n.children) && n.children.length > 0) walk(n.children);
+      });
+    };
+    walk(roots);
+    const unknownDeptNode = {
+      key: 'dept-unknown',
+      value: 'unknown',
+      title: '未分配部门',
+      selectable: false,
+      children: [] as any[],
+    };
+    ccUserList.forEach((user) => {
+      const userNode = {
+        key: `user-${user.id}`,
+        value: String(user.id),
+        title: user.nickname || user.username,
+        isLeaf: true,
+      };
+      const deptKey = user.deptId !== undefined && user.deptId !== null ? String(user.deptId) : '';
+      const deptNode = deptNodeMap.get(deptKey);
+      if (deptNode) {
+        deptNode.children = [...(deptNode.children || []), userNode];
+      } else {
+        unknownDeptNode.children.push(userNode);
+      }
+    });
+    if (unknownDeptNode.children.length > 0) {
+      roots.push(unknownDeptNode);
+    }
+    return roots;
+  }, [ccDeptTreeData, ccUserList]);
   // 隔离点列表
   const [isolationPoints, setIsolationPoints] = useState<SegregationPointVO[]>([]);
   // 表单列表
@@ -1318,10 +1420,24 @@ export default function ProcessDesigner() {
         console.error('加载表单列表失败:', error);
       }
     };
+
+    const loadCcOptions = async () => {
+      try {
+        const [depts, users] = await Promise.all([
+          deptApi.getSimpleDeptList(),
+          userApi.getSimpleUserList(),
+        ]);
+        setCcDeptList(depts || []);
+        setCcUserList(users || []);
+      } catch (error) {
+        console.error('加载抄送下拉数据失败:', error);
+      }
+    };
     
     loadRoleUsers();
     loadIsolationPoints();
     loadFormList();
+    loadCcOptions();
     getIsolationMethodDictList();
   }, []);
 
@@ -1350,6 +1466,8 @@ export default function ProcessDesigner() {
     messageTemplateCode: 'false',
     emailTemplateCode: 'false',
     appTemplateCode: 'false',
+    copyDeptIds: [] as string[],
+    copyUserIds: [] as string[],
   });
 
   // 实时缓存并更新节点配置,避免切换节点后丢失未保存的输入
@@ -1380,6 +1498,8 @@ export default function ProcessDesigner() {
         messageTemplateCode: nodeConfig.messageTemplateCode,
         emailTemplateCode: nodeConfig.emailTemplateCode,
         appTemplateCode: nodeConfig.appTemplateCode,
+        copyDeptIds: nodeConfig.copyDeptIds,
+        copyUserIds: nodeConfig.copyUserIds,
         nodeName: nodeConfig.nodeName,
         nodeIcon: nodeConfig.nodeIcon,
       };
@@ -1501,6 +1621,8 @@ export default function ProcessDesigner() {
           messageTemplateCode: topLevelMessageTemplateCode !== undefined ? topLevelMessageTemplateCode : (nodeData.messageTemplateCode || 'false'),
           emailTemplateCode: topLevelEmailTemplateCode !== undefined ? topLevelEmailTemplateCode : (nodeData.emailTemplateCode || 'false'),
           appTemplateCode: topLevelAppTemplateCode !== undefined ? topLevelAppTemplateCode : (nodeData.appTemplateCode || 'false'),
+          copyDeptIds: parseWorkflowCopyIds(nodeData.copyDeptIds, nodeData.ccDeptId),
+          copyUserIds: parseWorkflowCopyIds(nodeData.copyUserIds, nodeData.ccUserId),
           ...nodeData, // 保留其他可能的字段
         };
         
@@ -1916,6 +2038,8 @@ export default function ProcessDesigner() {
       messageTemplateCode: source.messageTemplateCode || 'false',
       emailTemplateCode: source.emailTemplateCode || 'false',
       appTemplateCode: source.appTemplateCode || 'false',
+      copyDeptIds: parseWorkflowCopyIds(source.copyDeptIds, source.ccDeptId),
+      copyUserIds: parseWorkflowCopyIds(source.copyUserIds, source.ccUserId),
     });
     
     // 如果是创建作业节点,且当前tab是"提交表单",自动切换到"节点信息"tab
@@ -2292,6 +2416,8 @@ export default function ProcessDesigner() {
               messageTemplateCode: nodeConfig.messageTemplateCode,
               emailTemplateCode: nodeConfig.emailTemplateCode,
               appTemplateCode: nodeConfig.appTemplateCode,
+              copyDeptIds: nodeConfig.copyDeptIds,
+              copyUserIds: nodeConfig.copyUserIds,
             },
           };
           // 缓存当前节点配置
@@ -3894,6 +4020,61 @@ export default function ProcessDesigner() {
                               </div>
                             </div>
                           </div>
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-3">
+                              通知对象
+                            </label>
+                            <div className="mb-3 space-y-1 text-xs text-gray-500">
+                              <div>1. 系统将默认发送消息给任务负责人(或上锁人\共锁人)</div>
+                              <div>2. 请选择消息抄送对象:</div>
+                            </div>
+                            <div className="space-y-3 bg-gray-50 p-3 rounded-lg">
+                              <div>
+                                <label className="block text-sm font-medium text-gray-700 mb-2">
+                                  抄送给部门(部门下的所有人):
+                                </label>
+                                <TreeSelect
+                                  multiple
+                                  value={nodeConfig.copyDeptIds.length ? nodeConfig.copyDeptIds : undefined}
+                                  onChange={(value) =>
+                                    setNodeConfig({
+                                      ...nodeConfig,
+                                      copyDeptIds: Array.isArray(value) ? value.map(String) : value != null && value !== '' ? [String(value)] : [],
+                                    })
+                                  }
+                                  placeholder="请选择部门(可多选)"
+                                  treeData={ccDeptTreeData}
+                                  className="w-full"
+                                  allowClear
+                                  showSearch
+                                  treeNodeFilterProp="title"
+                                  treeDefaultExpandAll
+                                />
+                              </div>
+                              <div>
+                                <label className="block text-sm font-medium text-gray-700 mb-2">
+                                  抄送给人员:
+                                </label>
+                                <TreeSelect
+                                  multiple
+                                  value={nodeConfig.copyUserIds.length ? nodeConfig.copyUserIds : undefined}
+                                  onChange={(value) =>
+                                    setNodeConfig({
+                                      ...nodeConfig,
+                                      copyUserIds: Array.isArray(value) ? value.map(String) : value != null && value !== '' ? [String(value)] : [],
+                                    })
+                                  }
+                                  placeholder="请选择人员(可多选)"
+                                  treeData={ccUserTreeData}
+                                  className="w-full"
+                                  allowClear
+                                  showSearch
+                                  treeNodeFilterProp="title"
+                                  treeDefaultExpandAll
+                                />
+                              </div>
+                            </div>
+                          </div>
                           {/* <div>
                             <label className="block text-sm font-medium text-gray-700 mb-2">
                               通知人