Parcourir la source

修复驾驶舱和我的任务以及任务管理的盲板拆除界面显示问题

pm il y a 3 mois
Parent
commit
d1a1ffabcd

+ 263 - 97
src/components/Dashboard.tsx

@@ -1,12 +1,13 @@
 import React, { useState, useEffect, useMemo } from '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, Space } from 'antd';
-import { UploadOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons';
+import { Card, Collapse, Modal, Form as AntdForm, Input, Button, message, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert, Space, Image } from 'antd';
+import { UploadOutlined, LockOutlined, CheckCircleOutlined, BarcodeOutlined, SendOutlined, KeyOutlined } from '@ant-design/icons';
 const { Panel } = Collapse;
 import { cockpitApi, CockpitStatisticsVO, JobVO, TaskVO } from '../api/cockpit';
 import { dictDataApi, DictDataVO } from '../api/DictData';
 import { taskManagementApi, MyTaskVO, MyTaskPageParam, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
+import { fileApi } from '../api/file';
 import { managerHomeApi, ManagerWorkCountVO, ManagerWorkItemVO, ManagerDayWorkCountVO } from '../api/managerHome';
 import { dateFormatter } from '../utils/formatTime';
 import { toast } from 'sonner';
@@ -187,6 +188,9 @@ export default function Dashboard() {
   const [approvalLoading, setApprovalLoading] = useState(false);
   const [submitLoading, setSubmitLoading] = useState(false);
   const [showOverdueJobs, setShowOverdueJobs] = useState(false); // 默认隐藏逾期作业
+  const [isolationDeviceNumber, setIsolationDeviceNumber] = useState('');
+  const [isolationFileList, setIsolationFileList] = useState<any[]>([]);
+  const [isolationSubmitLoading, setIsolationSubmitLoading] = useState(false);
 
   // 获取统计数据
   const fetchStatistics = async () => {
@@ -1217,6 +1221,15 @@ export default function Dashboard() {
         (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
       );
       
+      // 打开盲板/拆除详情时先清空设备编号和附件,后续若有 formData 再回填
+      if (isIsolation || isReleaseIsolation) {
+        const isolationType = String(detailDataWithWorkInfo.isolationType ?? '').trim();
+        if (isolationType === '0' || isolationType === '2') {
+          setIsolationDeviceNumber('');
+          setIsolationFileList([]);
+        }
+      }
+      
       // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
       if (isApproved && hasFormData) {
         console.log('Dashboard: ✅ 任务已通过,从 formData 中解析表单结构');
@@ -1303,6 +1316,21 @@ export default function Dashboard() {
               taskDetailForm.setFieldsValue(convertedFormValues);
               console.log('Dashboard: 表单数据已回填(从 formData)', convertedFormValues);
             }, 100);
+          } else if (
+            parsedFormData.deviceNumber !== undefined ||
+            (Array.isArray(parsedFormData.attachments) && parsedFormData.attachments.length > 0)
+          ) {
+            setIsolationDeviceNumber(parsedFormData.deviceNumber ?? '');
+            const list = Array.isArray(parsedFormData.attachments) ? parsedFormData.attachments : [];
+            setIsolationFileList(
+              list.map((item: any, idx: number) => ({
+                uid: `echo-${idx}-${item.url || item.name || idx}`,
+                name: item.name || `文件${idx + 1}`,
+                url: item.url || item.response,
+                status: 'done',
+                response: item.url || item.response,
+              }))
+            );
           } else {
             console.warn('Dashboard: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
             message.warning(t('cockpit.formDataIncomplete'));
@@ -2332,6 +2360,8 @@ export default function Dashboard() {
           setOriginalConf('');
           taskDetailForm.resetFields();
           setApprovalComment('');
+          setIsolationDeviceNumber('');
+          setIsolationFileList([]);
         }}
         footer={null}
         width={1000}
@@ -2403,47 +2433,174 @@ export default function Dashboard() {
 
                 // 隔离/方案节点、解除隔离节点和还锁节点
                 if (isIsolation || isReleaseIsolation || isReturnLock) {
+                  const isolationType = String(taskDetailData?.isolationType ?? '').trim();
+                  const isLockoutTagout = isolationType === '1';
+                  const isBlindPlate = isolationType === '0';
+                  const isDismantle = isolationType === '2';
+                  const showLockCabinet = isReturnLock || (isLockoutTagout && (isIsolation || isReleaseIsolation));
+                  const showDeviceForm = (isIsolation || isReleaseIsolation) && (isBlindPlate || isDismantle);
+
+                  if (showDeviceForm) {
+                    const deviceLabel = isReleaseIsolation
+                      ? (isBlindPlate ? t('form.releaseBlindPlateDeviceNo') : t('form.restoreDeviceNo'))
+                      : (isBlindPlate ? t('form.blindPlateDeviceNo') : t('form.dismantleDeviceNo'));
+                    return (
+                      <div key="isolation-device-form" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
+                        <div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)' }}>
+                          <Card
+                            style={{
+                              width: '100%',
+                              maxWidth: 500,
+                              boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
+                              borderRadius: 16,
+                              border: '1px solid rgba(22, 119, 255, 0.15)',
+                              overflow: 'hidden',
+                              background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
+                            }}
+                            bodyStyle={{ padding: 0 }}
+                          >
+                            <div style={{ padding: '24px 32px 28px' }}>
+                              <div style={{ marginBottom: 26 }}>
+                                <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
+                                  <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
+                                    <BarcodeOutlined style={{ fontSize: 14, color: '#1677ff' }} />
+                                  </span>
+                                  {deviceLabel}
+                                </label>
+                                <Input
+                                  value={isolationDeviceNumber}
+                                  onChange={(e) => !isApproved && setIsolationDeviceNumber(e.target.value)}
+                                  placeholder={`请输入${deviceLabel}`}
+                                  maxLength={100}
+                                  size="large"
+                                  disabled={isApproved}
+                                  prefix={<BarcodeOutlined style={{ color: '#91caff', marginRight: 8 }} />}
+                                  style={{ borderRadius: 10, borderColor: '#d9e8ff', fontSize: 15 }}
+                                />
+                              </div>
+                              <div>
+                                <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
+                                  <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
+                                    <UploadOutlined style={{ fontSize: 14, color: '#1677ff' }} />
+                                  </span>
+                                  附件上传
+                                  {!isApproved && <span style={{ marginLeft: 8, fontSize: 12, fontWeight: 400, color: '#69b1ff', background: '#e6f4ff', padding: '2px 8px', borderRadius: 6 }}>支持拖拽</span>}
+                                </label>
+                                {isApproved && isolationFileList.length > 0 ? (
+                                  <div style={{ borderRadius: 12, padding: '32px 20px', background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)', border: '2px solid #91caff', minHeight: 180, display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'center', justifyContent: 'center', alignContent: 'center' }}>
+                                    <Image.PreviewGroup>
+                                      {isolationFileList.filter((f: any) => /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String((f.url || f.response) || '')) || /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''))).map((file: any) => (
+                                        <Image key={file.uid} width={160} height={160} src={file.url || file.response} style={{ objectFit: 'cover', borderRadius: 10, cursor: 'pointer', flexShrink: 0 }} alt={file.name} />
+                                      ))}
+                                    </Image.PreviewGroup>
+                                    {isolationFileList.filter((f: any) => !/\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String((f.url || f.response) || '')) && !/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''))).map((file: any) => (
+                                      <a key={file.uid} href={file.url || file.response} target="_blank" rel="noopener noreferrer" style={{ display: 'block', padding: '12px 16px', background: '#fff', borderRadius: 10, color: '#1677ff', border: '1px solid #91caff', maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name}</a>
+                                    ))}
+                                  </div>
+                                ) : isApproved ? (
+                                  <div style={{ padding: 24, textAlign: 'center', color: '#8c8c8c', background: '#fafafa', borderRadius: 12 }}>暂无附件</div>
+                                ) : (
+                                  <Upload.Dragger
+                                    fileList={isolationFileList}
+                                    onChange={({ fileList }) => setIsolationFileList(fileList.slice(-5))}
+                                    multiple
+                                    showUploadList={{ showRemoveIcon: true }}
+                                    customRequest={async ({ file, onSuccess, onError }) => {
+                                      try {
+                                        const url = await fileApi.upload(file as File);
+                                        onSuccess?.(url);
+                                      } catch (e: any) {
+                                        onError?.(e);
+                                        message.error(e?.message || '文件上传失败');
+                                      }
+                                    }}
+                                    style={{ borderRadius: 12, padding: '32px 20px', background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)', border: '2px dashed #91caff' }}
+                                  >
+                                    <p className="ant-upload-drag-icon" style={{ marginBottom: 12 }}>
+                                      <span style={{ width: 64, height: 64, borderRadius: 16, background: 'linear-gradient(135deg, rgba(22,119,255,0.15) 0%, rgba(64,150,255,0.1) 100%)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
+                                        <UploadOutlined style={{ fontSize: 32, color: '#1677ff' }} />
+                                      </span>
+                                    </p>
+                                    <p className="ant-upload-text" style={{ margin: 0, color: '#0958d9', fontSize: 15, fontWeight: 500 }}>点击或拖拽文件到此区域上传</p>
+                                    <p className="ant-upload-hint" style={{ margin: '8px 0 0', color: '#69b1ff', fontSize: 13 }}>最多 5 个文件,支持多选</p>
+                                  </Upload.Dragger>
+                                )}
+                              </div>
+                            </div>
+                          </Card>
+                        </div>
+                        <div className="flex justify-end gap-3" style={{ padding: '20px 24px', borderTop: '1px solid rgba(22, 119, 255, 0.1)', flexShrink: 0, background: 'linear-gradient(0deg, #f0f7ff 0%, #fafcff 60%, #fff 100%)' }}>
+                          {!isApproved ? (
+                            <Button
+                              type="primary"
+                              loading={isolationSubmitLoading}
+                              onClick={async () => {
+                                const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                                if (nodeId == null) {
+                                  message.error(t('cockpit.nodeIdNotExist'));
+                                  return;
+                                }
+                                const uploadedFiles = isolationFileList.filter((f: any) => f.status === 'done' && (f.response != null));
+                                const uploadingCount = isolationFileList.filter((f: any) => f.status === 'uploading').length;
+                                if (uploadingCount > 0) {
+                                  message.warning('请等待文件上传完成');
+                                  return;
+                                }
+                                if (isolationFileList.length > 0 && uploadedFiles.length === 0) {
+                                  message.warning('请等待文件上传完成或移除未上传成功的文件');
+                                  return;
+                                }
+                                setIsolationSubmitLoading(true);
+                                try {
+                                  const attachmentUrls = uploadedFiles.map((f: any) => {
+                                    const url = typeof f.response === 'string' ? f.response : (f.response?.url ?? '');
+                                    return { name: f.name, url };
+                                  });
+                                  const payload = { deviceNumber: isolationDeviceNumber, attachments: attachmentUrls };
+                                  await taskManagementApi.updateNodeApproval({
+                                    nodeId: typeof nodeId === 'number' ? nodeId : Number(nodeId),
+                                    approvalStatus: 'approved',
+                                    formData: JSON.stringify(payload),
+                                  });
+                                  message.success(t('common.submit') + t('common.success'));
+                                  setTaskDetailVisible(false);
+                                  setTaskDetailData(null);
+                                  setIsolationDeviceNumber('');
+                                  setIsolationFileList([]);
+                                  fetchStatistics();
+                                } catch (error: any) {
+                                  message.error(error?.message || '提交失败');
+                                } finally {
+                                  setIsolationSubmitLoading(false);
+                                }
+                              }}
+                              icon={<SendOutlined />}
+                              style={{ minWidth: 120, height: 44, borderRadius: 10, fontSize: 15, fontWeight: 500, boxShadow: '0 4px 14px rgba(22, 119, 255, 0.35)' }}
+                            >
+                              {t('common.submit')}
+                            </Button>
+                          ) : (
+                            <Button onClick={() => { setTaskDetailVisible(false); setTaskDetailData(null); setIsolationDeviceNumber(''); setIsolationFileList([]); }} style={{ minWidth: 120, height: 44, borderRadius: 10, fontSize: 15, fontWeight: 500 }}>
+                              {t('common.cancel')}
+                            </Button>
+                          )}
+                        </div>
+                      </div>
+                    );
+                  }
+
                   return (
-                    <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' }}>
-                          <LockOutlined 
-                            style={{ 
-                              fontSize: '64px', 
-                              color: '#1890ff',
-                              display: 'block'
-                            }} 
-                          />
+                    <div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)' }}>
+                      <Card style={{ width: '100%', maxWidth: 500, textAlign: 'center', borderRadius: 16, boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)', border: '1px solid rgba(22, 119, 255, 0.15)', background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)' }} bodyStyle={{ padding: '48px 40px' }}>
+                        <div style={{ marginBottom: 28 }}>
+                          <span style={{ width: 88, height: 88, borderRadius: 20, background: 'linear-gradient(135deg, #1677ff 0%, #4096ff 50%, #69b1ff 100%)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 8px 24px rgba(22, 119, 255, 0.35)' }}>
+                            <LockOutlined style={{ fontSize: 42, color: '#fff' }} />
+                          </span>
                         </div>
-                        <div
-                          style={{
-                            fontSize: '20px',
-                            fontWeight: 500,
-                            color: '#333',
-                            lineHeight: '1.6'
-                          }}
-                        >
-                          此节点需要在锁柜系统中进行操作
+                        <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6, marginBottom: 8 }}>{t('form.lockCabinetTip')}</div>
+                        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, color: '#69b1ff' }}>
+                          <KeyOutlined />
+                          <span>完成取锁、取钥匙后,任务将自动推进</span>
                         </div>
                       </Card>
                     </div>
