Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4189eea278 | ||
|
|
d3fef49af2 | ||
|
|
12d40a5c6e | ||
|
|
6748490c3f | ||
|
|
2c19fc43de | ||
|
|
2ba5ebaddb | ||
|
|
448687115c | ||
|
|
0d4dc2d2bf | ||
|
|
121829a4bd | ||
|
|
3f6952612d | ||
|
|
5ca5d73f98 | ||
|
|
71d80e67f1 | ||
|
|
0e0969c50f | ||
|
|
8ab1a35f3b | ||
|
|
8744dfb0d8 | ||
|
|
b826f505ac | ||
|
|
465ad81069 | ||
|
|
c73bf05d41 | ||
|
|
7fd3b6fdff | ||
| 4bfab6518e | |||
|
|
4655259153 | ||
|
|
cdb23726f8 | ||
|
|
68a1c9d28d | ||
|
|
30e815c027 | ||
|
|
ce10f91b5b |
@@ -115,4 +115,10 @@ activate:
|
|||||||
private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo="
|
private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo="
|
||||||
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB"
|
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB"
|
||||||
|
|
||||||
|
freq-converter:
|
||||||
|
schedule-period: 200 #定时器运行间隔
|
||||||
|
tolerant: 1 #耐受状态
|
||||||
|
dt: 200 #延迟时间ms
|
||||||
|
direction: 0 #0为横向1为纵向
|
||||||
|
allow-error-duration: 6 #暂态持续时间允许最大误差ms
|
||||||
|
allow-error-residual-voltage: 2.0 #暂态幅值允许最多误差%
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo39
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo39
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo40
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo40
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo41
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo41
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
|||||||
74476
|
42428
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/extraResources/mysql/data/binlog.000034
Normal file
BIN
build/extraResources/mysql/data/binlog.000034
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/binlog.000035
Normal file
BIN
build/extraResources/mysql/data/binlog.000035
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/binlog.000036
Normal file
BIN
build/extraResources/mysql/data/binlog.000036
Normal file
Binary file not shown.
@@ -1,9 +1,3 @@
|
|||||||
.\binlog.000023
|
.\binlog.000034
|
||||||
.\binlog.000024
|
.\binlog.000035
|
||||||
.\binlog.000025
|
.\binlog.000036
|
||||||
.\binlog.000026
|
|
||||||
.\binlog.000027
|
|
||||||
.\binlog.000028
|
|
||||||
.\binlog.000029
|
|
||||||
.\binlog.000030
|
|
||||||
.\binlog.000031
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -42,7 +42,7 @@ function createTray() {
|
|||||||
tray = new Tray(iconPath);
|
tray = new Tray(iconPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
tray.setToolTip('NPQS-9100自动检测平台');
|
tray.setToolTip('变频器暂降耐受实验平台');
|
||||||
console.log('[Tray] Tray created successfully');
|
console.log('[Tray] Tray created successfully');
|
||||||
|
|
||||||
// 创建托盘菜单
|
// 创建托盘菜单
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# title
|
# title
|
||||||
VITE_GLOB_APP_TITLE=NPQS-9100自动检测平台
|
VITE_GLOB_APP_TITLE=变频器暂降耐受实验平台
|
||||||
|
|
||||||
# 本地运行端口号
|
# 本地运行端口号
|
||||||
VITE_PORT=18091
|
VITE_PORT=18091
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.1",
|
"version": "2.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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 被检设备管理模块
|
||||||
*/
|
*/
|
||||||
|
|||||||
52
frontend/src/api/device/freqConverter/index.ts
Normal file
52
frontend/src/api/device/freqConverter/index.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type {FreqConverter} from '@/api/device/interface/freqConverter'
|
||||||
|
import http from '@/api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name 变频器管理模块
|
||||||
|
*/
|
||||||
|
|
||||||
|
//获取设备类型
|
||||||
|
export const getFreqConverterList = (params: FreqConverter.ReqFreqConverterParams) => {
|
||||||
|
return http.post(`/freqConverter/list`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
//添加设备类型
|
||||||
|
export const addFreqConverter = (params: FreqConverter.ResFreqConverter) => {
|
||||||
|
return http.post(`/freqConverter/add`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
//编辑设备类型
|
||||||
|
export const updateFreqConverter = (params: FreqConverter.ResFreqConverter) => {
|
||||||
|
return http.post(`/freqConverter/update`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
//删除设备类型
|
||||||
|
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 || ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
51
frontend/src/api/device/interface/freqConverter.ts
Normal file
51
frontend/src/api/device/interface/freqConverter.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type {ReqPage, ResPage} from '@/api/interface'
|
||||||
|
|
||||||
|
// 变频器模块
|
||||||
|
export namespace FreqConverter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变频器数据表格分页查询参数
|
||||||
|
*/
|
||||||
|
export interface ReqFreqConverterParams extends ReqPage {
|
||||||
|
name?: string; // 名称
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变频器新增、修改、根据id查询返回的对象
|
||||||
|
*/
|
||||||
|
export interface ResFreqConverter {
|
||||||
|
id?: string; //变频器ID
|
||||||
|
name: string;//变频器名称
|
||||||
|
portName: string; //串口名称
|
||||||
|
slaveAddress: number; //从机地址
|
||||||
|
baudRate: number; //波特率
|
||||||
|
parity: string; //奇偶校验类型
|
||||||
|
dataBits: number; //数据位
|
||||||
|
stopBits: number; //停止位
|
||||||
|
timeoutMs: number; //超时时间(毫秒)
|
||||||
|
suffix?: number; //数据表后缀
|
||||||
|
state?: number;
|
||||||
|
testStatus?: number; //测试状态 0:未测试 1:测试完成
|
||||||
|
createBy?: string | null; //创建用户
|
||||||
|
createTime?: string | null; //创建时间
|
||||||
|
updateBy?: string | null; //更新用户
|
||||||
|
updateTime?: string | null; //更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变频器表格查询分页返回的对象;
|
||||||
|
*/
|
||||||
|
export interface ResFreqConverterPage extends ResPage<ResFreqConverter> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReqFreqConverterSCurveParams {
|
||||||
|
converterId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResTolerantPoint {
|
||||||
|
durationMs?: number | null;
|
||||||
|
residualVoltage?: number | null;
|
||||||
|
tolerant?: number | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,6 +130,7 @@ const initChart = () => {
|
|||||||
chart.resize()
|
chart.resize()
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
const getChartInstance = () => chart
|
||||||
const handlerBar = (options: any) => {
|
const handlerBar = (options: any) => {
|
||||||
if (Array.isArray(options.series)) {
|
if (Array.isArray(options.series)) {
|
||||||
options.series.forEach((item: any) => {
|
options.series.forEach((item: any) => {
|
||||||
@@ -253,7 +254,7 @@ onMounted(() => {
|
|||||||
initChart()
|
initChart()
|
||||||
resizeObserver.observe(chartRef.value!)
|
resizeObserver.observe(chartRef.value!)
|
||||||
})
|
})
|
||||||
defineExpose({ initChart })
|
defineExpose({ initChart, getChartInstance })
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
resizeObserver.unobserve(chartRef.value!)
|
resizeObserver.unobserve(chartRef.value!)
|
||||||
chart?.dispose()
|
chart?.dispose()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ? 全局默认配置项
|
// ? 全局默认配置项
|
||||||
|
|
||||||
// 首页地址(默认)
|
// 首页地址(默认)
|
||||||
export const HOME_URL: string = "/home/index";
|
export const HOME_URL: string = "/machine/freqConverter";
|
||||||
|
|
||||||
// export const HOME_URL: string = "/machine/controlSource";
|
// export const HOME_URL: string = "/machine/controlSource";
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { HOME_URL } from '@/config'
|
||||||
import { useAuthStore } from '@/stores/modules/auth'
|
import { useAuthStore } from '@/stores/modules/auth'
|
||||||
import { useModeStore } from '@/stores/modules/mode' // 引入模式 store
|
import { useModeStore } from '@/stores/modules/mode' // 引入模式 store
|
||||||
import { useTabsStore } from '@/stores/modules/tabs'
|
import { useTabsStore } from '@/stores/modules/tabs'
|
||||||
@@ -74,8 +75,8 @@ const handelOpen = async (item: string, key: string) => {
|
|||||||
await initDynamicRouter()
|
await initDynamicRouter()
|
||||||
|
|
||||||
// 只有当目标路径与当前路径不同时才跳转
|
// 只有当目标路径与当前路径不同时才跳转
|
||||||
if (router.currentRoute.value.path !== '/home/index') {
|
if (router.currentRoute.value.path !== HOME_URL) {
|
||||||
await router.push({ path: '/home/index' })
|
await router.push({ path: HOME_URL })
|
||||||
} else {
|
} else {
|
||||||
// 如果已在目标页面,手动触发组件更新
|
// 如果已在目标页面,手动触发组件更新
|
||||||
window.location.reload() // 或者采用其他方式刷新数据
|
window.location.reload() // 或者采用其他方式刷新数据
|
||||||
|
|||||||
@@ -22,14 +22,6 @@
|
|||||||
<el-icon><Edit /></el-icon>
|
<el-icon><Edit /></el-icon>
|
||||||
{{ t('header.changePassword') }}
|
{{ t('header.changePassword') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click="changeMode" v-if="authStore.showMenuFlag">
|
|
||||||
<el-icon><Switch /></el-icon>
|
|
||||||
{{ t('header.changeMode') }}
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item @click="openDialog('versionRegisterRef')">
|
|
||||||
<el-icon><SetUp /></el-icon>
|
|
||||||
{{ t('header.versionRegister') }}
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown trigger="hover" placement="left-start" v-if="userStore.userInfo.loginName == 'root'">
|
<el-dropdown trigger="hover" placement="left-start" v-if="userStore.userInfo.loginName == 'root'">
|
||||||
<div class="custom-dropdown-trigger">
|
<div class="custom-dropdown-trigger">
|
||||||
<el-icon><Tools /></el-icon>
|
<el-icon><Tools /></el-icon>
|
||||||
@@ -62,8 +54,6 @@
|
|||||||
<InfoDialog ref="infoRef"></InfoDialog>
|
<InfoDialog ref="infoRef"></InfoDialog>
|
||||||
<!-- passwordDialog -->
|
<!-- passwordDialog -->
|
||||||
<PasswordDialog ref="passwordRef"></PasswordDialog>
|
<PasswordDialog ref="passwordRef"></PasswordDialog>
|
||||||
<!-- versionRegisterDialog -->
|
|
||||||
<VersionDialog ref="versionRegisterRef"></VersionDialog>
|
|
||||||
<!-- ThemeDialog -->
|
<!-- ThemeDialog -->
|
||||||
<ThemeDialog ref="themeRef"></ThemeDialog>
|
<ThemeDialog ref="themeRef"></ThemeDialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -77,9 +67,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import InfoDialog from './InfoDialog.vue'
|
import InfoDialog from './InfoDialog.vue'
|
||||||
import PasswordDialog from './PasswordDialog.vue'
|
import PasswordDialog from './PasswordDialog.vue'
|
||||||
import ThemeDialog from './ThemeDialog.vue'
|
import ThemeDialog from './ThemeDialog.vue'
|
||||||
import VersionDialog from '@/views/system/versionRegister/index.vue'
|
import { Avatar, Sunny, Tools } from '@element-plus/icons-vue'
|
||||||
import { Avatar, Sunny, Switch, Tools } from '@element-plus/icons-vue'
|
|
||||||
import { useAuthStore } from '@/stores/modules/auth'
|
|
||||||
import { useDictStore } from '@/stores/modules/dict'
|
import { useDictStore } from '@/stores/modules/dict'
|
||||||
import { useAppSceneStore } from '@/stores/modules/mode'
|
import { useAppSceneStore } from '@/stores/modules/mode'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@@ -90,7 +78,6 @@ const dictStore = useDictStore()
|
|||||||
const username = computed(() => userStore.userInfo.name)
|
const username = computed(() => userStore.userInfo.name)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
// 初始化 i18n
|
// 初始化 i18n
|
||||||
const { t } = useI18n() // 使用 t 方法替代 $t
|
const { t } = useI18n() // 使用 t 方法替代 $t
|
||||||
@@ -111,12 +98,10 @@ const logout = () => {
|
|||||||
// 打开修改密码和个人信息弹窗
|
// 打开修改密码和个人信息弹窗
|
||||||
const infoRef = ref<InstanceType<typeof InfoDialog> | null>(null)
|
const infoRef = ref<InstanceType<typeof InfoDialog> | null>(null)
|
||||||
const passwordRef = ref<InstanceType<typeof PasswordDialog> | null>(null)
|
const passwordRef = ref<InstanceType<typeof PasswordDialog> | null>(null)
|
||||||
const versionRegisterRef = ref<InstanceType<typeof VersionDialog> | null>(null)
|
|
||||||
const themeRef = ref<InstanceType<typeof ThemeDialog> | null>(null)
|
const themeRef = ref<InstanceType<typeof ThemeDialog> | null>(null)
|
||||||
const openDialog = (ref: string) => {
|
const openDialog = (ref: string) => {
|
||||||
if (ref == 'infoRef') infoRef.value?.openDialog()
|
if (ref == 'infoRef') infoRef.value?.openDialog()
|
||||||
if (ref == 'passwordRef') passwordRef.value?.openDialog()
|
if (ref == 'passwordRef') passwordRef.value?.openDialog()
|
||||||
if (ref == 'versionRegisterRef') versionRegisterRef.value?.openDialog()
|
|
||||||
if (ref == 'themeRef') themeRef.value?.openDialog()
|
if (ref == 'themeRef') themeRef.value?.openDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,10 +115,6 @@ const changeScene = async (value: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//模式切换
|
//模式切换
|
||||||
const changeMode = async () => {
|
|
||||||
authStore.changeModel()
|
|
||||||
await router.push('/home/index')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useGlobalStore } from '@/stores/modules/global'
|
import { useGlobalStore } from '@/stores/modules/global'
|
||||||
import { useTabsStore } from '@/stores/modules/tabs'
|
import { useTabsStore } from '@/stores/modules/tabs'
|
||||||
import { useAuthStore } from '@/stores/modules/auth'
|
import { useAuthStore } from '@/stores/modules/auth'
|
||||||
|
import { HOME_URL } from '@/config'
|
||||||
import { TabPaneName, TabsPaneContext } from 'element-plus'
|
import { TabPaneName, TabsPaneContext } from 'element-plus'
|
||||||
import MoreButton from './components/MoreButton.vue'
|
import MoreButton from './components/MoreButton.vue'
|
||||||
|
|
||||||
@@ -73,13 +74,13 @@ watch(
|
|||||||
// 初始化需要固定的 tabs
|
// 初始化需要固定的 tabs
|
||||||
const initTabs = () => {
|
const initTabs = () => {
|
||||||
authStore.flatMenuListGet.forEach(item => {
|
authStore.flatMenuListGet.forEach(item => {
|
||||||
if (item.meta.isAffix && !item.meta.isHide && !item.meta.isFull) {
|
if (item.path === HOME_URL && !item.meta.isHide && !item.meta.isFull) {
|
||||||
const tabsParams = {
|
const tabsParams = {
|
||||||
icon: item.meta.icon,
|
icon: item.meta.icon,
|
||||||
title: item.meta.title,
|
title: item.meta.title,
|
||||||
path: item.path,
|
path: item.path,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
close: !item.meta.isAffix,
|
close: false,
|
||||||
isKeepAlive: item.meta.isKeepAlive,
|
isKeepAlive: item.meta.isKeepAlive,
|
||||||
unshift: true
|
unshift: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type AuthState } from '@/stores/interface'
|
|||||||
import { getAuthButtonListApi, getAuthMenuListApi } from '@/api/user/login'
|
import { getAuthButtonListApi, getAuthMenuListApi } from '@/api/user/login'
|
||||||
import { getAllBreadcrumbList, getFlatMenuList, getShowMenuList } from '@/utils'
|
import { getAllBreadcrumbList, getFlatMenuList, getShowMenuList } from '@/utils'
|
||||||
import { AUTH_STORE_KEY } from '@/stores/constant'
|
import { AUTH_STORE_KEY } from '@/stores/constant'
|
||||||
|
import { HOME_URL } from '@/config'
|
||||||
import { useModeStore } from '@/stores/modules/mode'
|
import { useModeStore } from '@/stores/modules/mode'
|
||||||
import { getLicense } from '@/api/activate'
|
import { getLicense } from '@/api/activate'
|
||||||
import type { Activate } from '@/api/activate/interface'
|
import type { Activate } from '@/api/activate/interface'
|
||||||
@@ -52,7 +53,7 @@ export const useAuthStore = defineStore(AUTH_STORE_KEY, {
|
|||||||
? filterMenuByExcludedNames(menuData, ['testSource', 'testScript', 'controlSource'])
|
? filterMenuByExcludedNames(menuData, ['testSource', 'testScript', 'controlSource'])
|
||||||
: filterMenuByExcludedNames(menuData, ['standardDevice'])
|
: filterMenuByExcludedNames(menuData, ['standardDevice'])
|
||||||
|
|
||||||
this.authMenuList = filteredMenu
|
this.authMenuList = normalizeHomeAffix(filteredMenu)
|
||||||
},
|
},
|
||||||
// Set RouteName
|
// Set RouteName
|
||||||
async setRouteName(name: string) {
|
async setRouteName(name: string) {
|
||||||
@@ -112,3 +113,22 @@ function filterMenuByExcludedNames(menuList: any[], excludedNames: string[]): an
|
|||||||
return !excludedNames.includes(menu.name)
|
return !excludedNames.includes(menu.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHomeAffix(menuList: any[]): any[] {
|
||||||
|
return menuList.map(menu => {
|
||||||
|
const nextMenu = { ...menu }
|
||||||
|
|
||||||
|
if (nextMenu.meta) {
|
||||||
|
nextMenu.meta = {
|
||||||
|
...nextMenu.meta,
|
||||||
|
isAffix: nextMenu.path === HOME_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(nextMenu.children) && nextMenu.children.length > 0) {
|
||||||
|
nextMenu.children = normalizeHomeAffix(nextMenu.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextMenu
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// src/stores/modules/mode.ts
|
// src/stores/modules/mode.ts
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const DEFAULT_MODE = '模拟式'
|
||||||
|
|
||||||
export const useModeStore = defineStore('mode', {
|
export const useModeStore = defineStore('mode', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
currentMode: localStorage.getItem('currentMode') || ('' as string)
|
currentMode: localStorage.getItem('currentMode') || DEFAULT_MODE
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setCurrentMode(modeName: string) {
|
setCurrentMode(modeName: string) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import piniaPersistConfig from '@/stores/helper/persist'
|
|||||||
import { USER_STORE_KEY } from '@/stores/constant'
|
import { USER_STORE_KEY } from '@/stores/constant'
|
||||||
import { logoutApi } from '@/api/user/login'
|
import { logoutApi } from '@/api/user/login'
|
||||||
import { useAuthStore } from '@/stores/modules/auth'
|
import { useAuthStore } from '@/stores/modules/auth'
|
||||||
import { useAppSceneStore, useModeStore } from '@/stores/modules/mode'
|
import { DEFAULT_MODE, useAppSceneStore, useModeStore } from '@/stores/modules/mode'
|
||||||
import { useDictStore } from '@/stores/modules/dict'
|
import { useDictStore } from '@/stores/modules/dict'
|
||||||
|
|
||||||
export const useUserStore = defineStore(USER_STORE_KEY, {
|
export const useUserStore = defineStore(USER_STORE_KEY, {
|
||||||
@@ -48,7 +48,7 @@ export const useUserStore = defineStore(USER_STORE_KEY, {
|
|||||||
this.setUserInfo({ id: '', name: '', loginName: '' })
|
this.setUserInfo({ id: '', name: '', loginName: '' })
|
||||||
this.setIsRefreshToken(false)
|
this.setIsRefreshToken(false)
|
||||||
dictStore.setDictData([])
|
dictStore.setDictData([])
|
||||||
modeStore.setCurrentMode('')
|
modeStore.setCurrentMode(DEFAULT_MODE)
|
||||||
appSceneStore.setCurrentMode('')
|
appSceneStore.setCurrentMode('')
|
||||||
await authStore.resetAuthStore()
|
await authStore.resetAuthStore()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<template #operation='scope'>
|
<template #operation='scope'>
|
||||||
<el-button v-auth.role="'edit'" type='primary' link :icon='EditPen' @click="openDrawer('edit', scope.row)" :disabled="scope.row.code == 'root'">编辑</el-button>
|
<el-button v-auth.role="'edit'" type='primary' link :icon='EditPen' @click="openDrawer('edit', scope.row)" :disabled="scope.row.code == 'root'">编辑</el-button>
|
||||||
<el-button v-auth.role="'delete'" type='primary' link :icon='Delete' @click='deleteAccount(scope.row)' :disabled="scope.row.code == 'root'">删除</el-button>
|
<el-button v-auth.role="'delete'" type='primary' link :icon='Delete' @click='deleteAccount(scope.row)' :disabled="scope.row.code == 'root'">删除</el-button>
|
||||||
<el-button v-auth.role="'SetPermissions'" type='primary' link :icon='Share' @click="openDrawer('设置权限', scope.row)" :disabled="scope.row.code == 'root'">设置权限</el-button>
|
<el-button v-auth.role="'SetPermissions'" type='primary' link :icon='Share' @click="openDrawer('设置权限', scope.row)">设置权限</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</ProTable>
|
</ProTable>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const login = (formEl: FormInstance | undefined) => {
|
|||||||
await tabsStore.setTabs([])
|
await tabsStore.setTabs([])
|
||||||
await keepAliveStore.setKeepAliveName([])
|
await keepAliveStore.setKeepAliveName([])
|
||||||
// 登录默认不显示菜单和导航栏
|
// 登录默认不显示菜单和导航栏
|
||||||
await authStore.resetAuthStore()
|
await authStore.setShowMenu()
|
||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
await router.push(HOME_URL)
|
await router.push(HOME_URL)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ const dictStore = useDictStore()
|
|||||||
const activeName = ref('')
|
const activeName = ref('')
|
||||||
const childActiveName = ref('')
|
const childActiveName = ref('')
|
||||||
const childActiveIndex = ref(0)
|
const childActiveIndex = ref(0)
|
||||||
const firstName = 'first'
|
const firstName = ref('first')
|
||||||
const viewRowRef = ref()
|
const viewRowRef = ref()
|
||||||
const communicationList = ref<[]>([])
|
const communicationList = ref<[]>([])
|
||||||
const testProjectPopupRef = ref()
|
const testProjectPopupRef = ref()
|
||||||
|
|||||||
@@ -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,820 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" plain :icon="Download" :disabled="!hasChartData" @click="downloadChartImage">
|
||||||
|
下载图片
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" plain :icon="Document" :disabled="!hasChartData" @click="exportChartData">
|
||||||
|
导出数据
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="!props.autoDrawCurve"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
class="draw-curve-button"
|
||||||
|
@click="drawCharacteristicCurve"
|
||||||
|
>
|
||||||
|
绘制特性曲线
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<MyEchart ref="chartRef" :options="chartOptions"/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {computed, nextTick, ref, watch} from 'vue'
|
||||||
|
import {ElMessage} from 'element-plus'
|
||||||
|
import {Document, Download} from '@element-plus/icons-vue'
|
||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
import MyEchart from '@/components/echarts/line/index.vue'
|
||||||
|
|
||||||
|
type ChartPointStatus = 'pass' | 'fail'
|
||||||
|
|
||||||
|
interface ChartPoint {
|
||||||
|
duration: number
|
||||||
|
residualVoltage: number
|
||||||
|
status: ChartPointStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CharacteristicCurvePoint {
|
||||||
|
duration: number
|
||||||
|
residualVoltage: number
|
||||||
|
time: string | null
|
||||||
|
timeMs: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NormalizedTolerantPoint {
|
||||||
|
duration: number
|
||||||
|
residualVoltage: number
|
||||||
|
tolerant: number | null
|
||||||
|
status: ChartPointStatus
|
||||||
|
time: string | null
|
||||||
|
timeMs: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedMapping?: Record<string, any> | null
|
||||||
|
webMsgSend?: any
|
||||||
|
resultData?: any
|
||||||
|
autoDrawCurve?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = {
|
||||||
|
pass: '#4e73df',
|
||||||
|
fail: '#4b4b4b'
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHARACTERISTIC_POINT_COLOR = '#ff4d4f'
|
||||||
|
|
||||||
|
const chartPoints = ref<ChartPoint[]>([])
|
||||||
|
const characteristicCurveData = ref<CharacteristicCurvePoint[]>([])
|
||||||
|
const characteristicCurveVisible = ref(false)
|
||||||
|
const chartRef = ref<any>(null)
|
||||||
|
|
||||||
|
const selectedMappingText = computed(() => {
|
||||||
|
if (!props.selectedMapping) {
|
||||||
|
return '未选择变频器'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `变频器:${props.selectedMapping.freqConverterName || '-'}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const xAxisMin = computed(() => {
|
||||||
|
return 0.001
|
||||||
|
// if (!positiveDurations.value.length) {
|
||||||
|
// return 0.001
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const minValue = Math.min(...positiveDurations.value)
|
||||||
|
// return Math.min(0.001, Number(minValue.toFixed(3)))
|
||||||
|
})
|
||||||
|
|
||||||
|
const xAxisMax = computed(() => {
|
||||||
|
return 1000
|
||||||
|
// if (!positiveDurations.value.length) {
|
||||||
|
// return 60
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const maxValue = Math.max(...positiveDurations.value)
|
||||||
|
// return Math.max(Number((maxValue * 1.05).toFixed(3)), 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortedChartPoints = computed(() => {
|
||||||
|
return [...chartPoints.value].sort((a, b) => {
|
||||||
|
if (a.duration !== b.duration) {
|
||||||
|
return a.duration - b.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.residualVoltage - b.residualVoltage
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortedCharacteristicCurveData = computed(() => {
|
||||||
|
return [...characteristicCurveData.value].sort((a, b) => {
|
||||||
|
// 保留1位小数
|
||||||
|
let aResidualVoltage = Math.floor(a.residualVoltage)
|
||||||
|
let bResidualVoltage = Math.floor(b.residualVoltage)
|
||||||
|
if (aResidualVoltage != bResidualVoltage) {
|
||||||
|
return a.residualVoltage - b.residualVoltage;
|
||||||
|
} else {
|
||||||
|
let aDuration = a.duration * 1000 - a.duration * 1000 % 10
|
||||||
|
let bDuration = b.duration * 1000 - b.duration * 1000 % 10
|
||||||
|
if (aDuration != bDuration) {
|
||||||
|
return a.duration - b.duration
|
||||||
|
} else if (a.timeMs !== null && b.timeMs !== null && a.timeMs !== b.timeMs) {
|
||||||
|
return a.timeMs - b.timeMs
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (a.timeMs !== null && b.timeMs !== null && a.timeMs !== b.timeMs) {
|
||||||
|
// return a.timeMs - b.timeMs
|
||||||
|
// } else {
|
||||||
|
// return 0
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return a.residualVoltage - b.residualVoltage
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const solidCharacteristicCurveSeriesData = computed(() => {
|
||||||
|
if (!characteristicCurveVisible.value) {
|
||||||
|
return [] as Array<[number, number, string]>
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedCharacteristicCurveData.value.map(item => [item.duration, item.residualVoltage, '特性曲线'])
|
||||||
|
})
|
||||||
|
|
||||||
|
const characteristicCurvePointSeriesData = computed(() => {
|
||||||
|
return sortedCharacteristicCurveData.value.map(item => ({
|
||||||
|
value: [item.duration, item.residualVoltage, '特性点']
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const passPointSeriesData = computed(() => {
|
||||||
|
return sortedChartPoints.value
|
||||||
|
.filter(item => item.status === 'pass')
|
||||||
|
.map(item => ({
|
||||||
|
value: [item.duration, item.residualVoltage, getStatusText(item.status)]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const failPointSeriesData = computed(() => {
|
||||||
|
return sortedChartPoints.value
|
||||||
|
.filter(item => item.status === 'fail')
|
||||||
|
.map(item => ({
|
||||||
|
value: [item.duration, item.residualVoltage, getStatusText(item.status)]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasChartData = computed(() => {
|
||||||
|
return sortedChartPoints.value.length > 0 || sortedCharacteristicCurveData.value.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatLogDurationLabel = (value: number) => {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value >= 1) {
|
||||||
|
return `${Number(value.toFixed(2))}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${Number(value.toFixed(3))}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizeFileName = (value: string) => {
|
||||||
|
return value.replace(/[\\/:*?"<>|]/g, '_').trim() || '未命名变频器'
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildFileName = (prefix: string, suffix: string) => {
|
||||||
|
const freqConverterName = sanitizeFileName(props.selectedMapping?.freqConverterName || '未命名变频器')
|
||||||
|
return `${prefix}_${freqConverterName}.${suffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerDownload = (url: string, fileName: string) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.style.display = 'none'
|
||||||
|
link.href = url
|
||||||
|
link.download = fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNumber = (value: unknown) => {
|
||||||
|
const result = Number(value)
|
||||||
|
return Number.isFinite(result) ? result : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsePointTime = (value: unknown) => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return {
|
||||||
|
time: null,
|
||||||
|
timeMs: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedValue = value.trim()
|
||||||
|
const match = normalizedValue.match(
|
||||||
|
/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return {
|
||||||
|
time: normalizedValue || null,
|
||||||
|
timeMs: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, year, month, day, hour, minute, second, millisecond] = match
|
||||||
|
const parsedDate = new Date(
|
||||||
|
Number(year),
|
||||||
|
Number(month) - 1,
|
||||||
|
Number(day),
|
||||||
|
Number(hour),
|
||||||
|
Number(minute),
|
||||||
|
Number(second),
|
||||||
|
Number(millisecond)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedDate.getFullYear() !== Number(year) ||
|
||||||
|
parsedDate.getMonth() !== Number(month) - 1 ||
|
||||||
|
parsedDate.getDate() !== Number(day) ||
|
||||||
|
parsedDate.getHours() !== Number(hour) ||
|
||||||
|
parsedDate.getMinutes() !== Number(minute) ||
|
||||||
|
parsedDate.getSeconds() !== Number(second) ||
|
||||||
|
parsedDate.getMilliseconds() !== Number(millisecond)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
time: normalizedValue,
|
||||||
|
timeMs: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
time: normalizedValue,
|
||||||
|
timeMs: parsedDate.getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
const {time, timeMs} = parsePointTime(source.time)
|
||||||
|
|
||||||
|
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,
|
||||||
|
time,
|
||||||
|
timeMs,
|
||||||
|
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) => {
|
||||||
|
return status === 'fail' ? '不耐受' : '耐受'
|
||||||
|
}
|
||||||
|
|
||||||
|
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: CharacteristicCurvePoint[] = []
|
||||||
|
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.time ? `${point.time}|${point.duration}|${point.residualVoltage}` : `${point.duration}|${point.residualVoltage}`
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key)
|
||||||
|
result.push({
|
||||||
|
duration: point.duration,
|
||||||
|
residualVoltage: point.residualVoltage,
|
||||||
|
time: point.time,
|
||||||
|
timeMs: point.timeMs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(node).forEach(item => {
|
||||||
|
if (item && typeof item === 'object') {
|
||||||
|
walk(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(rootPayload)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeCharacteristicCurvePoints = (points: CharacteristicCurvePoint[]) => {
|
||||||
|
if (!points.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPointMap = new Map(
|
||||||
|
characteristicCurveData.value.map(item => [
|
||||||
|
item.time ? `${item.time}|${item.duration}|${item.residualVoltage}` : `${item.duration}|${item.residualVoltage}`,
|
||||||
|
item
|
||||||
|
] as const)
|
||||||
|
)
|
||||||
|
|
||||||
|
points.forEach(item => {
|
||||||
|
const key = item.time ? `${item.time}|${item.duration}|${item.residualVoltage}` : `${item.duration}|${item.residualVoltage}`
|
||||||
|
existingPointMap.set(key, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
characteristicCurveData.value = Array.from(existingPointMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCharacteristicCurveVisibility = () => {
|
||||||
|
if (props.autoDrawCurve) {
|
||||||
|
characteristicCurveVisible.value = characteristicCurveData.value.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawCharacteristicCurve = () => {
|
||||||
|
if (!sortedCharacteristicCurveData.value.length) {
|
||||||
|
characteristicCurveVisible.value = false
|
||||||
|
ElMessage.warning('暂无特性曲线点')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristicCurveVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadChartImage = async () => {
|
||||||
|
if (!hasChartData.value) {
|
||||||
|
ElMessage.warning('暂无可下载的图表数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const chartInstance = chartRef.value?.getChartInstance?.()
|
||||||
|
if (!chartInstance) {
|
||||||
|
ElMessage.warning('图表尚未渲染完成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = chartInstance.getDataURL({
|
||||||
|
type: 'png',
|
||||||
|
pixelRatio: 2,
|
||||||
|
backgroundColor: '#ffffff'
|
||||||
|
})
|
||||||
|
|
||||||
|
triggerDownload(imageUrl, buildFileName('变频器耐受图', 'png'))
|
||||||
|
ElMessage.success('图表图片导出成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportChartData = () => {
|
||||||
|
if (!hasChartData.value) {
|
||||||
|
ElMessage.warning('暂无可导出的点位数据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new()
|
||||||
|
|
||||||
|
if (sortedChartPoints.value.length) {
|
||||||
|
const pointSheet = XLSX.utils.json_to_sheet(
|
||||||
|
sortedChartPoints.value.map((item, index) => ({
|
||||||
|
序号: index + 1,
|
||||||
|
持续时间_s: item.duration,
|
||||||
|
残余电压_pct: item.residualVoltage,
|
||||||
|
状态: getStatusText(item.status)
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
XLSX.utils.book_append_sheet(workbook, pointSheet, '耐受点')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortedCharacteristicCurveData.value.length) {
|
||||||
|
const curveSheet = XLSX.utils.json_to_sheet(
|
||||||
|
sortedCharacteristicCurveData.value.map((item, index) => ({
|
||||||
|
序号: index + 1,
|
||||||
|
持续时间_s: item.duration,
|
||||||
|
残余电压_pct: item.residualVoltage,
|
||||||
|
时间: item.time ?? ''
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
XLSX.utils.book_append_sheet(workbook, curveSheet, '特性点')
|
||||||
|
}
|
||||||
|
|
||||||
|
const workbookBuffer = XLSX.write(workbook, {bookType: 'xlsx', type: 'array'})
|
||||||
|
const blob = new Blob([workbookBuffer], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
})
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
try {
|
||||||
|
triggerDownload(blobUrl, buildFileName('变频器耐受图数据', 'xlsx'))
|
||||||
|
ElMessage.success('点位数据导出成功')
|
||||||
|
} finally {
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartOptions = computed(() => {
|
||||||
|
return {
|
||||||
|
title: {
|
||||||
|
text: ''
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: 30,
|
||||||
|
left: 48,
|
||||||
|
right: 22,
|
||||||
|
bottom: 52
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter(params: any) {
|
||||||
|
const rawValue = Array.isArray(params.value) ? params.value : params.value?.value
|
||||||
|
if (!Array.isArray(rawValue)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const [duration, residualVoltage, statusText] = rawValue
|
||||||
|
return [
|
||||||
|
`类型: ${params.seriesName}`,
|
||||||
|
`持续时间: ${duration} s`,
|
||||||
|
`残余电压: ${residualVoltage} %`,
|
||||||
|
...(statusText ? [`状态: ${statusText}`] : [])
|
||||||
|
].join('<br/>')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
data: ['特性曲线', '特性点', '耐受点', '不耐受点']
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'log',
|
||||||
|
name: '持续时间(s)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 34,
|
||||||
|
min: xAxisMin.value,
|
||||||
|
max: xAxisMax.value,
|
||||||
|
logBase: 10,
|
||||||
|
minorTick: {
|
||||||
|
show: true,
|
||||||
|
splitNumber: 10
|
||||||
|
},
|
||||||
|
minorSplitLine: {
|
||||||
|
show: false,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e8edf6'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#cfd8e6'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter(value: number) {
|
||||||
|
return formatLogDurationLabel(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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: false,
|
||||||
|
lineStyle: {
|
||||||
|
color: CHARACTERISTIC_POINT_COLOR,
|
||||||
|
width: 3
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: CHARACTERISTIC_POINT_COLOR
|
||||||
|
},
|
||||||
|
data: solidCharacteristicCurveSeriesData.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '特性点',
|
||||||
|
type: 'scatter',
|
||||||
|
symbolSize: 10,
|
||||||
|
itemStyle: {
|
||||||
|
color: CHARACTERISTIC_POINT_COLOR
|
||||||
|
},
|
||||||
|
data: characteristicCurvePointSeriesData.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '耐受点',
|
||||||
|
type: 'scatter',
|
||||||
|
symbolSize: 10,
|
||||||
|
itemStyle: {
|
||||||
|
color: STATUS_COLOR_MAP.pass
|
||||||
|
},
|
||||||
|
data: passPointSeriesData.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '不耐受点',
|
||||||
|
type: 'scatter',
|
||||||
|
symbolSize: 10,
|
||||||
|
itemStyle: {
|
||||||
|
color: STATUS_COLOR_MAP.fail
|
||||||
|
},
|
||||||
|
data: failPointSeriesData.value
|
||||||
|
}
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
animation: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.autoDrawCurve,
|
||||||
|
newValue => {
|
||||||
|
characteristicCurveVisible.value = newValue ? characteristicCurveData.value.length > 0 : false
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.webMsgSend,
|
||||||
|
newValue => {
|
||||||
|
if (!newValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPoints = extractPoints(newValue)
|
||||||
|
if (nextPoints.length) {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeCharacteristicCurvePoints(extractCharacteristicCurvePoints(newValue))
|
||||||
|
updateCharacteristicCurveVisibility()
|
||||||
|
},
|
||||||
|
{deep: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.resultData,
|
||||||
|
newValue => {
|
||||||
|
if (!newValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chartPoints.value = extractPoints(newValue)
|
||||||
|
characteristicCurveData.value = extractCharacteristicCurvePoints(newValue)
|
||||||
|
updateCharacteristicCurveVisibility()
|
||||||
|
},
|
||||||
|
{deep: true, immediate: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedMapping,
|
||||||
|
() => {
|
||||||
|
chartPoints.value = []
|
||||||
|
characteristicCurveData.value = []
|
||||||
|
characteristicCurveVisible.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 基础信息弹出框 -->
|
||||||
|
<el-dialog :model-value="dialogVisible" :title="dialogTitle" v-bind="dialogMiddle" @close="close" align-center>
|
||||||
|
<div>
|
||||||
|
<el-form :model="formContent" ref='dialogFormRef' :rules='baseRules' class="form-two">
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model='formContent.name' placeholder="请输入变频器名称" maxlength="32" show-word-limit/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="串口名称" prop="portName">
|
||||||
|
<el-input v-model='formContent.portName' placeholder="请输入串口名称" maxlength="32" show-word-limit/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="从机地址" prop="slaveAddress">
|
||||||
|
<el-input v-model='formContent.slaveAddress' placeholder="请输入从机地址" maxlength="32" show-word-limit/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="波特率" prop="baudRate">
|
||||||
|
<el-select v-model="formContent.baudRate" clearable placeholder="请选择波特率">
|
||||||
|
<el-option
|
||||||
|
v-for="item in baudRateList"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="奇偶校验类型" prop="parity">
|
||||||
|
<el-select v-model="formContent.parity" clearable placeholder="请选择奇偶校验类型">
|
||||||
|
<el-option key="None" label="None" :value="'None'"/>
|
||||||
|
<el-option key="Even" label="Even" :value="'Even'"/>
|
||||||
|
<el-option key="Odd" label="Odd" :value="'Odd'"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="数据位" prop="dataBits">
|
||||||
|
<el-select v-model="formContent.dataBits" clearable placeholder="请选择数据位">
|
||||||
|
<el-option key="4" label="4" :value="4"/>
|
||||||
|
<el-option key="5" label="5" :value="5"/>
|
||||||
|
<el-option key="6" label="6" :value="6"/>
|
||||||
|
<el-option key="7" label="7" :value="7"/>
|
||||||
|
<el-option key="8" label="8" :value="8"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="停止位" prop="stopBits">
|
||||||
|
<el-select v-model="formContent.stopBits" clearable placeholder="请选择停止位">
|
||||||
|
<el-option key="1" label="1" :value="1"/>
|
||||||
|
<el-option key="2" label="2" :value="2"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="超时时间(Ms)" prop="timeoutMs">
|
||||||
|
<el-input-number v-model="formContent.timeoutMs" placeholder="请输入超时时间" style="width: 100%" min="1"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="close()">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save()">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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'
|
||||||
|
|
||||||
|
// 定义弹出组件元信息
|
||||||
|
const dialogFormRef = ref()
|
||||||
|
|
||||||
|
function useMetaInfo() {
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const titleType = ref('add')
|
||||||
|
const formContent = ref<FreqConverter.ResFreqConverter>({
|
||||||
|
id: '',
|
||||||
|
name: '', //名称
|
||||||
|
portName: '',//串口名称
|
||||||
|
slaveAddress: 1, //从机地址
|
||||||
|
baudRate: 9600, //波特率
|
||||||
|
parity: 'None', //奇偶校验类型
|
||||||
|
dataBits: 8, //数据位
|
||||||
|
stopBits: 1, //停止位
|
||||||
|
timeoutMs: 500, //超时时间(毫秒)
|
||||||
|
})
|
||||||
|
const baudRateList = [75, 110, 134, 150, 300, 600, 1200, 1800, 2400, 4800, 7200, 9600, 14400, 19200, 38400, 57600, 115200, 128000]
|
||||||
|
return {dialogVisible, titleType, formContent, baudRateList}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {dialogVisible, titleType, formContent, baudRateList} = useMetaInfo()
|
||||||
|
|
||||||
|
|
||||||
|
// 清空formContent
|
||||||
|
const resetFormContent = () => {
|
||||||
|
formContent.value = {
|
||||||
|
id: '',
|
||||||
|
name: '', //名称
|
||||||
|
portName: '',//串口名称
|
||||||
|
slaveAddress: 1, //从机地址
|
||||||
|
baudRate: 9600, //波特率
|
||||||
|
parity: 'None', //奇偶校验类型
|
||||||
|
dataBits: 8, //数据位
|
||||||
|
stopBits: 1, //停止位
|
||||||
|
timeoutMs: 500, //超时时间(毫秒)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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' ? '新增变频器' : '编辑变频器'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
//定义校验规则
|
||||||
|
const baseRules: Record<string, Array<FormItemRule>> = {
|
||||||
|
name: [{required: true, message: '名称必填!', trigger: 'blur'}],
|
||||||
|
portName: [
|
||||||
|
{required: true, message: '串口名称必填!', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
slaveAddress: [
|
||||||
|
{required: true, message: '从机地址必填!', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
baudRate: [
|
||||||
|
{required: true, message: '波特率必填!', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
parity: [
|
||||||
|
{required: true, message: '奇偶校验类型必选!', trigger: 'change'}
|
||||||
|
],
|
||||||
|
dataBits: [
|
||||||
|
{required: true, message: '数据位必填!', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
stopBits: [
|
||||||
|
{required: true, message: '停止位必填!', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
timeoutMs: [
|
||||||
|
{required: true, message: '超时时间必填!', trigger: 'blur'}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const close = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 清空dialogForm中的值
|
||||||
|
resetFormContent()
|
||||||
|
// 重置表单
|
||||||
|
dialogFormRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存数据
|
||||||
|
const save = () => {
|
||||||
|
try {
|
||||||
|
dialogFormRef.value?.validate(async (valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
if (formContent.value.id) {
|
||||||
|
await updateFreqConverter(formContent.value);
|
||||||
|
} else {
|
||||||
|
await addFreqConverter(formContent.value);
|
||||||
|
}
|
||||||
|
ElMessage.success({message: `${dialogTitle.value}成功!`})
|
||||||
|
close()
|
||||||
|
// 刷新表格
|
||||||
|
await props.refreshTable!()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
//error('验证过程中出现错误', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开弹窗,可能是新增,也可能是编辑
|
||||||
|
const open = async (sign: string, data: Partial<FreqConverter.ResFreqConverter> = {}) => {
|
||||||
|
// 重置表单
|
||||||
|
dialogFormRef.value?.resetFields()
|
||||||
|
titleType.value = sign
|
||||||
|
dialogVisible.value = true
|
||||||
|
|
||||||
|
if (data.id) {
|
||||||
|
formContent.value = {...getDefaultFormContent(), ...data}
|
||||||
|
} else {
|
||||||
|
resetFormContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对外映射
|
||||||
|
defineExpose({open})
|
||||||
|
const props = defineProps<{
|
||||||
|
refreshTable: (() => Promise<void>) | undefined;
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<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"
|
||||||
|
:auto-draw-curve="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user