From 4a31c7d028d8125ccdad35dc4cc22ac265616eaa Mon Sep 17 00:00:00 2001 From: dhr <2216804034@qq.com> Date: Sun, 28 Sep 2025 17:23:00 +0800 Subject: [PATCH] 0928 --- src/views/zhinengxunjian/shiyanjilu.vue | 90 +++++- src/views/zhinengxunjian/shiyanrenwu.vue | 380 ++++++++++++++++++----- 2 files changed, 382 insertions(+), 88 deletions(-) diff --git a/src/views/zhinengxunjian/shiyanjilu.vue b/src/views/zhinengxunjian/shiyanjilu.vue index 68a859a..31d7711 100644 --- a/src/views/zhinengxunjian/shiyanjilu.vue +++ b/src/views/zhinengxunjian/shiyanjilu.vue @@ -64,16 +64,40 @@

本月完成试验

{{ statData.completed - }} - 较上月 {{ statData.completedGrowth >= 0 ? '↑' : '↓' }}{{ Math.abs(statData.completedGrowth) }}% + }} + 较上月 + {{ + Math.abs(statData.completedGrowth - 100) < 0.01 + ? '无增长' + : (statData.completedGrowth >= 0 ? '↑' : '↓') + Math.abs(statData.completedGrowth) + '%' + }}

试验通过率

- {{ statData.passRate }}% - 较上月 {{ statData.passRateGrowth >= 0 ? '↑' : '↓' }}{{ Math.abs(statData.passRateGrowth) }}% + {{ statData.passRate }}% + 较上月 + {{ + Math.abs(statData.passRateGrowth - 100) < 0.01 + ? '无增长' + : (statData.passRateGrowth >= 0 ? '↑' : '↓') + Math.abs(statData.passRateGrowth) + '%' + }}

@@ -85,8 +109,20 @@

平均试验时长

{{ statData.avgDuration - }} - 较上月 {{ statData.avgDurationGrowth >= 0 ? '↑' : '↓' }}{{ Math.abs(statData.avgDurationGrowth) }}分钟 + }} + 较上月 + {{ + Math.abs(statData.avgDurationGrowth - 100) < 0.01 + ? '无增长' + : (statData.avgDurationGrowth <= 0 ? '↓' : '↑') + Math.abs(statData.avgDurationGrowth) + '%' + }}

@@ -134,11 +170,11 @@

- {{ record.status === 'failed' ? '失败原因分析' : '试验结果' }} + {{ record.status === '3' ? '失败原因分析' : '试验结果' }}

- {{ record.status === 'failed' ? record.failReason || '未提供失败原因' : record.testFinal || '试验完成,未提供详细结果' }} + {{ record.status === '3' ? record.failReason || '未提供失败原因' : record.testFinal || '试验未完成,未提供详细结果' }}

