|
@@ -1,180 +1,175 @@
|
|
|
<!--工单处置管理-->
|
|
<!--工单处置管理-->
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="workorder-management">
|
|
|
|
|
- <!-- 页面头部统计 -->
|
|
|
|
|
- <div class="page-header">
|
|
|
|
|
- <div class="header-title">
|
|
|
|
|
|
|
+ <div class="min-h-screen bg-[#f0f2f6] p-5">
|
|
|
|
|
+ <div class="mb-5 flex flex-wrap items-center justify-between gap-4 rounded-[20px] bg-white px-6 py-4">
|
|
|
|
|
+ <div class="flex items-center gap-[14px] text-[20px] font-600">
|
|
|
<el-icon><Tickets /></el-icon>
|
|
<el-icon><Tickets /></el-icon>
|
|
|
<span>工单任务管理中心</span>
|
|
<span>工单任务管理中心</span>
|
|
|
<el-tag type="primary" effect="dark">待办工单 {{ pendingCount }}</el-tag>
|
|
<el-tag type="primary" effect="dark">待办工单 {{ pendingCount }}</el-tag>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="header-stats">
|
|
|
|
|
- <div class="stat" @click="filterStatus('')">
|
|
|
|
|
- <div class="num">{{ totalOrders }}</div>
|
|
|
|
|
- <div>全部工单</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat" @click="filterStatus('pending')">
|
|
|
|
|
- <div class="num warning">{{ pendingCount }}</div>
|
|
|
|
|
- <div>待处理</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat" @click="filterStatus('processing')">
|
|
|
|
|
- <div class="num">{{ processingCount }}</div>
|
|
|
|
|
- <div>处理中</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat" @click="filterStatus('feedback')">
|
|
|
|
|
- <div class="num">{{ feedbackCount }}</div>
|
|
|
|
|
- <div>待审批</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="stat" @click="filterStatus('completed')">
|
|
|
|
|
- <div class="num">{{ completedCount }}</div>
|
|
|
|
|
- <div>已完成</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="flex flex-wrap gap-3">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="searchKeyword"
|
|
|
|
|
+ placeholder="搜索工单编号/案件编号"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ :prefix-icon="Search"
|
|
|
|
|
+ class="w-[220px]"
|
|
|
|
|
+ @keyup.enter="loadOrders"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-button type="primary" @click="loadOrders">刷新</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="main-layout">
|
|
|
|
|
- <!-- 左侧工单列表 -->
|
|
|
|
|
- <div class="order-list-panel">
|
|
|
|
|
- <div class="list-header">
|
|
|
|
|
|
|
+ <div class="mb-5 flex flex-wrap gap-6">
|
|
|
|
|
+ <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === '' }" @click="filterStatus('')">
|
|
|
|
|
+ <div class="text-[26px] font-bold">{{ totalOrders }}</div>
|
|
|
|
|
+ <div>全部工单</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'pending' }" @click="filterStatus('pending')">
|
|
|
|
|
+ <div class="text-[26px] font-bold text-[#e6a23c]">{{ pendingCount }}</div>
|
|
|
|
|
+ <div>待处理</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'processing' }" @click="filterStatus('processing')">
|
|
|
|
|
+ <div class="text-[26px] font-bold">{{ processingCount }}</div>
|
|
|
|
|
+ <div>处理中</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'verify' }" @click="filterStatus('verify')">
|
|
|
|
|
+ <div class="text-[26px] font-bold">{{ verifyCount }}</div>
|
|
|
|
|
+ <div>待验收</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="min-w-[120px] cursor-pointer rounded-2xl bg-white px-4 py-3 text-center" :class="{ 'bg-[#ecf5ff]': statusFilter === 'completed' }" @click="filterStatus('completed')">
|
|
|
|
|
+ <div class="text-[26px] font-bold">{{ completedCount }}</div>
|
|
|
|
|
+ <div>已完成</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex gap-5">
|
|
|
|
|
+ <div class="w-[380px] flex flex-col overflow-hidden rounded-[20px] bg-white" v-loading="loading">
|
|
|
|
|
+ <div class="flex justify-between border-b border-[#e9ecef] px-4 py-4 font-600">
|
|
|
<span>工单列表</span>
|
|
<span>工单列表</span>
|
|
|
- <el-input v-model="searchKeyword" placeholder="搜索工单/案件编号" prefix-icon="Search" clearable size="small" style="width: 180px" />
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="order-list">
|
|
|
|
|
|
|
+ <div class="flex-1 overflow-y-auto p-3">
|
|
|
<div
|
|
<div
|
|
|
- v-for="order in paginatedOrders"
|
|
|
|
|
- :key="order.id"
|
|
|
|
|
- class="order-item"
|
|
|
|
|
- :class="{ active: selectedOrder?.id === order.id, [order.status]: true }"
|
|
|
|
|
- @click="selectOrder(order)"
|
|
|
|
|
|
|
+ v-for="order in paginatedOrders"
|
|
|
|
|
+ :key="order.orderId"
|
|
|
|
|
+ class="mb-2 cursor-pointer rounded-[14px] border border-transparent bg-[#fafbfc] p-3"
|
|
|
|
|
+ :class="{ 'border-[#409eff] bg-[#ecf5ff]': selectedOrder?.orderId === order.orderId }"
|
|
|
|
|
+ @click="selectOrder(order)"
|
|
|
>
|
|
>
|
|
|
- <div class="order-header">
|
|
|
|
|
- <span class="order-code">{{ order.code }}</span>
|
|
|
|
|
- <el-tag :type="getStatusType(order.status)" size="small">{{ getStatusText(order.status) }}</el-tag>
|
|
|
|
|
|
|
+ <div class="mb-1.5 flex justify-between">
|
|
|
|
|
+ <span class="font-600">{{ order.orderNo }}</span>
|
|
|
|
|
+ <el-tag :type="order.orderStatusTag" size="small">{{ order.orderStatusText }}</el-tag>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="order-case">关联案件: {{ order.caseCode }}</div>
|
|
|
|
|
- <div class="order-location">{{ order.address }}</div>
|
|
|
|
|
- <div class="order-time">创建: {{ order.createTime }}</div>
|
|
|
|
|
|
|
+ <div class="text-[12px] text-[#666]">关联案件: {{ order.caseNo || '—' }}</div>
|
|
|
|
|
+ <div class="my-1 text-[12px] text-[#666]">设备编号: {{ order.deviceCode || '—' }}</div>
|
|
|
|
|
+ <div class="text-[12px] text-[#666]">创建: {{ order.createTimeText }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-empty v-if="!paginatedOrders.length" description="暂无工单数据" :image-size="80" />
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="pagination-area">
|
|
|
|
|
|
|
+ <div class="border-t border-[#eee] p-3 text-center">
|
|
|
<el-pagination small layout="prev, pager, next" :total="filteredOrders.length" :page-size="pageSize" v-model:current-page="currentPage" />
|
|
<el-pagination small layout="prev, pager, next" :total="filteredOrders.length" :page-size="pageSize" v-model:current-page="currentPage" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 右侧工单详情与操作区 -->
|
|
|
|
|
- <div class="detail-panel" v-if="selectedOrder">
|
|
|
|
|
|
|
+ <div class="flex-1 overflow-y-auto rounded-[20px] bg-white p-5" v-if="selectedOrder" v-loading="detailLoading">
|
|
|
<el-tabs v-model="activeTab">
|
|
<el-tabs v-model="activeTab">
|
|
|
- <!-- 工单基本信息与案件信息 -->
|
|
|
|
|
<el-tab-pane label="工单信息" name="info">
|
|
<el-tab-pane label="工单信息" name="info">
|
|
|
- <div class="info-section">
|
|
|
|
|
- <div class="section-title">
|
|
|
|
|
- <span><el-icon><Document /></el-icon> 工单基本信息</span>
|
|
|
|
|
- <div class="assign-area">
|
|
|
|
|
- <el-select v-model="selectedWorker" placeholder="选择维护人员" size="small" style="width: 160px" clearable>
|
|
|
|
|
- <el-option v-for="worker in workers" :key="worker.id" :label="worker.name" :value="worker.id" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- <el-button type="primary" size="small" @click="assignWorker">下派任务</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-descriptions :column="2" border>
|
|
|
|
|
- <el-descriptions-item label="工单编号">{{ selectedOrder.code }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="工单状态">{{ getStatusText(selectedOrder.status) }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="关联案件编号">
|
|
|
|
|
- <el-link type="primary" @click="viewCase">{{ selectedOrder.caseCode }}</el-link>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="报警级别">
|
|
|
|
|
- <el-tag :type="selectedOrder.alertLevel === 'critical' ? 'danger' : 'warning'" size="small">
|
|
|
|
|
- {{ selectedOrder.alertLevel === 'critical' ? '严重' : '一般' }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="报警类型">{{ selectedOrder.alertType }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="权属单位">{{ selectedOrder.ownerUnit }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="报警描述" :span="2">{{ selectedOrder.alertDesc }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="详细地址" :span="2">{{ selectedOrder.address }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="用途">{{ selectedOrder.purpose || '市政排水' }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="指派人">{{ selectedOrder.assignee || '未指派' }}</el-descriptions-item>
|
|
|
|
|
- </el-descriptions>
|
|
|
|
|
|
|
+ <div class="mb-3 mt-4 flex items-center gap-2 text-[16px] font-600">
|
|
|
|
|
+ <span><el-icon><Document /></el-icon> 工单基本信息</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
|
|
+ <el-descriptions-item label="工单ID">{{ selectedOrder.orderId || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="工单编号">{{ selectedOrder.orderNo }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="报警ID">{{ selectedOrder.alarmId || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="工单状态">
|
|
|
|
|
+ <el-tag :type="selectedOrder.orderStatusTag">{{ selectedOrder.orderStatusText }}</el-tag>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="案件ID">{{ selectedOrder.caseId || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="关联案件编号">{{ selectedOrder.caseNo || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="设备ID">{{ selectedOrder.deviceId || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="优先级">
|
|
|
|
|
+ <el-tag :type="selectedOrder.orderLevelTag">{{ selectedOrder.orderLevelText }}</el-tag>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="工单类型">{{ selectedOrder.orderTypeText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="问题描述" :span="2">{{ selectedOrder.orderDesc || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="负责部门ID">{{ selectedOrder.deptId || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="设备类别">{{ selectedOrder.equipmentTypeName || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="接单人ID">{{ selectedOrder.receiveUser || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="派单人ID">{{ selectedOrder.dispatchUser || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="派单时间">{{ selectedOrder.dispatchTimeText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="创建时间">{{ selectedOrder.createTimeText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="计划完成时间">{{ selectedOrder.planFinishTimeText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="延期原因">{{ selectedOrder.delayedReason || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="延期截止时间">{{ selectedOrder.delayedDeadlineText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="验收人ID">{{ selectedOrder.verifyUser || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="验收时间">{{ selectedOrder.verifyTimeText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="实际完成时间">{{ selectedOrder.realFinishTimeText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="运维状态">{{ selectedOrder.currentStatusText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="在线状态">{{ selectedOrder.onlineStatusText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="报警状态">{{ selectedOrder.alarmStatusText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="状态更新时间">{{ selectedOrder.statusUpdateTimeText }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '—' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="更新时间">{{ selectedOrder.updateTimeText }}</el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
|
|
|
- <!-- 延期/缓办操作 -->
|
|
|
|
|
- <div class="delay-section">
|
|
|
|
|
- <div class="section-title"><el-icon><Timer /></el-icon> 工单延期/缓办</div>
|
|
|
|
|
- <div class="delay-buttons">
|
|
|
|
|
- <el-button size="small" type="warning" @click="openDelayDialog('延期')">申请延期</el-button>
|
|
|
|
|
- <el-button size="small" type="info" @click="openDelayDialog('缓办')">申请缓办</el-button>
|
|
|
|
|
|
|
+ <div class="mt-5 border-t border-[#e9ecef] pt-3">
|
|
|
|
|
+ <div class="mb-3 mt-4 flex items-center gap-2 text-[16px] font-600">
|
|
|
|
|
+ <span><el-icon><Timer /></el-icon> 工单操作</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-if="selectedOrder.delayRecords?.length" class="delay-history">
|
|
|
|
|
- <div v-for="(rec, idx) in selectedOrder.delayRecords" :key="idx" class="delay-item">
|
|
|
|
|
- {{ rec.type }} 至 {{ rec.deadline }},理由: {{ rec.reason }}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-tab-pane>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 处置反馈与审批 -->
|
|
|
|
|
- <el-tab-pane label="处置反馈与审批" name="feedback">
|
|
|
|
|
- <div class="feedback-section">
|
|
|
|
|
- <div class="section-title"><el-icon><Picture /></el-icon> 现场处置反馈(负责人上报)</div>
|
|
|
|
|
- <div v-if="selectedOrder.feedback">
|
|
|
|
|
- <el-descriptions :column="1" border>
|
|
|
|
|
- <el-descriptions-item label="处置人员">{{ selectedOrder.feedback.workerName }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="处置时间">{{ selectedOrder.feedback.feedbackTime }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="处置说明">{{ selectedOrder.feedback.description }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="现场照片">
|
|
|
|
|
- <div class="feedback-images">
|
|
|
|
|
- <el-image v-for="(img, idx) in selectedOrder.feedback.images" :key="idx" :src="img" fit="cover" class="feedback-img" preview-teleported />
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- </el-descriptions>
|
|
|
|
|
- <!-- 周边视频监控 -->
|
|
|
|
|
- <div class="video-section" v-if="selectedOrder.surroundVideo">
|
|
|
|
|
- <div class="sub-title"><el-icon><VideoCamera /></el-icon> 井盖周边视频监控</div>
|
|
|
|
|
- <div class="video-preview" @click="playVideo">点击查看周边监控录像</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="mb-3 flex items-center gap-3" v-if="canDispatch(selectedOrder)">
|
|
|
|
|
+ <el-input v-model="receiveUserDisplay" readonly placeholder="请选择接单人" class="w-[260px]" />
|
|
|
|
|
+ <el-button @click="openDispatchDialog">选择人员</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="dispatchOrder">下派任务</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-else class="no-feedback">暂无处置反馈信息,等待负责人上报</div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 审批操作栏 -->
|
|
|
|
|
- <div class="approval-section" v-if="selectedOrder.status === 'feedback'">
|
|
|
|
|
- <div class="section-title"><el-icon><Checked /></el-icon> 工单审批</div>
|
|
|
|
|
- <div class="approval-buttons">
|
|
|
|
|
- <el-button type="success" @click="approveOrder(true)">审批通过</el-button>
|
|
|
|
|
- <el-button type="danger" @click="approveOrder(false)">驳回重办</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-input type="textarea" v-model="approvalRemark" placeholder="审批意见(可选)" rows="2" style="margin-top: 12px" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div v-else-if="selectedOrder.status === 'completed'" class="approved-info">
|
|
|
|
|
- <el-alert title="工单已完成审批" type="success" :closable="false" show-icon />
|
|
|
|
|
|
|
+ <div class="flex flex-wrap gap-3">
|
|
|
|
|
+ <el-button v-if="canStartRepair(selectedOrder)" type="primary" @click="startRepair">开始维修</el-button>
|
|
|
|
|
+ <el-button v-if="canSubmitVerify(selectedOrder)" type="warning" @click="submitVerify">提交验收</el-button>
|
|
|
|
|
+ <el-button v-if="canApprove(selectedOrder)" type="success" @click="approveOrder(true)">验收通过</el-button>
|
|
|
|
|
+ <el-button v-if="canApprove(selectedOrder)" type="danger" @click="approveOrder(false)">驳回工单</el-button>
|
|
|
|
|
+ <el-button v-if="canApproveDelay(selectedOrder)" type="success" @click="approveDelay">延期审核通过</el-button>
|
|
|
|
|
+ <el-button v-if="canRejectDelay(selectedOrder)" type="danger" @click="rejectDelay">驳回延期</el-button>
|
|
|
|
|
+ <el-button v-if="canDelay(selectedOrder)" type="info" plain @click="openDelayDialog">申请延期</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <el-empty v-if="!hasOperationButtons(selectedOrder)" description="当前状态暂无可操作项" :image-size="80" />
|
|
|
</div>
|
|
</div>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
- <!-- 工单处理历史 -->
|
|
|
|
|
<el-tab-pane label="处理历史" name="history">
|
|
<el-tab-pane label="处理历史" name="history">
|
|
|
- <el-timeline>
|
|
|
|
|
- <el-timeline-item v-for="log in selectedOrder.processLogs" :key="log.id" :timestamp="log.time" :type="log.type">
|
|
|
|
|
- {{ log.content }}
|
|
|
|
|
- <span v-if="log.operator" class="operator">操作人: {{ log.operator }}</span>
|
|
|
|
|
|
|
+ <el-timeline v-if="selectedOrder.workOrderLogList?.length">
|
|
|
|
|
+ <el-timeline-item
|
|
|
|
|
+ v-for="log in selectedOrder.workOrderLogList"
|
|
|
|
|
+ :key="log.logId"
|
|
|
|
|
+ :timestamp="formatDateTime(log.operTime)"
|
|
|
|
|
+ :type="getLogType(log.operType)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ log.operDesc }}
|
|
|
|
|
+ <span v-if="log.operUserName" class="ml-3 text-[12px] text-[#909399]">操作人: {{ log.operUserName }}</span>
|
|
|
</el-timeline-item>
|
|
</el-timeline-item>
|
|
|
</el-timeline>
|
|
</el-timeline>
|
|
|
|
|
+ <el-empty v-else description="暂无工单日志" :image-size="80" />
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
</el-tabs>
|
|
</el-tabs>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 未选中状态 -->
|
|
|
|
|
- <div v-else class="empty-panel">
|
|
|
|
|
|
|
+ <div v-else class="flex flex-1 items-center justify-center rounded-[20px] bg-white">
|
|
|
<el-empty description="请从左侧选择工单进行处理" :image-size="140" />
|
|
<el-empty description="请从左侧选择工单进行处理" :image-size="140" />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 延期/缓办对话框 -->
|
|
|
|
|
- <el-dialog v-model="delayDialogVisible" :title="delayType + '申请'" width="450px">
|
|
|
|
|
- <el-form>
|
|
|
|
|
- <el-form-item :label="delayType === '延期' ? '延期截止时间' : '缓办截止时间'">
|
|
|
|
|
- <el-date-picker v-model="delayDeadline" type="datetime" placeholder="选择截止时间" format="YYYY-MM-DD HH:mm" />
|
|
|
|
|
|
|
+ <el-dialog v-model="delayDialogVisible" title="申请延期" width="450px">
|
|
|
|
|
+ <el-form label-width="88px">
|
|
|
|
|
+ <el-form-item label="截止时间">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="delayDeadline"
|
|
|
|
|
+ type="datetime"
|
|
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
+ placeholder="选择截止时间"
|
|
|
|
|
+ class="w-full"
|
|
|
|
|
+ />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item label="申请理由">
|
|
<el-form-item label="申请理由">
|
|
|
- <el-input type="textarea" v-model="delayReason" placeholder="请详细说明延期或缓办理由" rows="3" />
|
|
|
|
|
|
|
+ <el-input type="textarea" v-model="delayReason" placeholder="请详细说明延期理由" rows="3" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
<template #footer>
|
|
<template #footer>
|
|
@@ -182,286 +177,491 @@
|
|
|
<el-button type="primary" @click="submitDelay">提交申请</el-button>
|
|
<el-button type="primary" @click="submitDelay">提交申请</el-button>
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <el-dialog v-model="dispatchDialogVisible" title="下派任务" width="860px">
|
|
|
|
|
+ <div class="flex gap-4">
|
|
|
|
|
+ <div class="w-[260px] overflow-auto rounded-lg border border-[#ebeef5] p-3">
|
|
|
|
|
+ <div class="mb-3 font-600">部门树</div>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="deptKeyword"
|
|
|
|
|
+ placeholder="筛选部门"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="mb-3"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-tree
|
|
|
|
|
+ ref="deptTreeRef"
|
|
|
|
|
+ :data="deptOptions"
|
|
|
|
|
+ node-key="id"
|
|
|
|
|
+ :props="{ label: 'label', children: 'children' }"
|
|
|
|
|
+ default-expand-all
|
|
|
|
|
+ highlight-current
|
|
|
|
|
+ :filter-node-method="filterDeptNode"
|
|
|
|
|
+ @node-click="handleDeptClick"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex-1 rounded-lg border border-[#ebeef5] p-3">
|
|
|
|
|
+ <div class="mb-3 font-600">用户列表</div>
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ :data="dispatchUsers"
|
|
|
|
|
+ height="360"
|
|
|
|
|
+ border
|
|
|
|
|
+ highlight-current-row
|
|
|
|
|
+ v-loading="dispatchUserLoading"
|
|
|
|
|
+ @current-change="handleUserCurrentChange"
|
|
|
|
|
+ @row-click="handleUserCurrentChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column prop="userId" label="ID" width="90" />
|
|
|
|
|
+ <el-table-column prop="userName" label="用户名" min-width="120" />
|
|
|
|
|
+ <el-table-column prop="nickName" label="昵称" min-width="120" />
|
|
|
|
|
+ <el-table-column prop="dept.deptName" label="部门" min-width="140" show-overflow-tooltip />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <div class="mt-3 text-[#606266]">
|
|
|
|
|
+ 已选人员:{{ dispatchSelectedUser ? `${dispatchSelectedUser.userName} / ID: ${dispatchSelectedUser.userId}` : '未选择' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="dispatchDialogVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="confirmDispatchUser">确认选择</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, computed } from 'vue'
|
|
|
|
|
-import { Tickets, Search, Document, Timer, Picture, VideoCamera, Checked } from '@element-plus/icons-vue'
|
|
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
|
|
-
|
|
|
|
|
-// 模拟维护人员列表
|
|
|
|
|
-const workers = ref([
|
|
|
|
|
- { id: 'w1', name: '张建国 - 维修一组' },
|
|
|
|
|
- { id: 'w2', name: '李志强 - 维修二组' },
|
|
|
|
|
- { id: 'w3', name: '王大明 - 应急抢修队' },
|
|
|
|
|
- { id: 'w4', name: '陈辉 - 巡检专员' }
|
|
|
|
|
-])
|
|
|
|
|
-
|
|
|
|
|
-// 模拟工单数据
|
|
|
|
|
-const mockOrders = ref([
|
|
|
|
|
- {
|
|
|
|
|
- id: 1, code: 'WO-20250402-001', caseCode: 'CASE-20250402-001', alertLevel: 'critical', alertType: '倾斜超标',
|
|
|
|
|
- alertDesc: '井盖倾斜角度14.2°,超过阈值8°,有跌落风险', ownerUnit: '市政工程管理处', address: '人民路与解放路口东50m',
|
|
|
|
|
- purpose: '主干道排水', status: 'pending', createTime: '2025-04-02 14:25:00', assignee: null,
|
|
|
|
|
- processLogs: [{ id: 1, time: '2025-04-02 14:25:00', content: '工单自动创建', type: 'info', operator: '系统' }],
|
|
|
|
|
- delayRecords: [], feedback: null
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2, code: 'WO-20250402-002', caseCode: 'CASE-20250402-002', alertLevel: 'critical', alertType: '水位超限',
|
|
|
|
|
- alertDesc: '井下水位62cm,超过阈值40cm,溢水风险', ownerUnit: '水务集团', address: '滨江路化工园区南门',
|
|
|
|
|
- purpose: '化工区雨水排放', status: 'processing', createTime: '2025-04-02 14:00:00', assignee: '张建国',
|
|
|
|
|
- processLogs: [
|
|
|
|
|
- { id: 1, time: '2025-04-02 14:00:00', content: '工单自动创建', type: 'info', operator: '系统' },
|
|
|
|
|
- { id: 2, time: '2025-04-02 14:30:00', content: '已指派张建国前往处置', type: 'primary', operator: '调度中心' }
|
|
|
|
|
- ],
|
|
|
|
|
- delayRecords: [],
|
|
|
|
|
- feedback: null
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 3, code: 'WO-20250402-003', caseCode: 'CASE-20250402-003', alertLevel: 'warning', alertType: '异常震动',
|
|
|
|
|
- alertDesc: '异常震动112mg,超过阈值80mg,疑似外力冲击', ownerUnit: '市政设施管理处', address: '古城街北段树下',
|
|
|
|
|
- purpose: '老城区排水', status: 'feedback', createTime: '2025-04-02 12:15:00', assignee: '李志强',
|
|
|
|
|
- surroundVideo: true,
|
|
|
|
|
- feedback: {
|
|
|
|
|
- workerName: '李志强', feedbackTime: '2025-04-02 15:20:00', description: '现场井盖因车辆碾轧移位,已复位加固,设备恢复正常',
|
|
|
|
|
- images: ['https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg']
|
|
|
|
|
- },
|
|
|
|
|
- processLogs: [
|
|
|
|
|
- { id: 1, time: '2025-04-02 12:15:00', content: '工单自动创建', type: 'info', operator: '系统' },
|
|
|
|
|
- { id: 2, time: '2025-04-02 13:00:00', content: '指派李志强现场处置', type: 'primary', operator: '调度中心' },
|
|
|
|
|
- { id: 3, time: '2025-04-02 15:20:00', content: '负责人提交处置反馈,待审批', type: 'warning', operator: '李志强' }
|
|
|
|
|
- ],
|
|
|
|
|
- delayRecords: []
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 4, code: 'WO-20250402-004', caseCode: 'CASE-20250402-004', alertLevel: 'warning', alertType: '井盖开启异常',
|
|
|
|
|
- alertDesc: '井盖非正常开启', ownerUnit: '市政工程管理处', address: '南港路旧车站段',
|
|
|
|
|
- purpose: '市政井盖', status: 'completed', createTime: '2025-04-02 11:50:00', assignee: '王大明',
|
|
|
|
|
- feedback: {
|
|
|
|
|
- workerName: '王大明', feedbackTime: '2025-04-02 14:10:00', description: '经核实为附近施工人员误开,已关闭并加锁',
|
|
|
|
|
- images: ['https://fuss10.elemecdn.com/3/5e/4bbf4c1b1b4f1e1c0b4b4c1b1b4f1e1c.jpeg']
|
|
|
|
|
- },
|
|
|
|
|
- processLogs: [
|
|
|
|
|
- { id: 1, time: '2025-04-02 11:50:00', content: '工单创建', type: 'info', operator: '系统' },
|
|
|
|
|
- { id: 2, time: '2025-04-02 13:00:00', content: '指派王大明处置', type: 'primary', operator: '调度中心' },
|
|
|
|
|
- { id: 3, time: '2025-04-02 14:10:00', content: '提交反馈并审批通过', type: 'success', operator: '审批人' }
|
|
|
|
|
- ],
|
|
|
|
|
- delayRecords: [],
|
|
|
|
|
- approved: true
|
|
|
|
|
- }
|
|
|
|
|
-])
|
|
|
|
|
|
|
+import {computed, nextTick, onMounted, ref, watch} from 'vue'
|
|
|
|
|
+import {Document, Search, Tickets, Timer} from '@element-plus/icons-vue'
|
|
|
|
|
+import {ElMessage} from 'element-plus'
|
|
|
|
|
+import {getManholeWorkOrderDetail, getManholeWorkOrderPage, operManholeWorkOrder} from '@/api/pipeNetwork/basic'
|
|
|
|
|
+import {deptTreeSelect, listUser} from '@/api/system/user'
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_OPER_USER_ID = 1
|
|
|
|
|
|
|
|
|
|
+const orders = ref([])
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const detailLoading = ref(false)
|
|
|
const searchKeyword = ref('')
|
|
const searchKeyword = ref('')
|
|
|
const statusFilter = ref('')
|
|
const statusFilter = ref('')
|
|
|
const currentPage = ref(1)
|
|
const currentPage = ref(1)
|
|
|
const pageSize = 5
|
|
const pageSize = 5
|
|
|
-const selectedOrder = ref(mockOrders.value[0])
|
|
|
|
|
-const selectedWorker = ref('')
|
|
|
|
|
|
|
+const selectedOrder = ref(null)
|
|
|
const activeTab = ref('info')
|
|
const activeTab = ref('info')
|
|
|
|
|
+const receiveUserId = ref(null)
|
|
|
|
|
+const receiveUserDisplay = ref('')
|
|
|
const delayDialogVisible = ref(false)
|
|
const delayDialogVisible = ref(false)
|
|
|
-const delayType = ref('延期')
|
|
|
|
|
const delayDeadline = ref('')
|
|
const delayDeadline = ref('')
|
|
|
const delayReason = ref('')
|
|
const delayReason = ref('')
|
|
|
-const approvalRemark = ref('')
|
|
|
|
|
|
|
+const dispatchDialogVisible = ref(false)
|
|
|
|
|
+const dispatchUserLoading = ref(false)
|
|
|
|
|
+const deptOptions = ref([])
|
|
|
|
|
+const deptKeyword = ref('')
|
|
|
|
|
+const selectedDeptId = ref(undefined)
|
|
|
|
|
+const dispatchUsers = ref([])
|
|
|
|
|
+const dispatchSelectedUser = ref(null)
|
|
|
|
|
+const deptTreeRef = ref(null)
|
|
|
|
|
|
|
|
-// 过滤与分页
|
|
|
|
|
-const filteredOrders = computed(() => {
|
|
|
|
|
- let list = mockOrders.value
|
|
|
|
|
- if (statusFilter.value) list = list.filter(o => o.status === statusFilter.value)
|
|
|
|
|
- if (searchKeyword.value) {
|
|
|
|
|
- const kw = searchKeyword.value.toLowerCase()
|
|
|
|
|
- list = list.filter(o => o.code.toLowerCase().includes(kw) || o.caseCode.toLowerCase().includes(kw))
|
|
|
|
|
|
|
+function formatDateTime(value) {
|
|
|
|
|
+ if (!value) return '—'
|
|
|
|
|
+ return String(value).replace('T', ' ')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getOrderLevelMeta(level) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ 1: { text: '紧急', tag: 'danger' },
|
|
|
|
|
+ 2: { text: '一般', tag: 'warning' },
|
|
|
|
|
+ 3: { text: '低', tag: 'info' }
|
|
|
}
|
|
}
|
|
|
- return list
|
|
|
|
|
|
|
+ return map[Number(level)] || { text: '—', tag: 'info' }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getOrderTypeText(type) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ 1: '故障维修',
|
|
|
|
|
+ 2: '日常巡检',
|
|
|
|
|
+ 3: '设备保养'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[Number(type)] || '—'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getOrderStatusMeta(status) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ 1: { key: 'pending', text: '待派单', tag: 'danger' },
|
|
|
|
|
+ 2: { key: 'pending', text: '待接单', tag: 'danger' },
|
|
|
|
|
+ 3: { key: 'processing', text: '已接单', tag: 'warning' },
|
|
|
|
|
+ 4: { key: 'processing', text: '处理中', tag: 'warning' },
|
|
|
|
|
+ 5: { key: 'verify', text: '待验收', tag: 'primary' },
|
|
|
|
|
+ 6: { key: 'completed', text: '已结案', tag: 'success' },
|
|
|
|
|
+ 7: { key: 'completed', text: '已驳回', tag: 'danger' },
|
|
|
|
|
+ 8: { key: 'processing', text: '延期审核', tag: 'warning' },
|
|
|
|
|
+ 9: { key: 'completed', text: '已延期', tag: 'warning' }
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[Number(status)] || { key: '', text: '—', tag: 'info' }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getCurrentStatusText(status) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ 1: '在用',
|
|
|
|
|
+ 2: '闲置',
|
|
|
|
|
+ 3: '维修',
|
|
|
|
|
+ 4: '报废',
|
|
|
|
|
+ 5: '待入库'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[Number(status)] || '—'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getOnlineStatusText(status) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ 0: '离线',
|
|
|
|
|
+ 1: '在线'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[Number(status)] || '—'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getAlarmStatusText(status) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ 0: '正常',
|
|
|
|
|
+ 1: '报警'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[Number(status)] || '—'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function normalizeOrder(item) {
|
|
|
|
|
+ const levelMeta = getOrderLevelMeta(item.orderLevel)
|
|
|
|
|
+ const statusMeta = getOrderStatusMeta(item.orderStatus)
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ orderLevelText: levelMeta.text,
|
|
|
|
|
+ orderLevelTag: levelMeta.tag,
|
|
|
|
|
+ orderTypeText: getOrderTypeText(item.orderType),
|
|
|
|
|
+ orderStatusKey: statusMeta.key,
|
|
|
|
|
+ orderStatusText: statusMeta.text,
|
|
|
|
|
+ orderStatusTag: statusMeta.tag,
|
|
|
|
|
+ createTimeText: formatDateTime(item.createTime),
|
|
|
|
|
+ dispatchTimeText: formatDateTime(item.dispatchTime),
|
|
|
|
|
+ planFinishTimeText: formatDateTime(item.planFinishTime),
|
|
|
|
|
+ delayedDeadlineText: formatDateTime(item.delayedDeadline),
|
|
|
|
|
+ realFinishTimeText: formatDateTime(item.realFinishTime),
|
|
|
|
|
+ verifyTimeText: formatDateTime(item.verifyTime),
|
|
|
|
|
+ updateTimeText: formatDateTime(item.updateTime),
|
|
|
|
|
+ currentStatusText: getCurrentStatusText(item.equipmentStatus?.currentStatus),
|
|
|
|
|
+ onlineStatusText: getOnlineStatusText(item.equipmentStatus?.onlineStatus),
|
|
|
|
|
+ alarmStatusText: getAlarmStatusText(item.equipmentStatus?.alarmStatus),
|
|
|
|
|
+ statusUpdateTimeText: formatDateTime(item.equipmentStatus?.statusUpdateTime)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const filteredOrders = computed(() => {
|
|
|
|
|
+ if (!statusFilter.value) return orders.value
|
|
|
|
|
+ return orders.value.filter(item => item.orderStatusKey === statusFilter.value)
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
const paginatedOrders = computed(() => {
|
|
const paginatedOrders = computed(() => {
|
|
|
const start = (currentPage.value - 1) * pageSize
|
|
const start = (currentPage.value - 1) * pageSize
|
|
|
return filteredOrders.value.slice(start, start + pageSize)
|
|
return filteredOrders.value.slice(start, start + pageSize)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const totalOrders = computed(() => mockOrders.value.length)
|
|
|
|
|
-const pendingCount = computed(() => mockOrders.value.filter(o => o.status === 'pending').length)
|
|
|
|
|
-const processingCount = computed(() => mockOrders.value.filter(o => o.status === 'processing').length)
|
|
|
|
|
-const feedbackCount = computed(() => mockOrders.value.filter(o => o.status === 'feedback').length)
|
|
|
|
|
-const completedCount = computed(() => mockOrders.value.filter(o => o.status === 'completed').length)
|
|
|
|
|
-
|
|
|
|
|
-const filterStatus = (status) => { statusFilter.value = status; currentPage.value = 1 }
|
|
|
|
|
-const selectOrder = (order) => { selectedOrder.value = order; selectedWorker.value = order.assignee ? workers.value.find(w => w.name === order.assignee)?.id || '' : '' }
|
|
|
|
|
-
|
|
|
|
|
-const getStatusType = (status) => {
|
|
|
|
|
- if (status === 'pending') return 'danger'
|
|
|
|
|
- if (status === 'processing') return 'warning'
|
|
|
|
|
- if (status === 'feedback') return 'info'
|
|
|
|
|
- return 'success'
|
|
|
|
|
-}
|
|
|
|
|
-const getStatusText = (status) => {
|
|
|
|
|
- const map = { pending: '待处理', processing: '处理中', feedback: '待审批', completed: '已完成' }
|
|
|
|
|
- return map[status] || status
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 下派任务
|
|
|
|
|
-const assignWorker = () => {
|
|
|
|
|
- if (!selectedWorker.value) { ElMessage.warning('请选择维护人员'); return }
|
|
|
|
|
- const worker = workers.value.find(w => w.id === selectedWorker.value)
|
|
|
|
|
- if (selectedOrder.value.status === 'pending') {
|
|
|
|
|
- selectedOrder.value.status = 'processing'
|
|
|
|
|
- selectedOrder.value.assignee = worker.name
|
|
|
|
|
- selectedOrder.value.processLogs.push({
|
|
|
|
|
- id: Date.now(),
|
|
|
|
|
- time: new Date().toLocaleString(),
|
|
|
|
|
- content: `已下派任务给 ${worker.name}`,
|
|
|
|
|
- type: 'primary',
|
|
|
|
|
- operator: '平台调度员'
|
|
|
|
|
|
|
+const totalOrders = computed(() => orders.value.length)
|
|
|
|
|
+const pendingCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'pending').length)
|
|
|
|
|
+const processingCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'processing').length)
|
|
|
|
|
+const verifyCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'verify').length)
|
|
|
|
|
+const completedCount = computed(() => orders.value.filter(item => item.orderStatusKey === 'completed').length)
|
|
|
|
|
+
|
|
|
|
|
+async function loadOrders() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const keyword = searchKeyword.value.trim()
|
|
|
|
|
+ const res = await getManholeWorkOrderPage({
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 200,
|
|
|
|
|
+ orderNo: keyword || undefined,
|
|
|
|
|
+ caseNo: keyword || undefined
|
|
|
})
|
|
})
|
|
|
- ElMessage.success(`工单已下派至 ${worker.name}`)
|
|
|
|
|
- } else {
|
|
|
|
|
- ElMessage.warning('当前工单状态不可下派')
|
|
|
|
|
|
|
+ const records = Array.isArray(res?.data?.records) ? res.data.records : []
|
|
|
|
|
+ orders.value = records.map(normalizeOrder)
|
|
|
|
|
+ currentPage.value = 1
|
|
|
|
|
+ const first = filteredOrders.value[0]
|
|
|
|
|
+ if (!first) {
|
|
|
|
|
+ selectedOrder.value = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (selectedOrder.value) {
|
|
|
|
|
+ const matched = orders.value.find(item => item.orderId === selectedOrder.value.orderId)
|
|
|
|
|
+ if (matched) {
|
|
|
|
|
+ await selectOrder(matched)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ await selectOrder(first)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ orders.value = []
|
|
|
|
|
+ selectedOrder.value = null
|
|
|
|
|
+ ElMessage.error('工单列表加载失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 延期/缓办
|
|
|
|
|
-const openDelayDialog = (type) => {
|
|
|
|
|
- delayType.value = type
|
|
|
|
|
- delayDialogVisible.value = true
|
|
|
|
|
|
|
+async function selectOrder(order) {
|
|
|
|
|
+ detailLoading.value = true
|
|
|
|
|
+ receiveUserId.value = order.receiveUser || null
|
|
|
|
|
+ receiveUserDisplay.value = order.receiveUser ? `用户ID: ${order.receiveUser}` : ''
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getManholeWorkOrderDetail(order.orderId)
|
|
|
|
|
+ selectedOrder.value = normalizeOrder(res?.data || order)
|
|
|
|
|
+ receiveUserId.value = selectedOrder.value.receiveUser || null
|
|
|
|
|
+ receiveUserDisplay.value = selectedOrder.value.receiveUser ? `用户ID: ${selectedOrder.value.receiveUser}` : ''
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ selectedOrder.value = order
|
|
|
|
|
+ ElMessage.error('工单详情加载失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ detailLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function filterStatus(status) {
|
|
|
|
|
+ statusFilter.value = status
|
|
|
|
|
+ currentPage.value = 1
|
|
|
|
|
+ const first = filteredOrders.value[0]
|
|
|
|
|
+ if (first) {
|
|
|
|
|
+ selectOrder(first)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function canDispatch(order) {
|
|
|
|
|
+ return Number(order.orderStatus) === 1
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function canStartRepair(order) {
|
|
|
|
|
+ return [2, 3].includes(Number(order.orderStatus))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function canSubmitVerify(order) {
|
|
|
|
|
+ return Number(order.orderStatus) === 4
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function canApprove(order) {
|
|
|
|
|
+ return Number(order.orderStatus) === 5
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function canApproveDelay(order) {
|
|
|
|
|
+ return Number(order.orderStatus) === 8
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function canRejectDelay(order) {
|
|
|
|
|
+ return Number(order.orderStatus) === 8
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function canDelay(order) {
|
|
|
|
|
+ return [2, 3, 4].includes(Number(order.orderStatus))
|
|
|
}
|
|
}
|
|
|
-const submitDelay = () => {
|
|
|
|
|
- if (!delayDeadline.value) { ElMessage.warning('请选择截止时间'); return }
|
|
|
|
|
- if (!delayReason.value) { ElMessage.warning('请填写申请理由'); return }
|
|
|
|
|
- const record = {
|
|
|
|
|
- type: delayType.value,
|
|
|
|
|
- deadline: new Date(delayDeadline.value).toLocaleString(),
|
|
|
|
|
- reason: delayReason.value,
|
|
|
|
|
- applyTime: new Date().toLocaleString()
|
|
|
|
|
|
|
+
|
|
|
|
|
+function hasOperationButtons(order) {
|
|
|
|
|
+ if (!order) return false
|
|
|
|
|
+ return (
|
|
|
|
|
+ canDispatch(order) ||
|
|
|
|
|
+ canStartRepair(order) ||
|
|
|
|
|
+ canSubmitVerify(order) ||
|
|
|
|
|
+ canApprove(order) ||
|
|
|
|
|
+ canApproveDelay(order) ||
|
|
|
|
|
+ canRejectDelay(order) ||
|
|
|
|
|
+ canDelay(order)
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function operateCurrent(operType, operDesc, extra = {}) {
|
|
|
|
|
+ if (!selectedOrder.value?.orderId) return
|
|
|
|
|
+ await operManholeWorkOrder({
|
|
|
|
|
+ orderId: selectedOrder.value.orderId,
|
|
|
|
|
+ operType,
|
|
|
|
|
+ operDesc,
|
|
|
|
|
+ operUserId: DEFAULT_OPER_USER_ID,
|
|
|
|
|
+ ...extra
|
|
|
|
|
+ })
|
|
|
|
|
+ await loadOrders()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function dispatchOrder() {
|
|
|
|
|
+ if (!receiveUserId.value) {
|
|
|
|
|
+ ElMessage.warning('请先选择接单人')
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- if (!selectedOrder.value.delayRecords) selectedOrder.value.delayRecords = []
|
|
|
|
|
- selectedOrder.value.delayRecords.push(record)
|
|
|
|
|
- selectedOrder.value.processLogs.push({
|
|
|
|
|
- id: Date.now(),
|
|
|
|
|
- time: new Date().toLocaleString(),
|
|
|
|
|
- content: `申请${delayType.value},截止时间:${record.deadline},理由:${delayReason.value}`,
|
|
|
|
|
- type: 'warning',
|
|
|
|
|
- operator: '工单负责人'
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operateCurrent(2, `下派工单至用户${receiveUserId.value}`, { receiveUser: Number(receiveUserId.value) })
|
|
|
|
|
+ ElMessage.success('工单下派成功')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error?.message || '工单下派失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function filterDeptNode(value, data) {
|
|
|
|
|
+ if (!value) return true
|
|
|
|
|
+ return data.label?.includes(value)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watch(deptKeyword, value => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ deptTreeRef.value?.filter(value)
|
|
|
})
|
|
})
|
|
|
- ElMessage.success(`${delayType.value}申请已提交,待审批`)
|
|
|
|
|
- delayDialogVisible.value = false
|
|
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+async function loadDeptTree() {
|
|
|
|
|
+ const res = await deptTreeSelect()
|
|
|
|
|
+ deptOptions.value = Array.isArray(res?.data) ? res.data : []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadDispatchUsers(deptId) {
|
|
|
|
|
+ dispatchUserLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await listUser({
|
|
|
|
|
+ pageNum: 1,
|
|
|
|
|
+ pageSize: 100,
|
|
|
|
|
+ deptId,
|
|
|
|
|
+ status: '0'
|
|
|
|
|
+ })
|
|
|
|
|
+ dispatchUsers.value = Array.isArray(res?.rows) ? res.rows : []
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ dispatchUsers.value = []
|
|
|
|
|
+ ElMessage.error('用户列表加载失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ dispatchUserLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function openDispatchDialog() {
|
|
|
|
|
+ dispatchDialogVisible.value = true
|
|
|
|
|
+ dispatchSelectedUser.value = null
|
|
|
|
|
+ dispatchUsers.value = []
|
|
|
|
|
+ deptKeyword.value = ''
|
|
|
|
|
+ if (!deptOptions.value.length) {
|
|
|
|
|
+ await loadDeptTree()
|
|
|
|
|
+ }
|
|
|
|
|
+ const firstDept = findFirstDept(deptOptions.value)
|
|
|
|
|
+ if (firstDept?.id) {
|
|
|
|
|
+ selectedDeptId.value = firstDept.id
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ deptTreeRef.value?.setCurrentKey(firstDept.id)
|
|
|
|
|
+ await loadDispatchUsers(firstDept.id)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function findFirstDept(nodes = []) {
|
|
|
|
|
+ for (const node of nodes) {
|
|
|
|
|
+ if (node?.id) return node
|
|
|
|
|
+ if (node?.children?.length) {
|
|
|
|
|
+ const child = findFirstDept(node.children)
|
|
|
|
|
+ if (child) return child
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleDeptClick(data) {
|
|
|
|
|
+ selectedDeptId.value = data.id
|
|
|
|
|
+ loadDispatchUsers(data.id)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleUserCurrentChange(row) {
|
|
|
|
|
+ dispatchSelectedUser.value = row || null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function confirmDispatchUser() {
|
|
|
|
|
+ if (!dispatchSelectedUser.value?.userId) {
|
|
|
|
|
+ ElMessage.warning('请选择用户')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ receiveUserId.value = dispatchSelectedUser.value.userId
|
|
|
|
|
+ receiveUserDisplay.value = `${dispatchSelectedUser.value.userName} / ID: ${dispatchSelectedUser.value.userId}`
|
|
|
|
|
+ dispatchDialogVisible.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function startRepair() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operateCurrent(4, '开始维修')
|
|
|
|
|
+ ElMessage.success('已开始维修')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error?.message || '开始维修失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitVerify() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operateCurrent(5, '提交验收')
|
|
|
|
|
+ ElMessage.success('已提交验收')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error?.message || '提交验收失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function approveOrder(isApproved) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operateCurrent(isApproved ? 6 : 7, isApproved ? '验收通过' : '驳回工单')
|
|
|
|
|
+ ElMessage.success(isApproved ? '验收已通过' : '工单已驳回')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error?.message || '工单操作失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function approveDelay() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operateCurrent(9, '延期审核通过')
|
|
|
|
|
+ ElMessage.success('延期审核已通过')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error?.message || '延期审核通过失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function rejectDelay() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operateCurrent(7, '驳回延期')
|
|
|
|
|
+ ElMessage.success('延期申请已驳回')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error?.message || '驳回延期失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openDelayDialog() {
|
|
|
delayDeadline.value = ''
|
|
delayDeadline.value = ''
|
|
|
delayReason.value = ''
|
|
delayReason.value = ''
|
|
|
|
|
+ delayDialogVisible.value = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 审批功能
|
|
|
|
|
-const approveOrder = (isApproved) => {
|
|
|
|
|
- if (selectedOrder.value.status !== 'feedback') return
|
|
|
|
|
- if (isApproved) {
|
|
|
|
|
- selectedOrder.value.status = 'completed'
|
|
|
|
|
- selectedOrder.value.processLogs.push({
|
|
|
|
|
- id: Date.now(),
|
|
|
|
|
- time: new Date().toLocaleString(),
|
|
|
|
|
- content: `审批通过,工单办结。审批意见:${approvalRemark.value || '无'}`,
|
|
|
|
|
- type: 'success',
|
|
|
|
|
- operator: '审批人'
|
|
|
|
|
- })
|
|
|
|
|
- ElMessage.success('工单审批通过,已办结')
|
|
|
|
|
- } else {
|
|
|
|
|
- selectedOrder.value.status = 'processing'
|
|
|
|
|
- selectedOrder.value.processLogs.push({
|
|
|
|
|
- id: Date.now(),
|
|
|
|
|
- time: new Date().toLocaleString(),
|
|
|
|
|
- content: `审批驳回,需重新处置。驳回意见:${approvalRemark.value || '请重新反馈'}`,
|
|
|
|
|
- type: 'danger',
|
|
|
|
|
- operator: '审批人'
|
|
|
|
|
|
|
+async function submitDelay() {
|
|
|
|
|
+ if (!delayDeadline.value) {
|
|
|
|
|
+ ElMessage.warning('请选择截止时间')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!delayReason.value.trim()) {
|
|
|
|
|
+ ElMessage.warning('请填写延期理由')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ await operateCurrent(8, '申请延期', {
|
|
|
|
|
+ delayedDeadline: delayDeadline.value,
|
|
|
|
|
+ delayedReason: delayReason.value.trim()
|
|
|
})
|
|
})
|
|
|
- // 清除原反馈,等待重新上报
|
|
|
|
|
- selectedOrder.value.feedback = null
|
|
|
|
|
- ElMessage.warning('工单已驳回,请重新处置反馈')
|
|
|
|
|
|
|
+ delayDialogVisible.value = false
|
|
|
|
|
+ ElMessage.success('延期申请已提交')
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error?.message || '延期申请失败')
|
|
|
}
|
|
}
|
|
|
- approvalRemark.value = ''
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const viewCase = () => { ElMessage.info('跳转案件详情(演示)') }
|
|
|
|
|
-const playVideo = () => { ElMessage.info('模拟播放周边视频监控') }
|
|
|
|
|
|
|
+function getLogType(operType) {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ 2: 'primary',
|
|
|
|
|
+ 3: 'warning',
|
|
|
|
|
+ 4: 'primary',
|
|
|
|
|
+ 5: 'warning',
|
|
|
|
|
+ 6: 'success',
|
|
|
|
|
+ 7: 'danger',
|
|
|
|
|
+ 8: 'warning',
|
|
|
|
|
+ 9: 'success'
|
|
|
|
|
+ }
|
|
|
|
|
+ return map[Number(operType)] || 'info'
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadOrders()
|
|
|
|
|
+})
|
|
|
</script>
|
|
</script>
|
|
|
-
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-.workorder-management {
|
|
|
|
|
- background: #f0f2f6;
|
|
|
|
|
- min-height: 100vh;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
-}
|
|
|
|
|
-.page-header {
|
|
|
|
|
- background: white;
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- padding: 16px 24px;
|
|
|
|
|
- margin-bottom: 20px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-.header-title { display: flex; align-items: center; gap: 14px; font-size: 20px; font-weight: 600; }
|
|
|
|
|
-.header-stats { display: flex; gap: 24px; }
|
|
|
|
|
-.stat { text-align: center; cursor: pointer; padding: 4px 16px; border-radius: 30px; }
|
|
|
|
|
-.stat:hover { background: #ecf5ff; }
|
|
|
|
|
-.num { font-size: 26px; font-weight: bold; }
|
|
|
|
|
-.num.warning { color: #e6a23c; }
|
|
|
|
|
-.main-layout { display: flex; gap: 20px; }
|
|
|
|
|
-.order-list-panel {
|
|
|
|
|
- width: 380px;
|
|
|
|
|
- background: white;
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-}
|
|
|
|
|
-.list-header {
|
|
|
|
|
- padding: 16px;
|
|
|
|
|
- border-bottom: 1px solid #e9ecef;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
-}
|
|
|
|
|
-.order-list { flex: 1; overflow-y: auto; padding: 12px; }
|
|
|
|
|
-.order-item {
|
|
|
|
|
- padding: 12px;
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
- border-radius: 14px;
|
|
|
|
|
- background: #fafbfc;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- border: 1px solid transparent;
|
|
|
|
|
-}
|
|
|
|
|
-.order-item.active { background: #ecf5ff; border-color: #409eff; }
|
|
|
|
|
-.order-item.pending { border-left: 3px solid #f56c6c; }
|
|
|
|
|
-.order-item.processing { border-left: 3px solid #e6a23c; }
|
|
|
|
|
-.order-item.feedback { border-left: 3px solid #909399; }
|
|
|
|
|
-.order-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
|
|
|
|
|
-.order-code { font-weight: 600; }
|
|
|
|
|
-.order-location { font-size: 12px; color: #666; margin: 4px 0; }
|
|
|
|
|
-.order-time { font-size: 11px; color: #aaa; }
|
|
|
|
|
-.detail-panel {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- background: white;
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- padding: 20px;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
-}
|
|
|
|
|
-.section-title {
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
- margin: 16px 0 12px 0;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-.assign-area { display: flex; gap: 8px; }
|
|
|
|
|
-.delay-buttons { display: flex; gap: 12px; margin-bottom: 12px; }
|
|
|
|
|
-.delay-history { background: #fef7e0; padding: 8px 12px; border-radius: 8px; font-size: 13px; }
|
|
|
|
|
-.feedback-images { display: flex; gap: 12px; margin-top: 8px; }
|
|
|
|
|
-.feedback-img { width: 100px; height: 80px; border-radius: 8px; object-fit: cover; }
|
|
|
|
|
-.video-preview { background: #eef2fa; padding: 10px; border-radius: 12px; text-align: center; cursor: pointer; margin-top: 12px; }
|
|
|
|
|
-.approval-section { margin-top: 20px; border-top: 1px solid #e9ecef; padding-top: 16px; }
|
|
|
|
|
-.approval-buttons { display: flex; gap: 12px; margin: 12px 0; }
|
|
|
|
|
-.approved-info { margin-top: 16px; }
|
|
|
|
|
-.empty-panel { flex: 1; display: flex; align-items: center; justify-content: center; background: white; border-radius: 20px; }
|
|
|
|
|
-.operator { font-size: 12px; color: #909399; margin-left: 12px; }
|
|
|
|
|
-.pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
|
|
|
|
|
-</style>
|
|
|