This commit is contained in:
caozehui
2026-06-23 10:21:56 +08:00
parent cd51cfb052
commit 5ad3029e0b
10 changed files with 415 additions and 118 deletions

View File

@@ -19,6 +19,7 @@ export namespace DevType {
id: string; //设备类型ID
name: string;//设备类型名称
icd: string| null;//设备关联的ICD
pqdif: string| null;//设备关联的PQDIF
power: string| null;//工作电源
devVolt: number; //额定电压V
devCurr: number; //额定电流A
@@ -38,4 +39,4 @@ export namespace DevType {
export interface ResPqDevTypePage extends ResPage<ResPqDevType> {
}
}
}

View File

@@ -0,0 +1,26 @@
import type { ReqPage, ResPage } from '@/api/interface'
export namespace PQDIF {
export interface ReqPQDIFParams extends ReqPage {
name?: string
result?: number
}
export interface ResPQDIF {
id: string
name: string
filePath?: string | null
recordCount?: number | null
observationCount?: number | null
sampleValueCount?: number | null
result?: number | null
msg?: string | null
state: number
createBy?: string | null
createTime?: string | null
updateBy?: string | null
updateTime?: string | null
}
export interface ResPQDIFPage extends ResPage<ResPQDIF> {}
}

View File

@@ -0,0 +1,14 @@
import type { PQDIF } from '@/api/device/interface/pqdif'
import http from '@/api'
export const getPQDIFList = (params: PQDIF.ReqPQDIFParams) => {
return http.post<PQDIF.ResPQDIFPage>('/pqdif/list', params)
}
export const getPQDIFAllList = () => {
return http.get<PQDIF.ResPQDIF[]>('/pqdif/listAll')
}
export const importPQDIFJson = (params: FormData) => {
return http.upload('/pqdif/import/json', params, { loading: false })
}

View File