@@ -2502,47 +2659,50 @@ export default function Dashboard() {
                             })()}
                           </div>
                         ) : (
-                          <div 
+                          <div
                             style={{
                               display: 'flex',
                               justifyContent: 'center',
                               alignItems: 'center',
                               minHeight: '400px',
-                              padding: '40px 20px'
+                              padding: '24px',
+                              background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)',
                             }}
                           >
                             <Card
                               style={{
                                 width: '100%',
-                                maxWidth: '500px',
+                                maxWidth: 500,
                                 textAlign: 'center',
-                                borderRadius: '12px',
-                                boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
-                                border: '1px solid #e8e8e8'
-                              }}
-                              bodyStyle={{
-                                padding: '40px 30px'
+                                borderRadius: 16,
+                                boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
+                                border: '1px solid rgba(22, 119, 255, 0.15)',
+                                background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
                               }}
+                              bodyStyle={{ padding: '48px 40px' }}
                             >
-                              <div style={{ marginBottom: '24px' }}>
-                                <CheckCircleOutlined 
-                                  style={{ 
-                                    fontSize: '64px', 
-                                    color: '#1890ff',
-                                    display: 'block'
-                                  }} 
-                                />
+                              <div style={{ marginBottom: 24 }}>
+                                <span
+                                  style={{
+                                    width: 88,
+                                    height: 88,
+                                    borderRadius: 20,
+                                    background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #95de64 100%)',
+                                    display: 'inline-flex',
+                                    alignItems: 'center',
+                                    justifyContent: 'center',
+                                    boxShadow: '0 8px 24px rgba(82, 196, 26, 0.35)',
+                                  }}
+                                >
+                                  <CheckCircleOutlined style={{ fontSize: 42, color: '#fff' }} />
+                                </span>
                               </div>
