变频器功能页面

This commit is contained in:
caozehui
2026-04-17 09:15:58 +08:00
parent 465ad81069
commit b826f505ac
10 changed files with 1860 additions and 51 deletions

View File

@@ -1,6 +1,10 @@
import type {Device} from '@/api/device/interface/device'
import http from '@/api'
export const getPqDevListAll = () => {
return http.get<Device.ResPqDev[]>(`/pqDev/listAll`)
}
/**
* @name 被检设备管理模块
*/

View File

@@ -25,5 +25,28 @@ export const deleteFreqConverter = (params: string[]) => {
return http.post(`/freqConverter/delete`, params)
}
export const getFreqConverterResult = (params: { converterId?: string }) => {
return http.get(`/freqConverter/result?converterId=${params.converterId || ''}`)
}
export const startFreqConverterDetect = (params: {
converterId?: string;
monitorId?: string;
userId?: string | null;
reset?: boolean;
}) => {
return http.get(`/prepare/startFreqConverter`, params, {loading: false})
}
export const stopFreqConverterDetect = (params: {
userId?: string | null;
}) => {
return http.get(`/prepare/stopFreqConverter`, params, {loading: false})
}
export const getFreqConverterSCurve = (params: FreqConverter.ReqFreqConverterSCurveParams) => {
return http.get(`/freqConverter/scurve?converterId=${params.converterId || ''}`)
}

View File

@@ -25,6 +25,7 @@ export namespace FreqConverter {
timeoutMs: number; //超时时间(毫秒)
suffix?: number; //数据表后缀
state?: number;
testStatus?: number; //测试状态 0未测试 1测试完成
createBy?: string | null; //创建用户
createTime?: string | null; //创建时间
updateBy?: string | null; //更新用户
@@ -37,4 +38,14 @@ export namespace FreqConverter {
export interface ResFreqConverterPage extends ResPage<ResFreqConverter> {
}
}
export interface ReqFreqConverterSCurveParams {
converterId?: string;
}
export interface ResTolerantPoint {
durationMs?: number | null;
residualVoltage?: number | null;
tolerant?: number | null;
}
}

View File

