设备图片

This commit is contained in:
caozehui
2026-07-23 08:39:53 +08:00
parent f8ab1eb5f5
commit 049fb591c3
4 changed files with 172 additions and 77 deletions

View File

@@ -54,7 +54,6 @@ function appendIfPresent(formData: FormData, key: string, value: unknown) {
} }
formData.append(key, String(value)) formData.append(key, String(value))
} }
function appendMonitorFields(formData: FormData, monitorList: Monitor.ResPqMon[]) { function appendMonitorFields(formData: FormData, monitorList: Monitor.ResPqMon[]) {
monitorList.forEach((monitor, index) => { monitorList.forEach((monitor, index) => {
MONITOR_FIELD_NAMES.forEach(fieldName => { 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() const formData = new FormData()
DEVICE_FIELD_NAMES.forEach(fieldName => { DEVICE_FIELD_NAMES.forEach(fieldName => {
@@ -72,20 +71,13 @@ export function buildPqDevFormData(device: Device.ResPqDev, imageFile?: File | n
appendMonitorFields(formData, device.monitorList ?? []) appendMonitorFields(formData, device.monitorList ?? [])
if (imageFile) { imageFiles?.forEach(file => {
formData.append('image', imageFile) formData.append('images', file)
} })
deleteImageNames?.forEach(fileName => {
appendIfPresent(formData, 'deleteImageNames', fileName)
})
return formData return formData
} }
export function buildDeviceImagePreviewSrc(
hasImage?: boolean,
imageBase64?: string | null,
imageContentType?: string | null
) {
if (!hasImage || !imageBase64 || !imageContentType) {
return null
}
return `data:${imageContentType};base64,${imageBase64}`
}

View File

@@ -12,13 +12,13 @@ export const getPqDevList = (params: Device.ReqPqDevParams) => {
} }
//添加被检设备 //添加被检设备
export const addPqDev = (params: Device.ResPqDev, imageFile?: File | null) => { export const addPqDev = (params: Device.ResPqDev, imageFiles?: File[] | null) => {
return http.upload(`/pqDev/add`, buildPqDevFormData(params, imageFile)) return http.upload(`/pqDev/add`, buildPqDevFormData(params, imageFiles))
} }
//编辑被检设备 //编辑被检设备
export const updatePqDev = (params: Device.ResPqDev, imageFile?: File | null) => { export const updatePqDev = (params: Device.ResPqDev, imageFiles?: File[] | null, deleteImageNames?: string[]) => {
return http.upload(`/pqDev/update`, buildPqDevFormData(params, imageFile)) 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) return http.get(`/pqDev/getSelectOptions`, params)
} }
export const downloadDeviceImage = (id: string) => { export const downloadDeviceImage = (id: string, fileName: string) => {
return http.downloadGetWithHeaders(`/pqDev/image/download?id=${id}`) 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)}`)
} }

View File

@@ -3,6 +3,12 @@ import type { Monitor } from './monitor'
// 被检设备模块 // 被检设备模块
export namespace Device { export namespace Device {
export interface PqDevImage {
fileName: string
previewUrl: string
downloadUrl: string
}
/** /**
* 被检设备表格分页查询参数 * 被检设备表格分页查询参数
*/ */
@@ -100,8 +106,8 @@ export namespace Device {
checked?: boolean // 是否已选择 checked?: boolean // 是否已选择
disabled?: boolean // 是否禁用 disabled?: boolean // 是否禁用
hasImage?: boolean // 是否存在设备图片 hasImage?: boolean // 是否存在设备图片
imageBase64?: string | null // 设备图片 Base64用于详情回显 images?: PqDevImage[]
imageContentType?: string | null // 设备图片 MIME 类型 deleteImageNames?: string[]
} }
export interface SelectOption { export interface SelectOption {

View File

@@ -100,25 +100,34 @@
<input <input
ref="imageInputRef" ref="imageInputRef"
type="file" type="file"
multiple
accept=".jpg,.jpeg,.png,image/jpeg,image/png" accept=".jpg,.jpeg,.png,image/jpeg,image/png"
style="display: none" style="display: none"
@change="handleImageChange" @change="handleImageChange"
/> />
<div v-if="imagePreviewUrl" class="device-image-preview"> <div v-if="imageItems.length" class="device-image-list">
<img :src="imagePreviewUrl" alt="device image preview" /> <div v-for="item in imageItems" :key="item.uid" class="device-image-card">
<img :src="item.url" alt="device image preview" />
<div class="device-image-card-actions">
<el-button
v-if="item.persisted"
size="small"
link
type="primary"
@click="handleImageDownload(item)"
>
下载
</el-button>
<el-button size="small" link type="danger" @click="removeImage(item)">删除</el-button>
</div>
</div>
</div> </div>
<div v-else class="device-image-empty">暂无图片</div> <div v-else class="device-image-empty">暂无图片</div>
<div class="device-image-actions"> <div class="device-image-actions">
<el-button size="small" @click="triggerImageSelect"> <el-button size="small" :disabled="!canAddImage" @click="triggerImageSelect">
{{ hasPersistedImage || selectedImageFile ? '更换图片' : '上传图片' }} 上传图片
</el-button>
<el-button
v-if="formContent.id && hasPersistedImage"
size="small"
@click="handleImageDownload"
>
下载图片
</el-button> </el-button>
<span class="device-image-count">{{ imageItems.length }}/{{ MAX_DEVICE_IMAGE_COUNT }}</span>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@@ -286,8 +295,7 @@
import { dialogBig } from '@/utils/elementBind' import { dialogBig } from '@/utils/elementBind'
import { type Device } from '@/api/device/interface/device' import { type Device } from '@/api/device/interface/device'
import { ElMessage, type FormItemRule } from 'element-plus' import { ElMessage, type FormItemRule } from 'element-plus'
import { addPqDev, downloadDeviceImage, getSelectOptions, updatePqDev } from '@/api/device/device' import { addPqDev, downloadDeviceImage, getSelectOptions, previewDeviceImage, updatePqDev } from '@/api/device/device'
import { buildDeviceImagePreviewSrc } from '@/api/device/device/formData'
import { computed, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { useDictStore } from '@/stores/modules/dict' import { useDictStore } from '@/stores/modules/dict'
// 使用 dayjs 库格式化 // 使用 dayjs 库格式化
@@ -315,10 +323,18 @@ const monitorTableHeight = ref(375) // 默认高度
const selectOptions = ref<Record<string, Device.SelectOption[]>>({}) const selectOptions = ref<Record<string, Device.SelectOption[]>>({})
const imageInputRef = ref<HTMLInputElement>() const imageInputRef = ref<HTMLInputElement>()
const selectedImageFile = ref<File | null>(null) const MAX_DEVICE_IMAGE_COUNT = 5
const imagePreviewUrl = ref<string | null>(null) type DeviceImageItem = {
const imagePreviewObjectUrl = ref<string | null>(null) uid: string
const hasPersistedImage = ref(false) url: string
file?: File
fileName?: string
persisted: boolean
objectUrl?: boolean
}
const imageItems = ref<DeviceImageItem[]>([])
const deletedImageNames = ref<string[]>([])
const canAddImage = computed(() => imageItems.value.length < MAX_DEVICE_IMAGE_COUNT)
const pqChannelArray = ref([ const pqChannelArray = ref([
{ {
@@ -338,18 +354,18 @@ const onTabChange = (tabName: string | number) => {
} }
} }
const revokeImageObjectUrl = () => { const revokeImageObjectUrls = () => {
if (imagePreviewObjectUrl.value) { imageItems.value.forEach(item => {
URL.revokeObjectURL(imagePreviewObjectUrl.value) if (item.objectUrl) {
imagePreviewObjectUrl.value = null URL.revokeObjectURL(item.url)
} }
})
} }
const resetImageState = () => { const resetImageState = () => {
revokeImageObjectUrl() revokeImageObjectUrls()
selectedImageFile.value = null imageItems.value = []
imagePreviewUrl.value = null deletedImageNames.value = []
hasPersistedImage.value = false
if (imageInputRef.value) { if (imageInputRef.value) {
imageInputRef.value.value = '' imageInputRef.value.value = ''
} }
@@ -370,45 +386,99 @@ const validateImageFile = (file: File) => {
} }
const triggerImageSelect = () => { const triggerImageSelect = () => {
if (!canAddImage.value) {
ElMessage.warning({ message: `设备图片最多上传 ${MAX_DEVICE_IMAGE_COUNT}` })
return
}
imageInputRef.value?.click() imageInputRef.value?.click()
} }
const handleImageChange = (event: Event) => { const handleImageChange = (event: Event) => {
const input = event.target as HTMLInputElement const input = event.target as HTMLInputElement
const file = input.files?.[0] const files = Array.from(input.files ?? [])
if (!file) { if (!files.length) {
return return
} }
if (!validateImageFile(file)) {
input.value = ''
return
}
revokeImageObjectUrl()
selectedImageFile.value = file
imagePreviewObjectUrl.value = URL.createObjectURL(file)
imagePreviewUrl.value = imagePreviewObjectUrl.value
}
const updateImageEchoState = (device: Partial<Device.ResPqDev>) => { for (const file of files) {
revokeImageObjectUrl() if (!canAddImage.value) {
selectedImageFile.value = null ElMessage.warning({ message: `设备图片最多上传 ${MAX_DEVICE_IMAGE_COUNT}` })
hasPersistedImage.value = Boolean(device.hasImage) break
imagePreviewUrl.value = buildDeviceImagePreviewSrc(device.hasImage, device.imageBase64, device.imageContentType) }
if (imageInputRef.value) { if (!validateImageFile(file)) {
imageInputRef.value.value = '' continue
}
imageItems.value.push({
uid: `${Date.now()}-${Math.random()}`,
url: URL.createObjectURL(file),
file,
persisted: false,
objectUrl: true
})
}
input.value = ''
}
const updateImageEchoState = async (device: Partial<Device.ResPqDev>) => {
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 getDownloadFileName = (contentDisposition?: string | null) => {
const match = contentDisposition?.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i) 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 () => { const handleImageDownload = async (item: DeviceImageItem) => {
if (!formContent.id || !hasPersistedImage.value) { if (!formContent.id || !item.fileName) {
return return
} }
const response = await downloadDeviceImage(formContent.id) const response = await downloadDeviceImage(formContent.id, item.fileName)
const blobUrl = URL.createObjectURL(response.data) const blobUrl = URL.createObjectURL(response.data)
const link = document.createElement('a') const link = document.createElement('a')
link.href = blobUrl link.href = blobUrl
@@ -416,7 +486,6 @@ const handleImageDownload = async () => {
link.click() link.click()
URL.revokeObjectURL(blobUrl) URL.revokeObjectURL(blobUrl)
} }
function useMetaInfo() { function useMetaInfo() {
const dialogVisible = ref(false) const dialogVisible = ref(false)
const titleType = ref('add') const titleType = ref('add')
@@ -648,7 +717,7 @@ const save = () => {
formContent.monitorList = monitor.value formContent.monitorList = monitor.value
if (titleType.value != 'add') { if (titleType.value != 'add') {
await updatePqDev(formContent, selectedImageFile.value) await updatePqDev(formContent, pendingImageFiles(), deletedImageNames.value)
ElMessage.success({ message: `${dialogTitle.value}成功!` }) ElMessage.success({ message: `${dialogTitle.value}成功!` })
} else { } else {
// 新增需要把设备模式转成字典ID // 新增需要把设备模式转成字典ID
@@ -663,7 +732,7 @@ const save = () => {
if (protocolItem) { if (protocolItem) {
formContent.protocol = protocolItem.id formContent.protocol = protocolItem.id
} }
await addPqDev(formContent, selectedImageFile.value) await addPqDev(formContent, pendingImageFiles())
ElMessage.success({ message: `${dialogTitle.value}成功!` }) ElMessage.success({ message: `${dialogTitle.value}成功!` })
} }
@@ -730,7 +799,7 @@ const open = async (
formContent.inspectChannel = formContent.inspectChannel.split(',').filter(Boolean) formContent.inspectChannel = formContent.inspectChannel.split(',').filter(Boolean)
} }
monitor.value = data.monitorList monitor.value = data.monitorList
updateImageEchoState(data) await updateImageEchoState(data)
} else { } else {
monitor.value = [] monitor.value = []
resetFormContent() resetFormContent()
@@ -817,12 +886,19 @@ const props = defineProps<{
.device-image-field { .device-image-field {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
} }
.device-image-preview { .device-image-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.device-image-card {
position: relative;
width: 96px; width: 96px;
height: 96px; height: 96px;
border: 1px solid #dcdfe6; border: 1px solid #dcdfe6;
@@ -831,12 +907,24 @@ const props = defineProps<{
background: #f5f7fa; background: #f5f7fa;
} }
.device-image-preview img { .device-image-card img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; 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 { .device-image-empty {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -856,6 +944,11 @@ const props = defineProps<{
align-items: flex-start; align-items: flex-start;
} }
.device-image-count {
color: #909399;
font-size: 12px;
}
.device-image-actions :deep(.el-button + .el-button) { .device-image-actions :deep(.el-button + .el-button) {
margin-left: 0; margin-left: 0;
} }