6 Commits

Author SHA1 Message Date
dhr
3606ab7cf8 0928 2025-09-28 17:31:02 +08:00
dhr
4a31c7d028 0928 2025-09-28 17:23:00 +08:00
dhr
3f07f7afe3 0926 2025-09-26 20:32:14 +08:00
dhr
6b9bfb66b1 0925 2025-09-25 20:03:08 +08:00
dhr
9913a7854c 0924 2025-09-24 16:37:09 +08:00
dhr
80cca114a9 0922 2025-09-23 20:36:47 +08:00
28 changed files with 12903 additions and 3335 deletions

View File

@ -5,7 +5,7 @@ VITE_APP_TITLE = 新能源场站智慧运维平台
VITE_APP_ENV = 'development'
# 开发环境
VITE_APP_BASE_API = 'http://192.168.110.149:18899'
VITE_APP_BASE_API = 'http://192.168.110.210:18899'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'

View File

@ -47,3 +47,11 @@ export const uploadbaoxiu = (data) => {
data: data
});
};
export const baoxiuRecord = (data) => {
return request({
url: '/ops/report/record',
method: 'get',
params: data
});
};

View File

@ -0,0 +1,57 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
//查询列表
export const gongdanlist = (query) => {
return request({
url: '/ops/order/list',
method: 'get',
params: query
});
};
//新增待办事项
export const addgongdan = (data) => {
return request({
url: '/ops/order',
method: 'post',
data: data
});
};
//修改待办事项
export const updategongdan = (data) => {
return request({
url: '/ops/order',
method: 'put',
data: data
});
};
//删除待办事项
export function delgongdan(ids) {
return request({
url: `/ops/order/${ids}`, // 拼接ids作为路径参数
method: 'delete'
});
}
export const gongdanDetail = (id) => {
return request({
url: `/ops/order/${id}`,
method: 'get'
});
};
export const uploadgongdan = (data) => {
return request({
url: '/resource/oss/upload',
method: 'post',
data: data
});
};
export const gongdanRecord = (data) => {
return request({
url: '/ops/order/record',
method: 'get',
params: data
});
};

View File

@ -0,0 +1,57 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
//查询列表
export const jiedianlist = (query) => {
return request({
url: '/ops/node/list',
method: 'get',
params: query
});
};
//新增待办事项
export const addjiedian = (data) => {
return request({
url: '/ops/node',
method: 'post',
data: data
});
};
//修改待办事项
export const updatejiedian = (data) => {
return request({
url: '/ops/node',
method: 'put',
data: data
});
};
//删除待办事项
export function deljiedian(ids) {
return request({
url: `/ops/node/${ids}`, // 拼接ids作为路径参数
method: 'delete'
});
}
export const jiedianDetail = (id) => {
return request({
url: `/ops/node/${id}`,
method: 'get'
});
};
export const uploadjiedian = (data) => {
return request({
url: '/resource/oss/upload',
method: 'post',
data: data
});
};
export const jiedianRecord = (data) => {
return request({
url: '/ops/node/record',
method: 'get',
params: data
});
};

View File

@ -0,0 +1,57 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
//查询列表
export const qiangxiulist = (query) => {
return request({
url: '/ops/repair/list',
method: 'get',
params: query
});
};
//新增待办事项
export const addqiangxiu = (data) => {
return request({
url: '/ops/repair',
method: 'post',
data: data
});
};
//修改待办事项
export const updateqiangxiu = (data) => {
return request({
url: '/ops/repair',
method: 'put',
data: data
});
};
//删除待办事项
export function delqiangxiu(ids) {
return request({
url: `/ops/repair/${ids}`, // 拼接ids作为路径参数
method: 'delete'
});
}
export const qiangxiuDetail = (id) => {
return request({
url: `/ops/repair/${id}`,
method: 'get'
});
};
export const uploadqiangxiu = (data) => {
return request({
url: '/resource/oss/upload',
method: 'post',
data: data
});
};
export const qiangxiuRecord = (data) => {
return request({
url: '/ops/repair/record',
method: 'get',
params: data
});
};

View File

@ -39,3 +39,11 @@ export const syrenwuDetail = (id) => {
method: 'get'
});
};
export const syrenwujilu = (data) => {
return request({
url: '/ops/testTask/record',
method: 'get',
params: data
});
};

View File