@@ -0,0 +1,530 @@
<template>
<el-card class="section-card" shadow="never">
<template #header>
<div class="section-header">
<span>通道配对</span>
<span class="section-tip">选择设备后默认连接到第一个通道如需调整可删除后重新配对</span>
</div>
</template>
<div class="toolbar">
<div class="toolbar-item">
<span class="toolbar-label">设备</span>
<el-select
v-model="selectedDeviceId"
class="device-select"
filterable
:loading="loading"
placeholder="请选择设备"
>
<el-option
v-for="item in deviceOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</div>
</div>
<div v-if="selectedDevice" ref="flowContainerRef" class="flow-container" :style="{height: `${flowHeight}px`}">
<VueFlow
:nodes="nodes"
:edges="edges"
:edge-types="edgeTypes"
:nodes-draggable="false"
:elements-selectable="true"
:zoom-on-scroll="false"
:pan-on-drag="false"
:zoom-on-double-click="false"
:prevent-scrolling="true"
:min-zoom="0.35"
:max-zoom="1"
fit-view-on-init
@connect="handleConnect"
/>
</div>
<el-empty v-else description="请选择设备后进行通道配对" />
</el-card>
</template>
<script lang="ts" setup>
import {ElMessage} from 'element-plus'
import {computed, h, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {Position, VueFlow, useVueFlow, type Connection, type Edge, type Node} from '@vue-flow/core'
import {getPqDevListAll} from '@/api/device/device'
import {type Device} from '@/api/device/interface/device'
import {type FreqConverter} from '@/api/device/interface/freqConverter'
import FreqConverterDetectEdge from '@/views/machine/freqConverter/components/freqConverterDetectEdge.vue'
const props = defineProps<{
freqConverter: FreqConverter.ResFreqConverter | null;
}>()
const loading = ref(false)
const deviceOptions = ref<Device.ResPqDev[]>([])
const selectedDeviceId = ref('')
const nodes = ref<Node[]>([])
const edges = ref<Edge[]>([])
const flowContainerRef = ref<HTMLElement | null>(null)
const selectedTargetChannelId = ref('')
const {fitView} = useVueFlow()
const flowHeight = 340
let flowContainerResizeObserver: ResizeObserver | null = null
const edgeTypes = {
deletable: FreqConverterDetectEdge
}
const selectedDevice = computed(() => {
return deviceOptions.value.find(item => item.id === selectedDeviceId.value) || null
})
const createDeviceLabel = (title: string, lines: string[]) => {
return h(
'div',
{class: 'device-node-label'},
[
h('div', {class: 'device-node-title'}, title),
...lines.map(line => h('div', {class: 'device-node-line'}, line))
]
) as any
}
const createChannelLabel = (title: string) => {
return h(
'div',
{class: 'channel-node-label'},
h('span', {class: 'channel-node-text'}, title)
) as any
}
const createConnectionEdge = (source: string, target: string): Edge => ({
id: `${source}-${target}`,
source,
target,
type: 'deletable',
selectable: true,
focusable: true,
data: {
onDelete: removeConnectionById
},
style: {
stroke: 'var(--el-color-primary)'
}
})
const getDeviceChannels = (device: Device.ResPqDev | null) => {
if (!device) {
return [] as string[]
}
const channelCount = Math.max(Number(device.devChns) || 0, 0)
return Array.from({length: channelCount}, (_, index) => `${index + 1}`)
}
const buildNodes = (device: Device.ResPqDev) => {
const channelList = getDeviceChannels(device)
const flowNodes: Node[] = []
const channelCount = Math.max(channelList.length, 1)
const flowCanvasHeight = flowHeight
const gapY = 42
const channelHeight = 20
const cardHeight = 120
const cardWidth = 210
const layoutCenterY = flowCanvasHeight / 2
const channelGroupHeight = channelHeight + (channelCount - 1) * gapY
const startY = layoutCenterY - channelGroupHeight / 2
const cardY = layoutCenterY - cardHeight / 2
flowNodes.push({
id: `freq-converter-${props.freqConverter?.id || 'default'}`,
type: 'output',
data: {
label: createDeviceLabel('变频器', [
`名称:${props.freqConverter?.name || '-'}`,
`串口:${props.freqConverter?.portName || '-'}`,
`从机地址:${props.freqConverter?.slaveAddress ?? '-'}`,
`波特率:${props.freqConverter?.baudRate ?? '-'}`
])
},
position: {x: 84, y: cardY},
sourcePosition: Position.Right,
draggable: false,
selectable: false,
class: 'freq-converter-node',
style: {width: `${cardWidth}px`, height: `${cardHeight}px`, border: 'none', boxShadow: 'none', background: 'transparent'}
})
flowNodes.push({
id: `device-card-${device.id}`,
data: {
label: createDeviceLabel('设备', [
`名称:${device.name || '-'}`,
`类型:${device.devType || '-'}`,
`IP${device.ip || '-'}`,
`通道数:${channelList.length}`
])
},
position: {x: 654, y: cardY},
draggable: false,
selectable: false,
class: 'no-handle-node',
style: {width: `${cardWidth}px`, height: `${cardHeight}px`, border: 'none', boxShadow: 'none', background: 'transparent'}
})
channelList.forEach((channel, index) => {
const y = startY + index * gapY
flowNodes.push({
id: `device-channel-${device.id}-${channel}`,
type: 'input',
data: {label: createChannelLabel(`通道${channel}`)},
position: {x: 584, y},
targetPosition: Position.Left,
draggable: false,
selectable: false,
class: 'channel-node',
style: {width: '68px', height: `${channelHeight}px`, border: 'none', boxShadow: 'none', background: 'transparent'}
})
})
return flowNodes
}
const fitFlowView = async () => {
if (!nodes.value.length) {
return
}
await nextTick()
await fitView({
padding: 0.18,
duration: 0
})
}
const observeFlowContainer = () => {
flowContainerResizeObserver?.disconnect()
flowContainerResizeObserver = null
if (!flowContainerRef.value || typeof ResizeObserver === 'undefined') {
return
}
flowContainerResizeObserver = new ResizeObserver(() => {
void fitFlowView()
})
flowContainerResizeObserver.observe(flowContainerRef.value)
}
const getExpectedSourceId = () => `freq-converter-${props.freqConverter?.id || 'default'}`
const getFirstChannelId = (device: Device.ResPqDev | null) => {
if (!device) {
return ''
}
const [firstChannel] = getDeviceChannels(device)
return firstChannel ? `device-channel-${device.id}-${firstChannel}` : ''
}
const syncEdges = () => {
const source = getExpectedSourceId()
edges.value = selectedTargetChannelId.value ? [createConnectionEdge(source, selectedTargetChannelId.value)] : []
}
const rebuildNodes = async () => {
const currentDevice = selectedDevice.value
nodes.value = currentDevice ? buildNodes(currentDevice) : []
if (currentDevice && selectedTargetChannelId.value && !selectedTargetChannelId.value.startsWith(`device-channel-${currentDevice.id}-`)) {
selectedTargetChannelId.value = getFirstChannelId(currentDevice)
}
syncEdges()
await nextTick()
observeFlowContainer()
await fitFlowView()
}
const clearConnections = () => {
selectedTargetChannelId.value = ''
edges.value = []
}
const removeConnectionById = (edgeId: string) => {
if (edges.value.some(item => item.id === edgeId)) {
selectedTargetChannelId.value = ''
}
edges.value = edges.value.filter(item => item.id !== edgeId)
}
const handleConnect = (params: Connection) => {
if (!params.source || !params.target) {
return
}
const expectedSource = getExpectedSourceId()
const connectionNodeIds = [params.source, params.target]
const hasFreqConverterNode = connectionNodeIds.includes(expectedSource)
const deviceChannelId = connectionNodeIds.find(item => item.startsWith('device-channel-'))
const sourceNode = nodes.value.find(item => item.id === expectedSource)
const targetNode = nodes.value.find(item => item.id === deviceChannelId)
if (!hasFreqConverterNode || !deviceChannelId || sourceNode?.type !== 'output' || targetNode?.type !== 'input') {
ElMessage.warning('只能从左侧变频器连线到右侧设备通道')
return
}
if (edges.value.length > 0) {
ElMessage.warning('变频器只能选择一个设备通道,如需重选请先删除配对')
return
}
selectedTargetChannelId.value = deviceChannelId
syncEdges()
}
const loadDeviceOptions = async () => {
loading.value = true
try {
const result = await getPqDevListAll()
const records = Array.isArray(result?.data) ? result.data : []
deviceOptions.value = records
if (records.length) {
selectedDeviceId.value = records[0].id
return
}
selectedDeviceId.value = ''
} finally {
loading.value = false
}
}
const getChannelMapping = () => {
const edge = edges.value[0]
if (!edge) {
return null
}
const targetParts = edge.target.split('-')
return {
deviceId: selectedDevice.value?.id || '',
deviceName: selectedDevice.value?.name || '',
deviceChannel: targetParts[targetParts.length - 1],
freqConverterId: props.freqConverter?.id || '',
freqConverterName: props.freqConverter?.name || ''
}
}
watch(selectedDeviceId, async () => {
selectedTargetChannelId.value = getFirstChannelId(selectedDevice.value)
await rebuildNodes()
})
watch(
() => props.freqConverter?.id,
async () => {
selectedTargetChannelId.value = getFirstChannelId(selectedDevice.value)
await rebuildNodes()
}
)
onMounted(async () => {
await loadDeviceOptions()
selectedTargetChannelId.value = getFirstChannelId(selectedDevice.value)
await rebuildNodes()
})
onBeforeUnmount(() => {
flowContainerResizeObserver?.disconnect()
})
defineExpose({
getSelectedDevice: () => selectedDevice.value,
hasValidConnection: () => edges.value.length === 1,
getChannelMapping,
clearConnections,
resetSelection: () => {
const firstDevice = deviceOptions.value[0] || null
selectedDeviceId.value = firstDevice?.id || ''
selectedTargetChannelId.value = getFirstChannelId(firstDevice)
void rebuildNodes()
}
})
</script>
<style scoped>
.section-card {
border: 1px solid var(--el-border-color-light);
}
:deep(.section-card .el-card__header) {
padding: 10px 14px;
}
:deep(.section-card .el-card__body) {
padding: 10px 14px 14px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.section-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.toolbar-item {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.toolbar-label {
color: var(--el-text-color-regular);
white-space: nowrap;
}
.device-select {
width: 280px;
max-width: 100%;
}
.flow-container {
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
overflow: hidden;
}
:deep(.vue-flow__node) {
border-radius: 8px;
border: 1px solid var(--el-border-color);
background: var(--el-bg-color-page);
box-shadow: none;
}
:deep(.vue-flow__node.no-handle-node .vue-flow__handle) {
display: none;
}
:deep(.vue-flow__handle) {
width: 0;
height: 0;
min-width: 0;
min-height: 0;
background: transparent;
border: none;
overflow: visible;
}
:deep(.vue-flow__handle::before) {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--el-color-primary);
transform: translate(-50%, -50%);
}
:deep(.vue-flow__node.channel-node) {
border: none;
background: transparent;
box-shadow: none;
}
:deep(.vue-flow__node.channel-node .vue-flow__handle) {
top: 50%;
left: 0;
transform: translate(-50%, -50%);
}
:deep(.vue-flow__node.freq-converter-node .vue-flow__handle) {
top: 50%;
right: 0;
left: auto;
bottom: auto;
transform: translate(50%, -50%);
}
:deep(.device-node-label) {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
height: 100%;
box-sizing: border-box;
border: 1px solid var(--el-border-color);
border-radius: 10px;
background: var(--el-fill-color-light);
padding: 10px 12px;
color: var(--el-text-color-primary);
}
:deep(.device-node-title) {
font-weight: 600;
}
:deep(.device-node-line) {
font-size: 12px;
line-height: 1.4;
}
:deep(.channel-node-label) {
display: flex;
align-items: center;
justify-content: flex-start;
height: 100%;
padding: 0 0 0 10px;
font-size: 12px;
color: var(--el-text-color-primary);
text-align: left;
}
:deep(.channel-node-text) {
display: inline-flex;
align-items: center;
min-height: 20px;
line-height: 1;
}
@media (max-width: 1200px) {
.section-header {
align-items: flex-start;
flex-direction: column;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar-item {
flex-wrap: wrap;
}
.device-select {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import {Delete} from '@element-plus/icons-vue'
import {BaseEdge, EdgeLabelRenderer, getBezierPath, useVueFlow} from '@vue-flow/core'
import {computed} from 'vue'
const props = defineProps<{
id: string;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: string;
targetPosition: string;
markerEnd?: string;
style?: Record<string, any>;
selected?: boolean;
data?: {
onDelete?: (id: string) => void;
};
}>()
const {removeEdges} = useVueFlow()
const path = computed(() => getBezierPath(props))
const edgeStyle = computed(() => ({
...props.style,
strokeWidth: props.selected ? 3 : 2,
strokeLinecap: 'round',
stroke: props.selected ? 'var(--el-color-danger)' : props.style?.stroke || 'var(--el-color-primary)'
}))
const handleDelete = () => {
if (props.data?.onDelete) {
props.data.onDelete(props.id)
return
}
removeEdges(props.id)
}
</script>
<script lang="ts">
export default {
inheritAttrs: false
}
</script>
<template>
<BaseEdge :id="id" :path="path[0]" :marker-end="markerEnd" :style="edgeStyle" />
<EdgeLabelRenderer>
<div
v-if="selected"
class="edge-delete-trigger nodrag nopan"
:style="{
transform: `translate(-50%, -50%) translate(${path[1]}px, ${path[2]}px)`
}"
>
<el-popconfirm
title="是否删除当前连线?"
confirm-button-text=""
cancel-button-text=""
width="180"
@confirm="handleDelete"
>
<template #reference>
<button class="delete-button" type="button" aria-label="删除连线">
<el-icon><Delete /></el-icon>
</button>
</template>
</el-popconfirm>
</div>
</EdgeLabelRenderer>
</template>
<style scoped>
.edge-delete-trigger {
position: absolute;
pointer-events: all;
z-index: 20;
}
.delete-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: 1px solid var(--el-border-color);
border-radius: 50%;
background: var(--el-bg-color);
color: var(--el-color-danger);
cursor: pointer;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.14);
}
.delete-button:hover {
border-color: var(--el-color-danger);
}
</style>

View File

@@ -0,0 +1,601 @@
<template>
<el-card class="dip-chart-card" shadow="never">
<template #header>
<div class="card-header">
<div class="card-header-main">
<div class="card-title">耐受图</div>
<div class="card-subtitle">
{{ selectedMappingText }}
</div>
</div>
<el-button
type="primary"
plain
:loading="curveLoading"
class="draw-curve-button"
@click="drawCharacteristicCurve"
>
绘制特性曲线
</el-button>
</div>
</template>
<div class="chart-wrapper">
<MyEchart :options="chartOptions"/>
</div>
</el-card>
</template>
<script lang="ts" setup>
import {computed, ref, watch} from 'vue'
import {ElMessage} from 'element-plus'
import MyEchart from '@/components/echarts/line/index.vue'
import {getFreqConverterSCurve} from '@/api/device/freqConverter'
type ChartPointStatus = 'pass' | 'fail'
interface ChartPoint {
duration: number;
residualVoltage: number;
status: ChartPointStatus;
}
interface NormalizedTolerantPoint {
duration: number;
residualVoltage: number;
tolerant: number | null;
status: ChartPointStatus;
}
const props = defineProps<{
selectedMapping?: Record<string, any> | null;
webMsgSend?: any;
resultData?: any;
}>()
const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = {
pass: '#1d4ed8',
fail: '#111111'
}
const chartPoints = ref<ChartPoint[]>([])
const characteristicCurveData = ref<Array<[number, number]>>([])
const curveLoading = ref(false)
const selectedMappingText = computed(() => {
if (!props.selectedMapping) {
return '未选择变频器'
}
return `变频器:${props.selectedMapping.freqConverterName || '-'}`
})
const normalizeTolerantValue = (value: unknown) => {
if (value === undefined || value === null || value === '') {
return null
}
const result = Number(value)
if ([0, 1, 2].includes(result)) {
return result
}
return null
}
const normalizeDuration = (source: Record<string, any>) => {
return toNumber(
source.durationMs !== undefined && source.durationMs !== null
? Number(source.durationMs) / 1000
: source.duration ??
source.x ??
source.dipDuration ??
source.retainTime ??
source.durationValue
)
}
const normalizeResidualVoltageValue = (source: Record<string, any>) => {
return toNumber(
source.residualVoltage ??
source.y ??
source.residual ??
source.voltage ??
source.residual_value
)
}
const normalizeCurveData = (source: unknown) => {
if (!Array.isArray(source)) {
return [] as Array<[number, number]>
}
return source
.map(item => {
if (Array.isArray(item) && item.length >= 2) {
const duration = Number(item[0])
const residualVoltage = Number(item[1])
if (Number.isFinite(duration) && Number.isFinite(residualVoltage)) {
return [duration, residualVoltage] as [number, number]
}
}
if (item && typeof item === 'object') {
const record = item as Record<string, any>
const tolerant = normalizeTolerantValue(record.tolerant)
if (tolerant !== null && tolerant !== 2) {
return null
}
const duration = normalizeDuration(record)
const residualVoltage = normalizeResidualVoltageValue(record)
if (duration !== null && residualVoltage !== null) {
return [duration, residualVoltage] as [number, number]
}
}
return null
})
.filter((item): item is [number, number] => !!item)
}
const extractCurveData = (payload: any) => {
if (!payload) {
return [] as Array<[number, number]>
}
const candidates = [
payload,
payload?.data,
payload?.data?.records,
payload?.data?.points,
payload?.points,
payload?.records,
payload?.list
]
for (const candidate of candidates) {
const normalized = normalizeCurveData(candidate)
if (normalized.length) {
return normalized
}
}
return [] as Array<[number, number]>
}
const drawCharacteristicCurve = async () => {
const freqConverterId = props.selectedMapping?.freqConverterId
if (!freqConverterId) {
ElMessage.warning('未获取到变频器ID')
return
}
curveLoading.value = true
try {
const result = await getFreqConverterSCurve({
converterId: freqConverterId
})
const normalizedCurveData = extractCurveData(result)
if (!normalizedCurveData.length) {
characteristicCurveData.value = []
ElMessage.warning('未获取到特性曲线数据')
return
}
characteristicCurveData.value = normalizedCurveData
} catch (error) {
console.error('绘制特性曲线失败:', error)
characteristicCurveData.value = []
} finally {
curveLoading.value = false
}
}
const chartOptions = computed(() => {
const maxDuration = 2
// const maxDuration = Math.max(
// 2,
// ...chartPoints.value.map(item => item.duration).filter(item => Number.isFinite(item)),
// ...characteristicCurveData.value.map(item => item[0]).filter(item => Number.isFinite(item))
// )
return {
title: {
text: ''
},
grid: {
top: 30,
left: 48,
right: 22,
bottom: 52
},
tooltip: {
trigger: 'item',
formatter(params: any) {
if (params.seriesType === 'line') {
return ''
}
const [duration, residualVoltage, statusText] = params.value
return [
`持续时间: ${duration} s`,
`残余电压: ${residualVoltage} %`,
`状态: ${statusText}`
].join('<br/>')
}
},
legend: {
top: 0,
right: 0,
data: ['特性测试曲线']
},
xAxis: {
type: 'value',
name: '持续时间(s)',
nameLocation: 'middle',
nameGap: 34,
min: 0.00,
max: maxDuration,
interval: 0.1,
minorTick: {
show: true,
splitNumber: 10
},
minorSplitLine: {
show: true,
lineStyle: {
color: '#e8edf6'
}
},
splitLine: {
lineStyle: {
color: '#cfd8e6'
}
},
axisLabel: {
formatter(value: number) {
return value.toFixed(2)
}
}
},
yAxis: {
type: 'value',
name: '暂降幅值',
min: 0,
max: 100,
interval: 10,
minorTick: {
show: true,
splitNumber: 2
},
minorSplitLine: {
show: true,
lineStyle: {
color: '#e8edf6'
}
},
splitLine: {
lineStyle: {
color: '#cfd8e6'
}
},
axisLabel: {
formatter(value: number) {
return `${value}%`
}
}
},
dataZoom: [],
series: [
{
name: '特性测试曲线',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 7,
lineStyle: {
color: '#ff2a2a',
width: 3
},
itemStyle: {
color: '#ff2a2a'
},
data: characteristicCurveData.value
},
{
name: '暂降点',
type: 'scatter',
symbolSize: 10,
data: chartPoints.value.map(item => ({
value: [item.duration, item.residualVoltage, getStatusText(item.status)],
itemStyle: {
color: STATUS_COLOR_MAP[item.status]
}
}))
}
],
options: {
animation: false
}
}
})
const toNumber = (value: unknown) => {
const result = Number(value)
return Number.isFinite(result) ? result : null
}
const normalizeStatus = (value: unknown): ChartPointStatus => {
const rawValue = `${value ?? ''}`.trim().toLowerCase()
if (
value === 0 ||
rawValue === '0' ||
rawValue === 'false' ||
rawValue === 'fail' ||
rawValue === 'failed' ||
rawValue.includes('不耐受')
) {
return 'fail'
}
return 'pass'
}
const normalizeTolerantPoint = (source: Record<string, any>): NormalizedTolerantPoint | null => {
const duration = normalizeDuration(source)
const residualVoltage = normalizeResidualVoltageValue(source)
if (duration === null || residualVoltage === null) {
return null
}
if (duration <= 0 || residualVoltage < 0 || residualVoltage > 100) {
return null
}
const tolerant = normalizeTolerantValue(
source.tolerant ??
source.endure ??
source.isEndure ??
source.tolerable ??
source.isTolerable ??
source.status ??
source.pointStatus ??
source.result ??
source.state
)
return {
duration,
residualVoltage,
tolerant,
status:
tolerant === 0
? 'fail'
: tolerant === 1
? 'pass'
: normalizeStatus(
source.tolerant ??
source.endure ??
source.isEndure ??
source.tolerable ??
source.isTolerable ??
source.status ??
source.pointStatus ??
source.result ??
source.state
)
}
}
const getStatusText = (status: ChartPointStatus) => {
if (status === 'fail') {
return '不耐受'
}
return '耐受'
}
const normalizePoint = (source: Record<string, any>): ChartPoint | null => {
const point = normalizeTolerantPoint(source)
if (!point || point.tolerant === 2) {
return null
}
return {
duration: point.duration,
residualVoltage: point.residualVoltage,
status: point.status
}
}
const extractCharacteristicCurvePoints = (payload: any) => {
const result: Array<[number, number]> = []
const seen = new Set<string>()
const rootPayload = payload?.data && typeof payload.data === 'object' ? payload.data : payload
const walk = (node: any) => {
if (!node) {
return
}
if (Array.isArray(node)) {
node.forEach(item => walk(item))
return
}
if (typeof node !== 'object') {
return
}
const point = normalizeTolerantPoint(node)
if (point?.tolerant === 2) {
const key = `${point.duration}|${point.residualVoltage}`
if (!seen.has(key)) {
seen.add(key)
result.push([point.duration, point.residualVoltage])
}
}
Object.values(node).forEach(item => {
if (item && typeof item === 'object') {
walk(item)
}
})
}
walk(rootPayload)
return result
}
const extractPoints = (payload: any) => {
const result: ChartPoint[] = []
const seen = new Set<string>()
const rootPayload = payload?.data && typeof payload.data === 'object' ? payload.data : payload
const walk = (node: any) => {
if (!node) {
return
}
if (Array.isArray(node)) {
node.forEach(item => walk(item))
return
}
if (typeof node !== 'object') {
return
}
const point = normalizePoint(node)
if (point) {
const key = `${point.duration}|${point.residualVoltage}`
if (!seen.has(key)) {
seen.add(key)
result.push(point)
}
}
Object.values(node).forEach(item => {
if (item && typeof item === 'object') {
walk(item)
}
})
}
walk(rootPayload)
return result
}
watch(
() => props.webMsgSend,
newValue => {
if (!newValue) {
return
}
const nextPoints = extractPoints(newValue)
if (!nextPoints.length) {
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
return
}
const existingPointMap = new Map(
chartPoints.value.map(item => [`${item.duration}|${item.residualVoltage}`, item] as const)
)
nextPoints.forEach(item => {
const key = `${item.duration}|${item.residualVoltage}`
existingPointMap.set(key, item)
})
chartPoints.value = Array.from(existingPointMap.values())
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
},
{deep: true}
)
watch(
() => props.resultData,
newValue => {
if (!newValue) {
return
}
chartPoints.value = extractPoints(newValue)
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
},
{deep: true, immediate: true}
)
watch(
() => props.selectedMapping,
() => {
chartPoints.value = []
characteristicCurveData.value = []
}
)
</script>
<style scoped>
.dip-chart-card {
border: 1px solid var(--el-border-color-light);
}
:deep(.dip-chart-card .el-card__header) {
padding: 10px 14px;
}
:deep(.dip-chart-card .el-card__body) {
padding: 10px 14px 14px;
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.card-header-main {
min-width: 0;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.card-subtitle {
margin-top: 4px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.draw-curve-button {
margin-top: -2px;
flex-shrink: 0;
}
.chart-wrapper {
height: 400px;
}
</style>

View File

@@ -67,7 +67,7 @@ import {ElMessage, type FormItemRule} from 'element-plus'
import {computed, ref} from 'vue'
import {type FreqConverter} from '@/api/device/interface/freqConverter'
import {dialogMiddle} from '@/utils/elementBind'
import {addFreqConverter, updateFreqConverter} from '@/api/device/freqConverter/index.ts'
import {addFreqConverter, updateFreqConverter} from '@/api/device/freqConverter'
// 定义弹出组件元信息
const dialogFormRef = ref()
@@ -108,6 +108,18 @@ const resetFormContent = () => {
}
}
const getDefaultFormContent = (): FreqConverter.ResFreqConverter => ({
id: '',
name: '',
portName: '',
slaveAddress: 1,
baudRate: 9600,
parity: 'None',
dataBits: 8,
stopBits: 1,
timeoutMs: 500,
})
let dialogTitle = computed(() => {
return titleType.value === 'add' ? '新增变频器' : '编辑变频器'
})
@@ -171,14 +183,14 @@ const save = () => {
}
// 打开弹窗,可能是新增,也可能是编辑
const open = async (sign: string, data: FreqConverter.ResFreqConverter) => {
const open = async (sign: string, data: Partial<FreqConverter.ResFreqConverter> = {}) => {
// 重置表单
dialogFormRef.value?.resetFields()
titleType.value = sign
dialogVisible.value = true
if (data.id) {
formContent.value = {...data}
formContent.value = {...getDefaultFormContent(), ...data}
} else {
resetFormContent()
}
@@ -190,4 +202,4 @@ const props = defineProps<{
refreshTable: (() => Promise<void>) | undefined;
}>()
</script>
</script>

View File

@@ -0,0 +1,121 @@
<template>
<el-dialog
:model-value="dialogVisible"
title="检测结果"
v-bind="dialogBig"
:show-close="true"
destroy-on-close
align-center
@close="handleClose"
>
<div v-loading="loading" class="freq-converter-result-popup">
<FreqConverterDipChart
v-if="dialogVisible && selectedMapping"
:selected-mapping="selectedMapping"
:result-data="resultPayload"
/>
<el-empty v-else-if="!loading" description="暂无检测结果" />
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import {ElMessage} from 'element-plus'
import {dialogBig} from '@/utils/elementBind'
import {getFreqConverterResult} from '@/api/device/freqConverter'
import {type FreqConverter} from '@/api/device/interface/freqConverter'
import FreqConverterDipChart from '@/views/machine/freqConverter/components/freqConverterDipChart.vue'
const dialogVisible = ref(false)
const loading = ref(false)
const resultPayload = ref<any[]>([])
const selectedMapping = ref<Record<string, any> | null>(null)
const extractResultArray = (payload: any) => {
if (Array.isArray(payload)) {
return payload
}
if (Array.isArray(payload?.data)) {
return payload.data
}
if (Array.isArray(payload?.data?.records)) {
return payload.data.records
}
if (Array.isArray(payload?.records)) {
return payload.records
}
if (Array.isArray(payload?.list)) {
return payload.list
}
return [] as any[]
}
const buildSelectedMapping = (row: FreqConverter.ResFreqConverter) => {
return {
freqConverterId: row.id || '',
freqConverterName: row.name || '',
deviceId: '',
deviceName: '-',
deviceChannel: '-'
}
}
const resetState = () => {
resultPayload.value = []
selectedMapping.value = null
}
const handleClose = () => {
dialogVisible.value = false
resetState()
}
const open = async (row: FreqConverter.ResFreqConverter) => {
if (!row.id) {
ElMessage.warning('未获取到变频器ID')
return
}
loading.value = true
dialogVisible.value = true
resetState()
try {
const result = await getFreqConverterResult({converterId: row.id})
resultPayload.value = extractResultArray(result?.data ?? result)
selectedMapping.value = buildSelectedMapping(row)
} catch (error) {
console.error('获取变频器检测结果失败:', error)
ElMessage.error('获取检测结果失败')
handleClose()
} finally {
loading.value = false
}
}
defineExpose({open})
</script>
<style scoped>
.freq-converter-result-popup {
min-height: 460px;
}
:deep(.el-dialog__body) {
padding-top: 10px;
padding-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,375 @@
<template>
<el-dialog
:model-value="dialogVisible"
:title="dialogTitle"
:width="dialogWidth"
:style="dialogStyle"
:close-on-click-modal="dialogBig.closeOnClickModal"
:draggable="dialogBig.draggable"
:class="dialogBig.class"
:show-close="true"
:close-on-press-escape="false"
:before-close="handleBeforeClose"
destroy-on-close
align-center
>
<div class="freq-converter-test-popup">
<el-steps :active="currentStep - 1" finish-status="success" simple>
<el-step title="准备" />
<el-step title="检测" />
</el-steps>
<FreqConverterDetectChannelPairing
v-if="dialogVisible && currentStep === 1"
ref="channelPairingRef"
:freq-converter="currentFreqConverter"
/>
<FreqConverterDipChart
v-else-if="dialogVisible && currentStep === 2"
:selected-mapping="selectedMapping"
:web-msg-send="webSocketMessage"
:result-data="historyResultData"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button
v-if="!startDetectSuccess"
type="primary"
:disabled="currentStep !== 1 || startLoading"
:loading="startLoading"
@click="handleStart"
>
开始检测
</el-button>
<el-button type="danger" plain @click="handleExit">退出检测</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import {ElMessage, ElMessageBox} from 'element-plus'
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'
import {dialogBig} from '@/utils/elementBind'
import {type FreqConverter} from '@/api/device/interface/freqConverter'
import {getFreqConverterResult, startFreqConverterDetect, stopFreqConverterDetect} from '@/api/device/freqConverter'
import {JwtUtil} from '@/utils/jwtUtil'
import FreqConverterDetectChannelPairing from '@/views/machine/freqConverter/components/freqConverterDetectChannelPairing.vue'
import FreqConverterDipChart from '@/views/machine/freqConverter/components/freqConverterDipChart.vue'
import socketClient from '@/utils/webSocketClient'
const SOCKET_CALLBACK_KEY = 'aaa'
const props = defineProps<{
refreshTable?: (() => Promise<void>) | (() => void) | undefined;
}>()
const dialogVisible = ref(false)
const currentStep = ref(1)
const currentFreqConverter = ref<FreqConverter.ResFreqConverter | null>(null)
const channelPairingRef = ref<InstanceType<typeof FreqConverterDetectChannelPairing>>()
const webSocketMessage = ref<any>(null)
const historyResultData = ref<any>(null)
const socketServe = ref<typeof socketClient.Instance | null>(null)
const selectedMapping = ref<Record<string, any> | null>(null)
const startLoading = ref(false)
const startDetectSuccess = ref(false)
const hasSocketError = ref(false)
const viewportWidth = ref(typeof window === 'undefined' ? 1280 : window.innerWidth)
const dialogTitle = computed(() => '变频器检测')
const dialogWidth = computed(() => (viewportWidth.value < 820 ? 'calc(100vw - 24px)' : dialogBig.width))
const dialogStyle = computed(() => ({
maxWidth: dialogBig.maxWidth,
minWidth: viewportWidth.value < 820 ? '320px' : dialogBig.minWidth
}))
const SOCKET_ERROR_MESSAGE_MAP: Record<number, string> = {
10550: '设备连接异常',
10551: '设备触发报告异常',
10552: '重复的初始化操作',
10553: '通讯模块通讯异常',
10554: '报文解析异常',
10556: '不存在上线的设备'
}
const normalizeFormalRealPayload = (payload: any) => {
if (!payload || typeof payload !== 'object') {
return payload
}
if (typeof payload.data !== 'string') {
return payload
}
try {
return {
...payload,
data: JSON.parse(payload.data)
}
} catch (error) {
console.error('formal_real 数据解析失败:', error, payload.data)
return payload
}
}
const handleSocketMessage = (payload: any) => {
const requestId = `${payload?.requestId ?? ''}`
const normalizedRequestId = requestId.trim().toLowerCase()
const code = Number(payload?.code)
if (requestId === 'yjc_sbtxjy' && code !== 10200) {
hasSocketError.value = true
ElMessage.error(SOCKET_ERROR_MESSAGE_MAP[code] || `检测异常,错误码:${code}`)
return
}
if (normalizedRequestId.startsWith('formal_real')) {
webSocketMessage.value = normalizeFormalRealPayload(payload)
}
}
const updateViewportWidth = () => {
viewportWidth.value = window.innerWidth
}
const connectWebSocket = () => {
try {
if (socketServe.value) {
socketServe.value.unRegisterCallBack?.(SOCKET_CALLBACK_KEY)
if (socketServe.value.connected) {
socketServe.value.closeWs()
}
}
socketClient.Instance.connect()
socketServe.value = socketClient.Instance
socketServe.value.registerCallBack(SOCKET_CALLBACK_KEY, handleSocketMessage)
} catch (error) {
console.error('WebSocket连接处理失败:', error)
ElMessage.error('WebSocket连接建立失败请重试')
}
}
const closeWebSocket = () => {
try {
if (socketServe.value) {
socketServe.value.unRegisterCallBack?.(SOCKET_CALLBACK_KEY)
if (socketServe.value.connected) {
socketServe.value.closeWs()
}
socketServe.value = null
}
} catch (error) {
console.error('WebSocket关闭失败:', error)
socketServe.value = null
}
}
const resetState = () => {
currentStep.value = 1
webSocketMessage.value = null
historyResultData.value = null
currentFreqConverter.value = null
selectedMapping.value = null
startLoading.value = false
startDetectSuccess.value = false
hasSocketError.value = false
}
const closeDialog = () => {
dialogVisible.value = false
closeWebSocket()
resetState()
}
const stopDetect = async () => {
if (!startDetectSuccess.value) {
return
}
try {
await stopFreqConverterDetect({
userId: JwtUtil.getLoginName()
})
} catch (error) {
console.error('停止变频器检测失败:', error)
}
}
const confirmExit = async () => {
await ElMessageBox.confirm(
'检测未完成,是否退出当前检测流程?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
}
const isFreqConverterDetected = () => {
return Number(currentFreqConverter.value?.testStatus) === 1
}
const confirmResetLastDetectData = async () => {
if (!isFreqConverterDetected()) {
return true
}
try {
await ElMessageBox.confirm(
'是否覆盖上次检测数据',
'提示',
{
confirmButtonText: '是',
cancelButtonText: '否',
distinguishCancelAndClose: true,
type: 'warning'
}
)
return true
} catch (action) {
if (action === 'cancel') {
return false
}
return null
}
}
const handleBeforeClose = async (done: () => void) => {
if (hasSocketError.value) {
await stopDetect()
closeDialog()
await props.refreshTable?.()
done()
return
}
try {
await confirmExit()
await stopDetect()
closeDialog()
await props.refreshTable?.()
done()
} catch {
// 用户取消关闭
}
}
const handleStart = async () => {
if (currentStep.value === 1) {
const mapping = channelPairingRef.value?.getChannelMapping()
if (!mapping || !channelPairingRef.value?.hasValidConnection()) {
ElMessage.warning('请先选择设备通道并完成连线')
return
}
const reset = await confirmResetLastDetectData()
if (reset === null) {
return
}
startLoading.value = true
try {
if (reset === false) {
const historyResult = await getFreqConverterResult({
converterId: mapping.freqConverterId
})
historyResultData.value = historyResult?.data ?? historyResult
} else {
historyResultData.value = null
}
const res = await startFreqConverterDetect({
converterId: mapping.freqConverterId,
monitorId: `${mapping.deviceId}_${mapping.deviceChannel}`,
userId: JwtUtil.getLoginName(),
reset
})
if (res.code === 'A0000') {
selectedMapping.value = mapping
currentStep.value = 2
startDetectSuccess.value = true
return
}
} catch (error) {
console.error('开始变频器检测失败:', error)
ElMessage.error('开始检测失败')
} finally {
startLoading.value = false
}
return
}
}
const handleExit = async () => {
if (!hasSocketError.value) {
try {
await confirmExit()
} catch {
return
}
}
await stopDetect()
closeDialog()
await props.refreshTable?.()
}
const open = (row: FreqConverter.ResFreqConverter) => {
resetState()
currentFreqConverter.value = {...row}
dialogVisible.value = true
connectWebSocket()
}
onMounted(() => {
window.addEventListener('resize', updateViewportWidth)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateViewportWidth)
closeWebSocket()
})
defineExpose({open, channelPairingRef})
</script>
<style scoped>
.freq-converter-test-popup {
display: flex;
flex-direction: column;
gap: 10px;
}
.section-card {
border: 1px solid var(--el-border-color-light);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.section-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
}
:deep(.el-dialog__body) {
padding-top: 10px;
padding-bottom: 10px;
overflow: hidden;
}
</style>

View File

@@ -1,56 +1,96 @@
<template>
<div class='table-box' ref='popupBaseView'>
<template>
<div class="table-box" ref="popupBaseView">
<ProTable
ref='proTable'
:columns='columns'
:request-api='getTableList'
ref="proTable"
:columns="columns"
:request-api="getTableList"
>
<!-- :requestApi="getRoleList" -->
<!-- 表格 header 按钮 -->
<template #tableHeader='scope'>
<el-button v-auth.freqConverter="'add'" type='primary' :icon='CirclePlus' @click="openDialog('add')">新增</el-button>
<el-button v-auth.freqConverter="'delete'" type='danger' :icon='Delete' plain :disabled='!scope.isSelected'
@click='batchDelete(scope.selectedListIds)'>
<template #tableHeader="scope">
<el-button v-auth.freqConverter="'add'" type="primary" :icon="CirclePlus" @click="openDialog('add')">
新增
</el-button>
<el-button
v-auth.freqConverter="'delete'"
type="danger"
:icon="Delete"
plain
:disabled="!scope.isSelected"
@click="batchDelete(scope.selectedListIds)"
>
删除
</el-button>
</template>
<!-- 表格操作 -->
<template #operation='scope'>
<el-button v-auth.freqConverter="'edit'" type='primary' link :icon='EditPen' :model-value='false' @click="openDialog('edit', scope.row)">编辑
<template #operation="scope">
<el-button
v-auth.freqConverter="'edit'"
type="primary"
link
:icon="EditPen"
:model-value="false"
@click="openDialog('edit', scope.row)"
>
编辑
</el-button>
<el-button v-auth.freqConverter="'delete'" type='primary' link :icon='Delete' @click='handleDelete(scope.row)'>删除
<el-button
v-auth.freqConverter="'delete'"
type="primary"
link
:icon="Delete"
@click="handleDelete(scope.row)"
>
删除
</el-button>
<el-button v-auth.freqConverter="'test'" type='primary' link :icon='Stopwatch' @click='handleTest(scope.row)'>检测
<el-button
v-auth.freqConverter="'test'"
type="primary"
link
:icon="Stopwatch"
@click="handleTest(scope.row)"
>
检测
</el-button>
<el-button v-auth.freqConverter="'result'" type='primary' link :icon='Coin' @click='handleResult(scope.row)'>结果
<el-button
v-auth.freqConverter="'result'"
type="primary"
link
:icon="Coin"
:disabled="scope.row.testStatus !== 1"
@click="handleResult(scope.row)"
>
结果
</el-button>
</template>
</ProTable>
</div>
<FreqConverterPopup :refresh-table='proTable?.getTableList' ref='freqConverterPopup'/>
<FreqConverterPopup :refresh-table="proTable?.getTableList" ref="freqConverterPopup" />
<FreqConverterTestPopup :refresh-table="proTable?.getTableList" ref="freqConverterTestPopup" />
<FreqConverterResultPopup ref="freqConverterResultPopup" />
</template>
<script setup lang='tsx' name='freqConverter'>
<script setup lang="tsx" name="freqConverter">
import {type FreqConverter} from '@/api/device/interface/freqConverter'
import {useHandleData} from '@/hooks/useHandleData'
import ProTable from '@/components/ProTable/index.vue'
import {type ColumnProps, type ProTableInstance} from '@/components/ProTable/interface'
import {CirclePlus, Coin, Delete, EditPen, Stopwatch} from '@element-plus/icons-vue'
import {deleteFreqConverter, getFreqConverterList} from '@/api/device/freqConverter/index.ts'
import {deleteFreqConverter, getFreqConverterList} from '@/api/device/freqConverter'
import {reactive, ref} from 'vue'
import FreqConverterPopup from '@/views/machine/freqConverter/components/freqConverterPopup.vue'
import FreqConverterTestPopup from '@/views/machine/freqConverter/components/freqConverterTestPopup.vue'
import FreqConverterResultPopup from '@/views/machine/freqConverter/components/freqConverterResultPopup.vue'
// ProTable 实例
const proTable = ref<ProTableInstance>()
const freqConverterPopup = ref()
const freqConverterPopup = ref<InstanceType<typeof FreqConverterPopup>>()
const freqConverterTestPopup = ref<InstanceType<typeof FreqConverterTestPopup>>()
const freqConverterResultPopup = ref<InstanceType<typeof FreqConverterResultPopup>>()
const getTableList = async (params: any) => {
let newParams = JSON.parse(JSON.stringify(params))
const newParams = JSON.parse(JSON.stringify(params))
return getFreqConverterList(newParams)
}
// 表格配置项
const columns = reactive<ColumnProps<FreqConverter.ResFreqConverter>[]>([
{type: 'selection', fixed: 'left', width: 70},
{type: 'index', fixed: 'left', width: 70, label: '序号'},
@@ -68,49 +108,41 @@ const columns = reactive<ColumnProps<FreqConverter.ResFreqConverter>[]>([
width: 200,
render: scope => {
if (scope.row.createTime) {
const date = new Date(scope.row.createTime);
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 date = new Date(scope.row.createTime)
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}`
}
return '';
return ''
}
},
{prop: 'operation', label: '操作', fixed: 'right', width: 280},
{prop: 'operation', label: '操作', fixed: 'right', width: 280}
])
// 打开 drawer(新增、编辑)
const openDialog = (titleType: string, row: Partial<FreqConverter.ResFreqConverter> = {}) => {
freqConverterPopup.value?.open(titleType, row)
}
// 批量删除设备类型
const batchDelete = async (id: string[]) => {
await useHandleData(deleteFreqConverter, id, '删除所选变频器')
proTable.value?.clearSelection()
proTable.value?.getTableList()
}
// 删除设备类型
const handleDelete = async (params: FreqConverter.ResFreqConverter) => {
await useHandleData(deleteFreqConverter, [params.id], `删除【${params.name}】变频器`)
proTable.value?.getTableList()
}
const handleTest = async (params: FreqConverter.ResFreqConverter) => {
await useHandleData(deleteFreqConverter, [params.id], `删除【${params.name}】变频器`)
proTable.value?.getTableList()
freqConverterTestPopup.value?.open(params)
}
const handleResult = async (params: FreqConverter.ResFreqConverter) => {
await useHandleData(deleteFreqConverter, [params.id], `删除【${params.name}】变频器`)
proTable.value?.getTableList()
freqConverterResultPopup.value?.open(params)
}
</script>