Files
td_official/src/views/dhr_demo/shiyanguanli.vue

1588 lines
43 KiB
Vue
Raw Normal View History

2025-09-13 18:43:26 +08:00
<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>
<TitleComponent title="实验管理系统" subtitle="制定巡检计划,安排巡检任务,跟进巡检记录"></TitleComponent>
<!-- 选项卡和按钮组合 -->
<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>
<el-button type="primary" class="search-btn"> 搜索 </el-button>
</div>
<div class="action-buttons">
<el-button type="primary" class="create-btn" @click="showRecordDialog = true"> <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>
<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">查看详情</button>
</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">
<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">查看详情</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>
<!-- 6. 分页与试验系统结构一致 -->
<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>
</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.materialName" 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-input v-model="formData.refNumber" placeholder="请输入参考号" class="form-input" />
</el-form-item>
</div>
<!-- 应用编号 -->
<div class="form-row">
<el-form-item label="应用编号" class="form-item">
<el-select v-model="formData.appNumber" placeholder="请选择" class="form-input">
<el-option label="APP-2024-001" value="APP-2024-001" />
<el-option label="APP-2024-002" value="APP-2024-002" />
</el-select>
</el-form-item>
<el-form-item label="应用项目" class="form-item">
<el-input v-model="formData.appProject" placeholder="请输入应用项目" class="form-input" />
</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 label="张明" value="张明" />
<el-option label="李华" value="李华" />
<el-option label="王强" value="王强" />
</el-select>
</el-form-item>
<!-- 参与人员 -->
<el-form-item label="参与人员" class="form-item">
<el-input v-model="formData.participants" placeholder="张明、李华、王强、刘洋、赵云" class="form-input" />
</el-form-item>
<!-- 试验步骤 -->
<el-form-item label="试验步骤" class="form-item" style="width: 100%">
<div class="steps-container">
<div class="step-item" v-for="(step, index) in formData.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.content" placeholder="输入试验步骤" />
</div>
<el-button type="text" size="small" class="add-step-btn" @click="addStep">添加步骤</el-button>
</div>
</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">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue';
import router from '@/router';
import TitleComponent from '@/views/demo/components/TitleComponent.vue';
// 1. 选项卡状态管理
const activeTab = ref('plan'); // 默认为"巡检计划"
const timeRange = ref('month'); // 统计时间范围:月/周/日
// 2. 筛选条件
const filterStatus = ref('all');
const filterType = ref('all');
const dateRange = ref([]);
// 3. 分页参数
const currentPage = ref(1);
const pageSize = ref(20);
const totalRecords = ref(86); // 总记录数(模拟)
// 4. 巡检计划表格数据
const planTableData = ref([
{
name: '数据库月度性能巡检\n编号: INS-20240601',
type: '数据库',
cycle: '每月',
dateRange: '2024-06-01 至 2024-06-30',
progress: 75,
status: 'in-progress',
responsible: '张明'
},
{
name: '服务器日常巡检\n编号: INS-20240602',
type: '服务器',
cycle: '每日',
dateRange: '2024-06-01 至 2024-06-30',
progress: 92,
status: 'in-progress',
responsible: '李华'
},
{
name: '网络设备安全巡检\n编号: INS-20240603',
type: '网络设备',
cycle: '每周',
dateRange: '2024-06-01 至 2024-06-30',
progress: 50,
status: 'in-progress',
responsible: '王强'
},
{
name: '存储设备健康检查\n编号: INS-20240604',
type: '存储设备',
cycle: '每季度',
dateRange: '2024-06-01 至 2024-06-30',
progress: 0,
status: 'drafted',
responsible: '赵伟'
},
{
name: '安全设备策略巡检\n编号: INS-20240501',
type: '安全设备',
cycle: '每月',
dateRange: '2024-05-01 至 2024-05-31',
progress: 100,
status: 'completed',
responsible: '刘芳'
},
{
name: '中间件运行状态巡检\n编号: INS-20240502',
type: '中间件',
cycle: '每周',
dateRange: '2024-05-01 至 2024-05-31',
progress: 100,
status: 'completed',
responsible: '陈明'
},
{
name: '虚拟化平台巡检\n编号: INS-20240605',
type: '虚拟化',
cycle: '每月',
dateRange: '2024-06-01 至 2024-06-30',
progress: 30,
status: 'paused',
responsible: '张丽'
},
{
name: '备份系统有效性检查\n编号: INS-20240606',
type: '备份系统',
cycle: '每两周',
dateRange: '2024-06-01 至 2024-06-30',
progress: 0,
status: 'drafted',
responsible: '李强'
}
]);
// 5. 巡检任务表格数据
const taskTableData = ref([
{
name: 'DB服务器CPU监控',
planName: '数据库月度性能巡检',
type: '数据库',
target: '主数据库服务器',
deadline: '2024-06-15 18:00',
status: 'accepted',
executor: '张明'
},
{
name: '应用服务器内存检查',
planName: '服务器日常巡检',
type: '服务器',
target: '应用集群节点1-3',
deadline: '2024-06-10 24:00',
status: 'completed',
executor: '李华'
},
{
name: '防火墙规则审计',
planName: '网络设备安全巡检',
type: '网络设备',
target: '主防火墙',
deadline: '2024-06-12 18:00',
status: 'pending',
executor: '王强'
},
{
name: '存储阵列容量检查',
planName: '存储设备健康检查',
type: '存储设备',
target: '主存储阵列',
deadline: '2024-06-20 18:00',
status: 'pending',
executor: '赵伟'
},
{
name: 'WAF策略更新检查',
planName: '安全设备策略巡检',
type: '安全设备',
target: 'Web应用防火墙',
deadline: '2024-05-25 18:00',
status: 'completed',
executor: '刘芳'
}
]);
// 6. 统计数据(根据时间范围变化)
const statData = ref({
completed: 42,
problems: 7,
resolved: 5,
avgTime: '45分钟',
completeRate: 68,
resolveRate: 72,
timelyRate: 60
});
// 7. 问题类型分布
const problemCategories = ref([
{ name: '温度异常', rate: 85, color: '#3b82f6' },
{ name: '内存使用率', rate: 62, color: '#10b981' },
{ name: 'CPU负载', rate: 45, color: '#f97316' },
{ name: '响应时间', rate: 30, color: '#8b5cf6' },
{ name: '磁盘空间', rate: 15, color: '#ec4899' }
]);
// 8. 最近巡检记录
const recentRecords = ref([
{
name: '数据库性能巡检',
status: 'normal',
time: '2024-06-09 14:00-16:45',
executor: '张明',
summary: '数据库连接量128阈值500查询响应时间平均0.3S表空间使用率75%,整体状态正常'
},
{
name: '生产服务器日常巡检',
status: 'attention',
time: '2024-06-09 10:00-11:30',
executor: '李华',
summary: 'CPU平均使用率35%峰值59%内存使用率85%需关注磁盘空间62%,服务运行正常'
},
{
name: '网络设备安全巡检',
status: 'problem',
time: '2024-06-08 09:00-10:45',
executor: '王强',
summary: '防火墙部分规则异常,安全补丁需更新,访问控制和流量监控正常,已创建问题单#PRB-2024060801'
}
]);
// 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];
};
// 12. 方法:切换时间范围
const handleTimeRangeChange = (range) => {
timeRange.value = range;
updateStatData(range);
};
// 13. 方法:分页变化
const handlePageChange = (page) => {
currentPage.value = page;
// 实际应用中加载对应页数据
};
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
// 实际应用中重新加载数据
};
// 14. 方法:获取计划状态文本
const getStatusText = (status) => {
const statusMap = {
'drafted': '已起草',
'in-progress': '进行中',
'completed': '已完成',
'paused': '暂停'
};
return statusMap[status] || '';
};
// 15. 方法:获取任务状态文本
const getTaskStatusText = (status) => {
const statusMap = {
'pending': '待接受',
'accepted': '处理中',
'completed': '已完成',
'rejected': '已拒绝'
};
return statusMap[status] || '';
};
// 16. 方法:获取记录状态文本
const getRecordStatusText = (status) => {
const statusMap = {
'normal': '正常',
'attention': '需关注',
'problem': '有问题'
};
return statusMap[status] || '';
};
// 17. 方法:获取进度条颜色
const getProgressColor = (status) => {
const colorMap = { 'drafted': '#ccc', 'in-progress': '#3b82f6', 'completed': '#10b981', 'paused': '#9e9e9e' };
return colorMap[status] || '#ccc';
};
// 18. 新增实验记录弹窗相关
const showRecordDialog = ref(false);
// 表单数据
const formData = ref({
materialName: '',
testNumber: 'EX-20240601',
refNumber: '',
startDate: '',
endDate: '',
appNumber: '',
appProject: '',
testPurpose: '',
envRequirements: '',
manager: '',
participants: '张明、李华、王强、刘洋、赵云',
steps: [{ content: '' }, { content: '' }, { content: '' }],
equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
{ name: '温度控制系统', selected: false },
{ name: '负载生成工具', selected: false }
],
riskMitigation: ''
});
// 新设备输入
const newEquipment = ref('');
// 关闭弹窗
const handleClose = () => {
showRecordDialog.value = false;
};
// 保存实验记录
const handleSave = () => {
// 实际应用中这里应该有表单验证和提交逻辑
console.log('保存实验记录:', formData.value);
showRecordDialog.value = false;
// 保存成功后可以显示提示信息
};
// 添加新步骤
const addStep = () => {
formData.value.steps.push({ content: '' });
};
// 添加新设备
const addEquipment = () => {
if (newEquipment.value.trim()) {
formData.value.equipments.push({ name: newEquipment.value.trim(), selected: false });
newEquipment.value = '';
}
};
</script>
<style scoped>
/* 1. 基础容器样式(继承试验系统) */
.operation-inspection {
padding: 20px;
background-color: #f9fbfd;
min-height: 100vh;
}
/* 2. 顶部导航选项卡 */
.navigation-tabs {
display: flex;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
margin-bottom: 20px;
overflow: hidden;
}
.nav-tab {
padding: 12px 24px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: #6b7280;
flex: 1;
text-align: center;
border-right: 1px solid #f0f0f0;
}
.nav-tab:last-child {
border-right: none;
}
.nav-tab:hover:not(.active) {
background-color: #f3f4f6;
}
.nav-tab.active {
background-color: #165dff;
color: #fff;
font-weight: 500;
}
/* 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 {
background-color: #165dff;
border-color: #165dff;
}
/* 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: #e0efff;
color: #165dff;
}
.status-in-progress {
background-color: #e0f2fe;
color: #0284c7;
}
.status-completed {
background-color: #e6ffed;
color: #00b42a;
}
.status-paused {
background-color: #f2f3f5;
color: #86909c;
}
.status-pending {
background-color: #f9fafb;
color: #6b7280;
}
.status-accepted {
background-color: #eff6ff;
color: #2563eb;
}
.status-rejected {
background-color: #fee2e2;
color: #dc2626;
}
.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;
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);
}
/* 试验步骤样式 */
.steps-container {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
width: 100%;
}
.step-item {
display: flex;
align-items: center;
margin-bottom: 16px;
width: 100%;
}
.step-item:last-child {
margin-bottom: 0;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #165dff;
color: white;
font-size: 16px;
font-weight: 600;
margin-right: 16px;
flex-shrink: 0;
}
.step-input:focus {
border-color: #165dff;
outline: none;
}
.add-step-btn {
color: #165dff;
margin-top: 12px;
width: 100%;
text-align: center;
font-size: 14px;
}
/* 设备列表样式 */
.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%;
}
}
</style>