Files
maintenance_system/src/views/zhinengxunjian/shiyanjilu.vue
2025-09-28 18:54:52 +08:00

1287 lines
37 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="operation-inspection">
<!-- 顶部导航选项卡 -->
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab active" @click="handleInspection3">试验管理</div>
<div class="nav-tab" @click="handleInspection4">报修管理</div>
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div> -->
<!-- 选项卡和按钮组合 -->
<div class="tabs-wrapper">
<div style="display: flex; align-items: center; gap: 10px">
<el-button type="primary" @click="handleInspectionManagement1">实验计划</el-button>
<el-button type="primary" @click="handleInspectionManagement2">实验任务</el-button>
<el-button type="primary" @click="handleInspectionManagement3">实验记录</el-button>
</div>
</div>
<!-- 筛选和操作区域 -->
<div class="filter-and-actions">
<div class="filters">
<el-select v-model="filterStatus" placeholder="巡检状态" clearable>
<el-option label="全部状态" value="all"></el-option>
<el-option label="正常" value="normal"></el-option>
<el-option label="需关注" value="attention"></el-option>
<el-option label="有问题" value="problem"></el-option>
</el-select>
<el-select v-model="filterType" placeholder="巡检类型" clearable>
<el-option label="全部类型" value="all"></el-option>
<el-option label="数据库" value="database"></el-option>
<el-option label="服务器" value="server"></el-option>
<el-option label="网络设备" value="network"></el-option>
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
class="date-picker"
></el-date-picker>
<el-button icon="Search" type="primary" class="search-btn"> 搜索 </el-button>
</div>
</div>
<!-- 主内容区 -->
<div class="content-container">
<!-- 试验记录 -->
<div v-if="activeTab === 'record'" class="record-container">
<h2 class="section-title">试验记录与报告</h2>
<p class="section-subtitle">截止至 {{ currentDate }}</p>
<!-- 统计卡片 -->
<div class="stat-grid">
<div class="stat-card">
<p class="stat-label">本月完成试验</p>
<p class="stat-value">
{{ statData.completed
}}<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="{
'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>
<div class="stat-card">
<p class="stat-label">待分析记录</p>
<p class="stat-value">{{ statData.pendingAnalysis }}<span class="stat-change warning">需要及时处理</span></p>
</div>
<div class="stat-card">
<p class="stat-label">平均试验时长</p>
<p class="stat-value">
{{ statData.avgDuration
}}<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>
</div>
<!-- 试验记录列表 -->
<div class="test-records">
<!-- 动态生成试验记录卡片 -->
<div
v-for="(record, recordIndex) in testRecords"
:key="record.id"
class="test-record-card"
:class="{ 'passed': record.status === 'completed', 'failed': record.status === 'failed' }"
>
<div class="record-header">
<h3 class="record-title">{{ record.taskName || '试验任务' }}</h3>
<p class="record-date">
开始时间
{{ formatDate(record.beginTime) }}
<span class="record-time">计划完成时间: {{ record.planFinishTime ? formatDate(record.planFinishTime) : '未知' }}</span>
</p>
<span class="status-tag" :class="getStatusClass(record.status)">
{{ getStatusText(record.status) }}
</span>
</div>
<!-- 动态生成试验进度步骤条 -->
<div class="test-progress" v-if="record.nodes && record.nodes.length">
<template v-for="(node, index) in sortedNodes(record.nodes)" :key="node.id">
<div class="progress-step" :class="getNodeStatusClass(node.status, record.status)">
<div class="step-number">{{ node.code }}</div>
<div class="step-name">步骤名称{{ node.name }}</div>
<div class="step-name">预期试验目的{{ node.intendedPurpose }}</div>
</div>
<!-- 进度线最后一个节点没有线 -->
<div
v-if="index < sortedNodes(record.nodes).length - 1"
class="progress-line"
:class="getLineStatusClass(index, sortedNodes(record.nodes), record.status)"
></div>
</template>
</div>
<!-- 试验结果 -->
<div class="test-result" :class="{ 'failure-analysis': record.status === 'failed' }">
<h4 class="result-title">
{{ record.status === '3' ? '失败原因分析' : '试验结果' }}
</h4>
<p class="result-content">
{{ record.status === '3' ? record.failReason || '未提供失败原因' : record.testFinal || '试验未完成,未提供详细结果' }}
</p>
<p class="result-details" v-if="record.status !== 'failed'">
计划时间: {{ formatDate(record.planBeginTime) }} | 进度: {{ record.progress }}% | 负责人:
{{ record.personInfo?.userName || '未知' }}
</p>
<!-- 改进建议仅失败时显示 -->
<div class="improvement-suggestion" v-if="record.status === 'failed' && record.faileTips">
<i class="fas fa-lightbulb"></i>
<p>建议: {{ record.faileTips }}</p>
</div>
</div>
<div class="record-actions">
<button class="operate-btn view-btn" @click="handleViewDetail(record)">查看详情</button>
<button class="operate-btn report-btn">生成报告</button>
</div>
</div>
<!-- 无数据提示 -->
<div v-if="!testRecords.length" class="no-records">暂无试验记录数据</div>
</div>
</div>
<!-- 巡检计划表格 -->
<div v-if="activeTab === 'plan'" class="table-container">
<el-table :data="planTableData" border>
<el-table-column prop="name" label="计划名称" width="220">
<template #default="scope">
<div class="plan-name">{{ scope.row.name }}</div>
</template>
</el-table-column>
<el-table-column prop="type" label="巡检类型" width="120"></el-table-column>
<el-table-column prop="cycle" label="巡检周期" width="120"></el-table-column>
<el-table-column prop="dateRange" label="执行时间范围"></el-table-column>
<el-table-column prop="progress" label="完成进度" width="120">
<template #default="scope">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: scope.row.progress + '%', backgroundColor: getProgressColor(scope.row.status) }"></div>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<span :class="['status-tag', `status-${scope.row.status}`]">
{{ getStatusText(scope.row.status) }}
</span>
</template>
</el-table-column>
<el-table-column prop="responsible" label="负责人" width="120"></el-table-column>
<el-table-column label="操作" width="220">
<template #default="scope">
<div class="operation-buttons">
<button class="operate-btn edit-btn" v-if="['drafted', 'paused'].includes(scope.row.status)">编辑</button>
<button class="operate-btn execute-btn" v-if="scope.row.status === 'drafted'">执行</button>
<button class="operate-btn pause-btn" v-if="scope.row.status === 'in-progress'">暂停</button>
<button class="operate-btn resume-btn" v-if="scope.row.status === 'paused'">恢复</button>
<button class="operate-btn view-btn" @click="handleViewDetail(scope.row)">查看详情</button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 巡检任务表格 -->
<div v-if="activeTab === 'task'" class="table-container">
<el-table :data="taskTableData" border>
<el-table-column prop="name" label="任务名称" width="220"></el-table-column>
<el-table-column prop="planName" label="所属计划" width="180"></el-table-column>
<el-table-column prop="type" label="巡检类型" width="120"></el-table-column>
<el-table-column prop="target" label="巡检对象" width="150"></el-table-column>
<el-table-column prop="deadline" label="截止时间" width="160"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<span :class="['status-tag', `status-${scope.row.status}`]">
{{ getTaskStatusText(scope.row.status) }}
</span>
</template>
</el-table-column>
<el-table-column prop="executor" label="执行人" width="120"></el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<div class="operation-buttons">
<button class="operate-btn accept-btn" v-if="scope.row.status === 'pending'">接受</button>
<button class="operate-btn complete-btn" v-if="scope.row.status === 'accepted'">完成</button>
<button class="operate-btn view-btn" @click="handleViewDetail(scope.row)">查看详情</button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 分页 -->
<div class="pagination" v-if="activeTab !== 'record'">
<p class="total-records">显示1到{{ pageSize }}{{ totalRecords }}条记录</p>
<el-pagination
layout="prev, pager, next, jumper, sizes"
:total="totalRecords"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
@current-change="handlePageChange"
@size-change="handleSizeChange"
></el-pagination>
</div>
<!-- 详情弹窗 -->
<el-dialog v-model="detailDialogVisible" title="任务详情" width="800px" :close-on-click-modal="false" center>
<div v-if="detailData" class="task-detail-container">
<div class="detail-card">
<h3 class="card-title">基本信息</h3>
<div class="info-row">
<span class="info-label">任务名称</span>
<span class="info-value">{{ detailData.taskName }}</span>
<span class="info-label">任务状态</span>
<span class="info-value" :class="getStatusClass(detailData.status)">
{{ getStatusText(detailData.status) }}
</span>
</div>
<div class="info-row">
<span class="info-label">测试对象</span>
<span class="info-value">{{ detailData.testObject }}</span>
<span class="info-label">完成进度</span>
<span class="info-value">{{ detailData.progress }}%</span>
</div>
<div class="info-row">
<span class="info-label">开始时间</span>
<span class="info-value">{{ detailData.beginTime }}</span>
<span class="info-label">结束时间</span>
<span class="info-value">{{ detailData.endTime }}</span>
</div>
<div class="info-row">
<span class="info-label">时间信息</span>
<span class="info-value">{{ detailData.timeInfo ? detailData.timeInfo.replace(/,/g, '—') : '-' }}</span>
</div>
</div>
<div class="detail-card">
<h3 class="card-title">执行人信息</h3>
<div v-if="detailData.personInfo" class="info-row">
<span class="info-label">执行人姓名</span>
<span class="info-value">{{ detailData.personInfo.userName }}</span>
<span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.personInfo.phonenumber }}</span>
</div>
<div v-if="detailData.personInfo" class="info-row">
<span class="info-label">性别</span>
<span class="info-value">{{ detailData.personInfo.sex === '1' ? '男' : '女' }}</span>
</div>
</div>
<div class="detail-card">
<h3 class="card-title">关联计划</h3>
<div v-if="detailData.testPlan" class="info-row">
<span class="info-label">计划名称</span>
<span class="info-value">{{ detailData.testPlan.planName }}</span>
<span class="info-label">计划编号</span>
<span class="info-value">{{ detailData.testPlan.planCode }}</span>
</div>
<div v-if="detailData.testPlan" class="info-row">
<span class="info-label">计划时间</span>
<span class="info-value">{{ detailData.testPlan.beginTime }} {{ detailData.testPlan.endTime }}</span>
</div>
<div v-if="detailData.testPlan && detailData.testPlan.testDevice" class="info-row">
<span class="info-label">测试设备</span>
<span class="info-value">{{ detailData.testPlan.testDevice }}</span>
</div>
</div>
<div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card">
<h3 class="card-title">执行步骤</h3>
<div class="steps-container">
<div v-for="(node, index) in detailData.nodes" :key="node.id || index" class="step-item">
<div class="step-number">{{ node.code || index + 1 }}</div>
<div class="step-info">
<div class="step-name">{{ node.name || '未命名步骤' }}</div>
<div class="step-purpose">{{ node.intendedPurpose || '无说明' }}</div>
<div class="step-time">计划时间{{ formatDateTime(node.intendedTime) }}</div>
<div v-if="node.finishTime" class="step-finish-time">完成时间{{ formatDateTime(node.finishTime) }}</div>
<div v-if="node.remark" class="step-remark">备注{{ node.remark }}</div>
</div>
<div class="step-status" :class="getStatusClass(node.status)">
{{ node.status === '2' ? '未完成' : '已完成' }}
</div>
</div>
</div>
</div>
<div v-if="detailData.testFinal || detailData.failReason" class="detail-card">
<h3 class="card-title">执行结果</h3>
<div v-if="detailData.testFinal" class="info-row">
<span class="info-label">测试结果</span>
<span class="info-value">{{ detailData.testFinal }}</span>
</div>
<div v-if="detailData.failReason" class="info-row">
<span class="info-label">失败原因</span>
<span class="info-value fail-reason">{{ detailData.failReason }}</span>
</div>
</div>
</div>
<div v-else class="loading-details">
<p>加载中...</p>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import router from '@/router';
import { ElMessage } from 'element-plus';
import { syrenwulist, syrenwujilu, syrenwuDetail } from '@/api/zhinengxunjian/shiyan/renwu';
// 1. 选项卡状态管理
const activeTab = ref('record'); // 默认显示"试验记录"
// 2. 筛选条件
const filterStatus = ref('all');
const filterType = ref('all');
const dateRange = ref([]);
// 3. 试验记录数据
const testRecords = ref([]);
const planTableData = ref([]);
const taskTableData = ref([]);
// 4. 当前日期
const currentDate = computed(() => {
const date = new Date();
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')}`;
});
// 5. 统计数据
const statData = ref({
completed: 0,
passRate: 0,
pendingAnalysis: 0,
avgDuration: '0分钟',
// 新增:增长率相关数据
completedGrowth: 0,
passRateGrowth: 0,
avgDurationGrowth: 0
});
// 6. 分页相关
const currentPage = ref(1);
const pageSize = ref(20);
const totalRecords = ref(0);
// 7. 方法:获取试验记录数据
const getTestRecords = async () => {
try {
const response = await syrenwulist({
projectId: 1,
page: currentPage.value,
size: pageSize.value
});
console.log('syrenwulist API响应:', response);
if (response && response.code === 200 && response.rows) {
testRecords.value = response.rows;
totalRecords.value = response.total;
}
} catch (error) {
console.error('获取试验记录失败:', error);
}
};
// 8. 方法:获取统计数据
const getStatisticsData = async () => {
try {
const response = await syrenwujilu({ projectId: 1 });
console.log('syrenwujilu API响应:', response);
// 确保接口返回成功状态码(code=200)且有数据
if (response && response.code === 200 && response.data) {
// 映射API返回的数据到statData
const apiData = response.data;
statData.value.completed = parseInt(apiData.finishCount) || 0;
statData.value.passRate = parseFloat(apiData.passValue) || 0;
statData.value.pendingAnalysis = parseInt(apiData.failCount) || 0;
// 格式化平均试验时长
const avgTime = parseFloat(apiData.averageTestTime) || 0;
statData.value.avgDuration = `${avgTime}分钟`;
// 处理增长率数据
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);
// 可以在这里添加错误提示或默认值处理
ElMessage.warning('获取统计数据失败,请稍后重试');
}
} catch (error) {
console.error('获取统计数据异常:', error);
ElMessage.error('获取统计数据异常,请稍后重试');
}
};
// 9. 辅助方法对节点按code排序
const sortedNodes = (nodes) => {
return [...nodes].sort((a, b) => a.code - b.code);
};
// 10. 辅助方法:格式化日期
const formatDate = (dateString) => {
if (!dateString) return '未知日期';
const date = new Date(dateString);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
// 11. 辅助方法:格式化日期时间
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return '未知时间';
const date = new Date(dateTimeString);
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')}`;
};
// 12. 辅助方法:获取节点状态类名
const getNodeStatusClass = (nodeStatus, recordStatus) => {
// 节点状态: 2-未完成, 3-失败, 其他假设为已完成
// 记录状态: 'failed'-失败, 'completed'-完成, 其他为进行中
// 如果节点本身状态为3失败直接返回failed类名
if (nodeStatus === '3') {
return 'failed';
}
if (recordStatus === 'failed') {
// 如果记录失败,找到失败阶段的节点
return nodeStatus === '2' ? 'failed' : 'active';
} else if (recordStatus === 'completed') {
return 'active';
} else {
// 进行中状态已完成的节点标记为active
return nodeStatus !== '2' ? 'active' : '';
}
};
// 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') {
return 'active';
} else {
// 进行中状态已完成节点之间的线为active
return nodes[index].status !== '2' ? 'active' : '';
}
};
// 14. 辅助方法:获取进度颜色
const getProgressColor = (status) => {
const colorMap = {
'drafted': '#e5e7eb',
'in-progress': '#165dff',
'completed': '#00b42a',
'paused': '#86909c'
};
return colorMap[status] || '#e5e7eb';
};
// 15. 辅助方法:获取状态文本
const getStatusText = (status) => {
const statusMap = {
'1': '待执行',
'4': '执行中',
'2': '已延期',
'5': '已完成',
'3': '失败',
'completed': '已完成',
'failed': '失败',
'paused': '已延期',
'drafted': '待执行',
'in-progress': '执行中',
'normal': '已完成',
'attention': '执行中',
'problem': '失败'
};
return statusMap[status] || '未知状态';
};
// 16. 辅助方法:获取任务状态文本
const getTaskStatusText = (status) => {
const statusMap = {
'pending': '待执行',
'accepted': '执行中',
'completed': '已完成',
'rejected': '已拒绝',
'1': '待执行',
'4': '执行中',
'2': '已延期',
'5': '已完成',
'3': '失败'
};
return statusMap[status] || '未知状态';
};
// 17. 辅助方法:获取状态类名
const getStatusClass = (status) => {
const classMap = {
'1': 'tag-pending', // 待执行
'4': 'tag-executing', // 执行中
'2': 'tag-delayed', // 已延期
'5': 'tag-completed', // 已完成
'3': 'status-failed', // 失败
'completed': 'tag-completed',
'failed': 'status-failed',
'paused': 'tag-delayed',
'pending': 'tag-pending',
'accepted': 'tag-pending',
'rejected': 'status-failed',
'normal': 'tag-completed',
'attention': 'tag-executing',
'problem': 'status-failed'
};
return classMap[status] || 'tag-pending';
};
// 18. 分页事件处理
const handlePageChange = (page) => {
currentPage.value = page;
getTestRecords();
};
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
getTestRecords();
};
// 19. 导航方法
const handleInspection1 = () => {
router.push('/znxj/rili');
};
const handleInspection2 = () => {
router.push('/znxj/xjgl/InspectionManagement');
};
const handleInspection3 = () => {
router.push('/znxj/sygl/shiyanguanli');
};
const handleInspection4 = () => {
router.push('/znxj/bxgl/baoxiuguanli');
};
const handleInspection5 = () => {
router.push('/znxj/qxgl/qiangxiuguanli');
};
const handleInspection6 = () => {
router.push('/znxj/gdgl/gongdanliebiao');
};
const handleInspection7 = () => {
router.push('/znxj/ywzz/renyuanzhuangtai');
};
const handleInspectionManagement1 = () => {
activeTab.value = 'plan';
router.push('/znxj/sygl/shiyanguanli');
};
const handleInspectionManagement2 = () => {
activeTab.value = 'task';
router.push('/znxj/sygl/shiyanrenwu');
};
const handleInspectionManagement3 = () => {
activeTab.value = 'record';
router.push('/znxj/sygl/shiyanjilu');
};
// 20. 详情弹窗相关
const detailDialogVisible = ref(false);
const detailData = ref(null);
const isDetailLoading = ref(false);
// 22. 处理查看详情
const handleViewDetail = async (row) => {
try {
if (!row || !row.id) {
ElMessage.error('记录ID不存在无法查看详情');
return;
}
isDetailLoading.value = true;
const response = await syrenwuDetail(row.id);
if (response && response.code === 200) {
detailData.value = response.data;
detailDialogVisible.value = true;
} else {
ElMessage.error(response?.msg || '获取任务详情失败');
}
} catch (error) {
console.error('查看详情失败:', error);
ElMessage.error('获取任务详情失败');
} finally {
isDetailLoading.value = false;
}
};
// 24. 组件挂载时获取数据 - 确保页面进入时立即调用接口
onMounted(async () => {
// 直接并立即调用数据接口,确保页面加载时能获取到最新数据
try {
// 并行调用两个数据接口以提高加载速度
await Promise.all([getStatisticsData(), getTestRecords()]);
} catch (error) {
console.error('数据加载失败:', error);
ElMessage.error('数据加载失败,请刷新页面重试');
}
});
</script>
<style scoped>
@import url('./css/detail-dialog.css');
@import url('./css/step-bars.css');
/* 1. 基础容器样式 */
.operation-inspection {
padding: 20px;
background-color: #f9fbfd;
min-height: 100vh;
}
.navigation-tabs {
display: flex;
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
padding: 2px;
}
.nav-tab {
padding: 12px 24px;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 4px;
font-size: 14px;
color: #606266;
border-right: 1px solid #f0f0f0;
flex: 1;
text-align: center;
}
.nav-tab:last-child {
border-right: none;
}
.nav-tab:hover {
color: #409eff;
background-color: #ecf5ff;
}
.nav-tab.active {
background-color: #409eff;
color: #fff;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
}
/* 3. 选项卡样式 */
.tabs-wrapper {
background-color: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* 4. 头部容器 */
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
/* 5. 筛选和操作区域 */
.filter-and-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #fff;
border: 1px solid #e5e7eb;
border-radius: 0 0 4px 4px;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.el-select,
.date-picker {
width: 160px;
}
/* 6. 表格容器 */
.table-container {
background-color: #fff;
border-radius: 4px;
border: 1px solid #e5e7eb;
margin-bottom: 16px;
}
.el-table {
width: 100%;
}
.el-table th {
background-color: #f9fafb;
font-weight: 500;
color: #4b5563;
}
.plan-name {
white-space: pre-line;
}
/* 7. 进度条样式 */
.progress-bar {
height: 8px;
background-color: #f3f4f6;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
transition: width 0.3s ease;
}
/* 8. 状态标签样式 */
.status-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
border: 1px solid transparent;
}
/* 与试验任务页面相同的标签样式 */
.tag-pending {
background-color: #e6f7ff;
color: #1677ff;
border-color: #91d5ff;
}
.tag-delayed {
background-color: #fff2f0;
color: #ff4d4f;
border-color: #ffccc7;
}
.tag-executing {
background-color: #fffbe6;
color: #fa8c16;
border-color: #ffe58f;
}
.tag-completed {
background-color: #f6ffed;
color: #52c41a;
border-color: #b7eb8f;
}
/* 保留原有的部分样式以确保兼容性 */
.status-in-progress {
background-color: #fffbe6;
color: #fa8c16;
border-color: #ffe58f;
}
.status-completed {
background-color: #f6ffed;
color: #52c41a;
border-color: #b7eb8f;
}
.status-pending {
background-color: #e6f7ff;
color: #1677ff;
border-color: #91d5ff;
}
.status-failed {
background-color: #fff2f0;
color: #ff4d4f;
border-color: #ffccc7;
}
/* 9. 操作按钮样式 */
.operation-buttons {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.operate-btn {
padding: 2px 8px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
border: none;
background: none;
transition: all 0.2s;
}
.view-btn {
color: #165dff;
}
.view-btn:hover {
background-color: #e8f3ff;
}
.complete-btn {
color: #00b42a;
}
.complete-btn:hover {
background-color: #e6ffed;
}
.accept-btn {
color: #2563eb;
}
.accept-btn:hover {
background-color: #eff6ff;
}
.report-btn {
color: #ff7d00;
}
.report-btn:hover {
background-color: #fff7e0;
}
/* 10. 分页样式 */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
.total-records {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.el-pagination {
--el-pagination-item-active-bg-color: #165dff;
}
/* 11. 记录页面样式 */
.record-container {
background-color: #fff;
border-radius: 4px;
border: 1px solid #e5e7eb;
padding: 20px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #1f2329;
margin: 0 0 10px 0;
}
.section-subtitle {
font-size: 14px;
color: #6b7280;
margin: 0 0 20px 0;
text-align: right;
}
/* 12. 统计卡片样式 */
.stat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background-color: #f0f7ff;
border-radius: 8px;
padding: 16px;
border-left: 4px solid #165dff;
}
.stat-label {
font-size: 14px;
color: #6b7280;
margin: 0 0 8px 0;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #1f2329;
margin: 0;
display: flex;
align-items: baseline;
gap: 8px;
}
.stat-change {
font-size: 12px;
padding: 1px 6px;
border-radius: 4px;
white-space: nowrap;
}
.stat-change.up {
background-color: #e6ffed;
color: #00b42a;
}
.stat-change.down {
background-color: #fff1f0;
color: #f5222d;
}
.stat-change.warning {
background-color: #fff7e0;
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;
flex-direction: column;
gap: 20px;
}
.test-record-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.2s;
}
.test-record-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.test-record-card.passed {
border-left: 4px solid #00b42a;
}
.test-record-card.failed {
border-left: 4px solid #dc2626;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.record-title {
font-size: 16px;
font-weight: 500;
color: #1f2329;
margin: 0;
}
.record-date {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.record-time {
font-size: 12px;
color: #9ca3af;
margin-left: 8px;
}
/* 14. 试验进度样式 */
.test-progress {
display: flex;
align-items: center;
padding: 20px;
background-color: #fff;
gap: 12px;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #e5e7eb;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-bottom: 8px;
}
.progress-line {
flex: 1;
height: 2px;
background-color: #e5e7eb;
}
.progress-step.active .step-number {
background-color: #00b42a;
color: white;
}
.progress-step.active .step-name {
color: #00b42a;
font-weight: 500;
}
.progress-line.active {
background-color: #00b42a;
}
.progress-step.failed .step-number {
background-color: #dc2626;
color: white;
}
.progress-step.failed .step-name {
color: #dc2626;
}
.progress-line.failed {
background-color: #dc2626;
}
/* 15. 试验结果样式 */
.test-result {
padding: 16px 20px;
border-top: 1px solid #e5e7eb;
background-color: #fff;
}
.result-title {
font-size: 14px;
font-weight: 500;
color: #1f2329;
margin: 0 0 12px 0;
}
.result-content {
font-size: 14px;
color: #4b5563;
line-height: 1.5;
margin: 0 0 12px 0;
}
.result-details {
font-size: 13px;
color: #6b7280;
margin: 0;
}
.failure-analysis {
background-color: #fff5f5;
}
.improvement-suggestion {
margin-top: 12px;
padding: 10px 12px;
background-color: #fff8e6;
border-radius: 4px;
display: flex;
align-items: flex-start;
gap: 8px;
}
.improvement-suggestion i {
color: #ff7d00;
margin-top: 2px;
}
.improvement-suggestion p {
font-size: 13px;
color: #6b46c1;
margin: 0;
line-height: 1.5;
}
/* 16. 记录操作按钮 */
.record-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 16px 20px;
background-color: #f9fafb;
border-top: 1px solid #e5e7eb;
}
/* 17. 无数据提示样式 */
.no-records {
text-align: center;
padding: 40px 0;
color: #6b7280;
background-color: #fff;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
/* 18. 响应式适配 */
@media (max-width: 1200px) {
.stat-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.stat-grid {
grid-template-columns: 1fr;
}
.test-progress {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 16px;
}
.progress-step {
flex-direction: row;
width: 100%;
gap: 12px;
}
.step-number {
margin-bottom: 0;
}
.progress-line {
width: 2px;
height: 30px;
align-self: center;
}
.filters {
flex-direction: column;
align-items: stretch;
}
.el-select,
.date-picker {
width: 100%;
}
.action-buttons {
width: 100%;
justify-content: space-between;
}
.navigation-tabs {
flex-wrap: wrap;
}
.nav-tab {
flex: 1 1 auto;
min-width: 100px;
}
.record-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
</style>