18 Commits

Author SHA1 Message Date
caozehui
4189eea278 默认模式为模拟式 2026-05-11 20:37:44 +08:00
caozehui
d3fef49af2 微调 2026-05-11 20:17:11 +08:00
caozehui
12d40a5c6e 打包 2026-05-09 09:53:10 +08:00
caozehui
6748490c3f 隐藏非必要的页面 2026-05-09 08:43:33 +08:00
caozehui
2c19fc43de 微调 2026-05-08 13:16:19 +08:00
caozehui
2ba5ebaddb Merge remote-tracking branch 'origin/hainan' into hainan
# Conflicts:
#	frontend/src/views/machine/freqConverter/components/freqConverterDipChart.vue
2026-05-08 11:36:53 +08:00
caozehui
448687115c 实施测试特性曲线与结果展示特性曲线不一致问题 2026-05-08 11:32:45 +08:00
caozehui
0d4dc2d2bf 特性点连线顺序 2026-05-08 10:40:05 +08:00
caozehui
121829a4bd 特性点顺序问题 2026-05-07 19:18:05 +08:00
caozehui
3f6952612d 调整绘制特性点 2026-05-07 08:52:47 +08:00
caozehui
5ca5d73f98 实时推送&绘制特性点 2026-05-06 10:53:30 +08:00
caozehui
71d80e67f1 替换标题 2026-04-30 09:32:02 +08:00
caozehui
0e0969c50f 耐受实验调整为首页、替换标题 2026-04-30 09:31:01 +08:00
caozehui
8ab1a35f3b 耐受图调整、屏蔽非必要的菜单、登录后跳转到变频器页面 2026-04-27 08:39:30 +08:00
caozehui
8744dfb0d8 变频器耐受表X轴改用对数刻度、补充错误码 2026-04-22 19:32:28 +08:00
caozehui
b826f505ac 变频器功能页面 2026-04-17 09:15:58 +08:00
caozehui
465ad81069 微调 2026-04-14 15:06:09 +08:00
caozehui
c73bf05d41 变频器台账信息 2026-04-14 14:50:07 +08:00
101 changed files with 2591 additions and 151 deletions

View File

@@ -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="
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 #暂态幅值允许最多误差%

View File

@@ -1 +1 @@
11900
42428

Binary file not shown.

View File

@@ -1,13 +1,3 @@
.\binlog.000023
.\binlog.000024
.\binlog.000025
.\binlog.000026
.\binlog.000027
.\binlog.000028
.\binlog.000029
.\binlog.000030
.\binlog.000031
.\binlog.000032
.\binlog.000033
.\binlog.000034
.\binlog.000035
.\binlog.000036

View File

@@ -42,7 +42,7 @@ function createTray() {
tray = new Tray(iconPath);
}
tray.setToolTip('NPQS-9100自动检测平台');
tray.setToolTip('变频器暂降耐受实验平台');
console.log('[Tray] Tray created successfully');
// 创建托盘菜单

View File

@@ -1,5 +1,5 @@
# title
VITE_GLOB_APP_TITLE=NPQS-9100自动检测平台
VITE_GLOB_APP_TITLE=变频器暂降耐受实验平台
# 本地运行端口号
VITE_PORT=18091

View File

@@ -19,9 +19,9 @@ VITE_API_URL=/api
# 开发环境跨域代理,支持配置多个
VITE_PROXY=[["/api","http://127.0.0.1:18093/"]]
VITE_PROXY=[["/api","http://127.0.0.1:18092/"]]
#VITE_PROXY=[["/api","http://192.168.1.124:18092/"]]
#VITE_PROXY=[["/api","http://192.168.2.125:18092/"]]
# VITE_PROXY=[["/api","http://192.168.1.138:8080/"]]张文
# 开启激活验证
VITE_ACTIVATE_OPEN=true
VITE_ACTIVATE_OPEN=false

View File

@@ -23,6 +23,6 @@ VITE_PWA=true
# 线上环境接口地址
#VITE_API_URL="/api" # 打包时用
VITE_API_URL="http://127.0.0.1:18093/"
VITE_API_URL="http://127.0.0.1:18092/"
# 开启激活验证
VITE_ACTIVATE_OPEN=true
VITE_ACTIVATE_OPEN=false

