Bläddra i källkod

fix(frontend): 修复附件缩略图识别 & 弹窗换行显示

- 修复 isImageFile 通过文件后缀判断图片,解决编辑模式下缩略图不显示
- modal alert 系列支持 dangerouslyUseHTMLString,<br/> 换行生效
- 更新 showAttachmentDetail 换行
- 新增变更记录 docs/变更管理/code-adjust-record-2025-07-10-wuzilong.md
zlwu 1 år sedan
förälder
incheckning
1540bd1a1b

+ 24 - 0
docs/变更管理/code-adjust-record-2025-07-10-wuzilong.md

@@ -0,0 +1,24 @@
+# 2025-07-10 变更记录 (wuzilong)
+
+> 更新时间:2025-07-10 10:30:00
+
+## 修复内容
+
+1. **web/src/views/hq/pm/repairOrder/index.vue**
+   - 优化 `isImageFile` 方法:在缺少 MIME Type 时通过文件名/URL 后缀判定图片类型,解决编辑模式下附件缩略图不显示的问题。
+   - 调整 `showAttachmentDetail` 详情字符串,使用标准 `<br/>` 换行符。
+2. **web/src/plugins/modal.ts**
+   - 在 `alert/alertError/alertSuccess/alertWarning` 方法中添加 `dangerouslyUseHTMLString: true`,支持 HTML 换行展示。
+
+## 影响范围
+
+- 物业报修工单编辑/详情弹窗显示。
+- 所有使用 `$modal.alert` 的提示框内容可正确解析换行。
+
+## 测试情况
+
+- [x] 编辑工单时,图片附件正常显示缩略图。
+- [x] 点击附件详情,弹窗内容按行分隔显示。
+- [x] 其它模块使用 `$modal.alert` 的弹窗显示正常。
+
+--- 

+ 9 - 4
web/src/plugins/modal.ts

