设备图片

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))
}
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}`
}

View File

@@ -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)}`)
}

View File

@@ -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 {

View File

@@ -100,25 +100,34 @@
<input
ref="imageInputRef"
type="file"
multiple
accept=".jpg,.jpeg,.png,image/jpeg,image/png"
style="display: none"
@change="handleImageChange"
/>
<div v-if="imagePreviewUrl" class="device-image-preview">
<img :src="imagePreviewUrl" alt="device image preview" />
<div v-if="imageItems.length" class="device-image-list">
<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 v-else class="device-image-empty">暂无图片</div>
<div class="device-image-actions">
<el-button size="small" @click="triggerImageSelect">
{{ hasPersistedImage || selectedImageFile ? '更换图片' : '上传图片' }}
</el-button>
<el-button
v-if="formContent.id && hasPersistedImage"
size="small"
@click="handleImageDownload"
>
下载图片
<el-button size="small" :disabled="!canAddImage" @click="triggerImageSelect">
上传图片
</el-button>
<span class="device-image-count">{{ imageItems.length }}/{{ MAX_DEVICE_IMAGE_COUNT }}</span>
</div>
</div>
</el-form-item>
@@ -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<Record<string, Device.SelectOption[]>>({})
const imageInputRef = ref<HTMLInputElement>()
const selectedImageFile = ref<File | null>(null)
const imagePreviewUrl = ref<string | null>(null)
const imagePreviewObjectUrl = ref<string | null>(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<DeviceImageItem[]>([])
const deletedImageNames = ref<string[]>([])
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
}
for (const file of files) {
if (!canAddImage.value) {
ElMessage.warning({ message: `设备图片最多上传 ${MAX_DEVICE_IMAGE_COUNT}` })
break
}
if (!validateImageFile(file)) {
input.value = ''
return
continue
}
revokeImageObjectUrl()
selectedImageFile.value = file
imagePreviewObjectUrl.value = URL.createObjectURL(file)
imagePreviewUrl.value = imagePreviewObjectUrl.value
imageItems.value.push({
uid: `${Date.now()}-${Math.random()}`,
url: URL.createObjectURL(file),
file,
persisted: false,
objectUrl: true
})
}
input.value = ''
}
const updateImageEchoState = (device: Partial<Device.ResPqDev>) => {
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<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 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;
}