1900 lines
60 KiB
Vue
1900 lines
60 KiB
Vue
<template>
|
||
<div>
|
||
<div class="inspection-tasks">
|
||
<!-- 导航栏 -->
|
||
<!-- <div class="navigation-tabs">
|
||
<div class="nav-tab" @click="handleInspection1">待办事项</div>
|
||
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
|
||
<div class="nav-tab active" @click="handleInspection3">试验管理</div>
|
||
<div class="nav-tab" @click="handleInspection4">报修管理</div>
|
||
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
|
||
<div class="nav-tab" @click="handleInspection6">工单管理</div>
|
||
<div class="nav-tab" @click="handleInspection7">运维组织</div>
|
||
</div> -->
|
||
|
||
<!-- 选项卡 -->
|
||
<div class="tabs-wrapper">
|
||
<div style="display: flex; align-items: center; gap: 10px">
|
||
<el-button type="primary" @click="handleInspectionManagement1">试验计划</el-button>
|
||
<el-button type="primary" @click="handleInspectionManagement2">试验任务</el-button>
|
||
<el-button type="primary" @click="handleInspectionManagement3">试验记录</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选栏 -->
|
||
<div class="filter-bar">
|
||
<div class="filter-container">
|
||
<div class="filter-item">
|
||
<el-select v-model="taskStatus" placeholder="任务状态">
|
||
<el-option label="待执行" value="1"></el-option>
|
||
<el-option label="执行中" value="4"></el-option>
|
||
<el-option label="已延期" value="2"></el-option>
|
||
|
||
<el-option label="已完成" value="5"></el-option>
|
||
<el-option label="失败" value="3"></el-option>
|
||
</el-select>
|
||
</div>
|
||
<div class="filter-item">
|
||
<el-select v-model="planType" placeholder="全部计划">
|
||
<el-option label="每日巡检计划" value="1"></el-option>
|
||
<!-- 对应接口testPlanId -->
|
||
<el-option label="每周巡检计划" value="2"></el-option>
|
||
<el-option label="每月巡检计划" value="3"></el-option>
|
||
</el-select>
|
||
</div>
|
||
<div class="filter-item">
|
||
<el-select v-model="executor" placeholder="执行人">
|
||
<el-option label="全部人员" value="all"></el-option>
|
||
<el-option v-for="user in userList" :key="user.id" :label="user.userName" :value="user.id"></el-option>
|
||
</el-select>
|
||
</div>
|
||
<div class="filter-actions">
|
||
<el-button type="primary" icon="Search" class="search-btn" @click="handleSearch"> 搜索 </el-button>
|
||
<el-button type="primary" icon="Plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<el-skeleton v-if="loading" :count="8" :columns="3" class="mb-4"></el-skeleton>
|
||
|
||
<!-- 无数据提示 -->
|
||
<div v-else-if="total === 0" class="no-data">
|
||
<el-empty description="暂无试验任务数据"></el-empty>
|
||
</div>
|
||
|
||
<!-- 任务卡片列表 -->
|
||
<div class="task-cards" v-else>
|
||
<div class="task-card" v-for="task in pagedTasks" :key="task.id" :class="task.cardClass">
|
||
<div class="task-header">
|
||
<div class="task-title">{{ task.title }}</div>
|
||
<div class="task-status" :class="task.tagClass">{{ task.statusText }}</div>
|
||
</div>
|
||
|
||
<div class="task-details">
|
||
<!-- 失败卡片特殊展示 -->
|
||
<div v-if="task.status === '3'" class="failed-task-details">
|
||
<div class="detail-item">
|
||
<span class="detail-label">失败时间</span>
|
||
<span class="detail-value">{{ task.failTime || '未记录' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">试验阶段</span>
|
||
<span class="detail-value">{{ task.testStage || '未记录' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">执行人</span>
|
||
<span class="detail-value">{{ task.executor }}</span>
|
||
</div>
|
||
<div class="detail-item failed-reason-item">
|
||
<span class="detail-label">失败原因</span>
|
||
<span class="detail-value failed-reason">{{ task.originalData?.failReason || '未填写' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 其他状态的卡片展示 -->
|
||
<div v-else>
|
||
<div class="detail-item">
|
||
<span class="detail-label">计划时间</span>
|
||
<span class="detail-value">{{ task.planTime }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">测试对象</span>
|
||
<span class="detail-value">{{ task.target }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">执行人</span>
|
||
<span class="detail-value">{{ task.executor }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">关联计划</span>
|
||
<span class="detail-value">{{ task.relatedPlan }}</span>
|
||
</div>
|
||
|
||
<!-- 已延期(暂停)原因 -->
|
||
<div v-if="task.status === '2'" class="delay-reason">
|
||
<span class="detail-label">延期原因</span>
|
||
<span class="detail-value">{{ task.delayReason || '未填写' }}</span>
|
||
</div>
|
||
|
||
<!-- 执行中进度 -->
|
||
<div v-if="task.status === '4'" class="progress-container">
|
||
<span class="detail-label">完成进度</span>
|
||
<div class="progress-bar" style="flex: 1; padding-left: 0; margin-top: 0">
|
||
<el-progress :percentage="task.progress || 0" stroke-width="6" :stroke-color="task.progressColor"></el-progress>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已完成结果 -->
|
||
<div v-if="task.status === '5'" class="task-result">
|
||
<span class="detail-label">结果</span>
|
||
<span class="detail-value" :class="task.resultClass">{{ task.result }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="task-actions">
|
||
<!-- 失败卡片的特殊操作按钮 -->
|
||
<div v-if="task.status === '3'" class="failed-task-actions">
|
||
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
|
||
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
|
||
{{ task.actionText }}
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 其他状态的操作按钮 -->
|
||
<div v-else>
|
||
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
|
||
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
|
||
{{ task.actionText }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分页区域 -->
|
||
<div class="pagination-section" v-if="total > 0">
|
||
<div class="pagination-controls">
|
||
<el-pagination
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
:current-page="currentPage"
|
||
:page-sizes="[8, 12, 16, 20]"
|
||
:page-size="pageSize"
|
||
layout="prev, pager, next, jumper, ->, total"
|
||
:total="total"
|
||
background
|
||
>
|
||
</el-pagination>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加新任务弹窗 -->
|
||
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="750px" :before-close="handleCancelCreateTask">
|
||
<el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px">
|
||
<el-form-item label="任务名称" prop="taskName">
|
||
<el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" />
|
||
</el-form-item>
|
||
<el-form-item label="巡检对象" prop="inspectionTarget">
|
||
<el-input v-model="createTaskForm.inspectionTarget" placeholder="输入巡检内容" />
|
||
</el-form-item>
|
||
<el-form-item label="时间" prop="timeRange">
|
||
<el-date-picker
|
||
v-model="createTaskForm.timeRange"
|
||
type="datetimerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
format="YYYY-MM-DD HH:mm"
|
||
value-format="YYYY-MM-DD HH:mm"
|
||
placeholder="选择时间范围"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="关联计划" prop="relatedPlan">
|
||
<el-select v-model="createTaskForm.relatedPlan" placeholder="选择关联计划">
|
||
<el-option v-for="plan in experimentPlanList" :key="plan.id" :label="plan.planName" :value="plan.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="执行人" prop="executor">
|
||
<el-select v-model="createTaskForm.executor" placeholder="选择执行人">
|
||
<el-option v-for="user in userList" :key="user.id" :label="user.userName" :value="user.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="开始时间" prop="workTimeRange1">
|
||
<el-time-picker
|
||
v-model="createTaskForm.workTimeRange1"
|
||
type="timerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
format="HH:mm"
|
||
value-format="HH:mm"
|
||
class="form-input"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="结束时间" prop="workTimeRange2">
|
||
<el-time-picker
|
||
v-model="createTaskForm.workTimeRange2"
|
||
type="timerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
format="HH:mm"
|
||
value-format="HH:mm"
|
||
class="form-input"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<!-- 步骤条区域 -->
|
||
<el-form-item label="执行步骤" prop="steps">
|
||
<div class="steps-container">
|
||
<div class="step-item" v-for="(step, index) in createTaskForm.steps" :key="index">
|
||
<div class="step-number">{{ index + 1 }}</div>
|
||
<el-input v-model="step.name" placeholder="输入步骤名称" style="flex: 1; margin-right: 10px" />
|
||
<el-input v-model="step.intendedPurpose" placeholder="输入预期目的" style="flex: 1; margin-right: 10px" />
|
||
<el-date-picker
|
||
v-model="step.intendedTime"
|
||
type="datetime"
|
||
placeholder="选择计划时间"
|
||
format="YYYY-MM-DD HH:mm"
|
||
value-format="YYYY-MM-DD HH:mm"
|
||
style="width: 180px; margin-right: 10px"
|
||
/>
|
||
<el-button v-if="createTaskForm.steps.length > 1" type="text" @click="removeStep(index)" style="color: #f56c6c"> 删除 </el-button>
|
||
</div>
|
||
<el-button type="text" class="add-step-btn" @click="addStep">添加步骤</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="handleCancelCreateTask">取消</el-button>
|
||
<el-button type="primary" @click="handleSaveTask">保存任务</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 任务详情弹窗 -->
|
||
<el-dialog v-model="detailDialogVisible" title="任务详情" width="800px" :close-on-click-modal="false" center>
|
||
<div v-if="detailData" class="task-detail-container">
|
||
<!-- 任务基本信息卡片 -->
|
||
<div class="detail-card">
|
||
<h3 class="card-title">任务基本信息</h3>
|
||
<div class="card-content">
|
||
<div class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">任务名称:</span>
|
||
<span class="info-value">{{ detailData.taskName }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">任务状态:</span>
|
||
<span class="info-value" :class="getTaskStatusClass(detailData.status)">
|
||
{{ getStatusText(detailData.status) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">测试对象:</span>
|
||
<span class="info-value">{{ detailData.testObject }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">完成进度:</span>
|
||
<span class="info-value">{{ detailData.progress }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">开始时间:</span>
|
||
<span class="info-value">{{ detailData.beginTime }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">结束时间:</span>
|
||
<span class="info-value">{{ detailData.endTime }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">工作时间段:</span>
|
||
<span class="info-value">{{ detailData.timeInfo ? detailData.timeInfo.replace(/,/g, '—') : '-' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 执行人信息卡片 -->
|
||
<div class="detail-card">
|
||
<h3 class="card-title">执行人信息</h3>
|
||
<div class="card-content">
|
||
<div v-if="detailData.personInfo" class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">姓名:</span>
|
||
<span class="info-value">{{ detailData.personInfo.userName }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">联系电话:</span>
|
||
<span class="info-value">{{ detailData.personInfo.phonenumber }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="detailData.personInfo" class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">性别:</span>
|
||
<span class="info-value">{{ detailData.personInfo.sex === '1' ? '男' : '女' }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-else class="no-info">暂无执行人信息</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关联计划信息卡片 -->
|
||
<div class="detail-card">
|
||
<h3 class="card-title">关联计划信息</h3>
|
||
<div class="card-content">
|
||
<div v-if="detailData.testPlan" class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">计划名称:</span>
|
||
<span class="info-value">{{ detailData.testPlan.planName }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">计划编号:</span>
|
||
<span class="info-value">{{ detailData.testPlan.planCode }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="detailData.testPlan" class="info-row">
|
||
<div class="info-item">
|
||
<span class="info-label">计划时间:</span>
|
||
<span class="info-value">{{ detailData.testPlan.beginTime }} — {{ detailData.testPlan.endTime }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="detailData.testPlan && detailData.testPlan.testDevice" class="info-row">
|
||
<div class="info-item full-width">
|
||
<span class="info-label">测试设备:</span>
|
||
<span class="info-value">{{ detailData.testPlan.testDevice }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-else class="no-info">暂无关联计划信息</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)">
|
||
{{ node.status === '2' ? '未完成' : '已完成' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务执行结果信息卡片(如果有) -->
|
||
<div v-if="detailData.testFinal || detailData.failReason" class="detail-card">
|
||
<h3 class="card-title">执行结果信息</h3>
|
||
<div class="card-content">
|
||
<div v-if="detailData.testFinal" class="info-row">
|
||
<div class="info-item full-width">
|
||
<span class="info-label">测试结果:</span>
|
||
<span class="info-value">{{ detailData.testFinal }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="detailData.failReason" class="info-row">
|
||
<div class="info-item full-width">
|
||
<span class="info-label">失败原因:</span>
|
||
<span class="info-value fail-reason">{{ detailData.failReason }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="loading-details">
|
||
<el-skeleton :count="6" :columns="2" />
|
||
</div>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 日志弹窗 -->
|
||
<el-dialog v-model="logsDialogVisible" title="任务执行日志" width="700px" :close-on-click-modal="false">
|
||
<div v-if="!logsLoading" class="logs-container">
|
||
<div v-if="logsData.length > 0" class="logs-list">
|
||
<div v-for="(log, index) in logsData" :key="index" class="log-item">
|
||
<div class="log-time">{{ log.timestamp || '-' }}</div>
|
||
<div class="log-content">{{ log.content || '-' }}</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="no-logs">
|
||
<el-empty description="暂无执行日志" />
|
||
</div>
|
||
</div>
|
||
<div v-else class="loading-logs">
|
||
<el-skeleton :count="5" class="log-skeleton" />
|
||
</div>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="logsDialogVisible = false">关闭</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue';
|
||
import router from '@/router';
|
||
// 引入已定义的接口函数
|
||
import { syrenwulist, syrenwuDetail, addsyrenwu, updatesyrenwu } from '@/api/zhinengxunjian/shiyan/renwu';
|
||
import { shiyanlist } from '@/api/zhinengxunjian/shiyan';
|
||
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index';
|
||
import { addjiedian, updatejiedian } from '@/api/zhinengxunjian/jiedian/index';
|
||
// 引入Element Plus组件(提示/空状态/骨架屏/弹窗)
|
||
import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox, ElDialog } from 'element-plus';
|
||
|
||
// 日志弹窗相关变量
|
||
const logsDialogVisible = ref(false);
|
||
const logsData = ref([]);
|
||
const logsLoading = ref(false);
|
||
|
||
/**
|
||
* 根据任务ID获取完整的任务详情数据
|
||
* @param {string|number} taskId - 任务ID
|
||
* @returns {Promise<Object|null>} 完整的任务数据对象或null(获取失败)
|
||
*/
|
||
const getTaskDetails = async (taskId) => {
|
||
try {
|
||
const response = await syrenwuDetail(taskId);
|
||
if (response.code === 200 && response.data) {
|
||
// 直接返回接口数据,不做映射转换
|
||
return response.data;
|
||
} else {
|
||
const errorMsg = response.msg || '未知错误';
|
||
console.error(`获取任务详情失败: ${errorMsg}`);
|
||
ElMessage.warning(`获取任务详情失败:${errorMsg}`);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error('任务详情请求异常:', error);
|
||
ElMessage.error('任务详情请求异常:' + error.message);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// ============== 1. 基础状态管理 ==============
|
||
// 加载状态
|
||
const loading = ref(false);
|
||
// 筛选条件(与接口参数对应)
|
||
const taskStatus = ref(''); // 任务状态:1=待执行,2=暂停(已延期),3=失败,4=执行中,5=已完成
|
||
const planType = ref(''); // 关联计划ID:1=每日,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'); // 执行人ID:all=全部
|
||
|
||
// 用户列表(通过xunjianUserlist接口获取)
|
||
const userList = ref([]);
|
||
|
||
// 试验计划列表(通过shiyanlist接口获取)
|
||
const experimentPlanList = ref([]);
|
||
|
||
// 分页参数(与接口pageNum/pageSize对应)
|
||
const currentPage = ref(1); // 当前页
|
||
const pageSize = ref(8); // 每页条数
|
||
const total = ref(0); // 总条数
|
||
|
||
// 任务列表(接口返回数据)
|
||
const tasks = ref([]);
|
||
|
||
// 详情弹窗相关
|
||
const detailDialogVisible = ref(false);
|
||
const detailData = ref(null);
|
||
|
||
/**
|
||
* 获取任务状态文本
|
||
* @param {string} status - 任务状态码
|
||
* @returns {string} 状态文本
|
||
*/
|
||
const getStatusText = (status) => {
|
||
const statusMap = {
|
||
'1': '待执行',
|
||
'2': '已延期',
|
||
'3': '失败',
|
||
'4': '执行中',
|
||
'5': '已完成'
|
||
};
|
||
return statusMap[status] || '未知状态';
|
||
};
|
||
|
||
/**
|
||
* 获取任务状态对应的样式类
|
||
* @param {string} status - 任务状态码
|
||
* @returns {string} 样式类名
|
||
*/
|
||
|
||
/**
|
||
* 获取步骤状态对应的样式类
|
||
* @param {string|number} status - 步骤状态码
|
||
* @returns {string} 样式类名
|
||
*/
|
||
const getStatusClass = (status) => {
|
||
// 处理可能的数字输入
|
||
const statusStr = status?.toString() || '';
|
||
const statusClassMap = {
|
||
'1': 'status-pending',
|
||
'2': 'status-delayed',
|
||
'3': 'status-executing',
|
||
'4': 'status-completed'
|
||
};
|
||
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] || '未知状态';
|
||
};
|
||
|
||
// 创建任务弹窗
|
||
const createTaskDialogVisible = ref(false);
|
||
const createTaskFormRef = ref(null);
|
||
const createTaskForm = ref({
|
||
taskName: '', // 任务名称(接口taskName)
|
||
inspectionTarget: '', // 巡检对象(接口testObject)
|
||
timeRange: [], // 时间范围(开始=beginTime,结束=endTime)
|
||
relatedPlan: '', // 关联计划ID(接口testPlanId)
|
||
executor: '', // 执行人ID(接口person)
|
||
workTimeRange1: null, // 工作时间段1
|
||
workTimeRange2: null, // 工作时间段2
|
||
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }] // 步骤数据数组
|
||
});
|
||
// 创建任务表单规则
|
||
const createTaskRules = {
|
||
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||
inspectionTarget: [{ required: true, message: '请输入巡检对象', trigger: 'blur' }],
|
||
timeRange: [{ required: true, message: '请选择时间范围', trigger: 'change' }],
|
||
workTimeRange1: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
||
relatedPlan: [{ 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字符串
|
||
const getTaskTimeInfoString = () => {
|
||
const timeInfoArray = [];
|
||
|
||
// 处理工作时间段1
|
||
if (createTaskForm.value.workTimeRange1) {
|
||
if (Array.isArray(createTaskForm.value.workTimeRange1)) {
|
||
timeInfoArray.push(createTaskForm.value.workTimeRange1.join('-'));
|
||
} else if (typeof createTaskForm.value.workTimeRange1 === 'string') {
|
||
timeInfoArray.push(createTaskForm.value.workTimeRange1);
|
||
}
|
||
}
|
||
|
||
// 处理工作时间段2
|
||
if (createTaskForm.value.workTimeRange2) {
|
||
if (Array.isArray(createTaskForm.value.workTimeRange2)) {
|
||
timeInfoArray.push(createTaskForm.value.workTimeRange2.join('-'));
|
||
} else if (typeof createTaskForm.value.workTimeRange2 === 'string') {
|
||
timeInfoArray.push(createTaskForm.value.workTimeRange2);
|
||
}
|
||
}
|
||
|
||
return timeInfoArray.join(',');
|
||
};
|
||
|
||
const getUsersList = async () => {
|
||
try {
|
||
const response = await xunjianUserlist({});
|
||
if (response.code === 200) {
|
||
// 直接从接口返回的用户列表中提取信息
|
||
const users = response.rows || [];
|
||
|
||
// 将用户数据转换为所需格式:包含id和userName以适配模板和getUserById函数
|
||
userList.value = users.map((user) => ({
|
||
id: user.userId, // 用于标识和查找
|
||
userName: user.userName // 显示名称
|
||
}));
|
||
|
||
// 调试信息,确认数据格式正确
|
||
console.log('处理后的用户列表:', userList.value);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户列表失败:', error);
|
||
// 出错时提供默认选项,避免下拉框为空
|
||
userList.value = [{ label: '默认用户', value: '' }];
|
||
}
|
||
};
|
||
|
||
// 获取试验计划列表
|
||
const getExperimentPlanList = async () => {
|
||
try {
|
||
const response = await shiyanlist({});
|
||
if (response.code === 200) {
|
||
// 根据实际接口返回格式处理数据
|
||
experimentPlanList.value = response.rows || response.data?.rows || [];
|
||
}
|
||
} catch (error) {
|
||
console.error('获取试验计划列表失败:', error);
|
||
ElMessage.error('获取试验计划列表失败');
|
||
}
|
||
};
|
||
|
||
// ============== 2. 接口请求核心逻辑 ==============
|
||
/**
|
||
* 调用syrenwulist接口获取试验任务列表
|
||
* 接口参数:参考createDept数据结构,包含分页、筛选、排序
|
||
*/
|
||
const getTaskList = async () => {
|
||
try {
|
||
loading.value = true;
|
||
// 1. 构造接口请求参数(严格匹配createDept结构)
|
||
const requestParams = {
|
||
pageNum: currentPage.value,
|
||
pageSize: pageSize.value,
|
||
projectId: 1, // 项目ID(必需字段,需从全局状态/路由获取真实值)
|
||
status: taskStatus.value || undefined, // 任务状态(为空不传递)
|
||
testPlanId: planType.value || undefined, // 关联计划ID(筛选条件)
|
||
person: executor.value === 'all' ? undefined : executor.value // 执行人ID(筛选条件)
|
||
};
|
||
|
||
// 2. 调用接口(已引入的syrenwulist函数)
|
||
const response = await syrenwulist(requestParams);
|
||
// 根据实际接口返回格式:{ code: 200, rows: [], total: 0 }
|
||
|
||
if (response.code === 200) {
|
||
// 3. 接口数据映射为页面展示格式
|
||
tasks.value = (response.rows || []).map((item) => mapApiToView(item));
|
||
total.value = response.total || 0; // 同步总条数
|
||
} else {
|
||
ElMessage.error('获取任务列表失败:' + (response.msg || '未知错误'));
|
||
tasks.value = [];
|
||
total.value = 0;
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('接口请求异常:' + error.message);
|
||
tasks.value = [];
|
||
total.value = 0;
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 接口数据 → 页面展示数据映射
|
||
* @param {Object} apiData - 接口返回的单条任务数据
|
||
* @returns {Object} 页面所需的任务格式
|
||
*/
|
||
const mapApiToView = (apiData) => {
|
||
// 1. 状态映射:接口status(1-5)→ 页面状态文本/样式
|
||
const statusConfigMap = {
|
||
'1': {
|
||
statusText: '待执行',
|
||
cardClass: 'card-pending',
|
||
tagClass: 'tag-pending',
|
||
actionText: '开始执行',
|
||
actionClass: 'start-btn',
|
||
result: '-'
|
||
},
|
||
'2': {
|
||
statusText: '已延期',
|
||
cardClass: 'card-delayed',
|
||
tagClass: 'tag-delayed',
|
||
actionText: '重新安排',
|
||
actionClass: 'reschedule-btn',
|
||
result: '-'
|
||
},
|
||
'3': {
|
||
statusText: '失败',
|
||
cardClass: 'card-failed',
|
||
tagClass: 'tag-failed',
|
||
actionText: '重新执行',
|
||
actionClass: 'reschedule-btn',
|
||
result: '失败',
|
||
resultClass: 'result-abnormal'
|
||
},
|
||
'4': {
|
||
statusText: '执行中',
|
||
cardClass: 'card-executing',
|
||
tagClass: 'tag-executing',
|
||
actionText: '完成',
|
||
actionClass: 'complete-btn',
|
||
result: '-',
|
||
progressColor: '#FF7D00'
|
||
},
|
||
'5': {
|
||
statusText: '已完成',
|
||
cardClass: 'card-completed',
|
||
tagClass: 'tag-completed',
|
||
actionText: '查看报告',
|
||
actionClass: 'report-btn',
|
||
result: apiData.testFinal || '正常',
|
||
resultClass: apiData.testFinal === '异常' ? 'result-abnormal' : 'result-normal'
|
||
}
|
||
};
|
||
const statusConfig = statusConfigMap[apiData.status] || statusConfigMap['1'];
|
||
|
||
// 2. 执行人ID → 姓名映射(通过用户列表动态查找)
|
||
const getUserById = (userId) => {
|
||
const user = userList.value.find((u) => String(u.id) === String(userId));
|
||
return user ? user.userName : '未知人员';
|
||
};
|
||
|
||
// 3. 关联计划ID → 计划名称映射(通过试验计划列表动态查找)
|
||
const getPlanNameById = (planId) => {
|
||
const plan = experimentPlanList.value.find((p) => String(p.id) === String(planId));
|
||
return plan ? plan.planName : '无';
|
||
};
|
||
|
||
// 4. 格式化时间(接口date-time → 页面展示格式)
|
||
const formatTime = (timeStr) => (timeStr ? timeStr.replace('T', ' ').split('.')[0] : '未设置');
|
||
|
||
// 优先从apiData.testPlan中获取关联计划名称
|
||
let relatedPlanName = '无';
|
||
if (apiData.testPlan && apiData.testPlan.planName) {
|
||
relatedPlanName = apiData.testPlan.planName;
|
||
} else if (apiData.testPlanId) {
|
||
relatedPlanName = getPlanNameById(apiData.testPlanId);
|
||
}
|
||
|
||
// 优先从apiData.personInfo中获取执行人姓名
|
||
let executorName = '未知人员';
|
||
if (apiData.personInfo && apiData.personInfo.userName) {
|
||
executorName = apiData.personInfo.userName;
|
||
} else if (apiData.person) {
|
||
executorName = getUserById(apiData.person);
|
||
}
|
||
|
||
// 格式化失败时间
|
||
const formatFailTime = (timeStr) => {
|
||
if (timeStr) {
|
||
const date = new Date(timeStr);
|
||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(
|
||
date.getHours()
|
||
).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||
}
|
||
return '未记录';
|
||
};
|
||
|
||
// 生成试验阶段信息
|
||
const getTestStage = () => {
|
||
try {
|
||
// 优先查找nodes数组中处于执行中或失败的节点来确定当前试验阶段
|
||
if (apiData && apiData.nodes && Array.isArray(apiData.nodes)) {
|
||
// 查找执行中状态的节点
|
||
const executingNode = apiData.nodes.find((node) => {
|
||
if (!node || node.status === undefined) return false;
|
||
return node.status === '2' || node.status === 2;
|
||
});
|
||
|
||
// 如果有执行中的节点,根据code判断阶段
|
||
if (executingNode && executingNode.code !== undefined) {
|
||
const stepName = executingNode.name || '未命名步骤';
|
||
return `第${executingNode.code}步(${stepName})`;
|
||
}
|
||
|
||
// 查找失败状态的节点
|
||
const failedNode = apiData.nodes.find((node) => {
|
||
if (!node || node.status === undefined) return false;
|
||
return node.status === '3' || node.status === 3;
|
||
});
|
||
|
||
// 如果有失败的节点,根据code判断阶段
|
||
if (failedNode && failedNode.code !== undefined) {
|
||
const stepName = failedNode.name || '未命名步骤';
|
||
return `第${failedNode.code}步(${stepName})`;
|
||
}
|
||
|
||
// 查找已完成的节点,确定最后完成的阶段
|
||
const completedNodes = apiData.nodes.filter((node) => {
|
||
if (!node || node.status === undefined) return false;
|
||
return node.status === '4' || node.status === 4;
|
||
});
|
||
|
||
if (completedNodes.length > 0) {
|
||
// 按code排序,取最大的code
|
||
completedNodes.sort((a, b) => Number(b.code) - Number(a.code));
|
||
if (completedNodes[0].code !== undefined) {
|
||
const stepName = completedNodes[0].name || '未命名步骤';
|
||
return `第${completedNodes[0].code}步(${stepName})`;
|
||
}
|
||
}
|
||
}
|
||
// 如果没有找到符合条件的nodes数据,检查是否有明确的试验阶段信息
|
||
if (apiData && apiData.testStage) {
|
||
return apiData.testStage;
|
||
}
|
||
// 如果没有明确的阶段信息,尝试从关联计划中获取
|
||
if (apiData && apiData.testPlan && apiData.testPlan.stage) {
|
||
return apiData.testPlan.stage;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取试验阶段信息失败:', error);
|
||
}
|
||
return '未记录';
|
||
};
|
||
|
||
return {
|
||
id: apiData.id, // 任务ID(v-for的key,唯一标识)
|
||
title: apiData.taskName || '未命名任务', // 任务名称
|
||
status: apiData.status, // 原始状态值(用于条件判断)
|
||
statusText: statusConfig.statusText,
|
||
cardClass: statusConfig.cardClass,
|
||
tagClass: statusConfig.tagClass,
|
||
planTime: formatTime(apiData.beginTime), // 计划开始时间
|
||
target: apiData.testObject || '未指定', // 巡检对象(测试对象)
|
||
executor: executorName, // 执行人姓名
|
||
relatedPlan: relatedPlanName, // 关联计划名称
|
||
delayReason: apiData.pauseFor || '未填写', // 延期(暂停)原因
|
||
progress: apiData.progress || 0, // 执行进度(仅执行中显示)
|
||
progressColor: statusConfig.progressColor,
|
||
result: statusConfig.result,
|
||
resultClass: statusConfig.resultClass,
|
||
actionText: statusConfig.actionText,
|
||
actionClass: statusConfig.actionClass,
|
||
testFinal: apiData.testFinal, // 结果(用于详情页)
|
||
originalData: apiData, // 保存原始数据,用于后续操作
|
||
// 失败卡片特有字段
|
||
failTime: formatFailTime(apiData.failTime),
|
||
testStage: getTestStage()
|
||
};
|
||
};
|
||
|
||
// ============== 3. 筛选与分页事件 ==============
|
||
/**
|
||
* 搜索按钮点击:重置页码并重新请求数据
|
||
*/
|
||
const handleSearch = () => {
|
||
currentPage.value = 1; // 筛选时重置到第一页
|
||
getTaskList();
|
||
};
|
||
|
||
/**
|
||
* 每页条数变化
|
||
* @param {number} val - 新的每页条数
|
||
*/
|
||
const handleSizeChange = (val) => {
|
||
pageSize.value = val;
|
||
currentPage.value = 1; // 条数变化时重置页码
|
||
getTaskList();
|
||
};
|
||
|
||
/**
|
||
* 当前页码变化
|
||
* @param {number} val - 新的页码
|
||
*/
|
||
const handleCurrentChange = (val) => {
|
||
currentPage.value = val;
|
||
getTaskList();
|
||
};
|
||
|
||
// ============== 4. 任务操作逻辑(详情/执行/创建) ==============
|
||
/**
|
||
* 查看任务详情
|
||
* @param {Object} task - 任务数据
|
||
*/
|
||
const handleView = async (task) => {
|
||
try {
|
||
if (!task || !task.id) {
|
||
ElMessage.warning('任务ID不存在,请刷新页面重试');
|
||
console.error('任务缺少有效id,任务数据:', task);
|
||
return;
|
||
}
|
||
|
||
const response = await syrenwuDetail(task.id);
|
||
|
||
if (response.code === 200 && response.data) {
|
||
detailData.value = response.data;
|
||
detailDialogVisible.value = true;
|
||
} else {
|
||
const errorMsg = response.msg || '未知错误';
|
||
console.error(`获取任务详情失败: ${errorMsg}`);
|
||
ElMessage.error(`获取任务详情失败:${errorMsg}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取任务详情出错:', error);
|
||
ElMessage.error('获取任务详情出错,请重试');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 任务操作(开始执行/重新安排/完成等)
|
||
* @param {Object} task - 任务数据
|
||
*/
|
||
const handleAction = async (task) => {
|
||
try {
|
||
// 1. 获取当前任务的完整数据
|
||
const taskDetails = await getTaskDetails(task.id);
|
||
if (!taskDetails) {
|
||
ElMessage.error('获取任务详情失败');
|
||
return;
|
||
}
|
||
|
||
// 2. 创建包含所有参数的更新对象,确保所有字段都被包含
|
||
let updateParams = {
|
||
...taskDetails, // 包含所有原始字段
|
||
id: task.id
|
||
};
|
||
|
||
// 声明resultType变量,提升作用域
|
||
let resultType = null;
|
||
|
||
// 3. 根据任务状态只修改状态相关的字段
|
||
if (task.status === '4') {
|
||
// 执行中 → 完成:使用弹窗确认结果
|
||
try {
|
||
// 保持原有结构
|
||
} catch (error) {
|
||
console.error('捕获到异常:', error);
|
||
}
|
||
|
||
try {
|
||
const confirmResult = await ElMessageBox.confirm('请选择试验结果', '完成试验', {
|
||
confirmButtonText: '正常',
|
||
cancelButtonText: '异常',
|
||
type: 'warning',
|
||
distinguishCancelAndClose: true
|
||
});
|
||
// 用户点击确认(正常)
|
||
updateParams.status = '5';
|
||
updateParams.progress = 100;
|
||
updateParams.testFinal = '正常';
|
||
resultType = 'normal'; // 现在在外部作用域中定义
|
||
} catch (error) {
|
||
if (error === 'cancel') {
|
||
// 用户点击取消(异常),弹出失败原因输入框
|
||
try {
|
||
const failReasonResult = await ElMessageBox.prompt('请输入失败原因', '试验异常', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
inputPlaceholder: '请详细描述失败原因...',
|
||
inputValidator: (value) => {
|
||
if (!value || value.trim() === '') {
|
||
return '失败原因不能为空';
|
||
}
|
||
return true;
|
||
}
|
||
});
|
||
|
||
// 用户输入了失败原因并确认
|
||
updateParams.status = '3';
|
||
updateParams.progress = '';
|
||
updateParams.testFinal = '异常';
|
||
updateParams.failReason = failReasonResult.value; // 绑定失败原因参数
|
||
updateParams.failTime = formatLocalDateTime(new Date()); // 记录失败时间
|
||
resultType = 'abnormal';
|
||
|
||
// 将第一条未完成的步骤状态改为3(失败)
|
||
if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) {
|
||
const firstUnfinishedNode = taskDetails.nodes.find((node) => {
|
||
return node.status === '2' || node.status === 2;
|
||
});
|
||
if (firstUnfinishedNode) {
|
||
// 使用updatejiedian接口更新节点状态,构造完整的节点信息数组
|
||
const nodeUpdateParams = [
|
||
{
|
||
...firstUnfinishedNode,
|
||
status: '3',
|
||
updateTime: new Date().toISOString(),
|
||
// 确保包含所有必需字段
|
||
createDept: firstUnfinishedNode.createDept || 0,
|
||
createBy: firstUnfinishedNode.createBy || 0,
|
||
createTime: firstUnfinishedNode.createTime || new Date().toISOString(),
|
||
updateBy: firstUnfinishedNode.updateBy || 0,
|
||
params: firstUnfinishedNode.params || {
|
||
property1: 'string',
|
||
property2: 'string'
|
||
},
|
||
module: firstUnfinishedNode.module || 'string',
|
||
orderId: firstUnfinishedNode.orderId || 0,
|
||
code: firstUnfinishedNode.code || 0,
|
||
name: firstUnfinishedNode.name || 'string',
|
||
intendedPurpose: firstUnfinishedNode.intendedPurpose || 'string',
|
||
intendedTime: firstUnfinishedNode.intendedTime || new Date().toISOString(),
|
||
finishTime: firstUnfinishedNode.finishTime || '',
|
||
remark: firstUnfinishedNode.remark || ''
|
||
}
|
||
];
|
||
await updatejiedian(nodeUpdateParams);
|
||
// 更新本地数据以反映最新状态
|
||
firstUnfinishedNode.status = '3';
|
||
}
|
||
}
|
||
} catch (innerError) {
|
||
// 用户取消了失败原因输入
|
||
return;
|
||
}
|
||
} else {
|
||
// 关闭弹窗,不执行操作
|
||
return;
|
||
}
|
||
}
|
||
} else {
|
||
// 其他状态处理保持不变
|
||
switch (task.status) {
|
||
case '1': // 待执行 → 开始执行(状态改为4)
|
||
updateParams.status = '4';
|
||
updateParams.progress = 0; // 初始进度10%
|
||
// 设置开始时间为当前时间(使用本地时间而非UTC时间)
|
||
updateParams.planBeginTime = formatLocalDateTime(new Date());
|
||
break;
|
||
case '2': // 已延期 → 重新安排(状态改为1,重置时间)
|
||
updateParams.status = '1';
|
||
updateParams.beginTime = formatLocalDateTime(new Date());
|
||
break;
|
||
case '3': // 失败 → 重试(状态改为1)
|
||
updateParams.status = '1';
|
||
// 清空失败相关字段,使用适合各字段数据类型的默认值
|
||
updateParams.failReason = '';
|
||
updateParams.failTime = ''; // 时间类型字段使用null
|
||
updateParams.failPhase = ''; // 整数类型字段使用0
|
||
|
||
// 将失败的步骤状态改回2(未完成)
|
||
if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) {
|
||
const failedNodes = taskDetails.nodes.filter((node) => {
|
||
return node.status === '3' || node.status === 3;
|
||
});
|
||
|
||
// 构造包含所有失败节点的完整信息数组
|
||
const nodeUpdateParams = failedNodes.map((failedNode) => ({
|
||
...failedNode,
|
||
status: '2',
|
||
updateTime: new Date().toISOString(),
|
||
// 确保包含所有必需字段
|
||
createDept: failedNode.createDept || 0,
|
||
createBy: failedNode.createBy || 0,
|
||
createTime: failedNode.createTime || new Date().toISOString(),
|
||
updateBy: failedNode.updateBy || 0,
|
||
params: failedNode.params || {
|
||
property1: 'string',
|
||
property2: 'string'
|
||
},
|
||
module: failedNode.module || 'string',
|
||
orderId: failedNode.orderId || 0,
|
||
code: failedNode.code || 0,
|
||
name: failedNode.name || 'string',
|
||
intendedPurpose: failedNode.intendedPurpose || 'string',
|
||
intendedTime: failedNode.intendedTime || new Date().toISOString(),
|
||
finishTime: failedNode.finishTime || '',
|
||
remark: failedNode.remark || ''
|
||
}));
|
||
// 一次性调用updatejiedian接口更新所有节点
|
||
await updatejiedian(nodeUpdateParams);
|
||
// 更新本地数据以反映最新状态
|
||
for (const failedNode of failedNodes) {
|
||
failedNode.status = '2';
|
||
}
|
||
}
|
||
break;
|
||
default:
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 对于执行中状态('4')的任务,预先设置好时间字段
|
||
if (task.status === '4') {
|
||
// 根据结果类型设置相应的时间(使用本地时间而非UTC时间)
|
||
if (resultType === 'normal') {
|
||
updateParams.planFinishTime = formatLocalDateTime(new Date());
|
||
} else if (resultType === 'abnormal') {
|
||
updateParams.failTime = formatLocalDateTime(new Date());
|
||
}
|
||
}
|
||
|
||
// 调用更新接口(只调用一次)
|
||
const response = await updatesyrenwu(updateParams);
|
||
if (response.code === 200) {
|
||
ElMessage.success(`任务${task.actionText}成功`);
|
||
getTaskList(); // 刷新任务列表
|
||
} else {
|
||
ElMessage.error(`任务${task.actionText}失败:` + response.msg);
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error(`操作异常:` + error.message);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 格式化本地日期时间为 'YYYY-MM-DD HH:mm' 格式
|
||
* @param {Date} date - 日期对象
|
||
* @returns {string} 格式化后的日期时间字符串
|
||
*/
|
||
const formatLocalDateTime = (date) => {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||
};
|
||
|
||
/**
|
||
* 打开创建任务弹窗
|
||
*/
|
||
const handleCreateTask = async () => {
|
||
try {
|
||
// 打开弹窗前先获取用户列表和试验计划列表
|
||
await Promise.all([getUsersList(), getExperimentPlanList()]);
|
||
createTaskDialogVisible.value = true;
|
||
} catch (error) {
|
||
console.error('打开创建任务弹窗失败:', error);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 保存新任务(调用addsyrenwu接口)
|
||
*/
|
||
const handleSaveTask = async () => {
|
||
const formRef = createTaskFormRef.value;
|
||
if (!formRef) return;
|
||
|
||
// 1. 表单验证
|
||
formRef.validate(async (isValid) => {
|
||
if (isValid) {
|
||
try {
|
||
// 2. 构造创建任务的接口参数,严格按照要求的结构
|
||
const now = new Date();
|
||
|
||
// 获取timeInfo字符串
|
||
const taskTimeInfo = getTaskTimeInfoString();
|
||
|
||
if (!taskTimeInfo) {
|
||
ElMessage.warning('请至少填写一个工作时间段');
|
||
return;
|
||
}
|
||
|
||
// 验证所有步骤
|
||
const hasEmptyStep = createTaskForm.value.steps.some((step) => !step.name.trim() || !step.intendedPurpose.trim());
|
||
if (hasEmptyStep) {
|
||
ElMessage.warning('请填写完整所有步骤信息');
|
||
return;
|
||
}
|
||
|
||
// 处理步骤数据
|
||
let nodeIds = '';
|
||
if (createTaskForm.value.steps && createTaskForm.value.steps.length > 0) {
|
||
// 过滤非空步骤并映射为所需格式
|
||
const validSteps = createTaskForm.value.steps
|
||
.filter((step) => step.name.trim() && step.intendedPurpose.trim())
|
||
.map((step, index) => ({
|
||
createTime: new Date().toISOString(),
|
||
updateTime: new Date().toISOString(),
|
||
params: {},
|
||
module: 3,
|
||
code: index + 1,
|
||
name: step.name,
|
||
intendedPurpose: step.intendedPurpose,
|
||
intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
|
||
finishTime: '',
|
||
remark: '',
|
||
status: 2
|
||
}));
|
||
|
||
if (validSteps.length > 0) {
|
||
try {
|
||
// 调用addjiedian接口获取nodeIds
|
||
const jiedianResponse = await addjiedian(validSteps);
|
||
if (jiedianResponse.code === 200 && jiedianResponse.msg) {
|
||
nodeIds = jiedianResponse.msg; // 直接使用字符串格式,不转换为数组
|
||
}
|
||
} catch (error) {
|
||
console.error('添加节点失败:', error);
|
||
ElMessage.error('添加执行步骤失败');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
const createParams = {
|
||
createDept: 0, // 可根据实际情况从全局状态获取
|
||
createBy: 0, // 可根据实际情况从全局状态获取当前用户ID
|
||
createTime: now.toISOString(),
|
||
updateBy: 0,
|
||
updateTime: now.toISOString(),
|
||
params: {
|
||
property1: 'string',
|
||
property2: 'string'
|
||
},
|
||
projectId: 1, // 必需字段,需替换为真实项目ID
|
||
taskName: createTaskForm.value.taskName, // 任务名称
|
||
testObject: createTaskForm.value.inspectionTarget, // 测试对象(巡检对象)
|
||
beginTime: createTaskForm.value.timeRange[0], // 开始时间
|
||
endTime: createTaskForm.value.timeRange[1], // 结束时间
|
||
timeInfo: taskTimeInfo, // 使用统一的时间信息生成函数
|
||
person: createTaskForm.value.executor, // 执行人ID(必需)
|
||
status: '1', // 初始状态:待执行(必需)
|
||
testPlanId: createTaskForm.value.relatedPlan, // 关联计划ID(必需)
|
||
testSetting: '', // 测试设置
|
||
planBeginTime: '', // 计划开始时间(新增时为空)
|
||
progress: 0, // 初始进度0%
|
||
failReason: '',
|
||
failTime: '', // 失败时间(新增时为空)
|
||
failPhase: '',
|
||
faileAnalyze: '',
|
||
faileTips: '',
|
||
testLongTime: 0,
|
||
testFinal: '',
|
||
finalInfo: '',
|
||
pauseFor: '',
|
||
pauseTime: '', // 暂停时间(新增时为空)
|
||
planFinishTime: '', // 计划完成时间(新增时为空)
|
||
nodeIds: nodeIds // 步骤节点ID数组
|
||
};
|
||
|
||
// 3. 调用创建接口
|
||
const response = await addsyrenwu(createParams);
|
||
if (response.code === 200) {
|
||
ElMessage.success('任务创建成功');
|
||
createTaskDialogVisible.value = false;
|
||
getTaskList(); // 刷新任务列表
|
||
|
||
// 4. 重置表单
|
||
createTaskForm.value = {
|
||
taskName: '',
|
||
inspectionTarget: '',
|
||
timeRange: [],
|
||
relatedPlan: '',
|
||
executor: ''
|
||
};
|
||
formRef.resetFields(); // 重置表单验证状态
|
||
} else {
|
||
ElMessage.error('创建失败:' + response.msg);
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('创建异常:' + error.message);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 取消创建任务
|
||
*/
|
||
const handleCancelCreateTask = () => {
|
||
createTaskDialogVisible.value = false;
|
||
// 重置表单
|
||
createTaskFormRef.value?.resetFields();
|
||
createTaskForm.value = {
|
||
taskName: '',
|
||
inspectionTarget: '',
|
||
timeRange: [],
|
||
relatedPlan: '',
|
||
executor: '',
|
||
workTimeRange1: null,
|
||
workTimeRange2: null,
|
||
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
|
||
};
|
||
};
|
||
|
||
// ============== 5. 导航栏跳转逻辑(保持原有) ==============
|
||
const handleInspectionManagement1 = () => {
|
||
router.push('/znxj/sygl/shiyanguanli');
|
||
};
|
||
const handleInspectionManagement2 = () => {
|
||
router.push('/znxj/sygl/shiyanrenwu');
|
||
};
|
||
const handleInspectionManagement3 = () => {
|
||
router.push('/znxj/sygl/shiyanjilu');
|
||
};
|
||
const handleInspection1 = () => {
|
||
router.push('/znxj/rili');
|
||
};
|
||
const handleInspection2 = () => {
|
||
router.push('/znxj/xjgl/InspectionManagement');
|
||
};
|
||
const handleInspection3 = () => {
|
||
router.push('/znxj/sygl/shiyanguanli');
|
||
};
|
||
const handleInspection4 = () => {
|
||
router.push('/znxj/bxgl/baoxiuguanli');
|
||
};
|
||
const handleInspection5 = () => {
|
||
router.push('/znxj/qxgl/qiangxiuguanli');
|
||
};
|
||
const handleInspection6 = () => {
|
||
router.push('/znxj/gdgl/gongdanliebiao');
|
||
};
|
||
const handleInspection7 = () => {
|
||
router.push('/znxj/ywzz/renyuanzhuangtai');
|
||
};
|
||
|
||
// ============== 6. 页面初始化(加载任务列表和用户列表) ==============
|
||
onMounted(() => {
|
||
getUsersList();
|
||
getExperimentPlanList();
|
||
getTaskList();
|
||
});
|
||
|
||
// ============== 7. 分页数据(直接使用接口返回的分页列表) ==============
|
||
const pagedTasks = computed(() => {
|
||
return tasks.value;
|
||
});
|
||
// 获取任务状态对应的CSS类
|
||
const getTaskStatusClass = (status) => {
|
||
const statusStr = status?.toString() || '';
|
||
const statusMap = {
|
||
'1': 'status-pending',
|
||
'2': 'status-delayed',
|
||
'3': 'status-failed',
|
||
'4': 'status-running',
|
||
'5': 'status-completed'
|
||
};
|
||
return statusMap[statusStr] || 'status-pending';
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
@import url('./css/step-bars.css');
|
||
@import url('./css/detail-dialog.css');
|
||
|
||
/* 原有样式不变,新增无数据提示样式 */
|
||
.inspection-tasks {
|
||
padding: 20px;
|
||
background-color: #f5f7fa;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* 选项卡样式 */
|
||
.tabs-wrapper {
|
||
background-color: #fff;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
/* 筛选栏样式 */
|
||
.filter-bar {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||
padding: 16px 24px;
|
||
}
|
||
|
||
.filter-container {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
width: 100%;
|
||
}
|
||
|
||
.filter-item {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.filter-bar .el-select {
|
||
width: 180px;
|
||
height: 36px;
|
||
}
|
||
|
||
.filter-actions {
|
||
margin-left: auto;
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.search-btn,
|
||
.create-btn {
|
||
height: 36px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* 任务卡片样式 */
|
||
.task-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.task-card {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 16px 16px 60px 24px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
min-height: 280px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
/* 卡片左侧状态竖线 */
|
||
.task-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 6px;
|
||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
/* 卡片阴影样式 */
|
||
.card-pending {
|
||
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.15);
|
||
}
|
||
|
||
.card-delayed {
|
||
box-shadow: 0 4px 16px rgba(255, 77, 79, 0.15);
|
||
}
|
||
|
||
.card-executing {
|
||
box-shadow: 0 4px 16px rgba(250, 140, 22, 0.15);
|
||
}
|
||
|
||
.card-completed {
|
||
box-shadow: 0 4px 16px rgba(82, 196, 26, 0.15);
|
||
}
|
||
|
||
.card-failed {
|
||
box-shadow: 0 4px 16px rgba(255, 77, 79, 0.15);
|
||
}
|
||
|
||
/* 左侧状态线颜色 */
|
||
.card-pending::before {
|
||
background-color: #1677ff;
|
||
}
|
||
.card-delayed::before {
|
||
background-color: #ff4d4f;
|
||
}
|
||
.card-executing::before {
|
||
background-color: #fa8c16;
|
||
}
|
||
.card-completed::before {
|
||
background-color: #52c41a;
|
||
}
|
||
.card-failed::before {
|
||
background-color: #ff4d4f;
|
||
}
|
||
|
||
/* 卡片悬停效果 */
|
||
.task-card:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1), 0 4px 16px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.task-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #f0f2f5;
|
||
}
|
||
|
||
.task-title {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #1d2129;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* 状态标签样式 */
|
||
.task-status {
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.tag-pending {
|
||
background-color: #e6f7ff;
|
||
color: #1677ff;
|
||
border-color: #91d5ff;
|
||
}
|
||
|
||
.tag-delayed {
|
||
background-color: #fff2f0;
|
||
color: #ff4d4f;
|
||
border-color: #ffccc7;
|
||
}
|
||
|
||
.tag-executing {
|
||
background-color: #fffbe6;
|
||
color: #fa8c16;
|
||
border-color: #ffe58f;
|
||
}
|
||
|
||
.tag-completed {
|
||
background-color: #f6ffed;
|
||
color: #52c41a;
|
||
border-color: #b7eb8f;
|
||
}
|
||
|
||
.tag-failed {
|
||
background-color: #fff2f0;
|
||
color: #ff4d4f;
|
||
border-color: #ffccc7;
|
||
}
|
||
|
||
.task-details {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
margin-bottom: 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.detail-label {
|
||
flex: 0 0 80px;
|
||
color: #86909c;
|
||
}
|
||
|
||
.detail-value {
|
||
flex: 1;
|
||
color: #4e5969;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.delay-reason {
|
||
display: flex;
|
||
margin: 10px 0;
|
||
font-size: 13px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed #f0f2f5;
|
||
}
|
||
|
||
.progress-container {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 10px 0;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed #f0f2f5;
|
||
}
|
||
|
||
.progress-bar {
|
||
flex: 1;
|
||
padding-left: 10px;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.task-result {
|
||
display: flex;
|
||
margin: 10px 0;
|
||
font-size: 13px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed #f0f2f5;
|
||
}
|
||
|
||
.result-normal {
|
||
color: #00b42a;
|
||
}
|
||
|
||
.result-abnormal {
|
||
color: #f53f3f;
|
||
}
|
||
|
||
.task-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #f0f2f5;
|
||
position: absolute;
|
||
bottom: 16px;
|
||
right: 16px;
|
||
left: 16px;
|
||
background-color: #fff;
|
||
padding: 12px 0 0 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.action-btn {
|
||
font-size: 13px;
|
||
padding: 4px 10px;
|
||
}
|
||
|
||
.view-btn {
|
||
color: #165dff;
|
||
}
|
||
|
||
/* 失败卡片特殊样式 */
|
||
.failed-task-details {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.failed-reason-item {
|
||
padding-top: 8px;
|
||
border-top: 1px dashed #f0f2f5;
|
||
}
|
||
|
||
.failed-reason {
|
||
color: #f53f3f;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.failed-task-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* 分页区域样式 */
|
||
.pagination-section {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* 导航栏样式 */
|
||
.navigation-tabs {
|
||
display: flex;
|
||
margin-bottom: 20px;
|
||
background-color: #fff;
|
||
border-radius: 4px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||
padding: 2px;
|
||
}
|
||
|
||
.nav-tab {
|
||
padding: 12px 24px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
border-right: 1px solid #f0f0f0;
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-tab:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.nav-tab:hover {
|
||
color: #409eff;
|
||
background-color: #ecf5ff;
|
||
}
|
||
|
||
.nav-tab.active {
|
||
background-color: #409eff;
|
||
color: #fff;
|
||
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
|
||
}
|
||
|
||
/* 无数据提示样式 */
|
||
.no-data {
|
||
text-align: center;
|
||
padding: 40px 0;
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
/* 日志弹窗样式 */
|
||
.logs-container {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.logs-list {
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.log-item {
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f0f2f5;
|
||
}
|
||
|
||
.log-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.log-time {
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.log-content {
|
||
font-size: 14px;
|
||
color: #1d2129;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.no-logs {
|
||
text-align: center;
|
||
padding: 60px 0;
|
||
}
|
||
|
||
.log-skeleton {
|
||
margin: 12px 0;
|
||
}
|
||
|
||
/* 任务详情弹窗样式 */
|
||
.task-detail-container {
|
||
max-height: 600px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.fail-reason {
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.no-info {
|
||
color: #909399;
|
||
font-style: italic;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.loading-details {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.status-pending {
|
||
color: #e6a23c;
|
||
}
|
||
|
||
.status-completed {
|
||
color: #67c23a;
|
||
}
|
||
|
||
.status-delayed {
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.status-failed {
|
||
color: #909399;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 1200px) {
|
||
.task-cards {
|
||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.info-item {
|
||
min-width: 100%;
|
||
}
|
||
.info-row {
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.inspection-tasks {
|
||
padding: 16px;
|
||
}
|
||
|
||
.filter-container {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.task-cards {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|