@ -34,7 +34,7 @@ export const delxunjian = (ids) => {
//查询人员
export const xunjianUserlist = (query) => {
return request({
url: '/ops/constructionUser/list',
url: '/system/user/list',
method: 'get',
params: query
});

View File

@ -316,3 +316,58 @@ export const removeClass = (ele: HTMLElement, cls: string) => {
export const isExternal = (path: string) => {
return /^(https?:|http?:|mailto:|tel:)/.test(path);
};
/**
* 获取步骤状态对应的样式类
* @param {string|number} status - 步骤状态码
* @returns {string} 样式类名
*/
export const getStatusClass = (status: string | number): string => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap: Record<string, string> = {
'1': 'status-pending',
'2': 'status-executing',
'3': 'status-completed',
'4': 'status-delayed',
'5': 'status-failed'
};
return statusClassMap[statusStr] || 'status-unknown';
};
/**
* 格式化日期时间(用于步骤条)
* @param {string} dateTime - 日期时间字符串
* @returns {string} 格式化后的日期时间
*/
export const formatDateTime = (dateTime: string): string => {
if (!dateTime) return '-';
try {
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (error) {
return dateTime;
}
};
/**
* 获取步骤状态文本
* @param {string|number} status - 步骤状态码
* @returns {string} 状态文本
*/
export const getStepStatusText = (status: string | number): string => {
const statusStr = status?.toString() || '';
const statusMap: Record<string, string> = {
'1': '待执行',
'2': '执行中',
'3': '已完成',
'4': '已延期',
'5': '失败'
};
return statusMap[statusStr] || '未知状态';
};

View File

@ -23,12 +23,18 @@ export const globalHeaders = () => {
};
};
// 设置默认请求头
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
axios.defaults.headers['Accept'] = 'application/json, text/plain, */*';
axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000
timeout: 50000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Accept': 'application/json, text/plain, */*'
}
});
// 请求拦截器

View File

@ -2,7 +2,7 @@
<div>
<div class="operation-inspection">
<!-- 导航标签 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab active" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 子选项卡 -->
<div class="tabs-wrapper">
@ -48,8 +48,8 @@
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button type="primary" icon="el-icon-plus" class="create-btn" @click="handleCreate">手动创建计划</el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="handleSearch"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="handleCreate">手动创建计划</el-button>
</div>
</div>
@ -312,43 +312,101 @@
v-model="detailDialogVisible"
title="巡检计划详情"
width="800px"
class="detail-dialog"
center
:show-close="true"
custom-class="beautified-detail-dialog"
:before-close="handleCloseDetailDialog"
class="custom-experiment-dialog"
>
<div class="detail-content">
<div class="detail-header">
<h3 class="detail-title">{{ detailData.planName || '巡检计划' }}</h3>
<el-tag :type="detailData.status === '1' ? 'success' : 'info'" class="detail-status-tag">
{{ detailData.status === '1' ? '启用' : detailData.status === '2' ? '停用' : '-' }}
</el-tag>
<div 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">
<span class="info-label">计划名称</span>
<span class="info-value">{{ detailData.planName || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value task-status">
<el-tag :type="detailData.status === '1' ? 'success' : 'info'">
{{ detailData.status === '1' ? '启用' : detailData.status === '2' ? '停用' : '-' }}
</el-tag>
</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">计划类型</span>
<span class="info-value">{{ getPlanTypeText(detailData.planType) || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">巡检对象</span>
<span class="info-value">{{ getObjectTypeText(detailData.objectType) || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">巡检频率</span>
<span class="info-value">{{ detailData.inspectionFrequency || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">负责人</span>
<span class="info-value">{{ detailData.nickName || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">开始日期</span>
<span class="info-value">{{ formatDate(detailData.beginTime) || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">结束日期</span>
<span class="info-value">{{ formatDate(detailData.endTime) || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">计划开始时间</span>
<span class="info-value">{{ detailData.planBeginTime || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">持续时间</span>
<span class="info-value">{{ detailData.duration || '-' }}分钟</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">巡检项</span>
<div class="info-value">
<span v-for="(item, index) in detailData.itemVoList" :key="item.id" class="inspection-item-tag">
{{ item.name }}
<span v-if="index < detailData.itemVoList.length - 1" class="item-separator"></span>
</span>
<span v-if="!detailData.itemVoList || detailData.itemVoList.length === 0">-</span>
</div>
</div>
<div class="info-item">
<span class="info-label">电站ID</span>
<span class="info-value">{{ detailData.projectId || '-' }}</span>
</div>
</div>
</div>
</div>
<div class="detail-main">
<el-descriptions :column="{ xs: 1, sm: 1, md: 2, lg: 2 }" class="detail-descriptions" border>
<el-descriptions-item label="计划类型" class="detail-item">{{ getPlanTypeText(detailData.planType) || '-' }}</el-descriptions-item>
<el-descriptions-item label="巡检对象" class="detail-item">{{ getObjectTypeText(detailData.objectType) || '-' }}</el-descriptions-item>
<el-descriptions-item label="巡检频率" class="detail-item">{{ detailData.inspectionFrequency || '-' }}</el-descriptions-item>
<el-descriptions-item label="负责人" class="detail-item">{{ detailData.nickName || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始日期" class="detail-item">{{ formatDate(detailData.beginTime) || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束日期" class="detail-item">{{ formatDate(detailData.endTime) || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划开始时间" class="detail-item">{{ detailData.planBeginTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="持续时间" class="detail-item">{{ detailData.duration || '-' }}分钟</el-descriptions-item>
<el-descriptions-item label="巡检项ID" class="detail-item">{{ detailData.inspectionItemId || '-' }}</el-descriptions-item>
<el-descriptions-item label="电站ID" class="detail-item">{{ detailData.projectId || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="detailData.remark" class="detail-remark">
<h4 class="remark-title">备注信息</h4>
<p class="remark-content">{{ detailData.remark }}</p>
<!-- 备注信息 -->
<div v-if="detailData.remark" class="detail-card">
<h3 class="card-title">备注信息</h3>
<div class="card-content">
<div class="description-content">
{{ detailData.remark }}
</div>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDetailDialog" class="close-btn">关闭</el-button>
<el-button @click="closeDetailDialog">关闭</el-button>
</span>
</template>
</el-dialog>
@ -605,13 +663,14 @@ const formatDate = (dateString) => {
const getUsersList = async () => {
try {
const response = await xunjianUserlist();
const userRows = response?.data?.rows || response?.rows || [];
// 适配新接口格式检查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, index) => ({
label: item.userName || `用户${index + 1}`,
value: item.id || `id_${index}`
.map((item) => ({
label: item.userName || '未知用户',
value: String(item.userId || '') // 使用userId作为唯一标识
}));
if (userList.value.length === 0) {
@ -979,6 +1038,8 @@ const handleInspectionManagement3 = () => {
</script>
<style scoped>
@import url('./css/detail-dialog.css');
@import url('./css/step-bars.css');
.operation-inspection {
padding: 20px;
background-color: #f5f7fa;
@ -1122,47 +1183,127 @@ const handleInspectionManagement3 = () => {
color: #f56c6c;
}
.detail-dialog .el-dialog__body {
/* 弹窗样式 */
.create-plan-dialog .el-dialog__body {
padding: 24px;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
/* 详情弹窗样式 - 与工单列表页面保持一致 */
.custom-experiment-dialog .el-dialog__body {
max-height: 60vh;
overflow-y: auto;
padding: 24px;
}
.task-detail-container {
padding: 10px 0;
}
/* 详情卡片样式 */
.detail-card {
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f2f5;
}
.detail-title {
font-size: 18px;
font-weight: bold;
color: #303133;
.card-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.detail-status-tag {
padding: 4px 12px;
.card-content {
padding: 0 4px;
}
/* 信息行和信息项样式 */
.info-row {
display: flex;
margin-bottom: 16px;
flex-wrap: wrap;
}
.info-item {
flex: 0 0 50%;
margin-bottom: 12px;
display: flex;
align-items: flex-start;
}
.info-item.full-width {
flex: 0 0 100%;
}
.info-label {
font-weight: 500;
color: #86909c;
margin-right: 8px;
min-width: 80px;
flex-shrink: 0;
}
.info-value {
color: #4e5969;
flex: 1;
word-break: break-all;
font-size: 14px;
}
.detail-descriptions {
margin-bottom: 20px;
/* 骨架屏样式 */
.skeleton-loading {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-item .el-descriptions__label {
.skeleton-card {
background-color: #f5f5f5;
border-radius: 8px;
padding: 16px;
}
.skeleton-header {
height: 20px;
width: 30%;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 12px;
}
.skeleton-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-row {
height: 16px;
width: 100%;
background-color: #e0e0e0;
border-radius: 4px;
}
/* 优先级标签样式 */
.task-status {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: #606266;
border: 1px solid transparent;
}
.remark-title {
font-weight: 500;
margin-bottom: 8px;
color: #303133;
}
.remark-content {
.description-content {
padding: 12px;
background-color: #f5f7fa;
background-color: #f9f9f9;
border-radius: 4px;
line-height: 1.6;
color: #4e5969;
font-size: 13px;
}
</style>

View File

@ -2,7 +2,7 @@
<div>
<div class="execution-records">
<!-- 顶部导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,10 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab active" @click="handleInspection7">运维组织</div>
</div>
<!-- 页面标题 -->
<TitleComponent title="运维组织模块" subtitle="实时监控人员状态、车辆状态和班组状态"></TitleComponent>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -182,7 +179,6 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import router from '@/router';
import TitleComponent from './TitleComponent.vue';
import * as echarts from 'echarts'; // 导入ECharts
import renwuImage from '@/assets/images/renwu.png';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="execution-records">
<!-- 顶部导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,10 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab active" @click="handleInspection7">运维组织</div>
</div>
<!-- 页面标题 -->
<TitleComponent title="运维组织模块" subtitle="实时监控人员状态、车辆状态和班组状态"></TitleComponent>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -139,7 +136,6 @@
<script setup>
import { ref, computed } from 'vue';
import router from '@/router';
import TitleComponent from './TitleComponent.vue';
// 搜索和筛选条件
const searchKeyword = ref('');

View File

@ -0,0 +1,374 @@
/* 详情弹窗通用样式 */
/* 详情卡片样式 */
.detail-card {
margin-bottom: 20px;
padding: 20px;
background-color: #fafafa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.card-title {
margin: 0 0 16px 0;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.card-content {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 信息行和信息项样式 */
.info-row {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.info-item {
flex: 1;
min-width: 280px;
}
.info-item.full-width {
min-width: 100%;
}
.info-label {
display: inline-block;
width: 100px;
color: #606266;
font-weight: 500;
}
.info-value {
color: #303133;
word-break: break-word;
}
/* 步骤相关样式 - 详情弹窗专用 - 使用外部CSS样式 */
.task-detail-container .steps-container {
width: 100%;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fff;
}
.task-detail-container .step-item {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 15px;
background-color: #fafafa;
border-radius: 6px;
position: relative;
transition: all 0.3s ease;
}
.task-detail-container .step-item:hover {
background-color: #f5f7fa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.task-detail-container .step-number {
width: 32px;
height: 32px;
background-color: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
z-index: 1;
}
.task-detail-container .step-item:not(:last-child)::after {
content: '';
position: absolute;
top: 50px;
left: 16px;
width: 2px;
height: calc(100% + 5px);
background-color: #e4e7ed;
z-index: 0;
}
.task-detail-container .step-info {
flex: 1;
}
.task-detail-container .step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
.task-detail-container .step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
.task-detail-container .step-time,
.task-detail-container .step-finish-time,
.task-detail-container .step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
.task-detail-container .step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
.step-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
.step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
.step-time,
.step-finish-time,
.step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
/* 步骤状态样式 - 详情弹窗专用 */
.task-detail-container .step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
/* 步骤状态样式 - 待执行 */
.task-detail-container .step-status.status-pending {
background-color: #e6f7ff;
color: #1677ff;
border: 1px solid #91d5ff;
}
/* 步骤状态样式 - 执行中 */
.task-detail-container .step-status.status-executing {
background-color: #fffbe6;
color: #fa8c16;
border: 1px solid #ffe58f;
}
/* 步骤状态样式 - 已完成 */
.task-detail-container .step-status.status-completed {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
/* 步骤状态样式 - 已延期 */
.task-detail-container .step-status.status-delayed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* 通用状态颜色样式 */
.status-pending {
color: #e6a23c;
}
.status-executing {
color: #409eff;
}
.status-completed {
color: #67c23a;
}
.status-delayed {
color: #f56c6c;
}
.status-unknown {
color: #909399;
}
/* 加载状态样式 */
.loading-details {
padding: 20px 0;
}
/* 骨架屏加载 */
.skeleton-loading {
display: flex;
flex-direction: column;
gap: 20px;
}
.skeleton-card {
background-color: #f2f2f2;
border-radius: 8px;
padding: 20px;
animation: skeleton-loading 1.5s infinite;
}
.skeleton-header {
height: 24px;
width: 140px;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 16px;
}
.skeleton-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.skeleton-row {
height: 20px;
background-color: #e0e0e0;
border-radius: 4px;
}
.skeleton-row:nth-child(1) {
width: 70%;
}
.skeleton-row:nth-child(2) {
width: 90%;
}
.skeleton-row:nth-child(3) {
width: 60%;
}
@keyframes skeleton-loading {
0% {
opacity: 0.6;
}
50% {
opacity: 0.3;
}
100% {
opacity: 0.6;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.info-item {
min-width: 100%;
}
.info-row {
gap: 12px;
}
/* 步骤条响应式设计 */
.task-detail-container .steps-container {
padding: 10px;
}
.task-detail-container .step-item {
flex-direction: column;
align-items: flex-start;
padding: 10px;
margin-bottom: 10px;
}
.task-detail-container .step-item > * {
width: 100%;
margin-bottom: 10px;
margin-right: 0 !important;
}
.task-detail-container .step-number {
margin-bottom: 10px;
width: 24px;
height: 24px;
font-size: 12px;
}
.task-detail-container .step-item:not(:last-child)::after {
display: none;
}
}
/* 弹窗按钮样式 */
.dialog-footer .el-button {
padding: 10px 24px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.dialog-footer .el-button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
/* 其他相关样式 */
.fail-reason {
color: #f56c6c;
}
.no-info {
color: #909399;
font-style: italic;
padding: 10px 0;
}
.loading-state {
text-align: center;
padding: 80px 20px;
color: #6c757d;
font-size: 14px;
}
.loading-state i {
display: block;
font-size: 48px;
margin-bottom: 16px;
color: #1677ff;
}
.step-content {
padding: 30px 20px;
background-color: #fafafa;
border-radius: 8px;
margin-top: 20px;
}

View File

@ -0,0 +1,206 @@
/* 步骤容器样式 */
.steps-container {
width: 100%;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fff;
}
/* 单个步骤项样式 */
.step-item {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 15px;
background-color: #fafafa;
border-radius: 6px;
position: relative;
transition: all 0.3s ease;
}
/* 步骤项悬停效果 */
.step-item:hover {
background-color: #f5f7fa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 步骤序号样式 */
.step-number {
width: 32px;
height: 32px;
background-color: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
z-index: 1;
}
/* 步骤连接线样式 */
.step-item:not(:last-child)::after {
content: '';
position: absolute;
top: 50px;
left: 16px;
width: 2px;
height: calc(100% + 5px);
background-color: #e4e7ed;
z-index: 0;
}
/* 步骤内容样式 */
.step-content {
padding: 30px 20px;
background-color: #fafafa;
border-radius: 8px;
margin-top: 20px;
}
/* 步骤信息样式 */
.step-info {
flex: 1;
}
/* 步骤名称样式 */
.step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
/* 步骤目的样式 */
.step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
/* 步骤时间样式 */
.step-time,
.step-finish-time,
.step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
/* 添加步骤按钮样式 */
.add-step-btn {
color: #409eff;
display: block;
margin: 15px auto 0;
padding: 8px 16px;
border-radius: 4px;
transition: all 0.3s ease;
}
.add-step-btn:hover {
color: #66b1ff;
background-color: #ecf5ff;
}
/* 删除步骤按钮样式 */
.delete-step-btn {
color: #f56c6c;
flex-shrink: 0;
}
.delete-step-btn:hover {
color: #ff8590;
background-color: #fef0f0;
}
/* 步骤状态标签样式 */
.step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
/* 步骤状态样式 - 待执行 */
.step-status.status-pending {
background-color: #e6f7ff;
color: #1677ff;
border: 1px solid #91d5ff;
}
/* 步骤状态样式 - 执行中 */
.step-status.status-executing {
background-color: #fffbe6;
color: #fa8c16;
border: 1px solid #ffe58f;
}
/* 步骤状态样式 - 已完成 */
.step-status.status-completed {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
/* 步骤状态样式 - 已延期 */
.step-status.status-delayed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* 响应式设计 - 中等屏幕 */
@media (max-width: 1024px) {
.steps-container {
padding: 15px;
}
.step-item {
padding: 12px;
margin-bottom: 12px;
}
.step-number {
width: 28px;
height: 28px;
font-size: 13px;
margin-right: 12px;
}
}
/* 响应式设计 - 小屏幕 */
@media (max-width: 768px) {
.steps-container {
padding: 10px;
}
.step-item {
flex-direction: column;
align-items: flex-start;
padding: 10px;
margin-bottom: 10px;
}
.step-item > * {
width: 100%;
margin-bottom: 10px;
margin-right: 0 !important;
}
.step-number {
margin-bottom: 10px;
width: 24px;
height: 24px;
font-size: 12px;
}
.step-item:not(:last-child)::after {
display: none;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="box-container">
<!-- 导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab active" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<div class="main-content">
<!-- 左侧日历区域 -->
<div class="calendar-container">
@ -43,7 +43,7 @@
<div class="form-container">
<div class="form-header">
<h2>今日待办</h2>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAddTaskDialog">添加</el-button>
<el-button type="primary" icon="Plus" @click="openAddTaskDialog">添加</el-button>
</div>
<!-- 待办事项列表 - 动态渲染 -->
@ -54,6 +54,7 @@
class="todo-item"
:class="{ 'important': item.taskLevel === '重要', 'completed': item.status === 2 }"
>
<el-checkbox class="todo-checkbox" :checked="item.status === 2" @change="handleStatusChange(item, $event)"></el-checkbox>
<div
class="todo-color-indicator"
:class="{
@ -63,7 +64,6 @@
completed: item.status === 2
}"
></div>
<el-checkbox class="todo-checkbox" :checked="item.status === 2" @change="handleStatusChange(item, $event)"></el-checkbox>
<div class="todo-content">
<div class="todo-main">
<div class="todo-title">{{ item.title }}</div>
@ -590,16 +590,6 @@ const handleInspection7 = () => {
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;
}
/* 已完成任务的样式 */
.todo-color-indicator.completed {
background-color: #dcdfe6;
@ -609,7 +599,15 @@ const handleInspection7 = () => {
color: #909399;
text-decoration: line-through;
}
/* 导航栏样式 */
.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;
@ -849,13 +847,14 @@ const handleInspection7 = () => {
/* 悬停显示操作按钮 */
.todo-item:hover .todo-actions {
opacity: 1;
background: linear-gradient(to right, rgba(173, 216, 230, 0), rgb(64, 158, 255));
right: 0;
opacity: 0.8;
}
/* 内容区域平移以给按钮留出空间 */
/* 取消内容区域平移效果 */
.todo-item:hover .todo-content {
transform: translateX(-120px);
transform: none;
}
.action-icon {
@ -942,7 +941,7 @@ const handleInspection7 = () => {
background-color: #ff4d4f;
}
::v-deep .custom-date-cell {
:deep(.custom-date-cell) {
width: 100%;
height: 100%;
padding: 5px;
@ -983,13 +982,13 @@ const handleInspection7 = () => {
}
/* 穿透作用域,强制设置日历单元格为正方形 */
::v-deep .el-calendar-table td {
:deep(.el-calendar-table td) {
padding: 2px;
vertical-align: top;
width: 120px; /* 强制宽度 */
height: 120px; /* 强制高度(与宽度一致) */
}
::v-deep .el-calendar-day {
:deep(.el-calendar-day) {
padding: 0; /* 移除默认内边距 */
width: 100%;
height: 100%;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="operation-organization">
<!-- 顶部导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,10 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab active" @click="handleInspection7">运维组织</div>
</div>
<!-- 页面标题 -->
<TitleComponent title="运维组织模块" subtitle="实时监控人员状态、车辆状态和班组状态"></TitleComponent>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -133,11 +130,9 @@
<script setup>
import { ref, watch, onMounted } from 'vue';
import router from '@/router';
import TitleComponent from './TitleComponent.vue';
import * as echarts from 'echarts';
// 激活的选项卡
const activeTab = ref('personnel');
//
// 统计数据(保持原有数据不变)
const totalPersonnel = ref(36);
@ -449,36 +444,7 @@ const handleInspectionManagement3 = () => {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.custom-tabs {
padding-top: 1px;
}
.custom-tabs .el-tabs__header {
margin: 0 -20px;
padding: 0 20px;
border-bottom: 1px solid #e4e7ed;
}
.custom-tabs .el-tabs__nav-wrap::after {
height: 0;
}
.custom-tabs .el-tabs__item {
font-size: 14px;
color: #606266;
padding: 16px 20px;
margin-right: 20px;
}
.custom-tabs .el-tabs__item.is-active {
color: #165dff;
font-weight: 500;
border-bottom: 2px solid #165dff;
}
.custom-tabs .el-tabs__item:hover {
color: #165dff;
}
/* */
/* 内容容器样式 */
.content-container {

View File

@ -2,7 +2,7 @@
<div>
<div class="operation-inspection">
<!-- 1. 顶部导航选项卡对应原试验系统的外层导航 -->
<div class="navigation-tabs">
<!-- <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>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 选项卡和按钮组合 -->
<div class="tabs-wrapper">
@ -49,8 +49,8 @@
></el-date-picker>
</div>
<div class="action-buttons">
<el-button type="primary" class="search-btn"> 搜索 </el-button>
<el-button type="primary" class="create-btn" @click="openRecordDialog"> <i class="fas fa-plus"></i> 新增实验记录 </el-button>
<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>
@ -67,13 +67,7 @@
<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="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 align="center" prop="status" label="状态" width="100">
<template #default="scope">
<span :class="['status-tag', `status-${scope.row.status}`]">
@ -374,10 +368,10 @@
</el-form-item>
<el-form-item label="实验对象类型" class="form-item">
<el-select v-model="formData.testObject" placeholder="请选择实验对象类型" class="form-input">
<el-option label="1安全试验" value="1" />
<el-option label="2网络实验" value="2" />
<el-option label="3性能试验" value="3" />
<el-option label="4" value="4" />
<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>
@ -418,17 +412,6 @@
</el-select>
</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">
@ -471,99 +454,113 @@
:close-on-click-modal="false"
:close-on-press-escape="false"
class="custom-experiment-dialog"
center
>
<div class="detail-content">
<!-- 基础信息 -->
<div class="detail-section">
<h3 class="section-title">基础信息</h3>
<div class="detail-grid">
<div class="detail-item">
<label class="detail-label">计划名称:</label>
<span class="detail-value">{{ detailData.planName || '-' }}</span>
<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="detail-item">
<label class="detail-label">计划编号:</label>
<span class="detail-value">{{ detailData.planCode || '-' }}</span>
<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="detail-item">
<label class="detail-label">实验对象:</label>
<span class="detail-value">{{ getTestObjectText(detailData.testObject) || '-' }}</span>
</div>
<div class="detail-item">
<label class="detail-label">负责人:</label>
<span class="detail-value">{{ detailData.person?.userName || '-' }}</span>
</div>
<div class="detail-item">
<label class="detail-label">开始时间:</label>
<span class="detail-value">{{ detailData.beginTime ? formatDate(detailData.beginTime) : '-' }}</span>
</div>
<div class="detail-item">
<label class="detail-label">结束时间:</label>
<span class="detail-value">{{ detailData.endTime ? formatDate(detailData.endTime) : '-' }}</span>
<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-section">
<h3 class="section-title">实验设备</h3>
<div class="device-list">
<span v-for="(device, index) in detailData.testDevice.split(',')" :key="index" class="device-tag">
{{ device.trim() }}
</span>
</div>
</div>
<!-- 实验步骤 -->
<div v-if="detailData.testStep" class="detail-section">
<h3 class="section-title">实验步骤</h3>
<div class="steps-container">
<div v-for="(step, index) in detailData.testStep.split(',')" :key="index" class="step-item">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">{{ step.trim() }}</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-section">
<h3 class="section-title">实验信息</h3>
<div class="detail-textarea">
<label class="detail-label">实验说明:</label>
<div class="detail-text">{{ detailData.testInfo || '-' }}</div>
</div>
<div class="detail-textarea">
<label class="detail-label">实验设置:</label>
<div class="detail-text">{{ detailData.testSetting || '-' }}</div>
</div>
<div class="detail-textarea">
<label class="detail-label">解决方案:</label>
<div class="detail-text">{{ detailData.testSolutions || '-' }}</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-section">
<h3 class="section-title">参与人员</h3>
<div class="participant-list">
<div v-for="(person, index) in detailData.persons" :key="person.id" class="participant-item">
<span class="participant-name">{{ person.userName }}</span>
<span class="participant-team">{{ person.teamName }}</span>
<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-section">
<h3 class="section-title">巡检项目</h3>
<div class="inspection-list">
<div v-for="(item, index) in detailData.inspectionItemList" :key="item.id" class="inspection-item">
<span class="inspection-name">{{ item.name }}</span>
<span class="inspection-type">{{ item.type }}</span>
<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>
@ -811,7 +808,11 @@ const formData = ref({
envRequirements: '',
manager: '',
participants: [], // 改为数组存储多选的用户ID
steps: [{ content: '' }, { content: '' }, { content: '' }],
steps: [
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' }
],
equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
@ -839,20 +840,14 @@ const userList = ref([]);
const getUsersList = async () => {
try {
const response = await xunjianUserlist();
const userRows =
response?.data?.rows && Array.isArray(response.data.rows)
? response.data.rows
: response?.rows && Array.isArray(response.rows)
? response.rows
: Array.isArray(response)
? response
: [];
// 适配新接口格式检查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, index) => ({
label: item.userName || `用户${index + 1}`,
value: item.id || `id_${index}`
.map((item) => ({
label: item.userName || '未知用户',
value: String(item.userId || '') // 使用userId作为唯一标识
}));
if (userList.value.length === 0) {
@ -914,10 +909,6 @@ const handleSave = async () => {
personIds: formData.value.participants.join(','),
inspectionItems: '',
testSolutions: formData.value.riskMitigation,
testStep: formData.value.steps
.filter((step) => step.content.trim())
.map((step) => step.content)
.join(','),
testDevice: formData.value.equipments
.filter((equip) => equip.selected)
.map((equip) => equip.name)
@ -927,8 +918,9 @@ const handleSave = async () => {
id: editRecordId.value // 若后端用planId等需改为对应字段名
};
// 4. 调用接口
// 调用接口
let response;
if (editRecordId.value) {
// 编辑模式:调用更新接口
response = await updateshiyan(requestData);
@ -965,7 +957,6 @@ const resetForm = () => {
envRequirements: '', // 环境要求为空
manager: '', // 负责人为空
participants: [], // 参与人员为空数组
steps: [{ content: '' }, { content: '' }, { content: '' }], // 步骤内容为空
equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
@ -1041,24 +1032,6 @@ const handleEditRecord = async (row) => {
const recordDetail = detailResponse.data.rows?.[0] || detailResponse.data;
// 兼容两种数据结构可能在rows数组中也可能直接在data中
// 3. 处理testStep将逗号分隔的字符串转换为步骤数组
const steps = [];
if (recordDetail.testStep) {
// 拆分字符串(例如 "1. 213,2. 321" → ["1. 213", "2. 321"]
const stepItems = recordDetail.testStep.split(',');
stepItems.forEach((stepText) => {
// 移除序号前缀(如"1. "),只保留内容
const content = stepText.replace(/^\d+\.\s*/, '').trim();
if (content) {
steps.push({ content });
}
});
}
// 确保至少有3个步骤如果解析后为空
while (steps.length < 3) {
steps.push({ content: '' });
}
// 4. 处理testDevice将逗号分隔的字符串转换为设备数组
const equipments = [];
if (recordDetail.testDevice) {
@ -1104,7 +1077,6 @@ const handleEditRecord = async (row) => {
envRequirements: recordDetail.envRequirements || recordDetail.testSetting || '',
manager: recordDetail.manager || recordDetail.personCharge || '',
participants: participants, // 从personIds解析的数组
steps: steps, // 解析后的步骤数组
equipments: equipments, // 解析并合并后的设备数组
riskMitigation: recordDetail.riskMitigation || recordDetail.testSolutions || ''
};
@ -1134,7 +1106,18 @@ const handleEditRecord = async (row) => {
};
// 添加新步骤
const addStep = () => {
formData.value.steps.push({ content: '' });
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);
};
// 添加新设备
@ -1231,10 +1214,24 @@ const formatDate = (dateString) => {
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>
/* 1. 基础容器样式(继承试验系统) */
@import url('./css/detail-dialog.css');
.operation-inspection {
padding: 20px;
background-color: #f9fbfd;
@ -1276,7 +1273,7 @@ const formatDate = (dateString) => {
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
}
/* 3. 页面标题(与试验系统一致) */
/* 3. 页面标题 */
.page-header {
margin-bottom: 20px;
}
@ -1908,53 +1905,6 @@ const formatDate = (dateString) => {
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;
@ -2013,7 +1963,7 @@ const formatDate = (dateString) => {
border-color: #0d47a1;
}
/* 响应式设计 */
/* 响应式设计 - 保留必要的覆盖样式 */
@media (max-width: 768px) {
.custom-experiment-dialog {
width: 90% !important;
@ -2033,222 +1983,13 @@ const formatDate = (dateString) => {
.new-equipment-input {
width: 100%;
}
}
/* 详情弹窗样式 */
.custom-experiment-dialog .el-dialog__body {
padding: 20px;
overflow: hidden;
}
.detail-content {
max-height: 600px;
overflow-y: auto;
padding-right: 8px;
}
/* 详情区块 */
.detail-section {
margin-bottom: 24px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #ffffff;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1890ff;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e8f4ff;
}
/* 基础信息网格 */
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-label {
font-size: 13px;
font-weight: 500;
color: #6c757d;
}
.detail-value {
font-size: 14px;
color: #2c3e50;
padding: 4px 0;
}
/* 文本区域 */
.detail-textarea {
margin-bottom: 16px;
}
.detail-text {
font-size: 14px;
color: #495057;
line-height: 1.6;
padding: 8px 0;
min-height: 60px;
white-space: pre-wrap;
word-break: break-word;
}
/* 设备列表样式 */
.device-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.device-tag {
display: inline-block;
padding: 6px 12px;
background-color: #f0f9ff;
color: #1890ff;
border: 1px solid #bae7ff;
border-radius: 16px;
font-size: 13px;
}
/* 步骤条样式 */
.steps-container {
padding-left: 8px;
}
.step-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
position: relative;
}
.step-item:last-child {
margin-bottom: 0;
}
.step-item:not(:last-child)::after {
content: '';
position: absolute;
left: 17px;
top: 36px;
bottom: -16px;
width: 2px;
background-color: #e4e7ed;
z-index: 1;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #1890ff;
color: white;
font-size: 14px;
font-weight: 600;
margin-right: 16px;
flex-shrink: 0;
z-index: 2;
}
.step-content {
flex: 1;
padding: 8px 16px;
background-color: #fafafa;
border-radius: 6px;
font-size: 14px;
color: #2c3e50;
line-height: 1.5;
}
/* 列表样式 */
.participant-list,
.inspection-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.participant-item,
.inspection-item {
display: flex;
align-items: center;
gap: 24px;
padding: 12px 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.participant-name,
.inspection-name {
font-size: 14px;
font-weight: 500;
color: #2c3e50;
min-width: 120px;
}
.participant-team,
.participant-role,
.inspection-type {
font-size: 13px;
color: #6c757d;
}
.participant-item,
.inspection-item {
display: flex;
align-items: center;
gap: 24px;
padding: 12px 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.participant-name,
.inspection-name {
font-size: 14px;
font-weight: 500;
color: #2c3e50;
min-width: 120px;
}
.participant-team,
.participant-role,
.inspection-type {
font-size: 13px;
color: #6c757d;
}
/* 详情弹窗响应式设计 */
@media (max-width: 768px) {
.detail-grid {
grid-template-columns: 1fr;
}
.participant-item,
.inspection-item {
.info-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.participant-name,
.inspection-name {
min-width: auto;
.info-item {
min-width: 100%;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="inspection-tasks">
<!-- 导航栏 -->
<div class="navigation-tabs">
<!-- <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>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -29,7 +29,7 @@
<el-option label="待执行" value="1"></el-option>
<el-option label="执行中" value="4"></el-option>
<el-option label="已延期" value="2"></el-option>
<!-- 接口暂停对应页面已延期 -->
<el-option label="已完成" value="5"></el-option>
<el-option label="失败" value="3"></el-option>
</el-select>
@ -49,8 +49,8 @@
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button type="primary" icon="el-icon-plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="handleSearch"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
</div>
</div>
</div>
@ -72,49 +72,83 @@
</div>
<div class="task-details">
<div class="detail-item">
<span class="detail-label">计划时间</span>
<span class="detail-value">{{ task.planTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">测试对象</span>
<span class="detail-value">{{ task.target }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行人</span>
<span class="detail-value">{{ task.executor }}</span>
</div>
<div class="detail-item">
<span class="detail-label">关联计划</span>
<span class="detail-value">{{ task.relatedPlan }}</span>
</div>
<!-- 已延期暂停原因 -->
<div v-if="task.status === '2'" class="delay-reason">
<span class="detail-label">延期原因</span>
<span class="detail-value">{{ task.delayReason || '未填写' }}</span>
</div>
<!-- 执行中进度 -->
<div v-if="task.status === '4'" class="progress-container">
<span class="detail-label">完成进度</span>
<div class="progress-bar" style="flex: 1; padding-left: 0; margin-top: 0">
<el-progress :percentage="task.progress || 0" stroke-width="6" :stroke-color="task.progressColor"></el-progress>
<!-- 失败卡片特殊展示 -->
<div v-if="task.status === '3'" class="failed-task-details">
<div class="detail-item">
<span class="detail-label">失败时间</span>
<span class="detail-value">{{ task.failTime || '未记录' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">试验阶段</span>
<span class="detail-value">{{ task.testStage || '未记录' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行人</span>
<span class="detail-value">{{ task.executor }}</span>
</div>
<div class="detail-item failed-reason-item">
<span class="detail-label">失败原因</span>
<span class="detail-value failed-reason">{{ task.originalData?.failReason || '未填写' }}</span>
</div>
</div>
<!-- 已完成/失败结果 -->
<div v-if="task.status === '5' || task.status === '3'" class="task-result">
<span class="detail-label">结果</span>
<span class="detail-value" :class="task.resultClass">{{ task.result }}</span>
<!-- 其他状态的卡片展示 -->
<div v-else>
<div class="detail-item">
<span class="detail-label">计划时间</span>
<span class="detail-value">{{ task.planTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">测试对象</span>
<span class="detail-value">{{ task.target }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行人</span>
<span class="detail-value">{{ task.executor }}</span>
</div>
<div class="detail-item">
<span class="detail-label">关联计划</span>
<span class="detail-value">{{ task.relatedPlan }}</span>
</div>
<!-- 已延期暂停原因 -->
<div v-if="task.status === '2'" class="delay-reason">
<span class="detail-label">延期原因</span>
<span class="detail-value">{{ task.delayReason || '未填写' }}</span>
</div>
<!-- 执行中进度 -->
<div v-if="task.status === '4'" class="progress-container">
<span class="detail-label">完成进度</span>
<div class="progress-bar" style="flex: 1; padding-left: 0; margin-top: 0">
<el-progress :percentage="task.progress || 0" stroke-width="6" :stroke-color="task.progressColor"></el-progress>
</div>
</div>
<!-- 已完成结果 -->
<div v-if="task.status === '5'" class="task-result">
<span class="detail-label">结果</span>
<span class="detail-value" :class="task.resultClass">{{ task.result }}</span>
</div>
</div>
</div>
<div class="task-actions">
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
<!-- 失败卡片的特殊操作按钮 -->
<div v-if="task.status === '3'" class="failed-task-actions">
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
</div>
<!-- 其他状态的操作按钮 -->
<div v-else>
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
</div>
</div>
</div>
</div>
@ -137,7 +171,7 @@
</div>
<!-- 添加新任务弹窗 -->
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="500px" :before-close="handleCancelCreateTask">
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="750px" :before-close="handleCancelCreateTask">
<el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" />
@ -192,6 +226,27 @@
style="width: 100%"
/>
</el-form-item>
<!-- 步骤条区域 -->
<el-form-item label="执行步骤" prop="steps">
<div class="steps-container">
<div class="step-item" v-for="(step, index) in createTaskForm.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.name" placeholder="输入步骤名称" style="flex: 1; margin-right: 10px" />
<el-input v-model="step.intendedPurpose" placeholder="输入预期目的" style="flex: 1; margin-right: 10px" />
<el-date-picker
v-model="step.intendedTime"
type="datetime"
placeholder="选择计划时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
style="width: 180px; margin-right: 10px"
/>
<el-button v-if="createTaskForm.steps.length > 1" type="text" @click="removeStep(index)" style="color: #f56c6c"> 删除 </el-button>
</div>
<el-button type="text" class="add-step-btn" @click="addStep">添加步骤</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@ -215,7 +270,7 @@
</div>
<div class="info-item">
<span class="info-label">任务状态</span>
<span class="info-value" :class="getStatusClass(detailData.status)">
<span class="info-value" :class="getTaskStatusClass(detailData.status)">
{{ getStatusText(detailData.status) }}
</span>
</div>
@ -260,7 +315,7 @@
</div>
<div class="info-item">
<span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.personInfo.phone }}</span>
<span class="info-value">{{ detailData.personInfo.phonenumber }}</span>
</div>
</div>
<div v-if="detailData.personInfo" class="info-row">
@ -268,10 +323,6 @@
<span class="info-label">性别</span>
<span class="info-value">{{ detailData.personInfo.sex === '1' ? '男' : '女' }}</span>
</div>
<div class="info-item">
<span class="info-label">民族</span>
<span class="info-value">{{ detailData.personInfo.nation }}</span>
</div>
</div>
<div v-else class="no-info">暂无执行人信息</div>
</div>
@ -307,6 +358,26 @@
</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>
@ -335,19 +406,48 @@
</span>
</template>
</el-dialog>
<!-- 日志弹窗 -->
<el-dialog v-model="logsDialogVisible" title="任务执行日志" width="700px" :close-on-click-modal="false">
<div v-if="!logsLoading" class="logs-container">
<div v-if="logsData.length > 0" class="logs-list">
<div v-for="(log, index) in logsData" :key="index" class="log-item">
<div class="log-time">{{ log.timestamp || '-' }}</div>
<div class="log-content">{{ log.content || '-' }}</div>
</div>
</div>
<div v-else class="no-logs">
<el-empty description="暂无执行日志" />
</div>
</div>
<div v-else class="loading-logs">
<el-skeleton :count="5" class="log-skeleton" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="logsDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import { ref, computed, onMounted } from 'vue';
import router from '@/router';
// 引入已定义的接口函数
import { syrenwulist, syrenwuDetail, addsyrenwu, updatesyrenwu } from '@/api/zhinengxunjian/shiyan/renwu';
import { shiyanlist } from '@/api/zhinengxunjian/shiyan';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index';
import { addjiedian } from '@/api/zhinengxunjian/jiedian/index';
// 引入Element Plus组件提示/空状态/骨架屏/弹窗)
import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox } from 'element-plus';
import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox, ElDialog } from 'element-plus';
// 日志弹窗相关变量
const logsDialogVisible = ref(false);
const logsData = ref([]);
const logsLoading = ref(false);
/**
* 根据任务ID获取完整的任务详情数据
@ -379,6 +479,24 @@ const loading = ref(false);
// 筛选条件(与接口参数对应)
const taskStatus = ref(''); // 任务状态1=待执行2=暂停已延期3=失败4=执行中5=已完成
const planType = ref(''); // 关联计划ID1=每日2=每周3=每月
/**
* 将节点数据按模块分组
* @param {Array} nodes - 节点数据数组
* @returns {Array} 分组后的模块数组
*/
const groupNodesByModule = (nodes) => {
if (!nodes || !Array.isArray(nodes)) {
return [];
}
// 这里简单地将所有节点放在一个默认模块下实际应用中可以根据节点数据的module字段进行分组
const defaultGroup = {
module: '测试步骤',
items: nodes
};
return [defaultGroup];
};
const executor = ref('all'); // 执行人IDall=全部
// 用户列表通过xunjianUserlist接口获取
@ -420,15 +538,58 @@ const getStatusText = (status) => {
* @param {string} status - 任务状态码
* @returns {string} 样式类名
*/
/**
* 获取步骤状态对应的样式类
* @param {string|number} status - 步骤状态码
* @returns {string} 样式类名
*/
const getStatusClass = (status) => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap = {
'1': 'status-pending',
'2': 'status-delayed',
'3': 'status-failed',
'4': 'status-running',
'5': 'status-completed'
'3': 'status-executing',
'4': 'status-completed'
};
return statusClassMap[status] || '';
return statusClassMap[statusStr] || 'status-unknown';
};
/**
* 格式化日期时间(用于步骤条)
* @param {string} dateTime - 日期时间字符串
* @returns {string} 格式化后的日期时间
*/
const formatDateTime = (dateTime) => {
if (!dateTime) return '-';
try {
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (error) {
return dateTime;
}
};
/**
* 获取步骤状态文本
* @param {string|number} status - 步骤状态码
* @returns {string} 状态文本
*/
const getStepStatusText = (status) => {
const statusStr = status?.toString() || '';
const statusMap = {
'1': '待执行',
'2': '执行中',
'3': '已完成',
'4': '已延期'
};
return statusMap[statusStr] || '未知状态';
};
// 创建任务弹窗
@ -441,7 +602,8 @@ const createTaskForm = ref({
relatedPlan: '', // 关联计划ID接口testPlanId
executor: '', // 执行人ID接口person
workTimeRange1: null, // 工作时间段1
workTimeRange2: null // 工作时间段2
workTimeRange2: null, // 工作时间段2
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }] // 步骤数据数组
});
// 创建任务表单规则
const createTaskRules = {
@ -453,6 +615,21 @@ const createTaskRules = {
executor: [{ required: true, message: '请选择执行人', trigger: 'change' }]
};
// 添加步骤
const addStep = () => {
createTaskForm.value.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
};
// 删除步骤
const removeStep = (index) => {
// 确保至少保留一个步骤
if (createTaskForm.value.steps.length <= 1) {
ElMessage.warning('至少需要保留一个步骤');
return;
}
createTaskForm.value.steps.splice(index, 1);
};
// 构建timeInfo字符串
const getTaskTimeInfoString = () => {
const timeInfoArray = [];
@ -482,36 +659,13 @@ const getUsersList = async () => {
try {
const response = await xunjianUserlist({});
if (response.code === 200) {
// 从任务数据中提取用户信息
const usersMap = new Map(); // 使用Map确保id唯一
const tasks = response.rows || [];
// 直接从接口返回的用户列表中提取信息
const users = response.rows || [];
tasks.forEach((task) => {
// 提取personInfo中的用户信息
if (task.personInfo && task.personInfo.id && task.personInfo.userName) {
usersMap.set(task.personInfo.id, {
id: task.personInfo.id,
userName: task.personInfo.userName
});
}
// 提取testPlan.persons中的用户信息
if (task.testPlan && task.testPlan.persons && Array.isArray(task.testPlan.persons)) {
task.testPlan.persons.forEach((person) => {
if (person.id && person.userName) {
usersMap.set(person.id, {
id: person.id,
userName: person.userName
});
}
});
}
});
// 将Map转换为下拉选择器需要的格式{ label, value }
userList.value = Array.from(usersMap.values()).map((user) => ({
label: user.userName, // 显示在下拉框中的文本
value: user.id // 选中后的值
// 将用户数据转换为所需格式包含id和userName以适配模板和getUserById函数
userList.value = users.map((user) => ({
id: user.userId, // 用于标识和查找
userName: user.userName // 显示名称
}));
// 调试信息,确认数据格式正确
@ -604,9 +758,9 @@ const mapApiToView = (apiData) => {
},
'3': {
statusText: '失败',
cardClass: 'card-delayed',
tagClass: 'tag-delayed',
actionText: '重',
cardClass: 'card-failed',
tagClass: 'tag-failed',
actionText: '重新执行',
actionClass: 'reschedule-btn',
result: '失败',
resultClass: 'result-abnormal'
@ -663,6 +817,46 @@ const mapApiToView = (apiData) => {
executorName = getUserById(apiData.person);
}
// 格式化失败时间
const formatFailTime = (timeStr) => {
if (timeStr) {
const date = new Date(timeStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(
date.getHours()
).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
return '未记录';
};
// 生成试验阶段信息
const getTestStage = () => {
try {
// 优先查找nodes数组中status为2的第一条数据
if (apiData && apiData.nodes && Array.isArray(apiData.nodes)) {
const firstStatusTwoNode = apiData.nodes.find((node) => {
// 确保node存在且有status属性
if (!node || node.status === undefined) return false;
// 处理status可能是字符串或数字的情况
return node.status === '2' || node.status === 2;
});
if (firstStatusTwoNode && firstStatusTwoNode.name) {
return firstStatusTwoNode.name;
}
}
// 如果没有找到符合条件的nodes数据检查是否有明确的试验阶段信息
if (apiData && apiData.testStage) {
return apiData.testStage;
}
// 如果没有明确的阶段信息,尝试从关联计划中获取
if (apiData && apiData.testPlan && apiData.testPlan.stage) {
return apiData.testPlan.stage;
}
} catch (error) {
console.error('获取试验阶段信息失败:', error);
}
return '未记录';
};
return {
id: apiData.id, // 任务IDv-for的key唯一标识
title: apiData.taskName || '未命名任务', // 任务名称
@ -682,7 +876,10 @@ const mapApiToView = (apiData) => {
actionText: statusConfig.actionText,
actionClass: statusConfig.actionClass,
testFinal: apiData.testFinal, // 结果(用于详情页)
originalData: apiData // 保存原始数据,用于后续操作
originalData: apiData, // 保存原始数据,用于后续操作
// 失败卡片特有字段
failTime: formatFailTime(apiData.failTime),
testStage: getTestStage()
};
};
@ -762,9 +959,18 @@ const handleAction = async (task) => {
id: task.id
};
// 声明resultType变量提升作用域
let resultType = null;
// 3. 根据任务状态只修改状态相关的字段
if (task.status === '4') {
// 执行中 → 完成:使用弹窗确认结果
try {
// 保持原有结构
} catch (error) {
console.error('捕获到异常:', error);
}
try {
const confirmResult = await ElMessageBox.confirm('请选择试验结果', '完成试验', {
confirmButtonText: '正常',
@ -776,12 +982,47 @@ const handleAction = async (task) => {
updateParams.status = '5';
updateParams.progress = 100;
updateParams.testFinal = '正常';
resultType = 'normal'; // 现在在外部作用域中定义
} catch (error) {
if (error === 'cancel') {
// 用户点击取消(异常)
updateParams.status = '5';
updateParams.progress = 100;
updateParams.testFinal = '异常';
// 用户点击取消(异常),弹出失败原因输入框
try {
const failReasonResult = await ElMessageBox.prompt('请输入失败原因', '试验异常', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
inputPlaceholder: '请详细描述失败原因...',
inputValidator: (value) => {
if (!value || value.trim() === '') {
return '失败原因不能为空';
}
return true;
}
});
// 用户输入了失败原因并确认
updateParams.status = '3';
updateParams.progress = '';
updateParams.testFinal = '异常';
updateParams.failReason = failReasonResult.value; // 绑定失败原因参数
updateParams.failTime = formatLocalDateTime(new Date()); // 记录失败时间
resultType = 'abnormal';
// 将第一条未完成的步骤状态改为3失败
if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) {
const firstUnfinishedNode = taskDetails.nodes.find((node) => {
return node.status === '2' || node.status === 2;
});
if (firstUnfinishedNode) {
firstUnfinishedNode.status = '3';
// 确保更新到updateParams中
updateParams.nodes = [...taskDetails.nodes]; // 创建新数组以确保引用变更被检测到
}
}
} catch (innerError) {
// 用户取消了失败原因输入
return;
}
} else {
// 关闭弹窗,不执行操作
return;
@ -792,21 +1033,51 @@ const handleAction = async (task) => {
switch (task.status) {
case '1': // 待执行 → 开始执行状态改为4
updateParams.status = '4';
updateParams.progress = 10; // 初始进度10%
updateParams.progress = 0; // 初始进度10%
// 设置开始时间为当前时间使用本地时间而非UTC时间
updateParams.planBeginTime = formatLocalDateTime(new Date());
break;
case '2': // 已延期 → 重新安排状态改为1重置时间
updateParams.status = '1';
updateParams.beginTime = new Date().toISOString().slice(0, 16).replace('T', ' ');
updateParams.beginTime = formatLocalDateTime(new Date());
break;
case '3': // 失败 → 重试状态改为1
updateParams.status = '1';
// 清空失败相关字段,使用适合各字段数据类型的默认值
updateParams.failReason = '';
updateParams.failTime = ''; // 时间类型字段使用null
updateParams.failPhase = ''; // 整数类型字段使用0
// 将失败的步骤状态改回2未完成
if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) {
// 创建新数组以确保引用变更被检测到
const updatedNodes = taskDetails.nodes.map((node) => {
if (node.status === '3' || node.status === 3) {
return { ...node, status: '2' };
}
return node;
});
// 更新taskDetails和updateParams
taskDetails.nodes = updatedNodes;
updateParams.nodes = updatedNodes;
}
break;
default:
return;
}
}
// 调用更新接口
// 对于执行中状态('4')的任务,预先设置好时间字段
if (task.status === '4') {
// 根据结果类型设置相应的时间使用本地时间而非UTC时间
if (resultType === 'normal') {
updateParams.planFinishTime = formatLocalDateTime(new Date());
} else if (resultType === 'abnormal') {
updateParams.failTime = formatLocalDateTime(new Date());
}
}
// 调用更新接口(只调用一次)
const response = await updatesyrenwu(updateParams);
if (response.code === 200) {
ElMessage.success(`任务${task.actionText}成功`);
@ -819,6 +1090,20 @@ const handleAction = async (task) => {
}
};
/**
* 格式化本地日期时间为 'YYYY-MM-DD HH:mm' 格式
* @param {Date} date - 日期对象
* @returns {string} 格式化后的日期时间字符串
*/
const formatLocalDateTime = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
/**
* 打开创建任务弹窗
*/
@ -854,6 +1139,48 @@ const handleSaveTask = async () => {
return;
}
// 验证所有步骤
const hasEmptyStep = createTaskForm.value.steps.some((step) => !step.name.trim() || !step.intendedPurpose.trim());
if (hasEmptyStep) {
ElMessage.warning('请填写完整所有步骤信息');
return;
}
// 处理步骤数据
let nodeIds = '';
if (createTaskForm.value.steps && createTaskForm.value.steps.length > 0) {
// 过滤非空步骤并映射为所需格式
const validSteps = createTaskForm.value.steps
.filter((step) => step.name.trim() && step.intendedPurpose.trim())
.map((step, index) => ({
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
params: {},
module: 3,
code: index + 1,
name: step.name,
intendedPurpose: step.intendedPurpose,
intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
finishTime: '',
remark: '',
status: 2
}));
if (validSteps.length > 0) {
try {
// 调用addjiedian接口获取nodeIds
const jiedianResponse = await addjiedian(validSteps);
if (jiedianResponse.code === 200 && jiedianResponse.msg) {
nodeIds = jiedianResponse.msg; // 直接使用字符串格式,不转换为数组
}
} catch (error) {
console.error('添加节点失败:', error);
ElMessage.error('添加执行步骤失败');
return;
}
}
}
const createParams = {
createDept: 0, // 可根据实际情况从全局状态获取
createBy: 0, // 可根据实际情况从全局状态获取当前用户ID
@ -874,19 +1201,20 @@ const handleSaveTask = async () => {
status: '1', // 初始状态:待执行(必需)
testPlanId: createTaskForm.value.relatedPlan, // 关联计划ID必需
testSetting: '', // 测试设置
planBeginTime: createTaskForm.value.timeRange[0], // 计划开始时间
planBeginTime: '', // 计划开始时间(新增时为空)
progress: 0, // 初始进度0%
failReason: '',
failTime: now.toISOString(),
failPhase: 0,
failTime: '', // 失败时间(新增时为空)
failPhase: '',
faileAnalyze: '',
faileTips: '',
testLongTime: 0,
testFinal: '',
finalInfo: '',
pauseFor: '',
pauseTime: now.toISOString(),
planFinishTime: createTaskForm.value.timeRange[1] // 计划完成时间
pauseTime: '', // 暂停时间(新增时为空)
planFinishTime: '', // 计划完成时间(新增时为空)
nodeIds: nodeIds // 步骤节点ID数组
};
// 3. 调用创建接口
@ -927,7 +1255,10 @@ const handleCancelCreateTask = () => {
inspectionTarget: '',
timeRange: [],
relatedPlan: '',
executor: ''
executor: '',
workTimeRange1: null,
workTimeRange2: null,
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
};
};
@ -974,9 +1305,24 @@ onMounted(() => {
const pagedTasks = computed(() => {
return tasks.value;
});
// 获取任务状态对应的CSS类
const getTaskStatusClass = (status) => {
const statusStr = status?.toString() || '';
const statusMap = {
'1': 'status-pending',
'2': 'status-delayed',
'3': 'status-failed',
'4': 'status-running',
'5': 'status-completed'
};
return statusMap[statusStr] || 'status-pending';
};
</script>
<style scoped>
@import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
/* 原有样式不变,新增无数据提示样式 */
.inspection-tasks {
padding: 20px;
@ -1077,6 +1423,10 @@ const pagedTasks = computed(() => {
box-shadow: 0 4px 16px rgba(82, 196, 26, 0.15);
}
.card-failed {
box-shadow: 0 4px 16px rgba(255, 77, 79, 0.15);
}
/* 左侧状态线颜色 */
.card-pending::before {
background-color: #1677ff;
@ -1090,6 +1440,9 @@ const pagedTasks = computed(() => {
.card-completed::before {
background-color: #52c41a;
}
.card-failed::before {
background-color: #ff4d4f;
}
/* 卡片悬停效果 */
.task-card:hover {
@ -1146,6 +1499,12 @@ const pagedTasks = computed(() => {
border-color: #b7eb8f;
}
.tag-failed {
background-color: #fff2f0;
color: #ff4d4f;
border-color: #ffccc7;
}
.task-details {
margin-bottom: 16px;
}
@ -1229,24 +1588,26 @@ const pagedTasks = computed(() => {
color: #165dff;
}
.start-btn {
background-color: #165dff;
border-color: #165dff;
/* 失败卡片特殊样式 */
.failed-task-details {
margin-bottom: 16px;
}
.reschedule-btn {
background-color: #ff7d00;
border-color: #ff7d00;
.failed-reason-item {
padding-top: 8px;
border-top: 1px dashed #f0f2f5;
}
.complete-btn {
background-color: #00b42a;
border-color: #00b42a;
.failed-reason {
color: #f53f3f;
font-weight: 500;
}
.report-btn {
background-color: #86909c;
border-color: #86909c;
.failed-task-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
}
/* 分页区域样式 */
@ -1302,6 +1663,46 @@ const pagedTasks = computed(() => {
margin-bottom: 30px;
}
/* 日志弹窗样式 */
.logs-container {
max-height: 400px;
overflow-y: auto;
}
.logs-list {
padding: 10px 0;
}
.log-item {
padding: 12px 0;
border-bottom: 1px solid #f0f2f5;
}
.log-item:last-child {
border-bottom: none;
}
.log-time {
font-size: 12px;
color: #86909c;
margin-bottom: 4px;
}
.log-content {
font-size: 14px;
color: #1d2129;
line-height: 1.6;
}
.no-logs {
text-align: center;
padding: 60px 0;
}
.log-skeleton {
margin: 12px 0;
}
/* 任务详情弹窗样式 */
.task-detail-container {
max-height: 600px;
@ -1376,10 +1777,6 @@ const pagedTasks = computed(() => {
color: #e6a23c;
}
.status-running {
color: #409eff;
}
.status-completed {
color: #67c23a;
}

View File

@ -1,6 +1,6 @@
<template>
<div class="operation-inspection">
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab active" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -8,11 +8,11 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<div class="header-container">
<div class="header-actions">
<el-button type="primary" class="export-btn">筛选</el-button>
<el-button type="primary" class="create-btn">导出数据</el-button>
<el-button type="primary" icon="UploadFilled" class="create-btn">导出数据</el-button>
</div>
</div>
@ -54,7 +54,7 @@
></el-date-picker>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="fetchDashboardData">搜索</el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="fetchDashboardData">搜索</el-button>
</div>
</div>
@ -127,14 +127,14 @@
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">完成率</span>
<span class="text-gray-600">巡检完成率</span>
<span class="font-medium text-gray-800">{{ completionRate }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out" :style="{ width: completionRate + '%' }"></div>
</div>
</div>
<div>
<!-- <div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">解决率</span>
<span class="font-medium text-gray-800">{{ resolutionRate }}%</span>
@ -142,10 +142,10 @@
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div class="bg-red-500 h-2 rounded-full transition-all duration-1500 ease-out" :style="{ width: resolutionRate + '%' }"></div>
</div>
</div>
</div> -->
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">及时</span>
<span class="text-gray-600">解决效</span>
<span class="font-medium text-gray-800">{{ timelinessRate }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
@ -161,65 +161,8 @@
<!-- 发现问题种类 -->
<div class="py-4">
<h3 class="section-title">发现问题种类</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">温度异常率</span>
<span class="text-gray-500">{{ problemTypes.temperature }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.temperature + '%' }"
></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">内存使用率</span>
<span class="text-gray-500">{{ problemTypes.memory }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.memory + '%' }"
></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">CPU负载</span>
<span class="text-gray-500">{{ problemTypes.cpu }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out" :style="{ width: problemTypes.cpu + '%' }"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">响应时间</span>
<span class="text-gray-500">{{ problemTypes.responseTime }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.responseTime + '%' }"
></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">磁盘空间状态</span>
<span class="text-gray-500">{{ problemTypes.diskSpace }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.diskSpace + '%' }"
></div>
</div>
</div>
</div>
<!-- 柱状图容器 -->
<div id="problemTypesChart" class="bar-chart-container"></div>
</div>
</div>
</div>
@ -388,16 +331,17 @@ const avgCompletionTime = ref('45分钟');
// 问题类型数据
const problemTypes = ref({
temperature: 85, // 温度异常
memory: 62, // 内存使用率
cpu: 45, // CPU负载
responseTime: 30, // 响应时间
diskSpace: 15 // 磁盘空间状态
temperature: 0, // 温度异常数量
memory: 0, // 内存使用率问题数量
cpu: 0, // CPU负载问题数量
responseTime: 0, // 响应时间问题数量
diskSpace: 0 // 磁盘空间问题数量
});
// ECharts 图相关
// ECharts 图相关
const pieChartRef = ref(null);
let pieChart = null;
let barChart = null;
// 计算平均完成度
const averageRate = computed(() => (completionRate.value + resolutionRate.value + timelinessRate.value) / 3);
@ -426,7 +370,7 @@ const initPieChart = () => {
},
series: [
{
name: '进度指标',
name: '指标对比',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
@ -442,20 +386,15 @@ const initPieChart = () => {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold',
formatter: function (params) {
// 鼠标悬停时显示当前指标的百分比
return params.value + '%';
}
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: completionRate.value, name: '完成率', itemStyle: { color: '#5470c6' } },
{ value: resolutionRate.value, name: '解决率', itemStyle: { color: '#f56c6c' } },
{ value: timelinessRate.value, name: '及时率', itemStyle: { color: '#67c23a' } }
{ value: completionRate.value, name: '巡检完成率', itemStyle: { color: '#409eff' } },
{ value: timelinessRate.value, name: '解决率', itemStyle: { color: '#67c23a' } }
]
}
]
@ -506,11 +445,7 @@ const fetchDashboardData = async () => {
// 构建查询参数
const queryParams = {
projectId: 1,
type: type,
status: filterStatus.value !== 'all' ? filterStatus.value : undefined,
inspectionType: filterType.value !== 'all' ? filterType.value : undefined,
startTime: dateRange.value.length > 0 ? dateRange.value[0] : undefined,
endTime: dateRange.value.length > 0 ? dateRange.value[1] : undefined
type: type
};
// 调用接口获取数据
@ -526,22 +461,26 @@ const fetchDashboardData = async () => {
solvedProblems.value = data.solvedProblemCount || 0;
avgCompletionTime.value = data.averageCompletionTime ? `${data.averageCompletionTime}分钟` : '0分钟';
// 计算完成率、解决率、及时率
completionRate.value = data.finishInspectionCount && data.finishInspectionCount > 0 ? Math.round(Math.random() * 30 + 60) : 0;
resolutionRate.value = data.solvedProblemCount && data.problemCount ? Math.round((data.solvedProblemCount / data.problemCount) * 100) : 0;
timelinessRate.value = data.finishInspectionCount && data.finishInspectionCount > 0 ? Math.round(Math.random() * 30 + 50) : 0;
// 使用接口返回的xjwcl(巡检完成率)和jjxl(解决效率)
completionRate.value = data.xjwcl ? parseFloat(data.xjwcl) : 0;
timelinessRate.value = data.jjxl ? parseFloat(data.jjxl) : 0;
// 更新问题类型数据
// 由于接口不再返回解决率将其设置为0或保持原值
resolutionRate.value = 0;
// 更新问题类型数据 - 直接使用接口返回的数值,不再计算为百分比
problemTypes.value = {
temperature: data.sbyxzt ? Math.min(100, Math.round(data.sbyxzt * 5)) : 0, // 设备运行状态映射为温度异常
memory: data.ncsyl ? Math.min(100, data.ncsyl * 10) : 0, // 内存使用率
cpu: Math.round(Math.random() * 50 + 20), // CPU负载模拟数据
responseTime: data.xysj ? Math.min(100, data.xysj * 5) : 0, // 响应时间
diskSpace: data.cpsyl ? Math.min(100, data.cpsyl * 8) : 0 // 磁盘使用率
temperature: data.sbyxzt || 0, // 设备运行状态类型问题数量
memory: data.ncsyl || 0, // 内存使用率类型问题数量
cpu: data.fwzt || 0, // 服务状态类型问题数量
responseTime: data.xysj || 0, // 响应时间类型问题数量
diskSpace: data.cpsyl || 0 // 磁盘使用率类型问题数量
};
// 更新饼图
initPieChart();
// 更新柱状图
initBarChart();
} else {
ElMessage.error(response.msg || '获取数据失败');
}
@ -551,17 +490,115 @@ const fetchDashboardData = async () => {
}
};
// 页面加载时获取数据
// 页面加载时直接获取数据
onMounted(() => {
fetchDashboardData();
});
// 初始化柱状图
const initBarChart = () => {
const chartDom = document.getElementById('problemTypesChart');
if (!chartDom) return;
// 销毁旧实例
if (barChart) {
barChart.dispose();
}
// 创建新实例
barChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params) {
return params[0].name + ': ' + params[0].value + '个';
}
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '5%',
containLabel: true
},
xAxis: {
type: 'value',
name: '问题数量',
axisLabel: {
formatter: '{value}个'
},
splitLine: {
lineStyle: {
type: 'dashed'
}
}
},
yAxis: {
type: 'category',
data: ['温度异常', '内存使用率', 'CPU负载', '响应时间', '磁盘空间'],
axisLabel: {
interval: 0
}
},
series: [
{
name: '问题数量',
type: 'bar',
barWidth: '40%',
data: [
problemTypes.value.temperature,
problemTypes.value.memory,
problemTypes.value.cpu,
problemTypes.value.responseTime,
problemTypes.value.diskSpace
],
itemStyle: {
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: '#5470c6' },
{ offset: 1, color: '#91cc75' }
]),
borderRadius: [0, 4, 4, 0]
},
label: {
show: true,
position: 'right',
formatter: '{c}个'
}
}
]
};
barChart.setOption(option);
// 响应式处理
const handleResize = () => {
if (barChart) {
barChart.resize();
}
};
window.addEventListener('resize', handleResize);
// 组件卸载时移除事件监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
};
// 组件卸载时销毁图表实例
onUnmounted(() => {
if (pieChart) {
pieChart.dispose();
pieChart = null;
}
if (barChart) {
barChart.dispose();
barChart = null;
}
});
// 导航方法
@ -802,6 +839,17 @@ const handleInspectionManagement3 = () => {
margin: 0 auto;
}
/* 柱状图容器 */
.bar-chart-container {
width: 100%;
height: 350px;
margin: 0 auto;
background-color: #fafafa;
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* 区域标题 */
.section-title {
font-size: 14px;

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="inspection-tasks">
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab active" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -9,7 +9,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -42,8 +42,8 @@
<el-input v-model="executor" placeholder="执行人"></el-input>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button type="primary" icon="el-icon-plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="handleSearch"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
</div>
</div>
</div>
@ -133,7 +133,7 @@
</div>
<!-- 添加新任务弹窗 -->
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="500px" :before-close="handleCancelCreateTask">
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="750px" :before-close="handleCancelCreateTask">
<el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" />
@ -202,7 +202,7 @@
</el-select>
</el-form-item>
<el-form-item label="问题类型" prop="problemType">
<!-- <el-form-item label="问题类型" prop="problemType">
<el-select v-model="createTaskForm.problemType" placeholder="选择问题类型">
<el-option label="磁盘使用率" value="1" />
<el-option label="内存使用率" value="2" />
@ -210,6 +210,27 @@
<el-option label="响应时间" value="4" />
<el-option label="设备运行状态" value="5" />
</el-select>
</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 createTaskForm.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.name" placeholder="输入步骤名称" style="flex: 1; margin-right: 10px" />
<el-input v-model="step.intendedPurpose" placeholder="输入预期目的" style="flex: 1; margin-right: 10px" />
<el-date-picker
v-model="step.intendedTime"
type="datetime"
placeholder="选择计划时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
style="width: 180px; margin-right: 10px"
/>
<el-button v-if="createTaskForm.steps.length > 1" type="text" @click="removeStep(index)" style="color: #f56c6c"> 删除 </el-button>
</div>
<el-button type="text" class="add-step-btn" @click="addStep">添加步骤</el-button>
</div>
</el-form-item>
</el-form>
@ -304,7 +325,7 @@
</div>
<div class="info-item">
<span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.person?.phone || '-' }}</span>
<span class="info-value">{{ detailData.person?.phonenumber || '-' }}</span>
</div>
</div>
<div class="info-row">
@ -312,10 +333,6 @@
<span class="info-label">性别</span>
<span class="info-value">{{ detailData.person?.sex === '1' ? '男' : detailData.person?.sex === '2' ? '女' : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">民族</span>
<span class="info-value">{{ detailData.person?.nation || '-' }}</span>
</div>
</div>
</div>
</div>
@ -356,6 +373,26 @@
</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.taskType === '2' || detailData.taskType === 2" class="detail-card">
<h3 class="card-title">延期信息</h3>
@ -388,37 +425,10 @@
import { ref, computed, onMounted } from 'vue';
import router from '@/router';
import { xjrenwuDetail, xjrenwuExport, xjrenwulist, addxjrenwu, updatexjrenwu, delxjrenwu } from '@/api/zhinengxunjian/xunjian/renwu';
import { xjrenwuDetail, xjrenwulist, addxjrenwu, updatexjrenwu } from '@/api/zhinengxunjian/xunjian/renwu';
import { xunjianUserlist, xunjianlist } from '@/api/zhinengxunjian/xunjian/index';
import { ElMessage, ElLoading } from 'element-plus';
// 根据任务类型获取对应的文本1待执行2已延期3执行中4已完成
const getTaskTypeText = (type) => {
const typeMap = {
'1': '待执行',
'2': '已延期',
'3': '执行中',
'4': '已完成'
};
// 处理可能的数字输入
return typeMap[type.toString()] || '未知类型';
};
// 根据问题类型获取对应的文本1磁盘使用率2内存使用率3服务状态4响应时间5设备运行状态
const getProblemTypeText = (type) => {
const problemTypeMap = {
'1': '磁盘使用率',
'2': '内存使用率',
'3': '服务状态',
'4': '响应时间',
'5': '设备运行状态'
};
// 处理可能的数字输入
return problemTypeMap[type.toString()] || '未知问题';
};
// 激活的选项卡
const activeTab = ref('task');
import { addjiedian } from '@/api/zhinengxunjian/jiedian/index';
import { ElMessage, ElLoading, ElForm } from 'element-plus';
// 筛选条件
const taskStatus = ref('');
@ -428,7 +438,7 @@ const executor = ref('');
// 任务数据 - 初始为空数组通过API获取
const tasks = ref([]);
// 详情弹窗相关变量
// 任务详情弹窗相关变量
const detailDialogVisible = ref(false);
const detailData = ref(null);
const isDetailLoading = ref(false);
@ -459,6 +469,34 @@ const getStatusClass = (status) => {
return statusClassMap[statusStr] || 'status-unknown';
};
// 格式化日期时间
const formatDateTime = (dateTime) => {
if (!dateTime) return '-';
try {
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (error) {
return dateTime;
}
};
// 获取步骤状态文本
const getStepStatusText = (status) => {
const statusStr = status?.toString() || '';
const statusMap = {
'1': '待执行',
'2': '执行中',
'3': '已完成',
'4': '已延期'
};
return statusMap[statusStr] || '未知状态';
};
// 状态映射配置
const statusConfig = {
pending: {
@ -505,11 +543,10 @@ const getTaskList = async () => {
const params = {
pageSize: pageSize.value,
pageNum: currentPage.value,
personId: executor.value !== '' ? executor.value : undefined,
// 根据任务状态映射到后端需要的taskType
taskType: taskStatus.value ? mapTaskStatusToType(taskStatus.value) : undefined,
// 添加计划类型筛选
planType: planType.value || undefined
personId: 1,
taskType: taskStatus.value || undefined, // 任务状态
planType: planType.value || undefined, // 计划类型
personName: executor.value || undefined // 执行人
};
const response = await xjrenwulist(params);
@ -583,44 +620,6 @@ const getTaskList = async () => {
}
};
// 辅助函数将前端状态映射为后端需要的taskType
const mapTaskStatusToType = (status) => {
const statusMap = {
'pending': '1',
'delayed': '2',
'executing': '3',
'completed': '4'
};
return statusMap[status] || '';
};
// 根据person对象获取执行人姓名
const getExecutorName = (person) => {
if (person && typeof person === 'object' && person.userName) {
return person.userName;
}
const executorMap = {
'zhangming': '张明',
'lihua': '李华',
'wangqiang': '王强',
'zhaowei': '赵伟'
};
return executorMap[person] || '未知用户';
};
// 根据plan对象获取计划名称
const getPlanName = (plan) => {
if (plan && typeof plan === 'object' && plan.planName) {
return plan.planName;
}
const planMap = {
'daily': '每日巡检计划',
'weekly': '每周巡检计划',
'monthly': '每月巡检计划'
};
return planMap[plan] || '未知计划';
};
// 页面加载时获取数据
onMounted(() => {
getTaskList();
@ -675,10 +674,11 @@ const createTaskForm = ref({
timeRange: [],
workTimeRange1: null,
workTimeRange2: null,
relatedPlan: 'all',
relatedPlan: '',
executor: '',
taskType: '1', // 默认待执行
problemType: ''
// problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }] // 任务步骤数组
});
const createTaskRules = {
@ -694,6 +694,17 @@ const handleCreateTask = () => {
openCreateTaskDialog();
};
// 重置步骤表单
const resetStepForm = () => {
Object.keys(stepForm).forEach((key) => {
stepForm[key] = '';
});
currentStep.value = 0;
if (stepFormRef.value) stepFormRef.value.resetFields();
if (deviceFormRef.value) deviceFormRef.value.resetFields();
if (faultFormRef.value) faultFormRef.value.resetFields();
};
// 构建timeInfo字符串
const getTaskTimeInfoString = () => {
const timeInfoArray = [];
@ -719,6 +730,21 @@ const getTaskTimeInfoString = () => {
return timeInfoArray.join(',');
};
// 添加步骤
const addStep = () => {
createTaskForm.value.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
};
// 删除步骤
const removeStep = (index) => {
// 确保至少保留一个步骤
if (createTaskForm.value.steps.length <= 1) {
ElMessage.warning('至少需要保留一个步骤');
return;
}
createTaskForm.value.steps.splice(index, 1);
};
// 保存任务
const handleSaveTask = async () => {
// 表单验证
@ -727,6 +753,13 @@ const handleSaveTask = async () => {
return;
}
// 验证所有步骤
const hasEmptyStep = createTaskForm.value.steps.some((step) => !step.name.trim() || !step.intendedPurpose.trim());
if (hasEmptyStep) {
ElMessage.warning('请填写完整所有步骤信息');
return;
}
try {
// 获取timeInfo字符串
const taskTimeInfo = getTaskTimeInfoString();
@ -736,13 +769,47 @@ const handleSaveTask = async () => {
return;
}
// 准备步骤数据,与工单列表页面保持一致的格式
const stepsData = createTaskForm.value.steps
.filter((step) => step.name.trim() && step.intendedPurpose.trim())
.map((step, index) => ({
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
params: {},
module: 2,
code: index + 1,
name: step.name,
intendedPurpose: step.intendedPurpose,
intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
finishTime: '',
remark: '',
status: 2
}));
// 调用添加节点接口,直接传递步骤数组
const jiedianResponse = await addjiedian(stepsData);
if (jiedianResponse.code !== 200) {
ElMessage.error('创建步骤失败');
return;
}
// 获取返回的ids实际返回格式中msg字段包含ids字符串data为null
let nodeIds = '';
if (jiedianResponse.code === 200 && jiedianResponse.msg) {
nodeIds = jiedianResponse.msg;
} else {
ElMessage.warning('未获取到有效的步骤ID');
return;
}
// 构建接口所需的数据结构
const apiData = {
createDept: 0,
createBy: 0,
projectId: 1,
createTime: new Date().toISOString(),
updateBy: 0,
updateTime: new Date().toISOString(),
startTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
params: {
property1: 'string',
property2: 'string'
@ -757,7 +824,8 @@ const handleSaveTask = async () => {
personId: createTaskForm.value.executor !== 'all' ? createTaskForm.value.executor : 0,
taskProgress: 0,
taskType: createTaskForm.value.taskType,
problemType: createTaskForm.value.problemType
// problemType: createTaskForm.value.problemType,
nodeIds: nodeIds // 添加步骤ID字符串与工单列表页面保持一致
};
// 调用新增任务接口
@ -774,10 +842,11 @@ const handleSaveTask = async () => {
timeRange: [],
workTimeRange1: null,
workTimeRange2: null,
relatedPlan: 'all',
relatedPlan: '',
executor: '',
taskType: '1',
problemType: ''
// problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
};
// 重新获取任务列表
getTaskList();
@ -807,21 +876,14 @@ const planList = ref([]);
const getUsersList = async () => {
try {
const response = await xunjianUserlist();
const userRows =
response?.data?.rows && Array.isArray(response.data.rows)
? response.data.rows
: response?.rows && Array.isArray(response.rows)
? response.rows
: Array.isArray(response)
? response
: [];
// 适配新接口格式检查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, index) => ({
label: item.userName || `用户${index + 1}`,
value: item.id || `id_${index}`
.map((item) => ({
label: item.userName || '未知用户',
value: String(item.userId || '') // 使用userId作为唯一标识
}));
if (userList.value.length === 0) {
@ -853,11 +915,8 @@ const getPlansList = async () => {
label: item.planName || `计划${item.id}`,
value: item.id.toString()
}));
planList.value.unshift({ label: '全部计划', value: 'all' });
} catch (error) {
console.error('获取计划列表失败:', error);
planList.value = [{ label: '全部计划', value: 'all' }];
}
};
@ -875,8 +934,13 @@ const handleCancelCreateTask = () => {
taskName: '',
inspectionTarget: '',
timeRange: [],
relatedPlan: 'all',
executor: 'all'
workTimeRange1: null,
workTimeRange2: null,
relatedPlan: '',
executor: '',
taskType: '1',
// problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
};
};
@ -981,6 +1045,7 @@ const handleAction = async (task) => {
const updateData = {
...originalTask.rawData,
id: task.id,
startTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
taskType: '3', // 3表示执行中
status: 'executing',
taskProgress: 0
@ -1046,6 +1111,9 @@ const handleAction = async (task) => {
</script>
<style scoped>
@import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
.inspection-tasks {
padding: 20px;
background-color: #f5f7fa;
@ -1401,6 +1469,96 @@ const handleAction = async (task) => {
overflow-y: auto;
}
/* 步骤条展示样式 */
.step-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
padding: 12px;
background-color: #fafafa;
border-radius: 6px;
transition: all 0.3s ease;
}
.step-item:hover {
background-color: #f5f7fa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.step-number {
width: 28px;
height: 28px;
background-color: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
.step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
.step-time,
.step-finish-time,
.step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
.step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
/* 步骤状态样式 */
.step-status.status-pending {
background-color: #e6f7ff;
color: #1677ff;
border: 1px solid #91d5ff;
}
.step-status.status-executing {
background-color: #fffbe6;
color: #fa8c16;
border: 1px solid #ffe58f;
}
.step-status.status-completed {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.step-status.status-delayed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
.detail-card {
margin-bottom: 20px;
padding: 20px;
@ -1580,4 +1738,11 @@ const handleAction = async (task) => {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.step-content {
padding: 30px 20px;
background-color: #fafafa;
border-radius: 8px;
margin-top: 20px;
}
</style>

File diff suppressed because it is too large Load Diff