Files
pqs-9100_client/frontend/src/views/home/components/testPopup.vue
2025-10-15 08:49:11 +08:00

770 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 检测流程弹窗容器 -->
<el-dialog
:title="dialogTitle"
width="1550px"
:model-value="dialogVisible"
:before-close="beforeClose"
@close="handleClose"
height="1000px"
draggable
>
<!-- 步骤指示器容器 -->
<div class="steps-container">
<!-- 检测步骤进度条根据选择的检测项动态显示步骤 -->
<!-- 注意Element Plus的active属性从0开始但业务逻辑从1开始所以需要减1 -->
<el-steps
v-if="showSteps"
class="test-head-steps"
simple
:active="stepsActiveIndex - 1"
process-status="finish"
finish-status="success"
>
<!-- 预检测步骤 -->
<el-step
v-if="preTestSelected"
title="预检测"
:icon="stepsActive > 1 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : Edit"
@click="handleStepClick(1)"
/>
<!-- 守时检测步骤 -->
<el-step
v-if="timeTestSelected"
title="守时检测"
:icon="stepsActive > 2 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : UploadFilled"
@click="handleStepClick(2)"
/>
<!-- 系数校准步骤 -->
<el-step
v-if="channelsTestSelected"
title="系数校准"
:icon="stepsActive > 3 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : Odometer"
@click="handleStepClick(3)"
/>
<!-- 正式检测步骤 -->
<el-step
v-if="testSelected"
title="正式检测"
:icon="stepsActive > 4 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : Coin"
@click="handleStepClick(4)"
/>
<!-- 检测完成步骤 -->
<el-step title="检测完成" :icon="stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : Key" />
</el-steps>
</div>
<!-- 检测组件容器 - 根据当前步骤显示对应的检测组件 -->
<!-- 预检测组件 -->
<preTest
v-if="showComponent && preTestSelected"
v-show="preTestSelected && stepsActiveView == 1"
ref="preTestRef"
v-model:testStatus="preTestStatus"
:webMsgSend="webMsgSend"
/>
<!-- 守时检测组件 -->
<timeTest
v-if="showComponent && timeTestSelected"
v-show="timeTestSelected && stepsActiveView == 2"
v-model:testStatus="timeTestStatus"
/>
<!-- 系数校准检测组件 -->
<factorTest
v-if="showComponent && channelsTestSelected"
v-show="channelsTestSelected && stepsActiveView == 3"
v-model:testStatus="channelsTestStatus"
:webMsgSend="webMsgSend"
/>
<!-- 正式检测组件 -->
<test
v-if="showComponent && testSelected"
ref="testRef"
v-show="testSelected && stepsActiveView == 4"
v-model:testStatus="TestStatus"
:webMsgSend="webMsgSend"
@update:webMsgSend="webMsgSend = $event"
@sendPause="sendPause"
@sendResume="sendResume"
@closeWebSocket="closeWebSocket"
:stepsActive="stepsActive"
/>
<!-- 弹窗底部操作按钮区域 -->
<template #footer>
<div>
<!-- 开始检测按钮 - 当前状态为等待时显示 -->
<el-button type="primary" :icon="VideoPlay" v-if="ActiveStatue === 'waiting'" @click="handleSubmitFast">
开始检测
</el-button>
<!-- 初始化中按钮 - 禁用状态显示加载动画 -->
<el-button type="primary" v-if="TestStatus === 'test_init'" disabled>
<el-icon class="loading-box" style="color: #fff; margin-right: 8px">
<component :is="Refresh" />
</el-icon>
初始化中
</el-button>
<!-- 停止检测按钮 - 检测进行中时显示 -->
<el-button type="primary" v-if="TestStatus == 'process'" :icon="VideoPause" @click="handlePause()">
停止检测
</el-button>
<!-- 暂停中按钮 - 禁用状态显示加载动画 -->
<el-button type="warning" v-if="TestStatus === 'paused_ing'" disabled>
<el-icon class="loading-box" style="color: #fff; margin-right: 8px">
<component :is="Refresh" />
</el-icon>
暂停中
</el-button>
<!-- 继续检测按钮 - 检测已暂停时显示 -->
<el-button type="warning" v-if="TestStatus == 'paused'" :icon="VideoPlay" @click="sendResume">
继续检测
</el-button>
<!-- 下一步/完成/错误状态按钮 -->
<el-button
:type="ActiveStatue === 'success' ? 'primary' : 'danger'"
:icon="Right"
v-if="
nextStepText !== '下一步' &&
(ActiveStatue === 'success' ||
ActiveStatue === 'error' ||
ActiveStatue === 'connect_timeout' ||
ActiveStatue === 'pause_timeout')
"
@click="nextStep"
>
{{ nextStepText }}
</el-button>
<!-- 退出检测按钮 - 默认显示 -->
<el-button type="primary" @click="handleQuit" v-else>退出检测</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="tsx" setup name="testPopup">
// ====================== 导入依赖 ======================
import { onBeforeUnmount, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Coin,
Edit,
Key,
Odometer,
Refresh,
Right,
SuccessFilled,
UploadFilled,
VideoPause,
VideoPlay
} from '@element-plus/icons-vue'
import preTest from './preTest.vue'
import timeTest from './timeTest.vue'
import factorTest from './factorTest.vue'
import test from './test.vue'
import socketClient from '@/utils/webSocketClient'
import { useCheckStore } from '@/stores/modules/check'
import { pauseTest, resumeTest, startPreTest } from '@/api/socket/socket'
import { useUserStore } from '@/stores/modules/user'
import { JwtUtil } from '@/utils/jwtUtil'
// ====================== 状态管理 ======================
const userStore = useUserStore()
const checkStore = useCheckStore()
// ====================== 基础状态变量 ======================
const nextStepText = ref('下一步') // 下一步按钮文本
const dialogVisible = ref(false) // 弹窗显示状态
const dialogTitle = ref('') // 弹窗标题
const showComponent = ref(true) // 是否显示检测组件
const preTestRef = ref<{ initializeParameters: () => void } | null>(null) // 预检测组件引用
const testRef = ref<{ handlePause: () => void } | null>(null) // 正式检测组件引用
// ====================== 步骤控制相关变量 ======================
const showSteps = ref(false) // 是否显示步骤条
const stepsTotalNum = ref(-1) // 步骤总数(选中的检测项数量 + 1个完成步骤
const stepsActiveIndex = ref(1) // 当前正在执行的步骤索引用于UI显示高亮
const stepsActiveView = ref(-1) // 当前显示的检测组件步骤序号控制v-show显示哪个组件
const stepsActive = ref(-1) // 当前实际执行的业务步骤序号(控制业务逻辑分支)
// ====================== 状态管理变量 ======================
const ActiveStatue = ref('waiting') // 当前步骤的整体状态waiting/start/process/success/error等
const preTestStatus = ref('waiting') // 预检测执行状态
const timeTestStatus = ref('waiting') // 守时校验执行状态
const channelsTestStatus = ref('waiting') // 通道系数校准执行状态
const TestStatus = ref('waiting') // 正式检测执行状态
const webMsgSend = ref() // webSocket推送的数据用于组件间通信
// ====================== WebSocket 相关 ======================
const dataSocket = reactive<{
socketServe: typeof socketClient.Instance | null
}>({
socketServe: socketClient.Instance // WebSocket客户端实例
})
// ====================== 检测项选择状态 ======================
// 根据用户勾选的检测内容动态显示对应的检测步骤和组件
const preTestSelected = ref(false) // 是否选择预检测
const timeTestSelected = ref(false) // 是否选择守时检测
const channelsTestSelected = ref(false) // 是否选择系数校准
const testSelected = ref(false) // 是否选择正式检测
// ====================== 初始化操作 ======================
/**
* 初始化操作 - 重置所有状态并根据用户选择的检测项设置初始步骤
*/
const initOperate = () => {
// 重置所有状态为等待状态
ActiveStatue.value = 'waiting'
preTestStatus.value = 'waiting'
timeTestStatus.value = 'waiting'
channelsTestStatus.value = 'waiting'
TestStatus.value = 'waiting'
// 重置步骤和组件显示状态
stepsActiveIndex.value = 1
showComponent.value = true
// 从store中获取用户勾选的检测内容
preTestSelected.value = checkStore.selectTestItems.preTest
timeTestSelected.value = checkStore.selectTestItems.timeTest
channelsTestSelected.value = checkStore.selectTestItems.channelsTest
testSelected.value = checkStore.selectTestItems.test
// 计算总步骤数(选中的检测项 + 1个完成步骤
let count = 0
for (let key in checkStore.selectTestItems) {
if (checkStore.selectTestItems[key]) {
count++
}
}
stepsTotalNum.value = count + 1
// 根据选中的检测项,设置初始显示的步骤(按优先级:预检测 > 守时检测 > 系数校准 > 正式检测)
if (preTestSelected.value) {
stepsActiveView.value = 1 // 显示预检测组件
stepsActive.value = 1 // 业务逻辑设为预检测
return
}
if (timeTestSelected.value) {
stepsActiveView.value = 2 // 显示守时检测组件
stepsActive.value = 2 // 业务逻辑设为守时检测
return
}
if (channelsTestSelected.value) {
stepsActiveView.value = 3 // 显示系数校准组件
stepsActive.value = 3 // 业务逻辑设为系数校准
return
}
if (testSelected.value) {
stepsActiveView.value = 4 // 显示正式检测组件
stepsActive.value = 4 // 业务逻辑设为正式检测
return
}
}
// ====================== 弹窗开启方法 ======================
/**
* 打开检测弹窗
* @param title 弹窗标题
*/
const open = (title: string) => {
showSteps.value = true // 显示步骤条
initOperate() // 初始化所有状态和步骤
dialogTitle.value = title // 设置弹窗标题
dialogVisible.value = true // 显示弹窗
// 如果预检测组件存在,初始化其参数
if (preTestRef.value) {
preTestRef.value?.initializeParameters()
}
// 改进:无论什么状态,都先清理再重新建立连接
try {
// 先强制清理现有连接
if (dataSocket.socketServe) {
if (dataSocket.socketServe.connected) {
dataSocket.socketServe.closeWs()
}
// 清理回调
dataSocket.socketServe.unRegisterCallBack?.('aaa')
}
// 重新建立连接
socketClient.Instance.connect()
dataSocket.socketServe = socketClient.Instance
// 注册新的回调
dataSocket.socketServe.registerCallBack('aaa', res => {
// 将接收到的数据传递给子组件
webMsgSend.value = res
})
} catch (error) {
console.error('WebSocket连接处理失败:', error)
ElMessage.error('连接建立失败,请重试')
}
}
// ====================== 开始检测处理 ======================
/**
* 快速开始检测 - 根据当前步骤执行对应的检测逻辑
*/
const handleSubmitFast = () => {
// 获取设备ID列表和计划ID
let deviceIds = checkStore.devices.map(item => item.deviceId)
let planId = checkStore.plan.id
// 检查WebSocket连接状态
if (!dataSocket.socketServe.connected) {
ElMessage.error('webSocket连接中断')
return
}
// 根据当前激活的步骤执行对应的检测逻辑
switch (stepsActive.value) {
case 1: // 预检测步骤
if (preTestStatus.value == 'waiting') {
if (checkStore.selectTestItems.preTest) {
// 启动预检测
startPreTest({
userPageId: JwtUtil.getLoginName(),
devIds: deviceIds,
planId: planId,
reCheckType: checkStore.reCheckType == 1 ? '1' : '2', // 操作类型1为预检测2为正式检测
userId: userStore.userInfo.id,
temperature: checkStore.temperature,
humidity: checkStore.humidity,
testItemList: [
checkStore.selectTestItems.preTest,
checkStore.selectTestItems.channelsTest,
checkStore.selectTestItems.test
]
}).then(res => {
if (res.code !== 'A0000') {
ElMessageBox.alert('预检测失败', '检测失败', {
confirmButtonText: '确定',
type: 'error'
})
preTestStatus.value = 'error'
}
})
preTestStatus.value = 'start'
}
}
break
case 2: // 守时检测步骤
timeTestStatus.value = 'start'
break
case 3: // 系数校准步骤
if (channelsTestStatus.value == 'waiting') {
// 如果没有预检测且有系数校准,需要先进行初始化
if (!checkStore.selectTestItems.preTest && checkStore.selectTestItems.channelsTest) {
startPreTest({
userPageId: JwtUtil.getLoginName(),
devIds: deviceIds,
planId: planId,
reCheckType: checkStore.reCheckType == 1 ? '1' : '2',
userId: userStore.userInfo.id,
temperature: checkStore.temperature,
humidity: checkStore.humidity,
testItemList: [
checkStore.selectTestItems.preTest,
checkStore.selectTestItems.channelsTest,
checkStore.selectTestItems.test
]
}).then(res => {
if (res.code !== 'A0000') {
ElMessageBox.alert('系数校准失败', '检测失败', {
confirmButtonText: '确定',
type: 'error'
})
channelsTestStatus.value = 'error'
}
})
}
channelsTestStatus.value = 'start'
}
break
case 4: // 正式检测步骤
if (TestStatus.value == 'waiting') {
// 如果没有预检测和系数校准,直接进行正式检测需要先初始化
if (
!checkStore.selectTestItems.preTest &&
!checkStore.selectTestItems.channelsTest &&
checkStore.selectTestItems.test
) {
startPreTest({
userPageId: JwtUtil.getLoginName(),
devIds: deviceIds,
planId: planId,
reCheckType: checkStore.reCheckType == 1 ? '1' : '2',
userId: userStore.userInfo.id,
temperature: checkStore.temperature,
humidity: checkStore.humidity,
testItemList: [
checkStore.selectTestItems.preTest,
checkStore.selectTestItems.channelsTest,
checkStore.selectTestItems.test
]
}).then(res => {
if (res.code !== 'A0000') {
ElMessageBox.alert('正式检测失败', '检测失败', {
confirmButtonText: '确定',
type: 'error'
})
TestStatus.value = 'error'
}
})
}
TestStatus.value = 'start'
} else if (TestStatus.value == 'paused') {
// 如果是暂停状态,发送继续指令
sendResume()
}
break
default:
break
}
}
// ====================== 事件定义 ======================
const emit = defineEmits<{
(e: 'quitClicked'): void // 退出检测事件
}>()
// ====================== 状态监听器 ======================
// 监听各个检测步骤的状态变化,并同步到总体状态
watch(preTestStatus, function (newValue, oldValue) {
ActiveStatue.value = newValue // 同步到总体状态
})
watch(timeTestStatus, function (newValue, oldValue) {
ActiveStatue.value = newValue // 同步到总体状态
})
watch(channelsTestStatus, function (newValue, oldValue) {
ActiveStatue.value = newValue // 同步到总体状态
})
watch(TestStatus, function (newValue, oldValue) {
ActiveStatue.value = newValue // 同步到总体状态
})
// 监听总体状态变化,处理步骤切换和错误状态
watch(ActiveStatue, function (newValue, oldValue) {
// 处理错误状态
if (newValue === 'error') {
stepsActiveIndex.value = stepsTotalNum.value + 1 // 跳到最后一步
nextStepText.value = '检测失败'
}
// 处理成功完成状态(已到达最后一个检测步骤)
if (newValue === 'success' && stepsActiveIndex.value === stepsTotalNum.value - 1) {
stepsActiveIndex.value += 2 // 跳到完成状态
nextStepText.value = '检测完成'
}
// 处理连接超时状态
if (newValue === 'connect_timeout') {
stepsActiveIndex.value += 2 // 跳过当前步骤
nextStepText.value = '连接超时'
}
// 处理暂停超时状态
if (newValue === 'pause_timeout') {
stepsActiveIndex.value += 2 // 跳过当前步骤
nextStepText.value = '暂停超时'
}
// 处理成功状态的自动步骤切换(还有后续步骤时)
if (newValue === 'success' && stepsActiveIndex.value < stepsTotalNum.value - 1) {
nextStep() // 自动进入下一个测试步骤
handleSubmitFast() // 自动开始下一步检测
}
})
// ====================== 退出和暂停恢复处理 ======================
/**
* 处理退出检测
*/
const handleQuit = () => {
// 可以直接关闭的安全状态:未检测、检测完成、检测失败或异常情况
const safeExitStates = [
'waiting', // 未开始检测
'success', // 检测完成
'error', // 检测失败
'connect_timeout', // 连接超时
'pause_timeout' // 暂停超时
]
// 需要确认退出的状态:所有进行中、暂停中等状态
const needConfirmStates = [
'start', // 开始检测
'process', // 检测进行中
'test_init', // 初始化中
'paused', // 已暂停(用户可能想要继续)
'paused_ing' // 暂停中
]
if (safeExitStates.includes(ActiveStatue.value)) {
handleClose() // 安全状态直接关闭
} else {
beforeClose() // 需要确认的状态
}
}
/**
* 处理暂停检测
*/
const handlePause = () => {
sendPause() // 发送暂停指令
testRef.value?.handlePause() // 调用正式检测组件的暂停方法
}
/**
* 发送暂停指令
*/
const sendPause = () => {
TestStatus.value = 'paused_ing' // 设置为暂停中状态
pauseTest() // 调用暂停API
}
/**
* 发送继续检测指令
*/
const sendResume = () => {
// 调用继续检测API
resumeTest({
userPageId: JwtUtil.getLoginName(),
devIds: checkStore.devices.map(item => item.deviceId),
planId: checkStore.plan.id,
reCheckType: '2', // 操作类型0-系数校验1-预检测2-正式检测8-不合格项复检
userId: userStore.userInfo.id,
temperature: checkStore.temperature,
humidity: checkStore.humidity
})
// 发送继续成功消息给子组件
Object.assign(webMsgSend.value, {
requestId: 'Resume_Success'
})
}
/**
* 关闭WebSocket连接
*/
const closeWebSocket = () => {
try {
if (dataSocket.socketServe) {
// 先清理回调
dataSocket.socketServe.unRegisterCallBack?.('aaa')
// 再关闭连接
if (dataSocket.socketServe.connected) {
dataSocket.socketServe.closeWs()
}
// 清空引用
dataSocket.socketServe = null
}
} catch (error) {
console.error('WebSocket关闭失败:', error)
// 强制清空引用,确保下次能正常重新连接
dataSocket.socketServe = null
}
}
// ====================== 步骤切换处理 ======================
/**
* 进入下一步检测 - 复杂的步骤切换逻辑
*/
const nextStep = () => {
// 如果已到最后或遇到错误状态,直接关闭弹窗
if (
stepsActiveIndex.value == stepsTotalNum.value + 1 ||
ActiveStatue.value === 'error' ||
ActiveStatue.value === 'connect_timeout' ||
ActiveStatue.value === 'pause_timeout'
) {
handleClose()
return
}
// 如果不是错误状态,进行步骤切换
if (ActiveStatue.value != 'error') {
ActiveStatue.value = 'waiting' // 重置为等待状态
let tempStep = stepsActiveIndex.value
let idx = 1
stepsActiveIndex.value++ // 步骤索引递增
// 遍历检测项,找到下一个需要执行的步骤
for (let selectTestItemsKey in checkStore.selectTestItems) {
if (
tempStep == 0 &&
checkStore.selectTestItems[selectTestItemsKey as keyof typeof checkStore.selectTestItems]
) {
// 找到下一个要执行的检测项
stepsActiveView.value = idx // 设置显示的组件
stepsActive.value = idx // 设置业务逻辑步骤
return
}
if (
checkStore.selectTestItems[selectTestItemsKey as keyof typeof checkStore.selectTestItems] &&
tempStep != 0
) {
tempStep-- // 跳过已选择的检测项
}
idx++
}
}
}
/**
* 处理步骤点击 - 允许点击回到之前已完成的步骤
* @param step 点击的步骤序号
*/
const handleStepClick = (step: number) => {
if (step > stepsActive.value) {
return // 不能点击未完成的步骤
} else {
stepsActiveView.value = step // 切换到点击的步骤显示
}
}
// ====================== 数据清理和关闭处理 ======================
/**
* 清理所有数据状态
*/
function clearData() {
stepsTotalNum.value = -1
stepsActiveIndex.value = 1
stepsActiveView.value = -1
preTestStatus.value = 'waiting'
timeTestStatus.value = 'waiting'
channelsTestStatus.value = 'waiting'
TestStatus.value = 'waiting'
ActiveStatue.value = 'waiting'
nextStepText.value = '下一步'
}
/**
* 关闭前确认处理
*/
const beforeClose = () => {
// 如果检测未完成且不是错误状态,需要用户确认
if (stepsActiveIndex.value < stepsTotalNum.value && ActiveStatue.value != 'error') {
ElMessageBox.confirm('检测未完成,是否退出当前检测流程?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
handleClose() // 用户确认后关闭
})
} else {
handleClose() // 直接关闭
}
}
/**
* 关闭弹窗并清理资源
*/
const handleClose = () => {
showSteps.value = false // 隐藏步骤条
closeWebSocket() // 统一通过closeWebSocket函数关闭连接避免重复关闭
dialogVisible.value = false // 隐藏弹窗
clearData() // 清理所有状态数据
showComponent.value = false // 隐藏检测组件
emit('quitClicked') // 触发退出事件通知父组件
}
// ====================== 生命周期处理 ======================
/**
* 组件卸载前的清理工作
* 确保路由切换或组件销毁时正确关闭WebSocket连接
*/
onBeforeUnmount(() => {
closeWebSocket() // 组件销毁前关闭WebSocket连接
})
// ====================== 对外暴露的方法 ======================
defineExpose({ open }) // 只暴露open方法供父组件调用
</script>
<style scoped lang="scss">
.test-head-steps {
:deep(.el-step) {
.el-step__head.is-success {
color: #91cc75;
}
.el-step__title.is-success {
color: #91cc75;
}
}
}
:deep(.dialog-big .el-dialog__body) {
max-height: 840px !important;
}
.steps-container :deep(.test-head-steps) {
height: 80px;
margin-bottom: 10px;
}
.steps-container :deep(.el-step__title) {
font-size: 20px !important; /* 设置标题字体大小 */
vertical-align: baseline !important;
display: inline-block; /* 确保文字和图标在同一行 */
line-height: 1; /* 调整行高以确保底部对齐 */
}
.steps-container :deep(.el-step__icon-inner) {
font-size: 18px !important;
vertical-align: baseline !important;
display: inline-block; /* 确保文字和图标在同一行 */
line-height: 1; /* 调整行高以确保底部对齐 */
}
.steps-container :deep(.el-step__icon) {
font-size: 18px !important;
vertical-align: baseline !important;
display: inline-block; /* 确保文字和图标在同一行 */
line-height: 1; /* 调整行高以确保底部对齐 */
}
.loading-box {
animation: loading 1.5s linear infinite;
}
@keyframes loading {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>