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;
}