Просмотр исходного кода

fix: 更新前端配置及风险相关页面

yangjingjing 22 часов назад
Родитель
Сommit
0612627e3d

+ 0 - 4
.eslintrc-auto-import.json

@@ -5,10 +5,6 @@
     "ComputedRef": true,
     "DirectiveBinding": true,
     "EffectScope": true,
-    "ElLoading": true,
-    "ElMessage": true,
-    "ElMessageBox": true,
-    "ElNotification": true,
     "ExtractDefaultPropTypes": true,
     "ExtractPropTypes": true,
     "ExtractPublicPropTypes": true,

+ 22 - 7
src/utils/request.ts

@@ -135,21 +135,36 @@ service.interceptors.response.use(
         ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
           confirmButtonText: '重新登录',
           cancelButtonText: '取消',
-          type: 'warning'
+          type: 'warning',
+          // 防止点击外部/ESC 关闭时走 catch 而无法 reset 标记
+          closeOnPressEscape: false,
+          closeOnClickModal: false
         }).then(() => {
-          // 静默退出:不调 /auth/logout 后端接口,直接清本地状态
-          // (避免 token 过期时 logoutApi 返回 401 再次触发本拦截器递归弹窗)
-          useUserStore().logout(true).then(() => {
-            isRelogin.show = false;
+          // ★ 关键:先重置标记 + 同步清本地状态,再跳转
+          // (避免 .then 嵌套 await 卡住,导致用户感觉"点了没反应")
+          isRelogin.show = false;
+          try {
+            // 静默退出:不调 /auth/logout 后端接口,直接清本地状态
+            // (避免 token 过期时 logoutApi 返回 401 再次触发本拦截器递归弹窗)
+            useUserStore().logout(true);
+          } catch (e) {
+            console.error('[relogin] 清空本地登录态失败:', e);
+          }
+          // 强制跳转:router.replace 偶发会被中断,加 try/catch + 兜底 location.href
+          try {
             router.replace({
               path: '/login',
               query: {
                 redirect: encodeURIComponent(router.currentRoute.value.fullPath || '/')
               }
             });
-          });
+          } catch (e) {
+            console.error('[relogin] router.replace 失败,改用 location.href 兜底:', e);
+            const redirect = encodeURIComponent(router.currentRoute.value.fullPath || '/');
+            window.location.href = `/login?redirect=${redirect}`;
+          }
         }).catch(() => {
-          // 用户点"取消" — 重置标记,允许下次 401 再次弹窗
+          // 用户点"取消"或弹窗被关闭 — 重置标记,允许下次 401 再次弹窗
           isRelogin.show = false;
         });
       }

+ 179 - 46
src/views/risk/personnel/index.vue

