Explorar el Código

作业流程保存节点修复 站内信提示修复新增websocket

pm hace 4 meses
padre
commit
393167f9b1

+ 22 - 0
src/Dashboard.tsx

@@ -14,6 +14,7 @@ import NotificationManagement from './components/NotificationManagement';
 import FormManagement from './components/FormManagement';
 import MyTask from './components/MyTask';
 import TaskManagement from './components/TaskManagement';
+import MessageNotification from './components/MessageNotification';
 import { authApi } from './api';
 import { toast } from 'sonner';
 import { Toaster } from 'sonner';
@@ -755,6 +756,24 @@ export default function Dashboard() {
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [location.pathname, filteredSubMenuConfig]);
 
+  // 监听自定义事件,用于在同一页面切换菜单
+  useEffect(() => {
+    const handleSwitchToMenu = (event: CustomEvent) => {
+      const { menu, subMenu } = event.detail;
+      if (menu && subMenu) {
+        console.log('收到切换菜单事件:', { menu, subMenu });
+        setActiveMenu(menu);
+        setActiveSubMenu(subMenu);
+      }
+    };
+
+    window.addEventListener('switchToMenu', handleSwitchToMenu as EventListener);
+    
+    return () => {
+      window.removeEventListener('switchToMenu', handleSwitchToMenu as EventListener);
+    };
+  }, []);
+
   // 根据URL路径初始化菜单状态(仅在首次加载时执行)
   useEffect(() => {
     // 如果是详情页,设置 activeMenu 为空,直接显示详情页
@@ -1093,6 +1112,9 @@ export default function Dashboard() {
                 <span className="text-sm text-gray-700">{i18n.language === 'zh' ? t('nav.switchToEnglish') : t('nav.switchToChinese')}</span>
               </button>
               
+              {/* 消息通知 - 显示未读消息红点和消息列表 */}
+              <MessageNotification />
+
               {/* 通知管理 - 点击后进入页面,二级菜单在 tab 标签中显示 */}
               {notificationMenu && (
                 <div className="relative group">

+ 5 - 0
src/api/notification/in_site.ts

@@ -66,6 +66,11 @@ export const in_site = {
     return axiosInstance.get('/system/notify-message/get-unread-count');
   },
 
+  // 获取未读消息列表
+  getUnreadList: (): Promise<NotifyMessageVO[]> => {
+    return axiosInstance.get('/system/notify-message/get-unread-list');
+  },
+
   // 发送站内信
   sendMessage: (data: SendNotifyMessageVO): Promise<void> => {
     return axiosInstance.post('/system/notify-message/send', data);

+ 6 - 0
src/components/InboxMessage.tsx

@@ -245,6 +245,8 @@ export default function InboxMessage() {
       // 重新获取数据
       await fetchMessages();
       toast.success('标记已读成功');
+      // 触发事件,通知消息通知组件更新未读数量
+      window.dispatchEvent(new CustomEvent('messageRead'));
     } catch (error: any) {
       console.error('标记已读失败:', error);
       toast.error(error?.response?.data?.message || '标记已读失败');
@@ -262,6 +264,8 @@ export default function InboxMessage() {
       // 重新获取数据
       await fetchMessages();
       toast.success('全部标记已读成功');
+      // 触发事件,通知消息通知组件更新未读数量
+      window.dispatchEvent(new CustomEvent('messageRead'));
     } catch (error: any) {
       console.error('全部标记已读失败:', error);
       toast.error(error?.response?.data?.message || '全部标记已读失败');
@@ -284,6 +288,8 @@ export default function InboxMessage() {
           await in_site.markAsRead([id]);
           // 更新列表数据
           await fetchMessages();
+          // 触发事件,通知消息通知组件更新未读数量
+          window.dispatchEvent(new CustomEvent('messageRead'));
         } catch (error) {
           console.error('标记已读失败:', error);
           toast.error('标记已读失败');

+ 29 - 0
src/components/IsolationWork.tsx

@@ -454,6 +454,10 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
     },
     notificationPerson: '',
     notificationTime: '',
+    smsTemplateCode: 'false',
+    messageTemplateCode: 'false',
+    emailTemplateCode: 'false',
+    appTemplateCode: 'false',
   });
   
   // 节点配置缓存(Map<nodeId, config>)
@@ -1477,6 +1481,10 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                 },
                 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'),
               };
               
               // 将节点配置存入缓存
@@ -2008,6 +2016,10 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
         },
         notificationPerson: source.notificationPerson || '',
         notificationTime: nodeDO.notifyTime || source.notificationTime || '',
+        smsTemplateCode: source.smsTemplateCode || 'false',
+        messageTemplateCode: source.messageTemplateCode || 'false',
+        emailTemplateCode: source.emailTemplateCode || 'false',
+        appTemplateCode: source.appTemplateCode || 'false',
       };
       
       // 优先使用缓存中的配置(如果有),这样可以保持用户未保存的修改
@@ -2060,6 +2072,10 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
           },
           notificationPerson: source.notificationPerson || '',
           notificationTime: source.notificationTime || '',
+          smsTemplateCode: source.smsTemplateCode || 'false',
+          messageTemplateCode: source.messageTemplateCode || 'false',
+          emailTemplateCode: source.emailTemplateCode || 'false',
+          appTemplateCode: source.appTemplateCode || 'false',
         });
       }
     }
