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
110 changed files with 2633 additions and 791 deletions

View File

@@ -33,9 +33,9 @@ mybatis-plus:
#驼峰命名 #驼峰命名
map-underscore-to-camel-case: true map-underscore-to-camel-case: true
#配置sql日志输出 #配置sql日志输出
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#关闭日志输出 #关闭日志输出
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl # log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config: global-config:
db-config: db-config:
#指定主键生成策略 #指定主键生成策略
@@ -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 #暂态幅值允许最多误差%

View File

@@ -1 +1 @@
95428 42428

View File

@@ -1,16 +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.000034
.\binlog.000035 .\binlog.000035
.\binlog.000036 .\binlog.000036
.\binlog.000037
.\binlog.000038

View File

@@ -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');
// 创建托盘菜单 // 创建托盘菜单

View File

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

View File

@@ -19,7 +19,7 @@ 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.1.124:18092/"]]
#VITE_PROXY=[["/api","http://192.168.2.125:18092/"]] #VITE_PROXY=[["/api","http://192.168.2.125:18092/"]]
# VITE_PROXY=[["/api","http://192.168.1.138:8080/"]]张文 # VITE_PROXY=[["/api","http://192.168.1.138:8080/"]]张文

View File

@@ -23,6 +23,6 @@ VITE_PWA=true
# 线上环境接口地址 # 线上环境接口地址
#VITE_API_URL="/api" # 打包时用 #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=false VITE_ACTIVATE_OPEN=false

View File

@@ -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 被检设备管理模块
*/ */

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

@@ -69,30 +69,5 @@ export namespace Plan {
maxTime: number; maxTime: number;
} }
export interface PlanStatisticsItem {
itemId: string;
itemName: string;
unqualifiedCount: number;
}
export interface PlanStatistics {
planId: string;
planName: string;
totalCheckCount: number;
checkedDeviceCount: number;
uncheckedDeviceCount: number;
firstQualifiedDeviceCount: number;
secondQualifiedDeviceCount: number;
thirdOrMoreQualifiedDeviceCount: number;
qualifiedDeviceCount: number;
unqualifiedDeviceCount: number;
unqualifiedItemCount: number;
firstPassRate: number;
secondPassRate: number;
thirdOrMorePassRate: number;
unqualifiedRate: number;
itemDistributions: PlanStatisticsItem[];
}
} }

View File

