This commit is contained in:
dhr
2025-09-25 20:03:08 +08:00
parent 9913a7854c
commit 6b9bfb66b1
15 changed files with 4715 additions and 1591 deletions

View File

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

View File

@ -316,3 +316,58 @@ export const removeClass = (ele: HTMLElement, cls: string) => {
export const isExternal = (path: string) => { export const isExternal = (path: string) => {
return /^(https?:|http?:|mailto:|tel:)/.test(path); 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

@ -312,43 +312,101 @@
v-model="detailDialogVisible" v-model="detailDialogVisible"
title="巡检计划详情" title="巡检计划详情"
width="800px" width="800px"
class="detail-dialog" :before-close="handleCloseDetailDialog"
center class="custom-experiment-dialog"
:show-close="true"
custom-class="beautified-detail-dialog"
> >
<div class="detail-content"> <div class="task-detail-container">
<div class="detail-header"> <!-- 基础信息区 -->
<h3 class="detail-title">{{ detailData.planName || '巡检计划' }}</h3> <div class="detail-card">
<el-tag :type="detailData.status === '1' ? 'success' : 'info'" class="detail-status-tag"> <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' ? '停用' : '-' }} {{ detailData.status === '1' ? '启用' : detailData.status === '2' ? '停用' : '-' }}
</el-tag> </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>
<div class="detail-main"> <!-- 备注信息 -->
<el-descriptions :column="{ xs: 1, sm: 1, md: 2, lg: 2 }" class="detail-descriptions" border> <div v-if="detailData.remark" class="detail-card">
<el-descriptions-item label="计划类型" class="detail-item">{{ getPlanTypeText(detailData.planType) || '-' }}</el-descriptions-item> <h3 class="card-title">备注信息</h3>
<el-descriptions-item label="巡检对象" class="detail-item">{{ getObjectTypeText(detailData.objectType) || '-' }}</el-descriptions-item> <div class="card-content">
<el-descriptions-item label="巡检频率" class="detail-item">{{ detailData.inspectionFrequency || '-' }}</el-descriptions-item> <div class="description-content">
<el-descriptions-item label="负责人" class="detail-item">{{ detailData.nickName || '-' }}</el-descriptions-item> {{ detailData.remark }}
<el-descriptions-item label="开始日期" class="detail-item">{{ formatDate(detailData.beginTime) || '-' }}</el-descriptions-item> </div>
<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>
<div v-if="detailData.remark" class="detail-remark">
<h4 class="remark-title">备注信息</h4>
<p class="remark-content">{{ detailData.remark }}</p>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="closeDetailDialog" class="close-btn">关闭</el-button> <el-button @click="closeDetailDialog">关闭</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
@ -980,6 +1038,8 @@ const handleInspectionManagement3 = () => {
</script> </script>
<style scoped> <style scoped>
@import url('./css/detail-dialog.css');
@import url('./css/step-bars.css');
.operation-inspection { .operation-inspection {
padding: 20px; padding: 20px;
background-color: #f5f7fa; background-color: #f5f7fa;
@ -1123,47 +1183,127 @@ const handleInspectionManagement3 = () => {
color: #f56c6c; color: #f56c6c;
} }
.detail-dialog .el-dialog__body { /* 弹窗样式 */
.create-plan-dialog .el-dialog__body {
padding: 24px; padding: 24px;
} }
.detail-header { /* 详情弹窗样式 - 与工单列表页面保持一致 */
display: flex; .custom-experiment-dialog .el-dialog__body {
justify-content: space-between; max-height: 60vh;
align-items: center; overflow-y: auto;
padding: 24px;
}
.task-detail-container {
padding: 10px 0;
}
/* 详情卡片样式 */
.detail-card {
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px; margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f2f5;
} }
.detail-title { .card-title {
font-size: 18px; font-size: 16px;
font-weight: bold; font-weight: 600;
color: #303133; color: #1d2129;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
} }
.detail-status-tag { .card-content {
padding: 4px 12px; 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; 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; font-weight: 500;
color: #606266; border: 1px solid transparent;
} }
.remark-title { .description-content {
font-weight: 500;
margin-bottom: 8px;
color: #303133;
}
.remark-content {
padding: 12px; padding: 12px;
background-color: #f5f7fa; background-color: #f9f9f9;
border-radius: 4px; border-radius: 4px;
line-height: 1.6; line-height: 1.6;
color: #4e5969;
font-size: 13px;
} }
</style> </style>

View File

@ -1080,6 +1080,8 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
@import url('./css/detail-dialog.css');
@import url('./css/step-bars.css');
.inspection-tasks { .inspection-tasks {
padding: 20px; padding: 20px;
background-color: #f5f7fa; background-color: #f5f7fa;

View File

@ -1244,11 +1244,69 @@ const handleInspection7 = () => {
overflow-x: auto; overflow-x: auto;
} }
} }
/* 详情弹窗样式 */
.custom-experiment-dialog .el-dialog__body { .custom-experiment-dialog .el-dialog__body {
max-height: 60vh; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
padding: 24px; padding: 24px;
} }
/* 详情卡片样式 */
.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;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.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;
}
/* 详情弹窗样式 */ /* 详情弹窗样式 */
.custom-experiment-dialog { .custom-experiment-dialog {
.detail-content { .detail-content {
@ -1304,96 +1362,6 @@ const handleInspection7 = () => {
white-space: pre-wrap; white-space: pre-wrap;
} }
/* 任务详情容器 */
.task-detail-container {
padding: 0 10px;
}
/* 详情卡片样式 */
.detail-card {
margin-bottom: 20px;
border-radius: 8px;
background-color: #ffffff;
border: 1px solid #ebeef5;
overflow: hidden;
}
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
padding: 16px;
border-bottom: 1px solid #f0f2f5;
margin: 0;
}
.card-content {
padding: 16px;
}
.info-row {
display: flex;
flex-wrap: wrap;
margin-bottom: 12px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-item {
flex: 0 0 50%;
margin-bottom: 8px;
}
.info-item.full-width {
flex: 0 0 100%;
}
.info-label {
font-size: 14px;
color: #909399;
margin-right: 8px;
}
.info-value {
font-size: 14px;
color: #303133;
word-break: break-word;
}
/* 骨架屏样式 */
.skeleton-loading {
padding: 20px;
}
.skeleton-card {
margin-bottom: 16px;
border-radius: 8px;
background-color: #f5f7fa;
padding: 16px;
}
.skeleton-header {
width: 100%;
height: 20px;
background-color: #e8e8e8;
border-radius: 4px;
margin-bottom: 12px;
}
.skeleton-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-row {
height: 16px;
background-color: #e8e8e8;
border-radius: 4px;
}
/* 多图片展示容器样式 */ /* 多图片展示容器样式 */
.images-container { .images-container {
display: flex; display: flex;
@ -1443,20 +1411,7 @@ const handleInspection7 = () => {
color: #909399; color: #909399;
padding: 40px 0; padding: 40px 0;
} }
/* 加载状态样式 */
.loading-state {
text-align: center;
padding: 60px 0;
color: #909399;
}
.loading-state .el-icon-loading {
font-size: 36px;
margin-bottom: 12px;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.custom-experiment-dialog { .custom-experiment-dialog {
width: 90% !important; width: 90% !important;

View File

@ -0,0 +1,287 @@
/* 详情弹窗通用样式 */
/* 详情卡片样式 */
.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;
}
/* 步骤相关样式 */
.steps-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.step-item {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 16px;
background-color: #ffffff;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.step-number {
width: 28px;
height: 28px;
background-color: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
flex-shrink: 0;
}
.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;
}
/* 状态相关样式 */
.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;
}
/* 通用状态颜色样式 */
.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;
}
}
/* 弹窗按钮样式 */
.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;
}
}

View File

@ -207,7 +207,16 @@
<div class="steps-container"> <div class="steps-container">
<div class="step-item" v-for="(step, index) in createForm.steps" :key="index"> <div class="step-item" v-for="(step, index) in createForm.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div> <div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.content" placeholder="输入试验步骤" style="flex: 1; margin-right: 10px" /> <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="createForm.steps.length > 1" type="text" class="delete-step-btn" @click="deleteStep(index)" style="color: #f56c6c"> <el-button v-if="createForm.steps.length > 1" type="text" class="delete-step-btn" @click="deleteStep(index)" style="color: #f56c6c">
删除 删除
</el-button> </el-button>
@ -244,51 +253,77 @@
</el-dialog> </el-dialog>
<!-- 工单详情弹窗 --> <!-- 工单详情弹窗 -->
<el-dialog v-model="detailDialogVisible" title="工单详情" width="900px" class="beautiful-dialog" center> <el-dialog
<div v-if="isDetailLoading" class="loading-state"> v-model="detailDialogVisible"
<i class="el-icon-loading el-icon--loading"></i> title="工单详情"
<span>加载中...</span> width="800px"
:before-close="handleCloseDetailDialog"
class="custom-experiment-dialog"
>
<div v-if="isDetailLoading" class="skeleton-loading">
<div class="skeleton-card">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
</div>
</div>
<div class="skeleton-card">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
</div>
</div>
<div class="skeleton-card">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
</div>
</div>
</div> </div>
<div v-else-if="detailData" class="detail-container"> <div v-else-if="detailData" class="task-detail-container">
<!-- 基础信息区 --> <!-- 基础信息区 -->
<div class="detail-card"> <div class="detail-card">
<h3 class="card-title">基础信息</h3> <h3 class="card-title">基础信息</h3>
<div class="card-content"> <div class="card-content">
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">工单编号</span> <span class="info-label">工单编号</span>
<span class="detail-value">WO-{{ detailData.id }}</span> <span class="info-value">WO-{{ detailData.id }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">工单标题</span> <span class="info-label">工单标题</span>
<span class="detail-value">{{ detailData.title }}</span> <span class="info-value">{{ detailData.title }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">工单类型</span> <span class="info-label">工单类型</span>
<span class="detail-value">{{ mapCodeToType(detailData.type) }}</span> <span class="info-value">{{ mapCodeToType(detailData.type) }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">优先级</span> <span class="info-label">优先级</span>
<span class="detail-value task-status priority-{{ mapPriorityToClass(detailData.level) }}">{{ <span class="info-value task-status priority-{{ mapPriorityToClass(detailData.level) }}">{{
mapCodeToPriority(detailData.level) mapCodeToPriority(detailData.level)
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">创建人</span> <span class="info-label">创建人</span>
<span class="detail-value">{{ detailData.sendOrderPersonVo?.userName || '-' }}</span> <span class="info-value">{{ detailData.sendOrderPersonVo?.userName || '-' }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">创建时间</span> <span class="info-label">创建时间</span>
<span class="detail-value">{{ detailData.createTime ? formatDate(detailData.createTime) : '-' }}</span> <span class="info-value">{{ detailData.createTime ? formatDate(detailData.createTime) : '-' }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">执行人</span> <span class="detail-label">执行人</span>
<span class="detail-value">{{ detailData.getOrderPersonVo?.userName || '-' }}</span> <span class="detail-value">{{ detailData.getOrderPersonVo?.userName || '-' }}</span>
</div> </div>
@ -333,17 +368,19 @@
<!-- 步骤条 --> <!-- 步骤条 -->
<div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card"> <div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card">
<h3 class="card-title">执行步骤</h3> <h3 class="card-title">执行步骤</h3>
<div class="card-content"> <div class="steps-container">
<div v-for="(group, groupIndex) in groupNodesByModule(detailData.nodes)" :key="groupIndex" class="module-group"> <div v-for="(node, index) in detailData.nodes" :key="node.id || index" class="step-item">
<div class="module-title">{{ group.module }}</div> <div class="step-number">{{ node.code || index + 1 }}</div>
<el-steps :active="-1" finish-status="success" class="custom-steps"> <div class="step-info">
<el-step <div class="step-name">{{ node.name || '未命名步骤' }}</div>
v-for="(node, index) in group.items" <div class="step-purpose">{{ node.intendedPurpose || '无说明' }}</div>
:key="node.id" <div class="step-time">计划时间{{ formatDateTime(node.intendedTime) }}</div>
:title="node.name" <div v-if="node.finishTime" class="step-finish-time">完成时间{{ formatDateTime(node.finishTime) }}</div>
:description="`目的: ${node.intendedPurpose || '-'}`" <div v-if="node.remark" class="step-remark">备注{{ node.remark }}</div>
/> </div>
</el-steps> <div class="step-status" :class="getStatusClass(node.status)">
{{ getStepStatusText(node.status) }}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -414,8 +451,8 @@
<script setup> <script setup>
import { ref, computed, reactive } from 'vue'; import { ref, computed, reactive } from 'vue';
import router from '@/router'; import router from '@/router';
import { gongdanlist, addgongdan, updategongdan, gongdanDetail, uploadgongdan } from '@/api/zhinengxunjian/gongdan/index'; import { gongdanlist, addgongdan, updategongdan, gongdanDetail } from '@/api/zhinengxunjian/gongdan/index';
import { addjiedian } from '@/api/zhinengxunjian/jiedian'; import { addjiedian, updatejiedian } from '@/api/zhinengxunjian/jiedian';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian'; import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian';
import ImageUpload from '@/components/ImageUpload/index.vue'; import ImageUpload from '@/components/ImageUpload/index.vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
@ -605,7 +642,7 @@ const getPriorityTagType = (priority) => {
return priorityMap[priority] || 'default'; return priorityMap[priority] || 'default';
}; };
// 获取状态标签样式 // 状态标签样式
const getStatusTagType = (status) => { const getStatusTagType = (status) => {
const statusMap = { const statusMap = {
'已接单': 'primary', '已接单': 'primary',
@ -617,7 +654,47 @@ const getStatusTagType = (status) => {
return statusMap[status] || 'default'; return statusMap[status] || 'default';
}; };
// 搜索处理 // 根据步骤状态获取对应的样式类
const getStatusClass = (status) => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap = {
'1': 'status-pending',
'2': 'status-delayed',
'3': 'status-executing',
'4': 'status-completed'
};
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 handleSearch = () => { const handleSearch = () => {
currentPage.value = 1; // 重置到第一页 currentPage.value = 1; // 重置到第一页
fetchWorkOrderList(); // 重新获取数据 fetchWorkOrderList(); // 重新获取数据
@ -780,7 +857,7 @@ const confirmAssign = async () => {
// 从执行人列表中查找选中的执行人信息 // 从执行人列表中查找选中的执行人信息
const selectedExecutorInfo = executors.value.find((item) => item.userId === selectedExecutor.value); const selectedExecutorInfo = executors.value.find((item) => item.userId === selectedExecutor.value);
// 先获取完整的工单详情,确保有所有必要字段(与编辑弹窗一样的方式) // 先获取完整的工单详情,确保有所有必要字段(
const detailResponse = await gongdanDetail(currentTaskId.value); const detailResponse = await gongdanDetail(currentTaskId.value);
if (detailResponse.code !== 200) { if (detailResponse.code !== 200) {
ElMessage.error('获取工单详情失败'); ElMessage.error('获取工单详情失败');
@ -933,15 +1010,17 @@ const handleEdit = async (row) => {
const sortedNodes = [...workOrderDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0)); const sortedNodes = [...workOrderDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0));
// 转换为createForm.steps所需的格式 // 转换为createForm.steps所需的格式
createForm.steps = sortedNodes.map((node) => ({ createForm.steps = sortedNodes.map((node) => ({
content: node.intendedPurpose || '' name: node.name || '',
intendedPurpose: node.intendedPurpose || '',
intendedTime: node.intendedTime || ''
})); }));
// 确保至少有一个空步骤 // 确保至少有一个空步骤
if (createForm.steps.length === 0) { if (createForm.steps.length === 0) {
createForm.steps = [{ content: '' }]; createForm.steps = [{ name: '', intendedPurpose: '', intendedTime: '' }];
} }
} else { } else {
// 如果没有nodes数据重置为默认的一个空步骤 // 如果没有nodes数据重置为默认的一个空步骤
createForm.steps = [{ content: '' }]; createForm.steps = [{ name: '', intendedPurpose: '', intendedTime: '' }];
} }
// 存储当前编辑的工单ID用于区分是创建还是编辑操作 // 存储当前编辑的工单ID用于区分是创建还是编辑操作
@ -985,7 +1064,7 @@ const createForm = reactive({
resultDescription: '', resultDescription: '',
needAssignee: 'true', needAssignee: 'true',
assignee: '', assignee: '',
steps: [{ content: '' }], steps: [{ name: '', intendedPurpose: '', intendedTime: '' }],
file: '', file: '',
fileList: [] fileList: []
}); });
@ -1007,7 +1086,7 @@ const handleCreateWorkOrder = () => {
// 添加试验步骤 // 添加试验步骤
const addStep = () => { const addStep = () => {
createForm.steps.push({ content: '' }); createForm.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
}; };
// 删除试验步骤 // 删除试验步骤
@ -1030,23 +1109,54 @@ const submitCreate = async () => {
// 准备步骤数据 // 准备步骤数据
const stepsData = createForm.steps const stepsData = createForm.steps
.filter((step) => step.content.trim()) .filter((step) => step.name.trim() && step.intendedPurpose.trim())
.map((step, index) => ({ .map((step, index) => ({
createTime: new Date().toISOString(), createTime: new Date().toISOString(),
updateTime: new Date().toISOString(), updateTime: new Date().toISOString(),
params: {}, params: {},
module: 1, module: 1,
code: index + 1, code: index + 1,
name: `步骤${index + 1}`, name: step.name,
intendedPurpose: step.content, intendedPurpose: step.intendedPurpose,
intendedTime: new Date().toISOString(), intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
finishTime: new Date().toISOString(), finishTime: '',
remark: '', remark: '',
status: '1' // 使用数字代码而不是字符串,避免数据长度超出限制 status: 2
})); }));
// 首先调用addjiedian接口 // 编辑模式下需要为每个步骤添加id并调用updatejiedian接口
let nodeIds = '';
if (editingWorkOrderId.value) {
// 获取工单详情以获取原始步骤的id
const detailResponse = await gongdanDetail(editingWorkOrderId.value);
if (detailResponse.code !== 200) {
ElMessage.error('获取工单详情失败');
return;
}
const workOrderDetail = detailResponse.data;
if (workOrderDetail.nodes && Array.isArray(workOrderDetail.nodes)) {
// 按code排序原始nodes数组
const sortedNodes = [...workOrderDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0));
// 为新的步骤数据添加id
const updatedSteps = stepsData.map((step, index) => ({
...step,
id: sortedNodes[index]?.id || 0 // 使用原始步骤的id如果不存在则使用0
}));
// 调用updatejiedian接口更新步骤直接传递数组
const updateResponse = await updatejiedian(updatedSteps);
if (updateResponse.code !== 200) {
ElMessage.error('更新步骤失败');
return;
}
// 使用原始的nodeIds避免重新创建步骤
nodeIds = workOrderDetail.nodeIds;
}
} else {
// 创建模式下调用addjiedian接口直接传递数组
const jiedianResponse = await addjiedian(stepsData); const jiedianResponse = await addjiedian(stepsData);
if (jiedianResponse.code !== 200) { if (jiedianResponse.code !== 200) {
@ -1055,13 +1165,13 @@ const submitCreate = async () => {
} }
// 获取返回的ids实际返回格式中msg字段包含ids字符串data为null // 获取返回的ids实际返回格式中msg字段包含ids字符串data为null
let nodeIds = '';
if (jiedianResponse.code === 200 && jiedianResponse.msg) { if (jiedianResponse.code === 200 && jiedianResponse.msg) {
nodeIds = jiedianResponse.msg; nodeIds = jiedianResponse.msg;
} else { } else {
ElMessage.warning('未获取到有效的步骤ID'); ElMessage.warning('未获取到有效的步骤ID');
return; return;
} }
}
// 准备工单数据 // 准备工单数据
const workOrderData = { const workOrderData = {
@ -1199,6 +1309,9 @@ const handleInspectionManagement3 = () => {
</script> </script>
<style scoped> <style scoped>
@import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
.work-order-management { .work-order-management {
padding: 20px; padding: 20px;
background-color: #f5f7fa; background-color: #f5f7fa;
@ -1679,72 +1792,105 @@ const handleInspectionManagement3 = () => {
gap: 10px; gap: 10px;
} }
} }
/* 详情弹窗样式 */ /* 详情弹窗样式 - 与保修管理页面保持一致 */
.beautiful-dialog .el-dialog__body { .custom-experiment-dialog .el-dialog__body {
max-height: 600px; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 24px;
} }
.loading-state { .task-detail-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #909399;
}
.detail-container {
padding: 10px 0; padding: 10px 0;
} }
/* 详情卡片样式 */
.detail-card { .detail-card {
margin-bottom: 20px;
background-color: #fff; background-color: #fff;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); padding: 20px;
overflow: hidden; margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f2f5;
} }
.card-title { .card-title {
font-size: 14px; font-size: 16px;
font-weight: 500; font-weight: 600;
color: #1d2129; color: #1d2129;
padding: 12px 16px; margin-bottom: 16px;
background-color: #f7f8fa; padding-bottom: 12px;
border-bottom: 1px solid #f0f2f5; border-bottom: 2px solid #409eff;
margin-bottom: 0;
} }
.card-content { .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;
}
/* 骨架屏样式 */
.skeleton-loading {
display: flex;
flex-direction: column;
gap: 16px;
}
.skeleton-card {
background-color: #f5f5f5;
border-radius: 8px;
padding: 16px; padding: 16px;
} }
.info-row { .skeleton-header {
height: 20px;
width: 30%;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 12px;
}
.skeleton-content {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
margin-bottom: 8px; gap: 8px;
} }
.detail-item { .skeleton-row {
display: flex; height: 16px;
margin-bottom: 10px; width: 100%;
font-size: 13px; background-color: #e0e0e0;
flex: 0 0 50%; border-radius: 4px;
box-sizing: border-box;
padding-right: 12px;
}
.detail-label {
flex: 0 0 80px;
color: #86909c;
}
.detail-value {
flex: 1;
color: #4e5969;
word-break: break-all;
} }
/* 优先级标签样式 */ /* 优先级标签样式 */
@ -1784,23 +1930,6 @@ const handleInspectionManagement3 = () => {
font-size: 13px; font-size: 13px;
} }
.steps-container {
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
}
.module-group {
margin-bottom: 24px;
}
.module-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 12px;
color: #333;
}
/* 多图片展示容器样式 */ /* 多图片展示容器样式 */
.images-container { .images-container {
display: flex; display: flex;
@ -1948,23 +2077,7 @@ const handleInspectionManagement3 = () => {
line-height: 1.6; line-height: 1.6;
} }
.steps-container { /* 自定义步骤条样式覆盖 */
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
}
.module-group {
margin-bottom: 24px;
}
.module-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 12px;
color: #333;
}
.custom-steps .el-step__description { .custom-steps .el-step__description {
white-space: pre-wrap; white-space: pre-wrap;
font-size: 12px; font-size: 12px;

View File

@ -259,7 +259,16 @@
<div class="steps-container"> <div class="steps-container">
<div class="step-item" v-for="(step, index) in createForm.steps" :key="index"> <div class="step-item" v-for="(step, index) in createForm.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div> <div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.content" placeholder="输入试验步骤" style="flex: 1; margin-right: 10px" /> <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="createForm.steps.length > 1" type="text" class="delete-step-btn" @click="deleteStep(index)" style="color: #f56c6c"> <el-button v-if="createForm.steps.length > 1" type="text" class="delete-step-btn" @click="deleteStep(index)" style="color: #f56c6c">
删除 删除
</el-button> </el-button>
@ -295,77 +304,103 @@
</el-dialog> </el-dialog>
<!-- 工单详情弹窗 --> <!-- 工单详情弹窗 -->
<el-dialog v-model="detailDialogVisible" title="工单详情" width="900px" class="beautiful-dialog" center> <el-dialog
<div v-if="isDetailLoading" class="loading-state"> v-model="detailDialogVisible"
<i class="el-icon-loading el-icon--loading"></i> title="工单详情"
<span>加载中...</span> width="800px"
:before-close="handleCloseDetailDialog"
class="custom-experiment-dialog"
>
<div v-if="isDetailLoading" class="skeleton-loading">
<div class="skeleton-card">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
</div>
</div>
<div class="skeleton-card">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
</div>
</div>
<div class="skeleton-card">
<div class="skeleton-header"></div>
<div class="skeleton-content">
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
</div>
</div>
</div> </div>
<div v-else-if="detailData" class="detail-container"> <div v-else-if="detailData" class="task-detail-container">
<!-- 基础信息区 --> <!-- 基础信息区 -->
<div class="detail-card"> <div class="detail-card">
<h3 class="card-title">基础信息</h3> <h3 class="card-title">基础信息</h3>
<div class="card-content"> <div class="card-content">
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">工单编号</span> <span class="info-label">工单编号</span>
<span class="detail-value">WO-{{ detailData.id }}</span> <span class="info-value">WO-{{ detailData.id }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">工单标题</span> <span class="info-label">工单标题</span>
<span class="detail-value">{{ detailData.title }}</span> <span class="info-value">{{ detailData.title }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">工单类型</span> <span class="info-label">工单类型</span>
<span class="detail-value">{{ mapCodeToType(detailData.type) }}</span> <span class="info-value">{{ mapCodeToType(detailData.type) }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">优先级</span> <span class="info-label">优先级</span>
<span class="detail-value task-status priority-{{ mapPriorityToClass(detailData.level) }}">{{ <span class="info-value task-status priority-{{ mapPriorityToClass(detailData.level) }}">{{
mapCodeToPriority(detailData.level) mapCodeToPriority(detailData.level)
}}</span> }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">创建人</span> <span class="info-label">创建人</span>
<span class="detail-value">{{ detailData.sendOrderPersonVo?.userName || '-' }}</span> <span class="info-value">{{ detailData.sendOrderPersonVo?.userName || '-' }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">创建时间</span> <span class="info-label">创建时间</span>
<span class="detail-value">{{ detailData.createTime ? formatDate(detailData.createTime) : '-' }}</span> <span class="info-value">{{ detailData.createTime ? formatDate(detailData.createTime) : '-' }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">执行人</span> <span class="info-label">执行人</span>
<span class="detail-value">{{ detailData.getOrderPersonVo?.userName || '-' }}</span> <span class="info-value">{{ detailData.getOrderPersonVo?.userName || '-' }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">接单时间</span> <span class="info-label">接单时间</span>
<span class="detail-value">{{ detailData.getOrderTime ? formatDate(detailData.getOrderTime) : '-' }}</span> <span class="info-value">{{ detailData.getOrderTime ? formatDate(detailData.getOrderTime) : '-' }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">截止时间</span> <span class="info-label">截止时间</span>
<span class="detail-value">{{ detailData.endTime ? formatDate(detailData.endTime) : '-' }}</span> <span class="info-value">{{ detailData.endTime ? formatDate(detailData.endTime) : '-' }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">完成时间</span> <span class="info-label">完成时间</span>
<span class="detail-value">{{ detailData.finishiOrderTime ? formatDate(detailData.finishiOrderTime) : '-' }}</span> <span class="info-value">{{ detailData.finishiOrderTime ? formatDate(detailData.finishiOrderTime) : '-' }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
<div class="detail-item"> <div class="info-item">
<span class="detail-label">执行地点</span> <span class="info-label">执行地点</span>
<span class="detail-value">{{ detailData.position || '-' }}</span> <span class="info-value">{{ detailData.position || '-' }}</span>
</div> </div>
<div class="detail-item"> <div class="info-item">
<span class="detail-label">相关设备</span> <span class="info-label">相关设备</span>
<span class="detail-value">{{ detailData.device || '-' }}</span> <span class="info-value">{{ detailData.device || '-' }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -384,17 +419,19 @@
<!-- 步骤条 --> <!-- 步骤条 -->
<div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card"> <div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card">
<h3 class="card-title">执行步骤</h3> <h3 class="card-title">执行步骤</h3>
<div class="card-content"> <div class="steps-container">
<div v-for="(group, groupIndex) in groupNodesByModule(detailData.nodes)" :key="groupIndex" class="module-group"> <div v-for="(node, index) in detailData.nodes" :key="node.id || index" class="step-item">
<div class="module-title">{{ group.module }}</div> <div class="step-number">{{ node.code || index + 1 }}</div>
<el-steps :active="-1" finish-status="success" class="custom-steps"> <div class="step-info">
<el-step <div class="step-name">{{ node.name || '未命名步骤' }}</div>
v-for="(node, index) in group.items" <div class="step-purpose">{{ node.intendedPurpose || '无说明' }}</div>
:key="node.id" <div class="step-time">计划时间{{ formatDateTime(node.intendedTime) }}</div>
:title="node.name" <div v-if="node.finishTime" class="step-finish-time">完成时间{{ formatDateTime(node.finishTime) }}</div>
:description="`目的: ${node.intendedPurpose || '-'}`" <div v-if="node.remark" class="step-remark">备注{{ node.remark }}</div>
/> </div>
</el-steps> <div class="step-status" :class="getStatusClass(node.status)">
{{ getStepStatusText(node.status) }}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -465,15 +502,12 @@
<script setup> <script setup>
import { ref, computed, reactive } from 'vue'; import { ref, computed, reactive } from 'vue';
import router from '@/router'; import router from '@/router';
import { gongdanlist, gongdanRecord, updategongdan, gongdanDetail, uploadgongdan } from '@/api/zhinengxunjian/gongdan/index'; import { gongdanlist, gongdanRecord, updategongdan, gongdanDetail } from '@/api/zhinengxunjian/gongdan/index';
import { addjiedian } from '@/api/zhinengxunjian/jiedian'; import { updatejiedian } from '@/api/zhinengxunjian/jiedian';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian'; import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian';
import ImageUpload from '@/components/ImageUpload/index.vue'; import ImageUpload from '@/components/ImageUpload/index.vue';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
// 激活的选项卡
const activeTab = ref('list');
// 筛选条件 // 筛选条件
const workOrderType = ref('all'); const workOrderType = ref('all');
const workOrderStatus = ref('all'); const workOrderStatus = ref('all');
@ -662,6 +696,52 @@ const formatDate = (dateString) => {
return `${year}/${month}/${day} ${hours}:${minutes}`; return `${year}/${month}/${day} ${hours}:${minutes}`;
}; };
// 根据步骤状态获取对应的样式类
const getStatusClass = (status) => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap = {
'1': 'status-pending',
'2': 'status-delayed',
'3': 'status-executing',
'4': 'status-completed'
};
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 groupNodesByModule = (nodes) => {
if (!nodes || !nodes.length) return [];
return [{ module: '', items: nodes }];
// 初始化加载数据 // 初始化加载数据
const initData = async () => { const initData = async () => {
await fetchStatisticsData(); await fetchStatisticsData();
@ -797,17 +877,6 @@ const handleCurrentChange = (val) => {
currentPage.value = val; currentPage.value = val;
}; };
// 选项卡点击
const handleTabClick = (tab) => {
console.log('切换到选项卡:', tab.name);
// 重置筛选条件和分页
workOrderType.value = 'all';
workOrderStatus.value = 'all';
priority.value = 'all';
createDate.value = '';
currentPage.value = 1;
};
// 操作按钮事件 // 操作按钮事件
const handleViewDetail = async (row) => { const handleViewDetail = async (row) => {
try { try {
@ -875,10 +944,6 @@ const groupNodesByModule = (nodes) => {
})); }));
}; };
const handleEvaluate = (row) => {
console.log('评价:', row);
};
const handleCancel = (row) => { const handleCancel = (row) => {
console.log('取消工单:', row); console.log('取消工单:', row);
}; };
@ -1100,15 +1165,17 @@ const handleEdit = async (row) => {
const sortedNodes = [...workOrderDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0)); const sortedNodes = [...workOrderDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0));
// 转换为createForm.steps所需的格式 // 转换为createForm.steps所需的格式
createForm.steps = sortedNodes.map((node) => ({ createForm.steps = sortedNodes.map((node) => ({
content: node.intendedPurpose || '' name: node.name || '',
intendedPurpose: node.intendedPurpose || '',
intendedTime: node.intendedTime ? formatDate(node.intendedTime, 'YYYY-MM-DD HH:mm') : ''
})); }));
// 确保至少有一个空步骤 // 确保至少有一个空步骤
if (createForm.steps.length === 0) { if (createForm.steps.length === 0) {
createForm.steps = [{ content: '' }]; createForm.steps = [{ name: '', intendedPurpose: '', intendedTime: '' }];
} }
} else { } else {
// 如果没有nodes数据重置为默认的一个空步骤 // 如果没有nodes数据重置为默认的一个空步骤
createForm.steps = [{ content: '' }]; createForm.steps = [{ name: '', intendedPurpose: '', intendedTime: '' }];
} }
// 存储当前编辑的工单ID用于区分是创建还是编辑操作 // 存储当前编辑的工单ID用于区分是创建还是编辑操作
@ -1149,7 +1216,7 @@ const createForm = reactive({
description: '', description: '',
location: '', location: '',
relatedEquipment: '', relatedEquipment: '',
steps: [{ content: '' }], // 工单步骤数组 steps: [{ name: '', intendedPurpose: '', intendedTime: '' }], // 工单步骤数组
file: '', file: '',
fileList: [], fileList: [],
resultDescription: '', resultDescription: '',
@ -1168,7 +1235,7 @@ const createFormRules = {
// 添加试验步骤 // 添加试验步骤
const addStep = () => { const addStep = () => {
createForm.steps.push({ content: '' }); createForm.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
}; };
// 删除试验步骤 // 删除试验步骤
@ -1191,22 +1258,54 @@ const submitCreate = async () => {
// 准备步骤数据 // 准备步骤数据
const stepsData = createForm.steps const stepsData = createForm.steps
.filter((step) => step.content.trim()) .filter((step) => step.name.trim() && step.intendedPurpose.trim())
.map((step, index) => ({ .map((step, index) => ({
createTime: new Date().toISOString(), createTime: new Date().toISOString(),
updateTime: new Date().toISOString(), updateTime: new Date().toISOString(),
params: {}, params: {},
module: 1, module: 1,
code: index + 1, code: index + 1,
name: `步骤${index + 1}`, name: step.name,
intendedPurpose: step.content, intendedPurpose: step.intendedPurpose,
intendedTime: new Date().toISOString(), intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
finishTime: new Date().toISOString(), finishTime: '',
remark: '', remark: '',
status: '1' // 使用数字代码 status: 1 // 使用数字值而不是字符串,确保状态字段不为空
})); }));
// 首先调用addjiedian接口 // 编辑模式下需要为每个步骤添加id并调用updatejiedian接口
let nodeIds = '';
if (editingWorkOrderId.value) {
// 获取工单详情以获取原始步骤的id
const detailResponse = await gongdanDetail(editingWorkOrderId.value);
if (detailResponse.code !== 200) {
ElMessage.error('获取工单详情失败');
return;
}
const workOrderDetail = detailResponse.data;
if (workOrderDetail.nodes && Array.isArray(workOrderDetail.nodes)) {
// 按code排序原始nodes数组
const sortedNodes = [...workOrderDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0));
// 为新的步骤数据添加id
const updatedSteps = stepsData.map((step, index) => ({
...step,
id: sortedNodes[index]?.id || 0 // 使用原始步骤的id如果不存在则使用0
}));
// 调用updatejiedian接口更新步骤直接传递数组
const updateResponse = await updatejiedian(updatedSteps);
if (updateResponse.code !== 200) {
ElMessage.error('更新步骤失败');
return;
}
// 使用原始的nodeIds避免重新创建步骤
nodeIds = workOrderDetail.nodeIds;
}
} else {
// 创建模式下调用addjiedian接口直接传递数组
const jiedianResponse = await addjiedian(stepsData); const jiedianResponse = await addjiedian(stepsData);
if (jiedianResponse.code !== 200) { if (jiedianResponse.code !== 200) {
@ -1214,14 +1313,14 @@ const submitCreate = async () => {
return; return;
} }
// 获取返回的ids // 获取返回的ids实际返回格式中msg字段包含ids字符串data为null
let nodeIds = '';
if (jiedianResponse.code === 200 && jiedianResponse.msg) { if (jiedianResponse.code === 200 && jiedianResponse.msg) {
nodeIds = jiedianResponse.msg; nodeIds = jiedianResponse.msg;
} else { } else {
ElMessage.warning('未获取到有效的步骤ID'); ElMessage.warning('未获取到有效的步骤ID');
return; return;
} }
}
// 准备工单数据 // 准备工单数据
const workOrderData = { const workOrderData = {
@ -1268,7 +1367,7 @@ const submitCreate = async () => {
// 重置表单 // 重置表单
Object.keys(createForm).forEach((key) => { Object.keys(createForm).forEach((key) => {
if (key === 'steps') { if (key === 'steps') {
createForm[key] = [{ content: '' }]; createForm[key] = [{ name: '', intendedPurpose: '', intendedTime: '' }];
} else if (key === 'fileList') { } else if (key === 'fileList') {
createForm[key] = []; createForm[key] = [];
} else { } else {
@ -1298,7 +1397,7 @@ const cancelCreate = () => {
// 重置表单 // 重置表单
Object.keys(createForm).forEach((key) => { Object.keys(createForm).forEach((key) => {
if (key === 'steps') { if (key === 'steps') {
createForm[key] = [{ content: '' }]; createForm[key] = [{ name: '', intendedPurpose: '', intendedTime: '' }];
} else if (key === 'fileList') { } else if (key === 'fileList') {
createForm[key] = []; createForm[key] = [];
} else { } else {
@ -1341,9 +1440,16 @@ const handleInspectionManagement2 = () => {
const handleInspectionManagement3 = () => { const handleInspectionManagement3 = () => {
router.push('/rili/zhixingjilu'); router.push('/rili/zhixingjilu');
}; };
// 关闭详情弹窗
const handleCloseDetailDialog = () => {
detailDialogVisible.value = false;
};
</script> </script>
<style scoped> <style scoped>
@import url('./css/detail-dialog.css');
@import url('./css/step-bars.css');
.dispatch-records { .dispatch-records {
padding: 20px; padding: 20px;
background-color: #f5f7fa; background-color: #f5f7fa;
@ -1871,72 +1977,105 @@ const handleInspectionManagement3 = () => {
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.1); box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.1);
} }
/* 详情弹窗样式 */ /* 详情弹窗样式 - 与工单列表页面保持一致 */
.beautiful-dialog .el-dialog__body { .custom-experiment-dialog .el-dialog__body {
max-height: 600px; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 24px;
} }
.loading-state { .task-detail-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #909399;
}
.detail-container {
padding: 10px 0; padding: 10px 0;
} }
/* 详情卡片样式 */
.detail-card { .detail-card {
margin-bottom: 20px;
background-color: #fff; background-color: #fff;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); padding: 20px;
overflow: hidden; margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f2f5;
} }
.card-title { .card-title {
font-size: 14px; font-size: 16px;
font-weight: 500; font-weight: 600;
color: #1d2129; color: #1d2129;
padding: 12px 16px; margin-bottom: 16px;
background-color: #f7f8fa; padding-bottom: 12px;
border-bottom: 1px solid #f0f2f5; border-bottom: 2px solid #409eff;
margin-bottom: 0;
} }
.card-content { .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;
}
/* 骨架屏样式 */
.skeleton-loading {
display: flex;
flex-direction: column;
gap: 16px;
}
.skeleton-card {
background-color: #f5f5f5;
border-radius: 8px;
padding: 16px; padding: 16px;
} }
.info-row { .skeleton-header {
height: 20px;
width: 30%;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 12px;
}
.skeleton-content {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
margin-bottom: 8px; gap: 8px;
} }
.detail-item { .skeleton-row {
display: flex; height: 16px;
margin-bottom: 10px; width: 100%;
font-size: 13px; background-color: #e0e0e0;
flex: 0 0 50%; border-radius: 4px;
box-sizing: border-box;
padding-right: 12px;
}
.detail-label {
flex: 0 0 80px;
color: #86909c;
}
.detail-value {
flex: 1;
color: #4e5969;
word-break: break-all;
} }
/* 优先级标签样式 */ /* 优先级标签样式 */

View File

@ -251,10 +251,6 @@
<span class="info-label">维修人</span> <span class="info-label">维修人</span>
<span class="info-value">{{ detailData.sendPersonVo?.userName || '-' }}</span> <span class="info-value">{{ detailData.sendPersonVo?.userName || '-' }}</span>
</div> </div>
<div class="info-item">
<span class="info-label">所属团队</span>
<span class="info-value">{{ detailData.sendPersonVo?.teamName || '-' }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -281,12 +277,9 @@
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">现场支持</span> <span class="info-label">现场支持</span>
<span class="info-value">{{ <span class="info-value">{{ detailData.support || '-' }}</span>
detailData.support === '1' ? '支持' : detailData.support === '2' ? '不支持' : detailData.support || '-'
}}</span>
</div> </div>
</div> </div>
<!-- 已完成状态的额外信息 --> <!-- 已完成状态的额外信息 -->
<div v-if="detailData.status === '3'" class="info-row"> <div v-if="detailData.status === '3'" class="info-row">
<div class="info-item"> <div class="info-item">
@ -1386,45 +1379,51 @@ const handleInspectionManagement2 = () => {
} }
/* 详情弹窗样式 */ /* 详情弹窗样式 */
.task-detail-container { .custom-experiment-dialog .el-dialog__body {
max-height: 600px; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
padding: 24px;
} }
.task-detail-container {
padding: 10px 0;
}
/* 详情卡片样式 */
.detail-card { .detail-card {
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid #ebeef5; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border-radius: 4px; border: 1px solid #f0f2f5;
overflow: hidden;
} }
.card-title { .card-title {
padding: 16px 20px;
margin: 0;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 600;
color: #303133; color: #1d2129;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
} }
.card-content { .card-content {
padding: 20px; padding: 0 4px;
} }
/* 信息行和信息项样式 */
.info-row { .info-row {
display: flex; display: flex;
flex-wrap: wrap;
margin-bottom: 16px; margin-bottom: 16px;
} flex-wrap: wrap;
.info-row:last-child {
margin-bottom: 0;
} }
.info-item { .info-item {
flex: 0 0 50%; flex: 0 0 50%;
margin-bottom: 12px; margin-bottom: 12px;
display: flex;
align-items: flex-start;
} }
.info-item.full-width { .info-item.full-width {
@ -1432,47 +1431,104 @@ const handleInspectionManagement2 = () => {
} }
.info-label { .info-label {
color: #909399; font-weight: 500;
font-size: 14px; color: #86909c;
margin-right: 8px; margin-right: 8px;
min-width: 80px;
flex-shrink: 0;
} }
.info-value { .info-value {
color: #303133; color: #4e5969;
flex: 1;
word-break: break-all;
font-size: 14px; font-size: 14px;
word-break: break-word; }
/* 图片容器样式 */
.images-container {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 12px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 8px;
}
/* 单个图片项样式 */
.image-item {
flex: 0 0 auto;
width: 200px; /* 固定宽度 */
height: 160px; /* 固定高度 */
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease;
}
.image-item:hover {
transform: scale(1.03);
}
/* 图片样式 */
.detail-image {
width: 100%;
height: 100%;
object-fit: cover; /* 保持比例填充容器 */
display: block;
border-radius: 4px;
transition: transform 0.3s ease;
}
.detail-image:hover {
transform: scale(1.02);
}
/* 图片加载失败样式 */
.detail-image[src=''] {
background-color: #f0f0f0;
display: flex;
} }
/* 骨架屏样式 */ /* 骨架屏样式 */
.skeleton-loading { .skeleton-loading {
padding: 20px; display: flex;
flex-direction: column;
gap: 16px;
} }
.skeleton-card { .skeleton-card {
margin-bottom: 20px; background-color: #f5f5f5;
border-radius: 4px; border-radius: 8px;
overflow: hidden; padding: 16px;
background-color: #f8f9fa;
} }
.skeleton-header { .skeleton-header {
height: 56px; height: 20px;
background-color: #f1f3f5; width: 30%;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 12px;
} }
.skeleton-content { .skeleton-content {
padding: 20px; display: flex;
flex-direction: column;
gap: 8px;
} }
.skeleton-row { .skeleton-row {
height: 20px; height: 16px;
margin-bottom: 12px; background-color: #e0e0e0;
background-color: #e9ecef;
border-radius: 4px; border-radius: 4px;
} }
.skeleton-row:last-child { /* 无数据提示 */
margin-bottom: 0; .no-info {
text-align: center;
color: #909399;
padding: 60px 20px;
} }
/* 分配弹窗样式 */ /* 分配弹窗样式 */

View File

@ -423,7 +423,16 @@
<div class="steps-container"> <div class="steps-container">
<div class="step-item" v-for="(step, index) in formData.steps" :key="index"> <div class="step-item" v-for="(step, index) in formData.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div> <div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.content" placeholder="输入试验步骤" /> <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 <el-button
v-if="formData.steps.length > 1" v-if="formData.steps.length > 1"
type="text" type="text"
@ -524,9 +533,29 @@
</div> </div>
</div> </div>
<!-- 实验步骤 --> <!-- 执行步骤 -->
<div v-if="detailData.testStep" class="detail-section"> <div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card">
<h3 class="section-title">实验步骤</h3> <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)">
{{ getStepStatusText(node.status) }}
</div>
</div>
</div>
</div>
<!-- 兼容旧格式如果没有nodes数据但有testStep字符串 -->
<div v-else-if="detailData.testStep" class="detail-section">
<h3 class="section-title">执行步骤</h3>
<div class="steps-container"> <div class="steps-container">
<div v-for="(step, index) in detailData.testStep.split(',')" :key="index" class="step-item"> <div v-for="(step, index) in detailData.testStep.split(',')" :key="index" class="step-item">
<div class="step-number">{{ index + 1 }}</div> <div class="step-number">{{ index + 1 }}</div>
@ -589,7 +618,7 @@ import router from '@/router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { shiyanDetail, shiyanlist, addshiyan, updateshiyan } from '@/api/zhinengxunjian/shiyan/index'; import { shiyanDetail, shiyanlist, addshiyan, updateshiyan } from '@/api/zhinengxunjian/shiyan/index';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index'; import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index';
import { addjiedian } from '@/api/zhinengxunjian/jiedian/index'; import { addjiedian, updatejiedian } from '@/api/zhinengxunjian/jiedian/index';
// 1. 选项卡状态管理 // 1. 选项卡状态管理
const activeTab = ref('plan'); // 默认为"巡检计划" const activeTab = ref('plan'); // 默认为"巡检计划"
const timeRange = ref('month'); // 统计时间范围:月/周/日 const timeRange = ref('month'); // 统计时间范围:月/周/日
@ -822,7 +851,11 @@ const formData = ref({
envRequirements: '', envRequirements: '',
manager: '', manager: '',
participants: [], // 改为数组存储多选的用户ID participants: [], // 改为数组存储多选的用户ID
steps: [{ content: '' }, { content: '' }, { content: '' }], steps: [
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' }
],
equipments: [ equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false }, { name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false }, { name: '网络测试仪(型号:NT-5000)', selected: false },
@ -920,8 +953,8 @@ const handleSave = async () => {
inspectionItems: '', inspectionItems: '',
testSolutions: formData.value.riskMitigation, testSolutions: formData.value.riskMitigation,
testStep: formData.value.steps testStep: formData.value.steps
.filter((step) => step.content.trim()) .filter((step) => step.name.trim() || step.intendedPurpose.trim())
.map((step) => step.content) .map((step) => `${step.name || ''}: ${step.intendedPurpose || ''}`)
.join(','), .join(','),
testDevice: formData.value.equipments testDevice: formData.value.equipments
.filter((equip) => equip.selected) .filter((equip) => equip.selected)
@ -932,26 +965,64 @@ const handleSave = async () => {
id: editRecordId.value // 若后端用planId等需改为对应字段名 id: editRecordId.value // 若后端用planId等需改为对应字段名
}; };
// 4. 调用接口 // 4. 处理步骤数据并调用接口
let response; let response;
if (editRecordId.value) {
// 编辑模式:调用更新接口
response = await updateshiyan(requestData);
} else {
// 处理步骤数据格式 // 处理步骤数据格式
const stepsData = formData.value.steps const stepsData = formData.value.steps
.filter((step) => step.content.trim()) .filter((step) => step.name.trim() || step.intendedPurpose.trim())
.map((step, index) => ({ .map((step, index) => ({
createTime: new Date().toISOString(), createTime: new Date().toISOString(),
updateTime: new Date().toISOString(), updateTime: new Date().toISOString(),
remark: step.content.trim(), params: {},
status: '1', // 使用数字代码 module: 3,
// module值为2与工单列表的1不同 code: index + 1,
module: 2, name: step.name,
sort: index + 1 intendedPurpose: step.intendedPurpose,
intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
finishTime: '',
remark: '',
status: 2
})); }));
// 首先调用addjiedian接口 // 获取nodeIds
let nodeIds = '';
if (editRecordId.value) {
// 编辑模式获取试验详情以获取原始步骤的id
const detailResponse = await shiyanDetail(editRecordId.value);
if (detailResponse.code !== 200) {
ElMessage.error('获取试验详情失败');
return;
}
const experimentDetail = detailResponse.data.rows?.[0] || detailResponse.data;
// 兼容两种数据结构可能在rows数组中也可能直接在data中
if (experimentDetail.nodes && Array.isArray(experimentDetail.nodes)) {
// 按code排序原始nodes数组
const sortedNodes = [...experimentDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0));
// 为新的步骤数据添加id
const updatedSteps = stepsData.map((step, index) => ({
...step,
id: sortedNodes[index]?.id || 0 // 使用原始步骤的id如果不存在则使用0
}));
// 调用updatejiedian接口更新步骤直接传递数组
const updateResponse = await updatejiedian(updatedSteps);
if (updateResponse.code !== 200) {
ElMessage.error('更新步骤失败');
return;
}
// 使用原始的nodeIds避免重新创建步骤
nodeIds = experimentDetail.nodeIds;
}
// 编辑模式:调用更新接口
response = await updateshiyan(requestData);
} else {
// 新增模式调用addjiedian接口创建步骤
const jiedianResponse = await addjiedian(stepsData); const jiedianResponse = await addjiedian(stepsData);
if (jiedianResponse.code !== 200) { if (jiedianResponse.code !== 200) {
@ -960,7 +1031,6 @@ const handleSave = async () => {
} }
// 获取返回的ids实际返回格式中msg字段包含ids字符串 // 获取返回的ids实际返回格式中msg字段包含ids字符串
let nodeIds = '';
if (jiedianResponse.code === 200 && jiedianResponse.msg) { if (jiedianResponse.code === 200 && jiedianResponse.msg) {
nodeIds = jiedianResponse.msg; nodeIds = jiedianResponse.msg;
} else { } else {
@ -1002,7 +1072,11 @@ const resetForm = () => {
envRequirements: '', // 环境要求为空 envRequirements: '', // 环境要求为空
manager: '', // 负责人为空 manager: '', // 负责人为空
participants: [], // 参与人员为空数组 participants: [], // 参与人员为空数组
steps: [{ content: '' }, { content: '' }, { content: '' }], // 步骤内容为空 steps: [
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' }
], // 步骤内容为空
equipments: [ equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false }, { name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false }, { name: '网络测试仪(型号:NT-5000)', selected: false },
@ -1086,8 +1160,12 @@ const handleEditRecord = async (row) => {
const sortedNodes = [...recordDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0)); const sortedNodes = [...recordDetail.nodes].sort((a, b) => (a.code || 0) - (b.code || 0));
// 转换为所需的格式 // 转换为所需的格式
sortedNodes.forEach((node) => { sortedNodes.forEach((node) => {
if (node.intendedPurpose && node.intendedPurpose.trim()) { if ((node.name && node.name.trim()) || (node.intendedPurpose && node.intendedPurpose.trim())) {
steps.push({ content: node.intendedPurpose.trim() }); steps.push({
name: node.name || '',
intendedPurpose: node.intendedPurpose || '',
intendedTime: node.intendedTime || ''
});
} }
}); });
} }
@ -1099,13 +1177,18 @@ const handleEditRecord = async (row) => {
// 移除序号前缀(如"1. "),只保留内容 // 移除序号前缀(如"1. "),只保留内容
const content = stepText.replace(/^\d+\.\s*/, '').trim(); const content = stepText.replace(/^\d+\.\s*/, '').trim();
if (content) { if (content) {
steps.push({ content }); // 对于旧格式数据我们将内容放入intendedPurpose字段
steps.push({
name: `步骤${steps.length + 1}`,
intendedPurpose: content,
intendedTime: ''
});
} }
}); });
} }
// 确保至少有3个步骤如果解析后为空 // 确保至少有3个步骤如果解析后为空
while (steps.length < 3) { while (steps.length < 3) {
steps.push({ content: '' }); steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
} }
// 4. 处理testDevice将逗号分隔的字符串转换为设备数组 // 4. 处理testDevice将逗号分隔的字符串转换为设备数组
@ -1183,7 +1266,7 @@ const handleEditRecord = async (row) => {
}; };
// 添加新步骤 // 添加新步骤
const addStep = () => { const addStep = () => {
formData.value.steps.push({ content: '' }); formData.value.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
}; };
// 删除步骤 // 删除步骤
@ -1291,10 +1374,54 @@ const formatDate = (dateString) => {
const seconds = String(date.getSeconds()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}; };
// 根据步骤状态获取对应的样式类
const getStatusClass = (status) => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap = {
'1': 'status-pending',
'2': 'status-running',
'3': 'status-completed',
'4': 'status-delayed',
'5': 'status-failed'
};
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': '已延期',
'5': '失败'
};
return statusMap[statusStr] || '未知状态';
};
</script> </script>
<style scoped> <style scoped>
/* 1. 基础容器样式(继承试验系统) */ @import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
.operation-inspection { .operation-inspection {
padding: 20px; padding: 20px;
background-color: #f9fbfd; background-color: #f9fbfd;
@ -1336,7 +1463,7 @@ const formatDate = (dateString) => {
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
} }
/* 3. 页面标题(与试验系统一致) */ /* 3. 页面标题 */
.page-header { .page-header {
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -2094,221 +2221,4 @@ const formatDate = (dateString) => {
width: 100%; 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 {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.participant-name,
.inspection-name {
min-width: auto;
}
}
</style> </style>

View File

@ -1,6 +1,7 @@
<template> <template>
<div> <div>
<div class="operation-inspection"> <div class="operation-inspection">
<!-- 顶部导航选项卡 -->
<div class="navigation-tabs"> <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div> <div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div> <div class="nav-tab" @click="handleInspection2">巡检管理</div>
@ -11,6 +12,7 @@
<div class="nav-tab" @click="handleInspection7">运维组织</div> <div class="nav-tab" @click="handleInspection7">运维组织</div>
</div> </div>
<!-- 头部操作按钮 -->
<div class="header-container"> <div class="header-container">
<div class="header-actions"> <div class="header-actions">
<el-button type="primary" class="export-btn">筛选</el-button> <el-button type="primary" class="export-btn">筛选</el-button>
@ -27,7 +29,7 @@
</div> </div>
</div> </div>
<!-- 4. 筛选和操作区域 --> <!-- 筛选和操作区域 -->
<div class="filter-and-actions"> <div class="filter-and-actions">
<div class="filters"> <div class="filters">
<el-select v-model="filterStatus" placeholder="巡检状态" clearable> <el-select v-model="filterStatus" placeholder="巡检状态" clearable>
@ -54,13 +56,13 @@
class="date-picker" class="date-picker"
></el-date-picker> ></el-date-picker>
<el-button type="primary" icon="Search" class="search-btn"> 搜索 </el-button> <el-button type="primary" class="search-btn"> 搜索 </el-button>
</div> </div>
</div> </div>
<!-- 5. 主内容区 --> <!-- 主内容区 -->
<div class="content-container"> <div class="content-container">
<!-- 5.3 巡检记录根据图片调整 --> <!-- 试验记录 -->
<div v-if="activeTab === 'record'" class="record-container"> <div v-if="activeTab === 'record'" class="record-container">
<h2 class="section-title">试验记录与报告</h2> <h2 class="section-title">试验记录与报告</h2>
<p class="section-subtitle">截止至 {{ currentDate }}</p> <p class="section-subtitle">截止至 {{ currentDate }}</p>
@ -87,96 +89,62 @@
<!-- 试验记录列表 --> <!-- 试验记录列表 -->
<div class="test-records"> <div class="test-records">
<!-- 数据库性能巡检记录 --> <!-- 动态生成试验记录卡片 -->
<div class="test-record-card passed"> <div
v-for="(record, recordIndex) in testRecords"
:key="record.id"
class="test-record-card"
:class="{ 'passed': record.status === 'completed', 'failed': record.status === 'failed' }"
>
<div class="record-header"> <div class="record-header">
<h3 class="record-title">数据库性能巡检</h3> <h3 class="record-title">{{ record.taskName || '试验任务' }}</h3>
<p class="record-date"> <p class="record-date">
{{ testRecords[0].date }} <span class="record-time">耗时: {{ testRecords[0].duration }}</span> 开始时间
{{ formatDate(record.beginTime) }}
<span class="record-time">计划完成时间: {{ record.planFinishTime ? formatDate(record.planFinishTime) : '未知' }}</span>
</p> </p>
<span class="status-tag status-passed">通过</span> <span class="status-tag" :class="getStatusClass(record.status)">
{{ getStatusText(record.status) }}
</span>
</div> </div>
<!-- 试验进度 --> <!-- 动态生成试验进度步骤条 -->
<div class="test-progress"> <div class="test-progress" v-if="record.nodes && record.nodes.length">
<div class="progress-step active"> <template v-for="(node, index) in sortedNodes(record.nodes)" :key="node.id">
<div class="step-number">1</div> <div class="progress-step" :class="getNodeStatusClass(node.status, record.status)">
<div class="step-name">准备环境</div> <div class="step-number">{{ node.code }}</div>
</div> <div class="step-name">步骤名称{{ node.name }}</div>
<div class="progress-line active"></div> <div class="step-name">预期试验目的{{ node.intendedPurpose }}</div>
<div class="progress-step active">
<div class="step-number">2</div>
<div class="step-name">50%负载</div>
</div>
<div class="progress-line active"></div>
<div class="progress-step active">
<div class="step-number">3</div>
<div class="step-name">80%负载</div>
</div>
<div class="progress-line active"></div>
<div class="progress-step active">
<div class="step-number">4</div>
<div class="step-name">100%负载</div>
</div> </div>
<!-- 进度线最后一个节点没有线 -->
<div
v-if="index < sortedNodes(record.nodes).length - 1"
class="progress-line"
:class="getLineStatusClass(index, sortedNodes(record.nodes), record.status)"
></div>
</template>
</div> </div>
<!-- 试验结果 --> <!-- 试验结果 -->
<div class="test-result"> <div class="test-result" :class="{ 'failure-analysis': record.status === 'failed' }">
<h4 class="result-title">试验结果</h4> <h4 class="result-title">
<p class="result-content">系统在100%负载下稳定运行1小时CPU平均使用率92%内存使用率88%无崩溃或异常重启现象</p> {{ record.status === 'failed' ? '失败原因分析' : '试验结果' }}
<p class="result-details"> </h4>
平均响应时间: {{ testRecords[0].responseTime }} | 错误率: {{ testRecords[0].errorRate }} | 温度值: {{ testRecords[0].temperature }}
<p class="result-content">
{{ record.status === 'failed' ? record.failReason || '未提供失败原因' : record.testFinal || '试验完成,未提供详细结果' }}
</p> </p>
</div>
<div class="record-actions"> <p class="result-details" v-if="record.status !== 'failed'">
<button class="operate-btn view-btn">查看详情</button> 计划时间: {{ formatDate(record.planBeginTime) }} | 进度: {{ record.progress }}% | 负责人:
<button class="operate-btn report-btn">生成报告</button> {{ record.personInfo?.userName || '未知' }}
</div>
</div>
<!-- 1000用户并发测试记录 -->
<div class="test-record-card failed">
<div class="record-header">
<h3 class="record-title">1000用户并发测试</h3>
<p class="record-date">
{{ testRecords[1].date }} <span class="record-time">耗时: {{ testRecords[1].duration }}</span>
</p> </p>
<span class="status-tag status-failed">失败</span>
</div>
<!-- 试验进度 --> <!-- 改进建议仅失败时显示 -->
<div class="test-progress"> <div class="improvement-suggestion" v-if="record.status === 'failed' && record.faileTips">
<div class="progress-step active">
<div class="step-number">1</div>
<div class="step-name">准备环境</div>
</div>
<div class="progress-line active"></div>
<div class="progress-step active">
<div class="step-number">2</div>
<div class="step-name">300用户</div>
</div>
<div class="progress-line active"></div>
<div class="progress-step active">
<div class="step-number">3</div>
<div class="step-name">500用户</div>
</div>
<div class="progress-line failed"></div>
<div class="progress-step failed">
<div class="step-number">4</div>
<div class="step-name">800用户</div>
</div>
</div>
<!-- 失败原因分析 -->
<div class="test-result failure-analysis">
<h4 class="result-title">失败原因分析</h4>
<p class="result-content">当并发用户数达到780人时数据库连接耗尽新用户无法建立数据库连接导致系统响应超时</p>
<!-- 改进建议 -->
<div class="improvement-suggestion">
<i class="fas fa-lightbulb"></i> <i class="fas fa-lightbulb"></i>
<p>建议: 增加数据库连接池最大连接数优化长连接超时时间增加连接复用机制分析评估</p> <p>建议: {{ record.faileTips }}</p>
</div> </div>
</div> </div>
@ -185,10 +153,13 @@
<button class="operate-btn report-btn">生成报告</button> <button class="operate-btn report-btn">生成报告</button>
</div> </div>
</div> </div>
<!-- 无数据提示 -->
<div v-if="!testRecords.length" class="no-records">暂无试验记录数据</div>
</div> </div>
</div> </div>
<!-- 5.1 巡检计划表格 --> <!-- 巡检计划表格 -->
<div v-if="activeTab === 'plan'" class="table-container"> <div v-if="activeTab === 'plan'" class="table-container">
<el-table :data="planTableData" border> <el-table :data="planTableData" border>
<el-table-column prop="name" label="计划名称" width="220"> <el-table-column prop="name" label="计划名称" width="220">
@ -228,7 +199,7 @@
</el-table> </el-table>
</div> </div>
<!-- 5.2 巡检任务表格 --> <!-- 巡检任务表格 -->
<div v-if="activeTab === 'task'" class="table-container"> <div v-if="activeTab === 'task'" class="table-container">
<el-table :data="taskTableData" border> <el-table :data="taskTableData" border>
<el-table-column prop="name" label="任务名称" width="220"></el-table-column> <el-table-column prop="name" label="任务名称" width="220"></el-table-column>
@ -257,7 +228,7 @@
</div> </div>
</div> </div>
<!-- 6. 分页 --> <!-- 分页 -->
<div class="pagination" v-if="activeTab !== 'record'"> <div class="pagination" v-if="activeTab !== 'record'">
<p class="total-records">显示1到{{ pageSize }}{{ totalRecords }}条记录</p> <p class="total-records">显示1到{{ pageSize }}{{ totalRecords }}条记录</p>
<el-pagination <el-pagination
@ -275,11 +246,12 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import router from '@/router'; import router from '@/router';
import { syrenwulist, syrenwujilu, syrenwuDetail } from '@/api/zhinengxunjian/shiyan/renwu';
// 1. 选项卡状态管理 // 1. 选项卡状态管理
const activeTab = ref('record'); // 默认显示"巡检记录" const activeTab = ref('record'); // 默认显示"试验记录"
const showFilter = ref(false); const showFilter = ref(false);
// 2. 筛选条件 // 2. 筛选条件
@ -287,6 +259,11 @@ const filterStatus = ref('all');
const filterType = ref('all'); const filterType = ref('all');
const dateRange = ref([]); const dateRange = ref([]);
// 3. 试验记录数据
const testRecords = ref([]);
const planTableData = ref([]);
const taskTableData = ref([]);
// 4. 当前日期 // 4. 当前日期
const currentDate = computed(() => { const currentDate = computed(() => {
const date = new Date(); const date = new Date();
@ -297,28 +274,178 @@ const currentDate = computed(() => {
// 5. 统计数据 // 5. 统计数据
const statData = ref({ const statData = ref({
completed: 12, completed: 0,
passRate: 83, passRate: 0,
pendingAnalysis: 3, pendingAnalysis: 0,
avgDuration: '42分钟' avgDuration: '0分钟'
}); });
// 6. 试验记录数据 // 6. 分页相关
const testRecords = ref([ const currentPage = ref(1);
{ const pageSize = ref(20);
date: '2025-06-15', const totalRecords = ref(0);
duration: '1小时45分钟',
responseTime: '1.2s',
errorRate: '0%',
temperature: '72°C'
},
{
date: '2025-06-12',
duration: '2小时10分钟'
}
]);
// 9. 方法:切换顶部导航 // 7. 方法:获取试验记录数据
const getTestRecords = async () => {
try {
const response = await syrenwulist({
projectId: 1,
page: currentPage.value,
size: pageSize.value
});
console.log('syrenwulist API响应:', response);
if (response && response.code === 200 && response.rows) {
testRecords.value = response.rows;
totalRecords.value = response.total;
}
} catch (error) {
console.error('获取试验记录失败:', error);
}
};
// 8. 方法:获取统计数据
const getStatisticsData = async () => {
try {
const response = await syrenwujilu({ projectId: 1 });
console.log('syrenwujilu API响应:', response);
if (response && response.data) {
// 映射API返回的数据到statData
const apiData = response.data;
statData.value.completed = parseInt(apiData.finishCount) || 0;
statData.value.passRate = parseFloat(apiData.passValue) || 0;
statData.value.pendingAnalysis = parseInt(apiData.failCount) || 0;
// 格式化平均试验时长
const avgTime = parseInt(apiData.averageTestTime) || 0;
statData.value.avgDuration = `${avgTime}分钟`;
}
} catch (error) {
console.error('获取统计数据失败:', error);
}
};
// 9. 辅助方法对节点按code排序
const sortedNodes = (nodes) => {
return [...nodes].sort((a, b) => a.code - b.code);
};
// 10. 辅助方法:格式化日期
const formatDate = (dateString) => {
if (!dateString) return '未知日期';
const date = new Date(dateString);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
// 11. 辅助方法:格式化时长(假设单位为分钟)
const formatDuration = (minutes) => {
if (minutes < 60) {
return `${minutes}分钟`;
} else {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}小时${mins}分钟`;
}
};
// 12. 辅助方法:获取节点状态类名
const getNodeStatusClass = (nodeStatus, recordStatus) => {
// 节点状态: 2-未完成, 其他假设为已完成
// 记录状态: 'failed'-失败, 'completed'-完成, 其他为进行中
if (recordStatus === 'failed') {
// 如果记录失败,找到失败阶段的节点
return nodeStatus === '2' ? 'failed' : 'active';
} else if (recordStatus === 'completed') {
return 'active';
} else {
// 进行中状态已完成的节点标记为active
return nodeStatus !== '2' ? 'active' : '';
}
};
// 13. 辅助方法:获取进度线状态类名
const getLineStatusClass = (index, nodes, recordStatus) => {
// 如果记录失败找到第一个未完成的节点前的线为active
if (recordStatus === 'failed') {
return nodes[index].status !== '2' ? 'active' : 'failed';
} else if (recordStatus === 'completed') {
return 'active';
} else {
// 进行中状态已完成节点之间的线为active
return nodes[index].status !== '2' ? 'active' : '';
}
};
// 14. 辅助方法:获取进度颜色
const getProgressColor = (status) => {
const colorMap = {
'drafted': '#e5e7eb',
'in-progress': '#165dff',
'completed': '#00b42a',
'paused': '#86909c'
};
return colorMap[status] || '#e5e7eb';
};
// 15. 辅助方法:获取状态文本
const getStatusText = (status) => {
const statusMap = {
'1': '进行中',
'completed': '通过',
'failed': '失败',
'paused': '已暂停',
'drafted': '草稿',
'in-progress': '进行中',
'normal': '正常',
'attention': '需关注',
'problem': '有问题'
};
return statusMap[status] || '未知状态';
};
// 16. 辅助方法:获取任务状态文本
const getTaskStatusText = (status) => {
const statusMap = {
'pending': '待接受',
'accepted': '进行中',
'completed': '已完成',
'rejected': '已拒绝'
};
return statusMap[status] || '未知状态';
};
// 17. 辅助方法:获取状态类名
const getStatusClass = (status) => {
const classMap = {
'1': 'status-in-progress',
'completed': 'status-passed',
'failed': 'status-failed',
'paused': 'status-paused',
'pending': 'status-pending',
'accepted': 'status-accepted',
'rejected': 'status-rejected',
'normal': 'status-normal',
'attention': 'status-attention',
'problem': 'status-problem'
};
return classMap[status] || 'status-pending';
};
// 18. 分页事件处理
const handlePageChange = (page) => {
currentPage.value = page;
getTestRecords();
};
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
getTestRecords();
};
// 19. 导航方法
const handleInspection1 = () => { const handleInspection1 = () => {
router.push('/rili/rili'); router.push('/rili/rili');
}; };
@ -341,17 +468,28 @@ const handleInspection7 = () => {
router.push('/rili/renyuanzhuangtai'); router.push('/rili/renyuanzhuangtai');
}; };
const handleInspectionManagement1 = () => { const handleInspectionManagement1 = () => {
activeTab.value = 'plan';
router.push('/rili/shiyanguanli'); router.push('/rili/shiyanguanli');
}; };
const handleInspectionManagement2 = () => { const handleInspectionManagement2 = () => {
activeTab.value = 'task';
router.push('/rili/shiyanrenwu'); router.push('/rili/shiyanrenwu');
}; };
const handleInspectionManagement3 = () => { const handleInspectionManagement3 = () => {
activeTab.value = 'record';
router.push('/rili/shiyanjilu'); router.push('/rili/shiyanjilu');
}; };
// 20. 组件挂载时获取数据
onMounted(() => {
getStatisticsData();
getTestRecords();
});
</script> </script>
<style scoped> <style scoped>
@import url('./css/detail-dialog.css');
@import url('./css/step-bars.css');
/* 1. 基础容器样式 */ /* 1. 基础容器样式 */
.operation-inspection { .operation-inspection {
padding: 20px; padding: 20px;
@ -359,44 +497,38 @@ const handleInspectionManagement3 = () => {
min-height: 100vh; min-height: 100vh;
} }
/* 导航栏样式 */ /* 2. 顶部导航选项卡 */
.navigation-tabs { .navigation-tabs {
display: flex; display: flex;
margin-bottom: 20px;
background-color: #fff; background-color: #fff;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
padding: 2px; margin-bottom: 20px;
overflow: hidden;
} }
.nav-tab { .nav-tab {
padding: 12px 24px; padding: 12px 24px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s;
border-radius: 4px;
font-size: 14px; font-size: 14px;
color: #606266; color: #6b7280;
border-right: 1px solid #f0f0f0;
flex: 1; flex: 1;
text-align: center; text-align: center;
border-right: 1px solid #f0f0f0;
} }
.nav-tab:last-child { .nav-tab:last-child {
border-right: none; border-right: none;
} }
.nav-tab:hover:not(.active) {
.nav-tab:hover { background-color: #f3f4f6;
color: #409eff;
background-color: #ecf5ff;
} }
.nav-tab.active { .nav-tab.active {
background-color: #409eff; background-color: #165dff;
color: #fff; color: #fff;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); font-weight: 500;
} }
/* 选项卡样式 */ /* 3. 选项卡样式 */
.tabs-wrapper { .tabs-wrapper {
background-color: #fff; background-color: #fff;
padding: 20px; padding: 20px;
@ -405,34 +537,20 @@ const handleInspectionManagement3 = () => {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
} }
/* 筛选栏样式 */ /* 4. 头部容器 */
.filter-bar { .header-container {
background-color: #fff; display: flex;
padding: 20px; justify-content: space-between;
border-radius: 8px; align-items: center;
margin-bottom: 16px; margin-bottom: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
.header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.filter-item {
flex-shrink: 0;
}
.filter-bar .el-select,
.filter-bar .el-date-picker {
width: 150px;
height: 36px;
}
.filter-actions {
margin-left: auto;
display: flex;
gap: 10px; gap: 10px;
} }
/* 5. 筛选和操作区域 */ /* 5. 筛选和操作区域 */
.filter-and-actions { .filter-and-actions {
display: flex; display: flex;
@ -462,8 +580,8 @@ const handleInspectionManagement3 = () => {
} }
.search-btn, .search-btn,
.export-btn { .export-btn {
height: 36px; background-color: #165dff;
border-radius: 4px; border-color: #165dff;
} }
.filter-btn { .filter-btn {
background-color: #f3f4f6; background-color: #f3f4f6;
@ -662,17 +780,7 @@ const handleInspectionManagement3 = () => {
margin: 0 0 20px 0; margin: 0 0 20px 0;
text-align: right; text-align: right;
} }
/* 头部容器 - 替换了固定gap的flex布局 */
.header-container {
display: flex;
justify-content: flex-end;
align-items: center;
}
.header-actions {
display: flex;
gap: 10px;
}
/* 12. 统计卡片样式 */ /* 12. 统计卡片样式 */
.stat-grid { .stat-grid {
display: grid; display: grid;
@ -811,36 +919,35 @@ const handleInspectionManagement3 = () => {
.progress-line { .progress-line {
flex: 1; flex: 1;
height: 0; height: 2px;
border-top: 2px dashed #e5e7eb; background-color: #e5e7eb;
margin: 10px 0;
} }
.progress-step.active .step-number { .progress-step.active .step-number {
background-color: #10b981; background-color: #165dff;
color: white; color: white;
} }
.progress-step.active .step-name { .progress-step.active .step-name {
color: #10b981; color: #165dff;
font-weight: 500; font-weight: 500;
} }
.progress-line.active { .progress-line.active {
border-top-color: #10b981; background-color: #165dff;
} }
.progress-step.failed .step-number { .progress-step.failed .step-number {
background-color: red; background-color: #dc2626;
color: white; color: white;
} }
.progress-step.failed .step-name { .progress-step.failed .step-name {
color: red; color: #dc2626;
} }
.progress-line.failed { .progress-line.failed {
border-top-color: red; background-color: #dc2626;
} }
/* 15. 试验结果样式 */ /* 15. 试验结果样式 */
@ -906,7 +1013,17 @@ const handleInspectionManagement3 = () => {
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
} }
/* 17. 响应式适配 */ /* 17. 无数据提示样式 */
.no-records {
text-align: center;
padding: 40px 0;
color: #6b7280;
background-color: #fff;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
/* 18. 响应式适配 */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.stat-grid { .stat-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);

View File

@ -137,7 +137,7 @@
</div> </div>
<!-- 添加新任务弹窗 --> <!-- 添加新任务弹窗 -->
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="500px" :before-close="handleCancelCreateTask"> <el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="700px" :before-close="handleCancelCreateTask">
<el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px"> <el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px">
<el-form-item label="任务名称" prop="taskName"> <el-form-item label="任务名称" prop="taskName">
<el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" /> <el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" />
@ -192,6 +192,27 @@
style="width: 100%" style="width: 100%"
/> />
</el-form-item> </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> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@ -215,7 +236,7 @@
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">任务状态</span> <span class="info-label">任务状态</span>
<span class="info-value" :class="getStatusClass(detailData.status)"> <span class="info-value" :class="getTaskStatusClass(detailData.status)">
{{ getStatusText(detailData.status) }} {{ getStatusText(detailData.status) }}
</span> </span>
</div> </div>
@ -260,7 +281,7 @@
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">联系电话</span> <span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.personInfo.phone }}</span> <span class="info-value">{{ detailData.personInfo.phonenumber }}</span>
</div> </div>
</div> </div>
<div v-if="detailData.personInfo" class="info-row"> <div v-if="detailData.personInfo" class="info-row">
@ -268,10 +289,6 @@
<span class="info-label">性别</span> <span class="info-label">性别</span>
<span class="info-value">{{ detailData.personInfo.sex === '1' ? '男' : '女' }}</span> <span class="info-value">{{ detailData.personInfo.sex === '1' ? '男' : '女' }}</span>
</div> </div>
<div class="info-item">
<span class="info-label">民族</span>
<span class="info-value">{{ detailData.personInfo.nation }}</span>
</div>
</div> </div>
<div v-else class="no-info">暂无执行人信息</div> <div v-else class="no-info">暂无执行人信息</div>
</div> </div>
@ -307,6 +324,26 @@
</div> </div>
</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)">
{{ getStepStatusText(node.status) }}
</div>
</div>
</div>
</div>
<!-- 任务执行结果信息卡片如果有 --> <!-- 任务执行结果信息卡片如果有 -->
<div v-if="detailData.testFinal || detailData.failReason" class="detail-card"> <div v-if="detailData.testFinal || detailData.failReason" class="detail-card">
<h3 class="card-title">执行结果信息</h3> <h3 class="card-title">执行结果信息</h3>
@ -346,8 +383,10 @@ import router from '@/router';
import { syrenwulist, syrenwuDetail, addsyrenwu, updatesyrenwu } from '@/api/zhinengxunjian/shiyan/renwu'; import { syrenwulist, syrenwuDetail, addsyrenwu, updatesyrenwu } from '@/api/zhinengxunjian/shiyan/renwu';
import { shiyanlist } from '@/api/zhinengxunjian/shiyan'; import { shiyanlist } from '@/api/zhinengxunjian/shiyan';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index'; import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index';
import { addjiedian } from '@/api/zhinengxunjian/jiedian/index';
// 引入Element Plus组件提示/空状态/骨架屏/弹窗) // 引入Element Plus组件提示/空状态/骨架屏/弹窗)
import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox } from 'element-plus'; import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
/** /**
* 根据任务ID获取完整的任务详情数据 * 根据任务ID获取完整的任务详情数据
@ -379,6 +418,25 @@ const loading = ref(false);
// 筛选条件(与接口参数对应) // 筛选条件(与接口参数对应)
const taskStatus = ref(''); // 任务状态1=待执行2=暂停已延期3=失败4=执行中5=已完成 const taskStatus = ref(''); // 任务状态1=待执行2=暂停已延期3=失败4=执行中5=已完成
const planType = ref(''); // 关联计划ID1=每日2=每周3=每月 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=全部 const executor = ref('all'); // 执行人IDall=全部
// 用户列表通过xunjianUserlist接口获取 // 用户列表通过xunjianUserlist接口获取
@ -420,15 +478,58 @@ const getStatusText = (status) => {
* @param {string} status - 任务状态码 * @param {string} status - 任务状态码
* @returns {string} 样式类名 * @returns {string} 样式类名
*/ */
/**
* 获取步骤状态对应的样式类
* @param {string|number} status - 步骤状态码
* @returns {string} 样式类名
*/
const getStatusClass = (status) => { const getStatusClass = (status) => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap = { const statusClassMap = {
'1': 'status-pending', '1': 'status-pending',
'2': 'status-delayed', '2': 'status-delayed',
'3': 'status-failed', '3': 'status-executing',
'4': 'status-running', '4': 'status-completed'
'5': '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 +542,8 @@ const createTaskForm = ref({
relatedPlan: '', // 关联计划ID接口testPlanId relatedPlan: '', // 关联计划ID接口testPlanId
executor: '', // 执行人ID接口person executor: '', // 执行人ID接口person
workTimeRange1: null, // 工作时间段1 workTimeRange1: null, // 工作时间段1
workTimeRange2: null // 工作时间段2 workTimeRange2: null, // 工作时间段2
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }] // 步骤数据数组
}); });
// 创建任务表单规则 // 创建任务表单规则
const createTaskRules = { const createTaskRules = {
@ -453,6 +555,21 @@ const createTaskRules = {
executor: [{ required: true, message: '请选择执行人', trigger: 'change' }] 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字符串 // 构建timeInfo字符串
const getTaskTimeInfoString = () => { const getTaskTimeInfoString = () => {
const timeInfoArray = []; const timeInfoArray = [];
@ -831,6 +948,48 @@ const handleSaveTask = async () => {
return; 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 = { const createParams = {
createDept: 0, // 可根据实际情况从全局状态获取 createDept: 0, // 可根据实际情况从全局状态获取
createBy: 0, // 可根据实际情况从全局状态获取当前用户ID createBy: 0, // 可根据实际情况从全局状态获取当前用户ID
@ -863,7 +1022,8 @@ const handleSaveTask = async () => {
finalInfo: '', finalInfo: '',
pauseFor: '', pauseFor: '',
pauseTime: now.toISOString(), pauseTime: now.toISOString(),
planFinishTime: createTaskForm.value.timeRange[1] // 计划完成时间 planFinishTime: createTaskForm.value.timeRange[1], // 计划完成时间
nodeIds: nodeIds // 步骤节点ID数组
}; };
// 3. 调用创建接口 // 3. 调用创建接口
@ -904,7 +1064,10 @@ const handleCancelCreateTask = () => {
inspectionTarget: '', inspectionTarget: '',
timeRange: [], timeRange: [],
relatedPlan: '', relatedPlan: '',
executor: '' executor: '',
workTimeRange1: null,
workTimeRange2: null,
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
}; };
}; };
@ -954,6 +1117,9 @@ const pagedTasks = computed(() => {
</script> </script>
<style scoped> <style scoped>
@import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
/* 原有样式不变,新增无数据提示样式 */ /* 原有样式不变,新增无数据提示样式 */
.inspection-tasks { .inspection-tasks {
padding: 20px; padding: 20px;
@ -1068,6 +1234,27 @@ const pagedTasks = computed(() => {
background-color: #52c41a; background-color: #52c41a;
} }
/* 自定义步骤条样式覆盖 */
.custom-steps .el-step__description {
white-space: pre-wrap;
font-size: 12px;
color: #666;
line-height: 1.4;
}
.module-group {
margin-bottom: 20px;
}
.module-title {
font-size: 14px;
font-weight: 600;
color: #1d2129;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f2f5;
}
/* 卡片悬停效果 */ /* 卡片悬停效果 */
.task-card:hover { .task-card:hover {
transform: translateY(-3px); transform: translateY(-3px);
@ -1206,7 +1393,8 @@ const pagedTasks = computed(() => {
color: #165dff; color: #165dff;
} }
.start-btn { .start-btn,
.report-btn {
background-color: #165dff; background-color: #165dff;
border-color: #165dff; border-color: #165dff;
} }
@ -1221,11 +1409,6 @@ const pagedTasks = computed(() => {
border-color: #00b42a; border-color: #00b42a;
} }
.report-btn {
background-color: #86909c;
border-color: #86909c;
}
/* 分页区域样式 */ /* 分页区域样式 */
.pagination-section { .pagination-section {
display: flex; display: flex;

View File

@ -133,7 +133,7 @@
</div> </div>
<!-- 添加新任务弹窗 --> <!-- 添加新任务弹窗 -->
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="500px" :before-close="handleCancelCreateTask"> <el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="700px" :before-close="handleCancelCreateTask">
<el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px"> <el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px">
<el-form-item label="任务名称" prop="taskName"> <el-form-item label="任务名称" prop="taskName">
<el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" /> <el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" />
@ -211,6 +211,27 @@
<el-option label="设备运行状态" value="5" /> <el-option label="设备运行状态" value="5" />
</el-select> </el-select>
</el-form-item> </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> </el-form>
<template #footer> <template #footer>
@ -304,7 +325,7 @@
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">联系电话</span> <span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.person?.phone || '-' }}</span> <span class="info-value">{{ detailData.person?.phonenumber || '-' }}</span>
</div> </div>
</div> </div>
<div class="info-row"> <div class="info-row">
@ -312,10 +333,6 @@
<span class="info-label">性别</span> <span class="info-label">性别</span>
<span class="info-value">{{ detailData.person?.sex === '1' ? '男' : detailData.person?.sex === '2' ? '女' : '-' }}</span> <span class="info-value">{{ detailData.person?.sex === '1' ? '男' : detailData.person?.sex === '2' ? '女' : '-' }}</span>
</div> </div>
<div class="info-item">
<span class="info-label">民族</span>
<span class="info-value">{{ detailData.person?.nation || '-' }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -356,6 +373,26 @@
</div> </div>
</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)">
{{ getStepStatusText(node.status) }}
</div>
</div>
</div>
</div>
<!-- 执行结果信息卡片 --> <!-- 执行结果信息卡片 -->
<div v-if="detailData.taskType === '2' || detailData.taskType === 2" class="detail-card"> <div v-if="detailData.taskType === '2' || detailData.taskType === 2" class="detail-card">
<h3 class="card-title">延期信息</h3> <h3 class="card-title">延期信息</h3>
@ -388,37 +425,10 @@
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import router from '@/router'; 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 { xunjianUserlist, xunjianlist } from '@/api/zhinengxunjian/xunjian/index';
import { ElMessage, ElLoading } from 'element-plus'; import { addjiedian } from '@/api/zhinengxunjian/jiedian/index';
import { ElMessage, ElLoading, ElForm } 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');
// 筛选条件 // 筛选条件
const taskStatus = ref(''); const taskStatus = ref('');
@ -428,7 +438,7 @@ const executor = ref('');
// 任务数据 - 初始为空数组通过API获取 // 任务数据 - 初始为空数组通过API获取
const tasks = ref([]); const tasks = ref([]);
// 详情弹窗相关变量 // 任务详情弹窗相关变量
const detailDialogVisible = ref(false); const detailDialogVisible = ref(false);
const detailData = ref(null); const detailData = ref(null);
const isDetailLoading = ref(false); const isDetailLoading = ref(false);
@ -459,6 +469,34 @@ const getStatusClass = (status) => {
return statusClassMap[statusStr] || 'status-unknown'; 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 = { const statusConfig = {
pending: { pending: {
@ -505,11 +543,7 @@ const getTaskList = async () => {
const params = { const params = {
pageSize: pageSize.value, pageSize: pageSize.value,
pageNum: currentPage.value, pageNum: currentPage.value,
personId: executor.value !== '' ? executor.value : undefined, personId: 1
// 根据任务状态映射到后端需要的taskType
taskType: taskStatus.value ? mapTaskStatusToType(taskStatus.value) : undefined,
// 添加计划类型筛选
planType: planType.value || undefined
}; };
const response = await xjrenwulist(params); const response = await xjrenwulist(params);
@ -583,44 +617,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(() => { onMounted(() => {
getTaskList(); getTaskList();
@ -675,10 +671,11 @@ const createTaskForm = ref({
timeRange: [], timeRange: [],
workTimeRange1: null, workTimeRange1: null,
workTimeRange2: null, workTimeRange2: null,
relatedPlan: 'all', relatedPlan: '',
executor: '', executor: '',
taskType: '1', // 默认待执行 taskType: '1', // 默认待执行
problemType: '' problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }] // 任务步骤数组
}); });
const createTaskRules = { const createTaskRules = {
@ -694,6 +691,17 @@ const handleCreateTask = () => {
openCreateTaskDialog(); 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字符串 // 构建timeInfo字符串
const getTaskTimeInfoString = () => { const getTaskTimeInfoString = () => {
const timeInfoArray = []; const timeInfoArray = [];
@ -719,6 +727,21 @@ const getTaskTimeInfoString = () => {
return timeInfoArray.join(','); 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 () => { const handleSaveTask = async () => {
// 表单验证 // 表单验证
@ -727,6 +750,13 @@ const handleSaveTask = async () => {
return; return;
} }
// 验证所有步骤
const hasEmptyStep = createTaskForm.value.steps.some((step) => !step.name.trim() || !step.intendedPurpose.trim());
if (hasEmptyStep) {
ElMessage.warning('请填写完整所有步骤信息');
return;
}
try { try {
// 获取timeInfo字符串 // 获取timeInfo字符串
const taskTimeInfo = getTaskTimeInfoString(); const taskTimeInfo = getTaskTimeInfoString();
@ -736,12 +766,45 @@ const handleSaveTask = async () => {
return; 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 = { const apiData = {
createDept: 0, projectId: 1,
createBy: 0,
createTime: new Date().toISOString(), createTime: new Date().toISOString(),
updateBy: 0,
updateTime: new Date().toISOString(), updateTime: new Date().toISOString(),
params: { params: {
property1: 'string', property1: 'string',
@ -757,7 +820,8 @@ const handleSaveTask = async () => {
personId: createTaskForm.value.executor !== 'all' ? createTaskForm.value.executor : 0, personId: createTaskForm.value.executor !== 'all' ? createTaskForm.value.executor : 0,
taskProgress: 0, taskProgress: 0,
taskType: createTaskForm.value.taskType, taskType: createTaskForm.value.taskType,
problemType: createTaskForm.value.problemType problemType: createTaskForm.value.problemType,
nodeIds: nodeIds // 添加步骤ID字符串与工单列表页面保持一致
}; };
// 调用新增任务接口 // 调用新增任务接口
@ -774,10 +838,11 @@ const handleSaveTask = async () => {
timeRange: [], timeRange: [],
workTimeRange1: null, workTimeRange1: null,
workTimeRange2: null, workTimeRange2: null,
relatedPlan: 'all', relatedPlan: '',
executor: '', executor: '',
taskType: '1', taskType: '1',
problemType: '' problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
}; };
// 重新获取任务列表 // 重新获取任务列表
getTaskList(); getTaskList();
@ -846,11 +911,8 @@ const getPlansList = async () => {
label: item.planName || `计划${item.id}`, label: item.planName || `计划${item.id}`,
value: item.id.toString() value: item.id.toString()
})); }));
planList.value.unshift({ label: '全部计划', value: 'all' });
} catch (error) { } catch (error) {
console.error('获取计划列表失败:', error); console.error('获取计划列表失败:', error);
planList.value = [{ label: '全部计划', value: 'all' }];
} }
}; };
@ -868,8 +930,13 @@ const handleCancelCreateTask = () => {
taskName: '', taskName: '',
inspectionTarget: '', inspectionTarget: '',
timeRange: [], timeRange: [],
relatedPlan: 'all', workTimeRange1: null,
executor: 'all' workTimeRange2: null,
relatedPlan: '',
executor: '',
taskType: '1',
problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
}; };
}; };
@ -974,6 +1041,7 @@ const handleAction = async (task) => {
const updateData = { const updateData = {
...originalTask.rawData, ...originalTask.rawData,
id: task.id, id: task.id,
startTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
taskType: '3', // 3表示执行中 taskType: '3', // 3表示执行中
status: 'executing', status: 'executing',
taskProgress: 0 taskProgress: 0
@ -1039,6 +1107,9 @@ const handleAction = async (task) => {
</script> </script>
<style scoped> <style scoped>
@import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
.inspection-tasks { .inspection-tasks {
padding: 20px; padding: 20px;
background-color: #f5f7fa; background-color: #f5f7fa;
@ -1394,6 +1465,96 @@ const handleAction = async (task) => {
overflow-y: auto; 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 { .detail-card {
margin-bottom: 20px; margin-bottom: 20px;
padding: 20px; padding: 20px;
@ -1573,4 +1734,11 @@ const handleAction = async (task) => {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); 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> </style>

File diff suppressed because it is too large Load Diff