This commit is contained in:
dhr
2025-09-28 17:23:00 +08:00
parent 3f07f7afe3
commit 4a31c7d028
2 changed files with 382 additions and 88 deletions

View File

@ -64,16 +64,40 @@
<p class="stat-label">本月完成试验</p>
<p class="stat-value">
{{ statData.completed
}}<span class="stat-change" :class="statData.completedGrowth >= 0 ? 'up' : 'down'">
较上月 {{ statData.completedGrowth >= 0 ? '↑' : '↓' }}{{ Math.abs(statData.completedGrowth) }}%
}}<span
class="stat-change"
:class="{
'green': statData.completedGrowth > 0,
'gray': Math.abs(statData.completedGrowth - 100) < 0.01,
'red': statData.completedGrowth < 0
}"
>
较上月
{{
Math.abs(statData.completedGrowth - 100) < 0.01
? '无增长'
: (statData.completedGrowth >= 0 ? '↑' : '↓') + Math.abs(statData.completedGrowth) + '%'
}}
</span>
</p>
</div>
<div class="stat-card">
<p class="stat-label">试验通过率</p>
<p class="stat-value">
{{ statData.passRate }}%<span class="stat-change" :class="statData.passRateGrowth >= 0 ? 'up' : 'down'">
较上月 {{ statData.passRateGrowth >= 0 ? '↑' : '↓' }}{{ Math.abs(statData.passRateGrowth) }}%
{{ statData.passRate }}%<span
class="stat-change"
:class="{
'green': statData.passRateGrowth > 0,
'gray': Math.abs(statData.passRateGrowth - 100) < 0.01,
'red': statData.passRateGrowth < 0
}"
>
较上月
{{
Math.abs(statData.passRateGrowth - 100) < 0.01
? '无增长'
: (statData.passRateGrowth >= 0 ? '↑' : '↓') + Math.abs(statData.passRateGrowth) + '%'
}}
</span>
</p>
</div>
@ -85,8 +109,20 @@
<p class="stat-label">平均试验时长</p>
<p class="stat-value">
{{ statData.avgDuration
}}<span class="stat-change" :class="statData.avgDurationGrowth >= 0 ? 'down' : 'up'">
较上月 {{ statData.avgDurationGrowth >= 0 ? '↑' : '↓' }}{{ Math.abs(statData.avgDurationGrowth) }}分钟
}}<span
class="stat-change"
:class="{
'green': statData.avgDurationGrowth > 100, // 数据大于100上升时显示绿色
'gray': Math.abs(statData.avgDurationGrowth - 100) < 0.01,
'red': statData.avgDurationGrowth < 100 // 数据小于100下降时显示红色
}"
>
较上月
{{
Math.abs(statData.avgDurationGrowth - 100) < 0.01
? '无增长'
: (statData.avgDurationGrowth <= 0 ? '↓' : '↑') + Math.abs(statData.avgDurationGrowth) + '%'
}}
</span>
</p>
</div>
@ -134,11 +170,11 @@
<!-- 试验结果 -->
<div class="test-result" :class="{ 'failure-analysis': record.status === 'failed' }">
<h4 class="result-title">
{{ record.status === 'failed' ? '失败原因分析' : '试验结果' }}
{{ record.status === '3' ? '失败原因分析' : '试验结果' }}
</h4>
<p class="result-content">
{{ record.status === 'failed' ? record.failReason || '未提供失败原因' : record.testFinal || '试验完成,未提供详细结果' }}
{{ record.status === '3' ? record.failReason || '未提供失败原因' : record.testFinal || '试验完成,未提供详细结果' }}
</p>
<p class="result-details" v-if="record.status !== 'failed'">
@ -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 {

View File

@ -72,6 +72,28 @@
</div>
<div class="task-details">
<!-- 失败卡片特殊展示 -->
<div v-if="task.status === '3'" class="failed-task-details">
<div class="detail-item">
<span class="detail-label">失败时间</span>
<span class="detail-value">{{ task.failTime || '未记录' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">试验阶段</span>
<span class="detail-value">{{ task.testStage || '未记录' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行人</span>
<span class="detail-value">{{ task.executor }}</span>
</div>
<div class="detail-item failed-reason-item">
<span class="detail-label">失败原因</span>
<span class="detail-value failed-reason">{{ task.originalData?.failReason || '未填写' }}</span>
</div>
</div>
<!-- 其他状态的卡片展示 -->
<div v-else>
<div class="detail-item">
<span class="detail-label">计划时间</span>
<span class="detail-value">{{ task.planTime }}</span>
@ -103,19 +125,31 @@
</div>
</div>
<!-- 已完成/失败结果 -->
<div v-if="task.status === '5' || task.status === '3'" class="task-result">
<!-- 已完成结果 -->
<div v-if="task.status === '5'" class="task-result">
<span class="detail-label">结果</span>
<span class="detail-value" :class="task.resultClass">{{ task.result }}</span>
</div>
</div>
</div>
<div class="task-actions">
<!-- 失败卡片的特殊操作按钮 -->
<div v-if="task.status === '3'" class="failed-task-actions">
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
</div>
<!-- 其他状态的操作按钮 -->
<div v-else>
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
</div>
</div>
</div>
</div>
@ -372,6 +406,29 @@
</span>
</template>
</el-dialog>
<!-- 日志弹窗 -->
<el-dialog v-model="logsDialogVisible" title="任务执行日志" width="700px" :close-on-click-modal="false">
<div v-if="!logsLoading" class="logs-container">
<div v-if="logsData.length > 0" class="logs-list">
<div v-for="(log, index) in logsData" :key="index" class="log-item">
<div class="log-time">{{ log.timestamp || '-' }}</div>
<div class="log-content">{{ log.content || '-' }}</div>
</div>
</div>
<div v-else class="no-logs">
<el-empty description="暂无执行日志" />
</div>
</div>
<div v-else class="loading-logs">
<el-skeleton :count="5" class="log-skeleton" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="logsDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
@ -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, // 任务IDv-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;
// 用户点击取消(异常),弹出失败原因输入框
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 = '异常';
resultType = 'abnormal'; // 现在在外部作用域中定义
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;