From 049fb591c3d43c04b00c3e3e9719ed8922056c28 Mon Sep 17 00:00:00 2001 From: caozehui <2427765068@qq.com> Date: Thu, 23 Jul 2026 08:39:53 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/device/device/formData.ts | 24 +-- frontend/src/api/device/device/index.ts | 16 +- frontend/src/api/device/interface/device.ts | 10 +- .../machine/device/components/devicePopup.vue | 199 +++++++++++++----- 4 files changed, 172 insertions(+), 77 deletions(-) diff --git a/frontend/src/api/device/device/formData.ts b/frontend/src/api/device/device/formData.ts index 4db7651..6cdfbae 100644 --- a/frontend/src/api/device/device/formData.ts +++ b/frontend/src/api/device/device/formData.ts @@ -54,7 +54,6 @@ function appendIfPresent(formData: FormData, key: string, value: unknown) { } formData.append(key, String(value)) } - function appendMonitorFields(formData: FormData, monitorList: Monitor.ResPqMon[]) { monitorList.forEach((monitor, index) => { MONITOR_FIELD_NAMES.forEach(fieldName => { @@ -63,7 +62,7 @@ function appendMonitorFields(formData: FormData, monitorList: Monitor.ResPqMon[] }) } -export function buildPqDevFormData(device: Device.ResPqDev, imageFile?: File | null) { +export function buildPqDevFormData(device: Device.ResPqDev, imageFiles?: File[] | null, deleteImageNames?: string[]) { const formData = new FormData() DEVICE_FIELD_NAMES.forEach(fieldName => { @@ -72,20 +71,13 @@ export function buildPqDevFormData(device: Device.ResPqDev, imageFile?: File | n appendMonitorFields(formData, device.monitorList ?? []) - if (imageFile) { - formData.append('image', imageFile) - } + imageFiles?.forEach(file => { + formData.append('images', file) + }) + + deleteImageNames?.forEach(fileName => { + appendIfPresent(formData, 'deleteImageNames', fileName) + }) return formData } - -export function buildDeviceImagePreviewSrc( - hasImage?: boolean, - imageBase64?: string | null, - imageContentType?: string | null -) { - if (!hasImage || !imageBase64 || !imageContentType) { - return null - } - return `data:${imageContentType};base64,${imageBase64}` -} diff --git a/frontend/src/api/device/device/index.ts b/frontend/src/api/device/device/index.ts index 379c442..ec2b3a0 100644 --- a/frontend/src/api/device/device/index.ts +++ b/frontend/src/api/device/device/index.ts @@ -12,13 +12,13 @@ export const getPqDevList = (params: Device.ReqPqDevParams) => { } //添加被检设备 -export const addPqDev = (params: Device.ResPqDev, imageFile?: File | null) => { - return http.upload(`/pqDev/add`, buildPqDevFormData(params, imageFile)) +export const addPqDev = (params: Device.ResPqDev, imageFiles?: File[] | null) => { + return http.upload(`/pqDev/add`, buildPqDevFormData(params, imageFiles)) } //编辑被检设备 -export const updatePqDev = (params: Device.ResPqDev, imageFile?: File | null) => { - return http.upload(`/pqDev/update`, buildPqDevFormData(params, imageFile)) +export const updatePqDev = (params: Device.ResPqDev, imageFiles?: File[] | null, deleteImageNames?: string[]) => { + return http.upload(`/pqDev/update`, buildPqDevFormData(params, imageFiles, deleteImageNames)) } //删除被检设备 @@ -73,6 +73,10 @@ export const getSelectOptions = (params:{ pattern: string }) => { return http.get(`/pqDev/getSelectOptions`, params) } -export const downloadDeviceImage = (id: string) => { - return http.downloadGetWithHeaders(`/pqDev/image/download?id=${id}`) +export const downloadDeviceImage = (id: string, fileName: string) => { + return http.downloadGetWithHeaders(`/pqDev/image/download?id=${encodeURIComponent(id)}&fileName=${encodeURIComponent(fileName)}`) +} + +export const previewDeviceImage = (id: string, fileName: string) => { + return http.downloadGetWithHeaders(`/pqDev/image/preview?id=${encodeURIComponent(id)}&fileName=${encodeURIComponent(fileName)}`) } diff --git a/frontend/src/api/device/interface/device.ts b/frontend/src/api/device/interface/device.ts index 614875b..b363310 100644 --- a/frontend/src/api/device/interface/device.ts +++ b/frontend/src/api/device/interface/device.ts @@ -3,6 +3,12 @@ import type { Monitor } from './monitor' // 被检设备模块 export namespace Device { + export interface PqDevImage { + fileName: string + previewUrl: string + downloadUrl: string + } + /** * 被检设备表格分页查询参数 */ @@ -100,8 +106,8 @@ export namespace Device { checked?: boolean // 是否已选择 disabled?: boolean // 是否禁用 hasImage?: boolean // 是否存在设备图片 - imageBase64?: string | null // 设备图片 Base64,用于详情回显 - imageContentType?: string | null // 设备图片 MIME 类型 + images?: PqDevImage[] + deleteImageNames?: string[] } export interface SelectOption { diff --git a/frontend/src/views/machine/device/components/devicePopup.vue b/frontend/src/views/machine/device/components/devicePopup.vue index 907c2e3..c22a330 100644 --- a/frontend/src/views/machine/device/components/devicePopup.vue +++ b/frontend/src/views/machine/device/components/devicePopup.vue @@ -100,25 +100,34 @@ -
- device image preview +
+
+ device image preview +
+ + 下载 + + 删除 +
+
暂无图片
- - {{ hasPersistedImage || selectedImageFile ? '更换图片' : '上传图片' }} - - - 下载图片 + + 上传图片 + {{ imageItems.length }}/{{ MAX_DEVICE_IMAGE_COUNT }}
@@ -286,8 +295,7 @@ import { dialogBig } from '@/utils/elementBind' import { type Device } from '@/api/device/interface/device' import { ElMessage, type FormItemRule } from 'element-plus' -import { addPqDev, downloadDeviceImage, getSelectOptions, updatePqDev } from '@/api/device/device' -import { buildDeviceImagePreviewSrc } from '@/api/device/device/formData' +import { addPqDev, downloadDeviceImage, getSelectOptions, previewDeviceImage, updatePqDev } from '@/api/device/device' import { computed, reactive, ref } from 'vue' import { useDictStore } from '@/stores/modules/dict' // 使用 dayjs 库格式化 @@ -315,10 +323,18 @@ const monitorTableHeight = ref(375) // 默认高度 const selectOptions = ref>({}) const imageInputRef = ref() -const selectedImageFile = ref(null) -const imagePreviewUrl = ref(null) -const imagePreviewObjectUrl = ref(null) -const hasPersistedImage = ref(false) +const MAX_DEVICE_IMAGE_COUNT = 5 +type DeviceImageItem = { + uid: string + url: string + file?: File + fileName?: string + persisted: boolean + objectUrl?: boolean +} +const imageItems = ref([]) +const deletedImageNames = ref([]) +const canAddImage = computed(() => imageItems.value.length < MAX_DEVICE_IMAGE_COUNT) const pqChannelArray = ref([ { @@ -338,18 +354,18 @@ const onTabChange = (tabName: string | number) => { } } -const revokeImageObjectUrl = () => { - if (imagePreviewObjectUrl.value) { - URL.revokeObjectURL(imagePreviewObjectUrl.value) - imagePreviewObjectUrl.value = null - } +const revokeImageObjectUrls = () => { + imageItems.value.forEach(item => { + if (item.objectUrl) { + URL.revokeObjectURL(item.url) + } + }) } const resetImageState = () => { - revokeImageObjectUrl() - selectedImageFile.value = null - imagePreviewUrl.value = null - hasPersistedImage.value = false + revokeImageObjectUrls() + imageItems.value = [] + deletedImageNames.value = [] if (imageInputRef.value) { imageInputRef.value.value = '' } @@ -370,45 +386,99 @@ const validateImageFile = (file: File) => { } const triggerImageSelect = () => { + if (!canAddImage.value) { + ElMessage.warning({ message: `设备图片最多上传 ${MAX_DEVICE_IMAGE_COUNT} 张` }) + return + } imageInputRef.value?.click() } const handleImageChange = (event: Event) => { const input = event.target as HTMLInputElement - const file = input.files?.[0] - if (!file) { + const files = Array.from(input.files ?? []) + if (!files.length) { return } - if (!validateImageFile(file)) { - input.value = '' - return + + for (const file of files) { + if (!canAddImage.value) { + ElMessage.warning({ message: `设备图片最多上传 ${MAX_DEVICE_IMAGE_COUNT} 张` }) + break + } + if (!validateImageFile(file)) { + continue + } + imageItems.value.push({ + uid: `${Date.now()}-${Math.random()}`, + url: URL.createObjectURL(file), + file, + persisted: false, + objectUrl: true + }) } - revokeImageObjectUrl() - selectedImageFile.value = file - imagePreviewObjectUrl.value = URL.createObjectURL(file) - imagePreviewUrl.value = imagePreviewObjectUrl.value + input.value = '' } -const updateImageEchoState = (device: Partial) => { - revokeImageObjectUrl() - selectedImageFile.value = null - hasPersistedImage.value = Boolean(device.hasImage) - imagePreviewUrl.value = buildDeviceImagePreviewSrc(device.hasImage, device.imageBase64, device.imageContentType) - if (imageInputRef.value) { - imageInputRef.value.value = '' +const updateImageEchoState = async (device: Partial) => { + resetImageState() + const deviceId = device.id + if (!deviceId) { + return } + const images = device.images ?? [] + imageItems.value = await Promise.all( + images.map(async image => { + try { + const response = await previewDeviceImage(deviceId, image.fileName) + return { + uid: image.fileName, + url: URL.createObjectURL(response.data), + fileName: image.fileName, + persisted: true, + objectUrl: true + } + } catch (error) { + return { + uid: image.fileName, + url: resolveDeviceImageUrl(image.previewUrl), + fileName: image.fileName, + persisted: true + } + } + }) + ) } +const resolveDeviceImageUrl = (url: string) => { + if (/^(https?:)?\/\//i.test(url) || url.startsWith('blob:') || url.startsWith('data:')) { + return url + } + const baseUrl = (import.meta.env.VITE_API_URL as string).replace(/\/$/, '') + return `${baseUrl}/${url.replace(/^\//, '')}` +} + +const removeImage = (item: DeviceImageItem) => { + if (item.persisted && item.fileName && !deletedImageNames.value.includes(item.fileName)) { + deletedImageNames.value.push(item.fileName) + } + if (item.objectUrl) { + URL.revokeObjectURL(item.url) + } + imageItems.value = imageItems.value.filter(image => image.uid !== item.uid) +} + +const pendingImageFiles = () => imageItems.value.filter(item => item.file).map(item => item.file as File) + const getDownloadFileName = (contentDisposition?: string | null) => { const match = contentDisposition?.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i) - return decodeURIComponent(match?.[1] ?? match?.[2] ?? 'device-image') + return decodeURIComponent(match?.[1] ?? match?.[2] ?? 'device-image.jpg') } -const handleImageDownload = async () => { - if (!formContent.id || !hasPersistedImage.value) { +const handleImageDownload = async (item: DeviceImageItem) => { + if (!formContent.id || !item.fileName) { return } - const response = await downloadDeviceImage(formContent.id) + const response = await downloadDeviceImage(formContent.id, item.fileName) const blobUrl = URL.createObjectURL(response.data) const link = document.createElement('a') link.href = blobUrl @@ -416,7 +486,6 @@ const handleImageDownload = async () => { link.click() URL.revokeObjectURL(blobUrl) } - function useMetaInfo() { const dialogVisible = ref(false) const titleType = ref('add') @@ -648,7 +717,7 @@ const save = () => { formContent.monitorList = monitor.value if (titleType.value != 'add') { - await updatePqDev(formContent, selectedImageFile.value) + await updatePqDev(formContent, pendingImageFiles(), deletedImageNames.value) ElMessage.success({ message: `${dialogTitle.value}成功!` }) } else { // 新增需要把设备模式转成字典ID @@ -663,7 +732,7 @@ const save = () => { if (protocolItem) { formContent.protocol = protocolItem.id } - await addPqDev(formContent, selectedImageFile.value) + await addPqDev(formContent, pendingImageFiles()) ElMessage.success({ message: `${dialogTitle.value}成功!` }) } @@ -730,7 +799,7 @@ const open = async ( formContent.inspectChannel = formContent.inspectChannel.split(',').filter(Boolean) } monitor.value = data.monitorList - updateImageEchoState(data) + await updateImageEchoState(data) } else { monitor.value = [] resetFormContent() @@ -817,12 +886,19 @@ const props = defineProps<{ .device-image-field { display: flex; - align-items: center; + align-items: flex-start; gap: 10px; width: 100%; } -.device-image-preview { +.device-image-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.device-image-card { + position: relative; width: 96px; height: 96px; border: 1px solid #dcdfe6; @@ -831,12 +907,24 @@ const props = defineProps<{ background: #f5f7fa; } -.device-image-preview img { +.device-image-card img { width: 100%; height: 100%; object-fit: cover; } +.device-image-card-actions { + position: absolute; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + gap: 8px; + padding: 4px; + background: rgb(255 255 255 / 88%); +} + .device-image-empty { display: flex; align-items: center; @@ -856,6 +944,11 @@ const props = defineProps<{ align-items: flex-start; } +.device-image-count { + color: #909399; + font-size: 12px; +} + .device-image-actions :deep(.el-button + .el-button) { margin-left: 0; }