5 Commits
yn ... master

Author SHA1 Message Date
caozehui
ff4a197436 Merge branch 'dev' 2026-07-10 08:43:26 +08:00
caozehui
ad2d94a893 切换程控源页面保持页面缓存 2026-07-10 08:42:38 +08:00
caozehui
030e84ae16 被检设备上传图片功能 2026-07-09 16:35:53 +08:00
caozehui
60112e84e3 归档 2026-07-09 09:15:40 +08:00
caozehui
8f12d0e03e 暂态写入写出时间 2026-07-08 15:51:11 +08:00
88 changed files with 384 additions and 61 deletions

View File

@@ -54,13 +54,13 @@ webSocket:
port: 7778 port: 7778
#源参数下发,暂态数据默认值 #源参数下发,暂态数据默认值
Dip: #Dip:
# 暂态前时间s # 暂态前时间s
# fPreTime: 2f # fPreTime: 2f
#写入时间s #写入时间s
fRampIn: 0.001f # fRampIn: 0.001f
#写出时间s #写出时间s
fRampOut: 0.001f # fRampOut: 0.001f
# 暂态后时间s # 暂态后时间s
# fAfterTime: 3f # fAfterTime: 3f

View File

@@ -1 +1 @@
55936 51248

Binary file not shown.

View File

@@ -17,3 +17,4 @@
.\binlog.000039 .\binlog.000039
.\binlog.000040 .\binlog.000040
.\binlog.000041 .\binlog.000041
.\binlog.000042

View File

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

View File

@@ -1,5 +1,6 @@
import type {Device} from '@/api/device/interface/device' import type {Device} from '@/api/device/interface/device'
import http from '@/api' import http from '@/api'
import { buildPqDevFormData } from './formData'
/** /**
* @name 被检设备管理模块 * @name 被检设备管理模块
@@ -11,13 +12,13 @@ export const getPqDevList = (params: Device.ReqPqDevParams) => {
} }
//添加被检设备 //添加被检设备
export const addPqDev = (params: Device.ResPqDev) => { export const addPqDev = (params: Device.ResPqDev, imageFile?: File | null) => {
return http.post(`/pqDev/add`, params) return http.upload(`/pqDev/add`, buildPqDevFormData(params, imageFile))
} }
//编辑被检设备 //编辑被检设备
export const updatePqDev = (params: Device.ResPqDev) => { export const updatePqDev = (params: Device.ResPqDev, imageFile?: File | null) => {
return http.post(`/pqDev/update`, params) return http.upload(`/pqDev/update`, buildPqDevFormData(params, imageFile))
} }
//删除被检设备 //删除被检设备
@@ -71,3 +72,7 @@ export const getPqDev = () => {
export const getSelectOptions = (params:{ pattern: string }) => { export const getSelectOptions = (params:{ pattern: string }) => {
return http.get(`/pqDev/getSelectOptions`, params) return http.get(`/pqDev/getSelectOptions`, params)
} }
export const downloadDeviceImage = (id: string) => {
return http.downloadGetWithHeaders(`/pqDev/image/download?id=${id}`)
}

View File

@@ -91,6 +91,9 @@ export namespace Device {
monitorList: Monitor.ResPqMon[] monitorList: Monitor.ResPqMon[]
checked?: boolean // 是否已选择 checked?: boolean // 是否已选择
disabled?: boolean // 是否禁用 disabled?: boolean // 是否禁用
hasImage?: boolean // 是否存在设备图片
imageBase64?: string | null // 设备图片 Base64用于详情回显
imageContentType?: string | null // 设备图片 MIME 类型
} }
export interface SelectOption { export interface SelectOption {
@@ -116,4 +119,4 @@ export namespace Device {
* 被检设备表格查询分页返回的对象; * 被检设备表格查询分页返回的对象;
*/ */
export interface ResPqDevPage extends ResPage<ResPqDev> {} export interface ResPqDevPage extends ResPage<ResPqDev> {}
} }

View File

@@ -109,6 +109,10 @@ import {
shouldHandleConnectionTerminalMessage, shouldHandleConnectionTerminalMessage,
} from "./connectionState"; } from "./connectionState";
defineOptions({
name: "controlSource",
});
const show = ref(false); const show = ref(false);
const router = useRouter(); const router = useRouter();
const modeId = ref(); const modeId = ref();