View File

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

View File

@@ -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 || ''}`)
}

View 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;
}
}

View File

@@ -130,6 +130,7 @@ const initChart = () => {
chart.resize()
}, 0)
}
const getChartInstance = () => chart
const handlerBar = (options: any) => {
if (Array.isArray(options.series)) {
options.series.forEach((item: any) => {
@@ -253,7 +254,7 @@ onMounted(() => {
initChart()
resizeObserver.observe(chartRef.value!)
})
defineExpose({ initChart })
defineExpose({ initChart, getChartInstance })
onBeforeUnmount(() => {
resizeObserver.unobserve(chartRef.value!)
chart?.dispose()

View File

@@ -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";

View File

@@ -1,31 +1,32 @@
<template>
<div class="footer flx-align-center pl10">
<el-dropdown>
<div class="change_mode">
{{ title }}
<el-icon class="el-icon--right change_mode_down"><arrow-down /></el-icon>
<el-icon class="el-icon--right change_mode_up"><arrow-up /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in modeList"
:key="item.key"
:disabled="!item.activated"
@click="handelOpen(item.code, item.key)"
>
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<p style="margin: 0">
<a href="http://www.shining-electric.com/" target="_blank">2024 © 南京灿能电力自动化股份有限公司</a>
</p>
</div>
<div class="footer flx-align-center pl10">
<el-dropdown>
<div class="change_mode">
{{ title }}
<el-icon class="el-icon--right change_mode_down"><arrow-down /></el-icon>
<el-icon class="el-icon--right change_mode_up"><arrow-up /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in modeList"
:key="item.key"
:disabled="!item.activated"
@click="handelOpen(item.code, item.key)"
>
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<p style="margin: 0">
<a href="http://www.shining-electric.com/" target="_blank">2024 © 南京灿能电力自动化股份有限公司</a>
</p>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { HOME_URL } from '@/config'
import { useAuthStore } from '@/stores/modules/auth'
import { useModeStore } from '@/stores/modules/mode' // 引入模式 store
import { useTabsStore } from '@/stores/modules/tabs'
@@ -38,97 +39,97 @@ const modeStore = useModeStore()
const tabsStore = useTabsStore()
const title = computed(() => {
return modeStore.currentMode === '' ? '选择模块' : modeStore.currentMode + '模块'
return modeStore.currentMode === '' ? '选择模块' : modeStore.currentMode + '模块'
})
const activateInfo = authStore.activateInfo
const isActivateOpen = import.meta.env.VITE_ACTIVATE_OPEN
const modeList = [
{
name: '模拟式模块',
code: '模拟式',
key: 'simulate',
activated: isActivateOpen === 'true' ? activateInfo.simulate.permanently === 1 : true
},
{
name: '数字式模块',
code: '数字式',
key: 'digital',
activated: isActivateOpen === 'true' ? activateInfo.digital.permanently === 1 : true
},
{
name: '比对式模块',
code: '比对式',
key: 'contrast',
activated: isActivateOpen === 'true' ? activateInfo.contrast.permanently === 1 : true
}
{
name: '模拟式模块',
code: '模拟式',
key: 'simulate',
activated: isActivateOpen === 'true' ? activateInfo.simulate.permanently === 1 : true
},
{
name: '数字式模块',
code: '数字式',
key: 'digital',
activated: isActivateOpen === 'true' ? activateInfo.digital.permanently === 1 : true
},
{
name: '比对式模块',
code: '比对式',
key: 'contrast',
activated: isActivateOpen === 'true' ? activateInfo.contrast.permanently === 1 : true
}
]
const handelOpen = async (item: string, key: string) => {
if (isActivateOpen === 'true' && activateInfo[key].permanently !== 1) {
ElMessage.warning(`${item}模块未激活`)
return
}
await authStore.setShowMenu()
modeStore.setCurrentMode(item) // 将模式code存入 store
// 强制刷新页面
await tabsStore.closeMultipleTab()
await initDynamicRouter()
if (isActivateOpen === 'true' && activateInfo[key].permanently !== 1) {
ElMessage.warning(`${item}模块未激活`)
return
}
await authStore.setShowMenu()
modeStore.setCurrentMode(item) // 将模式code存入 store
// 强制刷新页面
await tabsStore.closeMultipleTab()
await initDynamicRouter()
// 只有当目标路径与当前路径不同时才跳转
if (router.currentRoute.value.path !== HOME_URL) {
await router.push({ path: HOME_URL })
} else {
// 如果已在目标页面,手动触发组件更新
window.location.reload() // 或者采用其他方式刷新数据
}
// 只有当目标路径与当前路径不同时才跳转
if (router.currentRoute.value.path !== '/home/index') {
await router.push({ path: '/home/index' })
} else {
// 如果已在目标页面,手动触发组件更新
window.location.reload() // 或者采用其他方式刷新数据
}
}
</script>
<style scoped lang="scss">
@use './index.scss';
.footer {
position: relative;
background-color: var(--el-color-primary);
// .el-button:hover {
// background-color: var(--el-color-primary) !important;
// border: none !important;
// outline: none !important;
// }
.change_mode {
color: #fff;
display: flex;
align-items: center;
justify-content: flex-start;
height: 100%;
width: auto;
font-size: 14px;
.change_mode_down {
display: block;
}
.change_mode_up {
display: none;
}
position: relative;
background-color: var(--el-color-primary);
// .el-button:hover {
// background-color: var(--el-color-primary) !important;
// border: none !important;
// outline: none !important;
// }
.change_mode {
color: #fff;
display: flex;
align-items: center;
justify-content: flex-start;
height: 100%;
width: auto;
font-size: 14px;
.change_mode_down {
display: block;
}
.change_mode:hover {
.change_mode_down {
display: none;
}
.change_mode_up {
display: block;
}
.change_mode_up {
display: none;
}
.el-dropdown {
z-index: 1001;
}
.change_mode:hover {
.change_mode_down {
display: none;
}
p {
position: absolute;
width: 100%;
height: 100%;
text-align: right;
line-height: 40px;
a {
color: #fff;
margin-right: 25px; // 增加右边距
}
.change_mode_up {
display: block;
}
}
.el-dropdown {
z-index: 1001;
}
p {
position: absolute;
width: 100%;
height: 100%;
text-align: right;
line-height: 40px;
a {
color: #fff;
margin-right: 25px; // 增加右边距
}
}
}
</style>

View File

@@ -22,14 +22,6 @@
<el-icon><Edit /></el-icon>
{{ t('header.changePassword') }}
</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'">
<div class="custom-dropdown-trigger">
<el-icon><Tools /></el-icon>
@@ -62,8 +54,6 @@
<InfoDialog ref="infoRef"></InfoDialog>
<!-- passwordDialog -->
<PasswordDialog ref="passwordRef"></PasswordDialog>
<!-- versionRegisterDialog -->
<VersionDialog ref="versionRegisterRef"></VersionDialog>
<!-- ThemeDialog -->
<ThemeDialog ref="themeRef"></ThemeDialog>
</template>
@@ -77,9 +67,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import InfoDialog from './InfoDialog.vue'
import PasswordDialog from './PasswordDialog.vue'
import ThemeDialog from './ThemeDialog.vue'
import VersionDialog from '@/views/system/versionRegister/index.vue'
import { Avatar, Sunny, Switch, Tools } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/modules/auth'
import { Avatar, Sunny, Tools } from '@element-plus/icons-vue'
import { useDictStore } from '@/stores/modules/dict'
import { useAppSceneStore } from '@/stores/modules/mode'
import { useI18n } from 'vue-i18n'
@@ -90,7 +78,6 @@ const dictStore = useDictStore()
const username = computed(() => userStore.userInfo.name)
const router = useRouter()
const authStore = useAuthStore()
// 初始化 i18n
const { t } = useI18n() // 使用 t 方法替代 $t
@@ -111,12 +98,10 @@ const logout = () => {
// 打开修改密码和个人信息弹窗
const infoRef = ref<InstanceType<typeof InfoDialog> | 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 openDialog = (ref: string) => {
if (ref == 'infoRef') infoRef.value?.openDialog()
if (ref == 'passwordRef') passwordRef.value?.openDialog()
if (ref == 'versionRegisterRef') versionRegisterRef.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>
<style scoped lang="scss">

View File

@@ -29,6 +29,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useGlobalStore } from '@/stores/modules/global'
import { useTabsStore } from '@/stores/modules/tabs'
import { useAuthStore } from '@/stores/modules/auth'
import { HOME_URL } from '@/config'
import { TabPaneName, TabsPaneContext } from 'element-plus'
import MoreButton from './components/MoreButton.vue'
@@ -73,13 +74,13 @@ watch(
// 初始化需要固定的 tabs
const initTabs = () => {
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 = {
icon: item.meta.icon,
title: item.meta.title,
path: item.path,
name: item.name,
close: !item.meta.isAffix,
close: false,
isKeepAlive: item.meta.isKeepAlive,
unshift: true
}

View File

@@ -3,6 +3,7 @@ import { type AuthState } from '@/stores/interface'
import { getAuthButtonListApi, getAuthMenuListApi } from '@/api/user/login'
import { getAllBreadcrumbList, getFlatMenuList, getShowMenuList } from '@/utils'
import { AUTH_STORE_KEY } from '@/stores/constant'
import { HOME_URL } from '@/config'
import { useModeStore } from '@/stores/modules/mode'
import { getLicense } from '@/api/activate'
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, ['standardDevice'])
this.authMenuList = filteredMenu
this.authMenuList = normalizeHomeAffix(filteredMenu)
},
// Set RouteName
async setRouteName(name: string) {
@@ -112,3 +113,22 @@ function filterMenuByExcludedNames(menuList: any[], excludedNames: string[]): an
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
})
}

View File

@@ -1,9 +1,11 @@
// src/stores/modules/mode.ts
import { defineStore } from 'pinia'
export const DEFAULT_MODE = '模拟式'
export const useModeStore = defineStore('mode', {
state: () => ({
currentMode: localStorage.getItem('currentMode') || ('' as string)
currentMode: localStorage.getItem('currentMode') || DEFAULT_MODE
}),
actions: {
setCurrentMode(modeName: string) {

View File

@@ -4,7 +4,7 @@ import piniaPersistConfig from '@/stores/helper/persist'
import { USER_STORE_KEY } from '@/stores/constant'
import { logoutApi } from '@/api/user/login'
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'
export const useUserStore = defineStore(USER_STORE_KEY, {
@@ -48,7 +48,7 @@ export const useUserStore = defineStore(USER_STORE_KEY, {
this.setUserInfo({ id: '', name: '', loginName: '' })
this.setIsRefreshToken(false)
dictStore.setDictData([])
modeStore.setCurrentMode('')
modeStore.setCurrentMode(DEFAULT_MODE)
appSceneStore.setCurrentMode('')
await authStore.resetAuthStore()
}

View File

@@ -18,7 +18,7 @@
<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="'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>
</ProTable>

View File

@@ -120,7 +120,7 @@ const login = (formEl: FormInstance | undefined) => {
await tabsStore.setTabs([])
await keepAliveStore.setKeepAliveName([])
// 登录默认不显示菜单和导航栏
await authStore.resetAuthStore()
await authStore.setShowMenu()
// 跳转到首页
await router.push(HOME_URL)
} finally {

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,406 @@
<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"
:auto-draw-curve="false"
/>
</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: '不存在上线的设备',
400:'请求格式或者参数错误',
404:'未知错误',
// 408:'超时',
// 409:'业务执行不符合预期',
500:'未知错误'
}
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 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 handleSocketMessage = (payload: any) => {
const requestId = `${payload?.requestId ?? ''}`
const normalizedRequestId = requestId.trim().toLowerCase()
const code = Number(payload?.code)
if (requestId === 'yjc_sbtxjy' && code !== 10200 || code in SOCKET_ERROR_MESSAGE_MAP) {
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 = extractResultArray(historyResult?.data ?? historyResult)
} else {
historyResultData.value = null
}
const res = await startFreqConverterDetect({
converterId: mapping.freqConverterId,
monitorId: `${mapping.deviceId}_${mapping.deviceChannel}`,
userId: JwtUtil.getLoginName(),
reset
})
if (res.code === 'A0000') {
selectedMapping.value = mapping
currentStep.value = 2
startDetectSuccess.value = true
return
}
} catch (error) {
console.error('开始变频器检测失败:', error)
ElMessage.error('开始检测失败')
} finally {
startLoading.value = false
}
return
}
}
const handleExit = async () => {
if (!hasSocketError.value) {
try {
await confirmExit()
} catch {
return
}
}
await stopDetect()
closeDialog()
await props.refreshTable?.()
}
const open = (row: FreqConverter.ResFreqConverter) => {
resetState()
currentFreqConverter.value = {...row}
dialogVisible.value = true
connectWebSocket()
}
onMounted(() => {
window.addEventListener('resize', updateViewportWidth)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateViewportWidth)
closeWebSocket()
})
defineExpose({open, channelPairingRef})
</script>
<style scoped>
.freq-converter-test-popup {
display: flex;
flex-direction: column;
gap: 10px;
}
.section-card {
border: 1px solid var(--el-border-color-light);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.section-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
}
:deep(.el-dialog__body) {
padding-top: 10px;
padding-bottom: 10px;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<div class="table-box" ref="popupBaseView">
<ProTable
ref="proTable"
:columns="columns"
:request-api="getTableList"
>
<template #tableHeader="scope">
<el-button v-auth.tolerance="'add'" type="primary" :icon="CirclePlus" @click="openDialog('add')">
新增
</el-button>
<el-button
v-auth.tolerance="'delete'"
type="danger"
:icon="Delete"
plain
:disabled="!scope.isSelected"
@click="batchDelete(scope.selectedListIds)"
>
删除
</el-button>
</template>
<template #operation="scope">
<el-button
v-auth.tolerance="'edit'"
type="primary"
link
:icon="EditPen"
:model-value="false"
@click="openDialog('edit', scope.row)"
>
编辑
</el-button>
<el-button
v-auth.tolerance="'delete'"
type="primary"
link
:icon="Delete"
@click="handleDelete(scope.row)"
>
删除
</el-button>
<el-button
v-auth.tolerance="'test'"
type="primary"
link
:icon="Stopwatch"
@click="handleTest(scope.row)"
>
检测
</el-button>
<el-button
v-auth.tolerance="'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" />
<FreqConverterTestPopup :refresh-table="proTable?.getTableList" ref="freqConverterTestPopup" />
<FreqConverterResultPopup ref="freqConverterResultPopup" />
</template>
<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'
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'
const proTable = ref<ProTableInstance>()
const freqConverterPopup = ref<InstanceType<typeof FreqConverterPopup>>()
const freqConverterTestPopup = ref<InstanceType<typeof FreqConverterTestPopup>>()
const freqConverterResultPopup = ref<InstanceType<typeof FreqConverterResultPopup>>()
const getTableList = async (params: any) => {
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: '序号'},
{prop: 'name', label: '名称', search: {el: 'input'}, minWidth: 100},
{prop: 'portName', label: '串口名称', minWidth: 90},
{prop: 'slaveAddress', label: '从机地址', minWidth: 90},
{prop: 'baudRate', label: '波特率', minWidth: 80},
{prop: 'parity', label: '奇偶校验类型', minWidth: 120},
{prop: 'dataBits', label: '数据位', minWidth: 80},
{prop: 'stopBits', label: '停止位', minWidth: 80},
{prop: 'timeoutMs', label: '超时时间(MS)', minWidth: 120},
{
prop: 'createTime',
label: '创建时间',
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}`
}
return ''
}
},
{prop: 'operation', label: '操作', fixed: 'right', width: 280}
])
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) => {
freqConverterTestPopup.value?.open(params)
}
const handleResult = async (params: FreqConverter.ResFreqConverter) => {
freqConverterResultPopup.value?.open(params)
}
</script>

Some files were not shown because too many files have changed in this diff Show More