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