@@ -2093,6 +2109,10 @@ 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,
         // 保持 completed 状态从缓存中读取,不覆盖
         completed: isSaved,
       };
@@ -4878,6 +4898,7 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                               ...workflowNodeConfig.notificationMethods,
                                               sms: e.target.checked,
                                             },
+                                            smsTemplateCode: e.target.checked ? 'true' : 'false',
                                           })
                                         }
                                         disabled={isViewMode}
@@ -4895,6 +4916,7 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                               ...workflowNodeConfig.notificationMethods,
                                               message: e.target.checked,
                                             },
+                                            messageTemplateCode: e.target.checked ? 'true' : 'false',
                                           })
                                         }
                                         disabled={isViewMode}
@@ -4912,6 +4934,7 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                               ...workflowNodeConfig.notificationMethods,
                                               email: e.target.checked,
                                             },
+                                            emailTemplateCode: e.target.checked ? 'true' : 'false',
                                           })
                                         }
                                         disabled={isViewMode}
@@ -4929,6 +4952,7 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                               ...workflowNodeConfig.notificationMethods,
                                               app: e.target.checked,
                                             },
+                                            appTemplateCode: e.target.checked ? 'true' : 'false',
                                           })
                                         }
                                         disabled={isViewMode}
@@ -5103,6 +5127,11 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                                         nodeUserDOList: nodeUserDOList.length > 0 ? nodeUserDOList : undefined,
                                         // 如果获取到了表单数据,将整个对象内容转换成字符串传递给 formData 字段
                                         formData: formData,
+                                        // 根据选中值传递模板代码参数(字符串类型的 'true' 或 'false')
+                                        smsTemplateCode: workflowNodeConfig.smsTemplateCode || 'false',
+                                        messageTemplateCode: workflowNodeConfig.messageTemplateCode || 'false',
+                                        emailTemplateCode: workflowNodeConfig.emailTemplateCode || 'false',
+                                        appTemplateCode: workflowNodeConfig.appTemplateCode || 'false',
                                       };
                                       
                                       await workJobApi.updateWorkflowWorkNode(updateParam);

+ 279 - 0
src/components/MessageNotification.tsx

