Parcourir la source

驾驶舱的任务列表也可以直接操作

pm il y a 3 mois
Parent
commit
c6116bc49a
2 fichiers modifiés avec 581 ajouts et 31 suppressions
  1. 285 17
      src/components/Dashboard.tsx
  2. 296 14
      src/components/ExecutorDashboard.tsx

+ 285 - 17
src/components/Dashboard.tsx

@@ -1,7 +1,7 @@
 import React, { useState, useEffect, useMemo } from 'react';
-import { ChevronDown, ChevronUp, Clock, PlayCircle, CheckCircle, AlertTriangle, List, Plus, RefreshCw, AlertCircle, FileText, Rocket } from 'lucide-react';
+import { ChevronDown, ChevronUp, Clock, PlayCircle, CheckCircle, AlertTriangle, List, Plus, RefreshCw, AlertCircle, FileText, Rocket, Eye } from 'lucide-react';
 import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, AreaChart, Area } from 'recharts';
-import { Card, Collapse, Modal, Form as AntdForm, Input, Button, message, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert } from 'antd';
+import { Card, Collapse, Modal, Form as AntdForm, Input, Button, message, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert, Space } from 'antd';
 import { UploadOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons';
 const { Panel } = Collapse;
 import { cockpitApi, CockpitStatisticsVO, JobVO, TaskVO } from '../api/cockpit';
@@ -1959,17 +1959,39 @@ export default function Dashboard() {
                                             </span>
                                           </td>
                                         <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
-                                          <button
-                                            className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
-                                            style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
-                                            onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
-                                            onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
-                                            onClick={() => {
-                                              handleViewTaskDetail(task);
-                                            }}
-                                          >
-                                            查看
-                                          </button>
+                                          {(() => {
+                                            const statusText = getTaskStatusText(taskStatus);
+                                            const statusTextLower = statusText.toLowerCase();
+                                            const isInProgress = statusTextLower.includes('进行中') || statusTextLower.includes('待审核') || statusTextLower.includes('pending') || statusTextLower.includes('执行中');
+                                            const isCompleted = statusTextLower.includes('已完成') || statusTextLower.includes('已通过') || statusTextLower.includes('approved') || statusTextLower.includes('完成') || statusTextLower.includes('执行完成');
+                                            const buttonText = isInProgress ? t('form.handleNow') : (isCompleted ? t('form.viewDetail') : t('form.viewDetail'));
+                                            return (
+                                              <Space size="small">
+                                                <Button
+                                                  type="link"
+                                                  size="small"
+                                                  icon={<Eye className="w-4 h-4" style={{ color: '#000000' }} />}
+                                                  onClick={(e) => {
+                                                    e.stopPropagation();
+                                                    e.preventDefault();
+                                                    handleViewTaskDetail(task);
+                                                  }}
+                                                  style={{ color: '#000000' }}
+                                                  className="transition-colors hover:text-[#1677ff] hover:underline"
+                                                  onMouseEnter={(e) => {
+                                                    e.currentTarget.style.color = '#1677ff';
+                                                    e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
+                                                  }}
+                                                  onMouseLeave={(e) => {
+                                                    e.currentTarget.style.color = '#000000';
+                                                    e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
+                                                  }}
+                                                >
+                                                  {buttonText}
+                                                </Button>
+                                              </Space>
+                                            );
+                                          })()}
                                         </td>
                                         </tr>
                                       );
@@ -2304,6 +2326,12 @@ export default function Dashboard() {
         onCancel={() => {
           setTaskDetailVisible(false);
           setTaskDetailData(null);
+          setFormData({ rule: [], option: {} });
+          setFormLoading(false);
+          setOriginalFields([]);
+          setOriginalConf('');
+          taskDetailForm.resetFields();
+          setApprovalComment('');
         }}
         footer={null}
         width={1000}
@@ -2529,14 +2557,14 @@ export default function Dashboard() {
                               flexShrink: 0
                             }}
                           >
-                            审核意见
+                            {t('form.reviewComment')}
                           </label>
                           <div className="flex-1">
                             <Input.TextArea
                               rows={4}
                               value={approvalComment}
                               onChange={(e) => setApprovalComment(e.target.value)}
-                              placeholder="请输入审核意见(可选)"
+                              placeholder={t('form.reviewCommentPlaceholder')}
                               maxLength={500}
                               showCount
                               disabled={isApproved}
@@ -2545,7 +2573,160 @@ export default function Dashboard() {
                           </div>
                         </div>
                       </div>
-                      {/* 底部按钮 - 审核节点不显示按钮(只读模式) */}
+                      {/* 底部按钮 - 审核通过/不通过 */}
+                      <div 
+                        className="flex justify-end gap-3"
+                        style={{
+                          padding: '16px 24px',
+                          borderTop: '1px solid #f0f0f0',
+                          flexShrink: 0
+                        }}
+                      >
+                        <Button
+                          danger
+                          loading={approvalLoading}
+                          disabled={isApproved}
+                          onClick={async () => {
+                            const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                            if (nodeId == null) {
+                              message.error(t('cockpit.nodeIdNotExist'));
+                              return;
+                            }
+                            const numNodeId = typeof nodeId === 'number' ? nodeId : Number(nodeId);
+                            if (formData.rule && formData.rule.length > 0) {
+                              try {
+                                await taskDetailForm.validateFields();
+                              } catch (err: any) {
+                                if (err?.errorFields?.length) {
+                                  message.error(err.errorFields[0]?.errors?.[0] || '请完善表单内容');
+                                } else {
+                                  message.error('请完善表单内容');
+                                }
+                                return;
+                              }
+                            }
+                            setApprovalLoading(true);
+                            try {
+                              let formDataString: string | undefined;
+                              if (formData.rule?.length && originalFields.length > 0) {
+                                const values = taskDetailForm.getFieldsValue();
+                                const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                  try {
+                                    const fieldObj = JSON.parse(fieldString);
+                                    const fieldName = fieldObj.name || fieldObj.field;
+                                    const fieldValue = values[fieldName];
+                                    fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                    return JSON.stringify(fieldObj);
+                                  } catch (e) {
+                                    return fieldString;
+                                  }
+                                });
+                                formDataString = JSON.stringify({
+                                  id: taskDetailData.formId ?? taskDetailData.id,
+                                  name: taskDetailData.nodeName || taskDetailData.workName || '未知',
+                                  conf: originalConf,
+                                  fields: fieldsWithValues
+                                });
+                              }
+                              const params: UpdateNodeApprovalParam = {
+                                nodeId: numNodeId,
+                                approvalStatus: 'rejected',
+                                approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
+                              };
+                              await taskManagementApi.updateNodeApproval(params);
+                              message.success(t('form.reviewRejectSuccess'));
+                              setTaskDetailVisible(false);
+                              setTaskDetailData(null);
+                              setFormData({ rule: [], option: {} });
+                              setFormLoading(false);
+                              setOriginalFields([]);
+                              setOriginalConf('');
+                              taskDetailForm.resetFields();
+                              setApprovalComment('');
+                              fetchStatistics();
+                            } catch (error: any) {
+                              message.error(error?.message || t('form.reviewRejectFailed'));
+                            } finally {
+                              setApprovalLoading(false);
+                            }
+                          }}
+                        >
+                          {t('form.reviewReject')}
+                        </Button>
+                        <Button
+                          type="primary"
+                          loading={approvalLoading}
+                          disabled={isApproved}
+                          onClick={async () => {
+                            const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                            if (nodeId == null) {
+                              message.error(t('cockpit.nodeIdNotExist'));
+                              return;
+                            }
+                            const numNodeId = typeof nodeId === 'number' ? nodeId : Number(nodeId);
+                            if (formData.rule && formData.rule.length > 0) {
+                              try {
+                                await taskDetailForm.validateFields();
+                              } catch (err: any) {
+                                if (err?.errorFields?.length) {
+                                  message.error(err.errorFields[0]?.errors?.[0] || '请完善表单内容');
+                                } else {
+                                  message.error('请完善表单内容');
+                                }
+                                return;
+                              }
+                            }
+                            setApprovalLoading(true);
+                            try {
+                              let formDataString: string | undefined;
+                              if (formData.rule?.length && originalFields.length > 0) {
+                                const values = taskDetailForm.getFieldsValue();
+                                const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                  try {
+                                    const fieldObj = JSON.parse(fieldString);
+                                    const fieldName = fieldObj.name || fieldObj.field;
+                                    const fieldValue = values[fieldName];
+                                    fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                    return JSON.stringify(fieldObj);
+                                  } catch (e) {
+                                    return fieldString;
+                                  }
+                                });
+                                formDataString = JSON.stringify({
+                                  id: taskDetailData.formId ?? taskDetailData.id,
+                                  name: taskDetailData.nodeName || taskDetailData.workName || '未知',
+                                  conf: originalConf,
+                                  fields: fieldsWithValues
+                                });
+                              }
+                              const params: UpdateNodeApprovalParam = {
+                                nodeId: numNodeId,
+                                approvalStatus: 'approved',
+                                approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
+                              };
+                              await taskManagementApi.updateNodeApproval(params);
+                              message.success(t('form.reviewApproveSuccess'));
+                              setTaskDetailVisible(false);
+                              setTaskDetailData(null);
+                              setFormData({ rule: [], option: {} });
+                              setFormLoading(false);
+                              setOriginalFields([]);
+                              setOriginalConf('');
+                              taskDetailForm.resetFields();
+                              setApprovalComment('');
+                              fetchStatistics();
+                            } catch (error: any) {
+                              message.error(error?.message || t('form.reviewApproveFailed'));
+                            } finally {
+                              setApprovalLoading(false);
+                            }
+                          }}
+                        >
+                          {t('form.reviewApprove')}
+                        </Button>
+                      </div>
                     </div>
                   );
                 }