-                              <div
-                                style={{
-                                  fontSize: '20px',
-                                  fontWeight: 500,
-                                  color: '#333',
-                                  lineHeight: '1.6'
-                                }}
-                              >
+                              <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
                                 无需填写表单,直接点击底部按钮提交即可
                               </div>
+                              <div style={{ marginTop: 12, fontSize: 13, color: '#69b1ff' }}>
+                                确认无误后点击「完成」结束本节点
+                              </div>
                             </Card>
                           </div>
                         )}
@@ -2782,58 +2942,64 @@ export default function Dashboard() {
                           })()}
                         </div>
                       ) : (
-                        <div 
+                        <div
                           style={{
+                            flex: 1,
+                            minHeight: 0,
+                            overflow: 'hidden',
                             display: 'flex',
-                            justifyContent: 'center',
                             alignItems: 'center',
-                            minHeight: '400px',
-                            padding: '40px 20px'
+                            justifyContent: 'center',
+                            padding: '24px',
+                            background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)',
                           }}
                         >
                           <Card
                             style={{
                               width: '100%',
-                              maxWidth: '500px',
+                              maxWidth: 500,
                               textAlign: 'center',
-                              borderRadius: '12px',
-                              boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
-                              border: '1px solid #e8e8e8'
-                            }}
-                            bodyStyle={{
-                              padding: '40px 30px'
+                              borderRadius: 16,
+                              boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
+                              border: '1px solid rgba(22, 119, 255, 0.15)',
+                              background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
                             }}