@@ -0,0 +1,279 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Bell, X, Eye } from 'lucide-react';
+import { Popover } from 'antd';
+import { in_site, type NotifyMessageVO } from '../api/notification/in_site';
+import { getUserId } from '../utils/auth';
+import { systemAttributeApi } from '../api/systemAttribute';
+import { connectWebsocket, closeWebsocket } from '../utils/webSocket';
+import { dateFormatter } from '../utils/formatTime';
+import { useNavigate } from 'react-router-dom';
+import { notification } from 'antd';
+
+export default function MessageNotification() {
+  const [unreadCount, setUnreadCount] = useState(0);
+  const [messageList, setMessageList] = useState<NotifyMessageVO[]>([]);
+  const [popoverVisible, setPopoverVisible] = useState(false);
+  const navigate = useNavigate();
+  const wsInitialized = useRef(false);
+
+  // 获取未读消息数量
+  const getUnreadCount = async () => {
+    try {
+      const count = await in_site.getUnreadCount();
+      setUnreadCount(count || 0);
+    } catch (error) {
+      console.error('获取未读消息数量失败:', error);
+    }
+  };
+
+  // 获取未读消息列表
+  const getUnreadList = async () => {
+    try {
+      const list = await in_site.getUnreadList();
+      setMessageList(list || []);
+      // 同时更新未读数量
+      setUnreadCount(list?.length || 0);
+    } catch (error) {
+      console.error('获取未读消息列表失败:', error);
+    }
+  };
+
+  // 跳转到站内信页面
+  const goToMessagePage = () => {
+    setPopoverVisible(false);
+    // 通过 sessionStorage 传递参数,让 Dashboard 自动切换到站内信
+    sessionStorage.setItem('navigateToMenu', JSON.stringify({
+      menu: 'notificationManagement',
+      subMenu: 'webmail'
+    }));
+    
+    // 如果当前已经在 dashboard 页面,触发自定义事件来切换菜单
+    if (window.location.pathname === '/dashboard') {
+      // 触发自定义事件,通知 Dashboard 切换菜单
+      window.dispatchEvent(new CustomEvent('switchToMenu', {
+        detail: { menu: 'notificationManagement', subMenu: 'webmail' }
+      }));
+    } else {
+      // 如果不在 dashboard 页面,导航过去
+      navigate('/dashboard');
+    }
+  };
+
+  // 初始化 WebSocket
+  const initWebSocket = async () => {
+    if (wsInitialized.current) {
+      return;
+    }
+
+    const userId = getUserId();
+    if (!userId) {
+      console.warn('未获取到 userId,不建立 WebSocket 连接');
+      return;
+    }
+
+    try {
+      // 获取 WebSocket 地址配置
+      const addressData = await systemAttributeApi.getIsSystemAttributeByKey('sys.websocket.address');
+      const url = addressData.sysAttrValue;
+      
+      // 判断是否为本地开发环境
+      const isLocalDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
+      const baseAddress = isLocalDev ? 'ws://192.168.0.10:48080' : url;
+      
+      const wsUrl = `${baseAddress}/websocket/messSend/${userId}`;
+      
+      connectWebsocket(
+        wsUrl,
+        { w: 'S' },
+        (msg) => {
+          console.log('接收消息:', msg);
+          
+          if (msg !== 'heartbeat' && msg !== 'pong') {
+            try {
+              const parsedMsg = JSON.parse(msg);
+              const content = parsedMsg.templateContent || parsedMsg.content || '';
+              
+              // 显示通知
+              notification.info({
+                message: '新消息提醒',
+                description: (
+                  <div 
+                    style={{ 
+                      maxHeight: '130px', 
+                      overflow: 'hidden', 
+                      textOverflow: 'ellipsis',
+                      display: '-webkit-box',
+                      WebkitLineClamp: 2,
+                      WebkitBoxOrient: 'vertical',
+                      wordBreak: 'break-word'
+                    }}
+                  >
+                    {content.replace(/<[^>]*>/g, '')}
+                  </div>
+                ),
+                duration: 8,
+                onClick: () => {
+                  goToMessagePage();
+                },
+                placement: 'bottomRight',
+              });
+              
+              // 立即刷新未读消息数量(延迟一小段时间确保后端已处理)
+              setTimeout(() => {
+                getUnreadCount();
+              }, 500);
+            } catch (error) {
+              console.error('消息解析失败:', error);
+            }
+          }
+        },
+        (err) => {
+          console.error('WebSocket 错误:', err);
+        }
+      );
+      
+      wsInitialized.current = true;
+    } catch (error) {
+      console.error('初始化 WebSocket 失败:', error);
+    }
+  };
+
+  // 组件挂载时初始化
+  useEffect(() => {
+    // 首次加载未读消息数量
+    getUnreadCount();
+    
+    // 初始化 WebSocket
+    initWebSocket();
+    
+    // 监听消息已读事件,立即更新未读数量
+    const handleMessageRead = () => {
+      console.log('收到消息已读事件,更新未读数量');
+      getUnreadCount();
+    };
+    
+    window.addEventListener('messageRead', handleMessageRead);
+    
+    // 轮询刷新未读消息数量(每30秒)
+    const pollInterval = setInterval(() => {
+      const userId = getUserId();
+      if (userId) {
+        getUnreadCount();
+      } else {
+        setUnreadCount(0);
+      }
+    }, 1000 * 30);
+
+    // 组件卸载时清理
+    return () => {
+      window.removeEventListener('messageRead', handleMessageRead);
+      clearInterval(pollInterval);
+      closeWebsocket();
+      wsInitialized.current = false;
+    };
+  }, []);
+
+  // 当点击消息图标时,获取消息列表
+  const handleIconClick = () => {
+    if (!popoverVisible) {
+      // 打开弹窗时,同时更新未读列表和数量
+      getUnreadList();
+      getUnreadCount();
+    }
+    setPopoverVisible(!popoverVisible);
+  };
+
+  // 消息列表内容
+  const messageContent = (
+    <div className="w-96" style={{ maxHeight: 'calc(100vh - 100px)', display: 'flex', flexDirection: 'column' }}>
+      <div className="p-4 border-b border-gray-200 flex-shrink-0">
+        <div className="flex items-center justify-between gap-4">
+          <h3 className="text-base font-semibold text-gray-900 flex-1">通知消息</h3>
+          <button
+            onClick={() => setPopoverVisible(false)}
+            className="p-1 hover:bg-gray-100 rounded transition-colors flex-shrink-0"
+          >
+            <X className="w-4 h-4 text-gray-500" />
+          </button>
+        </div>
+      </div>
+      
+      <div className="flex-1 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 200px)' }}>
+        {messageList.length === 0 ? (
+          <div className="flex flex-col items-center justify-center py-12 text-gray-400">
+            <Bell className="w-12 h-12 mb-2 opacity-50" />
+            <p className="text-sm">暂无未读消息</p>
+          </div>
+        ) : (
+          <div className="divide-y divide-gray-100">
+            {messageList.map((item) => (
+              <div
+                key={item.id}
+                className="p-4 hover:bg-gray-50 transition-colors cursor-pointer"
+                onClick={() => {
+                  goToMessagePage();
+                }}
+              >
+                <div className="flex items-start gap-3">
+                  <div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
+                    <Bell className="w-5 h-5 text-blue-600" />
+                  </div>
+                  <div className="flex-1 min-w-0">
+                    <div className="flex items-start justify-between gap-2 mb-1">
+                      <p className="text-sm font-medium text-gray-900 line-clamp-2">
+                        {item.templateNickname || '系统'}:{item.templateContent || item.title}
+                      </p>
+                    </div>
+                    <p className="text-xs text-gray-500">
+                      {item.createTime ? dateFormatter(item.createTime) : ''}
+                    </p>
+                  </div>
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+      
+      {messageList.length > 0 && (
+        <div className="p-4 border-t border-gray-200 flex-shrink-0">
+          <button
+            onClick={goToMessagePage}
+            className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
+          >
+            <Eye className="w-4 h-4 text-white" />
+            <span className="text-white">查看全部</span>
+          </button>
+        </div>
+      )}
+    </div>
+  );
+
+  return (
+    <Popover
+      content={messageContent}
+      trigger="click"
+      open={popoverVisible}
+      onOpenChange={setPopoverVisible}
+      placement="bottomRight"
+      overlayClassName="message-notification-popover"
+      overlayStyle={{ maxHeight: 'calc(100vh - 100px)', overflow: 'hidden' }}
+    >
+      <div className="relative">
+        <button
+          onClick={handleIconClick}
+          className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors"
+          title="消息通知"
+        >
+          <Bell className="w-5 h-5 text-gray-600" />
+          {unreadCount > 0 && (
+            <span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center font-semibold">
+              {unreadCount > 99 ? '99+' : unreadCount}
+            </span>
+          )}
+        </button>
+      </div>
+    </Popover>
+  );
+}
+

+ 179 - 3
src/components/MyTask.tsx

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { Eye, Search, RotateCcw } from 'lucide-react';
 import { Button, Space, Table as AntdTable, Input, message, Modal, Form as AntdForm, Card, Alert, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload } from 'antd';
-import { UploadOutlined, LockOutlined, KeyOutlined, WarningOutlined } from '@ant-design/icons';
+import { UploadOutlined, LockOutlined, KeyOutlined, WarningOutlined, CheckCircleOutlined } from '@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
 import { toast } from 'sonner';
 import dayjs, { Dayjs } from 'dayjs';
@@ -2002,7 +2002,49 @@ export default function MyTask() {
                           })()}
                         </div>
                       ) : (
-                        <div className="py-4 text-center text-gray-400 text-sm">{t('form.noFormContent')}</div>
+                        <div 
+                          style={{
+                            display: 'flex',
+                            justifyContent: 'center',
+                            alignItems: 'center',
+                            minHeight: '400px',
+                            padding: '40px 20px'
+                          }}
+                        >
+                          <Card
+                            style={{
+                              width: '100%',
+                              maxWidth: '500px',
+                              textAlign: 'center',
+                              borderRadius: '12px',
+                              boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
+                              border: '1px solid #e8e8e8'
+                            }}
+                            bodyStyle={{
+                              padding: '40px 30px'
+                            }}
+                          >
+                            <div style={{ marginBottom: '24px' }}>
+                              <CheckCircleOutlined 
+                                style={{ 
+                                  fontSize: '64px', 
+                                  color: '#1890ff',
+                                  display: 'block'
+                                }} 
+                              />
+                            </div>
+                            <div
+                              style={{
+                                fontSize: '20px',
+                                fontWeight: 500,
+                                color: '#333',
+                                lineHeight: '1.6'
+                              }}
+                            >
+                              无需填写表单,直接点击底部按钮提交即可
+                            </div>
+                          </Card>
+                        </div>
                       )}
 
                       {/* 审核意见 - label 和 textarea 同一行 */}
@@ -2075,10 +2117,56 @@ export default function MyTask() {
                             
                             setApprovalLoading(true);
                             try {
+                              // 如果有自定义表单,获取表单数据
+                              let formDataString: string | undefined = undefined;
+                              if (formData.rule && formData.rule.length > 0 && originalFields.length > 0) {
+                                try {
+                                  // 获取表单值
+                                  const values = detailForm.getFieldsValue();
+                                  
+                                  // 将填写值添加到原始 fields 数组的每个 JSON 字符串中
+                                  const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                    try {
+                                      // 解析字段 JSON 字符串
+                                      const fieldObj = JSON.parse(fieldString);
+                                      
+                                      // 获取字段名:优先使用 name(表单实际使用的字段名),其次使用 field
+                                      const fieldName = fieldObj.name || fieldObj.field;
+                                      
+                                      // 获取填写值
+                                      const fieldValue = values[fieldName];
+                                      
+                                      // 添加或更新 value 字段
+                                      fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                      
+                                      // 转回 JSON 字符串
+                                      return JSON.stringify(fieldObj);
+                                    } catch (e) {
+                                      console.error('MyTask: 解析字段 JSON 失败', e, fieldString);
+                                      return fieldString; // 如果解析失败,返回原始字符串
+                                    }
+                                  });
+                                  
+                                  // 构建完整的表单数据对象
+                                  const submitData = {
+                                    id: detailData.formId || detailData.id,
+                                    name: detailData.nodeName || '未知',
+                                    conf: originalConf,
+                                    fields: fieldsWithValues
+                                  };
+                                  
+                                  // 将表单数据转换为 JSON 字符串
+                                  formDataString = JSON.stringify(submitData);
+                                } catch (error) {
+                                  console.error('MyTask: 获取表单数据失败', error);
+                                }
+                              }
+                              
                               const params: UpdateNodeApprovalParam = {
                                 nodeId: nodeId,
                                 approvalStatus: 'rejected',
                                 approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
                               };
                               
                               console.log('MyTask: 调用审核不通过接口', params);
@@ -2141,10 +2229,56 @@ export default function MyTask() {
                             
                             setApprovalLoading(true);
                             try {
+                              // 如果有自定义表单,获取表单数据
+                              let formDataString: string | undefined = undefined;
+                              if (formData.rule && formData.rule.length > 0 && originalFields.length > 0) {
+                                try {
+                                  // 获取表单值
+                                  const values = detailForm.getFieldsValue();
+                                  
+                                  // 将填写值添加到原始 fields 数组的每个 JSON 字符串中
+                                  const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                    try {
+                                      // 解析字段 JSON 字符串
+                                      const fieldObj = JSON.parse(fieldString);
+                                      
+                                      // 获取字段名:优先使用 name(表单实际使用的字段名),其次使用 field
+                                      const fieldName = fieldObj.name || fieldObj.field;
+                                      
+                                      // 获取填写值
+                                      const fieldValue = values[fieldName];
+                                      
+                                      // 添加或更新 value 字段
+                                      fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                      
+                                      // 转回 JSON 字符串
+                                      return JSON.stringify(fieldObj);
+                                    } catch (e) {
+                                      console.error('MyTask: 解析字段 JSON 失败', e, fieldString);
+                                      return fieldString; // 如果解析失败,返回原始字符串
+                                    }
+                                  });
+                                  
+                                  // 构建完整的表单数据对象
+                                  const submitData = {
+                                    id: detailData.formId || detailData.id,
+                                    name: detailData.nodeName || '未知',
+                                    conf: originalConf,
+                                    fields: fieldsWithValues
+                                  };
+                                  
+                                  // 将表单数据转换为 JSON 字符串
+                                  formDataString = JSON.stringify(submitData);
+                                } catch (error) {
+                                  console.error('MyTask: 获取表单数据失败', error);
+                                }
+                              }
+                              
                               const params: UpdateNodeApprovalParam = {
                                 nodeId: nodeId,
                                 approvalStatus: 'approved',
                                 approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
                               };
                               
                               console.log('MyTask: 调用审核通过接口', params);
@@ -2230,7 +2364,49 @@ export default function MyTask() {
                         })()}
                       </div>
                     ) : (
-                      <div className="py-4 text-center text-gray-400 text-sm">{t('form.noFormContent')}</div>
+                      <div 
+                        style={{
+                          display: 'flex',
+                          justifyContent: 'center',
+                          alignItems: 'center',
+                          minHeight: '400px',
+                          padding: '40px 20px'
+                        }}
+                      >
+                        <Card
+                          style={{
+                            width: '100%',
+                            maxWidth: '500px',
+                            textAlign: 'center',
+                            borderRadius: '12px',
+                            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
+                            border: '1px solid #e8e8e8'
+                          }}
+                          bodyStyle={{
+                            padding: '40px 30px'
+                          }}
+                        >
+                          <div style={{ marginBottom: '24px' }}>
+                            <CheckCircleOutlined 
+                              style={{ 
+                                fontSize: '64px', 
+                                color: '#1890ff',
+                                display: 'block'
+                              }} 
+                            />
+                          </div>
+                          <div
+                            style={{
+                              fontSize: '20px',
+                              fontWeight: 500,
+                              color: '#333',
+                              lineHeight: '1.6'
+                            }}
+                          >
+                            无需填写表单,直接点击底部按钮提交即可
+                          </div>
+                        </Card>
+                      </div>
                     )}
 
                     </div>

