变频器功能页面
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
import type {Device} from '@/api/device/interface/device'
|
import type {Device} from '@/api/device/interface/device'
|
||||||
import http from '@/api'
|
import http from '@/api'
|
||||||
|
|
||||||
|
export const getPqDevListAll = () => {
|
||||||
|
return http.get<Device.ResPqDev[]>(`/pqDev/listAll`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name 被检设备管理模块
|
* @name 被检设备管理模块
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,5 +25,28 @@ export const deleteFreqConverter = (params: string[]) => {
|
|||||||
return http.post(`/freqConverter/delete`, params)
|
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 || ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export namespace FreqConverter {
|
|||||||
timeoutMs: number; //超时时间(毫秒)
|
timeoutMs: number; //超时时间(毫秒)
|
||||||
suffix?: number; //数据表后缀
|
suffix?: number; //数据表后缀
|
||||||
state?: number;
|
state?: number;
|
||||||
|
testStatus?: number; //测试状态 0:未测试 1:测试完成
|
||||||
createBy?: string | null; //创建用户
|
createBy?: string | null; //创建用户
|
||||||
createTime?: string | null; //创建时间
|
createTime?: string | null; //创建时间
|
||||||
updateBy?: string | null; //更新用户
|
updateBy?: string | null; //更新用户
|
||||||
@@ -37,4 +38,14 @@ export namespace FreqConverter {
|
|||||||
export interface ResFreqConverterPage extends ResPage<ResFreqConverter> {
|
export interface ResFreqConverterPage extends ResPage<ResFreqConverter> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReqFreqConverterSCurveParams {
|
||||||
|
converterId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResTolerantPoint {
|
||||||
|
durationMs?: number | null;
|
||||||
|
residualVoltage?: number | null;
|
||||||
|
tolerant?: number | null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -67,7 +67,7 @@ import {ElMessage, type FormItemRule} from 'element-plus'
|
|||||||
import {computed, ref} from 'vue'
|
import {computed, ref} from 'vue'
|
||||||
import {type FreqConverter} from '@/api/device/interface/freqConverter'
|
import {type FreqConverter} from '@/api/device/interface/freqConverter'
|
||||||
import {dialogMiddle} from '@/utils/elementBind'
|
import {dialogMiddle} from '@/utils/elementBind'
|
||||||
import {addFreqConverter, updateFreqConverter} from '@/api/device/freqConverter/index.ts'
|
import {addFreqConverter, updateFreqConverter} from '@/api/device/freqConverter'
|
||||||
|
|
||||||
// 定义弹出组件元信息
|
// 定义弹出组件元信息
|
||||||
const dialogFormRef = ref()
|
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(() => {
|
let dialogTitle = computed(() => {
|
||||||
return titleType.value === 'add' ? '新增变频器' : '编辑变频器'
|
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()
|
dialogFormRef.value?.resetFields()
|
||||||
titleType.value = sign
|
titleType.value = sign
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
|
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
formContent.value = {...data}
|
formContent.value = {...getDefaultFormContent(), ...data}
|
||||||
} else {
|
} else {
|
||||||
resetFormContent()
|
resetFormContent()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -1,56 +1,96 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class='table-box' ref='popupBaseView'>
|
<div class="table-box" ref="popupBaseView">
|
||||||
<ProTable
|
<ProTable
|
||||||
ref='proTable'
|
ref="proTable"
|
||||||
:columns='columns'
|
:columns="columns"
|
||||||
:request-api='getTableList'
|
:request-api="getTableList"
|
||||||
>
|
>
|
||||||
<!-- :requestApi="getRoleList" -->
|
<template #tableHeader="scope">
|
||||||
<!-- 表格 header 按钮 -->
|
<el-button v-auth.freqConverter="'add'" type="primary" :icon="CirclePlus" @click="openDialog('add')">
|
||||||
<template #tableHeader='scope'>
|
新增
|
||||||
<el-button v-auth.freqConverter="'add'" type='primary' :icon='CirclePlus' @click="openDialog('add')">新增</el-button>
|
</el-button>
|
||||||
<el-button v-auth.freqConverter="'delete'" type='danger' :icon='Delete' plain :disabled='!scope.isSelected'
|
<el-button
|
||||||
@click='batchDelete(scope.selectedListIds)'>
|
v-auth.freqConverter="'delete'"
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
plain
|
||||||
|
:disabled="!scope.isSelected"
|
||||||
|
@click="batchDelete(scope.selectedListIds)"
|
||||||
|
>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<!-- 表格操作 -->
|
|
||||||
<template #operation='scope'>
|
<template #operation="scope">
|
||||||
<el-button v-auth.freqConverter="'edit'" type='primary' link :icon='EditPen' :model-value='false' @click="openDialog('edit', scope.row)">编辑
|
<el-button
|
||||||
|
v-auth.freqConverter="'edit'"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:icon="EditPen"
|
||||||
|
:model-value="false"
|
||||||
|
@click="openDialog('edit', scope.row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
</el-button>
|
</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>
|
||||||
<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>
|
||||||
<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>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</ProTable>
|
</ProTable>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang='tsx' name='freqConverter'>
|
<script setup lang="tsx" name="freqConverter">
|
||||||
import {type FreqConverter} from '@/api/device/interface/freqConverter'
|
import {type FreqConverter} from '@/api/device/interface/freqConverter'
|
||||||
import {useHandleData} from '@/hooks/useHandleData'
|
import {useHandleData} from '@/hooks/useHandleData'
|
||||||
import ProTable from '@/components/ProTable/index.vue'
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
import {type ColumnProps, type ProTableInstance} from '@/components/ProTable/interface'
|
import {type ColumnProps, type ProTableInstance} from '@/components/ProTable/interface'
|
||||||
import {CirclePlus, Coin, Delete, EditPen, Stopwatch} from '@element-plus/icons-vue'
|
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 {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 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) => {
|
const getTableList = async (params: any) => {
|
||||||
let newParams = JSON.parse(JSON.stringify(params))
|
const newParams = JSON.parse(JSON.stringify(params))
|
||||||
return getFreqConverterList(newParams)
|
return getFreqConverterList(newParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 表格配置项
|
|
||||||
const columns = reactive<ColumnProps<FreqConverter.ResFreqConverter>[]>([
|
const columns = reactive<ColumnProps<FreqConverter.ResFreqConverter>[]>([
|
||||||
{type: 'selection', fixed: 'left', width: 70},
|
{type: 'selection', fixed: 'left', width: 70},
|
||||||
{type: 'index', fixed: 'left', width: 70, label: '序号'},
|
{type: 'index', fixed: 'left', width: 70, label: '序号'},
|
||||||
@@ -68,49 +108,41 @@ const columns = reactive<ColumnProps<FreqConverter.ResFreqConverter>[]>([
|
|||||||
width: 200,
|
width: 200,
|
||||||
render: scope => {
|
render: scope => {
|
||||||
if (scope.row.createTime) {
|
if (scope.row.createTime) {
|
||||||
const date = new Date(scope.row.createTime);
|
const date = new Date(scope.row.createTime)
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear()
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
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> = {}) => {
|
const openDialog = (titleType: string, row: Partial<FreqConverter.ResFreqConverter> = {}) => {
|
||||||
freqConverterPopup.value?.open(titleType, row)
|
freqConverterPopup.value?.open(titleType, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 批量删除设备类型
|
|
||||||
const batchDelete = async (id: string[]) => {
|
const batchDelete = async (id: string[]) => {
|
||||||
await useHandleData(deleteFreqConverter, id, '删除所选变频器')
|
await useHandleData(deleteFreqConverter, id, '删除所选变频器')
|
||||||
proTable.value?.clearSelection()
|
proTable.value?.clearSelection()
|
||||||
proTable.value?.getTableList()
|
proTable.value?.getTableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除设备类型
|
|
||||||
const handleDelete = async (params: FreqConverter.ResFreqConverter) => {
|
const handleDelete = async (params: FreqConverter.ResFreqConverter) => {
|
||||||
await useHandleData(deleteFreqConverter, [params.id], `删除【${params.name}】变频器`)
|
await useHandleData(deleteFreqConverter, [params.id], `删除【${params.name}】变频器`)
|
||||||
proTable.value?.getTableList()
|
proTable.value?.getTableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTest = async (params: FreqConverter.ResFreqConverter) => {
|
const handleTest = async (params: FreqConverter.ResFreqConverter) => {
|
||||||
await useHandleData(deleteFreqConverter, [params.id], `删除【${params.name}】变频器`)
|
freqConverterTestPopup.value?.open(params)
|
||||||
proTable.value?.getTableList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResult = async (params: FreqConverter.ResFreqConverter) => {
|
const handleResult = async (params: FreqConverter.ResFreqConverter) => {
|
||||||
await useHandleData(deleteFreqConverter, [params.id], `删除【${params.name}】变频器`)
|
freqConverterResultPopup.value?.open(params)
|
||||||
proTable.value?.getTableList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
Reference in New Issue
Block a user