+                            bodyStyle={{ padding: '48px 40px' }}
                           >
-                            <div style={{ marginBottom: '24px' }}>
-                              <CheckCircleOutlined 
-                                style={{ 
-                                  fontSize: '64px', 
-                                  color: '#1890ff',
-                                  display: 'block'
-                                }} 
-                              />
+                            <div style={{ marginBottom: 24 }}>
+                              <span
+                                style={{
+                                  width: 88,
+                                  height: 88,
+                                  borderRadius: 20,
+                                  background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #95de64 100%)',
+                                  display: 'inline-flex',
+                                  alignItems: 'center',
+                                  justifyContent: 'center',
+                                  boxShadow: '0 8px 24px rgba(82, 196, 26, 0.35)',
+                                }}
+                              >
+                                <CheckCircleOutlined style={{ fontSize: 42, color: '#fff' }} />
+                              </span>
                             </div>
-                            <div
-                              style={{
-                                fontSize: '20px',
-                                fontWeight: 500,
-                                color: '#333',
-                                lineHeight: '1.6'
-                              }}
-                            >
+                            <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
                               无需填写表单,直接点击底部按钮提交即可
                             </div>
+                            <div style={{ marginTop: 12, fontSize: 13, color: '#69b1ff' }}>
+                              确认无误后点击「完成」结束本节点
+                            </div>
                           </Card>
                         </div>
                       )}
                     </div>
                     {/* 底部按钮 - 取消 + 提交/完成 */}
-                    <div 
+                    <div
                       className="flex justify-end gap-3"
                       style={{
-                        padding: '16px 24px',
-                        borderTop: '1px solid #f0f0f0',
-                        flexShrink: 0
+                        padding: '20px 24px',
+                        borderTop: '1px solid rgba(22, 119, 255, 0.1)',
+                        flexShrink: 0,
+                        background: 'linear-gradient(0deg, #f0f7ff 0%, #fafcff 60%, #fff 100%)',
                       }}
                     >
                       <Button

+ 263 - 97
src/components/ExecutorDashboard.tsx

@@ -1,10 +1,11 @@
 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, Space } from 'antd';