+ 179 - 3
src/components/TaskManagement.tsx

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { Eye, Search, RotateCcw } from 'lucide-react';
 import { Button, Space, Table as AntdTable, Input, message, Modal, Form as AntdForm, Card, Alert, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload } from 'antd';
-import { UploadOutlined, LockOutlined, KeyOutlined, WarningOutlined } from '@ant-design/icons';
+import { UploadOutlined, LockOutlined, KeyOutlined, WarningOutlined, CheckCircleOutlined } from '@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
 import { toast } from 'sonner';
 import dayjs, { Dayjs } from 'dayjs';
@@ -2001,7 +2001,49 @@ export default function TaskManagement() {
                           })()}
                         </div>
                       ) : (
-                        <div className="py-4 text-center text-gray-400 text-sm">{t('form.noFormContent')}</div>
+                        <div 
+                          style={{
+                            display: 'flex',
+                            justifyContent: 'center',
+                            alignItems: 'center',
+                            minHeight: '400px',
+                            padding: '40px 20px'
+                          }}
+                        >
+                          <Card
+                            style={{
+                              width: '100%',
+                              maxWidth: '500px',
+                              textAlign: 'center',
+                              borderRadius: '12px',
+                              boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
+                              border: '1px solid #e8e8e8'
+                            }}
+                            bodyStyle={{
+                              padding: '40px 30px'
+                            }}
+                          >
+                            <div style={{ marginBottom: '24px' }}>
+                              <CheckCircleOutlined 
+                                style={{ 
+                                  fontSize: '64px', 
+                                  color: '#1890ff',
+                                  display: 'block'
+                                }} 
+                              />
+                            </div>
+                            <div
+                              style={{
+                                fontSize: '20px',
+                                fontWeight: 500,
+                                color: '#333',
+                                lineHeight: '1.6'
+                              }}
+                            >
+                              无需填写表单,直接点击底部按钮提交即可
+                            </div>
+                          </Card>
+                        </div>
                       )}
 
                       {/* 审核意见 - label 和 textarea 同一行 */}
