diff --git a/frontend/src/api/device/device/formData.ts b/frontend/src/api/device/device/formData.ts new file mode 100644 index 0000000..4db7651 --- /dev/null +++ b/frontend/src/api/device/device/formData.ts @@ -0,0 +1,91 @@ +import type { Device } from '@/api/device/interface/device' +import type { Monitor } from '@/api/device/interface/monitor' + +const DEVICE_FIELD_NAMES = [ + 'id', + 'name', + 'pattern', + 'devType', + 'manufacturer', + 'createDate', + 'createId', + 'hardwareVersion', + 'softwareVersion', + 'protocol', + 'ip', + 'port', + 'encryptionFlag', + 'series', + 'devKey', + 'sampleId', + 'arrivedDate', + 'cityName', + 'gdName', + 'subName', + 'reportPath', + 'icdId', + 'planId', + 'factorFlag', + 'preinvestmentPlan', + 'delegate', + 'inspectChannel', + 'inspectDate', + 'harmSysId', + 'importFlag' +] as const + +const MONITOR_FIELD_NAMES = [ + 'id', + 'devId', + 'busbar', + 'name', + 'num', + 'pt', + 'ct', + 'connection', + 'statInterval', + 'harmSysId', + 'checkFlag' +] as const + +function appendIfPresent(formData: FormData, key: string, value: unknown) { + if (value === null || value === undefined || value === '') { + return + } + formData.append(key, String(value)) +} + +function appendMonitorFields(formData: FormData, monitorList: Monitor.ResPqMon[]) { + monitorList.forEach((monitor, index) => { + MONITOR_FIELD_NAMES.forEach(fieldName => { + appendIfPresent(formData, `monitorList[${index}].${fieldName}`, monitor[fieldName]) + }) + }) +} + +export function buildPqDevFormData(device: Device.ResPqDev, imageFile?: File | null) { + const formData = new FormData() + + DEVICE_FIELD_NAMES.forEach(fieldName => { + appendIfPresent(formData, fieldName, device[fieldName]) + }) + + appendMonitorFields(formData, device.monitorList ?? []) + + if (imageFile) { + formData.append('image', imageFile) + } + + 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 b8c1fc9..379c442 100644 --- a/frontend/src/api/device/device/index.ts +++ b/frontend/src/api/device/device/index.ts @@ -1,5 +1,6 @@ import type {Device} from '@/api/device/interface/device' import http from '@/api' +import { buildPqDevFormData } from './formData' /** * @name 被检设备管理模块 @@ -11,13 +12,13 @@ export const getPqDevList = (params: Device.ReqPqDevParams) => { } //添加被检设备 -export const addPqDev = (params: Device.ResPqDev) => { - return http.post(`/pqDev/add`, params) +export const addPqDev = (params: Device.ResPqDev, imageFile?: File | null) => { + return http.upload(`/pqDev/add`, buildPqDevFormData(params, imageFile)) } //编辑被检设备 -export const updatePqDev = (params: Device.ResPqDev) => { - return http.post(`/pqDev/update`, params) +export const updatePqDev = (params: Device.ResPqDev, imageFile?: File | null) => { + return http.upload(`/pqDev/update`, buildPqDevFormData(params, imageFile)) } //删除被检设备 @@ -71,3 +72,7 @@ export const getPqDev = () => { 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}`) +} diff --git a/frontend/src/api/device/interface/device.ts b/frontend/src/api/device/interface/device.ts index fb8687b..9fe7510 100644 --- a/frontend/src/api/device/interface/device.ts +++ b/frontend/src/api/device/interface/device.ts @@ -91,6 +91,9 @@ export namespace Device { monitorList: Monitor.ResPqMon[] checked?: boolean // 是否已选择 disabled?: boolean // 是否禁用 + hasImage?: boolean // 是否存在设备图片 + imageBase64?: string | null // 设备图片 Base64,用于详情回显 + imageContentType?: string | null // 设备图片 MIME 类型 } export interface SelectOption { @@ -116,4 +119,4 @@ export namespace Device { * 被检设备表格查询分页返回的对象; */ export interface ResPqDevPage extends ResPage {} -} \ No newline at end of file +} diff --git a/frontend/src/views/machine/device/components/devicePopup.vue b/frontend/src/views/machine/device/components/devicePopup.vue index c4cb5f4..907c2e3 100644 --- a/frontend/src/views/machine/device/components/devicePopup.vue +++ b/frontend/src/views/machine/device/components/devicePopup.vue @@ -95,6 +95,33 @@ /> + +
+ +
+ device image preview +
+
暂无图片
+
+ + {{ hasPersistedImage || selectedImageFile ? '更换图片' : '上传图片' }} + + + 下载图片 + +
+
+
参数信息 @@ -259,7 +286,8 @@ import { dialogBig } from '@/utils/elementBind' import { type Device } from '@/api/device/interface/device' import { ElMessage, type FormItemRule } from 'element-plus' -import { addPqDev, getSelectOptions, updatePqDev } from '@/api/device/device' +import { addPqDev, downloadDeviceImage, getSelectOptions, updatePqDev } from '@/api/device/device' +import { buildDeviceImagePreviewSrc } from '@/api/device/device/formData' import { computed, reactive, ref } from 'vue' import { useDictStore } from '@/stores/modules/dict' // 使用 dayjs 库格式化 @@ -286,6 +314,11 @@ const activeTab = ref('0') // '0' 对应第一个 tab 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 pqChannelArray = ref([ { @@ -305,6 +338,85 @@ const onTabChange = (tabName: string | number) => { } } +const revokeImageObjectUrl = () => { + if (imagePreviewObjectUrl.value) { + URL.revokeObjectURL(imagePreviewObjectUrl.value) + imagePreviewObjectUrl.value = null + } +} + +const resetImageState = () => { + revokeImageObjectUrl() + selectedImageFile.value = null + imagePreviewUrl.value = null + hasPersistedImage.value = false + if (imageInputRef.value) { + imageInputRef.value.value = '' + } +} + +const validateImageFile = (file: File) => { + const allowedTypes = ['image/jpeg', 'image/png'] + const isAllowedType = allowedTypes.includes(file.type) || /\.(jpe?g|png)$/i.test(file.name) + if (!isAllowedType) { + ElMessage.warning({ message: '仅支持 JPG、JPEG、PNG 图片' }) + return false + } + if (file.size > 10 * 1024 * 1024) { + ElMessage.warning({ message: '图片大小不能超过 10MB' }) + return false + } + return true +} + +const triggerImageSelect = () => { + imageInputRef.value?.click() +} + +const handleImageChange = (event: Event) => { + const input = event.target as HTMLInputElement + const file = input.files?.[0] + if (!file) { + return + } + if (!validateImageFile(file)) { + input.value = '' + return + } + revokeImageObjectUrl() + selectedImageFile.value = file + imagePreviewObjectUrl.value = URL.createObjectURL(file) + imagePreviewUrl.value = imagePreviewObjectUrl.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 getDownloadFileName = (contentDisposition?: string | null) => { + const match = contentDisposition?.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i) + return decodeURIComponent(match?.[1] ?? match?.[2] ?? 'device-image') +} + +const handleImageDownload = async () => { + if (!formContent.id || !hasPersistedImage.value) { + return + } + const response = await downloadDeviceImage(formContent.id) + const blobUrl = URL.createObjectURL(response.data) + const link = document.createElement('a') + link.href = blobUrl + link.download = getDownloadFileName(response.headers['content-disposition']) + link.click() + URL.revokeObjectURL(blobUrl) +} + function useMetaInfo() { const dialogVisible = ref(false) const titleType = ref('add') @@ -473,6 +585,7 @@ const close = () => { dialogVisible.value = false // 清空dialogForm中的值 resetFormContent() + resetImageState() // 重置表单 dialogFormRef.value?.resetFields() } @@ -535,7 +648,7 @@ const save = () => { formContent.monitorList = monitor.value if (titleType.value != 'add') { - await updatePqDev(formContent) + await updatePqDev(formContent, selectedImageFile.value) ElMessage.success({ message: `${dialogTitle.value}成功!` }) } else { // 新增需要把设备模式转成字典ID @@ -550,7 +663,7 @@ const save = () => { if (protocolItem) { formContent.protocol = protocolItem.id } - await addPqDev(formContent) + await addPqDev(formContent, selectedImageFile.value) ElMessage.success({ message: `${dialogTitle.value}成功!` }) } @@ -617,9 +730,11 @@ const open = async ( formContent.inspectChannel = formContent.inspectChannel.split(',').filter(Boolean) } monitor.value = data.monitorList + updateImageEchoState(data) } else { monitor.value = [] resetFormContent() + resetImageState() //只有比对式设备ID由前端传 if (currentMode === '比对式') formContent.id = generateUUID().replaceAll('-', '') @@ -694,4 +809,54 @@ defineExpose({ open }) const props = defineProps<{ refreshTable: (() => Promise) | undefined }>() - \ No newline at end of file + +