feat: 文件展示与提交逻辑优化

This commit is contained in:
DXC
2026-04-23 10:25:28 +08:00
parent 03518c99f3
commit 1e17547c6e
2 changed files with 138 additions and 21 deletions

View File

@ -254,23 +254,28 @@
<span v-if="getImagesOnly(row.generalImage).length > 1" class="more-badge">+{{getImagesOnly(row.generalImage).length}}</span>
</div>
<el-popover v-if="row.generalManual && row.generalManual.length > 0" placement="top" trigger="hover" width="200">
<el-popover v-if="row.generalManual && row.generalManual.length > 0" placement="top" trigger="hover" width="260">
<template #reference>
<el-button link type="primary" :icon="Document" />
<el-button link type="primary" :icon="row.generalManual.some(l => !isExternalLink(l) && !isImageFile(l)) ? Files : Document" />
</template>
<div style="display: flex; flex-direction: column; gap: 5px;">
<div v-for="(link, idx) in row.generalManual" :key="idx">
<el-link v-if="isExternalLink(link)" :href="link" target="_blank" type="primary" :underline="false">
说明书 {{idx+1}} <el-icon><Link /></el-icon>
</el-link>
<el-image v-else-if="isImageFile(link)"
style="width: 100px; height: 100px"
:src="getImageUrl(link)"
:preview-src-list="[getImageUrl(link)]"
fit="cover"
preview-teleported
<!-- 图片文件 -->
<div v-for="(link, idx) in row.generalManual.filter(l => !isExternalLink(l) && isImageFile(l))" :key="'img-' + idx">
<el-image
style="width: 80px; height: 80px; cursor: pointer;"
:src="getImageUrl(link)"
:preview-src-list="row.generalManual.filter(l => !isExternalLink(l) && isImageFile(l)).map(u => getImageUrl(u))"
fit="cover"
preview-teleported
/>
<el-link v-else :href="getImageUrl(link)" target="_blank" type="info">PDF 文件 {{idx+1}}</el-link>
<span style="font-size: 12px; color: #999;">图片 {{idx+1}}</span>
</div>
<!-- 非图片文件 -->
<div v-for="(link, idx) in row.generalManual.filter(l => !isExternalLink(l) && !isImageFile(l))" :key="'file-' + idx">
<el-link @click.prevent="handleDownloadConfirm(link)" type="info" :underline="false">
<el-icon><Files /></el-icon>
{{ link.split('/').pop() }}
</el-link>
</div>
</div>
</el-popover>
@ -457,7 +462,32 @@
:on-remove="(file) => handleRemoveImage(file, 'generalManual')"
:before-upload="beforeAvatarUpload"
>
<el-icon><Plus /></el-icon>
<template #default>
<div v-if="!fileListManual.length" class="upload-add-trigger">
<el-icon><Plus /></el-icon>
</div>
</template>
<template #file="{ file }">
<div class="upload-file-item">
<template v-if="isImageFile(file.url)">
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
</template>
<template v-else>
<div class="file-thumbnail">
<el-icon size="28"><Document /></el-icon>
<span class="file-name">{{ truncateFileName(file.name) }}</span>
</div>
</template>
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePreviewPicture(file)">
<el-icon><ZoomIn /></el-icon>
</span>
<span class="el-upload-list__item-delete" @click="() => handleRemoveImage(file, 'generalManual')">
<el-icon><Delete /></el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<div class="camera-card" @click="triggerCamera('generalManual')">
<el-icon><Camera /></el-icon><span class="text">拍照</span>
@ -563,7 +593,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download, Bell, CircleCheck } from '@element-plus/icons-vue';
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download, Bell, CircleCheck, Files, ZoomIn, Delete } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useUserStore } from '@/stores/user';
@ -673,6 +703,9 @@ const cameraDialogVisible = ref(false);
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null);
const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage');
// 脏检查 - 记录编辑前的原始数据
const originalForm = ref<any>(null);
// 复选框选中数据
const selectedItems = ref<MaterialBaseVO[]>([]);
const handleSelectionChange = (selection: MaterialBaseVO[]) => {
@ -1115,6 +1148,9 @@ const handleEdit = (row: MaterialBaseVO) => {
const data = JSON.parse(JSON.stringify(row));
Object.assign(form.value, data);
// 深拷贝保存原始数据用于脏检查
originalForm.value = JSON.parse(JSON.stringify(data));
if (data.category) {
const parts = data.category.split('/');
if (parts.length > 0) {
@ -1163,6 +1199,33 @@ const checkDuplicate = async (name: string, spec: string): Promise<boolean> => {
return false;
};
const isArraysEqual = (a: any[], b: any[]): boolean => {
if (a.length !== b.length) return false;
const sortedA = [...a].sort();
const sortedB = [...b].sort();
return sortedA.every((val, idx) => val === sortedB[idx]);
};
const buildPartialPayload = (current: any, original: any): any => {
const payload: any = { id: current.id };
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName'];
for (const key of compareFields) {
const currentVal = current[key];
const originalVal = original[key];
// 处理数组比较generalImage, generalManual
if (Array.isArray(currentVal) && Array.isArray(originalVal)) {
if (!isArraysEqual(currentVal, originalVal)) {
payload[key] = currentVal;
}
} else if (currentVal !== originalVal) {
payload[key] = currentVal;
}
}
return payload;
};
const submitForm = async () => {
if (!formRef.value) return;
@ -1188,19 +1251,44 @@ const submitForm = async () => {
if (prefixStr && suffixStr) fullCategory = prefixStr + '/' + suffixStr;
else fullCategory = prefixStr || suffixStr;
const payload = {
// 构建最终表单数据
const finalForm = {
...form.value,
category: fullCategory,
generalImage: finalImageList,
generalManual: finalManualList
};
// 脏检查:只提交变更的字段
let payload: any;
if (form.value.id && originalForm.value) {
// 编辑模式:生成部分更新 payload
payload = buildPartialPayload(finalForm, originalForm.value);
// 如果分类被修改,需要确保包含在 payload 中
if (payload.category === undefined && fullCategory !== originalForm.value.category) {
payload.category = fullCategory;
}
} else {
// 新增模式:提交完整数据
payload = finalForm;
}
// 如果没有变更,提示用户
const changedKeys = Object.keys(payload).filter(k => k !== 'id');
if (changedKeys.length === 0) {
ElMessage.info('没有检测到数据变更,无需保存');
submitLoading.value = false;
dialog.visible = false;
return;
}
const requestApi = form.value.id ? updateMaterialBase : addMaterialBase;
const actionText = form.value.id ? '修改' : '新增';
await requestApi(payload);
ElMessage.success(`${actionText}成功`);
dialog.visible = false;
originalForm.value = null;
getList();
getOptionsList();
} catch (error: any) {
@ -1227,6 +1315,7 @@ const resetForm = () => {
imageExternalUrl.value = '';
manualExternalUrl.value = '';
originalForm.value = null;
if (formRef.value) formRef.value.resetFields();
};
@ -1358,8 +1447,21 @@ const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp)$/i.test(url) }
const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(url) }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && isImageFile(item)) }
const getNonImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && !isImageFile(item)) }
const truncateFileName = (name: string, maxLen = 12) => { return name.length > maxLen ? name.slice(0, maxLen - 3) + '...' : name }
const handleDownloadConfirm = (link: string) => {
const fileName = link.split('/').pop() || '文件';
ElMessageBox.confirm(`确认要下载/查看「${fileName}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
window.open(getImageUrl(link), '_blank');
}).catch(() => {});
}
const beforeAvatarUpload = (rawFile: any) => {
const isTypeValid = ['image/jpeg', 'image/png', 'application/pdf'].includes(rawFile.type);
@ -1410,8 +1512,13 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' |
}
const handlePreviewPicture = (uploadFile: any) => {
dialogImageUrl.value = uploadFile.url!;
dialogVisibleImage.value = true
const fileUrl = uploadFile.url || uploadFile.response?.url || '';
if (isImageFile(fileUrl)) {
dialogImageUrl.value = getImageUrl(fileUrl);
dialogVisibleImage.value = true;
} else {
window.open(getImageUrl(fileUrl), '_blank');
}
}
const triggerCamera = (field: 'generalImage' | 'generalManual') => {
@ -1528,6 +1635,16 @@ onMounted(() => {
.file-preview-cell { display: flex; align-items: center; justify-content: center; position: relative; }
.more-badge { position: absolute; top: -5px; right: -5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 4px; font-size: 10px; transform: scale(0.9); }
/* 上传文件项样式 - 非图片文件显示 */
.upload-file-item { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; }
.upload-file-item .el-upload-list__item-thumbnail { width: 100%; height: 100%; object-fit: cover; }
.upload-file-item .file-thumbnail { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; background: #f5f7fa; color: #606266; }
.upload-file-item .file-thumbnail .file-name { font-size: 10px; margin-top: 4px; text-align: center; padding: 0 4px; word-break: break-all; max-width: 90px; }
.upload-file-item .el-upload-list__item-actions { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.6); opacity: 0; transition: opacity 0.3s; }
.upload-file-item:hover .el-upload-list__item-actions { opacity: 1; }
.upload-file-item .el-upload-list__item-actions .el-icon { color: #fff; font-size: 20px; cursor: pointer; margin: 0 4px; }
.upload-add-trigger { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
/* 预警行样式 - 加深颜色 */
:deep(.warning-row-red) {
--el-table-tr-bg-color: #ffcdd2 !important;