@@ -2074,10 +2116,56 @@ export default function TaskManagement() {
                             
                             setApprovalLoading(true);
                             try {
+                              // 如果有自定义表单,获取表单数据
+                              let formDataString: string | undefined = undefined;
+                              if (formData.rule && formData.rule.length > 0 && originalFields.length > 0) {
+                                try {
+                                  // 获取表单值
+                                  const values = detailForm.getFieldsValue();
+                                  
+                                  // 将填写值添加到原始 fields 数组的每个 JSON 字符串中
+                                  const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                    try {
+                                      // 解析字段 JSON 字符串
+                                      const fieldObj = JSON.parse(fieldString);
+                                      
+                                      // 获取字段名:优先使用 name(表单实际使用的字段名),其次使用 field
+                                      const fieldName = fieldObj.name || fieldObj.field;
+                                      
+                                      // 获取填写值
+                                      const fieldValue = values[fieldName];
+                                      
+                                      // 添加或更新 value 字段
+                                      fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                      
+                                      // 转回 JSON 字符串
+                                      return JSON.stringify(fieldObj);
+                                    } catch (e) {
+                                      console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
+                                      return fieldString; // 如果解析失败,返回原始字符串
+                                    }
+                                  });
+                                  
+                                  // 构建完整的表单数据对象
+                                  const submitData = {
+                                    id: detailData.formId || detailData.id,
+                                    name: detailData.nodeName || '未知',
+                                    conf: originalConf,
+                                    fields: fieldsWithValues
+                                  };
+                                  
+                                  // 将表单数据转换为 JSON 字符串
+                                  formDataString = JSON.stringify(submitData);
+                                } catch (error) {
+                                  console.error('TaskManagement: 获取表单数据失败', error);
+                                }
+                              }
+                              
                               const params: UpdateNodeApprovalParam = {
                                 nodeId: nodeId,
                                 approvalStatus: 'rejected',
                                 approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
                               };
                               
                               console.log('TaskManagement: 调用审核不通过接口', params);