@@ -2646,7 +2827,94 @@ export default function Dashboard() {
                         </div>
                       )}
                     </div>
-                    {/* 底部按钮 - 其他节点类型不显示按钮(只读模式) */}
+                    {/* 底部按钮 - 取消 + 提交/完成 */}
+                    <div 
+                      className="flex justify-end gap-3"
+                      style={{
+                        padding: '16px 24px',
+                        borderTop: '1px solid #f0f0f0',
+                        flexShrink: 0
+                      }}
+                    >
+                      <Button
+                        onClick={() => {
+                          setTaskDetailVisible(false);
+                          setTaskDetailData(null);
+                          setFormData({ rule: [], option: {} });
+                          setOriginalFields([]);
+                          setOriginalConf('');
+                          taskDetailForm.resetFields();
+                        }}
+                      >
+                        {t('common.cancel')}
+                      </Button>
+                      <Button
+                        type="primary"
+                        loading={submitLoading}
+                        disabled={isApproved}
+                        onClick={async () => {
+                          const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                          if (nodeId == null) {
+                            message.error(t('cockpit.nodeIdNotExist'));
+                            return;
+                          }
+                          const numNodeId = typeof nodeId === 'number' ? nodeId : Number(nodeId);
+                          setSubmitLoading(true);
+                          try {
+                            const values = formData.rule?.length ? await taskDetailForm.validateFields() : {};
+                            const fieldsWithValues = originalFields.map((fieldString: string) => {
+                              try {
+                                const fieldObj = JSON.parse(fieldString);
+                                const fieldName = fieldObj.name || fieldObj.field;
+                                const fieldValue = values[fieldName];
+                                fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                return JSON.stringify(fieldObj);
+                              } catch (e) {
+                                return fieldString;
+                              }
+                            });
+                            const submitData = {
+                              id: taskDetailData.formId ?? taskDetailData.id,
+                              name: taskDetailData.nodeName || taskDetailData.workName || '未知',
+                              conf: originalConf,
+                              fields: fieldsWithValues
+                            };
+                            const formDataString = JSON.stringify(submitData);
+                            const params: UpdateNodeApprovalParam = {
+                              nodeId: numNodeId,
+                              approvalStatus: 'approved',
+                              approvalOpinion: undefined,
+                              formData: formDataString
+                            };
+                            await taskManagementApi.updateNodeApproval(params);
+                            const nodeType = String(taskDetailData?.type || taskDetailData?.nodeType || '').trim();
+                            const isComplete = nodeType === 'complete';
+                            message.success(isComplete ? t('form.completeSuccess') : t('common.submit') + t('common.success'));
+                            setTaskDetailVisible(false);
+                            setTaskDetailData(null);
+                            setFormData({ rule: [], option: {} });
+                            setFormLoading(false);
+                            setOriginalFields([]);
+                            setOriginalConf('');
+                            taskDetailForm.resetFields();
+                            fetchStatistics();
+                          } catch (error: any) {
+                            if (error?.errorFields) {
+                              message.error('请填写完整的表单内容');
+                            } else {
+                              message.error(error?.message || '提交失败');
+                            }
+                          } finally {
+                            setSubmitLoading(false);
+                          }
+                        }}
+                      >
+                        {(() => {
+                          const nodeType = String(taskDetailData?.type || taskDetailData?.nodeType || '').trim();
+                          return nodeType === 'complete' ? t('form.complete') : t('common.submit');
+                        })()}
+                      </Button>
+                    </div>
                   </div>
                 );
               })()}

