设备图片
This commit is contained in:
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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)}`)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user