@@ -2140,10 +2228,56 @@ export default function TaskManagement() {
                             
                             setApprovalLoading(true);
                             try {
+                              // 如果有自定义表单,获取表单数据
+                              let formDataString: string | undefined = undefined;
+                              if (formData.rule && formData.rule.length > 0 && originalFields.length > 0) {
+                                try {
+                                  // 获取表单值
+                                  const values = detailForm.getFieldsValue();
+                                  
+                                  // 将填写值添加到原始 fields 数组的每个 JSON 字符串中
+                                  const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                    try {
+                                      // 解析字段 JSON 字符串
+                                      const fieldObj = JSON.parse(fieldString);
+                                      
+                                      // 获取字段名:优先使用 name(表单实际使用的字段名),其次使用 field
+                                      const fieldName = fieldObj.name || fieldObj.field;
+                                      
+                                      // 获取填写值
+                                      const fieldValue = values[fieldName];
+                                      
+                                      // 添加或更新 value 字段
+                                      fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                      
+                                      // 转回 JSON 字符串
+                                      return JSON.stringify(fieldObj);
+                                    } catch (e) {
+                                      console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
+                                      return fieldString; // 如果解析失败,返回原始字符串
+                                    }
+                                  });
+                                  
+                                  // 构建完整的表单数据对象
+                                  const submitData = {
+                                    id: detailData.formId || detailData.id,
+                                    name: detailData.nodeName || '未知',
+                                    conf: originalConf,
+                                    fields: fieldsWithValues
+                                  };
+                                  
+                                  // 将表单数据转换为 JSON 字符串
+                                  formDataString = JSON.stringify(submitData);
+                                } catch (error) {
+                                  console.error('TaskManagement: 获取表单数据失败', error);
+                                }
+                              }
+                              
                               const params: UpdateNodeApprovalParam = {
                                 nodeId: nodeId,
                                 approvalStatus: 'approved',
                                 approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
                               };
                               
                               console.log('TaskManagement: 调用审核通过接口', params);