@@ -435,6 +471,9 @@ const getStatisticsData = async () => { // 处理增长率数据 statData.value.completedGrowth = parseInt(apiData.finishCountAdd) || 0; statData.value.passRateGrowth = parseFloat(apiData.passValueAdd) || 0; + + // 对于平均试验时长,时长减少是好的,所以我们需要反转逻辑 + // 这里直接使用从API获取的增长率值,但在显示时根据正负来判断样式 statData.value.avgDurationGrowth = parseFloat(apiData.averageTestTimeAdd) || 0; } else { console.warn('获取统计数据失败或返回格式不正确:', response); @@ -470,8 +509,14 @@ const formatDateTime = (dateTimeString) => { // 12. 辅助方法:获取节点状态类名 const getNodeStatusClass = (nodeStatus, recordStatus) => { - // 节点状态: 2-未完成, 其他假设为已完成 + // 节点状态: 2-未完成, 3-失败, 其他假设为已完成 // 记录状态: 'failed'-失败, 'completed'-完成, 其他为进行中 + + // 如果节点本身状态为3(失败),直接返回failed类名 + if (nodeStatus === '3') { + return 'failed'; + } + if (recordStatus === 'failed') { // 如果记录失败,找到失败阶段的节点 return nodeStatus === '2' ? 'failed' : 'active'; @@ -486,6 +531,12 @@ const getNodeStatusClass = (nodeStatus, recordStatus) => { // 13. 辅助方法:获取进度线状态类名 const getLineStatusClass = (index, nodes, recordStatus) => { // 如果记录失败,找到第一个未完成的节点前的线为active + + // 检查当前节点状态是否为3(失败) + if (nodes[index].status === '3') { + return 'failed'; + } + if (recordStatus === 'failed') { return nodes[index].status !== '2' ? 'active' : 'failed'; } else if (recordStatus === 'completed') { @@ -962,6 +1013,21 @@ onMounted(async () => { color: #ff7d00; } +.stat-change.green { + background-color: #e6ffed; + color: #00b42a; +} + +.stat-change.red { + background-color: #fff1f0; + color: #f5222d; +} + +.stat-change.gray { + background-color: #f5f5f5; + color: #999; +} + /* 13. 试验记录样式 */ .test-records { display: flex; @@ -1053,17 +1119,17 @@ onMounted(async () => { } .progress-step.active .step-number { - background-color: #165dff; + background-color: #00b42a; color: white; } .progress-step.active .step-name { - color: #165dff; + color: #00b42a; font-weight: 500; } .progress-line.active { - background-color: #165dff; + background-color: #00b42a; } .progress-step.failed .step-number { diff --git a/src/views/zhinengxunjian/shiyanrenwu.vue b/src/views/zhinengxunjian/shiyanrenwu.vue index a9e5866..9dec2e3 100644 --- a/src/views/zhinengxunjian/shiyanrenwu.vue +++ b/src/views/zhinengxunjian/shiyanrenwu.vue @@ -72,49 +72,83 @@

-
- 计划时间 - {{ task.planTime }} -
-
- 测试对象 - {{ task.target }} -
-
- 执行人 - {{ task.executor }} -
-
- 关联计划 - {{ task.relatedPlan }} -
- - -
- 延期原因 - {{ task.delayReason || '未填写' }} -
- - -
- 完成进度 -
- + +
+
+ 失败时间 + {{ task.failTime || '未记录' }} +
+
+ 试验阶段 + {{ task.testStage || '未记录' }} +
+
+ 执行人 + {{ task.executor }} +
+
+ 失败原因 + {{ task.originalData?.failReason || '未填写' }}
- -
- 结果 - {{ task.result }} + +
+
+ 计划时间 + {{ task.planTime }} +
+
+ 测试对象 + {{ task.target }} +
+
+ 执行人 + {{ task.executor }} +
+
+ 关联计划 + {{ task.relatedPlan }} +
+ + +
+ 延期原因 + {{ task.delayReason || '未填写' }} +
+ + +
+ 完成进度 +
+ +
+
+ + +
+ 结果 + {{ task.result }} +
- 详情 - - {{ task.actionText }} - + +
+ 详情 + + {{ task.actionText }} + +
+ + +
+ 详情 + + {{ task.actionText }} + +
@@ -372,6 +406,29 @@ + + + +
+
+
+
{{ log.timestamp || '-' }}
+
{{ log.content || '-' }}
+
+
+
+ +
+
+
+ +
+ +
@@ -385,7 +442,12 @@ import { shiyanlist } from '@/api/zhinengxunjian/shiyan'; import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index'; import { addjiedian } from '@/api/zhinengxunjian/jiedian/index'; // 引入Element Plus组件(提示/空状态/骨架屏/弹窗) -import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox } from 'element-plus'; +import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox, ElDialog } from 'element-plus'; + +// 日志弹窗相关变量 +const logsDialogVisible = ref(false); +const logsData = ref([]); +const logsLoading = ref(false); /** * 根据任务ID获取完整的任务详情数据 @@ -696,9 +758,9 @@ const mapApiToView = (apiData) => { }, '3': { statusText: '失败', - cardClass: 'card-delayed', - tagClass: 'tag-delayed', - actionText: '重试', + cardClass: 'card-failed', + tagClass: 'tag-failed', + actionText: '重新执行', actionClass: 'reschedule-btn', result: '失败', resultClass: 'result-abnormal' @@ -755,6 +817,46 @@ const mapApiToView = (apiData) => { executorName = getUserById(apiData.person); } + // 格式化失败时间 + const formatFailTime = (timeStr) => { + if (timeStr) { + const date = new Date(timeStr); + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String( + date.getHours() + ).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; + } + return '未记录'; + }; + + // 生成试验阶段信息 + const getTestStage = () => { + try { + // 优先查找nodes数组中status为2的第一条数据 + if (apiData && apiData.nodes && Array.isArray(apiData.nodes)) { + const firstStatusTwoNode = apiData.nodes.find((node) => { + // 确保node存在且有status属性 + if (!node || node.status === undefined) return false; + // 处理status可能是字符串或数字的情况 + return node.status === '2' || node.status === 2; + }); + if (firstStatusTwoNode && firstStatusTwoNode.name) { + return firstStatusTwoNode.name; + } + } + // 如果没有找到符合条件的nodes数据,检查是否有明确的试验阶段信息 + if (apiData && apiData.testStage) { + return apiData.testStage; + } + // 如果没有明确的阶段信息,尝试从关联计划中获取 + if (apiData && apiData.testPlan && apiData.testPlan.stage) { + return apiData.testPlan.stage; + } + } catch (error) { + console.error('获取试验阶段信息失败:', error); + } + return '未记录'; + }; + return { id: apiData.id, // 任务ID(v-for的key,唯一标识) title: apiData.taskName || '未命名任务', // 任务名称 @@ -774,7 +876,10 @@ const mapApiToView = (apiData) => { actionText: statusConfig.actionText, actionClass: statusConfig.actionClass, testFinal: apiData.testFinal, // 结果(用于详情页) - originalData: apiData // 保存原始数据,用于后续操作 + originalData: apiData, // 保存原始数据,用于后续操作 + // 失败卡片特有字段 + failTime: formatFailTime(apiData.failTime), + testStage: getTestStage() }; }; @@ -880,11 +985,44 @@ const handleAction = async (task) => { resultType = 'normal'; // 现在在外部作用域中定义 } catch (error) { if (error === 'cancel') { - // 用户点击取消(异常) - updateParams.status = '5'; - updateParams.progress = 100; - updateParams.testFinal = '异常'; - resultType = 'abnormal'; // 现在在外部作用域中定义 + // 用户点击取消(异常),弹出失败原因输入框 + try { + const failReasonResult = await ElMessageBox.prompt('请输入失败原因', '试验异常', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + inputPlaceholder: '请详细描述失败原因...', + inputValidator: (value) => { + if (!value || value.trim() === '') { + return '失败原因不能为空'; + } + return true; + } + }); + + // 用户输入了失败原因并确认 + updateParams.status = '3'; + updateParams.progress = ''; + updateParams.testFinal = '异常'; + updateParams.failReason = failReasonResult.value; // 绑定失败原因参数 + updateParams.failTime = formatLocalDateTime(new Date()); // 记录失败时间 + resultType = 'abnormal'; + + // 将第一条未完成的步骤状态改为3(失败) + if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) { + const firstUnfinishedNode = taskDetails.nodes.find((node) => { + return node.status === '2' || node.status === 2; + }); + if (firstUnfinishedNode) { + firstUnfinishedNode.status = '3'; + // 确保更新到updateParams中 + updateParams.nodes = taskDetails.nodes; + } + } + } catch (innerError) { + // 用户取消了失败原因输入 + return; + } } else { // 关闭弹窗,不执行操作 return; @@ -895,50 +1033,51 @@ const handleAction = async (task) => { switch (task.status) { case '1': // 待执行 → 开始执行(状态改为4) updateParams.status = '4'; - updateParams.progress = 10; // 初始进度10% - // 设置开始时间为当前时间 - updateParams.planBeginTime = new Date().toISOString().slice(0, 16).replace('T', ' '); + updateParams.progress = 0; // 初始进度10% + // 设置开始时间为当前时间(使用本地时间而非UTC时间) + updateParams.planBeginTime = formatLocalDateTime(new Date()); break; case '2': // 已延期 → 重新安排(状态改为1,重置时间) updateParams.status = '1'; - updateParams.beginTime = new Date().toISOString().slice(0, 16).replace('T', ' '); + updateParams.beginTime = formatLocalDateTime(new Date()); break; case '3': // 失败 → 重试(状态改为1) updateParams.status = '1'; + // 清空失败相关字段,使用适合各字段数据类型的默认值 + updateParams.failReason = ''; + updateParams.failTime = ''; // 时间类型字段使用null + updateParams.failPhase = ''; // 整数类型字段使用0 + + // 将失败的步骤状态改回2(未完成) + if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) { + taskDetails.nodes.forEach((node) => { + if (node.status === '3' || node.status === 3) { + node.status = '2'; + } + }); + // 确保更新到updateParams中 + updateParams.nodes = taskDetails.nodes; + } break; default: return; } } - // 调用更新接口 + // 对于执行中状态('4')的任务,预先设置好时间字段 + if (task.status === '4') { + // 根据结果类型设置相应的时间(使用本地时间而非UTC时间) + if (resultType === 'normal') { + updateParams.planFinishTime = formatLocalDateTime(new Date()); + } else if (resultType === 'abnormal') { + updateParams.failTime = formatLocalDateTime(new Date()); + } + } + + // 调用更新接口(只调用一次) const response = await updatesyrenwu(updateParams); if (response.code === 200) { ElMessage.success(`任务${task.actionText}成功`); - - // 只有在接口调用成功后才设置时间 - if (task.status === '4') { - // 获取最新的任务详情,确保包含所有字段 - const latestTaskDetails = await getTaskDetails(task.id); - if (latestTaskDetails) { - // 创建包含所有字段的新参数对象 - const timeUpdateParams = { - ...latestTaskDetails, - id: task.id - }; - - // 根据结果类型设置相应的时间(现在resultType已在作用域内) - if (resultType === 'normal') { - timeUpdateParams.planFinishTime = new Date().toISOString().slice(0, 16).replace('T', ' '); - } else if (resultType === 'abnormal') { - timeUpdateParams.failTime = new Date().toISOString().slice(0, 16).replace('T', ' '); - } - - // 再次调用接口更新时间 - await updatesyrenwu(timeUpdateParams); - } - } - getTaskList(); // 刷新任务列表 } else { ElMessage.error(`任务${task.actionText}失败:` + response.msg); @@ -948,6 +1087,20 @@ const handleAction = async (task) => { } }; +/** + * 格式化本地日期时间为 'YYYY-MM-DD HH:mm' 格式 + * @param {Date} date - 日期对象 + * @returns {string} 格式化后的日期时间字符串 + */ +const formatLocalDateTime = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}`; +}; + /** * 打开创建任务弹窗 */ @@ -1049,7 +1202,7 @@ const handleSaveTask = async () => { progress: 0, // 初始进度0% failReason: '', failTime: '', // 失败时间(新增时为空) - failPhase: 0, + failPhase: '', faileAnalyze: '', faileTips: '', testLongTime: 0, @@ -1267,6 +1420,10 @@ const getTaskStatusClass = (status) => { box-shadow: 0 4px 16px rgba(82, 196, 26, 0.15); } +.card-failed { + box-shadow: 0 4px 16px rgba(255, 77, 79, 0.15); +} + /* 左侧状态线颜色 */ .card-pending::before { background-color: #1677ff; @@ -1280,6 +1437,9 @@ const getTaskStatusClass = (status) => { .card-completed::before { background-color: #52c41a; } +.card-failed::before { + background-color: #ff4d4f; +} /* 卡片悬停效果 */ .task-card:hover { @@ -1336,6 +1496,12 @@ const getTaskStatusClass = (status) => { border-color: #b7eb8f; } +.tag-failed { + background-color: #fff2f0; + color: #ff4d4f; + border-color: #ffccc7; +} + .task-details { margin-bottom: 16px; } @@ -1419,6 +1585,28 @@ const getTaskStatusClass = (status) => { color: #165dff; } +/* 失败卡片特殊样式 */ +.failed-task-details { + margin-bottom: 16px; +} + +.failed-reason-item { + padding-top: 8px; + border-top: 1px dashed #f0f2f5; +} + +.failed-reason { + color: #f53f3f; + font-weight: 500; +} + +.failed-task-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; +} + /* 分页区域样式 */ .pagination-section { display: flex; @@ -1472,6 +1660,46 @@ const getTaskStatusClass = (status) => { margin-bottom: 30px; } +/* 日志弹窗样式 */ +.logs-container { + max-height: 400px; + overflow-y: auto; +} + +.logs-list { + padding: 10px 0; +} + +.log-item { + padding: 12px 0; + border-bottom: 1px solid #f0f2f5; +} + +.log-item:last-child { + border-bottom: none; +} + +.log-time { + font-size: 12px; + color: #86909c; + margin-bottom: 4px; +} + +.log-content { + font-size: 14px; + color: #1d2129; + line-height: 1.6; +} + +.no-logs { + text-align: center; + padding: 60px 0; +} + +.log-skeleton { + margin: 12px 0; +} + /* 任务详情弹窗样式 */ .task-detail-container { max-height: 600px;