+ 296 - 14
src/components/ExecutorDashboard.tsx

@@ -1,10 +1,10 @@
 import React, { useState, useEffect, useMemo } from 'react';
 import { CheckCircle, Eye, ShoppingCart, HelpCircle, PlayCircle, Clock, AlertCircle, Rocket } from 'lucide-react';
 import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
-import { Card, Button, Table, Tag, message, Modal, Form as AntdForm, Input, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert } from 'antd';
+import { Card, Button, Table, Tag, message, Modal, Form as AntdForm, Input, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert, Space } from 'antd';
 import { UploadOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons';
 import { executorHomeApi, ExecutorNodeCountVO, ExecutorNodeVO, ExecutorDayCompletedVO } from '../api/executorHome';
-import { myTaskApi, MyTaskNodeDetailVO } from '../api/mytask';
+import { myTaskApi, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
 import { formatDateWithFormat, dateFormatter } from '../utils/formatTime';
 import { toast } from 'sonner';
 import { useNavigate } from 'react-router-dom';
@@ -178,6 +178,8 @@ export default function ExecutorDashboard() {
   const [originalConf, setOriginalConf] = useState<string>('');
   const [taskDetailForm] = AntdForm.useForm();
   const [approvalComment, setApprovalComment] = useState('');
+  const [approvalLoading, setApprovalLoading] = useState(false);
+  const [submitLoading, setSubmitLoading] = useState(false);
   
   // 获取字典数据
   const fetchDictData = async () => {
@@ -1432,16 +1434,38 @@ export default function ExecutorDashboard() {
     {
       title: t('cockpit.action'),
       key: 'action',
-      width: 100,
+      width: 120,
       render: (_: any, record: ExecutorNodeVO) => {
+        const statusText = getTaskStatusText(record.approvalStatus);
+        const statusTextLower = statusText.toLowerCase();
+        const isInProgress = statusTextLower.includes('进行中') || statusTextLower.includes('待审核') || statusTextLower.includes('pending') || statusTextLower.includes('执行中') || statusTextLower.includes('in progress');
+        const isCompleted = statusTextLower.includes('已完成') || statusTextLower.includes('已通过') || statusTextLower.includes('approved') || statusTextLower.includes('完成') || statusTextLower.includes('执行完成') || statusTextLower.includes('completed');
+        const buttonText = isInProgress ? t('form.handleNow') : (isCompleted ? t('form.viewDetail') : t('form.viewDetail'));
         return (
-          <Button
-            type="primary"
-            size="small"
-            onClick={() => handleViewTask(record)}
-          >
-            查看
-          </Button>
+          <Space size="small">
+            <Button
+              type="link"
+              size="small"
+              icon={<Eye className="w-4 h-4" style={{ color: '#000000' }} />}
+              onClick={(e) => {
+                e.stopPropagation();
+                e.preventDefault();
+                handleViewTask(record);
+              }}
+              style={{ color: '#000000' }}
+              className="transition-colors hover:text-[#1677ff] hover:underline"
+              onMouseEnter={(e) => {
+                e.currentTarget.style.color = '#1677ff';
+                e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
+              }}
+              onMouseLeave={(e) => {
+                e.currentTarget.style.color = '#000000';
+                e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
+              }}
+            >
+              {buttonText}
+            </Button>
+          </Space>
         );
       },
     },
@@ -1633,6 +1657,12 @@ export default function ExecutorDashboard() {
         onCancel={() => {
           setTaskDetailVisible(false);
           setTaskDetailData(null);
+          setFormData({ rule: [], option: {} });
+          setFormLoading(false);
+          setOriginalFields([]);
+          setOriginalConf('');
+          taskDetailForm.resetFields();
+          setApprovalComment('');
         }}
         footer={null}
         width={1000}
@@ -1858,14 +1888,14 @@ export default function ExecutorDashboard() {
                               flexShrink: 0
                             }}
                           >
-                            审核意见
+                            {t('form.reviewComment')}
                           </label>
                           <div className="flex-1">
                             <Input.TextArea
                               rows={4}
                               value={approvalComment}
                               onChange={(e) => setApprovalComment(e.target.value)}
-                              placeholder={t('cockpit.auditOpinionPlaceholder')}
+                              placeholder={t('form.reviewCommentPlaceholder')}
                               maxLength={500}
                               showCount
                               disabled={isApproved}
@@ -1874,7 +1904,168 @@ export default function ExecutorDashboard() {
                           </div>
                         </div>
                       </div>
-                      {/* 底部按钮 - 审核节点不显示按钮(只读模式) */}
+                      {/* 底部按钮 - 审核通过/不通过 */}
+                      <div 
+                        className="flex justify-end gap-3"
+                        style={{
+                          padding: '16px 24px',
+                          borderTop: '1px solid #f0f0f0',
+                          flexShrink: 0
+                        }}
+                      >
+                        <Button
+                          danger
+                          loading={approvalLoading}
+                          disabled={isApproved}
+                          onClick={async () => {
+                            const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                            if (nodeId == null) {
+                              message.warning(t('cockpit.nodeIdNotExist'));
+                              return;
+                            }
+                            const numNodeId = typeof nodeId === 'number' ? nodeId : Number(nodeId);
+                            if (isNaN(numNodeId)) {
+                              message.warning(t('cockpit.nodeIdInvalid'));
+                              return;
+                            }
+                            if (formData.rule && formData.rule.length > 0) {
+                              try {
+                                await taskDetailForm.validateFields();
+                              } catch (err: any) {
+                                if (err?.errorFields?.length) {
+                                  message.error(err.errorFields[0]?.errors?.[0] || '请完善表单内容');
+                                } else {
+                                  message.error('请完善表单内容');
+                                }
+                                return;
+                              }
+                            }
+                            setApprovalLoading(true);
+                            try {
+                              let formDataString: string | undefined;
+                              if (formData.rule?.length && originalFields.length > 0) {
+                                const values = taskDetailForm.getFieldsValue();
+                                const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                  try {
+                                    const fieldObj = JSON.parse(fieldString);
+                                    const fieldName = fieldObj.name || fieldObj.field;
+                                    const fieldValue = values[fieldName];
+                                    fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                    return JSON.stringify(fieldObj);
+                                  } catch (e) {
+                                    return fieldString;
+                                  }
+                                });
+                                formDataString = JSON.stringify({
+                                  id: taskDetailData.formId ?? taskDetailData.id,
+                                  name: taskDetailData.nodeName || taskDetailData.workName || '未知',
+                                  conf: originalConf,
+                                  fields: fieldsWithValues
+                                });
+                              }
+                              const params: UpdateNodeApprovalParam = {
+                                nodeId: numNodeId,
+                                approvalStatus: 'rejected',
+                                approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
+                              };
+                              await myTaskApi.updateNodeApproval(params);
+                              message.success(t('form.reviewRejectSuccess'));
+                              setTaskDetailVisible(false);
+                              setTaskDetailData(null);
+                              setFormData({ rule: [], option: {} });
+                              setFormLoading(false);
+                              setOriginalFields([]);
+                              setOriginalConf('');
+                              taskDetailForm.resetFields();
+                              setApprovalComment('');
+                              fetchAllData();
+                            } catch (error: any) {
+                              message.error(error?.message || t('form.reviewRejectFailed'));
+                            } finally {
+                              setApprovalLoading(false);
+                            }
+                          }}
+                        >
+                          {t('form.reviewReject')}
+                        </Button>
+                        <Button
+                          type="primary"
+                          loading={approvalLoading}
+                          disabled={isApproved}
+                          onClick={async () => {
+                            const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                            if (nodeId == null) {
+                              message.warning(t('cockpit.nodeIdNotExist'));
+                              return;
+                            }
+                            const numNodeId = typeof nodeId === 'number' ? nodeId : Number(nodeId);
+                            if (isNaN(numNodeId)) {
+                              message.warning(t('cockpit.nodeIdInvalid'));
+                              return;
+                            }
+                            if (formData.rule && formData.rule.length > 0) {
+                              try {
+                                await taskDetailForm.validateFields();
+                              } catch (err: any) {
+                                if (err?.errorFields?.length) {
+                                  message.error(err.errorFields[0]?.errors?.[0] || '请完善表单内容');
+                                } else {
+                                  message.error('请完善表单内容');
+                                }
+                                return;
+                              }
+                            }
+                            setApprovalLoading(true);
+                            try {
+                              let formDataString: string | undefined;
+                              if (formData.rule?.length && originalFields.length > 0) {
+                                const values = taskDetailForm.getFieldsValue();
+                                const fieldsWithValues = originalFields.map((fieldString: string) => {
+                                  try {
+                                    const fieldObj = JSON.parse(fieldString);
+                                    const fieldName = fieldObj.name || fieldObj.field;
+                                    const fieldValue = values[fieldName];
+                                    fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                    return JSON.stringify(fieldObj);
+                                  } catch (e) {
+                                    return fieldString;
+                                  }
+                                });
+                                formDataString = JSON.stringify({
+                                  id: taskDetailData.formId ?? taskDetailData.id,
+                                  name: taskDetailData.nodeName || taskDetailData.workName || '未知',
+                                  conf: originalConf,
+                                  fields: fieldsWithValues
+                                });
+                              }
+                              const params: UpdateNodeApprovalParam = {
+                                nodeId: numNodeId,
+                                approvalStatus: 'approved',
+                                approvalOpinion: approvalComment || undefined,
+                                formData: formDataString,
+                              };
+                              await myTaskApi.updateNodeApproval(params);
+                              message.success(t('form.reviewApproveSuccess'));
+                              setTaskDetailVisible(false);
+                              setTaskDetailData(null);
+                              setFormData({ rule: [], option: {} });
+                              setFormLoading(false);
+                              setOriginalFields([]);
+                              setOriginalConf('');
+                              taskDetailForm.resetFields();
+                              setApprovalComment('');
+                              fetchAllData();
+                            } catch (error: any) {
+                              message.error(error?.message || t('form.reviewApproveFailed'));
+                            } finally {
+                              setApprovalLoading(false);
+                            }
+                          }}
+                        >
+                          {t('form.reviewApprove')}
+                        </Button>
+                      </div>
                     </div>
                   );
                 }
@@ -1975,7 +2166,98 @@ export default function ExecutorDashboard() {
                         </div>
                       )}
                     </div>
-                    {/* 底部按钮 - 其他节点类型不显示按钮(只读模式) */}
+                    {/* 底部按钮 - 取消 + 提交/完成 */}
+                    <div 
+                      className="flex justify-end gap-3"
+                      style={{
+                        padding: '16px 24px',
+                        borderTop: '1px solid #f0f0f0',
+                        flexShrink: 0
+                      }}
+                    >
+                      <Button
+                        onClick={() => {
+                          setTaskDetailVisible(false);
+                          setTaskDetailData(null);
+                          setFormData({ rule: [], option: {} });
+                          setOriginalFields([]);
+                          setOriginalConf('');
+                          taskDetailForm.resetFields();
+                        }}
+                      >
+                        {t('common.cancel')}
+                      </Button>
+                      <Button
+                        type="primary"
+                        loading={submitLoading}
+                        disabled={isApproved}
+                        onClick={async () => {
+                          const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                          if (nodeId == null) {
+                            message.warning(t('cockpit.nodeIdNotExist'));
+                            return;
+                          }
+                          const numNodeId = typeof nodeId === 'number' ? nodeId : Number(nodeId);
+                          if (isNaN(numNodeId)) {
+                            message.warning(t('cockpit.nodeIdInvalid'));
+                            return;
+                          }
+                          setSubmitLoading(true);
+                          try {
+                            const values = formData.rule?.length ? await taskDetailForm.validateFields() : {};
+                            const fieldsWithValues = originalFields.map((fieldString: string) => {
+                              try {
+                                const fieldObj = JSON.parse(fieldString);
+                                const fieldName = fieldObj.name || fieldObj.field;
+                                const fieldValue = values[fieldName];
+                                fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
+                                return JSON.stringify(fieldObj);
+                              } catch (e) {
+                                return fieldString;
+                              }
+                            });
+                            const submitData = {
+                              id: taskDetailData.formId ?? taskDetailData.id,
+                              name: taskDetailData.nodeName || taskDetailData.workName || '未知',
+                              conf: originalConf,
+                              fields: fieldsWithValues
+                            };
+                            const formDataString = JSON.stringify(submitData);
+                            const params: UpdateNodeApprovalParam = {
+                              nodeId: numNodeId,
+                              approvalStatus: 'approved',
+                              approvalOpinion: undefined,
+                              formData: formDataString
+                            };
+                            await myTaskApi.updateNodeApproval(params);
+                            const nodeType = String(taskDetailData?.type || taskDetailData?.nodeType || '').trim();
+                            const isComplete = nodeType === 'complete';
+                            message.success(isComplete ? t('form.completeSuccess') : t('common.submit') + t('common.success'));
+                            setTaskDetailVisible(false);
+                            setTaskDetailData(null);
+                            setFormData({ rule: [], option: {} });
+                            setFormLoading(false);
+                            setOriginalFields([]);
+                            setOriginalConf('');
+                            taskDetailForm.resetFields();
+                            fetchAllData();
+                          } catch (error: any) {
+                            if (error?.errorFields) {
+                              message.error('请填写完整的表单内容');
+                            } else {
+                              message.error(error?.message || '提交失败');
+                            }
+                          } finally {
+                            setSubmitLoading(false);
+                          }
+                        }}
+                      >
+                        {(() => {
+                          const nodeType = String(taskDetailData?.type || taskDetailData?.nodeType || '').trim();
+                          return nodeType === 'complete' ? t('form.complete') : t('common.submit');
+                        })()}
+                      </Button>
+                    </div>
                   </div>
                 );
               })()}