-import { UploadOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons';
+import { Card, Button, Table, Tag, message, Modal, Form as AntdForm, Input, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert, Space, Image } from 'antd';
+import { UploadOutlined, LockOutlined, CheckCircleOutlined, BarcodeOutlined, SendOutlined, KeyOutlined } from '@ant-design/icons';
 import { executorHomeApi, ExecutorNodeCountVO, ExecutorNodeVO, ExecutorDayCompletedVO } from '../api/executorHome';
 import { myTaskApi, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
+import { fileApi } from '../api/file';
 import { formatDateWithFormat, dateFormatter } from '../utils/formatTime';
 import { toast } from 'sonner';
 import { useNavigate } from 'react-router-dom';
@@ -180,6 +181,9 @@ export default function ExecutorDashboard() {
   const [approvalComment, setApprovalComment] = useState('');
   const [approvalLoading, setApprovalLoading] = useState(false);
   const [submitLoading, setSubmitLoading] = useState(false);
+  const [isolationDeviceNumber, setIsolationDeviceNumber] = useState('');
+  const [isolationFileList, setIsolationFileList] = useState<any[]>([]);
+  const [isolationSubmitLoading, setIsolationSubmitLoading] = useState(false);
   
   // 获取字典数据
   const fetchDictData = async () => {
@@ -1063,6 +1067,15 @@ export default function ExecutorDashboard() {
         (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
       );
       
+      // 打开盲板/拆除详情时先清空设备编号和附件,后续若有 formData 再回填
+      if (isIsolation || isReleaseIsolation) {
+        const isolationType = String(detailDataWithWorkInfo.isolationType ?? '').trim();
+        if (isolationType === '0' || isolationType === '2') {
+          setIsolationDeviceNumber('');
+          setIsolationFileList([]);
+        }
+      }
+      
       // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
       if (isApproved && hasFormData) {
         console.log('ExecutorDashboard: ✅ 任务已通过,从 formData 中解析表单结构');
@@ -1149,6 +1162,21 @@ export default function ExecutorDashboard() {
               taskDetailForm.setFieldsValue(convertedFormValues);
               console.log('ExecutorDashboard: 表单数据已回填(从 formData)', convertedFormValues);
             }, 100);
+          } else if (
+            parsedFormData.deviceNumber !== undefined ||
+            (Array.isArray(parsedFormData.attachments) && parsedFormData.attachments.length > 0)
+          ) {
+            setIsolationDeviceNumber(parsedFormData.deviceNumber ?? '');
+            const list = Array.isArray(parsedFormData.attachments) ? parsedFormData.attachments : [];
+            setIsolationFileList(
+              list.map((item: any, idx: number) => ({
+                uid: `echo-${idx}-${item.url || item.name || idx}`,
+                name: item.name || `文件${idx + 1}`,
+                url: item.url || item.response,
+                status: 'done',
+                response: item.url || item.response,
+              }))
+            );
           } else {
             console.warn('ExecutorDashboard: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
             message.warning(t('cockpit.formDataIncomplete'));
@@ -1663,6 +1691,8 @@ export default function ExecutorDashboard() {
           setOriginalConf('');
           taskDetailForm.resetFields();
           setApprovalComment('');
+          setIsolationDeviceNumber('');
+          setIsolationFileList([]);
         }}
         footer={null}
         width={1000}
@@ -1734,47 +1764,174 @@ export default function ExecutorDashboard() {
 
                 // 隔离/方案节点、解除隔离节点和还锁节点
                 if (isIsolation || isReleaseIsolation || isReturnLock) {
+                  const isolationType = String(taskDetailData?.isolationType ?? '').trim();
+                  const isLockoutTagout = isolationType === '1';
+                  const isBlindPlate = isolationType === '0';
+                  const isDismantle = isolationType === '2';
+                  const showLockCabinet = isReturnLock || (isLockoutTagout && (isIsolation || isReleaseIsolation));
+                  const showDeviceForm = (isIsolation || isReleaseIsolation) && (isBlindPlate || isDismantle);
+
+                  if (showDeviceForm) {
+                    const deviceLabel = isReleaseIsolation
+                      ? (isBlindPlate ? t('form.releaseBlindPlateDeviceNo') : t('form.restoreDeviceNo'))
+                      : (isBlindPlate ? t('form.blindPlateDeviceNo') : t('form.dismantleDeviceNo'));
+                    return (
+                      <div key="isolation-device-form" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
+                        <div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)' }}>
+                          <Card
+                            style={{
+                              width: '100%',
+                              maxWidth: 500,
+                              boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
+                              borderRadius: 16,
+                              border: '1px solid rgba(22, 119, 255, 0.15)',
+                              overflow: 'hidden',
+                              background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
+                            }}
+                            bodyStyle={{ padding: 0 }}
+                          >
+                            <div style={{ padding: '24px 32px 28px' }}>
+                              <div style={{ marginBottom: 26 }}>
+                                <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
+                                  <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
+                                    <BarcodeOutlined style={{ fontSize: 14, color: '#1677ff' }} />
+                                  </span>
+                                  {deviceLabel}
+                                </label>
+                                <Input
+                                  value={isolationDeviceNumber}
+                                  onChange={(e) => !isApproved && setIsolationDeviceNumber(e.target.value)}
+                                  placeholder={`请输入${deviceLabel}`}
+                                  maxLength={100}
+                                  size="large"
+                                  disabled={isApproved}
+                                  prefix={<BarcodeOutlined style={{ color: '#91caff', marginRight: 8 }} />}
+                                  style={{ borderRadius: 10, borderColor: '#d9e8ff', fontSize: 15 }}
+                                />
+                              </div>
+                              <div>
+                                <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
+                                  <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
+                                    <UploadOutlined style={{ fontSize: 14, color: '#1677ff' }} />
+                                  </span>
+                                  附件上传
+                                  {!isApproved && <span style={{ marginLeft: 8, fontSize: 12, fontWeight: 400, color: '#69b1ff', background: '#e6f4ff', padding: '2px 8px', borderRadius: 6 }}>支持拖拽</span>}
+                                </label>
+                                {isApproved && isolationFileList.length > 0 ? (
+                                  <div style={{ borderRadius: 12, padding: '32px 20px', background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)', border: '2px solid #91caff', minHeight: 180, display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'center', justifyContent: 'center', alignContent: 'center' }}>
+                                    <Image.PreviewGroup>
+                                      {isolationFileList.filter((f: any) => /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String((f.url || f.response) || '')) || /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''))).map((file: any) => (
+                                        <Image key={file.uid} width={160} height={160} src={file.url || file.response} style={{ objectFit: 'cover', borderRadius: 10, cursor: 'pointer', flexShrink: 0 }} alt={file.name} />
+                                      ))}
+                                    </Image.PreviewGroup>
+                                    {isolationFileList.filter((f: any) => !/\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String((f.url || f.response) || '')) && !/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''))).map((file: any) => (
+                                      <a key={file.uid} href={file.url || file.response} target="_blank" rel="noopener noreferrer" style={{ display: 'block', padding: '12px 16px', background: '#fff', borderRadius: 10, color: '#1677ff', border: '1px solid #91caff', maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name}</a>
+                                    ))}
+                                  </div>
+                                ) : isApproved ? (
+                                  <div style={{ padding: 24, textAlign: 'center', color: '#8c8c8c', background: '#fafafa', borderRadius: 12 }}>暂无附件</div>
+                                ) : (
+                                  <Upload.Dragger
+                                    fileList={isolationFileList}
+                                    onChange={({ fileList }) => setIsolationFileList(fileList.slice(-5))}
+                                    multiple
+                                    showUploadList={{ showRemoveIcon: true }}
+                                    customRequest={async ({ file, onSuccess, onError }) => {
+                                      try {
+                                        const url = await fileApi.upload(file as File);
+                                        onSuccess?.(url);
+                                      } catch (e: any) {
+                                        onError?.(e);
+                                        message.error(e?.message || '文件上传失败');
+                                      }
+                                    }}
+                                    style={{ borderRadius: 12, padding: '32px 20px', background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)', border: '2px dashed #91caff' }}
+                                  >
+                                    <p className="ant-upload-drag-icon" style={{ marginBottom: 12 }}>
+                                      <span style={{ width: 64, height: 64, borderRadius: 16, background: 'linear-gradient(135deg, rgba(22,119,255,0.15) 0%, rgba(64,150,255,0.1) 100%)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
+                                        <UploadOutlined style={{ fontSize: 32, color: '#1677ff' }} />
+                                      </span>
+                                    </p>
+                                    <p className="ant-upload-text" style={{ margin: 0, color: '#0958d9', fontSize: 15, fontWeight: 500 }}>点击或拖拽文件到此区域上传</p>
+                                    <p className="ant-upload-hint" style={{ margin: '8px 0 0', color: '#69b1ff', fontSize: 13 }}>最多 5 个文件,支持多选</p>
+                                  </Upload.Dragger>
+                                )}
+                              </div>
+                            </div>
+                          </Card>
+                        </div>
+                        <div className="flex justify-end gap-3" style={{ padding: '20px 24px', borderTop: '1px solid rgba(22, 119, 255, 0.1)', flexShrink: 0, background: 'linear-gradient(0deg, #f0f7ff 0%, #fafcff 60%, #fff 100%)' }}>
+                          {!isApproved ? (
+                            <Button
+                              type="primary"
+                              loading={isolationSubmitLoading}
+                              onClick={async () => {
+                                const nodeId = taskDetailData.nodeId ?? taskDetailData.id;
+                                if (nodeId == null) {
+                                  message.warning(t('cockpit.nodeIdNotExist'));
+                                  return;
+                                }
+                                const uploadedFiles = isolationFileList.filter((f: any) => f.status === 'done' && (f.response != null));
+                                const uploadingCount = isolationFileList.filter((f: any) => f.status === 'uploading').length;
+                                if (uploadingCount > 0) {
+                                  message.warning('请等待文件上传完成');
+                                  return;
+                                }
+                                if (isolationFileList.length > 0 && uploadedFiles.length === 0) {
+                                  message.warning('请等待文件上传完成或移除未上传成功的文件');
+                                  return;
+                                }
+                                setIsolationSubmitLoading(true);
+                                try {
+                                  const attachmentUrls = uploadedFiles.map((f: any) => {
+                                    const url = typeof f.response === 'string' ? f.response : (f.response?.url ?? '');
+                                    return { name: f.name, url };
+                                  });
+                                  const payload = { deviceNumber: isolationDeviceNumber, attachments: attachmentUrls };
+                                  await myTaskApi.updateNodeApproval({
+                                    nodeId: typeof nodeId === 'number' ? nodeId : Number(nodeId),
+                                    approvalStatus: 'approved',
+                                    formData: JSON.stringify(payload),
+                                  });
+                                  message.success(t('common.submit') + t('common.success'));
+                                  setTaskDetailVisible(false);
+                                  setTaskDetailData(null);
+                                  setIsolationDeviceNumber('');
+                                  setIsolationFileList([]);
+                                  fetchAllData();
+                                } catch (error: any) {
+                                  message.error(error?.message || '提交失败');
+                                } finally {
+                                  setIsolationSubmitLoading(false);
+                                }
+                              }}
+                              icon={<SendOutlined />}
+                              style={{ minWidth: 120, height: 44, borderRadius: 10, fontSize: 15, fontWeight: 500, boxShadow: '0 4px 14px rgba(22, 119, 255, 0.35)' }}
+                            >
+                              {t('common.submit')}
+                            </Button>
+                          ) : (
+                            <Button onClick={() => { setTaskDetailVisible(false); setTaskDetailData(null); setIsolationDeviceNumber(''); setIsolationFileList([]); }} style={{ minWidth: 120, height: 44, borderRadius: 10, fontSize: 15, fontWeight: 500 }}>
+                              {t('common.cancel')}
+                            </Button>
+                          )}
+                        </div>
+                      </div>
+                    );
+                  }
+
                   return (
-                    <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' }}>
-                          <LockOutlined 
-                            style={{ 
-                              fontSize: '64px', 
-                              color: '#1890ff',
-                              display: 'block'
-                            }} 
-                          />
+                    <div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)' }}>
+                      <Card style={{ width: '100%', maxWidth: 500, textAlign: 'center', borderRadius: 16, boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)', border: '1px solid rgba(22, 119, 255, 0.15)', background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)' }} bodyStyle={{ padding: '48px 40px' }}>
+                        <div style={{ marginBottom: 28 }}>
+                          <span style={{ width: 88, height: 88, borderRadius: 20, background: 'linear-gradient(135deg, #1677ff 0%, #4096ff 50%, #69b1ff 100%)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 8px 24px rgba(22, 119, 255, 0.35)' }}>
+                            <LockOutlined style={{ fontSize: 42, color: '#fff' }} />
+                          </span>
                         </div>
-                        <div
-                          style={{
-                            fontSize: '20px',
-                            fontWeight: 500,
-                            color: '#333',
-                            lineHeight: '1.6'
-                          }}
-                        >
-                          此节点需要在锁柜系统中进行操作
+                        <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6, marginBottom: 8 }}>{t('form.lockCabinetTip')}</div>
+                        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, color: '#69b1ff' }}>
+                          <KeyOutlined />
+                          <span>完成取锁、取钥匙后,任务将自动推进</span>
                         </div>
                       </Card>
                     </div>