@@ -2229,7 +2363,49 @@ export default function TaskManagement() {
                         })()}
                       </div>
                     ) : (
-                      <div className="py-4 text-center text-gray-400 text-sm">{t('form.noFormContent')}</div>
+                      <div 
+                        style={{
+                          display: 'flex',
+                          justifyContent: 'center',
+                          alignItems: 'center',
+                          minHeight: '400px',
+                          padding: '40px 20px'
+                        }}
+                      >
+                        <Card
+                          style={{
+                            width: '100%',
+                            maxWidth: '500px',
+                            textAlign: 'center',
+                            borderRadius: '12px',
+                            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
+                            border: '1px solid #e8e8e8'
+                          }}
+                          bodyStyle={{
+                            padding: '40px 30px'
+                          }}
+                        >
+                          <div style={{ marginBottom: '24px' }}>
+                            <CheckCircleOutlined 
+                              style={{ 
+                                fontSize: '64px', 
+                                color: '#1890ff',
+                                display: 'block'
+                              }} 
+                            />
+                          </div>
+                          <div
+                            style={{
+                              fontSize: '20px',
+                              fontWeight: 500,
+                              color: '#333',
+                              lineHeight: '1.6'
+                            }}
+                          >
+                            无需填写表单,直接点击底部按钮提交即可
+                          </div>
+                        </Card>
+                      </div>
                     )}
 
                     </div>

