Files
pqs-9100_client/frontend/src/views/home/components/test.vue
2025-11-05 15:35:54 +08:00

1331 lines
46 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>
<div>
<!-- 主检测界面 -->
<div class='dialog' v-bind='dialogBig'>
<!-- 标题栏显示时间进度条日志按钮 -->
<div class='dialog-title'>
<!-- 检测用时显示 -->
<div class='timeView'>
<el-icon style='margin: 0px 5px;'>
<Clock />
</el-icon>
<span>检测用时{{ timeView }}</span>
</div>
<!-- 检测进度条 -->
<el-progress
style='width: 82%; margin-right: 3%;'
:percentage='percentage'
:color='customColors' />
<!-- 检测项进度按钮 -->
<el-button style='width: 10%' type='text' :icon='InfoFilled' @click='showTestLog'>检测项进度</el-button>
</div>
<!-- 检测结果表格内容区 -->
<div class='dialog-content'>
<!-- 检测结果表格 -->
<el-table :data='checkResultView' row-key='scriptType' height='450px'
:header-cell-style="{ background: 'var(--el-color-primary)', color: '#eee', textAlign: 'center' } "
style='width: 100%'
border>
<!-- 固定列检测项目名称 -->
<el-table-column fixed prop='scriptName' label='检测项目' width='150px' align='center'>
</el-table-column>
<!-- 动态列当通道数不超过最大限制时显示详细通道 -->
<template v-if='chnSum<=MAX_CHN_SUM'>
<!-- 遍历设备列表 -->
<el-table-column v-for='(item,index1) in deviceList' :key='item.deviceId' :label='item.deviceName'
:min-width='110' align='center'>
<!-- 遍历每个设备的通道 -->
<el-table-column v-for='(chnItem,index2) in item.chnNum' :key='`${item.deviceId}${chnItem}`'
:label="'通道'+chnItem" align='center'>
<template #default='{row}'>
<!-- 通道状态提示 -->
<el-tooltip
:content="row.devices[index1].chnResult[index2].icon==='More'?'暂无数据'
:row.devices[index1].chnResult[index2].icon==='CircleCheckFilled'?'符合'
:row.devices[index1].chnResult[index2].icon==='Close'?'不符合'
:row.devices[index1].chnResult[index2].icon==='WarnTriangleFilled'?'数据异常'
:row.devices[index1].chnResult[index2].icon==='Loading'?'检测中':'连接中断'"
placement='right'>
<!-- 通道状态按钮 -->
<el-button
:disabled='row.devices[index1].chnResult[index2].color===CheckData.ButtonColorEnum.INFO || row.devices[index1].chnResult[index2].color===CheckData.ButtonColorEnum.LOADING'
:color='row.devices[index1].chnResult[index2].color'
size='small'
@click='handleClick(item,chnItem,row.scriptType)'
style='align-self: center;'
>
<!-- 加载状态图标 -->
<el-icon v-if="row.devices[index1].chnResult[index2].icon==='Loading'" class='loading-box'
style='color: #fff'>
<component :is='Loading' />
</el-icon>
<!-- 其他状态图标 -->
<el-icon v-else style='color: #fff'>
<component :is='row.devices[index1].chnResult[index2].icon' />
</el-icon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table-column>
</template>
</el-table>
</div>
</div>
<!-- 检测日志抽屉容器 -->
<div class='drawer-container'>
<!-- 从下往上弹出的检测日志抽屉 -->
<el-drawer v-model='drawer' title='检测项进度' direction='btt' :size="'38%'">
<!-- 日志内容滚动容器 -->
<div ref='scrollContainerRef' style='height: 100%; overflow-y: auto;'>
<!-- 日志列表根据类型显示不同颜色 -->
<p v-for='(item, index) in testLogList'
:key='index'
:style="{color:item.type==='error'?'#F56C6C':item.type==='warning'?'#e6a23c':'var(--el-text-color-regular)'}">
{{ item.log }}<br />
</p>
</div>
</el-drawer>
</div>
<!-- 单通道单测试项详情弹窗 -->
<dataCheckSingleChannelSingleTestPopup ref='dataCheckSingleChannelSingleTestPopupRef' />
</div>
</template>
<script lang='tsx' setup name='test'>
// Element Plus 图标导入
import { InfoFilled, Loading } from '@element-plus/icons-vue'
// 单通道单测试项详情弹窗组件
import dataCheckSingleChannelSingleTestPopup from './dataCheckSingleChannelSingleTestPopup.vue'
// Vue 3 Composition API
import { computed, reactive, ref, toRef, watch } from 'vue'
// 对话框大小绑定工具
import { dialogBig } from '@/utils/elementBind'
// 检测数据类型定义
import { CheckData } from '@/api/check/interface'
// 检测相关状态管理
import { useCheckStore } from '@/stores/modules/check'
// Element Plus 消息组件
import { ElMessage, ElMessageBox } from 'element-plus'
// 检测相关API
import { getBigTestItem } from '@/api/check/test'
import { getAutoGenerate } from '@/api/user/login'
import { generateDevReport } from '@/api/plan/plan'
import { useModeStore } from '@/stores/modules/mode' // 引入模式 store
import { useDictStore } from '@/stores/modules/dict'
// 获取检测状态管理实例
const checkStore = useCheckStore()
const modeStore = useModeStore()
const dictStore = useDictStore()
// 最大通道数限制,超过此数量将使用汇总显示
const MAX_CHN_SUM = 12
// 总测试项数
let checkTotal = 0
// 组件属性定义
const props = defineProps({
// 测试状态waiting/start/process/paused/success等
testStatus: {
type: String,
default: 'waiting',
},
// 当前步骤索引
stepsActive: {
type: Number,
},
// WebSocket消息数据
webMsgSend: {
type: Object,
default: () => ({}),
},
})
// 定义向父组件发送的事件
const emit = defineEmits(['update:testStatus', 'update:webMsgSend', 'sendPause', 'sendResume', 'closeWebSocket'])
// ========== 界面状态相关 ==========
// 测试项进度抽屉是否打开
const drawer = ref(false)
// 进度条颜色配置
const customColors = [{ color: '#91cc75', percentage: 100 }]
// ========== 检测数据相关 ==========
// 检测脚本配置数据
let scriptData: CheckData.ScriptItem[] = []
// 被检测的设备列表
const deviceList = reactive<CheckData.Device[]>([])
// 当前正在执行的测试项索引
let activeIndex = 0
// 检测进度百分比
const percentage = ref(0)
// ========== 时间计算相关 ==========
// 定时器实例
let timer: any = null
// 总计时秒数
const timeCount = ref(0)
// 格式化的时间显示HH:MM:SS
const timeView = ref('00:00:00')
// 测试项开始检测时间(或继续检测时间)
const startData = ref(new Date())
// 测试项检测结束时间(或暂停时的时间)
const endData = ref(new Date())
// 累计时间差(毫秒)
const timeDifference = ref(0)
// ========== 检测结果相关 ==========
// 详细检测结果数据(详细到每个设备的每个通道)
const checkResult = reactive<CheckData.ScriptChnItem[]>([])
// 存储检测失败的测试项信息(只要有一个通道不合格,该测试项就被记录)
let errorCheckItem: Array<{ scriptType: string, type: CheckData.ChnCheckResultEnum }> = []
// 检测日志列表
const testLogList = reactive<CheckData.LogItem[]>([{ type: 'info', log: '暂无数据,等待检测开始' }])
// ========== 响应式引用 ==========
// 将props转为ref便于watch监听
const testStatus = toRef(props, 'testStatus')
const webMsgSend = toRef(props, 'webMsgSend')
// ========== DOM引用 ==========
// 日志滚动容器引用
const scrollContainerRef = ref()
// 单通道单测试项详情弹窗组件引用
const dataCheckSingleChannelSingleTestPopupRef = ref<InstanceType<typeof dataCheckSingleChannelSingleTestPopup>>()
// ========== 计算属性 ==========
// 计算所有设备的总通道数
const chnSum = computed(() => {
let sum = 0
deviceList.forEach((item) => {
sum += item.chnNum
})
return sum
})
// 将原始检测结果转换为表格展示所需的视图数据
const checkResultView: ComputedRef<CheckData.ScriptChnViewItem[]> = computed(() => {
let result: CheckData.ScriptChnViewItem[] = checkResult.map(item => {
let temp: CheckData.ScriptChnViewItem = {
scriptType: item.scriptType,
scriptName: item.scriptName,
devices: [],
}
// 遍历每个设备,生成对应的按钮显示数据
item.devices.forEach(device => {
let tempChnBtnResult: CheckData.ButtonResult[] = []
// 只有在总通道数不超过最大限制时,才显示详细通道
if (chnSum.value <= MAX_CHN_SUM) {
// 为每个通道生成对应的按钮配置(颜色+图标)
for (let j = 0; j < device.chnResult.length; j++) {
switch (device.chnResult[j]) {
case CheckData.ChnCheckResultEnum.UNKNOWN:
tempChnBtnResult.push({ color: CheckData.ButtonColorEnum.INFO, icon: 'More' })
break
case CheckData.ChnCheckResultEnum.LOADING:
tempChnBtnResult.push({ color: CheckData.ButtonColorEnum.LOADING, icon: 'Loading' })
break
case CheckData.ChnCheckResultEnum.SUCCESS:
tempChnBtnResult.push({ color: CheckData.ButtonColorEnum.SUCCESS, icon: 'CircleCheckFilled' })
break
case CheckData.ChnCheckResultEnum.FAIL:
tempChnBtnResult.push({ color: CheckData.ButtonColorEnum.DANGER, icon: 'Close' })
break
case CheckData.ChnCheckResultEnum.TIMEOUT:
tempChnBtnResult.push({ color: CheckData.ButtonColorEnum.WARNING, icon: 'Link' })
break
case CheckData.ChnCheckResultEnum.ERRORDATA:
tempChnBtnResult.push({ color: CheckData.ButtonColorEnum.WARNING, icon: 'WarnTriangleFilled' })
break
case CheckData.ChnCheckResultEnum.NOT_PART_IN_ERROR:
tempChnBtnResult.push({ color: CheckData.ButtonColorEnum.INFO, icon: 'Minus' })
break
default:
break
}
}
}
temp.devices.push({
deviceId: device.deviceId,
deviceName: device.deviceName,
chnResult: tempChnBtnResult,
})
})
return temp
})
return result
})
// ========== 监听器 ==========
// 监听测试状态变化
watch(testStatus, function(newValue, oldValue) {
// 开始测试
if (newValue == 'start') {
// 根据测试项配置决定是否需要初始化
if (!checkStore.selectTestItems.preTest && !checkStore.selectTestItems.channelsTest) {
ElMessage.success('初始化开始!')
emit('update:testStatus', 'test_init')
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:初始化开始!` })
} else {
// 直接进入检测流程
emit('update:testStatus', 'process')
}
// 开始计时
startTimeCount()
// 显示测试日志抽屉
showTestLog()
// 重置时间记录
startData.value = new Date()
timeDifference.value = 0
}
// 需要停止计时的状态
if (newValue == 'recheck' || newValue == 'error' || newValue == 'test_init_fail' || newValue == 'connect_timeout' || newValue == 'pause_timeout' || oldValue == 'error_flow_end') {
stopTimeCount()
}
})
// 防重复处理计数器
let count = 0
// 监听WebSocket消息变化处理各种检测状态和错误
watch(webMsgSend, function(newValue, oldValue) {
// 只在非等待状态下处理消息
if (testStatus.value !== 'waiting') {
// 步骤4正式检测阶段的消息处理
if (props.stepsActive === 4) {
// ========== 初始化错误码处理 ==========
if (newValue.code == 10520) {
ElMessageBox.alert('报文解析异常!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:报文解析异常!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10521) {
ElMessageBox.alert('程控源參数有误!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:程控源參数有误!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10522) {
ElMessageBox.alert('测试项解析有误!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:测试项解析有误!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10523) {
if (count === 0) {
ElMessageBox.alert('源连接失败!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:源连接失败!` })
emit('update:testStatus', 'test_init_fail')
count++
}
} else if (newValue.code == 10524) {
ElMessageBox.alert('获取源控制权失败!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:获取源控制权失败!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10525) {
ElMessageBox.alert('重置源失败!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:重置源失败!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10527) {
ElMessageBox.alert('源未进行初始化!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:源未进行初始化!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10528) {
ElMessageBox.alert('目标源有误(该用户已控制其他源,在关闭前无法操作新的源)', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({
type: 'error',
log: `${new Date().toLocaleString()}:目标源有误(该用户已控制其他源,在关闭前无法操作新的源)`,
})
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10529) {
ElMessageBox.alert('源状态有误,无法响应报文(例如源处于输出状态,无法响应初始化报文)', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({
type: 'error',
log: `${new Date().toLocaleString()}:源状态有误,无法响应报文(例如源处于输出状态,无法响应初始化报文)`,
})
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10550) {
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}${newValue.data}设备连接异常!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10551) {
ElMessageBox.alert(`${newValue.data}设备触发报告异常!`, '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}${newValue.data}设备触发报告异常!` })
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10552) {
ElMessageBox.alert('存在已经初始化步骤,执行自动关闭,请重新发起检测', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({
type: 'error',
log: `${new Date().toLocaleString()}:存在已经初始化步骤,执行自动关闭,请重新发起检测!`,
})
emit('update:testStatus', 'test_init_fail')
} else {
// ========== 根据请求ID处理不同类型的消息 ==========
switch (newValue.requestId) {
// 服务端错误
case 'server_error':
if (newValue.operateCode === 'server_error' && count === 0) {
ElMessageBox.alert('服务端主动关闭连接!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:服务端主动关闭连接!` })
emit('update:testStatus', 'test_init_fail')
count++
}
break
// 设备端错误
case 'device_error':
if (newValue.operateCode === 'device_error' && count === 0) {
ElMessageBox.alert('设备端主动关闭连接!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:设备端主动关闭连接!` })
emit('update:testStatus', 'test_init_fail')
count++
}
break
// 正式测试相关消息
case 'formal_real':
switch (newValue.operateCode) {
case 'stop_timeout':
ElMessageBox.alert(`暂停时间已过10分钟本次检测已失效请重新发起检测`, '暂停时间过长', {
confirmButtonText: '确定',
type: 'error',
})
emit('update:testStatus', 'pause_timeout')
break
}
break
case 'error_flow_end':
ElMessageBox.alert(`当前流程存在异常结束!`, '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
emit('update:testStatus', 'test_init_fail')
break
case 'socket_timeout':
ElMessageBox.alert('连接超时!', '连接超时', {
confirmButtonText: '确定',
type: 'error',
})
emit('update:testStatus', 'connect_timeout')
break
case 'connect':
switch (newValue.operateCode) {
case 'Source':
ElMessageBox.alert('源通讯失败,请检查源连接情况!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:源通讯失败!` })
break
case 'Dev':
ElMessageBox.alert('设备通讯失败,请检查设备连接情况!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:设备通讯失败!` })
break
}
emit('update:testStatus', 'test_init_fail')
break
// 源通信校验阶段
case 'yjc_ytxjy':
switch (newValue.operateCode) {
case 'INIT_GATHER':
if (newValue.code == 10200) {
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:源初始化成功!` })
percentage.value = 1
}
if (newValue.code == -1) {
ElMessageBox.alert('源未知异常!', '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:源未知异常!` })
emit('update:testStatus', 'test_init_fail')
}
break
}
break
// 设备通信校验阶段
case 'yjc_sbtxjy':
switch (newValue.operateCode) {
case 'INIT_GATHER$01':
if (newValue.code == 25001) {
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:设备通讯校验成功!` })
percentage.value = 2
}
break
}
break
// 协议校验阶段
case 'yjc_xyjy':
switch (newValue.operateCode) {
case 'VERIFY_MAPPING$01':
if (newValue.code == 25001) {
ElMessage.success('初始化成功!')
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:协议校验成功!` })
timeDifference.value = +new Date().getTime() - startData.value.getTime()
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:初始化成功!` })
percentage.value = 3
// 初始化完成,准备开始正式检测
activeIndex = getNextActiveIndex() + 2
emit('update:testStatus', 'process')
} else if (newValue.code == 25002) {
let data = JSON.parse(newValue.data)
ElMessageBox.alert(`脚本与icd校验失败icd名称${data['icdType']} -> 校验项:${data['dataType']}`, '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({
type: 'error',
log: `${new Date().toLocaleString()}脚本与icd校验失败icd名称${data['icdType']} -> 校验项:${data['dataType']}`,
})
emit('update:testStatus', 'test_init_fail')
} else if (newValue.code == 10500) {
ElMessageBox.alert(`装置中未找到该icd`, '初始化失败', {
confirmButtonText: '确定',
type: 'error',
})
testLogList.push({
type: 'error',
log: `${new Date().toLocaleString()}装置中未找到该icd`,
})
emit('update:testStatus', 'test_init_fail')
}
break
}
break
// 暂停测试成功
case 'preStopTest':
if (newValue.operateCode == 'stop') {
ElMessage.success('暂停成功')
emit('update:testStatus', 'paused')
pauseSuccessCallback()
}
break
// 继续测试成功
case 'Resume_Success':
ElMessage.success('开始继续检测')
emit('update:testStatus', 'process')
handleResumeTest()
break
// ========== 各类检测项的开始和结束消息处理 ==========
// 频率检测
case 'FREQ_Start':
handleStartItem('FREQ', newValue.desc)
break
case 'FREQ_End':
handleEndItem('FREQ', newValue.desc, newValue.data)
break
case 'V_Start':
handleStartItem('V', newValue.desc)
break
case 'V_End':
handleEndItem('V', newValue.desc, newValue.data)
break
case 'P_Start':
handleStartItem('P', newValue.desc)
break
case 'P_End':
handleEndItem('P', newValue.desc, newValue.data)
break
case 'HV_Start':
handleStartItem('HV', newValue.desc)
break
case 'HV_End':
handleEndItem('HV', newValue.desc, newValue.data)
break
case 'HI_Start':
handleStartItem('HI', newValue.desc)
break
case 'HI_End':
handleEndItem('HI', newValue.desc, newValue.data)
break
case 'HP_Start':
handleStartItem('HP', newValue.desc)
break
case 'HP_End':
handleEndItem('HP', newValue.desc, newValue.data)
break
case 'HSV_Start':
handleStartItem('HSV', newValue.desc)
break
case 'HSV_End':
handleEndItem('HSV', newValue.desc, newValue.data)
break
case 'HSI_Start':
handleStartItem('HSI', newValue.desc)
break
case 'HSI_End':
handleEndItem('HSI', newValue.desc, newValue.data)
break
case 'VOLTAGE_Start':
handleStartItem('VOLTAGE', newValue.desc)
break
case 'VOLTAGE_End':
handleEndItem('VOLTAGE', newValue.desc, newValue.data)
break
case 'I_Start':
handleStartItem('I', newValue.desc)
break
case 'I_End':
handleEndItem('I', newValue.desc, newValue.data)
break
case 'IMBV_Start':
handleStartItem('IMBV', newValue.desc)
break
case 'IMBV_End':
handleEndItem('IMBV', newValue.desc, newValue.data)
break
case 'IMBA_Start':
handleStartItem('IMBA', newValue.desc)
break
case 'IMBA_End':
handleEndItem('IMBA', newValue.desc, newValue.data)
break
case 'F_Start':
handleStartItem('F', newValue.desc)
break
case 'F_End':
handleEndItem('F', newValue.desc, newValue.data)
break
// 检测结束
case 'Quit':
break
}
}
}
}
}, { deep: true })
// ========== 检测项处理函数 ==========
// 处理检测项开始消息
const handleStartItem = (code: string, desc: string | undefined) => {
if (desc === undefined) {
// 大测试项开始
activeIndex = getActiveIndex(code)
updateCheckResultView(code, true) // 更新界面为加载状态
updateLog(true) // 记录开始日志
} else {
// 子测试项开始
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}${desc}准确度检测:开始` })
}
}
// 处理检测项结束消息
const handleEndItem = (code: string, desc: string | undefined, devices: CheckData.DeviceCheckResult[] = []) => {
if (desc === undefined) {
// 大测试项结束
updatePercentage() // 更新进度
updateCheckResultView(code, false, devices) // 更新检测结果
updateLog(false) // 记录结束日志
if (testStatus.value != 'paused') {
// 如果未暂停,继续下一个测试项
activeIndex = getNextActiveIndex(code)
}
} else {
// 子测试项结束,根据结果记录不同类型的日志
let result = getResult(devices)
if (result === 1) {
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}${desc}检测结束:符合` })
}
if (result === 2) {
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}${desc}检测结束:不符合` })
}
if (result === 4) {
testLogList.push({ type: 'warning', log: `${new Date().toLocaleString()}${desc}检测结束:数据异常` })
}
}
}
// 更新检测进度
const updatePercentage = async () => {
if (activeIndex < checkTotal) {
// 计算并更新进度百分比
percentage.value = Math.trunc(activeIndex / checkTotal * 100)
} else {
// 所有检测项完成
percentage.value = 100
// 先完成所有后续操作再emit success
try {
// 检查是否需要自动生成报告
let { data: autoGenerate } = await getAutoGenerate()
if (autoGenerate == 1) {
// 自动生成报告
let devIdList = checkStore.devices.map(item => {
return item.deviceId
})
await generateDevReport({
'planId': checkStore.plan.id,
'devIdList': devIdList,
'scriptId': checkStore.plan.scriptId,
'planCode': checkStore.plan.code + ''
})
}
// 提示检测完成
await ElMessageBox.alert('检测全部结束,你可以停留在此页面查看检测结果,或返回首页进行复检、报告生成和归档等操作', '检测完成', {
confirmButtonText: '确定',
})
// 关闭WebSocket连接
emit('closeWebSocket')
} finally {
// 最后才emit success避免中断后续操作
emit('update:testStatus', 'success')
}
}
}
// ========== 生命周期钩子 ==========
// 组件挂载前初始化数据
onBeforeMount(async () => {
await initScriptData() // 获取检测脚本配置
initDeviceList() // 初始化设备列表
initCheckResult() // 初始化检测结果数据结构
})
// ========== 界面交互函数 ==========
// 显示测试日志抽屉
const showTestLog = () => {
drawer.value = true
}
// ========== 数据初始化函数 ==========
// 从服务器获取检测脚本配置数据
const initScriptData = async () => {
const pattern = dictStore.getDictData('Pattern').find(item => item.name === modeStore.currentMode)?.id ?? '';
// 根据复检类型、计划ID、设备ID列表获取测试项
let response: any = await getBigTestItem({
reCheckType: checkStore.reCheckType,
planId: checkStore.plan.id,
devIds: checkStore.devices.map(item => item.deviceId),
patternId: pattern
})
// 格式化脚本数据
let temp = response.data.map((item: any) => {
return {
...item,
scriptName: item.scriptName,
}
})
// 保存脚本数据并设置总数
scriptData.push(...temp)
checkTotal = scriptData.length
}
// 从store中获取设备列表数据
const initDeviceList = () => {
Object.assign(deviceList, checkStore.devices)
}
// 初始化检测结果数据结构(每个测试项的每个设备的每个通道)
const initCheckResult = () => {
let result: CheckData.ScriptChnItem[] = []
// 为每个检测脚本创建结果数据结构
scriptData.forEach(item => {
let temp: CheckData.ScriptChnItem = {
scriptType: item.id,
scriptName: item.scriptName,
devices: [],
}
// 为每个设备创建通道结果
for (let i = 0; i < deviceList?.length; i++) {
let tempChnResult: CheckData.ChnCheckResultEnum[] = []
// 初始化所有通道状态为UNKNOWN
for (let j = 0; j < deviceList[i].chnNum; j++) {
tempChnResult.push(CheckData.ChnCheckResultEnum.UNKNOWN)
}
temp.devices.push({
deviceId: deviceList[i].deviceId,
deviceName: deviceList[i].deviceName,
chnResult: tempChnResult,
})
}
result.push(temp)
})
Object.assign(checkResult, result)
}
// ========== 数据更新函数 ==========
// 更新指定测试项的检测结果
const updateCheckResult = (data: CheckData.ScriptChnItem) => {
const { scriptType } = { ...data }
// 找到对应的测试项并更新其结果
checkResult.forEach(item => {
if (item.scriptType == scriptType) {
// 更新每个设备的通道结果
for (let i = 0; i < item.devices.length; i++) {
let targetDevice = data.devices.find(dev => dev.deviceId === item.devices[i].deviceId)
if (targetDevice !== undefined) {
// 深拷贝结果数据
item.devices[i].chnResult = [...targetDevice.chnResult]
}
}
}
})
}
// 滚动日志到底部
const scrollToBottom = () => {
if (scrollContainerRef.value) {
setTimeout(() => {
scrollContainerRef.value.scrollTop = scrollContainerRef.value.scrollHeight + 70
},10)
}
}
// ========== 工具函数 ==========
// 生成随机整数0到max-1
function getRandomInt(max: number): number {
return Math.floor(Math.random() * max)
}
// 将毫秒时间差转换为可读的时间格式
function getTimeDifference(timeDifference: number): string {
// 时间单位常量
const millisecondsPerDay = 1000 * 60 * 60 * 24
const millisecondsPerHour = 1000 * 60 * 60
const millisecondsPerMinute = 1000 * 60
const millisecondsPerSecond = 1000
// 计算各时间单位的值
const days = Math.floor(timeDifference / millisecondsPerDay)
const hours = Math.floor((timeDifference % millisecondsPerDay) / millisecondsPerHour)
const minutes = Math.floor((timeDifference % millisecondsPerHour) / millisecondsPerMinute)
const seconds = Math.floor((timeDifference % millisecondsPerMinute) / millisecondsPerSecond)
// 根据时间长度返回合适的格式
if (days > 0) {
return `: ${days} 天, ${hours} 小时, ${minutes} 分钟, ${seconds}`
} else if (hours > 0) {
return `: ${hours} 小时, ${minutes} 分钟, ${seconds}`
} else {
return `: ${minutes} 分钟, ${seconds}`
}
}
// 监听日志列表变化,自动滚动到底部
watch(testLogList, () => {
scrollToBottom()
}, { deep: true })
// ========== 日志记录函数 ==========
// 更新检测日志(记录检测项的开始和结束)
const updateLog = (isStart: boolean) => {
const currentTime = ref(new Date().toLocaleString())
let timeDifferenceItem = 0
// 确保在有效的测试项范围内
if (activeIndex <= checkTotal) {
if (isStart) {
// 记录测试项开始
startData.value = new Date()
testLogList.push({
type: 'info',
log: currentTime.value + ` ${scriptData[activeIndex - 1].scriptName}准确度检测:开始`,
})
} else {
// 记录测试项结束
endData.value = new Date()
timeDifferenceItem = endData.value.getTime() - startData.value.getTime()
timeDifference.value += timeDifferenceItem
// 根据检测结果记录不同类型的日志
let errorItem = getErrorCheckItem(scriptData[activeIndex - 1].id)
switch (errorItem?.type) {
case CheckData.ChnCheckResultEnum.SUCCESS:
testLogList.push({
type: 'info',
log: currentTime.value + ` ${scriptData[activeIndex - 1].scriptName}准确度检测结束:符合,用时` + getTimeDifference(timeDifferenceItem),
})
break
case CheckData.ChnCheckResultEnum.FAIL:
testLogList.push({
type: 'error',
log: currentTime.value + ` ${scriptData[activeIndex - 1].scriptName}准确度检测结束:不符合,用时` + getTimeDifference(timeDifferenceItem),
})
break
case CheckData.ChnCheckResultEnum.TIMEOUT:
testLogList.push({
type: 'warning',
log: currentTime.value + ` ${scriptData[activeIndex - 1].scriptName}准确度检测结束:连接超时,用时` + getTimeDifference(timeDifferenceItem),
})
break
case CheckData.ChnCheckResultEnum.ERRORDATA:
testLogList.push({
type: 'warning',
log: currentTime.value + ` ${scriptData[activeIndex - 1].scriptName}准确度检测结束:数据异常,用时` + getTimeDifference(timeDifferenceItem),
})
break
}
// 更新总时间显示
timeView.value = secondToTime(timeDifference.value / 1000)
// 如果是最后一个测试项,记录总结日志
if (activeIndex === checkTotal) {
testLogList.push({
type: 'info',
log: currentTime.value + ' :检测结束,总用时' + getTimeDifference(timeDifference.value),
})
stopTimeCount()
}
}
}
}
// ========== 错误处理函数 ==========
// 设置测试项错误状态(分析所有通道结果,确定整体状态)
const setErrorCheckItem = (scriptType: string, devices: CheckData.DeviceCheckResult[]) => {
let type = 1 // 默认为成功
let tempChnResult: CheckData.ChnCheckResultEnum[] = []
// 收集所有设备所有通道的结果
for (let i = 0; i < devices.length; i++) {
tempChnResult.push(...devices[i].chnResult)
}
// 按优先级确定整体状态(数据异常 > 失败 > 超时)
if (tempChnResult.some(item => item === CheckData.ChnCheckResultEnum.ERRORDATA)) {
type = CheckData.ChnCheckResultEnum.ERRORDATA
}
if (tempChnResult.some(item => item === CheckData.ChnCheckResultEnum.FAIL)) {
type = CheckData.ChnCheckResultEnum.FAIL
}
if (tempChnResult.some(item => item === CheckData.ChnCheckResultEnum.TIMEOUT)) {
type = CheckData.ChnCheckResultEnum.TIMEOUT
// 处理超时情况:记录日志并弹出提示
for (let i = 0; i < devices.length; i++) {
if (devices[i].chnResult.some(item => item === CheckData.ChnCheckResultEnum.TIMEOUT)) {
testLogList.push({
type: 'warning',
log: `${new Date().toLocaleString()} ${devices[i].deviceName}连接超时`,
})
ElMessageBox.alert('连接超时!', '连接超时', {
confirmButtonText: '确定',
type: 'error',
})
emit('update:testStatus', 'connect_timeout')
}
}
}
// 记录测试项的错误状态
errorCheckItem.push({ scriptType, type: type })
}
// 获取指定测试项的错误状态
// 判断该检测项是否全部合格(所有设备所有通道所有子检测项目全部合格为合格,否则为不合格)
function getErrorCheckItem(scriptType: string) {
let results = errorCheckItem.filter((item) => item.scriptType === scriptType)
if (results.length > 0) {
return results[0]
} else {
return null
}
}
// 更新检测结果视图
const updateCheckResultView = (scriptCode: string, isStart: boolean, devices: CheckData.DeviceCheckResult[] = []) => {
// 根据代码找到对应的测试项ID
let scriptType = scriptData.filter(item => item.code === scriptCode)[0]?.id
let temp = null
if (isStart) {
// 开始时设置为加载状态
temp = getLoadingResult(scriptType)
} else {
// 结束时设置实际结果并记录错误状态
setErrorCheckItem(scriptType, devices)
temp = {
scriptType,
devices,
}
}
updateCheckResult(temp)
}
// 生成加载状态的结果数据(所有通道显示为正在检测)
const getLoadingResult = (scriptType: string) => {
let devices = []
// 为每个设备的每个通道设置LOADING状态
devices = deviceList.map(item => {
let tempChnResult: CheckData.ChnCheckResultEnum[] = []
for (let i = 0; i < item.chnNum; i++) {
tempChnResult.push(CheckData.ChnCheckResultEnum.LOADING)
}
return {
deviceId: item.deviceId,
deviceName: item.deviceName,
chnResult: tempChnResult,
}
})
let tempScriptChnItem: CheckData.ScriptChnItem = {
scriptType,
devices,
}
return tempScriptChnItem
}
// 分析设备检测结果,返回整体状态
const getResult = (devices: CheckData.DeviceCheckResult[] = []) => {
let type = 1 // 默认成功
let tempChnResult: CheckData.ChnCheckResultEnum[] = []
// 收集所有设备的所有通道结果
for (let i = 0; i < devices.length; i++) {
tempChnResult.push(...devices[i].chnResult)
}
// 按优先级确定结果类型(数据异常 > 失败)
if (tempChnResult.some(item => item === CheckData.ChnCheckResultEnum.ERRORDATA)) {
type = CheckData.ChnCheckResultEnum.ERRORDATA
}
if (tempChnResult.some(item => item === CheckData.ChnCheckResultEnum.FAIL)) {
type = CheckData.ChnCheckResultEnum.FAIL
}
return type
}
// ========== 事件处理函数 ==========
// 点击通道按钮,查看检测详情
// 参数:设备信息,通道号(-1代表查看全部通道测试项类型
const handleClick = (item: any, chnNum: number, scriptType: string) => {
// 查找对应的检测结果
let checkResultItem = checkResult.find(obj => obj.scriptType === scriptType)
let flag = -1 // -1:正常, 0:超时, 1:数据异常
if (checkResultItem) {
let device = checkResultItem.devices.find(obj => obj.deviceId === item.deviceId)
if (device) {
let chnResult = device.chnResult
if (chnNum === -1) {
// 查看全部通道的状态
if (chnResult.findIndex(obj => obj === CheckData.ChnCheckResultEnum.TIMEOUT) !== -1) {
flag = 0
}
if (chnResult.findIndex(obj => obj === CheckData.ChnCheckResultEnum.ERRORDATA) !== -1) {
flag = 1
}
} else {
// 查看特定通道的状态
if (chnResult[chnNum - 1] === CheckData.ChnCheckResultEnum.TIMEOUT) {
flag = 0
}
if (chnResult[chnNum - 1] === CheckData.ChnCheckResultEnum.ERRORDATA) {
flag = 1
}
}
}
}
// 根据状态进行不同处理
if (flag === 0) {
// 超时状态:显示提示信息
ElMessageBox.alert('连接超时,请检查设备通讯是否正常', '连接超时', {
confirmButtonText: '确定',
type: 'warning',
})
}
if (flag === -1 || flag === 1) {
// 正常或数据异常:打开详情弹窗
checkStore.setShowDetailType(2)
dataCheckSingleChannelSingleTestPopupRef.value?.open(item.deviceId, chnNum + '', scriptType)
}
}
// ========== 暂停/继续相关函数 ==========
// 处理暂停请求
const handlePause = () => {
testLogList.push({
type: 'error',
log: `${new Date().toLocaleString()}:当前测试小项正在执行中,将在该小项执行结束后暂停...`,
})
}
// 暂停成功回调
const pauseSuccessCallback = () => {
// 记录暂停时的时间
endData.value = new Date()
let diffTime = endData.value.getTime() - startData.value.getTime()
timeDifference.value += diffTime
// 记录暂停日志
testLogList.push({
type: 'info',
log: `${new Date().toLocaleString()}:暂停检测`,
})
stopTimeCount()
}
// 处理继续检测
const handleResumeTest = () => {
// 重新开始计时
startData.value = new Date()
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:开始重新检测!` })
resumeTimeCount()
}
// ========== 测试项索引管理函数 ==========
// 根据测试项代码获取对应的索引号从1开始
const getActiveIndex = (code: string): number => {
for (let i = 0; i < scriptData.length; i++) {
if (scriptData[i].code === code) {
return i + 1
}
}
return -1
}
// 根据当前测试项代码获取下一个测试项的索引号
const getNextActiveIndex = (code: string = ''): number => {
if (code === '') {
return -1
}
for (let i = 0; i < scriptData.length; i++) {
if (scriptData[i].code === code) {
return i + 2 // 下一个测试项索引
}
}
return -1
}
// ========== 时间计数器管理函数 ==========
// 开始计时
const startTimeCount = () => {
if (!timer) {
timer = setInterval(() => {
timeCount.value = timeCount.value + 1
timeView.value = secondToTime(timeCount.value)
}, 1000)
}
}
// 停止计时
const stopTimeCount = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
// 恢复计时(用于暂停后继续)
const resumeTimeCount = () => {
timer = setInterval(() => {
timeCount.value = timeCount.value + 1
timeView.value = secondToTime(timeCount.value)
}, 1000)
}
// 将秒数转换为 HH:MM:SS 格式
const secondToTime = (second: number) => {
let h: string | number = Math.floor(second / 3600) // 小时
let m: string | number = Math.floor((second - h * 3600) / 60) // 分钟
let s: string | number = Math.floor(second % 60) // 秒
// 补齐前导零
h = h < 10 ? '0' + h : h
m = m < 10 ? '0' + m : m
s = s < 10 ? '0' + s : s
return h + ':' + m + ':' + s
}
// 组件卸载前清理定时器和响应式引用
onBeforeUnmount(() => {
// 清理定时器
if (timer) {
clearInterval(timer)
timer = null
}
// 清理响应式数组引用,防止内存泄漏
deviceList.splice(0)
checkResult.splice(0)
testLogList.splice(0)
errorCheckItem.splice(0)
// 重置其他状态
scriptData.splice(0)
activeIndex = 0
checkTotal = 0
count = 0
})
// 暴露给父组件的方法
defineExpose({
handlePause, // 暂停方法
})
</script>
<!-- ========== 样式定义 ========== -->
<style scoped lang='scss'>
/* 表格样式 */
:deep(.el-table .header-row) {
background-color: #f5f7fa;
}
:deep(.el-table .warning-row) {
color: red;
}
.el-table .success-row {
--el-table-tr-bg-color: var(--el-color-success-light-9);
}
/* 主对话框容器 */
.dialog {
display: flex;
flex-direction: column;
overflow-y: hidden;
overflow-x: hidden;
}
/* 对话框标题栏 */
.dialog-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 10px;
margin-bottom: 10px;
/* 时间显示区域 */
.timeView {
display: flex;
align-items: center;
color: #91cc75;
width: 28%;
margin-right: 0px;
text-align: left;
font-size: 26px;
font-weight: bold;
}
}
/* 对话框内容区 */
.dialog-content {
max-height: 450px;
overflow-y: hidden;
}
/* 折叠面板头部样式 */
:deep(.el-collapse-item__header) {
height: 30px;
}
/* 日志显示区域 */
.dialog-log {
height: 50px;
overflow-y: hidden;
p {
margin: 5px 0;
font-size: 14px;
}
}
/* 抽屉容器样式 */
.drawer-container {
:deep(header.el-drawer__header) {
color: #fff !important;
background-color: var(--el-color-primary) !important;
.el-drawer__close-btn svg:hover {
color: #ccc !important;
}
.el-drawer__title {
color: #fff !important;
}
}
}
/* 加载动画样式 */
.loading-box {
animation: loading 1.5s linear infinite;
}
@keyframes loading {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
<style lang='scss' scoped>
/* 表格按钮样式 */
:deep(.el-button--small) {
height: 20px !important;
width: 20px !important;
}
/* 表格单元格内边距 */
:deep(.el-table--default td ) {
padding: 5px 0 !important;
}
</style>