@@ -1833,47 +1990,50 @@ export default function ExecutorDashboard() {
                             })()}
                           </div>
                         ) : (
-                          <div 
+                          <div
                             style={{
                               display: 'flex',
                               justifyContent: 'center',
                               alignItems: 'center',
                               minHeight: '400px',
-                              padding: '40px 20px'
+                              padding: '24px',
+                              background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)',
                             }}
                           >
                             <Card
                               style={{
                                 width: '100%',
-                                maxWidth: '500px',
+                                maxWidth: 500,
                                 textAlign: 'center',
-                                borderRadius: '12px',
-                                boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
-                                border: '1px solid #e8e8e8'
-                              }}
-                              bodyStyle={{
-                                padding: '40px 30px'
+                                borderRadius: 16,
+                                boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
+                                border: '1px solid rgba(22, 119, 255, 0.15)',
+                                background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
                               }}
+                              bodyStyle={{ padding: '48px 40px' }}
                             >
-                              <div style={{ marginBottom: '24px' }}>
-                                <CheckCircleOutlined 
-                                  style={{ 
-                                    fontSize: '64px', 
-                                    color: '#1890ff',
-                                    display: 'block'
-                                  }} 
-                                />
+                              <div style={{ marginBottom: 24 }}>
+                                <span
+                                  style={{
+                                    width: 88,
+                                    height: 88,
+                                    borderRadius: 20,
+                                    background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #95de64 100%)',
+                                    display: 'inline-flex',
+                                    alignItems: 'center',
+                                    justifyContent: 'center',
+                                    boxShadow: '0 8px 24px rgba(82, 196, 26, 0.35)',
+                                  }}
+                                >
+                                  <CheckCircleOutlined style={{ fontSize: 42, color: '#fff' }} />
+                                </span>
                               </div>
