Files
maintenance_system/src/views/zhinengxunjian/shiyanguanli.vue
2025-09-26 20:32:14 +08:00

1996 lines
61 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">
<!-- 1. 顶部导航选项卡对应原试验系统的外层导航 -->
<!-- <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>
<!-- 4. 筛选和操作区域与试验系统filter-and-actions结构一致 -->
<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>
</div>
<div class="action-buttons">
<el-button type="primary" icon="Search" class="search-btn"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="openRecordDialog"> <i class="fas fa-plus"></i> 新增实验记录 </el-button>
</div>
</div>
<!-- 5. 主内容区根据选中选项卡切换内容 -->
<div class="content-container">
<!-- 5.1 巡检计划表格与试验系统表格结构一致 -->
<div v-if="activeTab === 'plan'" class="table-container">
<el-table :data="planTableData" border v-loading="loading">
<el-table-column align="center" prop="name" label="计划名称" width="220">
<template #default="scope">
<div class="plan-name">{{ scope.row.name }}</div>
</template>
</el-table-column>
<el-table-column align="center" prop="type" label="巡检类型" width="120"></el-table-column>
<el-table-column align="center" prop="cycle" label="巡检周期" width="120"></el-table-column>
<el-table-column align="center" prop="dateRange" label="执行时间范围"></el-table-column>
<el-table-column align="center" 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 align="center" prop="responsible" label="负责人" width="120"></el-table-column>
<el-table-column align="center" label="操作" width="220">
<template #default="scope">
<div class="operation-buttons">
<!-- 草稿状态 -->
<template v-if="scope.row.status === 'drafted'">
<el-button type="text" class="operate-btn edit-btn" @click="handleEditRecord(scope.row)">编辑</el-button>
<el-button type="text" class="operate-btn submit-btn">提交审批</el-button>
<el-button type="text" class="operate-btn delete-btn">删除</el-button>
</template>
<!-- 已批准状态 -->
<template v-else-if="scope.row.status === 'approved'">
<el-button type="text" class="operate-btn edit-btn" @click="handleEditRecord(scope.row)">编辑</el-button>
<el-button type="text" class="operate-btn view-btn" @click="handleViewDetail(scope.row)">详情</el-button>
<!-- 当testPlanType为1时显示停用按钮 -->
<el-button v-if="scope.row.testPlanType === '1'" type="text" class="operate-btn stop-btn" @click="handleStart(scope.row, '2')">
停用
</el-button>
<!-- 当testPlanType不存在或为2时显示启用按钮 -->
<el-button
v-else-if="!scope.row.testPlanType || scope.row.testPlanType === '2'"
type="text"
class="operate-btn start-btn"
@click="handleStart(scope.row, '1')"
>
启用
</el-button>
</template>
<!-- 进行中状态 -->
<template v-else-if="scope.row.status === 'in-progress'">
<el-button type="text" class="operate-btn edit-btn" @click="handleEditRecord(scope.row)">编辑</el-button>
<el-button type="text" class="operate-btn view-btn" @click="handleViewDetail(scope.row)">详情</el-button>
<el-button type="text" class="operate-btn track-btn">跟踪</el-button>
</template>
<!-- 已完成状态 -->
<template v-else-if="scope.row.status === 'completed'">
<el-button type="text" class="operate-btn edit-btn" @click="handleEditRecord(scope.row)">编辑</el-button>
<el-button type="text" class="operate-btn view-btn" @click="handleViewDetail(scope.row)">详情</el-button>
<el-button type="text" class="operate-btn report-btn">查看报告</el-button>
</template>
<!-- 默认显示详情 -->
<template v-else>
<el-button type="text" class="operate-btn view-btn" @click="handleViewDetail(scope.row)">详情</el-button>
</template>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 5.2 巡检任务表格结构与计划表格一致数据不同 -->
<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">
<el-button type="text" class="operate-btn accept-btn" v-if="scope.row.status === 'pending'">接受</el-button>
<el-button type="text" class="operate-btn complete-btn" v-if="scope.row.status === 'accepted'">完成</el-button>
<el-button type="text" class="operate-btn view-btn" @click="handleViewDetail(scope.row)">查看详情</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 5.3 巡检记录整合原右侧卡片和统计图表 -->
<div v-if="activeTab === 'record'" class="record-container">
<div class="main-content-container">
<!-- 左侧统计与图表区 -->
<div class="left-content">
<div class="content-card">
<div class="card-header">
<h2 class="card-title">巡检记录统计</h2>
<div class="time-range-buttons">
<button class="time-btn" :class="{ active: timeRange === 'month' }" @click="handleTimeRangeChange('month')"></button>
<button class="time-btn" :class="{ active: timeRange === 'week' }" @click="handleTimeRangeChange('week')"></button>
<button class="time-btn" :class="{ active: timeRange === 'day' }" @click="handleTimeRangeChange('day')"></button>
</div>
</div>
<div class="card-body">
<!-- 统计卡片 -->
<div class="stat-grid">
<div class="stat-card">
<p class="stat-label">已完成巡检</p>
<p class="stat-value">{{ statData.completed }}</p>
</div>
<div class="stat-card">
<p class="stat-label">发现问题数</p>
<p class="stat-value">{{ statData.problems }}</p>
</div>
<div class="stat-card">
<p class="stat-label">已解决问题</p>
<p class="stat-value">{{ statData.resolved }}</p>
</div>
<div class="stat-card">
<p class="stat-label">平均完成时间</p>
<p class="stat-value">{{ statData.avgTime }}</p>
</div>
</div>
<div class="divider"></div>
<!-- 进度与图表区 -->
<div class="chart-container">
<!-- 左侧饼图 -->
<div class="pie-chart">
<p class="chart-title">问题解决率</p>
<div class="pie-wrapper">
<svg class="w-full h-full" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="#f3f4f6" stroke-width="10" />
<circle
cx="50"
cy="50"
r="45"
fill="none"
stroke="#10b981"
stroke-width="10"
:stroke-dasharray="282.74"
:stroke-dashoffset="282.74 * (1 - statData.resolveRate / 100)"
transform="rotate(-90 50 50)"
/>
<circle
cx="50"
cy="50"
r="45"
fill="none"
stroke="#f97316"
stroke-width="10"
:stroke-dasharray="282.74 * (1 - statData.resolveRate / 100)"
stroke-dashoffset="0"
transform="rotate(-90 50 50)"
/>
</svg>
<div class="pie-center">
<p class="text-sm text-gray-500">解决率</p>
<p class="text-lg font-bold text-gray-800">{{ statData.resolveRate }}%</p>
</div>
</div>
<div class="pie-legend">
<div class="legend-item">
<div class="legend-color resolved"></div>
<span class="legend-text">已解决</span>
</div>
<div class="legend-item">
<div class="legend-color unresolved"></div>
<span class="legend-text">未解决</span>
</div>
</div>
</div>
<!-- 右侧进度条 -->
<div class="progress-bars">
<div class="progress-item">
<div class="progress-header">
<span class="progress-label">巡检完成率</span>
<span class="progress-value">{{ statData.completeRate }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: statData.completeRate + '%', backgroundColor: '#3b82f6' }"></div>
</div>
</div>
<div class="progress-item">
<div class="progress-header">
<span class="progress-label">问题解决率</span>
<span class="progress-value">{{ statData.resolveRate }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: statData.resolveRate + '%', backgroundColor: '#10b981' }"></div>
</div>
</div>
<div class="progress-item">
<div class="progress-header">
<span class="progress-label">任务及时率</span>
<span class="progress-value">{{ statData.timelyRate }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: statData.timelyRate + '%', backgroundColor: '#8b5cf6' }"></div>
</div>
</div>
</div>
</div>
<div class="divider"></div>
<!-- 问题分类统计 -->
<div class="problem-category">
<h3 class="section-title">问题类型分布</h3>
<div class="category-list">
<div class="category-item" v-for="(item, index) in problemCategories" :key="index">
<div class="category-header">
<span class="category-label">{{ item.name }}</span>
<span class="category-value">{{ item.rate }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: item.rate + '%', backgroundColor: item.color }"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧最近巡检记录 -->
<div class="right-content">
<div class="content-card">
<div class="card-header">
<h2 class="card-title">最近巡检记录</h2>
</div>
<div class="card-body record-list">
<div class="inspection-record" v-for="(record, index) in recentRecords" :key="index">
<div class="record-header">
<h3 class="record-title">{{ record.name }}</h3>
<span :class="['status-tag', `status-${record.status}`]">
{{ getRecordStatusText(record.status) }}
</span>
</div>
<p class="record-meta">{{ record.time }} · {{ record.executor }}</p>
<div class="record-summary">{{ record.summary }}</div>
<div class="record-actions">
<button class="operate-btn view-btn">查看详情</button>
<button class="operate-btn report-btn">生成报告</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination" v-if="activeTab !== 'record'">
<p class="total-records">
显示第{{ (currentPage - 1) * pageSize + 1 }}{{ Math.min(currentPage * pageSize, totalRecords) }}{{ 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>
</div>
<!-- 新增实验记录弹窗 -->
<el-dialog
v-model="showRecordDialog"
title="新增实验记录"
width="1100px"
:before-close="handleClose"
class="custom-experiment-dialog"
:close-on-click-modal="false"
>
<el-form :model="createForm" :rules="createFormRules" ref="createFormRef" label-width="120px" class="custom-form">
<div class="form-container">
<!-- 材料名称 -->
<el-form-item label="计划名称" class="form-item">
<el-input v-model="formData.planName" placeholder="请输入计划名称" class="form-input" />
</el-form-item>
<!-- 试验时间 -->
<div class="form-row">
<el-form-item label="预计开始时间" class="form-item">
<el-date-picker v-model="formData.startDate" type="date" placeholder="请选择日期" value-format="YYYY-MM-DD" class="form-input" />
</el-form-item>
<el-form-item label="预计结束时间" class="form-item">
<el-date-picker v-model="formData.endDate" type="date" placeholder="请选择日期" value-format="YYYY-MM-DD" class="form-input" />
</el-form-item>
</div>
<!-- 试验编号 -->
<div class="form-row">
<el-form-item label="计划编号" class="form-item">
<el-input v-model="formData.testNumber" placeholder="EX-20240601" class="form-input" />
</el-form-item>
<el-form-item label="实验对象类型" class="form-item">
<el-select v-model="formData.testObject" placeholder="请选择实验对象类型" class="form-input">
<el-option label="安全试验" value="1" />
<el-option label="网络实验" value="2" />
<el-option label="性能试验" value="3" />
<el-option label="其他试验" value="4" />
</el-select>
</el-form-item>
</div>
<!-- 试验目的 -->
<el-form-item label="试验目的与预期" class="form-item">
<el-input
v-model="formData.testPurpose"
type="textarea"
placeholder="请简要描述本次试验的目的,检验标准及试验预期达到的效果"
class="form-input"
rows="2"
/>
</el-form-item>
<!-- 试验环境要求 -->
<el-form-item label="试验环境要求" class="form-item">
<el-input
v-model="formData.envRequirements"
type="textarea"
placeholder="请描述试验所需的硬件、软件、网络环境等要求"
class="form-input"
rows="2"
/>
</el-form-item>
<!-- 负责人 -->
<el-form-item label="负责人" class="form-item">
<el-select v-model="formData.manager" placeholder="请选择试验负责人" class="form-input">
<el-option v-for="user in userList" :key="user.value" :label="user.label" :value="user.value" />
</el-select>
</el-form-item>
<!-- 参与人员 -->
<el-form-item label="参与人员" class="form-item">
<el-select v-model="formData.participants" placeholder="请选择参与人员" class="form-input" multiple collapse-tags>
<el-option v-for="user in userList" :key="user.value" :label="user.label" :value="user.value" />
</el-select>
</el-form-item>
<!-- 所需设备与准备 -->
<el-form-item label="所需资源与设备" class="form-item" style="width: 100%">
<div class="equipment-list">
<div class="equipment-item" v-for="(equip, index) in formData.equipments" :key="index">
<el-checkbox v-model="equip.selected">{{ equip.name }}</el-checkbox>
</div>
<div class="add-equipment">
<el-input v-model="newEquipment" placeholder="添加其他资源" />
<el-button type="primary" size="small" @click="addEquipment">添加</el-button>
</div>
</div>
</el-form-item>
<!-- 风险识别与应对措施 -->
<el-form-item label="风险识别" class="form-item">
<el-input
v-model="formData.riskMitigation"
type="textarea"
placeholder="请描述试验中存在的风险及相应的应对措施"
class="form-input"
rows="2"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="saveLoading">保存</el-button>
</span>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog
v-model="showDetailDialog"
title="实验记录详情"
width="800px"
:loading="detailLoading"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="custom-experiment-dialog"
center
>
<div v-if="detailData" class="task-detail-container">
<!-- 基础信息卡片 -->
<div class="detail-card">
<h3 class="card-title">基础信息</h3>
<div class="card-content">
<div class="info-row">
<div class="info-item">
<label class="info-label">计划名称:</label>
<span class="info-value">{{ detailData.planName || '-' }}</span>
</div>
<div class="info-item">
<label class="info-label">计划编号:</label>
<span class="info-value">{{ detailData.planCode || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<label class="info-label">实验对象:</label>
<span class="info-value">{{ getTestObjectText(detailData.testObject) || '-' }}</span>
</div>
<div class="info-item">
<label class="info-label">负责人:</label>
<span class="info-value">{{ detailData.person?.userName || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<label class="info-label">开始时间:</label>
<span class="info-value">{{ detailData.beginTime ? formatDate(detailData.beginTime) : '-' }}</span>
</div>
<div class="info-item">
<label class="info-label">结束时间:</label>
<span class="info-value">{{ detailData.endTime ? formatDate(detailData.endTime) : '-' }}</span>
</div>
</div>
</div>
</div>
<!-- 实验设备 -->
<div v-if="detailData.testDevice" class="detail-card">
<h3 class="card-title">实验设备</h3>
<div class="card-content">
<div v-for="(device, index) in detailData.testDevice.split(',')" :key="index" class="info-item">
<label class="info-label">设备{{ index + 1 }}:</label>
<span class="info-value">{{ device.trim() }}</span>
</div>
</div>
</div>
<!-- 实验信息 -->
<div class="detail-card">
<h3 class="card-title">实验信息</h3>
<div class="card-content">
<div class="info-item full-width">
<label class="info-label">实验说明:</label>
<div class="info-value">{{ detailData.testInfo || '-' }}</div>
</div>
<div class="info-item full-width">
<label class="info-label">实验设置:</label>
<div class="info-value">{{ detailData.testSetting || '-' }}</div>
</div>
<div class="info-item full-width">
<label class="info-label">解决方案:</label>
<div class="info-value">{{ detailData.testSolutions || '-' }}</div>
</div>
</div>
</div>
<!-- 参与人员 -->
<div v-if="detailData.persons && detailData.persons.length > 0" class="detail-card">
<h3 class="card-title">参与人员</h3>
<div class="card-content">
<div v-for="(person, index) in detailData.persons" :key="person.id" class="info-row">
<div class="info-item">
<label class="info-label">姓名:</label>
<span class="info-value">{{ person.userName }}</span>
</div>
<div class="info-item">
<label class="info-label">团队:</label>
<span class="info-value">{{ person.teamName }}</span>
</div>
</div>
</div>
</div>
<!-- 巡检项目 -->
<div v-if="detailData.inspectionItemList && detailData.inspectionItemList.length > 0" class="detail-card">
<h3 class="card-title">巡检项目</h3>
<div class="card-content">
<div v-for="(item, index) in detailData.inspectionItemList" :key="item.id" class="info-row">
<div class="info-item">
<label class="info-label">项目名称:</label>
<span class="info-value">{{ item.name }}</span>
</div>
<div class="info-item">
<label class="info-label">项目类型:</label>
<span class="info-value">{{ item.type }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-else class="loading-details">
<el-skeleton :count="6" :columns="2" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDetailDialog = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import router from '@/router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { shiyanDetail, shiyanlist, addshiyan, updateshiyan } from '@/api/zhinengxunjian/shiyan/index';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index';
// 1. 选项卡状态管理
const activeTab = ref('plan'); // 默认为"巡检计划"
const timeRange = ref('month'); // 统计时间范围:月/周/日
// 2. 筛选条件
const filterStatus = ref('all');
const filterType = ref('all');
const dateRange = ref([]);
// 分页参数
const currentPage = ref(1);
const pageSize = ref(20);
const totalRecords = ref(0);
// 4. 巡检计划表格数据
const planTableData = ref([]);
const loading = ref(false); // 加载状态
const fetchExperimentData = async () => {
loading.value = true;
try {
const queryParams = {
projectId: 1,
pageSize: pageSize.value,
pageNum: currentPage.value
// 其他参数...
};
const response = await shiyanlist(queryParams);
if (response && response.code === 200) {
planTableData.value = (response.rows || []).map((item) => ({
// 关键将ID转为字符串存储避免大整数精度丢失
id: String(item.id), // 强制转为字符串
name: `${item.planName}\n编号: ${item.planCode}`,
type: getTestObjectText(item.testObject),
cycle: item.cycle || '每月',
dateRange: `${item.beginTime || ''}${item.endTime || ''}`,
progress: calculateProgress(item.testStatus),
status: mapStatus(item.testStatus),
responsible: item.person?.userName || '未知负责人',
// 保留testPlanType字段用于显示启用/停用按钮
testPlanType: item.testPlanType
}));
totalRecords.value = response.total || 0;
}
} catch (error) {
console.error('获取实验数据失败:', error);
} finally {
loading.value = false;
}
};
// 辅助方法
const getTestObjectText = (type) => {
const typeMap = {
'1': '安全试验',
'2': '网络实验',
'3': '性能试验',
'4': '其他试验'
};
return typeMap[type] || '未知类型';
};
const calculateProgress = (status) => {
const progressMap = {
'1': 30, // 已批准
'2': 60, // 进行中
'3': 100, // 已完成
'4': 0 // 未通过
};
return progressMap[status] || 0;
};
const mapStatus = (status) => {
const statusMap = {
'1': 'approved', // 已批准
'2': 'in-progress', // 进行中
'3': 'completed', // 已完成
'4': 'rejected', // 未通过
'5': 'drafted' // 草稿
};
return statusMap[status] || 'drafted';
};
// 页面加载时获取数据
onMounted(() => {
fetchExperimentData();
getUsersList(); // 加载用户列表
});
// 巡检任务表格数据
const taskTableData = ref([]);
// 统计数据
const statData = ref({
completed: 0,
problems: 0,
resolved: 0,
avgTime: '0分钟',
completeRate: 0,
resolveRate: 0,
timelyRate: 0
});
// 问题类型分布
const problemCategories = ref([]);
// 最近巡检记录
const recentRecords = ref([]);
// 9. 方法:切换顶部导航
const handleInspection1 = () => {
router.push('/rili/rili');
};
const handleInspection2 = () => {
router.push('/rili/InspectionManagement');
};
const handleInspection3 = () => {
router.push('/rili/shiyanguanli');
};
const handleInspection4 = () => {
router.push('/rili/baoxiuguanli');
};
const handleInspection5 = () => {
router.push('/rili/qiangxiuguanli');
};
const handleInspection6 = () => {
router.push('/rili/gongdanliebiao');
};
const handleInspection7 = () => {
router.push('/rili/renyuanzhuangtai');
};
const handleInspectionManagement1 = () => {
router.push('/rili/shiyanguanli');
};
const handleInspectionManagement2 = () => {
router.push('/rili/shiyanrenwu');
};
const handleInspectionManagement3 = () => {
router.push('/rili/shiyanjilu');
};
// 10. 方法:切换功能选项卡
const switchTab = (tab) => {
activeTab.value = tab;
// 实际应用中需根据选项卡加载对应数据
if (tab === 'record') {
// 加载统计数据
updateStatData(timeRange.value);
}
};
// 11. 方法:更新统计数据(根据时间范围)
const updateStatData = (range) => {
const mockData = {
month: { completed: 42, problems: 7, resolved: 5, avgTime: '45分钟', completeRate: 68, resolveRate: 72, timelyRate: 60 },
week: { completed: 12, problems: 2, resolved: 1, avgTime: '40分钟', completeRate: 75, resolveRate: 50, timelyRate: 65 },
day: { completed: 2, problems: 0, resolved: 0, avgTime: '35分钟', completeRate: 100, resolveRate: 100, timelyRate: 100 }
};
statData.value = mockData[range];
};
// 切换时间范围
const handleTimeRangeChange = (range) => {
timeRange.value = range;
updateStatData(range);
};
// 分页变化处理
const handlePageChange = (page) => {
currentPage.value = page;
fetchExperimentData();
};
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
fetchExperimentData();
};
// 状态文本映射
const getStatusText = (status) => {
const statusMap = {
'drafted': '草稿',
'approved': '已批准',
'in-progress': '进行中',
'completed': '已完成',
'rejected': '未通过'
};
return statusMap[status] || '';
};
const getTaskStatusText = (status) => {
const statusMap = {
'pending': '待接受',
'accepted': '处理中',
'completed': '已完成',
'rejected': '已拒绝'
};
return statusMap[status] || '';
};
const getRecordStatusText = (status) => {
const statusMap = {
'normal': '正常',
'attention': '需关注',
'problem': '有问题'
};
return statusMap[status] || '';
};
// 进度条颜色
const getProgressColor = (status) => {
const colorMap = { 'drafted': '#ccc', 'in-progress': '#3b82f6', 'completed': '#10b981', 'paused': '#9e9e9e' };
return colorMap[status] || '#ccc';
};
// 18. 新增实验记录弹窗相关
const showRecordDialog = ref(false);
const saveLoading = ref(false); // 保存加载状态
// 表单数据
const formData = ref({
planName: '',
testNumber: 'EX-20240601',
refNumber: '',
startDate: '',
endDate: '',
testPurpose: '',
envRequirements: '',
manager: '',
participants: [], // 改为数组存储多选的用户ID
steps: [
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' }
],
equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
{ name: '温度控制系统', selected: false },
{ name: '负载生成工具', selected: false }
],
riskMitigation: ''
});
// 当前编辑的记录ID
const editRecordId = ref(null);
// 选中的用户列表(用于显示为多选框)
const selectedUsers = ref([]);
// 用户选择弹窗
const showUserSelectDialog = ref(false);
const availableUsers = ref([]);
const selectedUserIds = ref([]);
// 用户列表(用于负责人和参与人员选择)
const userList = ref([]);
// 获取用户列表
const getUsersList = async () => {
try {
const response = await xunjianUserlist();
// 适配新接口格式检查code为200且rows为数组
const userRows = response.code === 200 && response.rows && Array.isArray(response.rows) ? response.rows : [];
userList.value = userRows
.filter((item) => item && typeof item === 'object')
.map((item) => ({
label: item.userName || '未知用户',
value: String(item.userId || '') // 使用userId作为唯一标识
}));
if (userList.value.length === 0) {
userList.value = [{ label: '默认用户', value: 'default' }];
}
} catch (error) {
console.error('获取用户失败:', error);
userList.value = [{ label: '默认用户', value: 'default' }];
}
};
// 新设备输入
const newEquipment = ref('');
// 关闭弹窗
const handleClose = () => {
showRecordDialog.value = false;
};
// 3. 优化保存函数修复testNumber生成逻辑仅在提交时生成不预先填充
const handleSave = async () => {
try {
saveLoading.value = true;
// 1. 编辑模式校验ID有效性保持字符串ID避免大整数问题
if (editRecordId.value) {
// 仅校验ID存在性不转换为数字
if (!editRecordId.value || typeof editRecordId.value !== 'string' || editRecordId.value.trim() === '') {
ElMessage.error('编辑记录ID无效无法保存');
return;
}
}
// 2. 表单基础校验(增强必填项校验)
const { planName } = formData.value;
if (!planName.trim()) {
ElMessage.warning('请填写材料名称');
return;
}
// 新增时自动生成试验编号(提交时才生成,不在输入框预先显示)
const finalTestNumber = editRecordId.value
? formData.value.testNumber // 编辑时保留原编号
: `EX-${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(
2,
'0'
)}-${Date.now().toString().slice(-4)}`;
// 3. 构建请求数据
const requestData = {
projectId: 1,
planName: finalTestNumber, // 使用最终生成的编号
planCode: finalTestNumber,
testObject: '3',
beginTime: formData.value.startDate ? new Date(formData.value.startDate).toISOString() : '',
endTime: formData.value.endDate ? new Date(formData.value.endDate).toISOString() : '',
testInfo: formData.value.testPurpose,
testSetting: formData.value.envRequirements,
personCharge: formData.value.manager,
personIds: formData.value.participants.join(','),
inspectionItems: '',
testSolutions: formData.value.riskMitigation,
testDevice: formData.value.equipments
.filter((equip) => equip.selected)
.map((equip) => equip.name)
.join(','),
testStatus: '1',
// 关键修复编辑时添加主键ID与后端更新接口参数名一致
id: editRecordId.value // 若后端用planId等需改为对应字段名
};
// 调用接口
let response;
if (editRecordId.value) {
// 编辑模式:调用更新接口
response = await updateshiyan(requestData);
} else {
// 新增模式调用添加接口删除请求参数中的id避免后端报错
const { id, ...addData } = requestData;
response = await addshiyan(addData);
}
if (response && response.code === 200) {
ElMessage.success(editRecordId.value ? '更新成功' : '新增成功');
showRecordDialog.value = false;
fetchExperimentData(); // 重新加载列表,刷新数据
resetForm(); // 重置表单和编辑状态
} else {
ElMessage.error(response?.msg || (editRecordId.value ? '更新失败' : '新增失败'));
}
} catch (error) {
console.error(editRecordId.value ? '更新异常:' : '新增异常:', error);
ElMessage.error('系统异常,请稍后重试');
} finally {
saveLoading.value = false;
}
};
const resetForm = () => {
formData.value = {
planName: '', // 材料名称为空
testNumber: '', // 试验编号为空(原逻辑自动生成,改为新增时为空)
refNumber: '', // 参考号为空
startDate: '', // 开始日期为空
endDate: '', // 结束日期为空
testPurpose: '', // 试验目的为空
envRequirements: '', // 环境要求为空
manager: '', // 负责人为空
participants: [], // 参与人员为空数组
equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
{ name: '温度控制系统', selected: false },
{ name: '负载生成工具', selected: false }
], // 设备默认不选中
riskMitigation: '' // 风险识别为空
};
// 关键修复重置编辑状态标识清空编辑ID
editRecordId.value = null;
selectedUsers.value = [];
showUserSelectDialog.value = false;
availableUsers.value = [];
selectedUserIds.value = [];
newEquipment.value = '';
};
// 2. 打开新增弹窗时强制重置表单
const openRecordDialog = () => {
getUsersList();
resetForm(); // 确保打开时表单为空
showRecordDialog.value = true;
};
// 详情弹窗相关
const showDetailDialog = ref(false);
const detailLoading = ref(false);
const detailData = ref({});
// 2. 修复查看详情以字符串形式传递ID
const handleViewDetail = async (row) => {
try {
console.log('原始row数据:', row);
if (!row || !row.id) {
ElMessage.error('记录ID不存在无法查看详情');
return;
}
// 直接使用字符串ID不转为数字
const recordId = row.id;
console.log('请求详情的ID字符串:', recordId, '类型:', typeof recordId);
// 调用接口时直接传递字符串ID
const response = await shiyanDetail(recordId);
if (response.code === 200 && response.data) {
Object.assign(detailData.value, response.data);
showDetailDialog.value = true;
} else {
throw new Error(response.msg || '获取详情失败');
}
} catch (error) {
console.error('获取详情失败:', error);
ElMessage.error(`获取记录详情失败: ${error.message}`);
}
};
const handleEditRecord = async (row) => {
try {
// 1. 先校验row和id的有效性保留之前的校验逻辑
if (!row || !row.id) {
throw new Error('选中的记录不存在ID无法编辑');
}
// 关键修复设置编辑记录ID字符串格式避免精度丢失
editRecordId.value = row.id;
// 2. 调用接口获取详情使用字符串ID避免精度丢失
const detailResponse = await shiyanDetail(row.id);
if (detailResponse.code !== 200) {
throw new Error(detailResponse.msg || '获取记录详情失败');
}
const recordDetail = detailResponse.data.rows?.[0] || detailResponse.data;
// 兼容两种数据结构可能在rows数组中也可能直接在data中
// 4. 处理testDevice将逗号分隔的字符串转换为设备数组
const equipments = [];
if (recordDetail.testDevice) {
const deviceNames = recordDetail.testDevice.split(',');
deviceNames.forEach((name) => {
const trimmedName = name.trim();
if (trimmedName) {
equipments.push({ name: trimmedName, selected: true });
}
});
}
// 补充默认设备(未在记录中的设备默认不选中)
const defaultEquipments = [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
{ name: '温度控制系统', selected: false },
{ name: '负载生成工具', selected: false }
];
// 合并记录中的设备和默认设备(去重)
defaultEquipments.forEach((equip) => {
if (!equipments.some((e) => e.name === equip.name)) {
equipments.push(equip);
}
});
// 5. 处理参与人员从personIds解析
const participants = [];
if (recordDetail.personIds) {
// personIds是逗号分隔的用户ID字符串如"1968950664352530437,123"
participants.push(...recordDetail.personIds.split(',').filter((id) => id.trim()));
}
// 6. 填充表单数据(完整映射所有字段)
formData.value = {
planName: recordDetail.planName || '',
testNumber: recordDetail.testNumber || recordDetail.planCode || '',
refNumber: recordDetail.refNumber || '',
// 处理日期格式确保与date-picker兼容
startDate: recordDetail.startDate || recordDetail.beginTime?.split(' ')[0] || '',
endDate: recordDetail.endDate || recordDetail.endTime?.split(' ')[0] || '',
testPurpose: recordDetail.testPurpose || recordDetail.testInfo || '',
envRequirements: recordDetail.envRequirements || recordDetail.testSetting || '',
manager: recordDetail.manager || recordDetail.personCharge || '',
participants: participants, // 从personIds解析的数组
equipments: equipments, // 解析并合并后的设备数组
riskMitigation: recordDetail.riskMitigation || recordDetail.testSolutions || ''
};
// 7. 处理参与人员显示(匹配用户列表)
if (formData.value.participants.length > 0) {
selectedUsers.value = formData.value.participants.map((userId) => {
const user = userList.value.find((u) => u.value === userId);
return {
id: userId,
userName: user ? user.label : `未知用户(${userId})`,
selected: true
};
});
} else {
selectedUsers.value = [];
}
// 8. 打开编辑弹窗
showRecordDialog.value = true;
} catch (error) {
console.error('编辑记录异常:', error);
ElMessage.error(`获取记录详情失败: ${error.message || '请稍后重试'}`);
} finally {
loading.value = false;
}
};
// 添加新步骤
const addStep = () => {
formData.value.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
};
// 删除步骤
const deleteStep = (index) => {
// 确保至少保留一个步骤
if (formData.value.steps.length <= 1) {
ElMessage.warning('至少需要保留一个步骤');
return;
}
// 从数组中删除指定索引的步骤
formData.value.steps.splice(index, 1);
};
// 添加新设备
const addEquipment = () => {
if (newEquipment.value.trim()) {
formData.value.equipments.push({ name: newEquipment.value.trim(), selected: false });
newEquipment.value = '';
}
};
// 启动计划函数
// 处理启用/停用操作
const handleStart = async (row, actionType) => {
try {
// 1. 检查记录ID的有效性
if (!row || !row.id) {
ElMessage.error('记录不存在或ID无效无法操作');
return;
}
// 2. 确定操作类型1=启用2=停用)
const isEnable = actionType === '1';
const operationText = isEnable ? '启用' : '停用';
// 3. 显示确认对话框
await ElMessageBox.confirm(`确定要${operationText}该试验计划吗?`, '操作确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 4. 获取完整的记录详情数据
const detailResponse = await shiyanDetail(row.id);
if (detailResponse.code !== 200 || !detailResponse.data) {
throw new Error(`获取记录详情失败,无法${operationText}`);
}
const recordDetail = detailResponse.data.rows?.[0] || detailResponse.data;
// 5. 构建完整的请求数据,包含所有必要字段
const requestData = {
id: recordDetail.id, // 确保包含ID
projectId: recordDetail.projectId || 1,
planName: recordDetail.planName || '',
planCode: recordDetail.planCode || '',
testObject: recordDetail.testObject || '3',
beginTime: recordDetail.beginTime || '',
endTime: recordDetail.endTime || '',
testInfo: recordDetail.testInfo || '',
testSetting: recordDetail.testSetting || '',
personCharge: recordDetail.personCharge || '',
personIds: recordDetail.personIds || '',
inspectionItems: recordDetail.inspectionItems || '',
testSolutions: recordDetail.testSolutions || '',
testStep: recordDetail.testStep || '',
testDevice: recordDetail.testDevice || '',
testStatus: recordDetail.testStatus || '1',
// 根据操作类型设置testPlanType的值1=启用2=停用)
testPlanType: actionType
};
// 5. 调用接口传递完整数据但只修改testPlanType
const response = await updateshiyan(requestData);
// 6. 处理接口响应
if (response.code === 200) {
// 6.1 提示操作成功
ElMessage.success(`已成功${operationText}该试验计划`);
// 6.2 刷新数据列表
fetchExperimentData();
} else {
// 6.3 接口返回失败时提示
ElMessage.error(`${operationText}失败:${response.msg || '服务器异常'}`);
}
} catch (error) {
// 7. 捕获网络或代码错误,用户取消操作时也会进入这里
if (error !== 'cancel') {
// 排除用户主动取消的情况
console.error('启动操作失败:', error);
ElMessage.error(`操作失败:${error.message || '请检查网络或重试'}`);
}
}
};
// 日期格式化函数
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
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');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 日期时间格式化函数
const formatDateTime = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
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');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
</script>
<style scoped>
@import url('./css/detail-dialog.css');
.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. 页面标题 */
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #1f2329;
margin: 0 0 5px 0;
}
.page-description {
font-size: 14px;
color: #6b7280;
margin: 0;
}
/* 4. 功能选项卡导航(与试验系统一致) */
.tabs-nav {
display: flex;
background-color: #fff;
border: 1px solid #e5e7eb;
border-radius: 4px 4px 0 0;
overflow: hidden;
margin-bottom: -1px;
}
.tab-btn {
padding: 12px 24px;
background: none;
border: none;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn.active {
background-color: #fff;
color: #165dff;
border-top: 2px solid #165dff;
font-weight: 500;
}
.tab-btn:not(.active) {
color: #6b7280;
background-color: #f9fafb;
}
.tab-btn:not(.active):hover {
background-color: #f3f4f6;
}
/* 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;
}
.action-buttons {
display: flex;
gap: 12px;
}
.el-select,
.date-picker {
width: 160px;
}
.search-btn,
.export-btn,
.create-btn {
height: 36px;
border-radius: 4px;
}
/* 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: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-drafted {
background-color: #f2f3f5;
color: #86909c;
}
.status-approved {
background-color: #eff6ff;
color: #2563eb;
}
.status-in-progress {
background-color: #fff7e0;
color: #ff7d00;
}
.status-completed {
background-color: #e6ffed;
color: #00b42a;
}
.status-rejected {
background-color: #fff2f0;
color: #f5222d;
}
.status-pending {
background-color: #f9fafb;
color: #6b7280;
}
.status-accepted {
background-color: #e0f2fe;
color: #0284c7;
}
.status-normal {
background-color: #e6ffed;
color: #00b42a;
}
.status-attention {
background-color: #fff7e0;
color: #ff7d00;
}
.status-problem {
background-color: #fff2f0;
color: #f5222d;
}
/* 9. 操作按钮样式(扩展试验系统) */
.operation-buttons {
display: flex;
justify-content: center;
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;
}
.edit-btn {
color: #165dff;
}
.edit-btn:hover {
background-color: #e8f3ff;
}
.execute-btn {
color: #00b42a;
}
.execute-btn:hover {
background-color: #e6ffed;
}
.pause-btn {
color: #ff7d00;
}
.pause-btn:hover {
background-color: #fff7e0;
}
.resume-btn {
color: #722ed1;
}
.resume-btn:hover {
background-color: #f3e8ff;
}
.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 {
display: flex;
flex-direction: column;
gap: 16px;
}
.main-content-container {
display: flex;
gap: 16px;
height: calc(100% - 20px);
}
.left-content {
flex: 2;
display: flex;
flex-direction: column;
}
.right-content {
flex: 1;
display: flex;
flex-direction: column;
}
/* 12. 内容卡片样式 */
.content-card {
background-color: #fff;
border-radius: 4px;
border: 1px solid #e5e7eb;
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
.card-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 500;
color: #1f2329;
margin: 0;
}
.card-body {
padding: 16px;
flex: 1;
overflow-y: auto;
}
/* 13. 时间范围按钮 */
.time-range-buttons {
display: flex;
gap: 8px;
}
.time-btn {
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
border: 1px solid #e5e7eb;
background-color: #f9fafb;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
}
.time-btn.active {
background-color: #165dff;
color: #fff;
border-color: #165dff;
}
/* 14. 统计卡片样式 */
.stat-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.stat-card {
background-color: #f9fafb;
border-radius: 4px;
padding: 12px;
}
.stat-label {
font-size: 14px;
color: #6b7280;
margin: 0 0 8px 0;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: #1f2329;
margin: 0;
}
/* 15. 分隔线 */
.divider {
height: 1px;
background-color: #e5e7eb;
margin: 16px 0;
}
/* 16. 图表容器 */
.chart-container {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
}
.pie-chart {
flex: 1;
min-width: 200px;
display: flex;
flex-direction: column;
align-items: center;
}
.chart-title {
font-size: 14px;
color: #6b7280;
margin: 0 0 8px 0;
align-self: flex-start;
}
.pie-wrapper {
position: relative;
width: 180px;
height: 180px;
}
.pie-center {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pie-legend {
display: flex;
gap: 16px;
margin-top: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #6b7280;
}
.legend-color {
width: 8px;
height: 8px;
border-radius: 50%;
}
.legend-color.resolved {
background-color: #10b981;
}
.legend-color.unresolved {
background-color: #f97316;
}
/* 17. 进度条组 */
.progress-bars {
flex: 2;
min-width: 250px;
display: flex;
flex-direction: column;
gap: 12px;
}
.progress-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.progress-header {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.progress-label {
color: #6b7280;
}
.progress-value {
font-weight: 500;
color: #1f2329;
}
/* 18. 问题分类 */
.problem-category {
margin-bottom: 8px;
}
.section-title {
font-size: 14px;
font-weight: 500;
color: #1f2329;
margin: 0 0 12px 0;
}
.category-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.category-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.category-header {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.category-label {
color: #6b7280;
}
.category-value {
color: #1f2329;
}
/* 19. 最近巡检记录列表 */
.record-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.inspection-record {
border: 1px solid #e5e7eb;
border-radius: 4px;
padding: 12px;
background-color: #f9fafb;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.record-title {
font-size: 14px;
font-weight: 500;
color: #1f2329;
margin: 0;
}
.record-meta {
font-size: 12px;
color: #6b7280;
margin: 0 0 8px 0;
}
.record-summary {
font-size: 12px;
color: #1f2329;
margin: 0 0 8px 0;
line-height: 1.5;
}
.record-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 20. 响应式适配 */
@media (max-width: 1200px) {
.main-content-container {
flex-direction: column;
}
.stat-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.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;
}
}
/* 选项卡样式 */
.tabs-wrapper {
background-color: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* 筛选栏样式 */
.filter-bar {
background-color: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.filter-item {
flex-shrink: 0;
}
.filter-bar .el-select,
.filter-bar .el-date-picker {
width: 150px;
height: 36px;
}
.filter-actions {
margin-left: auto;
display: flex;
gap: 10px;
}
/* 新增实验记录弹窗样式 */
.custom-experiment-dialog {
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
overflow: hidden;
}
.custom-experiment-dialog .el-dialog__header {
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
padding: 20px 24px;
}
.custom-experiment-dialog .el-dialog__title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
.custom-experiment-dialog .el-dialog__body {
padding: 24px;
overflow: visible;
}
.form-container {
padding: 0;
}
.form-row {
display: flex;
gap: 24px;
margin-bottom: 20px;
}
.form-item {
flex: 1;
margin-bottom: 20px !important;
}
.el-form-item__label {
font-size: 14px;
font-weight: 500;
color: #495057;
padding-right: 12px;
}
.form-input {
width: 100%;
border-radius: 6px;
transition: all 0.3s ease;
}
.form-input:focus {
border-color: #165dff;
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
}
/* 设备列表样式 */
.equipment-list {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
width: 100%;
}
.equipment-item {
margin-bottom: 12px;
display: flex;
align-items: center;
padding: 6px 0;
}
.equipment-item:last-child {
margin-bottom: 0;
}
.el-checkbox__label {
font-size: 14px;
color: #495057;
margin-left: 8px;
}
.add-equipment {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
background-color: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.dialog-footer .el-button {
padding: 8px 20px;
border-radius: 6px;
font-size: 14px;
}
.dialog-footer .el-button--primary {
background-color: #165dff;
border-color: #165dff;
}
.dialog-footer .el-button--primary:hover {
background-color: #0d47a1;
border-color: #0d47a1;
}
/* 响应式设计 - 保留必要的覆盖样式 */
@media (max-width: 768px) {
.custom-experiment-dialog {
width: 90% !important;
margin: 0 auto;
}
.form-row {
flex-direction: column;
gap: 0;
}
.add-equipment {
flex-direction: column;
align-items: stretch;
}
.new-equipment-input {
width: 100%;
}
.info-row {
flex-direction: column;
}
.info-item {
min-width: 100%;
}
}
</style>