@@ -21,28 +21,33 @@ export default {
   // 弹出提示
   alert(content: any) {
     ElMessageBox.alert(content, '系统提示', {
-      confirmButtonClass: 'el-button--primary'
+      confirmButtonClass: 'el-button--primary',
+      // 允许在内容中使用 <br/> 等 HTML
+      dangerouslyUseHTMLString: true
     });
   },
   // 错误提示
   alertError(content: any) {
     ElMessageBox.alert(content, '系统提示', {
       type: 'error',
-      confirmButtonClass: 'el-button--primary'
+      confirmButtonClass: 'el-button--primary',
+      dangerouslyUseHTMLString: true
     });
   },
   // 成功提示
   alertSuccess(content: any) {
     ElMessageBox.alert(content, '系统提示', {
       type: 'success',
-      confirmButtonClass: 'el-button--primary'
+      confirmButtonClass: 'el-button--primary',
+      dangerouslyUseHTMLString: true
     });
   },
   // 警告提示
   alertWarning(content: any) {
     ElMessageBox.alert(content, '系统提示', {
       type: 'warning',
-      confirmButtonClass: 'el-button--primary'
+      confirmButtonClass: 'el-button--primary',
+      dangerouslyUseHTMLString: true
     });
   },
   // 通知提示

+ 221 - 28
web/src/views/hq/pm/repairOrder/index.vue

@@ -24,9 +24,18 @@
               </el-col>
               <el-col :span="6">
                 <el-form-item label="维修类型" prop="categoryId">
-                  <el-select v-model="queryParams.categoryId" placeholder="请选择维修类型" clearable style="width: 100%">
-                    <el-option v-for="category in categoryOptions" :key="category.value" :label="category.label" :value="category.value" />
-                  </el-select>
+                  <el-tree-select
+                    v-model="queryParams.categoryId"
+                    :data="categoryTreeOptions"
+                    :props="{ value: 'id', label: 'categoryName', children: 'children' }"
+                    value-key="id"
+                    placeholder="请选择维修类型"
+                    check-strictly
+                    :render-after-expand="false"
+                    clearable
+                    filterable
+                    style="width: 100%"
+                  />
                 </el-form-item>
               </el-col>
             </el-row>
@@ -83,7 +92,7 @@
     </transition>
 
     <!-- 统计卡片区域 - 仅维护管理员和超级管理员可见 -->
-    <div v-if="canViewStatistics" class="mb-[10px]">
+    <!-- <div v-if="canViewStatistics" class="mb-[10px]">
       <el-row :gutter="16">
         <el-col :span="3">
           <el-card shadow="hover" class="stat-card stat-card-primary">
@@ -190,7 +199,7 @@
           </el-card>
         </el-col>
       </el-row>
-    </div>
+    </div> -->
 
     <!-- 表格区域 -->
     <el-card shadow="never" class="button-card table-card">
@@ -200,15 +209,13 @@
             <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['pm:repairOrder:add']">新增报修</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button  icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['pm:repairOrder:edit']">修改</el-button>
+            <el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['pm:repairOrder:edit']">修改</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button  icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['pm:repairOrder:remove']"
-              >删除</el-button
-            >
+            <el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['pm:repairOrder:remove']">删除</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button  icon="Download" @click="handleExport" v-hasPermi="['pm:repairOrder:export']">导出</el-button>
+            <el-button icon="Download" @click="handleExport" v-hasPermi="['pm:repairOrder:export']">导出</el-button>
           </el-col>
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
@@ -268,7 +275,7 @@
               <span v-else>-</span>
             </template>
           </el-table-column>
-          <el-table-column label="评价得分" align="center" prop="evaluationScore" width="100">
+          <el-table-column label="评价得分" align="center" prop="evaluationScore" width="200">
             <template #default="scope">
               <el-rate
                 v-if="scope.row.evaluationScore"
@@ -336,7 +343,7 @@
                   <el-button
                     link
                     type="warning"
-                    icon="Edit"
+                    icon="collection"
                     @click="handleUpdateProgress(scope.row)"
                     v-hasPermi="['pm:repairOrder:updateProgress']"
                   ></el-button>
@@ -361,7 +368,7 @@
                   <el-button
                     link
                     type="warning"
-                    icon="Edit"
+                    icon="collection"
                     @click="handleUpdateProgress(scope.row)"
                     v-hasPermi="['pm:repairOrder:updateProgress']"
                   ></el-button>
@@ -517,6 +524,8 @@
               v-model:file-list="attachmentList"
               class="upload-demo"
               drag
+              list-type="picture-card"
+              :on-preview="handleFilePreview"
               :action="uploadAction"
               :headers="uploadHeaders"
               :data="uploadData"
@@ -527,15 +536,43 @@
               :on-remove="handleRemoveAttachment"
               :on-change="handleFileChange"
               multiple
-              :limit="5"
+              :limit="15"
               :show-file-list="true"
               accept="image/*,video/*,.pdf,.doc,.docx,.txt"
             >
-              <el-icon class="el-icon--upload"><upload-filled /></el-icon>
-              <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+              <!-- <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+              <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> -->
               <template #tip>
-                <div class="el-upload__tip">支持图片、视频、PDF、Word文档等格式,单个文件大小不超过10MB,最多上传5个文件</div>
-                <div class="el-upload__tip" v-if="!form.id" style="color: #e6a23c">新增时文件将在保存工单后上传</div>
+                <div class="el-upload__tip">
+                  支持图片、视频、PDF、Word文档等格式,单个文件大小不超过10MB,最多上传15个文件。
+                  <span class="el-upload__tip" v-if="!form.id" style="color: #ff0000">(新增时文件将在保存工单后上传)</span>
+                </div>
+              </template>
+
+              <!-- 调整拖拽上传图标与文字在同一行、居中对齐 -->
+              <div class="el-upload-dragger1">
+                <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+                <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+              </div>
+
+              <!-- 自定义文件卡片缩略图(含文件名) -->
+              <template #file="{ file }">
+                <div class="upload-file-card">
+                  <!-- 缩略图区域 -->
+                  <div class="upload-file-thumb">
+                    <el-image
+                      v-if="isImageFile(file) && file.url"
+                      :src="file.url"
+                      fit="cover"
+                      :preview-src-list="[file.url]"
+                      class="upload-thumb-image"
+                    />
+                    <div v-else class="upload-file-placeholder">
+                      <el-icon><Document /></el-icon>
+                      <span class="file-ext">{{ file.name }}</span>
+                    </div>
+                  </div>
+                </div>
               </template>
             </el-upload>
           </div>
@@ -1187,6 +1224,7 @@ import {
 import { useUserStore } from '@/store/modules/user';
 import { getInfo } from '@/api/login'; // 添加获取用户详细信息的API
 import { getToken } from '@/utils/auth'; // 修复:添加getToken导入
+import { vLoading } from 'element-plus';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
@@ -1253,7 +1291,7 @@ const pm_order_status = ref([
   { value: '3', label: '进行中' },
   { value: '4', label: '已完成' },
   { value: '5', label: '已评价' },
-  { value: '6', label: '已归档' },
+  // { value: '6', label: '已归档' },
   { value: '9', label: '已取消' }
 ]);
 
@@ -1448,6 +1486,21 @@ const satisfactionLevelOptions = ref([
 const getList = async () => {
   loading.value = true;
   queryParams.value.createTimeRange = daterangeCreateTime.value;
+
+  // 设置创建时间范围
+  queryParams.value.createTimeRange = daterangeCreateTime.value;
+
+  // 将创建时间范围写入 params,供后台 between 过滤使用
+  if (daterangeCreateTime.value && daterangeCreateTime.value.length === 2 && daterangeCreateTime.value[0] && daterangeCreateTime.value[1]) {
+    queryParams.value.params = {
+      ...queryParams.value.params,
+      beginCreateTime: daterangeCreateTime.value[0],
+      endCreateTime: daterangeCreateTime.value[1]
+    };
+  } else if (queryParams.value.params) {
+    delete queryParams.value.params.beginCreateTime;
+    delete queryParams.value.params.endCreateTime;
+  }
   const res = await listRepairOrder(queryParams.value);
   // 适配后端返回格式:支持新格式{total, rows, code, msg}和旧格式{code, msg, data: {total, rows}}
   if ((res as any).rows && (res as any).total !== undefined) {
@@ -1992,13 +2045,13 @@ const canPreview = (attachment: any) => {
 
 /** 显示附件详情 */
 const showAttachmentDetail = (attachment: any) => {
-  const details = `文件名称:${attachment.fileName}
-文件大小:${formatFileSize(attachment.fileSize)}
-文件类型:${getFileTypeLabel(attachment.fileType)}
-上传者:${attachment.uploaderName || '未知'}
-上传时间:${attachment.createTime}
-上传阶段:${getUploadStageText(attachment.uploadStage)}
-存储路径:${attachment.filePath}
+  const details = `文件名称:${attachment.fileName}<br/>
+文件大小:${formatFileSize(attachment.fileSize)}<br/>
+文件类型:${getFileTypeLabel(attachment.fileType)}<br/>
+上传者:${attachment.uploaderName || '未知'}<br/>
+上传时间:${attachment.createTime}<br/>
+上传阶段:${getUploadStageText(attachment.uploadStage)}<br/>
+存储路径:${attachment.filePath}<br/>
 文件描述:${attachment.description || '无'}`;
 
   proxy?.$modal.alert(details);
@@ -2600,6 +2653,56 @@ onMounted(() => {
   getWorkerOptions();
   getCurrentUserInfo();
 });
+
+/** 卡片预览 */
+const handleFilePreview = (file: any) => {
+  // 如果文件已经上传成功,优先使用服务器返回的url
+  let url = file.url || (file.response && file.response.data && file.response.data.filePath);
+
+  // 若还未上传,则尝试使用本地预览地址
+  if (!url && file.raw) {
+    url = URL.createObjectURL(file.raw);
+  }
+
+  if (!url) return;
+
+  // 对图片使用图片预览,其余直接新窗口打开
+  const isImage = file.type ? file.type.startsWith('image') : file.raw && file.raw.type.startsWith('image');
+  if (isImage) {
+    // 利用 Element Plus ImageViewer 进行预览
+    const viewer = (proxy as any)?.$imageViewer;
+    viewer && viewer([url]);
+  } else {
+    window.open(url, '_blank');
+  }
+};
+
+/** 判断是否为图片文件 */
+const isImageFile = (file: any): boolean => {
+  // 优先使用文件 MIME type 判断
+  const type = file.type || (file.raw && file.raw.type) || '';
+  if (type && type.startsWith('image')) {
+    return true;
+  }
+
+  // 编辑场景下从数据库加载的附件通常缺少 MIME type
+  // 通过文件名或 URL 后缀进行判断
+  const name: string = file.name || file.response?.data?.fileName || '';
+  const url: string = file.url || file.response?.data?.filePath || '';
+  const source = (name || url).toLowerCase();
+
+  // 常见图片扩展名
+  const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff', '.ico'];
+
+  return IMAGE_EXTS.some((ext) => source.endsWith(ext));
+};
+
+/** 获取文件扩展名小写 */
+const getFileExt = (fileName: string): string => {
+  if (!fileName) return '';
+  const idx = fileName.lastIndexOf('.');
+  return idx !== -1 ? fileName.substring(idx + 1).toLowerCase() : '';
+};
 </script>
 
 <style lang="scss" scoped>
@@ -2763,7 +2866,7 @@ onMounted(() => {
 .attachment-upload-container {
   .upload-demo {
     .el-upload {
-      border: 2px dashed #d9d9d9;
+      /* border: 2px dashed #d9d9d9; */
       border-radius: 6px;
       cursor: pointer;
       position: relative;
@@ -2777,9 +2880,69 @@ onMounted(() => {
 
     .el-upload__tip {
       font-size: 12px;
-      color: #606266;
+      color: #000000;
       margin-top: 7px;
     }
+
+    // 调整拖拽上传图标与文字在同一行、居中对齐
+    .el-upload-dragger1 {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      gap: 15px;
+      padding: 0px !important;
+    }
+
+    .el-upload-dragger1 .el-icon--upload {
+      margin: 0;
+      padding: 0px !important;
+    }
+
+    /* 文件卡片占位封面样式 */
+    .upload-file-placeholder {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      color: #909399;
+      font-size: 14px;
+
+      .el-icon {
+        font-size: 28px;
+        margin-bottom: 4px;
+      }
+
+      .file-ext {
+        font-size: 12px;
+        text-transform: uppercase;
+        text-align: center;
+      }
+    }
+  }
+
+  /* 让附件卡片一行展示 4 个 */
+  :deep(.el-upload-list--picture-card) {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 9px;
+    margin-top: 12px;
+
+    /* 确保卡片宽度自适应 */
+    .el-upload-list__item {
+      margin: 0;
+    }
+  }
+}
+
+:deep(.el-upload-dragger) {
+  border: 0px !important;
+  padding: 0px !important;
+  .el-upload-dragger1 {
+    border: 0px !important;
+    padding: 0px !important;
   }
 }
 
@@ -3341,4 +3504,34 @@ onMounted(() => {
 :deep(.el-icon) {
   display: flex !important;
 }
+
+/* 卡片整体布局包含文件名 */
+.upload-file-card {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.upload-file-thumb {
+  width: 100%;
+  aspect-ratio: 1 / 1;
+  position: relative;
+}
+
+.upload-file-thumb .el-image,
+.upload-file-thumb .upload-file-placeholder {
+  width: 100%;
+  height: 100%;
+}
+
+.upload-file-name {
+  width: 100%;
+  margin-top: 4px;
+  font-size: 12px;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
 </style>