-                              <div
-                                style={{
-                                  fontSize: '20px',
-                                  fontWeight: 500,
-                                  color: '#333',
-                                  lineHeight: '1.6'
-                                }}
-                              >
+                              <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
                                 无需填写表单,直接点击底部按钮提交即可
                               </div>
+                              <div style={{ marginTop: 12, fontSize: 13, color: '#69b1ff' }}>
+                                确认无误后点击「完成」结束本节点
+                              </div>
                             </Card>
                           </div>
                         )}
@@ -2121,58 +2281,64 @@ export default function ExecutorDashboard() {
                           })()}
                         </div>
                       ) : (
-                        <div 
+                        <div
                           style={{
+                            flex: 1,
+                            minHeight: 0,
+                            overflow: 'hidden',
                             display: 'flex',
-                            justifyContent: 'center',
                             alignItems: 'center',
-                            minHeight: '400px',
-                            padding: '40px 20px'
+                            justifyContent: 'center',
+                            padding: '24px',
+                            background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)',
                           }}
                         >
                           <Card
                             style={{
                               width: '100%',
-                              maxWidth: '500px',
+                              maxWidth: 500,
                               textAlign: 'center',
-                              borderRadius: '12px',
-                              boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
-                              border: '1px solid #e8e8e8'
-                            }}
-                            bodyStyle={{
-                              padding: '40px 30px'
+                              borderRadius: 16,
+                              boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
+                              border: '1px solid rgba(22, 119, 255, 0.15)',
+                              background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
                             }}