@@ -94,10 +94,6 @@ export const staticsAnalyse = (params: { id: string[] }) => {
return http.download('/adPlan/analyse', params) return http.download('/adPlan/analyse', params)
} }
export const getPlanStatistics = (params: { planId: string; manufacturer?: string; devType?: string }) => {
return http.post<Plan.PlanStatistics>(`/adPlan/statistics`, params)
}
//根据计划id分页查询被检设 //根据计划id分页查询被检设
export const getDevListByPlanId = (params: any) => { export const getDevListByPlanId = (params: any) => {
return http.post(`/adPlan/listDevByPlanId`, params) return http.post(`/adPlan/listDevByPlanId`, params)

View File

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

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"; // export const HOME_URL: string = "/machine/controlSource";

View File

@@ -1,31 +1,32 @@
<template> <template>
<div class="footer flx-align-center pl10"> <div class="footer flx-align-center pl10">
<el-dropdown> <el-dropdown>
<div class="change_mode"> <div class="change_mode">
{{ title }} {{ title }}
<el-icon class="el-icon--right change_mode_down"><arrow-down /></el-icon> <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> <el-icon class="el-icon--right change_mode_up"><arrow-up /></el-icon>
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item
v-for="item in modeList" v-for="item in modeList"
:key="item.key" :key="item.key"
:disabled="!item.activated" :disabled="!item.activated"
@click="handelOpen(item.code, item.key)" @click="handelOpen(item.code, item.key)"
> >
{{ item.name }} {{ item.name }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<p style="margin: 0"> <p style="margin: 0">
<a href="http://www.shining-electric.com/" target="_blank">2024 © 南京灿能电力自动化股份有限公司</a> <a href="http://www.shining-electric.com/" target="_blank">2024 © 南京灿能电力自动化股份有限公司</a>
</p> </p>
</div> </div>
</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'
@@ -38,97 +39,97 @@ const modeStore = useModeStore()
const tabsStore = useTabsStore() const tabsStore = useTabsStore()
const title = computed(() => { const title = computed(() => {
return modeStore.currentMode === '' ? '选择模块' : modeStore.currentMode + '模块' return modeStore.currentMode === '' ? '选择模块' : modeStore.currentMode + '模块'
}) })
const activateInfo = authStore.activateInfo const activateInfo = authStore.activateInfo
const isActivateOpen = import.meta.env.VITE_ACTIVATE_OPEN const isActivateOpen = import.meta.env.VITE_ACTIVATE_OPEN
const modeList = [ const modeList = [
{ {
name: '模拟式模块', name: '模拟式模块',
code: '模拟式', code: '模拟式',
key: 'simulate', key: 'simulate',
activated: isActivateOpen === 'true' ? activateInfo.simulate.permanently === 1 : true activated: isActivateOpen === 'true' ? activateInfo.simulate.permanently === 1 : true
}, },
{ {
name: '数字式模块', name: '数字式模块',
code: '数字式', code: '数字式',
key: 'digital', key: 'digital',
activated: isActivateOpen === 'true' ? activateInfo.digital.permanently === 1 : true activated: isActivateOpen === 'true' ? activateInfo.digital.permanently === 1 : true
}, },
{ {
name: '比对式模块', name: '比对式模块',
code: '比对式', code: '比对式',
key: 'contrast', key: 'contrast',
activated: isActivateOpen === 'true' ? activateInfo.contrast.permanently === 1 : true activated: isActivateOpen === 'true' ? activateInfo.contrast.permanently === 1 : true
} }
] ]
const handelOpen = async (item: string, key: string) => { const handelOpen = async (item: string, key: string) => {
if (isActivateOpen === 'true' && activateInfo[key].permanently !== 1) { if (isActivateOpen === 'true' && activateInfo[key].permanently !== 1) {
ElMessage.warning(`${item}模块未激活`) ElMessage.warning(`${item}模块未激活`)
return return
} }
await authStore.setShowMenu() await authStore.setShowMenu()
modeStore.setCurrentMode(item) // 将模式code存入 store modeStore.setCurrentMode(item) // 将模式code存入 store
// 强制刷新页面 // 强制刷新页面
await tabsStore.closeMultipleTab() await tabsStore.closeMultipleTab()
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() // 或者采用其他方式刷新数据
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@use './index.scss'; @use './index.scss';
.footer { .footer {
position: relative; position: relative;
background-color: var(--el-color-primary); background-color: var(--el-color-primary);
// .el-button:hover { // .el-button:hover {
// background-color: var(--el-color-primary) !important; // background-color: var(--el-color-primary) !important;
// border: none !important; // border: none !important;
// outline: none !important; // outline: none !important;
// } // }
.change_mode { .change_mode {
color: #fff; color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
height: 100%; height: 100%;
width: auto; width: auto;
font-size: 14px; font-size: 14px;
.change_mode_down { .change_mode_down {
display: block; display: block;
}
.change_mode_up {
display: none;
}
} }
.change_mode:hover { .change_mode_up {
.change_mode_down { display: none;
display: none;
}
.change_mode_up {
display: block;
}
} }
.el-dropdown { }
z-index: 1001; .change_mode:hover {
.change_mode_down {
display: none;
} }
p { .change_mode_up {
position: absolute; display: block;
width: 100%;
height: 100%;
text-align: right;
line-height: 40px;
a {
color: #fff;
margin-right: 25px; // 增加右边距
}
} }
}
.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> </style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
} }

View File

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

View File

@@ -91,7 +91,7 @@
type="primary" type="primary"
icon="Clock" icon="Clock"
@click="handleTest('手动检测')" @click="handleTest('手动检测')"
v-if="form.activeTabs === 0 && modeStore.currentMode != '比对式'" v-if="form.activeTabs === 0 && modeStore.currentMode == '模拟式'"
> >
手动检测 手动检测
</el-button> </el-button>
@@ -483,7 +483,7 @@ const columns = reactive<ColumnProps<Device.ResPqDev>[]>([
sortable: true, sortable: true,
isShow: checkStateShow, isShow: checkStateShow,
render: scope => { render: scope => {
return scope.row.checkState === 0 ? '未检' : scope.row.checkState === 1 ? '检测中' : scope.row.checkState === 2 ? '检测完成':'归档' return scope.row.checkState === 0 ? '未检' : scope.row.checkState === 1 ? '检测中' : '检测完成'
} }
}, },
{ {
@@ -494,12 +494,10 @@ const columns = reactive<ColumnProps<Device.ResPqDev>[]>([
render: scope => { render: scope => {
if (scope.row.checkResult === 0) { if (scope.row.checkResult === 0) {
return <el-tag type="danger">不符合</el-tag> return <el-tag type="danger">不符合</el-tag>
} else if (scope.row.checkResult === 0) {
return '不符合'
} else if (scope.row.checkResult === 1) { } else if (scope.row.checkResult === 1) {
return '符合' return '符合'
}else if(scope.row.checkResult === 2) { } else if (scope.row.checkResult === 2) {
return '未检' return '未检'
} }
return '' return ''
} }
@@ -541,6 +539,7 @@ const columns = reactive<ColumnProps<Device.ResPqDev>[]>([
{ prop: 'operation', label: '操作', fixed: 'right', minWidth :200,isShow: operationShow } { prop: 'operation', label: '操作', fixed: 'right', minWidth :200,isShow: operationShow }
]) ])
let testType = 'test' // 检测类型:'test'-检测 'reTest'-复检 let testType = 'test' // 检测类型:'test'-检测 'reTest'-复检
let qualifiedCount = 0 //合格数量
//比对单个报告生成 //比对单个报告生成
@@ -576,6 +575,8 @@ const handleSelectionChange = (selection: any[]) => {
} else { } else {
testType = 'reTest' testType = 'reTest'
} }
qualifiedCount=selection.filter(item => item.checkResult == 1).length
let devices: CheckData.Device[] = selection.map((item: any) => { let devices: CheckData.Device[] = selection.map((item: any) => {
return { return {
deviceId: item.id, deviceId: item.id,
@@ -598,19 +599,6 @@ const handleSelectionChange = (selection: any[]) => {
} }
} }
const isUncheckedDevice = (device: Device.ResPqDev) => Number(device.checkState) === 0 || Number(device.checkResult) === 2
const hasCheckedSelectedDevice = () => channelsSelection.value.some(device => !isUncheckedDevice(device))
const hasUncheckedSelectedDevice = () => channelsSelection.value.some(device => isUncheckedDevice(device))
const hasCheckedUnqualifiedSelectedDevice = () =>
channelsSelection.value.some(device => !isUncheckedDevice(device) && Number(device.checkResult) === 0)
const shouldShowRecheckModeDialog = () => hasCheckedSelectedDevice()
const canUseUnqualifiedItemRecheck = () => hasCheckedUnqualifiedSelectedDevice() && !hasUncheckedSelectedDevice()
//查询 //查询
const handleSearch = () => { const handleSearch = () => {
proTable.value?.getTableList() proTable.value?.getTableList()
@@ -935,12 +923,12 @@ const handleTest = async (val: string) => {
dialogTitle.value = val dialogTitle.value = val
if (val === '手动检测') { if (val === '手动检测') {
checkStore.setShowDetailType(2) checkStore.setShowDetailType(2)
if (shouldShowRecheckModeDialog()) { if (testType === 'reTest') {
ElMessageBox.confirm('请选择复检检测方式', '设备复检', { ElMessageBox.confirm('请选择复检检测方式', '设备复检', {
distinguishCancelAndClose: true, distinguishCancelAndClose: true,
confirmButtonText: '不合格项复检', confirmButtonText: '不合格项复检',
cancelButtonText: '全部复检', cancelButtonText: '全部复检',
showConfirmButton: canUseUnqualifiedItemRecheck(), showConfirmButton:qualifiedCount<=0,
type: 'warning' type: 'warning'
}) })
.then(() => { .then(() => {
@@ -975,12 +963,11 @@ const handleTest = async (val: string) => {
checkStore.setCheckType(1) checkStore.setCheckType(1)
checkStore.initSelectTestItems() checkStore.initSelectTestItems()
// 一键检测 // 一键检测
if (shouldShowRecheckModeDialog() && modeStore.currentMode != '比对式') { if (testType === 'reTest' && modeStore.currentMode != '比对式') {
ElMessageBox.confirm('请选择复检检测方式', '设备复检', { ElMessageBox.confirm('请选择复检检测方式', '设备复检', {
distinguishCancelAndClose: true, distinguishCancelAndClose: true,
confirmButtonText: '不合格项复检', confirmButtonText: '不合格项复检',
cancelButtonText: '全部复检', cancelButtonText: '全部复检',
showConfirmButton: canUseUnqualifiedItemRecheck(),
type: 'warning' type: 'warning'
}) })
.then(() => { .then(() => {
@@ -1100,7 +1087,7 @@ const openDrawer = async (title: string, row: any) => {
if (title === '检测数据查询') { if (title === '检测数据查询') {
checkStore.setShowDetailType(0) checkStore.setShowDetailType(0)
if (modeStore.currentMode == '模拟式'||modeStore.currentMode == '数字式') { if (modeStore.currentMode == '模拟式') {
dataCheckPopupRef.value?.open(row.id, '-1', null) dataCheckPopupRef.value?.open(row.id, '-1', null)
} else if (modeStore.currentMode == '比对式') { } else if (modeStore.currentMode == '比对式') {
dataCheckSingleChannelSingleTestPopupRef.value?.open(row, null, row.id, 2) dataCheckSingleChannelSingleTestPopupRef.value?.open(row, null, row.id, 2)
@@ -1108,7 +1095,7 @@ const openDrawer = async (title: string, row: any) => {
} }
if (title === '误差体系更换') { if (title === '误差体系更换') {
checkStore.setShowDetailType(1) checkStore.setShowDetailType(1)
if (modeStore.currentMode == '模拟式'||modeStore.currentMode == '数字式') { if (modeStore.currentMode == '模拟式') {
dataCheckPopupRef.value?.open(row.id, '-1', null) dataCheckPopupRef.value?.open(row.id, '-1', null)
} else if (modeStore.currentMode == '比对式') { } else if (modeStore.currentMode == '比对式') {
dataCheckSingleChannelSingleTestPopupRef.value?.open(row, null, row.id, 2) dataCheckSingleChannelSingleTestPopupRef.value?.open(row, null, row.id, 2)

View File

@@ -29,7 +29,7 @@
@node-click="handleNodeClick" @node-click="handleNodeClick"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="custom-tree-node"> <span class="custom-tree-node" style="display: flex; align-items: center;">
<!-- 父节点图标 --> <!-- 父节点图标 -->
<Platform <Platform
v-if="!data.pid" v-if="!data.pid"
@@ -39,52 +39,50 @@
}" }"
/> />
<!-- 节点名称 --> <!-- 节点名称 -->
<span class="node-label">{{ node.label }}</span> <span>{{ node.label }}</span>
<span class="node-actions"> <!-- 子节点右侧图标 + tooltip -->
<PieChart <el-tooltip
v-if="isCompletedPlanNode(node.data)" v-if="
class="node-action-icon" node.label != '未检' &&
@click.stop="openStatistics(node.data)" node.label != '检测中' &&
style="margin-right: 8px" node.label != '检测完成' &&
/> hasChildrenInPlanTable(node.data)
<!-- 子节点右侧图标 + tooltip --> "
<el-tooltip placement="top"
v-if=" :manual="true"
node.label != '未检' && content="子计划信息"
node.label != '检测中' && >
node.label != '检测完成' && <List
hasChildrenInPlanTable(node.data) @click.stop="childDetail(node.data)"
style="
width: 16px;
height: 16px;
margin-left: 8px;
cursor: pointer;
color: var(--el-color-primary);
" "
placement="top" />
:manual="true" </el-tooltip>
content="子计划信息"
>
<List class="node-action-icon" @click.stop="childDetail(node.data)" />
</el-tooltip>
</span>
</span> </span>
</template> </template>
</el-tree> </el-tree>
</div> </div>
</div> </div>
<SourceOpen ref="openSourceView" :width="width" :height="height + 175"></SourceOpen> <SourceOpen ref="openSourceView" :width="width" :height="height + 175"></SourceOpen>
<PlanStatisticsPopup ref="planStatisticsPopupRef" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { type Plan } from '@/api/plan/interface' import { type Plan } from '@/api/plan/interface'
import { List, Menu, PieChart, Platform } from '@element-plus/icons-vue' import { List, Menu, Platform } from '@element-plus/icons-vue'
import { nextTick, onMounted, ref, watch } from 'vue' import { nextTick, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useCheckStore } from '@/stores/modules/check' import { useCheckStore } from '@/stores/modules/check'
import { ElTooltip } from 'element-plus' import { ElTooltip } from 'element-plus'
import SourceOpen from '@/views/plan/planList/components/childrenPlan.vue' import SourceOpen from '@/views/plan/planList/components/childrenPlan.vue'
import PlanStatisticsPopup from '@/views/plan/planList/components/planStatisticsPopup.vue'
import { getPlanList } from '@/api/plan/plan.ts' import { getPlanList } from '@/api/plan/plan.ts'
import { useModeStore } from '@/stores/modules/mode' // 引入模式 store import { useModeStore } from '@/stores/modules/mode' // 引入模式 store
import { useDictStore } from '@/stores/modules/dict' import { useDictStore } from '@/stores/modules/dict'
const openSourceView = ref() const openSourceView = ref()
const planStatisticsPopupRef = ref<InstanceType<typeof PlanStatisticsPopup> | null>(null)
const router = useRouter() const router = useRouter()
const checkStore = useCheckStore() const checkStore = useCheckStore()
const filterText = ref('') const filterText = ref('')
@@ -213,14 +211,6 @@ const childDetail = (data: Plan.ResPlan) => {
} }
} }
const isCompletedPlanNode = (data: Partial<Plan.ResPlan>) => {
return [1, 2].includes(Number(data.testState))
}
const openStatistics = (data: Partial<Plan.ResPlan>) => {
planStatisticsPopupRef.value?.open(data)
}
function buildTree(flatList: any[]): any[] { function buildTree(flatList: any[]): any[] {
const map = new Map() const map = new Map()
const tree: any[] = [] const tree: any[] = []
@@ -303,40 +293,6 @@ defineExpose({ getTreeData, clickTableToTree })
margin-top: 12px; margin-top: 12px;
} }
:deep(.el-tree-node__content) {
padding-right: 6px;
}
.custom-tree-node {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
}
.node-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-actions {
flex: none;
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: 8px;
}
.node-action-icon {
width: 16px;
height: 16px;
cursor: pointer;
color: var(--el-color-primary);
}
//.filter-tree span { //.filter-tree span {
// font-size: 16px; // font-size: 16px;
// display:block; // display:block;

View File

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

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>

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