@@ -76,18 +76,14 @@
             <span>{{ maskIdCard(scope.row.idCard) }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="岗位证书" align="center" min-width="200">
+        <el-table-column label="岗位证书" align="center" min-width="120">
           <template #default="scope">
-            <el-tag
-              v-for="(cert, idx) in getPersonnelCerts(scope.row.id)"
-              :key="idx"
-              :type="getCertTypeTag(cert.certType)"
-              size="small"
-              style="margin-right: 4px; margin-bottom: 4px"
-            >
-              {{ cert.certName || cert.certType }}
-            </el-tag>
-            <span v-if="getPersonnelCerts(scope.row.id).length === 0" class="text-placeholder">暂无证书</span>
+            <template v-if="getPersonnelCerts(scope.row.id).length > 0">
+              <el-tag type="primary" size="small" effect="plain">
+                {{ getPersonnelCerts(scope.row.id).length }} 个证书
+              </el-tag>
+            </template>
+            <span v-else class="text-placeholder">暂无证书</span>
           </template>
         </el-table-column>
         <el-table-column label="工作门店" align="center" prop="storeName" :show-overflow-tooltip="true" width="160" />
@@ -118,7 +114,7 @@
     <el-dialog
       v-model="dialog.visible"
       :title="dialog.title"
-      width="820px"
+      width="960px"
       append-to-body
       :close-on-click-modal="false"
       @close="closeDialog"
@@ -246,7 +242,7 @@
               <el-select
                 v-model="cert.certType"
                 placeholder="请选择证书类型"
-                style="width: 220px"
+                style="width: 180px"
               >
                 <el-option
                   v-for="d in certTypeOptions"
@@ -255,6 +251,17 @@
                   :value="d.value"
                 />
               </el-select>
+              <el-date-picker
+                v-model="cert.validPeriod"
+                type="daterange"
+                range-separator="至"
+                start-placeholder="生效日期"
+                end-placeholder="到期日期"
+                value-format="YYYY-MM-DD"
+                format="YYYY-MM-DD"
+                style="width: 240px"
+                unlink-panels
+              />
               <el-upload
                 v-model:file-list="cert.fileList"
                 :http-request="customCertUpload"
@@ -290,20 +297,44 @@
     </el-dialog>
 
     <!-- 查看证书弹窗 -->
-    <el-dialog v-model="certDialog.visible" title="岗位证书" width="640px" append-to-body>
-      <el-descriptions v-if="currentCerts.length > 0" :column="1" border>
+    <el-dialog v-model="certDialog.visible" title="岗位证书" width="720px" append-to-body>
+      <el-descriptions v-if="currentCerts.length > 0" :column="2" border>
         <el-descriptions-item
           v-for="(cert, idx) in currentCerts"
           :key="idx"
           :label="cert.certName || cert.certType"
+          :span="2"
         >
-          <div v-if="cert.certFile" class="cert-view-box">
-            <el-icon :size="22" class="cert-view-icon"><Document /></el-icon>
-            <el-link :href="cert.certFile" target="_blank" type="primary" style="margin-left: 6px">
-              查看证书文件
-            </el-link>
+          <div class="cert-view-row">
+            <span class="cert-view-meta">
+              <span class="meta-label">有效期:</span>
+              <span class="meta-value">
+                {{ formatCertPeriod(cert.issueDate, cert.expireDate) }}
+              </span>
+            </span>
+            <span class="cert-view-meta">
+              <el-tag
+                :type="getCertStatusTag(cert.certStatus, cert.expireDate)"
+                size="small"
+              >
+                {{ getCertStatusLabel(cert.certStatus, cert.expireDate) }}
+              </el-tag>
+            </span>
+            <span class="cert-view-meta">
+              <template v-if="cert.certFile">
+                <el-icon :size="20" class="cert-view-icon"><Document /></el-icon>
+                <el-link
+                  :href="cert.certFile"
+                  target="_blank"
+                  type="primary"
+                  style="margin-left: 6px"
+                >
+                  查看证书文件
+                </el-link>
+              </template>
+              <span v-else class="text-placeholder">未上传证书文件</span>
+            </span>
           </div>
-          <span v-else class="text-placeholder">未上传证书文件</span>
         </el-descriptions-item>
       </el-descriptions>
       <div v-else class="empty-tip">该人员暂无岗位证书</div>
@@ -340,6 +371,7 @@ interface CertItem {
   certType: string;
   certName?: string;
   certFile?: string;
+  validPeriod?: string[]; // [生效日期, 到期日期]
   fileList: UploadUserFile[];
 }
 
@@ -573,6 +605,43 @@ const getCertTypeTag = (type: string) => {
   return map[type] || 'info';
 };
 
+/** 计算证书有效状态:已过期/即将到期(≤30天)/有效 */
+const calcCertStatus = (expireDate?: string): { label: string; tag: string } => {
+  if (!expireDate) return { label: '未知', tag: 'info' };
+  const expire = new Date(expireDate).getTime();
+  if (Number.isNaN(expire)) return { label: '未知', tag: 'info' };
+  const now = Date.now();
+  if (expire < now) return { label: '已过期', tag: 'danger' };
+  const diffDays = Math.floor((expire - now) / (1000 * 60 * 60 * 24));
+  if (diffDays <= 30) return { label: '即将到期', tag: 'warning' };
+  return { label: '有效', tag: 'success' };
+};
+
+/** 查看证书弹窗用:获取证书状态(优先用后端 certStatus,否则按 expireDate 实时计算) */
+const getCertStatusLabel = (certStatus?: string, expireDate?: string) => {
+  if (certStatus && certStatus !== '有效') return certStatus;
+  return calcCertStatus(expireDate).label;
+};
+
+const getCertStatusTag = (certStatus?: string, expireDate?: string): any => {
+  if (certStatus && certStatus !== '有效') {
+    const map: Record<string, string> = {
+      已过期: 'danger',
+      即将到期: 'warning',
+      有效: 'success',
+      无效: 'danger'
+    };
+    return map[certStatus] || 'info';
+  }
+  return calcCertStatus(expireDate).tag;
+};
+
+/** 格式化证书有效期区间显示 */
+const formatCertPeriod = (issueDate?: string, expireDate?: string) => {
+  const fmt = (d?: string) => (d ? d.substring(0, 10) : '—');
+  return `${fmt(issueDate)} ~ ${fmt(expireDate)}`;
+};
+
 // 人员类型 tag 颜色(按字典 value 映射,与设计稿一致)
 const getJobTypeTag = (type: string) => {
   const map: Record<string, string> = {
@@ -655,23 +724,34 @@ const handleUpdate = async (row: RiskPersonnelVo) => {
   try {
     const certRes = await listPersonnelCertByPersonnel(row.id);
     const certs = certRes.data || [];
-    form.value.certList = certs.map((c) => ({
-      uid: -Math.floor(Math.random() * 100000),
-      id: c.id,
-      certType: c.certType,
-      certName: c.certName,
-      certFile: c.certFile,
-      fileList: c.certFile
-        ? [
-            {
-              uid: -Math.floor(Math.random() * 100000),
-              name: c.certName || c.certType || '证书',
-              status: 'success',
-              url: c.certFile
-            }
-          ]
-        : []
-    }));
+    form.value.certList = certs.map((c) => {
+      // 后端 issueDate/expireDate → 前端 validPeriod 日期范围
+      let validPeriod: string[] | undefined;
+      if (c.issueDate || c.expireDate) {
+        validPeriod = [
+          c.issueDate ? c.issueDate.substring(0, 10) : '',
+          c.expireDate ? c.expireDate.substring(0, 10) : ''
+        ];
+      }
+      return {
+        uid: -Math.floor(Math.random() * 100000),
+        id: c.id,
+        certType: c.certType,
+        certName: c.certName,
+        certFile: c.certFile,
+        validPeriod,
+        fileList: c.certFile
+          ? [
+              {
+                uid: -Math.floor(Math.random() * 100000),
+                name: c.certName || c.certType || '证书',
+                status: 'success',
+                url: c.certFile
+              }
+            ]
+          : []
+      };
+    });
   } catch (e) {
     form.value.certList = [];
   }
@@ -771,13 +851,39 @@ const submitForm = () => {
     if (valid) {
       const certBos = (form.value.certList || [])
         .filter((c: CertItem) => c.certType)
-        .map((c: CertItem) => ({
-          id: c.id,
-          certType: c.certType,
-          certName: c.certType,
-          certFile: c.certFile || '',
-          certStatus: '有效'
-        }));
+        .map((c: CertItem) => {
+          // 证书有效期 validPeriod → issueDate / expireDate
+          const issueDate =
+            c.validPeriod && c.validPeriod[0]
+              ? `${c.validPeriod[0]} 00:00:00`
+              : '';
+          const expireDate =
+            c.validPeriod && c.validPeriod[1]
+              ? `${c.validPeriod[1]} 23:59:59`
+              : '';
+
+          // 根据到期日期自动判断证书状态
+          let certStatus = '有效';
+          if (expireDate) {
+            const expire = new Date(expireDate).getTime();
+            if (expire < Date.now()) {
+              certStatus = '已过期';
+            } else {
+              const diffDays = Math.floor((expire - Date.now()) / (1000 * 60 * 60 * 24));
+              if (diffDays <= 30) certStatus = '即将到期';
+            }
+          }
+
+          return {
+            id: c.id,
+            certType: c.certType,
+            certName: c.certType,
+            certFile: c.certFile || '',
+            issueDate,
+            expireDate,
+            certStatus
+          };
+        });
 
       const personnel = {
         id: form.value.id,
@@ -890,8 +996,16 @@ onMounted(async () => {
 .cert-item {
   display: flex;
   align-items: center;
-  gap: 12px;
+  flex-wrap: wrap;
+  gap: 10px;
   margin-bottom: 12px;
+  padding: 10px 12px;
+  background: #fafbfc;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+}
+.cert-item:hover {
+  border-color: #c0c4cc;
 }
 .cert-upload-trigger {
   display: inline-flex;
@@ -913,4 +1027,23 @@ onMounted(async () => {
 .cert-view-icon {
   color: #e6a23c;
 }
+.cert-view-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 18px;
+}
+.cert-view-meta {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+}
+.cert-view-row .meta-label {
+  color: #909399;
+}
+.cert-view-row .meta-value {
+  color: #303133;
+  font-weight: 500;
+}
 </style>

+ 130 - 3
src/views/risk/warning/index.vue

@@ -174,6 +174,24 @@
             style="width: 100%"
           />
         </el-form-item>
+
+        <!-- 附件上传:基于 MinIO -->
+        <el-form-item label="附件上传">
+          <el-upload
+            v-model:file-list="attachmentFileList"
+            :http-request="customAttachmentUpload"
+            :limit="1"
+            :accept="'.pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png'"
+            :before-upload="beforeAttachmentUpload"
+            :on-remove="handleAttachmentRemove"
+            list-type="text"
+          >
+            <el-button type="primary" plain>选择文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">支持 PDF/Word/Excel/图片格式,单个文件不超过 10MB</div>
+            </template>
+          </el-upload>
+        </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -198,6 +216,16 @@
         <el-descriptions-item label="警示内容">
           <div class="multi-line">{{ viewForm.warningContent || '—' }}</div>
         </el-descriptions-item>
+        <el-descriptions-item label="附件" v-if="viewForm.attachmentUrl">
+          <el-link
+            :href="viewForm.attachmentUrl"
+            target="_blank"
+            type="primary"
+          >
+            <el-icon style="margin-right: 4px"><Document /></el-icon>
+            {{ viewForm.attachmentName || '查看附件' }}
+          </el-link>
+        </el-descriptions-item>
         <el-descriptions-item label="备注">{{ viewForm.remark || '—' }}</el-descriptions-item>
       </el-descriptions>
       <template #footer>
@@ -211,10 +239,11 @@
 import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
 import { useUserStore } from '@/store/modules/user';
 import { listWarning, getWarning, addWarning, delWarning } from '@/api/risk/warning';
+import { uploadFile } from '@/api/risk/file';
 import { getDicts } from '@/api/system/dict/data';
 import type { RiskWarningBo, RiskWarningVo } from '@/api/risk/types';
-import { FormInstance } from 'element-plus';
-import { Plus } from '@element-plus/icons-vue';
+import { FormInstance, ElMessage, UploadFile, UploadRawFile, UploadUserFile } from 'element-plus';
+import { Plus, Document } from '@element-plus/icons-vue';
 import type { ComponentInternalInstance } from 'vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -227,6 +256,11 @@ const dateRange = ref<string[]>([]);
 const warningTypeOptions = ref<Array<{ label: string; value: string }>>([]);
 const warningLevelOptions = ref<Array<{ label: string; value: string }>>([]);
 
+// 附件上传
+const attachmentFileList = ref<UploadUserFile[]>([]);
+// 标记当前上传请求是否已处理完毕(防止双回调:http-request 内部 onSuccess + el-upload 内部 onError 同时触发)
+const attachmentUploadHandled = ref(false);
+
 const queryFormRef = ref<FormInstance>();
 const formRef = ref<FormInstance>();
 
@@ -418,8 +452,12 @@ const reset = () => {
     expireTime: '',
     releaserName: '',
     publishScope: '全员',
-    status: '已发布'
+    status: '已发布',
+    attachmentUrl: '',
+    attachmentName: ''
   } as any;
+  attachmentFileList.value = [];
+  attachmentUploadHandled.value = false;
   formRef.value?.resetFields();
 };
 
@@ -436,6 +474,95 @@ const formatDateTime = (val: any): string => {
   return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
 };
 
+// ============ 附件上传(走 axios baseURL,避免依赖 :action) ============
+const beforeAttachmentUpload = (file: UploadRawFile): boolean => {
+  // 类型/大小校验,失败直接拒绝(让 el-upload 不进入上传流程)
+  const validTypes = [
+    'application/pdf',
+    'application/msword',
+    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+    'application/vnd.ms-excel',
+    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+    'image/jpeg',
+    'image/png',
+    'image/jpg'
+  ];
+  // 部分浏览器可能识别不了 docx 的 mime,用扩展名兜底
+  const validExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.jpg', '.jpeg', '.png'];
+  const fileName = (file.name || '').toLowerCase();
+  const extOk = validExts.some((ext) => fileName.endsWith(ext));
+  if (!validTypes.includes(file.type) && !extOk) {
+    ElMessage.error('只支持 PDF/Word/Excel/图片 格式!');
+    return false;
+  }
+  if (file.size / 1024 / 1024 >= 10) {
+    ElMessage.error('附件大小不能超过 10MB!');
+    return false;
+  }
+  // 开始新一轮上传,重置处理标记
+  attachmentUploadHandled.value = false;
+  return true;
+};
+
+/**
+ * 自定义上传:走 axios baseURL(避免 el-upload 兜底 fetch :action 失败又触发 on-error)
+ * 关键:只通过 options.onSuccess / options.onError 二选一通知结果,
+ *      且无论成功/失败都设置 attachmentUploadHandled, 防止上层 on-success/on-error 重复 toast
+ */
+const customAttachmentUpload = async (options: any): Promise<void> => {
+  try {
+    const res: any = await uploadFile(options.file);
+    if (res && res.code === 200 && res.data?.url) {
+      // 1. 把后端返回的 url 挂到 file 对象(供提交时读取)
+      const url = res.data.url;
+      const originalName = res.data.originalName || options.file.name;
+      if (options.file) options.file.url = url;
+      // 2. 同步到 file-list 中,确保刷新弹窗后还能取到
+      const idx = attachmentFileList.value.findIndex((f) => f.uid === options.file.uid);
+      const newFile: any = {
+        ...(attachmentFileList.value[idx] || {}),
+        uid: options.file.uid,
+        name: originalName,
+        status: 'success',
+        url,
+        response: res
+      };
+      if (idx >= 0) {
+        attachmentFileList.value.splice(idx, 1, newFile);
+      } else {
+        attachmentFileList.value.push(newFile);
+      }
+      // 3. 回填到表单字段(后端 Bo 字段 attachmentUrl / attachmentName)
+      form.value.attachmentUrl = url;
+      form.value.attachmentName = originalName;
+      // 4. 标记已处理,通知 el-upload
+      attachmentUploadHandled.value = true;
+      options.onSuccess(res, options.file);
+      ElMessage.success(`附件 ${originalName} 上传成功`);
+    } else {
+      // 业务返回非 200(被 axios 拦截器解包后 res.data.code 才是 200,但这里再判断一次兜底)
+      attachmentUploadHandled.value = true;
+      const msg = res?.msg || '上传失败';
+      options.onError(new Error(msg));
+      ElMessage.error(`附件上传失败: ${msg}`);
+    }
+  } catch (err: any) {
+    // 这里只触发一次,不会双重 toast
+    if (attachmentUploadHandled.value) return;
+    attachmentUploadHandled.value = true;
+    const msg = err?.message || '未知错误';
+    options.onError(err);
+    ElMessage.error(`附件上传失败: ${msg}`);
+  }
+};
+
+const handleAttachmentRemove = (file: UploadFile) => {
+  // 用户主动移除附件 → 清空表单字段
+  form.value.attachmentUrl = '';
+  form.value.attachmentName = '';
+  attachmentUploadHandled.value = false;
+};
+
 onMounted(async () => {
   await loadDicts();
   await getList();

+ 96 - 111
src/views/risk/workReport/index.vue

@@ -127,13 +127,14 @@
     <el-dialog
       v-model="dialog.visible"
       :title="dialog.title"
-      width="780px"
+      width="860px"
       append-to-body
       :close-on-click-modal="false"
+      class="work-report-dialog"
       @close="closeDialog"
     >
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
-        <el-row :gutter="20">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="24">
           <el-col :span="12">
             <el-form-item label="作业类型" prop="workType">
               <el-select v-model="form.workType" placeholder="请选择作业类型" style="width: 100%">
@@ -149,30 +150,23 @@
           </el-col>
         </el-row>
 
-        <el-row :gutter="20">
-          <el-col :span="24">
-            <el-form-item label="工期" prop="durationDays">
-              <el-input-number
+        <el-row :gutter="24">
+          <el-col :span="12">
+            <el-form-item label="工期(天)" prop="durationDays">
+              <el-input
                 v-model="form.durationDays"
-                :min="1"
-                :max="3650"
-                :step="1"
-                placeholder="请输入工期,如:3天"
-                controls-position="right"
-                style="width: 100%"
-                @change="handleDurationChange"
+                placeholder="如:3"
+                clearable
+                @input="handleDurationChange"
               />
             </el-form-item>
           </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
           <el-col :span="12">
-            <el-form-item label="施工开始时间" prop="workStartTime">
+            <el-form-item label="开始时间" prop="workStartTime">
               <el-date-picker
                 v-model="form.workStartTime"
                 type="datetime"
-                placeholder="请选择施工开始时间"
+                placeholder="请选择"
                 value-format="YYYY-MM-DD HH:mm:ss"
                 format="YYYY-MM-DD HH:mm"
                 style="width: 100%"
@@ -180,12 +174,15 @@
               />
             </el-form-item>
           </el-col>
+        </el-row>
+
+        <el-row :gutter="24">
           <el-col :span="12">
-            <el-form-item label="施工结束时间" prop="workEndTime">
+            <el-form-item label="结束时间" prop="workEndTime">
               <el-date-picker
                 v-model="form.workEndTime"
                 type="datetime"
-                placeholder="请选择施工结束时间"
+                placeholder="请选择"
                 value-format="YYYY-MM-DD HH:mm:ss"
                 format="YYYY-MM-DD HH:mm"
                 style="width: 100%"
@@ -193,22 +190,25 @@
               />
             </el-form-item>
           </el-col>
-        </el-row>
-
-        <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="现场安全员" prop="reporterName">
-              <el-input v-model="form.reporterName" placeholder="请输入现场安全员姓名" maxlength="50" />
+              <el-input v-model="form.reporterName" placeholder="请输入姓名" maxlength="50" />
             </el-form-item>
           </el-col>
+        </el-row>
+
+        <el-row :gutter="24">
           <el-col :span="12">
             <el-form-item label="联系电话" prop="contactPhone">
               <el-input v-model="form.contactPhone" placeholder="请输入联系电话" maxlength="20" />
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <!-- 占位列,保持两列布局视觉平衡 -->
+            <div class="form-placeholder"></div>
+          </el-col>
         </el-row>
 
-        <!-- 现场施工图片 -->
         <el-form-item label="现场施工图片">
           <el-upload
             v-model:file-list="photoFileList"
@@ -220,26 +220,17 @@
             :on-error="handleUploadError"
             :on-exceed="handlePhotoExceed"
             :on-remove="handlePhotoRemove"
+            drag
+            multiple
           >
-            <template #trigger>
-              <div class="upload-trigger-area">
-                <div class="upload-action-row">
-                  <el-button type="primary" plain>选择文件</el-button>
-                  <span class="upload-placeholder">{{ photoFileList.length > 0 ? `已选择 ${photoFileList.length} 个文件` : '未选择任何文件' }}</span>
-                </div>
-                <div class="upload-tip-box">
-                  <div class="upload-tip-icon">
-                    <el-icon><UploadFilled /></el-icon>
-                  </div>
-                  <div class="upload-tip-text">点击上传现场施工图片</div>
-                  <div class="upload-tip-desc">支持JPG、PNG格式,单张不超过5MB,最多5张</div>
-                </div>
-              </div>
+            <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+            <template #tip>
+              <div class="el-upload__tip">支持 JPG/PNG 格式,单张不超过 5MB,最多 5 张</div>
             </template>
           </el-upload>
         </el-form-item>
 
-        <!-- 施工证件 -->
         <el-form-item label="施工证件">
           <el-upload
             v-model:file-list="certFileList"
@@ -251,21 +242,12 @@
             :on-error="handleUploadError"
             :on-exceed="handleCertExceed"
             :on-remove="handleCertRemove"
+            drag
           >
-            <template #trigger>
-              <div class="upload-trigger-area">
-                <div class="upload-action-row">
-                  <el-button type="primary" plain>选择文件</el-button>
-                  <span class="upload-placeholder">{{ certFileList.length > 0 ? '已选择 1 个文件' : '未选择任何文件' }}</span>
-                </div>
-                <div class="upload-tip-box">
-                  <div class="upload-tip-icon">
-                    <el-icon><Document /></el-icon>
-                  </div>
-                  <div class="upload-tip-text">点击上传施工证件</div>
-                  <div class="upload-tip-desc">支持PDF、JPG、PNG格式,单个不超过10MB</div>
-                </div>
-              </div>
+            <el-icon class="el-icon--upload"><Document /></el-icon>
+            <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+            <template #tip>
+              <div class="el-upload__tip">支持 PDF/JPG/PNG 格式,单个不超过 10MB</div>
             </template>
           </el-upload>
         </el-form-item>
@@ -274,7 +256,7 @@
           <el-input
             v-model="form.workContent"
             type="textarea"
-            :rows="4"
+            :rows="3"
             placeholder="请输入施工说明(0-500字)"
             maxlength="500"
             show-word-limit
@@ -769,10 +751,11 @@ const computeDurationByRange = (startTime: string, endTime: string): number => {
 };
 
 /** 工期变化 → 若有开始时间,自动算出结束时间 */
-const handleDurationChange = (val: number | undefined) => {
-  if (!val || val <= 0) return;
+const handleDurationChange = (val: string | number | undefined) => {
+  const num = Number(val);
+  if (!num || num <= 0) return;
   if (form.value.workStartTime) {
-    form.value.workEndTime = computeEndTimeByDuration(form.value.workStartTime, val);
+    form.value.workEndTime = computeEndTimeByDuration(form.value.workStartTime, num);
   }
 };
 
@@ -949,6 +932,60 @@ onMounted(async () => {
   overflow-y: auto;
   box-sizing: border-box;
 }
+
+/* 弹窗视觉优化 */
+:deep(.work-report-dialog .el-dialog) {
+  border-radius: 8px;
+  overflow: hidden;
+}
+:deep(.work-report-dialog .el-dialog__title) {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2937;
+}
+:deep(.work-report-dialog .el-dialog__header) {
+  padding: 18px 24px;
+  margin-right: 0;
+  border-bottom: 1px solid #f3f4f6;
+}
+:deep(.work-report-dialog .el-dialog__body) {
+  padding: 20px 28px 8px;
+}
+:deep(.work-report-dialog .el-dialog__footer) {
+  padding: 12px 24px 18px;
+  border-top: 1px solid #f3f4f6;
+}
+:deep(.work-report-dialog .el-form-item) {
+  margin-bottom: 18px;
+}
+:deep(.work-report-dialog .el-form-item__label) {
+  font-size: 13px;
+  color: #4b5563;
+  font-weight: 500;
+  white-space: nowrap;
+}
+:deep(.work-report-dialog .el-input__wrapper),
+:deep(.work-report-dialog .el-select__wrapper),
+:deep(.work-report-dialog .el-date-editor.el-input__wrapper),
+:deep(.work-report-dialog .el-textarea__inner) {
+  border-radius: 4px;
+}
+:deep(.work-report-dialog .el-upload) {
+  width: 100%;
+}
+:deep(.work-report-dialog .el-upload-dragger) {
+  padding: 14px 0;
+  border-radius: 4px;
+}
+:deep(.work-report-dialog .el-upload__tip) {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+.form-placeholder {
+  height: 100%;
+  min-height: 32px;
+}
 .page-header {
   margin-bottom: 16px;
 }
@@ -980,58 +1017,6 @@ onMounted(async () => {
   color: #606266;
 }
 
-.upload-trigger-area {
-  display: block;
-  cursor: pointer;
-}
-.upload-action-row {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-.upload-placeholder {
-  font-size: 13px;
-  color: #606266;
-}
-.upload-tip-box {
-  margin-top: 8px;
-  padding: 16px;
-  background: #f5f7fa;
-  border-radius: 4px;
-  border: 1px dashed #dcdfe6;
-  text-align: center;
-  transition: all 0.2s;
-}
-.upload-trigger-area:hover .upload-tip-box {
-  background: #ecf5ff;
-  border-color: #409eff;
-}
-.upload-tip-icon {
-  text-align: center;
-  color: #409eff;
-  font-size: 32px;
-  line-height: 1;
-}
-.upload-tip-text {
-  text-align: center;
-  font-size: 13px;
-  color: #409eff;
-  margin-top: 6px;
-}
-.upload-tip-desc {
-  text-align: center;
-  font-size: 12px;
-  color: #909399;
-  margin-top: 4px;
-}
-:deep(.el-upload) {
-  display: block;
-  width: 100%;
-}
-:deep(.el-upload-dragger) {
-  display: none;
-}
-
 .work-content-text {
   white-space: pre-wrap;
   word-break: break-all;