+                            bodyStyle={{ padding: '48px 40px' }}
                           >
-                            <div style={{ marginBottom: '24px' }}>
-                              <CheckCircleOutlined 
-                                style={{ 
-                                  fontSize: '64px', 
-                                  color: '#1890ff',
-                                  display: 'block'
-                                }} 
-                              />
+                            <div style={{ marginBottom: 24 }}>
+                              <span
+                                style={{
+                                  width: 88,
+                                  height: 88,
+                                  borderRadius: 20,
+                                  background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #95de64 100%)',
+                                  display: 'inline-flex',
+                                  alignItems: 'center',
+                                  justifyContent: 'center',
+                                  boxShadow: '0 8px 24px rgba(82, 196, 26, 0.35)',
+                                }}
+                              >
+                                <CheckCircleOutlined style={{ fontSize: 42, color: '#fff' }} />
+                              </span>
                             </div>
-                            <div
-                              style={{
-                                fontSize: '20px',
-                                fontWeight: 500,
-                                color: '#333',
-                                lineHeight: '1.6'
-                              }}
-                            >
+                            <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
                               无需填写表单,直接点击底部按钮提交即可
                             </div>
+                            <div style={{ marginTop: 12, fontSize: 13, color: '#69b1ff' }}>
+                              确认无误后点击「完成」结束本节点
+                            </div>
                           </Card>
                         </div>
                       )}
                     </div>
                     {/* 底部按钮 - 取消 + 提交/完成 */}
-                    <div 
+                    <div
                       className="flex justify-end gap-3"
                       style={{
-                        padding: '16px 24px',
-                        borderTop: '1px solid #f0f0f0',
-                        flexShrink: 0
+                        padding: '20px 24px',
+                        borderTop: '1px solid rgba(22, 119, 255, 0.1)',
+                        flexShrink: 0,
+                        background: 'linear-gradient(0deg, #f0f7ff 0%, #fafcff 60%, #fff 100%)',
                       }}
                     >
                       <Button

+ 1 - 1
src/components/MyTask.tsx

@@ -1196,8 +1196,8 @@ export default function MyTask() {
             );
             console.log('MyTask: 盲板/拆除 formData 已回显', { deviceNumber: parsedFormData.deviceNumber, attachmentsCount: list.length });
           } else {
+            // 完成/结束等节点可能无表单内容,不提示“表单数据不完整”,有则显示、无则不显示即可
             console.warn('MyTask: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
-            message.warning('表单数据不完整');
           }
         } catch (e) {
           console.error('MyTask: 解析 formData 失败', e);

+ 1 - 1
src/components/TaskManagement.tsx

@@ -1209,8 +1209,8 @@ export default function TaskManagement() {
             );
             console.log('TaskManagement: 盲板/拆除 formData 已回显', { deviceNumber: parsedFormData.deviceNumber, attachmentsCount: list.length });
           } else {
+            // 完成/结束等节点可能无表单内容,不提示“表单数据不完整”,有则显示、无则不显示即可
             console.warn('TaskManagement: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
-            message.warning('表单数据不完整');
           }
         } catch (e) {
           console.error('TaskManagement: 解析 formData 失败', e);