|
|
@@ -1,651 +1,953 @@
|
|
|
-<!--工单列表-->
|
|
|
+<!--设备故障工单(故障维修 orderType=1,参考排水/燃气工单形态)-->
|
|
|
+<!--数据源:/api/work-order/*(orderType=1 过滤;equipmentType 为空 = 所有设备)-->
|
|
|
+<!--需求 5.2.4.4.1:手动新增字段=设备类型/设备ID/设备位置/设备厂家/维护人员/联系方式/故障时间/故障描述-->
|
|
|
<template>
|
|
|
- <div class="prototype-container">
|
|
|
- <!-- 头部 -->
|
|
|
- <div class="header">
|
|
|
- <div class="logo">
|
|
|
- <h2>🔧 设备故障工单管理系统</h2>
|
|
|
- <p>故障自动预警 | 智能派单 | 工单闭环管理</p>
|
|
|
+ <div class="order-container">
|
|
|
+ <!-- 顶部搜索区(参考排水/燃气工单) -->
|
|
|
+ <el-card class="search-card" shadow="never">
|
|
|
+ <div class="search-form">
|
|
|
+ <el-form :model="queryParams" ref="queryFormRef" :inline>
|
|
|
+ <el-form-item label="优先级">
|
|
|
+ <el-select v-model="queryParams.orderLevel" placeholder="全部优先级" clearable style="width: 120px">
|
|
|
+ <el-option v-for="(v, k) in ORDER_LEVEL_MAP" :key="k" :label="v.text" :value="Number(k)"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-select v-model="queryParams.orderStatus" placeholder="全部状态" clearable style="width: 120px">
|
|
|
+ <el-option v-for="(v, k) in ORDER_STATUS_MAP" :key="k" :label="v.text" :value="Number(k)"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="设备编码">
|
|
|
+ <el-input v-model="queryParams.deviceCode" placeholder="设备编码" clearable style="width: 150px" @keyup.enter="handleSearch"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="关键词">
|
|
|
+ <el-input v-model="queryParams.keyword" placeholder="工单编号/设备编码/描述" clearable style="width: 200px" @keyup.enter="handleSearch"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="handleSearch">查询</el-button>
|
|
|
+ <el-button @click="handleReset">重置</el-button>
|
|
|
+ <el-button type="primary" plain @click="openAddDialog">➕ 新增故障工单</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
</div>
|
|
|
- <div class="header-stats">
|
|
|
- <div class="stat-card pending">
|
|
|
- <span class="stat-value">{{ pendingCount }}</span>
|
|
|
- <span class="stat-label">待处理</span>
|
|
|
- </div>
|
|
|
- <div class="stat-card processing">
|
|
|
- <span class="stat-value">{{ processingCount }}</span>
|
|
|
- <span class="stat-label">处理中</span>
|
|
|
- </div>
|
|
|
- <div class="stat-card completed">
|
|
|
- <span class="stat-value">{{ completedCount }}</span>
|
|
|
- <span class="stat-label">已完成</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="main-layout">
|
|
|
- <!-- 左侧:实时故障监控与自动生成 -->
|
|
|
- <div class="left-panel">
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 🚨 实时故障监控
|
|
|
- <span class="live-badge">● 自动监测中</span>
|
|
|
- </div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="fault-monitor">
|
|
|
- <div class="monitor-list">
|
|
|
- <div v-for="fault in recentFaults" :key="fault.id" class="fault-item" :class="{ new: fault.isNew }">
|
|
|
- <div class="fault-time">{{ fault.time }}</div>
|
|
|
- <div class="fault-device">{{ fault.deviceName }}</div>
|
|
|
- <div class="fault-message">{{ fault.message }}</div>
|
|
|
- <div class="fault-order-status">
|
|
|
- <span v-if="fault.orderCreated" class="order-created">✅ 工单已生成</span>
|
|
|
- <span v-else class="order-creating">⏳ 正在生成工单...</span>
|
|
|
- </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 主容器:左列表 + 右详情(参考排水/燃气工单) -->
|
|
|
+ <el-row :gutter="16" class="main-row">
|
|
|
+ <!-- 左侧:工单列表 -->
|
|
|
+ <el-col :span="8" class="col-fill">
|
|
|
+ <el-card class="box-card" shadow="always">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>故障工单列表</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-scrollbar class="list-scroll">
|
|
|
+ <div class="order-list">
|
|
|
+ <div
|
|
|
+ v-for="order in tableData"
|
|
|
+ :key="order.orderId"
|
|
|
+ class="order-item"
|
|
|
+ :class="{ 'active': selectedOrderId === order.orderId }"
|
|
|
+ @click="handleSelectOrder(order)"
|
|
|
+ >
|
|
|
+ <div class="order-header">
|
|
|
+ <span class="order-no">{{ order.orderNo }}</span>
|
|
|
+ <el-tag :type="orderStatusType(order.orderStatus)" size="small" effect="light" class="status-badge">{{ orderStatusText(order.orderStatus) }}</el-tag>
|
|
|
</div>
|
|
|
- <div v-if="recentFaults.length === 0" class="empty-fault">
|
|
|
- 暂无实时故障,系统运行正常
|
|
|
+ <div class="order-body">
|
|
|
+ <div class="order-tags">
|
|
|
+ <el-tag :type="orderLevelType(order.orderLevel)" size="small" effect="plain">{{ orderLevelText(order.orderLevel) }}</el-tag>
|
|
|
+ <el-tag v-if="order.hazardNo" size="small" effect="plain">隐患 {{ order.hazardNo }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="order-desc" v-if="order.orderDesc">{{ order.orderDesc }}</div>
|
|
|
+ <div class="device-code">设备编号:{{ order.deviceCode || '-' }}</div>
|
|
|
+ <div class="create-time">故障时间:{{ timeText(order.createTime) }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <el-empty v-if="tableData.length === 0" description="暂无数据" :image-size="100"/>
|
|
|
</div>
|
|
|
+ </el-scrollbar>
|
|
|
+ <div class="pagination-wrapper">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pageNum"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :total="total"
|
|
|
+ layout="total, prev, pager, next"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ class="pagination"
|
|
|
+ />
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 手动新增工单 -->
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- ➕ 手动新增故障工单
|
|
|
- <button @click="openAddModal" class="btn-primary btn-sm">+ 新增工单</button>
|
|
|
- </div>
|
|
|
- <div class="card-body" v-if="showAddForm">
|
|
|
- <div class="add-form">
|
|
|
- <div class="form-row">
|
|
|
- <div class="form-group half">
|
|
|
- <label class="form-label">设备类型 *</label>
|
|
|
- <select v-model="newOrder.deviceType" class="form-select">
|
|
|
- <option value="">请选择</option>
|
|
|
- <option value="压力传感器">压力传感器</option>
|
|
|
- <option value="流量传感器">流量传感器</option>
|
|
|
- <option value="温度传感器">温度传感器</option>
|
|
|
- <option value="泄漏检测仪">泄漏检测仪</option>
|
|
|
- <option value="水位监测仪">水位监测仪</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="form-group half">
|
|
|
- <label class="form-label">设备ID *</label>
|
|
|
- <input v-model="newOrder.deviceId" class="form-input" placeholder="如: PS-001">
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="form-row">
|
|
|
- <div class="form-group half">
|
|
|
- <label class="form-label">设备位置 *</label>
|
|
|
- <input v-model="newOrder.location" class="form-input" placeholder="设备安装位置">
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 右侧:工单详情 -->
|
|
|
+ <el-col :span="16" class="col-fill">
|
|
|
+ <el-card v-loading="detailLoading" class="box-card" shadow="always">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span v-if="selectedOrder && selectedOrder.orderId">工单详情 - {{ selectedOrder.orderNo }}</span>
|
|
|
+ <span v-else>请选择工单查看详情</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div v-if="selectedOrder && selectedOrder.orderId" class="detail-body">
|
|
|
+ <el-tabs v-model="activeTab" class="detail-tabs">
|
|
|
+ <!-- 工单信息 -->
|
|
|
+ <el-tab-pane label="工单信息" name="info">
|
|
|
+ <el-descriptions :column="2" border size="small">
|
|
|
+ <el-descriptions-item label="工单编号">{{ selectedOrder.orderNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="工单状态">
|
|
|
+ <el-tag :type="orderStatusType(selectedOrder.orderStatus)" size="small">{{ orderStatusText(selectedOrder.orderStatus) }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="关联案件编号">{{ selectedOrder.caseNo || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="关联隐患编号">{{ selectedOrder.hazardNo || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="工单类型">{{ orderTypeText(selectedOrder.orderType) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="优先级">
|
|
|
+ <el-tag :type="orderLevelType(selectedOrder.orderLevel)" size="small">{{ orderLevelText(selectedOrder.orderLevel) }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="接单人">{{ userNames(selectedOrder.receiveUser) || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="问题描述" :span="2">{{ selectedOrder.orderDesc || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="故障时间">{{ timeText(selectedOrder.createTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="派单时间">{{ timeText(selectedOrder.dispatchTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="计划完成">{{ timeText(selectedOrder.planFinishTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="实际完成">{{ timeText(selectedOrder.realFinishTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="验收时间">{{ timeText(selectedOrder.verifyTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ timeText(selectedOrder.createTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="更新时间">{{ timeText(selectedOrder.updateTime) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="处置结果" :span="2">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <!-- 关联设备信息(需求 5.2.4.4.1 设备维度) -->
|
|
|
+ <div v-if="detailDevice" class="device-section">
|
|
|
+ <h4 class="section-title">🔗 关联设备</h4>
|
|
|
+ <el-descriptions :column="2" border size="small">
|
|
|
+ <el-descriptions-item label="设备类型">{{ detailDevice.equipmentTypeName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备编码">{{ detailDevice.equipmentCode || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="安装位置">{{ detailDevice.equipmentLocation || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="管点地址">{{ detailDevice.pointAddress || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备厂家">{{ detailDevice.manufacturer || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="所属管线">{{ detailDevice.networkName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="维护人员">{{ detailDevice.maintainer || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="联系方式">{{ detailDevice.maintainerPhone || '-' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
</div>
|
|
|
- <div class="form-group half">
|
|
|
- <label class="form-label">设备厂家</label>
|
|
|
- <input v-model="newOrder.manufacturer" class="form-input" placeholder="设备生产厂家">
|
|
|
+
|
|
|
+ <!-- 工单图片 -->
|
|
|
+ <div v-if="workImageList.length > 0" class="image-section">
|
|
|
+ <h4 class="section-title">工单图片</h4>
|
|
|
+ <div class="image-list">
|
|
|
+ <image-preview v-for="(img, index) in workImageList" :key="index" :src="img" :width="100" :height="100"/>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="form-row">
|
|
|
- <div class="form-group half">
|
|
|
- <label class="form-label">维护人员 *</label>
|
|
|
- <select v-model="newOrder.maintainer" class="form-select">
|
|
|
- <option value="">请选择</option>
|
|
|
- <option value="张工">张工(设备组)</option>
|
|
|
- <option value="李工">李工(运维组)</option>
|
|
|
- <option value="王工">王工(抢修组)</option>
|
|
|
- <option value="赵工">赵工(技术组)</option>
|
|
|
- </select>
|
|
|
+
|
|
|
+ <!-- 工单操作(参考排水/燃气工单按钮组) -->
|
|
|
+ <div class="action-section">
|
|
|
+ <h4 class="section-title">工单操作</h4>
|
|
|
+ <div class="action-buttons">
|
|
|
+ <el-button v-if="canOperate.dispatch(selectedOrder)" type="primary" @click="handleDispatch">下派任务</el-button>
|
|
|
+ <el-button v-if="canOperate.verify(selectedOrder)" type="warning" @click="handleVerify">核查</el-button>
|
|
|
+ <el-button v-if="canOperate.process(selectedOrder)" type="success" @click="handleProcess">处理</el-button>
|
|
|
+ <el-button v-if="canOperate.complete(selectedOrder)" type="danger" @click="handleComplete">提交验收</el-button>
|
|
|
+ <el-button v-if="canOperate.close(selectedOrder)" type="success" @click="handleClose">验收通过</el-button>
|
|
|
+ <el-button v-if="canOperate.cancel(selectedOrder)" type="info" plain @click="handleCancel">取消工单</el-button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="form-group half">
|
|
|
- <label class="form-label">联系方式</label>
|
|
|
- <input v-model="newOrder.contact" class="form-input" placeholder="维护人员电话">
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 处理历史 -->
|
|
|
+ <el-tab-pane label="处理历史" name="history">
|
|
|
+ <div class="timeline-scroll">
|
|
|
+ <el-timeline v-if="currentOrderLogs && currentOrderLogs.length > 0">
|
|
|
+ <el-timeline-item
|
|
|
+ v-for="log in currentOrderLogs"
|
|
|
+ :key="log.logId || log.id"
|
|
|
+ :timestamp="timeText(log.operTime)"
|
|
|
+ placement="top"
|
|
|
+ :type="orderLogType(log.operType)"
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <div class="timeline-content">
|
|
|
+ <div class="timestamp">{{ timeText(log.operTime) }}</div>
|
|
|
+ <div class="operator-info">
|
|
|
+ <span class="operator-name">{{ log.operUserName || '-' }}</span>
|
|
|
+ <span class="oper-type">· {{ orderLogText(log.operType) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="oper-desc">{{ log.operDesc || '-' }}</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
+ <el-empty v-else description="暂无处理记录" :image-size="100"/>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">故障时间 *</label>
|
|
|
- <input type="datetime-local" v-model="newOrder.faultTime" class="form-input">
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">故障描述 *</label>
|
|
|
- <textarea v-model="newOrder.description" class="form-textarea" rows="2" placeholder="请描述故障现象..."></textarea>
|
|
|
- </div>
|
|
|
- <div class="form-actions">
|
|
|
- <button @click="cancelAdd" class="btn-outline">取消</button>
|
|
|
- <button @click="submitManualOrder" class="btn-primary">提交工单</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="card-body" v-else>
|
|
|
- <div class="empty-add" @click="openAddModal">
|
|
|
- <span class="add-icon">+</span>
|
|
|
- <span>点击新增故障工单</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 右侧:工单列表 -->
|
|
|
- <div class="right-panel">
|
|
|
- <div class="card">
|
|
|
- <div class="card-header">
|
|
|
- 📋 设备故障工单列表
|
|
|
- <span class="list-count">共 {{ filteredOrders.length }} 条记录</span>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
</div>
|
|
|
- <div class="card-body">
|
|
|
- <!-- 查询筛选栏 -->
|
|
|
- <div class="filter-bar">
|
|
|
- <select v-model="queryParams.deviceType" class="filter-select">
|
|
|
- <option value="">设备类型</option>
|
|
|
- <option value="压力传感器">压力传感器</option>
|
|
|
- <option value="流量传感器">流量传感器</option>
|
|
|
- <option value="温度传感器">温度传感器</option>
|
|
|
- <option value="泄漏检测仪">泄漏检测仪</option>
|
|
|
- </select>
|
|
|
- <input type="text" v-model="queryParams.deviceId" class="filter-input" placeholder="设备ID">
|
|
|
- <input type="date" v-model="queryParams.startDate" class="filter-input" placeholder="开始日期">
|
|
|
- <span class="filter-sep">至</span>
|
|
|
- <input type="date" v-model="queryParams.endDate" class="filter-input" placeholder="结束日期">
|
|
|
- <button @click="searchOrders" class="btn-primary btn-sm">查询</button>
|
|
|
- <button @click="resetQuery" class="btn-outline btn-sm">重置</button>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- 工单表格 -->
|
|
|
- <div class="order-table-wrapper">
|
|
|
- <table class="data-table">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>工单编号</th>
|
|
|
- <th>设备类型</th>
|
|
|
- <th>设备ID</th>
|
|
|
- <th>设备位置</th>
|
|
|
- <th>故障描述</th>
|
|
|
- <th>故障时间</th>
|
|
|
- <th>维护人员</th>
|
|
|
- <th>状态</th>
|
|
|
- <th>操作</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- <tr v-for="order in paginatedOrders" :key="order.id">
|
|
|
- <td class="order-no">{{ order.orderNo }}</td>
|
|
|
- <td>{{ order.deviceType }}</td>
|
|
|
- <td>{{ order.deviceId }}</td>
|
|
|
- <td>{{ order.location }}</td>
|
|
|
- <td class="desc-cell">{{ order.description }}</td>
|
|
|
- <td class="time-cell">{{ order.faultTime }}</td>
|
|
|
- <td>{{ order.maintainer }}</td>
|
|
|
- <td>
|
|
|
- <span class="status-badge" :class="getStatusClass(order.status)">
|
|
|
- {{ getStatusText(order.status) }}
|
|
|
- </span>
|
|
|
- </td>
|
|
|
- <td class="action-cell">
|
|
|
- <button @click="viewOrderDetail(order)" class="btn-view">查看</button>
|
|
|
- <button v-if="order.status === 'pending'" @click="updateStatus(order, 'processing')" class="btn-start">开始处理</button>
|
|
|
- <button v-if="order.status === 'processing'" @click="completeOrder(order)" class="btn-complete">完成</button>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- <tr v-if="filteredOrders.length === 0">
|
|
|
- <td colspan="9" class="empty-row">暂无工单数据</td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 分页 -->
|
|
|
- <div class="pagination" v-if="totalPages > 1">
|
|
|
- <button v-for="page in totalPages" :key="page" class="page-btn" :class="{ active: currentPage === page }"
|
|
|
- @click="currentPage = page">{{ page }}</button>
|
|
|
- </div>
|
|
|
+ <el-empty v-else description="请从左侧选择工单" :image-size="150"/>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 新增工单弹窗(需求 5.2.4.4.1:设备类型/设备ID/设备位置/设备厂家/维护人员/联系方式/故障时间/故障描述) -->
|
|
|
+ <el-dialog v-model="addVisible" title="➕ 新增故障工单" width="680px" append-to-body destroy-on-close>
|
|
|
+ <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
|
|
|
+ <el-form-item label="关联设备" prop="deviceId">
|
|
|
+ <el-select v-model="addForm.deviceId" filterable remote clearable placeholder="输入设备编码/名称搜索" :remote-method="searchDevice" :loading="deviceLoading" style="width:100%" @change="onDeviceChange">
|
|
|
+ <el-option v-for="dev in deviceOptions" :key="dev.equipmentId" :label="`${dev.equipmentName || dev.equipmentCode} (${dev.equipmentCode})`" :value="dev.equipmentId"/>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 设备信息卡:选择设备后自动带出 设备类型/设备ID/设备位置/设备厂家/维护人员/联系方式 -->
|
|
|
+ <el-form-item v-if="deviceInfo" label="设备信息">
|
|
|
+ <div class="device-info-card">
|
|
|
+ <el-descriptions :column="2" border size="small">
|
|
|
+ <el-descriptions-item label="设备类型">{{ deviceInfo.equipmentTypeName || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备ID">{{ deviceInfo.equipmentCode || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备位置">{{ deviceInfo.equipmentLocation || deviceInfo.pointAddress || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="设备厂家">{{ deviceInfo.manufacturer || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="维护人员">{{ deviceInfo.maintainer || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="联系方式">{{ deviceInfo.maintainerPhone || '-' }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="设备编码">
|
|
|
+ <el-input v-model="addForm.deviceCode" placeholder="未选设备时手动填写" clearable/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="优先级" prop="orderLevel">
|
|
|
+ <el-radio-group v-model="addForm.orderLevel">
|
|
|
+ <el-radio-button :value="1">紧急</el-radio-button>
|
|
|
+ <el-radio-button :value="2">一般</el-radio-button>
|
|
|
+ <el-radio-button :value="3">低</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="故障时间">
|
|
|
+ <el-date-picker v-model="addForm.faultTime" type="datetime" placeholder="故障发生时间(默认当前)" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="计划完成时间">
|
|
|
+ <el-date-picker v-model="addForm.planFinishTime" type="datetime" placeholder="请选择计划完成时间" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="故障描述" prop="orderDesc">
|
|
|
+ <el-input v-model="addForm.orderDesc" type="textarea" :rows="4" placeholder="请描述故障现象 / 设备离线信息等" maxlength="500" show-word-limit/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="addVisible=false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitAdd">提交工单</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 派单弹窗(部门树 + 用户多选,参考排水/燃气工单) -->
|
|
|
+ <el-dialog v-model="dispatchDialogVisible" title="下派任务 - 选择接单人" width="750px" append-to-body>
|
|
|
+ <div style="display: flex; gap: 16px; min-height: 350px;">
|
|
|
+ <div style="width: 280px; border-right: 1px solid #e4e7ed; padding-right: 12px;">
|
|
|
+ <el-input v-model="deptFilterText" placeholder="筛选部门" clearable size="small" style="margin-bottom: 8px"/>
|
|
|
+ <el-tree
|
|
|
+ ref="deptTreeRef"
|
|
|
+ :data="deptTreeData"
|
|
|
+ :props="{ label: 'deptName', children: 'children' }"
|
|
|
+ :filter-node-method="filterDeptNode"
|
|
|
+ node-key="deptId"
|
|
|
+ highlight-current
|
|
|
+ @node-click="handleDeptClick"
|
|
|
+ style="font-size: 14px"
|
|
|
+ />
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 工单详情模态框 -->
|
|
|
- <div v-if="detailModalVisible" class="modal-overlay" @click.self="closeDetailModal">
|
|
|
- <div class="modal-content">
|
|
|
- <h3>📋 工单详细信息</h3>
|
|
|
- <div class="detail-info">
|
|
|
- <div class="info-row"><span class="label">工单编号:</span>{{ currentOrder?.orderNo }}</div>
|
|
|
- <div class="info-row"><span class="label">设备类型:</span>{{ currentOrder?.deviceType }}</div>
|
|
|
- <div class="info-row"><span class="label">设备ID:</span>{{ currentOrder?.deviceId }}</div>
|
|
|
- <div class="info-row"><span class="label">设备位置:</span>{{ currentOrder?.location }}</div>
|
|
|
- <div class="info-row"><span class="label">设备厂家:</span>{{ currentOrder?.manufacturer || '-' }}</div>
|
|
|
- <div class="info-row"><span class="label">维护人员:</span>{{ currentOrder?.maintainer }}</div>
|
|
|
- <div class="info-row"><span class="label">联系方式:</span>{{ currentOrder?.contact || '-' }}</div>
|
|
|
- <div class="info-row"><span class="label">故障时间:</span>{{ currentOrder?.faultTime }}</div>
|
|
|
- <div class="info-row"><span class="label">故障描述:</span>{{ currentOrder?.description }}</div>
|
|
|
- <div class="info-row"><span class="label">创建时间:</span>{{ currentOrder?.createTime }}</div>
|
|
|
- <div class="info-row" v-if="currentOrder?.startTime"><span class="label">开始处理:</span>{{ currentOrder?.startTime }}</div>
|
|
|
- <div class="info-row" v-if="currentOrder?.completeTime"><span class="label">完成时间:</span>{{ currentOrder?.completeTime }}</div>
|
|
|
- <div class="info-row" v-if="currentOrder?.processResult"><span class="label">处理结果:</span>{{ currentOrder?.processResult }}</div>
|
|
|
- </div>
|
|
|
- <div class="form-actions">
|
|
|
- <button @click="closeDetailModal" class="btn-primary">关闭</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 完成工单模态框 -->
|
|
|
- <div v-if="completeModalVisible" class="modal-overlay" @click.self="closeCompleteModal">
|
|
|
- <div class="modal-content" style="width:450px;">
|
|
|
- <h3>✅ 工单完成反馈</h3>
|
|
|
- <div class="order-info-summary">
|
|
|
- <div><span class="label">工单号:</span>{{ currentOrder?.orderNo }}</div>
|
|
|
- <div><span class="label">设备:</span>{{ currentOrder?.deviceId }} - {{ currentOrder?.deviceType }}</div>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label class="form-label">处理结果</label>
|
|
|
- <textarea v-model="processResult" class="form-textarea" rows="3" placeholder="请描述处理过程及结果..."></textarea>
|
|
|
- </div>
|
|
|
- <div class="form-actions">
|
|
|
- <button @click="closeCompleteModal" class="btn-outline">取消</button>
|
|
|
- <button @click="submitComplete" class="btn-primary">提交完成</button>
|
|
|
+ <div style="flex: 1; min-width: 0;">
|
|
|
+ <el-table
|
|
|
+ ref="userTableRef"
|
|
|
+ :data="userList"
|
|
|
+ border size="small"
|
|
|
+ max-height="300"
|
|
|
+ row-key="userId"
|
|
|
+ @selection-change="handleSelectionChange"
|
|
|
+ @row-click="handleRowClick"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="40" align="center" :reserve-selection="true"/>
|
|
|
+ <el-table-column prop="userName" label="用户名" width="100"/>
|
|
|
+ <el-table-column prop="nickName" label="昵称" width="100"/>
|
|
|
+ <el-table-column prop="dept.deptName" label="部门" show-overflow-tooltip/>
|
|
|
+ </el-table>
|
|
|
+ <div v-if="userList.length === 0" style="text-align: center; color: #909399; padding: 40px 0;">请先在左侧选择部门</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ <el-form label-width="80px" style="margin-top: 16px">
|
|
|
+ <el-form-item label="备注"><el-input v-model="dispatchForm.remarks" placeholder="请输入派单备注"/></el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <span style="float: left; line-height: 32px; color: #606266;">
|
|
|
+ 已选人员:
|
|
|
+ <span v-if="selectedUserNames.length > 0" style="color: #409eff; font-weight: bold;">{{ selectedUserNames.join('、') }}</span>
|
|
|
+ <span v-else style="color: #909399">未选择</span>
|
|
|
+ ({{ dispatchForm.handlerIds.length }})
|
|
|
+ </span>
|
|
|
+ <el-button @click="dispatchDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitDispatch">确认</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 核查/处理弹窗 -->
|
|
|
+ <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px" append-to-body>
|
|
|
+ <el-form label-width="90px">
|
|
|
+ <el-form-item label="结果说明" required>
|
|
|
+ <el-input v-model="actionFormData.solution" type="textarea" :rows="5" :placeholder="actionTitle === '核查工单' ? '请输入核查结论(属实/不属实、现场情况)' : '请输入故障原因、处理结果及处理措施'" maxlength="500" show-word-limit/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="actionDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitAction">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 提交验收弹窗 -->
|
|
|
+ <el-dialog v-model="completeDialogVisible" title="提交验收" width="500px" append-to-body>
|
|
|
+ <el-form label-width="80px">
|
|
|
+ <el-form-item label="验收意见">
|
|
|
+ <el-input v-model="completeForm.result" type="textarea" :rows="4" placeholder="请填写维修处置结果说明" maxlength="500" show-word-limit/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="completeDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitting" @click="submitComplete">提交</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 验收通过/结案弹窗 -->
|
|
|
+ <el-dialog v-model="closeDialogVisible" title="验收通过" width="500px" append-to-body>
|
|
|
+ <el-form label-width="80px">
|
|
|
+ <el-form-item label="验收结果">
|
|
|
+ <el-input v-model="closeForm.result" type="textarea" :rows="4" placeholder="请填写验收结果(结案后关联隐患同步为已整改、设备报警状态重置)" maxlength="500" show-word-limit/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="closeDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="success" :loading="submitting" @click="submitClose">确认验收</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
|
-
|
|
|
-// ==================== 工单数据 ====================
|
|
|
-const orders = ref([
|
|
|
- { id: 1, orderNo: 'WO-20250508-001', deviceType: '压力传感器', deviceId: 'PS-001', location: '城东泵站', manufacturer: '华仪科技', maintainer: '张工', contact: '13800001111', faultTime: '2025-05-08 09:23:15', description: '设备离线,数据上报异常,超过30分钟未回传', createTime: '2025-05-08 09:23:18', status: 'pending', startTime: null, completeTime: null, processResult: null },
|
|
|
- { id: 2, orderNo: 'WO-20250508-002', deviceType: '泄漏检测仪', deviceId: 'LD-005', location: '化工园区', manufacturer: '安防科技', maintainer: '李工', contact: '13800002222', faultTime: '2025-05-08 07:30:22', description: '可燃气浓度超阈值报警,需现场核查', createTime: '2025-05-08 07:30:25', status: 'processing', startTime: '2025-05-08 08:15:00', completeTime: null, processResult: null },
|
|
|
- { id: 3, orderNo: 'WO-20250507-003', deviceType: '水位监测仪', deviceId: 'WL-006', location: '东区排水口', manufacturer: '水文科技', maintainer: '王工', contact: '13800003333', faultTime: '2025-05-07 14:20:33', description: '水位数据异常波动,需校准', createTime: '2025-05-07 14:20:36', status: 'completed', startTime: '2025-05-07 14:35:00', completeTime: '2025-05-07 15:20:00', processResult: '已校准传感器,数据恢复正常' },
|
|
|
- { id: 4, orderNo: 'WO-20250508-004', deviceType: '流量传感器', deviceId: 'FL-002', location: '城南枢纽', manufacturer: '水测智能', maintainer: '张工', contact: '13800001111', faultTime: '2025-05-08 06:15:00', description: '流量数据中断,设备可能离线', createTime: '2025-05-08 06:15:03', status: 'pending', startTime: null, completeTime: null, processResult: null }
|
|
|
-])
|
|
|
-
|
|
|
-// 实时故障监控模拟数据
|
|
|
-const recentFaults = ref([
|
|
|
- { id: 101, time: '09:23:15', deviceName: '压力传感器 PS-001', message: '设备离线,数据上报异常', orderCreated: true, isNew: false },
|
|
|
- { id: 102, time: '07:30:22', deviceName: '泄漏检测仪 LD-005', message: '可燃气浓度超阈值报警', orderCreated: true, isNew: false },
|
|
|
- { id: 103, time: '06:15:00', deviceName: '流量传感器 FL-002', message: '流量数据中断', orderCreated: true, isNew: false }
|
|
|
-])
|
|
|
-
|
|
|
-// ==================== 查询参数 ====================
|
|
|
-const queryParams = ref({
|
|
|
- deviceType: '',
|
|
|
- deviceId: '',
|
|
|
- startDate: '',
|
|
|
- endDate: ''
|
|
|
-})
|
|
|
-const currentPage = ref(1)
|
|
|
-const pageSize = 8
|
|
|
-
|
|
|
-// 过滤后的工单(时间倒序)
|
|
|
-const filteredOrders = computed(() => {
|
|
|
- let result = [...orders.value]
|
|
|
- if (queryParams.value.deviceType) {
|
|
|
- result = result.filter(o => o.deviceType === queryParams.value.deviceType)
|
|
|
- }
|
|
|
- if (queryParams.value.deviceId) {
|
|
|
- result = result.filter(o => o.deviceId.toLowerCase().includes(queryParams.value.deviceId.toLowerCase()))
|
|
|
- }
|
|
|
- if (queryParams.value.startDate) {
|
|
|
- result = result.filter(o => o.faultTime >= queryParams.value.startDate)
|
|
|
- }
|
|
|
- if (queryParams.value.endDate) {
|
|
|
- result = result.filter(o => o.faultTime <= queryParams.value.endDate + ' 23:59:59')
|
|
|
- }
|
|
|
- // 时间倒序排列
|
|
|
- return result.sort((a, b) => new Date(b.faultTime) - new Date(a.faultTime))
|
|
|
-})
|
|
|
+import { ref, reactive, computed, watch, onMounted } from 'vue'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import ImagePreview from '@/components/ImagePreview'
|
|
|
+import { listWorkOrder, getWorkOrderDetail, addWorkOrder, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, cancelWorkOrder } from '@/api/work/order'
|
|
|
+import { getEquipmentFullPage } from '@/api/pipeNetwork/basic'
|
|
|
+import { listDept } from '@/api/system/dept'
|
|
|
+import { listUser } from '@/api/system/user'
|
|
|
+import { handleTree } from '@/utils/ruoyi'
|
|
|
+import { ORDER_STATUS_MAP, ORDER_LEVEL_MAP, orderStatusText, orderStatusType, orderTypeText, orderLevelText, orderLevelType, orderLogText, orderLogType, canOperate, timeText } from './workOrderDict'
|
|
|
+
|
|
|
+// ========== 查询 & 列表(orderType=1 故障维修) ==========
|
|
|
+const listLoading = ref(false)
|
|
|
+const tableData = ref([])
|
|
|
+const pageNum = ref(1)
|
|
|
+const pageSize = ref(10)
|
|
|
+const total = ref(0)
|
|
|
+const queryFormRef = ref(null)
|
|
|
+const queryParams = reactive({ orderLevel: null, orderStatus: null, deviceCode: '', keyword: '' })
|
|
|
+
|
|
|
+async function handleSearch() {
|
|
|
+ listLoading.value = true
|
|
|
+ try {
|
|
|
+ const params = { orderType: 1 }
|
|
|
+ if (queryParams.orderLevel) params.orderLevel = queryParams.orderLevel
|
|
|
+ if (queryParams.orderStatus) params.orderStatus = queryParams.orderStatus
|
|
|
+ if (queryParams.deviceCode) params.deviceCode = queryParams.deviceCode
|
|
|
+ if (queryParams.keyword) params.keyword = queryParams.keyword
|
|
|
+ const res = await listWorkOrder(params, pageNum.value, pageSize.value)
|
|
|
+ if (res.data) {
|
|
|
+ tableData.value = res.data.records || []
|
|
|
+ total.value = Number(res.data.total || 0)
|
|
|
+ }
|
|
|
+ } catch(e) { ElMessage.error('查询失败'); console.error(e) }
|
|
|
+ finally { listLoading.value = false }
|
|
|
+}
|
|
|
+
|
|
|
+function handleReset() {
|
|
|
+ queryFormRef.value && queryFormRef.value.resetFields()
|
|
|
+ queryParams.orderLevel = null
|
|
|
+ queryParams.orderStatus = null
|
|
|
+ queryParams.deviceCode = ''
|
|
|
+ queryParams.keyword = ''
|
|
|
+ pageNum.value = 1
|
|
|
+ selectedOrderId.value = null
|
|
|
+ selectedOrder.value = {}
|
|
|
+ handleSearch()
|
|
|
+}
|
|
|
|
|
|
-const paginatedOrders = computed(() => {
|
|
|
- const start = (currentPage.value - 1) * pageSize
|
|
|
- return filteredOrders.value.slice(start, start + pageSize)
|
|
|
+// ========== 选中工单 & 详情(合并 res.data.order + logs) ==========
|
|
|
+const selectedOrderId = ref(null)
|
|
|
+const selectedOrder = ref({})
|
|
|
+const activeTab = ref('info')
|
|
|
+const currentOrderLogs = ref([])
|
|
|
+const detailLoading = ref(false)
|
|
|
+const detailDevice = ref(null)
|
|
|
+
|
|
|
+const workImageList = computed(() => {
|
|
|
+ if (!selectedOrder.value.workImage) return []
|
|
|
+ return selectedOrder.value.workImage.split(',').filter(img => img && img.trim())
|
|
|
})
|
|
|
|
|
|
-const totalPages = computed(() => Math.ceil(filteredOrders.value.length / pageSize))
|
|
|
+async function handleSelectOrder(order) {
|
|
|
+ selectedOrderId.value = order.orderId
|
|
|
+ selectedOrder.value = { ...order }
|
|
|
+ activeTab.value = 'info'
|
|
|
+ currentOrderLogs.value = []
|
|
|
+ detailDevice.value = null
|
|
|
+ await loadOrderDetail(order.orderId)
|
|
|
+}
|
|
|
|
|
|
-// 统计数据
|
|
|
-const pendingCount = computed(() => orders.value.filter(o => o.status === 'pending').length)
|
|
|
-const processingCount = computed(() => orders.value.filter(o => o.status === 'processing').length)
|
|
|
-const completedCount = computed(() => orders.value.filter(o => o.status === 'completed').length)
|
|
|
+async function loadOrderDetail(orderId) {
|
|
|
+ detailLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getWorkOrderDetail(orderId)
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ selectedOrder.value = { ...selectedOrder.value, ...res.data.order }
|
|
|
+ currentOrderLogs.value = res.data.logs || []
|
|
|
+ if (res.data.order?.deviceCode) {
|
|
|
+ loadDeviceInfo(res.data.order.deviceCode)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch(e) { ElMessage.error('加载工单详情失败'); console.error(e) }
|
|
|
+ finally { detailLoading.value = false }
|
|
|
+}
|
|
|
|
|
|
-// ==================== 辅助函数 ====================
|
|
|
-const getStatusText = (status) => {
|
|
|
- const map = { pending: '待处理', processing: '处理中', completed: '已完成' }
|
|
|
- return map[status] || status
|
|
|
+async function loadDeviceInfo(codeOrId) {
|
|
|
+ try {
|
|
|
+ const res = await getEquipmentFullPage(1, 1, { equipmentCode: codeOrId })
|
|
|
+ const rows = res.data?.rows || res.data?.records || []
|
|
|
+ if (rows.length) { detailDevice.value = rows[0]; return }
|
|
|
+ const res2 = await getEquipmentFullPage(1, 1, { equipmentId: codeOrId })
|
|
|
+ const rows2 = res2.data?.rows || res2.data?.records || []
|
|
|
+ if (rows2.length) detailDevice.value = rows2[0]
|
|
|
+ } catch(e) { console.warn('加载设备信息失败', e) }
|
|
|
}
|
|
|
|
|
|
-const getStatusClass = (status) => {
|
|
|
- const map = { pending: 'status-pending', processing: 'status-processing', completed: 'status-completed' }
|
|
|
- return map[status] || ''
|
|
|
+function handleSizeChange() {
|
|
|
+ pageNum.value = 1
|
|
|
+ selectedOrderId.value = null
|
|
|
+ selectedOrder.value = {}
|
|
|
+ handleSearch()
|
|
|
}
|
|
|
|
|
|
-// 生成工单编号
|
|
|
-const generateOrderNo = () => {
|
|
|
- const today = new Date()
|
|
|
- const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '')
|
|
|
- const count = orders.value.length + 1
|
|
|
- return `WO-${dateStr}-${String(count).padStart(3, '0')}`
|
|
|
+function handlePageChange() {
|
|
|
+ selectedOrderId.value = null
|
|
|
+ selectedOrder.value = {}
|
|
|
+ handleSearch()
|
|
|
}
|
|
|
|
|
|
-// ==================== 自动生成故障工单 ====================
|
|
|
-let faultInterval = null
|
|
|
-let faultIdCounter = 200
|
|
|
-
|
|
|
-// 模拟接收设备故障并自动生成工单
|
|
|
-const simulateFaultReceive = () => {
|
|
|
- if (Math.random() < 0.02) { // 约5%概率触发新故障
|
|
|
- const faultsList = [
|
|
|
- { deviceType: '压力传感器', deviceId: 'PS-008', location: '北环加压站', manufacturer: '华仪科技', message: '压力波动异常,超出预警范围' },
|
|
|
- { deviceType: '温度传感器', deviceId: 'TP-010', location: '城西换热站', manufacturer: '北方仪表', message: '温度超过85℃警戒线' },
|
|
|
- { deviceType: '泄漏检测仪', deviceId: 'LD-012', location: '南站调压站', manufacturer: '安防科技', message: '可燃气浓度异常波动' }
|
|
|
- ]
|
|
|
- const randomFault = faultsList[Math.floor(Math.random() * faultsList.length)]
|
|
|
- const now = new Date()
|
|
|
- const timeStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`
|
|
|
- const timeDisplay = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`
|
|
|
-
|
|
|
- // 添加到实时监控
|
|
|
- const newFault = {
|
|
|
- id: faultIdCounter++,
|
|
|
- time: timeDisplay,
|
|
|
- deviceName: `${randomFault.deviceType} ${randomFault.deviceId}`,
|
|
|
- message: randomFault.message,
|
|
|
- orderCreated: false,
|
|
|
- isNew: true
|
|
|
+// ========== 用户映射(接单人 ID → 姓名) ==========
|
|
|
+const userMap = ref({})
|
|
|
+function userNames(ids) {
|
|
|
+ if (!ids) return ''
|
|
|
+ return String(ids).split(',').map(id => userMap.value[id] || id).join('、')
|
|
|
+}
|
|
|
+async function loadUsers() {
|
|
|
+ try {
|
|
|
+ const res = await listUser({ pageNum: 1, pageSize: 200 })
|
|
|
+ if (res.rows) {
|
|
|
+ userMap.value = Object.fromEntries(res.rows.map(u => [u.userId, u.nickName || u.userName]))
|
|
|
}
|
|
|
- recentFaults.value.unshift(newFault)
|
|
|
- if (recentFaults.value.length > 10) recentFaults.value.pop()
|
|
|
-
|
|
|
- // 自动生成工单
|
|
|
- setTimeout(() => {
|
|
|
- createFaultOrder(randomFault.deviceType, randomFault.deviceId, randomFault.location, randomFault.manufacturer, randomFault.message, timeStr)
|
|
|
- newFault.orderCreated = true
|
|
|
- newFault.isNew = false
|
|
|
- }, 1500)
|
|
|
- }
|
|
|
+ } catch(e) { console.warn('加载用户列表失败', e) }
|
|
|
}
|
|
|
|
|
|
-// 创建故障工单并自动派发
|
|
|
-const createFaultOrder = (deviceType, deviceId, location, manufacturer, message, faultTime) => {
|
|
|
- // 根据设备类型自动分配维护人员
|
|
|
- const maintainerMap = {
|
|
|
- '压力传感器': '张工',
|
|
|
- '温度传感器': '李工',
|
|
|
- '泄漏检测仪': '王工',
|
|
|
- '流量传感器': '张工',
|
|
|
- '水位监测仪': '赵工'
|
|
|
- }
|
|
|
- const maintainer = maintainerMap[deviceType] || '张工'
|
|
|
- const contactMap = { '张工': '13800001111', '李工': '13800002222', '王工': '13800003333', '赵工': '13800004444' }
|
|
|
-
|
|
|
- const newOrder = {
|
|
|
- id: orders.value.length + 1,
|
|
|
- orderNo: generateOrderNo(),
|
|
|
- deviceType: deviceType,
|
|
|
- deviceId: deviceId,
|
|
|
- location: location,
|
|
|
- manufacturer: manufacturer,
|
|
|
- maintainer: maintainer,
|
|
|
- contact: contactMap[maintainer],
|
|
|
- faultTime: faultTime,
|
|
|
- description: message,
|
|
|
- createTime: new Date().toLocaleString(),
|
|
|
- status: 'pending',
|
|
|
- startTime: null,
|
|
|
- completeTime: null,
|
|
|
- processResult: null
|
|
|
+// ========== 新增工单(需求 5.2.4.4.1) ==========
|
|
|
+const addVisible = ref(false)
|
|
|
+const addFormRef = ref(null)
|
|
|
+const addForm = reactive({ deviceId: '', deviceCode: '', orderLevel: 2, faultTime: '', planFinishTime: '', orderDesc: '' })
|
|
|
+const addRules = {
|
|
|
+ orderLevel: [{ required: true, message: '请选择优先级', trigger: 'change' }],
|
|
|
+ orderDesc: [{ required: true, message: '请填写故障描述', trigger: 'blur' }]
|
|
|
+}
|
|
|
+const deviceOptions = ref([])
|
|
|
+const deviceLoading = ref(false)
|
|
|
+const deviceInfo = ref(null)
|
|
|
+const submitting = ref(false)
|
|
|
+
|
|
|
+function openAddDialog() {
|
|
|
+ addForm.deviceId = ''; addForm.deviceCode = ''; addForm.orderLevel = 2
|
|
|
+ addForm.faultTime = ''; addForm.planFinishTime = ''; addForm.orderDesc = ''
|
|
|
+ deviceOptions.value = []
|
|
|
+ deviceInfo.value = null
|
|
|
+ addVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+async function searchDevice(query) {
|
|
|
+ if (!query) { deviceOptions.value = []; return }
|
|
|
+ deviceLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getEquipmentFullPage(1, 20, { equipmentCode: query })
|
|
|
+ // 后端返回 MyBatis-Plus Page(records),兼容 rows 命名
|
|
|
+ if (res.data) deviceOptions.value = res.data.records || res.data.rows || []
|
|
|
+ } catch(e) { console.warn('设备搜索失败', e) }
|
|
|
+ finally { deviceLoading.value = false }
|
|
|
+}
|
|
|
+
|
|
|
+// 选中设备后:带出设备信息卡 + 自动填充设备编码
|
|
|
+function onDeviceChange(id) {
|
|
|
+ deviceInfo.value = null
|
|
|
+ if (!id) { addForm.deviceCode = ''; return }
|
|
|
+ // equipmentId 可能是数字/字符串,统一转字符串比较
|
|
|
+ const dev = deviceOptions.value.find(d => String(d.equipmentId) === String(id))
|
|
|
+ if (dev) {
|
|
|
+ deviceInfo.value = dev
|
|
|
+ addForm.deviceCode = dev.equipmentCode || ''
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- orders.value.unshift(newOrder)
|
|
|
- console.log(`自动生成工单 ${newOrder.orderNo} 并派发给 ${maintainer}`)
|
|
|
+async function submitAdd() {
|
|
|
+ await addFormRef.value.validate().catch(() => { throw new Error('校验失败') })
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const data = { orderType: 1, orderLevel: addForm.orderLevel, orderDesc: addForm.orderDesc }
|
|
|
+ if (addForm.deviceId) data.deviceId = addForm.deviceId
|
|
|
+ if (addForm.deviceCode) data.deviceCode = addForm.deviceCode
|
|
|
+ if (addForm.planFinishTime) data.planFinishTime = addForm.planFinishTime
|
|
|
+ // 故障时间:手动指定则作为工单创建时间(列表"故障时间"列展示 createTime)
|
|
|
+ if (addForm.faultTime) data.createTime = addForm.faultTime
|
|
|
+ const res = await addWorkOrder(data)
|
|
|
+ ElMessage.success('工单创建成功')
|
|
|
+ addVisible.value = false
|
|
|
+ handleSearch()
|
|
|
+ } catch(e) { ElMessage.error('创建失败:' + (e.msg || e.message || '请重试')); console.error(e) }
|
|
|
+ finally { submitting.value = false }
|
|
|
}
|
|
|
|
|
|
-// ==================== 手动新增工单 ====================
|
|
|
-const showAddForm = ref(false)
|
|
|
-const newOrder = ref({
|
|
|
- deviceType: '',
|
|
|
- deviceId: '',
|
|
|
- location: '',
|
|
|
- manufacturer: '',
|
|
|
- maintainer: '',
|
|
|
- contact: '',
|
|
|
- faultTime: '',
|
|
|
- description: ''
|
|
|
-})
|
|
|
+// ========== 派单(部门树 + 用户多选,参考排水/燃气工单) ==========
|
|
|
+const dispatchDialogVisible = ref(false)
|
|
|
+const dispatchForm = reactive({ handlerIds: [], remarks: '' })
|
|
|
+const deptTreeRef = ref(null)
|
|
|
+const userTableRef = ref(null)
|
|
|
+const deptTreeData = ref([])
|
|
|
+const deptFilterText = ref('')
|
|
|
+const userList = ref([])
|
|
|
+const selectedUserList = ref([])
|
|
|
+
|
|
|
+watch(deptFilterText, (val) => { deptTreeRef.value && deptTreeRef.value.filter(val) })
|
|
|
+function filterDeptNode(value, data) {
|
|
|
+ if (!value) return true
|
|
|
+ return data.deptName.includes(value)
|
|
|
+}
|
|
|
|
|
|
-const openAddModal = () => {
|
|
|
- showAddForm.value = true
|
|
|
- newOrder.value = {
|
|
|
- deviceType: '',
|
|
|
- deviceId: '',
|
|
|
- location: '',
|
|
|
- manufacturer: '',
|
|
|
- maintainer: '',
|
|
|
- contact: '',
|
|
|
- faultTime: new Date().toISOString().slice(0, 16),
|
|
|
- description: ''
|
|
|
- }
|
|
|
+async function loadDeptTree() {
|
|
|
+ if (deptTreeData.value.length > 0) return
|
|
|
+ try {
|
|
|
+ const res = await listDept()
|
|
|
+ deptTreeData.value = handleTree(res.data || [], 'deptId')
|
|
|
+ } catch(e) { console.warn('加载部门失败', e) }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDeptClick(node) {
|
|
|
+ try {
|
|
|
+ userTableRef.value && userTableRef.value.clearSelection()
|
|
|
+ selectedUserList.value = []
|
|
|
+ dispatchForm.handlerIds = []
|
|
|
+ const res = await listUser({ deptId: node.deptId, pageNum: 1, pageSize: 100 })
|
|
|
+ userList.value = res.rows || []
|
|
|
+ } catch(e) { console.warn('加载部门用户失败', e) }
|
|
|
+}
|
|
|
+
|
|
|
+function handleSelectionChange(selection) {
|
|
|
+ selectedUserList.value = selection
|
|
|
+ dispatchForm.handlerIds = selection.map(item => item.userId)
|
|
|
}
|
|
|
|
|
|
-const cancelAdd = () => {
|
|
|
- showAddForm.value = false
|
|
|
+function handleRowClick(row) {
|
|
|
+ userTableRef.value && userTableRef.value.toggleRowSelection(row)
|
|
|
}
|
|
|
|
|
|
-const submitManualOrder = () => {
|
|
|
- if (!newOrder.value.deviceType || !newOrder.value.deviceId || !newOrder.value.location || !newOrder.value.maintainer || !newOrder.value.faultTime || !newOrder.value.description) {
|
|
|
- alert('请填写所有必填项')
|
|
|
+const selectedUserNames = computed(() => selectedUserList.value.map(item => item.nickName || item.userName))
|
|
|
+
|
|
|
+function handleDispatch() {
|
|
|
+ if (!canOperate.dispatch(selectedOrder.value)) {
|
|
|
+ ElMessage.warning('仅可对待派单工单进行派单操作')
|
|
|
return
|
|
|
}
|
|
|
+ Object.assign(dispatchForm, { handlerIds: [], remarks: '' })
|
|
|
+ selectedUserList.value = []
|
|
|
+ userList.value = []
|
|
|
+ loadDeptTree()
|
|
|
+ dispatchDialogVisible.value = true
|
|
|
+}
|
|
|
|
|
|
- const contactMap = { '张工': '13800001111', '李工': '13800002222', '王工': '13800003333', '赵工': '13800004444' }
|
|
|
- const faultTimeFormatted = newOrder.value.faultTime.replace('T', ' ')
|
|
|
-
|
|
|
- const newOrderData = {
|
|
|
- id: orders.value.length + 1,
|
|
|
- orderNo: generateOrderNo(),
|
|
|
- deviceType: newOrder.value.deviceType,
|
|
|
- deviceId: newOrder.value.deviceId,
|
|
|
- location: newOrder.value.location,
|
|
|
- manufacturer: newOrder.value.manufacturer || '',
|
|
|
- maintainer: newOrder.value.maintainer,
|
|
|
- contact: newOrder.value.contact || contactMap[newOrder.value.maintainer] || '',
|
|
|
- faultTime: faultTimeFormatted,
|
|
|
- description: newOrder.value.description,
|
|
|
- createTime: new Date().toLocaleString(),
|
|
|
- status: 'pending',
|
|
|
- startTime: null,
|
|
|
- completeTime: null,
|
|
|
- processResult: null
|
|
|
+async function submitDispatch() {
|
|
|
+ if (!dispatchForm.handlerIds || dispatchForm.handlerIds.length === 0) {
|
|
|
+ ElMessage.warning('请选择至少一个接单人')
|
|
|
+ return
|
|
|
}
|
|
|
-
|
|
|
- orders.value.unshift(newOrderData)
|
|
|
- alert(`工单 ${newOrderData.orderNo} 已创建并派发给 ${newOrder.value.maintainer}`)
|
|
|
- showAddForm.value = false
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ await dispatchWorkOrder({
|
|
|
+ orderId: selectedOrder.value.orderId,
|
|
|
+ handlerIds: dispatchForm.handlerIds,
|
|
|
+ remarks: dispatchForm.remarks
|
|
|
+ })
|
|
|
+ ElMessage.success('派单成功!已向 ' + dispatchForm.handlerIds.length + ' 人下发任务')
|
|
|
+ dispatchDialogVisible.value = false
|
|
|
+ refreshAfterAction()
|
|
|
+ } catch(e) { ElMessage.error('派单失败:' + (e.msg || e.message || '请重试')); console.error(e) }
|
|
|
+ finally { submitting.value = false }
|
|
|
}
|
|
|
|
|
|
-// ==================== 工单操作 ====================
|
|
|
-const detailModalVisible = ref(false)
|
|
|
-const completeModalVisible = ref(false)
|
|
|
-const currentOrder = ref(null)
|
|
|
-const processResult = ref('')
|
|
|
+// ========== 核查/处理(参考排水/燃气工单) ==========
|
|
|
+const actionDialogVisible = ref(false)
|
|
|
+const actionTitle = ref('')
|
|
|
+const actionFormData = reactive({ solution: '' })
|
|
|
+
|
|
|
+function handleVerify() {
|
|
|
+ if (!canOperate.verify(selectedOrder.value)) {
|
|
|
+ ElMessage.warning('当前工单不允许核查')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ actionTitle.value = '核查工单'
|
|
|
+ actionFormData.solution = ''
|
|
|
+ actionDialogVisible.value = true
|
|
|
+}
|
|
|
|
|
|
-const viewOrderDetail = (order) => {
|
|
|
- currentOrder.value = order
|
|
|
- detailModalVisible.value = true
|
|
|
+function handleProcess() {
|
|
|
+ if (!canOperate.process(selectedOrder.value)) {
|
|
|
+ ElMessage.warning('当前工单不允许处理')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ actionTitle.value = '处理工单'
|
|
|
+ actionFormData.solution = ''
|
|
|
+ actionDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
-const closeDetailModal = () => {
|
|
|
- detailModalVisible.value = false
|
|
|
- currentOrder.value = null
|
|
|
+async function submitAction() {
|
|
|
+ if (!actionFormData.solution.trim()) {
|
|
|
+ ElMessage.warning('请填写说明')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ const data = {
|
|
|
+ orderId: selectedOrder.value.orderId,
|
|
|
+ handleResult: 'solved',
|
|
|
+ solution: actionFormData.solution
|
|
|
+ }
|
|
|
+ if (actionTitle.value === '核查工单') {
|
|
|
+ await verifyWorkOrder(data)
|
|
|
+ } else {
|
|
|
+ await processWorkOrder(data)
|
|
|
+ }
|
|
|
+ ElMessage.success(actionTitle.value + '成功')
|
|
|
+ actionDialogVisible.value = false
|
|
|
+ refreshAfterAction()
|
|
|
+ } catch(e) { ElMessage.error('操作失败:' + (e.msg || e.message || '请重试')); console.error(e) }
|
|
|
+ finally { submitting.value = false }
|
|
|
}
|
|
|
|
|
|
-const updateStatus = (order, status) => {
|
|
|
- order.status = status
|
|
|
- if (status === 'processing') {
|
|
|
- order.startTime = new Date().toLocaleString()
|
|
|
+// ========== 提交验收(后端 completeOrder 要求状态 4) ==========
|
|
|
+const completeDialogVisible = ref(false)
|
|
|
+const completeForm = reactive({ result: '' })
|
|
|
+
|
|
|
+function handleComplete() {
|
|
|
+ if (!canOperate.complete(selectedOrder.value)) {
|
|
|
+ ElMessage.warning('当前工单不允许提交验收')
|
|
|
+ return
|
|
|
}
|
|
|
- alert(`工单 ${order.orderNo} 状态已更新为${getStatusText(status)}`)
|
|
|
+ completeForm.result = ''
|
|
|
+ completeDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
-const completeOrder = (order) => {
|
|
|
- currentOrder.value = order
|
|
|
- processResult.value = ''
|
|
|
- completeModalVisible.value = true
|
|
|
+async function submitComplete() {
|
|
|
+ if (!completeForm.result.trim()) {
|
|
|
+ ElMessage.warning('请填写验收意见')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ await completeWorkOrder(selectedOrder.value.orderId, completeForm.result)
|
|
|
+ ElMessage.success('提交验收成功')
|
|
|
+ completeDialogVisible.value = false
|
|
|
+ refreshAfterAction()
|
|
|
+ } catch(e) { ElMessage.error('提交失败:' + (e.msg || e.message || '请重试')); console.error(e) }
|
|
|
+ finally { submitting.value = false }
|
|
|
}
|
|
|
|
|
|
-const closeCompleteModal = () => {
|
|
|
- completeModalVisible.value = false
|
|
|
- currentOrder.value = null
|
|
|
- processResult.value = ''
|
|
|
+// ========== 验收通过/结案(参考排水/燃气工单) ==========
|
|
|
+const closeDialogVisible = ref(false)
|
|
|
+const closeForm = reactive({ result: '' })
|
|
|
+
|
|
|
+function handleClose() {
|
|
|
+ if (!canOperate.close(selectedOrder.value)) {
|
|
|
+ ElMessage.warning('当前工单不允许验收通过')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ closeForm.result = ''
|
|
|
+ closeDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
-const submitComplete = () => {
|
|
|
- if (currentOrder.value) {
|
|
|
- currentOrder.value.status = 'completed'
|
|
|
- currentOrder.value.completeTime = new Date().toLocaleString()
|
|
|
- currentOrder.value.processResult = processResult.value || '处理完成'
|
|
|
- alert(`工单 ${currentOrder.value.orderNo} 已完成`)
|
|
|
- closeCompleteModal()
|
|
|
+async function submitClose() {
|
|
|
+ if (!closeForm.result.trim()) {
|
|
|
+ ElMessage.warning('请填写验收结果')
|
|
|
+ return
|
|
|
}
|
|
|
+ submitting.value = true
|
|
|
+ try {
|
|
|
+ await closeWorkOrder({
|
|
|
+ orderId: selectedOrder.value.orderId,
|
|
|
+ result: closeForm.result
|
|
|
+ })
|
|
|
+ ElMessage.success('验收通过,工单已结案')
|
|
|
+ closeDialogVisible.value = false
|
|
|
+ refreshAfterAction()
|
|
|
+ } catch(e) { ElMessage.error('验收失败:' + (e.msg || e.message || '请重试')); console.error(e) }
|
|
|
+ finally { submitting.value = false }
|
|
|
}
|
|
|
|
|
|
-// ==================== 查询相关 ====================
|
|
|
-const searchOrders = () => {
|
|
|
- currentPage.value = 1
|
|
|
+// ========== 取消工单 ==========
|
|
|
+function handleCancel() {
|
|
|
+ ElMessageBox.confirm(`确定取消工单「${selectedOrder.value.orderNo}」吗?`, '取消工单', { type: 'warning' })
|
|
|
+ .then(async () => {
|
|
|
+ try {
|
|
|
+ await cancelWorkOrder(selectedOrder.value.orderId)
|
|
|
+ ElMessage.success('工单已取消')
|
|
|
+ selectedOrderId.value = null
|
|
|
+ selectedOrder.value = {}
|
|
|
+ handleSearch()
|
|
|
+ } catch(e) { ElMessage.error('取消失败:' + (e.msg || e.message || '请重试')); console.error(e) }
|
|
|
+ }).catch(() => {})
|
|
|
}
|
|
|
|
|
|
-const resetQuery = () => {
|
|
|
- queryParams.value = { deviceType: '', deviceId: '', startDate: '', endDate: '' }
|
|
|
- currentPage.value = 1
|
|
|
+/** 操作完成后刷新列表并重新加载当前选中工单 */
|
|
|
+async function refreshAfterAction() {
|
|
|
+ await handleSearch()
|
|
|
+ const fresh = tableData.value.find(o => o.orderId === selectedOrderId.value)
|
|
|
+ if (fresh) {
|
|
|
+ await handleSelectOrder(fresh)
|
|
|
+ } else {
|
|
|
+ selectedOrderId.value = null
|
|
|
+ selectedOrder.value = {}
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// ==================== 生命周期 ====================
|
|
|
onMounted(() => {
|
|
|
- faultInterval = setInterval(simulateFaultReceive, 20000)
|
|
|
-})
|
|
|
-
|
|
|
-onUnmounted(() => {
|
|
|
- if (faultInterval) clearInterval(faultInterval)
|
|
|
+ loadUsers()
|
|
|
+ handleSearch()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
-<style scoped>
|
|
|
-* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
-
|
|
|
-.prototype-container {
|
|
|
- max-width: 1600px;
|
|
|
- margin: 0 auto;
|
|
|
- background: #f0f4f8;
|
|
|
- border-radius: 28px;
|
|
|
- box-shadow: 0 20px 35px -12px rgba(0, 0, 0, 0.15);
|
|
|
- overflow: hidden;
|
|
|
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
|
-}
|
|
|
+<style scoped lang="scss">
|
|
|
+.order-container {
|
|
|
+ padding: 0 16px 16px;
|
|
|
|
|
|
-/* 头部 */
|
|
|
-.header {
|
|
|
- background: #0a2b3c;
|
|
|
- padding: 16px 28px;
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 16px;
|
|
|
- border-bottom: 3px solid #e67e22;
|
|
|
-}
|
|
|
+ .search-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+
|
|
|
+ :deep(.el-card__body) {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-form {
|
|
|
+ .el-form {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+
|
|
|
+ .el-form-item {
|
|
|
+ margin-bottom: 0;
|
|
|
+ min-width: 150px;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-left: auto;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .main-row {
|
|
|
+ height: calc(100vh - 200px);
|
|
|
+
|
|
|
+ .col-fill {
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .box-card {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ :deep(.el-card__body) {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .list-scroll {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .order-list {
|
|
|
+ padding-right: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .order-item {
|
|
|
+ padding: 16px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ }
|
|
|
|
|
|
-.logo h2 { color: white; font-weight: 600; font-size: 1.3rem; }
|
|
|
-.logo p { color: #bdd8e8; font-size: 0.7rem; }
|
|
|
-
|
|
|
-.header-stats { display: flex; gap: 12px; }
|
|
|
-.stat-card { background: rgba(255,255,255,0.15); padding: 6px 14px; border-radius: 40px; text-align: center; color: white; }
|
|
|
-.stat-card.pending { background: #f39c12; }
|
|
|
-.stat-card.processing { background: #3498db; }
|
|
|
-.stat-card.completed { background: #27ae60; }
|
|
|
-.stat-value { font-weight: 700; font-size: 1.2rem; margin-right: 6px; }
|
|
|
-.stat-label { font-size: 0.65rem; opacity: 0.9; }
|
|
|
-
|
|
|
-/* 布局 */
|
|
|
-.main-layout { display: flex; flex-wrap: wrap; }
|
|
|
-.left-panel { width: 380px; background: #f9fbfd; padding: 20px; }
|
|
|
-.right-panel { flex: 1; background: #ffffff; padding: 20px; min-width: 550px; }
|
|
|
-
|
|
|
-/* 卡片 */
|
|
|
-.card { background: white; border-radius: 20px; border: 1px solid #e6edf3; margin-bottom: 20px; overflow: hidden; }
|
|
|
-.card-header { background: #f9fafc; padding: 14px 18px; border-bottom: 1px solid #eef2f6; font-weight: 700; font-size: 0.85rem; color: #1d5a78; display: flex; justify-content: space-between; align-items: center; }
|
|
|
-.card-body { padding: 16px; }
|
|
|
-
|
|
|
-/* 实时故障监控 */
|
|
|
-.fault-monitor { max-height: 280px; overflow-y: auto; }
|
|
|
-.fault-item { background: #fef9e8; border-radius: 12px; padding: 10px; margin-bottom: 8px; border-left: 3px solid #e74c3c; }
|
|
|
-.fault-item.new { animation: highlight 1s ease; background: #fff3e0; }
|
|
|
-.fault-time { font-size: 0.6rem; color: #6f8ea8; font-family: monospace; }
|
|
|
-.fault-device { font-weight: 700; font-size: 0.75rem; margin: 4px 0; }
|
|
|
-.fault-message { font-size: 0.7rem; color: #e74c3c; }
|
|
|
-.fault-order-status { margin-top: 6px; font-size: 0.65rem; }
|
|
|
-.order-created { color: #27ae60; }
|
|
|
-.order-creating { color: #f39c12; }
|
|
|
-.empty-fault { text-align: center; padding: 30px; color: #8aaec2; }
|
|
|
-.live-badge { font-size: 0.65rem; background: #e74c3c; color: white; padding: 2px 10px; border-radius: 20px; animation: blink 1s infinite; }
|
|
|
-
|
|
|
-/* 手动新增表单 */
|
|
|
-.add-form { margin-top: 8px; }
|
|
|
-.empty-add { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px; color: #8aaec2; cursor: pointer; border: 1px dashed #cbdde6; border-radius: 16px; }
|
|
|
-.empty-add:hover { border-color: #e67e22; color: #e67e22; }
|
|
|
-.add-icon { font-size: 2rem; margin-bottom: 8px; }
|
|
|
-.form-row { display: flex; gap: 12px; margin-bottom: 12px; }
|
|
|
-.form-group { margin-bottom: 12px; }
|
|
|
-.form-group.half { flex: 1; }
|
|
|
-.form-label { font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; display: block; color: #2c5a72; }
|
|
|
-.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid #cbdde6; font-size: 0.75rem; }
|
|
|
-.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 16px; }
|
|
|
-.btn-primary, .btn-outline, .btn-sm { padding: 5px 12px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; }
|
|
|
-.btn-primary { background: #e67e22; border: none; color: white; }
|
|
|
-.btn-primary:hover { background: #d35400; }
|
|
|
-.btn-outline { background: white; border: 1px solid #e67e22; color: #e67e22; }
|
|
|
-
|
|
|
-/* 筛选栏 */
|
|
|
-.filter-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; align-items: center; }
|
|
|
-.filter-select, .filter-input { padding: 6px 12px; border-radius: 20px; border: 1px solid #cbdde6; font-size: 0.7rem; }
|
|
|
-.filter-input { width: 130px; }
|
|
|
-.filter-sep { font-size: 0.7rem; color: #6f8ea8; }
|
|
|
-.list-count { font-size: 0.65rem; font-weight: normal; color: #6f8ea8; }
|
|
|
-
|
|
|
-/* 表格 */
|
|
|
-.order-table-wrapper { overflow-x: auto; }
|
|
|
-.data-table { width: 100%; border-collapse: collapse; font-size: 0.7rem; }
|
|
|
-.data-table th { background: #f8fafc; padding: 10px 8px; text-align: left; font-weight: 600; color: #2c5a72; border-bottom: 1px solid #e2edf2; }
|
|
|
-.data-table td { padding: 10px 8px; border-bottom: 1px solid #f0f4f7; vertical-align: middle; }
|
|
|
-.order-no { font-family: monospace; color: #e67e22; }
|
|
|
-.time-cell { white-space: nowrap; font-family: monospace; }
|
|
|
-.desc-cell { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
-.action-cell { white-space: nowrap; display: flex; gap: 6px; }
|
|
|
-.empty-row { text-align: center; padding: 30px; color: #8aaec2; }
|
|
|
-
|
|
|
-/* 状态标签 */
|
|
|
-.status-badge { padding: 2px 10px; border-radius: 20px; font-size: 0.65rem; }
|
|
|
-.status-pending { background: #fef3c7; color: #f39c12; }
|
|
|
-.status-processing { background: #e3f2fd; color: #3498db; }
|
|
|
-.status-completed { background: #d5f5e3; color: #27ae60; }
|
|
|
-
|
|
|
-/* 操作按钮 */
|
|
|
-.btn-view { background: #95a5a6; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-.btn-start { background: #3498db; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-.btn-complete { background: #27ae60; color: white; border: none; padding: 4px 10px; border-radius: 20px; font-size: 0.65rem; cursor: pointer; }
|
|
|
-
|
|
|
-/* 分页 */
|
|
|
-.pagination { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
|
-.page-btn { background: white; border: 1px solid #cbdde6; padding: 4px 10px; border-radius: 20px; font-size: 0.7rem; cursor: pointer; }
|
|
|
-.page-btn.active { background: #e67e22; color: white; border-color: #e67e22; }
|
|
|
-
|
|
|
-/* 模态框 */
|
|
|
-.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
|
|
-.modal-content { background: white; border-radius: 24px; width: 480px; max-width: 90%; padding: 24px; max-height: 85vh; overflow-y: auto; }
|
|
|
-.detail-info { background: #f8fafc; border-radius: 16px; padding: 16px; margin-top: 8px; }
|
|
|
-.info-row { margin-bottom: 10px; font-size: 0.75rem; }
|
|
|
-.info-row .label { font-weight: 600; width: 100px; display: inline-block; color: #2c5a72; }
|
|
|
-.order-info-summary { background: #f8fafc; border-radius: 12px; padding: 12px; margin-bottom: 16px; font-size: 0.7rem; }
|
|
|
-
|
|
|
-@keyframes highlight { 0% { background: #fff3e0; } 100% { background: #fef9e8; } }
|
|
|
-@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
|
-</style>
|
|
|
+ &.active {
|
|
|
+ background-color: #ecf5ff;
|
|
|
+ border-color: #409eff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .order-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+
|
|
|
+ .order-no {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-badge {
|
|
|
+ min-width: 60px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .order-body {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+
|
|
|
+ .order-tags {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .order-desc {
|
|
|
+ margin-top: 4px;
|
|
|
+ color: #303133;
|
|
|
+ font-size: 13px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-code, .create-time {
|
|
|
+ margin-top: 4px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination-wrapper {
|
|
|
+ padding: 16px 0;
|
|
|
+ border-top: 1px solid #e4e7ed;
|
|
|
+ margin-top: 8px;
|
|
|
+
|
|
|
+ .pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-scroll {
|
|
|
+ max-height: calc(100vh - 340px);
|
|
|
+ overflow-y: auto;
|
|
|
+ padding-right: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-section, .image-section, .action-section {
|
|
|
+ margin-top: 24px;
|
|
|
+ padding-top: 24px;
|
|
|
+ border-top: 1px solid #e4e7ed;
|
|
|
+
|
|
|
+ .section-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-list {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .action-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ min-width: 100px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 新增工单:设备信息卡(需求 5.2.4.4.1) */
|
|
|
+ .device-info-card {
|
|
|
+ width: 100%;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 12px;
|
|
|
+ border: 1px solid #e4eaf1;
|
|
|
+
|
|
|
+ :deep(.el-descriptions__body) {
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .timeline-content {
|
|
|
+ .timestamp {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .operator-info {
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+
|
|
|
+ .operator-name {
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .oper-type {
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .oper-desc {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|