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

- 修复 isImageFile 通过文件后缀判断图片,解决编辑模式下缩略图不显示
- modal alert 系列支持 dangerouslyUseHTMLString,<br/> 换行生效
- 更新 showAttachmentDetail 换行
- 新增变更记录 docs/变更管理/code-adjust-record-2025-07-10-wuzilong.md
This commit is contained in:
zlwu committed 2025-07-10 13:59:41 +08:00
1 parent 3f9838e8f1
commit 1540bd1a1b
3 files changed
+254 -32

No files matched your search

@@ -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
View File
@@ -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
View File
@@ -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>