View File

@@ -95,6 +95,33 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="设备图片" class="device-image-form-item">
<div class="device-image-field">
<input
ref="imageInputRef"
type="file"
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>
<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>
</div>
</div>
</el-form-item>
<el-divider>参数信息</el-divider> <el-divider>参数信息</el-divider>
<el-form-item label="通道数" prop="devChns" v-if="DevIsShow"> <el-form-item label="通道数" prop="devChns" v-if="DevIsShow">
<el-input v-model="formContent.devChns" :disabled="true" /> <el-input v-model="formContent.devChns" :disabled="true" />
@@ -259,7 +286,8 @@
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, 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 { computed, reactive, ref } from 'vue'
import { useDictStore } from '@/stores/modules/dict' import { useDictStore } from '@/stores/modules/dict'
// 使用 dayjs 库格式化 // 使用 dayjs 库格式化
@@ -286,6 +314,11 @@ const activeTab = ref('0') // '0' 对应第一个 tab
const monitorTableHeight = ref(375) // 默认高度 const monitorTableHeight = ref(375) // 默认高度
const selectOptions = ref<Record<string, Device.SelectOption[]>>({}) 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 pqChannelArray = ref([ 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<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 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() { function useMetaInfo() {
const dialogVisible = ref(false) const dialogVisible = ref(false)
const titleType = ref('add') const titleType = ref('add')
@@ -473,6 +585,7 @@ const close = () => {
dialogVisible.value = false dialogVisible.value = false
// 清空dialogForm中的值 // 清空dialogForm中的值
resetFormContent() resetFormContent()
resetImageState()
// 重置表单 // 重置表单
dialogFormRef.value?.resetFields() dialogFormRef.value?.resetFields()
} }
@@ -535,7 +648,7 @@ const save = () => {
formContent.monitorList = monitor.value formContent.monitorList = monitor.value
if (titleType.value != 'add') { if (titleType.value != 'add') {
await updatePqDev(formContent) await updatePqDev(formContent, selectedImageFile.value)
ElMessage.success({ message: `${dialogTitle.value}成功!` }) ElMessage.success({ message: `${dialogTitle.value}成功!` })
} else { } else {
// 新增需要把设备模式转成字典ID // 新增需要把设备模式转成字典ID
@@ -550,7 +663,7 @@ const save = () => {
if (protocolItem) { if (protocolItem) {
formContent.protocol = protocolItem.id formContent.protocol = protocolItem.id
} }
await addPqDev(formContent) await addPqDev(formContent, selectedImageFile.value)
ElMessage.success({ message: `${dialogTitle.value}成功!` }) ElMessage.success({ message: `${dialogTitle.value}成功!` })
} }
@@ -617,9 +730,11 @@ 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)
} else { } else {
monitor.value = [] monitor.value = []
resetFormContent() resetFormContent()
resetImageState()
//只有比对式设备ID由前端传 //只有比对式设备ID由前端传
if (currentMode === '比对式') formContent.id = generateUUID().replaceAll('-', '') if (currentMode === '比对式') formContent.id = generateUUID().replaceAll('-', '')
@@ -694,4 +809,54 @@ defineExpose({ open })
const props = defineProps<{ const props = defineProps<{
refreshTable: (() => Promise<void>) | undefined refreshTable: (() => Promise<void>) | undefined
}>() }>()
</script> </script>
<style scoped>
.form-three {
align-items: flex-start;
}
.device-image-field {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.device-image-preview {
width: 96px;
height: 96px;
border: 1px solid #dcdfe6;
border-radius: 6px;
overflow: hidden;
background: #f5f7fa;
}
.device-image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.device-image-empty {
display: flex;
align-items: center;
justify-content: center;
width: 96px;
height: 96px;
border: 1px dashed #dcdfe6;
border-radius: 6px;
color: #909399;
background: #fafafa;
}
.device-image-actions {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.device-image-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
</style>

View File

@@ -39,7 +39,9 @@ let scriptForm: any = {
ftransValue: '', // 暂态幅值 ftransValue: '', // 暂态幅值
retainTime: '', // 暂态持续时间 retainTime: '', // 暂态持续时间
fpreTime:2.0, fpreTime:2.0,
fafterTime:3.0 fafterTime:3.0,
frampIn:0.001, //写入时间
frampOut:0.001 //写出时间
} //暂态 } //暂态
}, },
{ {
@@ -71,7 +73,9 @@ let scriptForm: any = {
ftransValue: '', // 暂态幅值 ftransValue: '', // 暂态幅值
retainTime: '', // 暂态持续时间 retainTime: '', // 暂态持续时间
fpreTime:2.0, fpreTime:2.0,
fafterTime:3.0 fafterTime:3.0,
frampIn:0.001, //写入时间
frampOut:0.001 //写出时间
} //暂态 } //暂态
}, },
{ {
@@ -103,7 +107,9 @@ let scriptForm: any = {
ftransValue: '', // 暂态幅值 ftransValue: '', // 暂态幅值
retainTime: '', // 暂态持续时间 retainTime: '', // 暂态持续时间
fpreTime:2.0, fpreTime:2.0,
fafterTime:3.0 fafterTime:3.0,
frampIn:0.001, //写入时间
frampOut:0.001 //写出时间
} //暂态 } //暂态
}, },
{ {
@@ -135,7 +141,9 @@ let scriptForm: any = {
ftransValue: '', // 暂态幅值 ftransValue: '', // 暂态幅值
retainTime: '', // 暂态持续时间 retainTime: '', // 暂态持续时间
fpreTime:2.0, fpreTime:2.0,
fafterTime:3.0 fafterTime:3.0,
frampIn:0.001, //写入时间
frampOut:0.001 //写出时间
} //暂态 } //暂态
}, },
{ {
@@ -167,7 +175,9 @@ let scriptForm: any = {
ftransValue: '', // 暂态幅值 ftransValue: '', // 暂态幅值
retainTime: '', // 暂态持续时间 retainTime: '', // 暂态持续时间
fpreTime:2.0, fpreTime:2.0,
fafterTime:3.0 fafterTime:3.0,
frampIn:0.001, //写入时间
frampOut:0.001 //写出时间
} //暂态 } //暂态
}, },
{ {
@@ -199,7 +209,9 @@ let scriptForm: any = {
ftransValue: '', // 暂态幅值 ftransValue: '', // 暂态幅值
retainTime: '', // 暂态持续时间 retainTime: '', // 暂态持续时间
fpreTime:2.0, fpreTime:2.0,
fafterTime:3.0 fafterTime:3.0,
frampIn:0.001, //写入时间
frampOut:0.001 //写出时间
} //暂态 } //暂态
} }
] ]

View File

@@ -8,14 +8,14 @@
<!-- 全局设置菜单内容 --> <!-- 全局设置菜单内容 -->
<div class="test-pane-scroll"> <div class="test-pane-scroll">
<el-radio-group v-model="form.subType" class="test-radio-group"> <el-radio-group v-model="form.subType" class="test-radio-group">
<el-radio <el-radio
v-for="item in tabChildren" v-for="item in tabChildren"
:key="item" :key="item"
:label="item.label" :label="item.label"
:value="item.code" :value="item.code"
border border
class="test-radio-item" class="test-radio-item"
/> />
<!-- <el-radio v-for="item in tabChildren" :value="item.value" border>{{ item.label }}</el-radio> --> <!-- <el-radio v-for="item in tabChildren" :value="item.value" border>{{ item.label }}</el-radio> -->
</el-radio-group> </el-radio-group>
</div> </div>
@@ -162,7 +162,7 @@
<TestScriptFlickerTab :childForm="childForm" /> <TestScriptFlickerTab :childForm="childForm" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="暂态编辑" v-if="childForm[0].dipFlag || childForm[1].dipFlag"> <el-tab-pane label="暂态编辑" v-if="childForm[0].dipFlag || childForm[1].dipFlag">
<TestScriptDipTab :childForm="childForm" @setRetainTime="setRetainTime" @setFPreTime="setFPreTime" @setFAfterTime="setFAfterTime" /> <TestScriptDipTab :childForm="childForm" @setRetainTime="setRetainTime" @setFPreTime="setFPreTime" @setFAfterTime="setFAfterTime" @setFRampIn="setFRampIn" @setFRampOut="setFRampOut"/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
@@ -314,6 +314,16 @@ const setFAfterTime = (val: any) => {
item.dipData.fafterTime = val item.dipData.fafterTime = val
}) })
} }
const setFRampIn = (val: any) => {
form.value.channelList.forEach((item: any) => {
item.dipData.frampIn = val
})
}
const setFRampOut = (val: any) => {
form.value.channelList.forEach((item: any) => {
item.dipData.frampOut = val
})
}
// 保存数据 // 保存数据
const save = () => { const save = () => {
@@ -745,37 +755,37 @@ defineExpose({ open })
flex: 0 0 auto; flex: 0 0 auto;
} }
.test-pane-scroll { .test-pane-scroll {
height: 295px; height: 295px;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
overflow-x: auto; overflow-x: auto;
overflow-y: auto; overflow-y: auto;
} }
.test-radio-group { .test-radio-group {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
min-width: 100%; min-width: 100%;
flex-direction: column; flex-direction: column;
} }
.left-tabs { .left-tabs {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
} }
:deep(.test-radio-group .el-radio) { :deep(.test-radio-group .el-radio) {
width: max-content; width: max-content;
max-width: none; max-width: none;
} }
:deep(.test-radio-group .test-radio-item) { :deep(.test-radio-group .test-radio-item) {
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
:deep(.test-radio-group .el-radio__label) { :deep(.test-radio-group .el-radio__label) {
white-space: nowrap; white-space: nowrap;
} }
</style> </style>

View File

@@ -31,7 +31,7 @@
<el-input <el-input
type="number" type="number"
@input="handleInput" @input="handleInput"
v-model="form[0].dipData.ftransValue" v-model.number="form[0].dipData.ftransValue"
:disabled="!form[0].dipFlag" :disabled="!form[0].dipFlag"
onkeypress="return (/[\d]/.test(String.fromCharCode(event.keyCode)))" onkeypress="return (/[\d]/.test(String.fromCharCode(event.keyCode)))"
/> />
@@ -39,7 +39,7 @@
<el-form-item label="持续时间(周波)"> <el-form-item label="持续时间(周波)">
<el-input <el-input
type="number" type="number"
v-model="form[0].dipData.retainTime" v-model.number="form[0].dipData.retainTime"
@input="handleInputRetainTime" @input="handleInputRetainTime"
:disabled="!form[0].dipFlag" :disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))" onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
@@ -48,7 +48,7 @@
<el-form-item label="前延时间(S)"> <el-form-item label="前延时间(S)">
<el-input <el-input
type="number" type="number"
v-model="form[0].dipData.fpreTime" v-model.number="form[0].dipData.fpreTime"
@input="validateFPreTime" @input="validateFPreTime"
:disabled="!form[0].dipFlag" :disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))" onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
@@ -57,12 +57,30 @@
<el-form-item label="后延时间(S)"> <el-form-item label="后延时间(S)">
<el-input <el-input
type="number" type="number"
v-model="form[0].dipData.fafterTime" v-model.number="form[0].dipData.fafterTime"
@input="validateFAfterTime" @input="validateFAfterTime"
:disabled="!form[0].dipFlag" :disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))" onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
/> />
</el-form-item> </el-form-item>
<el-form-item label="写入时间(S)">
<el-input
type="number"
v-model.number="form[0].dipData.frampIn"
@input="validateFRampIn"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
/>
</el-form-item>
<el-form-item label="写出时间(S)">
<el-input
type="number"
v-model.number="form[0].dipData.frampOut"
@input="validateFRampOut"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
/>
</el-form-item>
</el-form> </el-form>
</div> </div>
</div> </div>
@@ -135,6 +153,20 @@ const validateFAfterTime = value => {
emit('setFAfterTime', value ) emit('setFAfterTime', value )
} }
} }
const validateFRampIn = value => {
if (value <= 0) {
ElMessage.warning('写入时间不能小于等于0S')
props.childForm[0].dipData.frampIn = 0.001
emit('setFRampIn', 0.001)
}
}
const validateFRampOut = value => {
if (value <= 0) {
ElMessage.warning('写出时间不能小于等于0S')
props.childForm[0].dipData.frampOut = 0.001
emit('setFRampOut', 0.001)
}
}
</script> </script>