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;