变频器功能页面
This commit is contained in:
@@ -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 被检设备管理模块
|
||||
*/
|
||||
|
||||
@@ -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 || ''}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {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>
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user