@@ -0,0 +1,171 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="title"
width="460px"
destroy-on-close
:close-on-click-modal="!loading"
:show-close="!loading"
>
<div v-loading="loading" :element-loading-text="loadingText" class="json-import-content">
<el-upload
ref="uploadRef"
action="#"
class="upload"
:auto-upload="false"
:limit="1"
accept=".json,application/json"
:on-exceed="handleExceed"
:on-change="handleChange"
:on-remove="handleRemove"
:disabled="loading"
>
<el-button type="primary" :icon="Upload" :disabled="loading">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">{{ tip }}</div>
</template>
</el-upload>
<div v-if="loading" class="json-import-loading-text">{{ loadingText }}</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button :disabled="loading" @click="close">取消</el-button>
<el-button type="primary" :loading="loading" @click="submit">开始导入</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Upload } from '@element-plus/icons-vue'
import {
ElMessage,
ElNotification,
genFileId,
type UploadFile,
type UploadInstance,
type UploadProps,
type UploadRawFile
} from 'element-plus'
const props = withDefaults(
defineProps<{
title: string
requestApi: (params: FormData) => Promise<unknown>
tip?: string
loadingText?: string
successMessage?: string
}>(),
{
tip: '请上传 .json 文件',
loadingText: '导入中,请稍候...',
successMessage: '导入成功'
}
)
const emit = defineEmits<{
(e: 'success'): void
}>()
const dialogVisible = ref(false)
const loading = ref(false)
const uploadRef = ref<UploadInstance | null>(null)
const selectedFile = ref<File | null>(null)
const open = () => {
dialogVisible.value = true
}
const close = () => {
if (loading.value) {
return
}
dialogVisible.value = false
selectedFile.value = null
uploadRef.value?.clearFiles()
}
const isJsonFile = (file: UploadRawFile | File) => {
return file.name.toLowerCase().endsWith('.json') || file.type === 'application/json'
}
const notifyInvalidFile = () => {
ElNotification({
title: '温馨提示',
message: '上传文件只能是 json 格式!',
type: 'warning'
})
}
const setSelectedFile = (file: UploadRawFile | File | undefined) => {
if (!file) {
selectedFile.value = null
return
}
if (!isJsonFile(file)) {
notifyInvalidFile()
selectedFile.value = null
uploadRef.value?.clearFiles()
return
}
selectedFile.value = file
}
const handleExceed: UploadProps['onExceed'] = files => {
uploadRef.value?.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
uploadRef.value?.handleStart(file)
setSelectedFile(file)
}
const handleChange = (uploadFile: UploadFile) => {
setSelectedFile(uploadFile.raw as File | undefined)
}
const handleRemove = () => {
selectedFile.value = null
}
const submit = async () => {
if (loading.value) {
return
}
if (!selectedFile.value) {
ElMessage.warning('请选择文件!')
return
}
const formData = new FormData()
formData.append('file', selectedFile.value)
loading.value = true
try {
await props.requestApi(formData)
ElMessage.success(props.successMessage)
dialogVisible.value = false
selectedFile.value = null
uploadRef.value?.clearFiles()
emit('success')
} finally {
loading.value = false
}
}
defineExpose({
open,
close
})
</script>
<style scoped>
.json-import-content {
min-height: 120px;
}
.json-import-loading-text {
margin-top: 12px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
</style>

View File

@@ -192,7 +192,7 @@ export default class SocketService {
* WebSocket连接配置
*/
private config: SocketConfig = {
url: `ws://127.0.0.1:7778/hello`,
url: `ws://127.0.0.1:7777/hello`,
heartbeatInterval: 9000, // 9秒心跳间隔
reconnectDelay: 5000, // 5秒重连延迟
maxReconnectAttempts: 5, // 最多重连5次

View File

@@ -65,6 +65,16 @@
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="设备关联PQDIF" prop="pqdif" >
<el-select v-model="formContent.pqdif" clearable placeholder="请选择PQDIF">
<el-option
v-for="item in pqdifOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="录波指令" prop="waveCmd" v-if="modeStore.currentMode == '比对式'">
<el-input v-model='formContent.waveCmd' placeholder="请输入录波指令" maxlength="32" show-word-limit/>
@@ -89,6 +99,7 @@
import { ref,computed, Ref } from 'vue'
import { type DevType } from '@/api/device/interface/devType'
import { type ICD } from '@/api/device/interface/icd'
import { type PQDIF } from '@/api/device/interface/pqdif'
import {dialogMiddle} from '@/utils/elementBind'
import { useDictStore } from '@/stores/modules/dict'
import {addDevType,updateDevType} from '@/api/device/devType'
@@ -100,6 +111,7 @@
const dialogFormRef = ref()
const scene = ref('')
const icdOptions = ref<ICD.StandardOption[]>([])
const pqdifOptions = ref<PQDIF.ResPQDIF[]>([])
function useMetaInfo() {
const dialogVisible = ref(false)
const titleType = ref('add')
@@ -107,6 +119,7 @@
id: '', //设备类型ID
name: '',//设备类型名称
icd: '',//设备关联的ICD
pqdif: '',//设备关联的PQDIF
power: 'AC/DC 110V-220V',//工作电源
devVolt: 57.74, //额定电压V
devCurr: 5, //额定电流A
@@ -127,6 +140,7 @@ const resetFormContent = () => {
id: '', //设备类型ID
name: '',//设备类型名称
icd: '',//设备关联的ICD
pqdif: '',//设备关联的PQDIF
power: 'AC/DC 110V-220V',//工作电源
devVolt: 57.74, //额定电压V
devCurr: 5, //额定电流A
@@ -174,6 +188,9 @@ const resetFormContent = () => {
],
icd: [
{ required: true, message: '设备关联ICD必选', trigger: 'change' }
],
pqdif: [
{ required: true, message: '设备关联PQDIF必选', trigger: 'change' }
]
};
@@ -209,13 +226,14 @@ const close = () => {
}
// 打开弹窗,可能是新增,也可能是编辑
const open = async (sign: string, data: DevType.ResPqDevType,icd: ICD.StandardOption[],currentScene: string) => {
const open = async (sign: string, data: DevType.ResPqDevType,icd: ICD.StandardOption[],pqdif: PQDIF.ResPQDIF[],currentScene: string) => {
// 重置表单
dialogFormRef.value?.resetFields()
titleType.value = sign
dialogVisible.value = true
scene.value = currentScene
icdOptions.value = icd
pqdifOptions.value = pqdif
if (data.id) {
formContent.value = { ...data }

View File

@@ -45,6 +45,8 @@ import { onBeforeMount, reactive, ref } from 'vue'
import { useModeStore, useAppSceneStore } from '@/stores/modules/mode'
import { getICDAllList } from '@/api/device/icd'
import type { ICD } from '@/api/device/interface/icd'
import { getPQDIFAllList } from '@/api/device/pqdif'
import type { PQDIF } from '@/api/device/interface/pqdif'
// defineOptions({
// name: 'devType',
@@ -56,6 +58,7 @@ import type { ICD } from '@/api/device/interface/icd'
const proTable = ref<ProTableInstance>()
const devTypePopup = ref()
const icdOptions = ref<ICD.StandardOption[]>([])
const pqdifOptions = ref<PQDIF.ResPQDIF[]>([])
const getTableList = async (params: any) => {
let newParams = JSON.parse(JSON.stringify(params))
@@ -111,6 +114,15 @@ import type { ICD } from '@/api/device/interface/icd'
return <span>{name?.name}</span>
},
},
{
prop: 'pqdif',
label: '设备关联PQDIF',
minWidth: 200,
render: (scope) => {
const name = pqdifOptions.value.find(option => option.id === scope.row.pqdif)
return <span>{name?.name || '--'}</span>
},
},
{
prop: 'createTime',
label: '创建时间',
@@ -136,7 +148,7 @@ import type { ICD } from '@/api/device/interface/icd'
// 打开 drawer(新增、编辑)
const openDialog = (titleType: string, row: Partial<DevType.ResPqDevType> = {}) => {
devTypePopup.value?.open(titleType, row,icdOptions.value,appSceneStore.currentScene)
devTypePopup.value?.open(titleType, row,icdOptions.value,pqdifOptions.value,appSceneStore.currentScene)
}
@@ -156,11 +168,12 @@ import type { ICD } from '@/api/device/interface/icd'
onBeforeMount(async () => {
const response = await getICDAllList()
const [response, pqdifResponse] = await Promise.all([getICDAllList(), getPQDIFAllList()])
icdOptions.value = (response.data as ICD.ResICD[]).map(item => ({
id: item.id,
name: item.name
}))
pqdifOptions.value = pqdifResponse.data as PQDIF.ResPQDIF[]
})
</script>

View File

@@ -33,50 +33,20 @@
</ProTable>
</div>
<IcdPopup ref="icdPopup" :refresh-table="proTable?.getTableList" />
<el-dialog
v-model="importDialogVisible"
<JsonImportDialog
ref="jsonImportDialog"
title="导入ICD"
width="460px"
destroy-on-close
:close-on-click-modal="!importLoading"
:show-close="!importLoading"
>
<div v-loading="importLoading" element-loading-text="导入中,请稍候..." class="icd-import-content">
<el-upload
ref="importUploadRef"
action="#"
class="upload"
:auto-upload="false"
:limit="1"
accept=".json,application/json"
:on-exceed="handleImportExceed"
:before-upload="beforeImportUpload"
:on-change="handleImportChange"
:on-remove="handleImportRemove"
:disabled="importLoading"
>
<el-button type="primary" :icon="Upload" :disabled="importLoading">选择文件</el-button>
<template #tip>
<div class="el-upload__tip">请上传 .json 文件</div>
</template>
</el-upload>
<div v-if="importLoading" class="icd-import-loading-text">导入中请稍候...</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button :disabled="importLoading" @click="closeImportDialog">取消</el-button>
<el-button type="primary" :loading="importLoading" @click="submitImport">开始导入</el-button>
</div>
</template>
</el-dialog>
:request-api="importICDJson"
@success="handleImportSuccess"
/>
</template>
<script setup lang="tsx" name="useIcd">
import { reactive, ref } from 'vue'
import { CirclePlus, Delete, Download, EditPen, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElNotification, genFileId, type UploadFile, type UploadInstance, type UploadProps, type UploadRawFile } from 'element-plus'
import type { ICD } from '@/api/device/interface/icd'
import ProTable from '@/components/ProTable/index.vue'
import JsonImportDialog from '@/components/JsonImportDialog/index.vue'
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
import { useHandleData } from '@/hooks/useHandleData'
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
@@ -90,10 +60,7 @@ defineOptions({
const proTable = ref<ProTableInstance>()
const icdPopup = ref<InstanceType<typeof IcdPopup>>()
const importDialogVisible = ref(false)
const importLoading = ref(false)
const importUploadRef = ref<UploadInstance | null>(null)
const selectedImportFile = ref<File | null>(null)
const jsonImportDialog = ref<InstanceType<typeof JsonImportDialog>>()
const getTableList = async (params: ICD.ReqICDParams) => {
return getICDList({ ...params })
@@ -128,7 +95,7 @@ const columns = reactive<ColumnProps<ICD.ResICD>[]>([
prop: 'type',
label: '类型',
minWidth: 180,
enum: ICD_TYPE_OPTIONS,
enum: [...ICD_TYPE_OPTIONS],
fieldNames: { label: 'label', value: 'value' },
search: {
el: 'select',
@@ -180,79 +147,10 @@ const handleExport = async (row: ICD.ResICD) => {
}
const openImportDialog = () => {
importDialogVisible.value = true
jsonImportDialog.value?.open()
}
const closeImportDialog = () => {
if (importLoading.value) {
return
}
importDialogVisible.value = false
selectedImportFile.value = null
importUploadRef.value?.clearFiles()
}
const beforeImportUpload = (file: UploadRawFile) => {
const isJsonFile = file.name.toLowerCase().endsWith('.json') || file.type === 'application/json'
if (!isJsonFile) {
ElNotification({
title: '温馨提示',
message: '上传文件只能是 json 格式!',
type: 'warning'
})
}
return isJsonFile
}
const handleImportExceed: UploadProps['onExceed'] = files => {
importUploadRef.value?.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
importUploadRef.value?.handleStart(file)
selectedImportFile.value = file
}
const handleImportChange: UploadProps['onChange'] = uploadFile => {
selectedImportFile.value = (uploadFile.raw as File | undefined) ?? null
}
const handleImportRemove = () => {
selectedImportFile.value = null
}
const submitImport = async () => {
if (importLoading.value) {
return
}
if (!selectedImportFile.value) {
ElMessage.warning('请选择文件!')
return
}
const formData = new FormData()
formData.append('file', selectedImportFile.value)
importLoading.value = true
try {
await importICDJson(formData)
ElMessage.success('导入成功')
importDialogVisible.value = false
selectedImportFile.value = null
importUploadRef.value?.clearFiles()
await proTable.value?.getTableList()
} finally {
importLoading.value = false
}
const handleImportSuccess = () => {
proTable.value?.getTableList()
}
</script>
<style scoped>
.icd-import-content {
min-height: 120px;
}
.icd-import-loading-text {
margin-top: 12px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<div class="table-box">
<ProTable ref="proTable" :columns="columns" :request-api="getTableList">
<template #tableHeader>
<el-button v-auth.pqdif="'import'" type="primary" plain :icon="Upload" @click="openImportDialog">
导入
</el-button>
</template>
</ProTable>
</div>
<JsonImportDialog
ref="jsonImportDialog"
title="导入PQDIF"
:request-api="importPQDIFJson"
@success="handleImportSuccess"
/>
</template>
<script setup lang="tsx" name="usePqdif">
import { reactive, ref } from 'vue'
import { Upload } from '@element-plus/icons-vue'
import type { PQDIF } from '@/api/device/interface/pqdif'
import { getPQDIFList, importPQDIFJson } from '@/api/device/pqdif'
import ProTable from '@/components/ProTable/index.vue'
import JsonImportDialog from '@/components/JsonImportDialog/index.vue'
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
defineOptions({
name: 'pqdif'
})
const proTable = ref<ProTableInstance>()
const jsonImportDialog = ref<InstanceType<typeof JsonImportDialog>>()
const getTableList = async (params: PQDIF.ReqPQDIFParams) => {
return getPQDIFList({ ...params })
}
const formatDateTime = (value?: string | null) => {
if (!value) {
return '--'
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return value
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const RESULT_OPTIONS = [
{ label: '成功', value: 1 },
{ label: '失败', value: 0 }
]
const columns = reactive<ColumnProps<PQDIF.ResPQDIF>[]>([
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
{
prop: 'name',
label: 'PQDIF文件名称',
minWidth: 180,
search: { el: 'input' }
},
{
prop: 'filePath',
label: '原始文件路径',
minWidth: 260,
render: scope => scope.row.filePath || '--'
},
{
prop: 'recordCount',
label: 'Record总数',
width: 130,
render: scope => String(scope.row.recordCount ?? '--')
},
{
prop: 'observationCount',
label: 'Observation总数',
width: 150,
render: scope => String(scope.row.observationCount ?? '--')
},
{
prop: 'sampleValueCount',
label: '样例采样值数量',
width: 160,
render: scope => String(scope.row.sampleValueCount ?? '--')
},
{
prop: 'result',
label: '解析结果',
width: 120,
enum: RESULT_OPTIONS,
fieldNames: { label: 'label', value: 'value' },
search: {
el: 'select',
props: {
clearable: true
}
},
render: scope => {
if (scope.row.result === 1) {
return <el-tag type="success">成功</el-tag>
}
if (scope.row.result === 0) {
return <el-tag type="danger">失败</el-tag>
}
return '--'
}
},
{
prop: 'msg',
label: '解析提示',
minWidth: 220,
render: scope => scope.row.msg || '--'
},
{
prop: 'createTime',
label: '创建时间',
width: 180,
render: scope => formatDateTime(scope.row.createTime)
},
{
prop: 'updateTime',
label: '更新时间',
width: 180,
render: scope => formatDateTime(scope.row.updateTime)
}
])
const openImportDialog = () => {
jsonImportDialog.value?.open()
}
const handleImportSuccess = () => {
proTable.value?.getTableList()
}
</script>

14
frontend/vitest.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "jsdom",
include: ["frontend/src/**/*.spec.ts"]
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src")
}
}
});