+ 150 - 0
src/utils/webSocket.ts

@@ -0,0 +1,150 @@
+// WebSocket 工具类
+
+type MessageCallback = (message: string) => void;
+type ErrorCallback = (error: Event) => void;
+
+let ws: WebSocket | null = null;
+let reconnectTimer: NodeJS.Timeout | null = null;
+let heartbeatTimer: NodeJS.Timeout | null = null;
+let reconnectAttempts = 0;
+const maxReconnectAttempts = 5;
+const reconnectDelay = 3000; // 3秒
+const heartbeatInterval = 30000; // 30秒心跳
+
+/**
+ * 连接 WebSocket
+ * @param url WebSocket 地址
+ * @param params 连接参数(会作为查询参数)
+ * @param onMessage 消息回调
+ * @param onError 错误回调
+ */
+export const connectWebsocket = (
+  url: string,
+  params: Record<string, any> = {},
+  onMessage: MessageCallback,
+  onError?: ErrorCallback
+) => {
+  // 如果已经连接,先关闭
+  if (ws && ws.readyState === WebSocket.OPEN) {
+    closeWebsocket();
+  }
+
+  // 构建带参数的 URL
+  const paramString = new URLSearchParams(params).toString();
+  const fullUrl = paramString ? `${url}?${paramString}` : url;
+
+  try {
+    ws = new WebSocket(fullUrl);
+
+    ws.onopen = () => {
+      console.log('WebSocket 连接成功');
+      reconnectAttempts = 0;
+      
+      // 启动心跳
+      startHeartbeat();
+    };
+
+    ws.onmessage = (event) => {
+      const message = event.data;
+      
+      // 处理心跳消息
+      if (message === 'heartbeat' || message === 'ping') {
+        // 响应心跳
+        if (ws && ws.readyState === WebSocket.OPEN) {
+          ws.send('pong');
+        }
+        return;
+      }
+
+      // 调用消息回调
+      onMessage(message);
+    };
+
+    ws.onerror = (error) => {
+      console.error('WebSocket 错误:', error);
+      if (onError) {
+        onError(error);
+      }
+    };
+
+    ws.onclose = () => {
+      console.log('WebSocket 连接关闭');
+      stopHeartbeat();
+      
+      // 尝试重连
+      if (reconnectAttempts < maxReconnectAttempts) {
+        reconnectAttempts++;
+        console.log(`尝试重连 (${reconnectAttempts}/${maxReconnectAttempts})...`);
+        reconnectTimer = setTimeout(() => {
+          connectWebsocket(url, params, onMessage, onError);
+        }, reconnectDelay);
+      } else {
+        console.error('WebSocket 重连次数已达上限,停止重连');
+      }
+    };
+  } catch (error) {
+    console.error('WebSocket 连接失败:', error);
+    if (onError) {
+      onError(error as Event);
+    }
+  }
+};
+
+/**
+ * 关闭 WebSocket 连接
+ */
+export const closeWebsocket = () => {
+  if (reconnectTimer) {
+    clearTimeout(reconnectTimer);
+    reconnectTimer = null;
+  }
+  
+  stopHeartbeat();
+  
+  if (ws) {
+    ws.onopen = null;
+    ws.onmessage = null;
+    ws.onerror = null;
+    ws.onclose = null;
+    
+    if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
+      ws.close();
+    }
+    
+    ws = null;
+  }
+  
+  reconnectAttempts = 0;
+  console.log('WebSocket 已关闭');
+};
+
+/**
+ * 启动心跳
+ */
+const startHeartbeat = () => {
+  stopHeartbeat();
+  
+  heartbeatTimer = setInterval(() => {
+    if (ws && ws.readyState === WebSocket.OPEN) {
+      ws.send('heartbeat');
+    }
+  }, heartbeatInterval);
+};
+
+/**
+ * 停止心跳
+ */
+const stopHeartbeat = () => {
+  if (heartbeatTimer) {
+    clearInterval(heartbeatTimer);
+    heartbeatTimer = null;
+  }
+};
+
+/**
+ * 检查 WebSocket 连接状态
+ */
+export const isWebSocketConnected = (): boolean => {
+  return ws !== null && ws.readyState === WebSocket.OPEN;
+};
+