| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872 |
- <!--运维任务管理 - 窨井设备异常派发中心-->
- <template>
- <div class="terminal-dispatch">
- <!-- 页面头部 -->
- <div class="page-header">
- <div class="header-left">
- <el-icon><Warning /></el-icon>
- <span class="title">窨井设备异常派发中心</span>
- <el-tag type="danger" effect="dark">未处理异常 {{ abnormalCount }}</el-tag>
- </div>
- <div class="header-right">
- <el-input
- v-model="searchKeyword"
- placeholder="搜索设备编号/名称/位置"
- clearable
- :prefix-icon="Search"
- style="width: 240px"
- @keyup.enter="currentPage = 1"
- @clear="currentPage = 1"
- />
- <el-select v-model="deviceTypeFilter" placeholder="设备类型" clearable style="width: 130px" @change="currentPage = 1">
- <el-option label="供水窨井" value="5" />
- <el-option label="排水窨井" value="9" />
- <el-option label="燃气窨井" value="10" />
- </el-select>
- <el-button type="primary" :icon="Refresh" @click="loadDevices" :loading="loading">刷新</el-button>
- </div>
- </div>
- <!-- 统计卡片 -->
- <div class="stats-cards">
- <div class="stat-card critical">
- <div class="stat-value">{{ criticalCount }}</div>
- <div class="stat-label">严重异常</div>
- </div>
- <div class="stat-card warning">
- <div class="stat-value">{{ warningCount }}</div>
- <div class="stat-label">一般异常</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">{{ pendingCount }}</div>
- <div class="stat-label">待派发</div>
- </div>
- <div class="stat-card">
- <div class="stat-value">{{ dispatchedCount }}</div>
- <div class="stat-label">已派发</div>
- </div>
- </div>
- <!-- 批量操作栏 -->
- <div class="batch-bar" v-if="selectedRows.length > 0">
- <span>已选择 <strong>{{ selectedRows.length }}</strong> 个异常设备</span>
- <el-button type="primary" @click="batchDispatch">批量提交工单</el-button>
- <el-button @click="clearSelection">取消选择</el-button>
- </div>
- <!-- 主内容区:左侧异常设备列表 + 右侧详情 -->
- <div class="main-layout">
- <!-- 左侧异常设备列表 -->
- <div class="terminal-list-panel">
- <div class="list-header">
- <span>异常设备列表({{ filteredTerminals.length }} 台)</span>
- <el-checkbox v-model="selectAll" @change="handleSelectAll">全选</el-checkbox>
- </div>
- <div class="terminal-list" v-loading="loading">
- <div
- v-for="terminal in paginatedTerminals"
- :key="terminal.id"
- class="terminal-item"
- :class="{
- selected: selectedTerminal?.id === terminal.id,
- critical: terminal.abnormalLevel === 'critical',
- warning: terminal.abnormalLevel === 'warning'
- }"
- @click="selectTerminal(terminal)"
- >
- <el-checkbox
- :model-value="selectedRows.includes(terminal.id)"
- @change="(val) => toggleSelect(terminal.id, val)"
- class="select-checkbox"
- @click.stop
- />
- <div class="terminal-content">
- <div class="terminal-header">
- <span class="terminal-code">{{ terminal.code }}</span>
- <el-tag :type="terminal.abnormalLevel === 'critical' ? 'danger' : 'warning'" size="small">
- {{ terminal.abnormalLevel === 'critical' ? '严重' : '一般' }}
- </el-tag>
- </div>
- <div class="terminal-name">{{ terminal.name }}</div>
- <div class="terminal-location">
- <el-icon><Location /></el-icon> {{ terminal.locationShort }}
- </div>
- <div class="terminal-desc">{{ terminal.abnormalDesc }}</div>
- <div class="terminal-time">异常时间: {{ terminal.abnormalTime || '—' }}</div>
- <div class="terminal-footer">
- <el-tag v-if="terminal.workOrderSubmitted" type="success" size="small" effect="plain">已派发</el-tag>
- <el-tag v-else type="danger" size="small" effect="plain">待派发</el-tag>
- <span class="type-tag">{{ terminal.deviceTypeName }}</span>
- </div>
- </div>
- </div>
- <el-empty v-if="!loading && filteredTerminals.length === 0" description="暂无异常设备" :image-size="80" />
- </div>
- <div class="pagination-area" v-if="filteredTerminals.length > pageSize">
- <el-pagination
- small
- layout="prev, pager, next"
- :total="filteredTerminals.length"
- :page-size="pageSize"
- v-model:current-page="currentPage"
- />
- </div>
- </div>
- <!-- 右侧详情面板 -->
- <div class="detail-panel" v-if="selectedTerminal">
- <el-tabs v-model="activeTab" @tab-change="onTabChange">
- <!-- 设备信息 -->
- <el-tab-pane label="设备信息" name="info">
- <div class="info-section">
- <div class="section-title">
- <span><el-icon><Monitor /></el-icon> 设备基本信息</span>
- <el-button
- type="primary"
- size="small"
- @click="openDispatchDialog(selectedTerminal)"
- :disabled="selectedTerminal.workOrderSubmitted"
- >
- {{ selectedTerminal.workOrderSubmitted ? '已派发' : '提交工单' }}
- </el-button>
- </div>
- <el-descriptions :column="2" border>
- <el-descriptions-item label="设备编号">{{ selectedTerminal.code }}</el-descriptions-item>
- <el-descriptions-item label="设备名称">{{ selectedTerminal.name }}</el-descriptions-item>
- <el-descriptions-item label="设备类型">{{ selectedTerminal.deviceTypeName }}</el-descriptions-item>
- <el-descriptions-item label="设备位置">{{ selectedTerminal.location }}</el-descriptions-item>
- <el-descriptions-item label="异常级别">
- <el-tag :type="selectedTerminal.abnormalLevel === 'critical' ? 'danger' : 'warning'">
- {{ selectedTerminal.abnormalLevel === 'critical' ? '严重异常' : '一般异常' }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="派发状态">
- <el-tag :type="selectedTerminal.workOrderSubmitted ? 'success' : 'danger'">
- {{ selectedTerminal.workOrderSubmitted ? '已派发' : '待派发' }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="异常描述" :span="2">{{ selectedTerminal.abnormalDesc }}</el-descriptions-item>
- </el-descriptions>
- </div>
- <!-- 实时监测数据 -->
- <div class="info-section">
- <div class="section-title"><el-icon><DataAnalysis /></el-icon> 实时监测数据</div>
- <div class="monitor-grid">
- <div class="monitor-item" :class="{ alert: selectedTerminal.status.tilt }">
- <div class="monitor-label">倾斜角度</div>
- <div class="monitor-value">{{ selectedTerminal.tiltAngle }}°</div>
- <div class="monitor-extra">阈值: {{ selectedTerminal.threshold }}°</div>
- </div>
- <div class="monitor-item" :class="{ alert: selectedTerminal.status.waterInfiltration }">
- <div class="monitor-label">水浸状态</div>
- <div class="monitor-value">{{ selectedTerminal.status.waterInfiltration ? '报警' : '正常' }}</div>
- </div>
- <div class="monitor-item" :class="{ alert: selectedTerminal.status.waterLevel }">
- <div class="monitor-label">水位状态</div>
- <div class="monitor-value">{{ selectedTerminal.status.waterLevel ? '超标' : '正常' }}</div>
- </div>
- <div class="monitor-item" :class="{ alert: selectedTerminal.status.lowBattery }">
- <div class="monitor-label">电量</div>
- <div class="monitor-value">{{ selectedTerminal.battery }}%</div>
- </div>
- <div class="monitor-item" :class="{ alert: selectedTerminal.status.lowSignal }">
- <div class="monitor-label">信号强度</div>
- <div class="monitor-value">{{ selectedTerminal.signal }}%</div>
- </div>
- <div class="monitor-item">
- <div class="monitor-label">温度</div>
- <div class="monitor-value">{{ selectedTerminal.temperature || '—' }}°C</div>
- </div>
- </div>
- <div class="last-upload">最后上传时间: {{ selectedTerminal.lastTime || '—' }}</div>
- </div>
- <!-- 位置信息 -->
- <div class="location-section">
- <div class="section-title"><el-icon><MapLocation /></el-icon> 设备位置信息</div>
- <div class="location-info">
- <el-icon :size="20" color="#f56c6c"><Location /></el-icon>
- <span>{{ selectedTerminal.location }}</span>
- <span v-if="selectedTerminal.lng" class="coords">(经度: {{ selectedTerminal.lng }}, 纬度: {{ selectedTerminal.lat }})</span>
- </div>
- </div>
- </el-tab-pane>
- <!-- 处置记录 -->
- <el-tab-pane label="处置记录" name="history">
- <div class="history-section">
- <div class="section-title"><el-icon><Document /></el-icon> 工单处置记录</div>
- <div v-loading="loadingOrders">
- <el-timeline v-if="workOrders.length">
- <el-timeline-item
- v-for="order in workOrders"
- :key="order.orderId"
- :timestamp="order.createTime || ''"
- :type="Number(order.orderStatus) >= 6 ? 'success' : 'primary'"
- >
- <div class="record-content">
- <div class="record-row"><strong>工单编号:</strong> {{ order.orderNo || '—' }}</div>
- <div class="record-row"><strong>工单类型:</strong> {{ orderTypeText(order.orderType) }}</div>
- <div class="record-row"><strong>优先级:</strong>
- <el-tag :type="orderLevelTagType(order.orderLevel)" size="small">{{ priorityText(order.orderLevel) }}</el-tag>
- </div>
- <div class="record-row"><strong>状态:</strong>
- <el-tag :type="orderStatusTagType(order.orderStatus)" size="small">{{ orderStatusText(order.orderStatus) }}</el-tag>
- </div>
- <div class="record-row" v-if="order.orderDesc"><strong>描述:</strong> {{ order.orderDesc }}</div>
- </div>
- </el-timeline-item>
- </el-timeline>
- <el-empty v-else description="暂无工单处置记录" :image-size="80" />
- </div>
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- <!-- 未选中状态 -->
- <div v-else class="empty-panel">
- <el-empty description="请从左侧选择异常设备查看详情" :image-size="140" />
- </div>
- </div>
- <!-- 提交工单对话框 -->
- <el-dialog v-model="dispatchDialogVisible" title="提交工单" width="520px" @close="dispatchForm = { orderType: 1, orderLevel: 2, orderDesc: '', owner: '' }">
- <el-form :model="dispatchForm" label-width="100px">
- <el-form-item label="设备名称">
- <el-input :model-value="currentDispatchTerminal?.name" disabled />
- </el-form-item>
- <el-form-item label="异常描述">
- <el-input type="textarea" :model-value="currentDispatchTerminal?.abnormalDesc" disabled :rows="2" />
- </el-form-item>
- <el-form-item label="工单类型" required>
- <el-select v-model="dispatchForm.orderType" placeholder="选择工单类型" style="width: 100%">
- <el-option label="故障维修" :value="1" />
- <el-option label="日常巡检" :value="2" />
- <el-option label="设备保养" :value="3" />
- </el-select>
- </el-form-item>
- <el-form-item label="优先级" required>
- <el-select v-model="dispatchForm.orderLevel" placeholder="选择优先级" style="width: 100%">
- <el-option label="紧急" :value="1" />
- <el-option label="一般" :value="2" />
- <el-option label="低" :value="3" />
- </el-select>
- </el-form-item>
- <el-form-item label="问题描述" required>
- <el-input type="textarea" v-model="dispatchForm.orderDesc" placeholder="填写问题描述、维修要求等" :rows="3" />
- </el-form-item>
- <el-form-item label="负责人">
- <el-input v-model="dispatchForm.owner" placeholder="填写负责人姓名(选填)" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="dispatchDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmDispatch" :loading="submitting">确认提交</el-button>
- </template>
- </el-dialog>
- <!-- 批量提交工单对话框 -->
- <el-dialog v-model="batchDispatchDialogVisible" title="批量提交工单" width="450px">
- <p>将向以下 {{ batchDispatchIds.length }} 个异常设备提交工单:</p>
- <div class="batch-list">
- <el-tag v-for="id in batchDispatchIds.slice(0, 5)" :key="id" size="small" style="margin: 4px">
- {{ getTerminalCode(id) }}
- </el-tag>
- <span v-if="batchDispatchIds.length > 5">等{{ batchDispatchIds.length }}个设备</span>
- </div>
- <el-form style="margin-top: 16px" label-width="80px">
- <el-form-item label="工单类型">
- <el-select v-model="batchDispatchForm.orderType" style="width: 100%">
- <el-option label="故障维修" :value="1" />
- <el-option label="日常巡检" :value="2" />
- <el-option label="设备保养" :value="3" />
- </el-select>
- </el-form-item>
- <el-form-item label="优先级">
- <el-select v-model="batchDispatchForm.orderLevel" style="width: 100%">
- <el-option label="紧急" :value="1" />
- <el-option label="一般" :value="2" />
- <el-option label="低" :value="3" />
- </el-select>
- </el-form-item>
- <el-form-item label="问题描述">
- <el-input type="textarea" v-model="batchDispatchForm.orderDesc" :rows="2" placeholder="批量工单描述" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="batchDispatchDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmBatchDispatch" :loading="submitting">确认批量提交</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted } from 'vue'
- import { Warning, Search, Refresh, Location, Monitor, MapLocation, Document, DataAnalysis } from '@element-plus/icons-vue'
- import { ElMessage } from 'element-plus'
- import { getManholeAlarmMonitorList, addManholeWorkOrder, getManholeWorkOrderProcessingPage } from '@/api/pipeNetwork/basic'
- // ==================== 常量 ====================
- const TYPE_MAP = { '5': '供水窨井', '9': '排水窨井', '10': '燃气窨井' }
- const LOW_BATTERY_THRESHOLD = 15
- const LOW_SIGNAL_THRESHOLD = 20
- // ==================== 状态 ====================
- const loading = ref(false)
- const loadingOrders = ref(false)
- const submitting = ref(false)
- const devices = ref([])
- const searchKeyword = ref('')
- const deviceTypeFilter = ref('')
- const currentPage = ref(1)
- const pageSize = 8
- const selectedRows = ref([])
- const selectAll = ref(false)
- const selectedTerminal = ref(null)
- const activeTab = ref('info')
- const workOrders = ref([])
- const dispatchDialogVisible = ref(false)
- const currentDispatchTerminal = ref(null)
- const dispatchForm = ref({ orderType: 1, orderLevel: 2, orderDesc: '', owner: '' })
- const batchDispatchDialogVisible = ref(false)
- const batchDispatchIds = ref([])
- const batchDispatchForm = ref({ orderType: 1, orderLevel: 2, orderDesc: '' })
- // ==================== 数据加载 ====================
- async function loadDevices() {
- loading.value = true
- try {
- const keyword = searchKeyword.value?.trim()
- const res = await getManholeAlarmMonitorList({ keyword: keyword || undefined })
- const result = res.data || {}
- const allDevices = (result.devices || []).map(item => normalizeDevice(item))
- // 只保留有异常的设备
- devices.value = allDevices.filter(d => d.hasAlert)
- // 默认选中第一个
- if (devices.value.length && !selectedTerminal.value) {
- selectedTerminal.value = devices.value[0]
- } else if (devices.value.length === 0) {
- selectedTerminal.value = null
- }
- } catch (error) {
- devices.value = []
- const msg = error?.message || error?.msg || '未知错误'
- ElMessage.error('设备数据加载失败: ' + msg)
- } finally {
- loading.value = false
- }
- }
- async function loadWorkOrders(deviceId) {
- if (!deviceId) return
- loadingOrders.value = true
- try {
- const res = await getManholeWorkOrderProcessingPage({
- deviceId,
- pageNum: 1,
- pageSize: 20
- })
- workOrders.value = res.data?.records || res.rows || []
- } catch (error) {
- workOrders.value = []
- console.error('[operationTask] 工单记录加载失败:', error)
- } finally {
- loadingOrders.value = false
- }
- }
- // ==================== 设备数据规范化 ====================
- function normalizeDevice(raw) {
- const md = raw?.manholeData || {}
- const st = raw?.equipmentStatus || {}
- const typeId = raw?.equipmentTypeId || ''
- // 数值解析
- const tiltAngle = parseFloat(md.tiltAngle) || 0
- const angleThreshold = parseFloat(md.angleAlarmThreshold) || 15
- const battery = parseFloat(md.batteryLevel) || 0
- const signal = parseFloat(md.signalStrength) || 0
- const temperature = md.temperatureValue ? parseFloat(md.temperatureValue).toFixed(1) : null
- const isOnline = Number(st.onlineStatus) === 1
- // 异常判定
- const tilt = isOnline && tiltAngle > angleThreshold
- const waterInfiltration = isOnline && `${md.waterInfiltrationAlarmStatus}` === '1'
- const waterLevel = isOnline && `${md.waterLevelAlarmStatus}` === '1'
- const lowBattery = battery > 0 && battery <= LOW_BATTERY_THRESHOLD
- const lowSignal = signal > 0 && signal <= LOW_SIGNAL_THRESHOLD
- // 告警等级
- const isCritical = tilt || waterInfiltration || waterLevel
- const isWarning = !isCritical && (lowBattery || lowSignal)
- const hasAlert = isCritical || isWarning
- const alertLevel = isCritical ? 'critical' : (isWarning ? 'warning' : null)
- // 异常描述
- const parts = []
- if (tilt) parts.push(`倾斜角度${tiltAngle}°超过阈值${angleThreshold}°`)
- if (waterInfiltration) parts.push('水浸报警')
- if (waterLevel) parts.push('水位超标报警')
- if (lowBattery) parts.push(`电量仅剩${battery}%`)
- if (lowSignal) parts.push(`信号强度${signal}%`)
- // 推荐工单类型
- let suggestedOrderType = 1
- if (lowBattery) suggestedOrderType = 3
- else if (lowSignal) suggestedOrderType = 2
- return {
- id: raw?.equipmentId || raw?.equipmentCode || '',
- code: raw?.equipmentCode || raw?.equipmentId || '',
- equipmentId: raw?.equipmentId || '',
- name: raw?.equipmentName || raw?.equipmentCode || '未命名设备',
- typeId,
- deviceTypeName: TYPE_MAP[typeId] || raw?.equipmentTypeName || '未知类型',
- location: raw?.equipmentLocation || '暂无位置信息',
- locationShort: (raw?.equipmentLocation || '暂无位置').slice(0, 20),
- lng: raw?.longitude ?? null,
- lat: raw?.latitude ?? null,
- battery,
- signal,
- temperature,
- tiltAngle,
- threshold: angleThreshold,
- status: { tilt, waterInfiltration, waterLevel, lowBattery, lowSignal },
- hasAlert,
- alertLevel,
- abnormalLevel: alertLevel,
- abnormalDesc: parts.join(';') || '设备状态正常',
- abnormalTime: md.uploadTime || md.createTime || '',
- lastTime: md.uploadTime || md.createTime || '',
- workOrderSubmitted: !!raw?.workOrderSubmitted,
- suggestedOrderType
- }
- }
- // ==================== 计算属性 ====================
- const filteredTerminals = computed(() => {
- let list = devices.value
- if (searchKeyword.value) {
- const kw = searchKeyword.value.toLowerCase()
- list = list.filter(t =>
- t.code.toLowerCase().includes(kw) ||
- t.name.toLowerCase().includes(kw) ||
- t.location.toLowerCase().includes(kw)
- )
- }
- if (deviceTypeFilter.value) {
- list = list.filter(t => t.typeId === deviceTypeFilter.value)
- }
- return list
- })
- const paginatedTerminals = computed(() => {
- const start = (currentPage.value - 1) * pageSize
- return filteredTerminals.value.slice(start, start + pageSize)
- })
- const abnormalCount = computed(() => devices.value.length)
- const criticalCount = computed(() => devices.value.filter(t => t.abnormalLevel === 'critical').length)
- const warningCount = computed(() => devices.value.filter(t => t.abnormalLevel === 'warning').length)
- const pendingCount = computed(() => devices.value.filter(t => !t.workOrderSubmitted).length)
- const dispatchedCount = computed(() => devices.value.filter(t => t.workOrderSubmitted).length)
- // ==================== 辅助函数 ====================
- const orderTypeText = (t) => ({ 1: '故障维修', 2: '日常巡检', 3: '设备保养' }[t] || '—')
- const priorityText = (l) => ({ 1: '紧急', 2: '一般', 3: '低' }[l] || '—')
- const orderStatusText = (s) => ({
- 1: '待派单', 2: '待接单', 3: '已接单', 4: '处理中',
- 5: '待验收', 6: '已完成', 7: '已关闭', 8: '延期审核', 9: '已归档'
- }[s] || '—')
- const orderLevelTagType = (l) => ({ 1: 'danger', 2: 'warning', 3: 'info' }[l] || 'info')
- const orderStatusTagType = (s) => {
- const n = Number(s)
- if (n >= 6) return 'success'
- if (n >= 3) return 'primary'
- if (n >= 1) return 'warning'
- return 'info'
- }
- // ==================== 选择操作 ====================
- const handleSelectAll = (val) => {
- if (val) {
- selectedRows.value = paginatedTerminals.value.map(t => t.id)
- } else {
- selectedRows.value = []
- }
- }
- const toggleSelect = (id, val) => {
- if (val) {
- if (!selectedRows.value.includes(id)) selectedRows.value.push(id)
- } else {
- selectedRows.value = selectedRows.value.filter(i => i !== id)
- }
- selectAll.value = paginatedTerminals.value.length > 0 &&
- paginatedTerminals.value.every(t => selectedRows.value.includes(t.id))
- }
- const clearSelection = () => {
- selectedRows.value = []
- selectAll.value = false
- }
- const selectTerminal = (terminal) => {
- selectedTerminal.value = terminal
- if (activeTab.value === 'history') {
- loadWorkOrders(terminal.equipmentId)
- }
- }
- const onTabChange = (tabName) => {
- if (tabName === 'history' && selectedTerminal.value) {
- loadWorkOrders(selectedTerminal.value.equipmentId)
- }
- }
- const getTerminalCode = (id) => {
- const terminal = devices.value.find(t => t.id === id)
- return terminal?.code || id
- }
- // ==================== 提交工单 ====================
- const openDispatchDialog = (terminal) => {
- currentDispatchTerminal.value = terminal
- dispatchForm.value = {
- orderType: terminal.suggestedOrderType || 1,
- orderLevel: terminal.abnormalLevel === 'critical' ? 1 : 2,
- orderDesc: terminal.abnormalDesc || '',
- owner: ''
- }
- dispatchDialogVisible.value = true
- }
- const confirmDispatch = async () => {
- if (!dispatchForm.value.orderDesc?.trim()) {
- ElMessage.warning('请填写问题描述')
- return
- }
- if (!currentDispatchTerminal.value?.equipmentId) {
- ElMessage.warning('设备信息缺失,无法提交工单')
- return
- }
- submitting.value = true
- try {
- const terminal = currentDispatchTerminal.value
- const desc = dispatchForm.value.owner
- ? `${dispatchForm.value.orderDesc}(负责人:${dispatchForm.value.owner})`
- : dispatchForm.value.orderDesc
- await addManholeWorkOrder({
- deviceId: terminal.equipmentId,
- orderType: dispatchForm.value.orderType,
- orderLevel: dispatchForm.value.orderLevel,
- orderDesc: desc
- })
- // 标记已派发
- terminal.workOrderSubmitted = true
- ElMessage.success('工单已提交,相关部门将即刻处理')
- dispatchDialogVisible.value = false
- // 如果在处置记录tab,刷新工单列表
- if (activeTab.value === 'history') {
- loadWorkOrders(terminal.equipmentId)
- }
- } catch (error) {
- const msg = error?.message || error?.msg || '未知错误'
- ElMessage.error('工单提交失败: ' + msg)
- } finally {
- submitting.value = false
- }
- }
- // ==================== 批量提交工单 ====================
- const batchDispatch = () => {
- if (selectedRows.value.length === 0) {
- ElMessage.warning('请先选择异常设备')
- return
- }
- batchDispatchIds.value = [...selectedRows.value]
- batchDispatchForm.value = { orderType: 1, orderLevel: 2, orderDesc: '' }
- batchDispatchDialogVisible.value = true
- }
- const confirmBatchDispatch = async () => {
- if (!batchDispatchForm.value.orderDesc?.trim()) {
- ElMessage.warning('请填写问题描述')
- return
- }
- submitting.value = true
- try {
- let successCount = 0
- let failCount = 0
- for (const id of batchDispatchIds.value) {
- const terminal = devices.value.find(t => t.id === id)
- if (!terminal || terminal.workOrderSubmitted) continue
- try {
- await addManholeWorkOrder({
- deviceId: terminal.equipmentId,
- orderType: batchDispatchForm.value.orderType,
- orderLevel: batchDispatchForm.value.orderLevel,
- orderDesc: batchDispatchForm.value.orderDesc
- })
- terminal.workOrderSubmitted = true
- successCount++
- } catch {
- failCount++
- }
- }
- if (successCount > 0) {
- ElMessage.success(`成功提交 ${successCount} 个工单${failCount > 0 ? `,${failCount}个失败` : ''}`)
- } else {
- ElMessage.error('批量提交工单全部失败')
- }
- batchDispatchDialogVisible.value = false
- clearSelection()
- } catch (error) {
- const msg = error?.message || error?.msg || '未知错误'
- ElMessage.error('批量提交失败: ' + msg)
- } finally {
- submitting.value = false
- }
- }
- // ==================== 生命周期 ====================
- onMounted(() => {
- loadDevices()
- })
- </script>
- <style scoped>
- .terminal-dispatch {
- background: #f0f2f6;
- min-height: 100vh;
- padding: 20px;
- }
- .page-header {
- background: white;
- border-radius: 16px;
- padding: 16px 24px;
- margin-bottom: 20px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 12px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
- }
- .header-left {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .header-left .title {
- font-size: 20px;
- font-weight: 600;
- color: #1f2d3d;
- }
- .header-right {
- display: flex;
- gap: 12px;
- align-items: center;
- flex-wrap: wrap;
- }
- .stats-cards {
- display: flex;
- gap: 16px;
- margin-bottom: 20px;
- flex-wrap: wrap;
- }
- .stat-card {
- background: white;
- border-radius: 12px;
- padding: 12px 24px;
- text-align: center;
- min-width: 110px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
- }
- .stat-card.critical .stat-value { color: #f56c6c; }
- .stat-card.warning .stat-value { color: #e6a23c; }
- .stat-value { font-size: 28px; font-weight: bold; }
- .stat-label { font-size: 13px; color: #606266; margin-top: 4px; }
- .batch-bar {
- background: #ecf5ff;
- border-radius: 12px;
- padding: 12px 20px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- gap: 16px;
- }
- .main-layout {
- display: flex;
- gap: 20px;
- }
- .terminal-list-panel {
- width: 420px;
- background: white;
- border-radius: 16px;
- 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;
- }
- .terminal-list {
- flex: 1;
- overflow-y: auto;
- padding: 12px;
- max-height: calc(100vh - 280px);
- }
- .terminal-item {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- padding: 12px;
- margin-bottom: 10px;
- border-radius: 12px;
- background: #fafbfc;
- border: 1px solid #e9ecef;
- cursor: pointer;
- transition: all 0.2s;
- }
- .terminal-item.selected {
- background: #ecf5ff;
- border-color: #409eff;
- }
- .terminal-item.critical { border-left: 3px solid #f56c6c; }
- .terminal-item.warning { border-left: 3px solid #e6a23c; }
- .select-checkbox { margin-top: 2px; }
- .terminal-content { flex: 1; }
- .terminal-header {
- display: flex;
- justify-content: space-between;
- margin-bottom: 4px;
- }
- .terminal-code { font-weight: 600; font-size: 14px; }
- .terminal-name { font-size: 13px; color: #303133; margin-bottom: 4px; }
- .terminal-location { font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 4px; margin-bottom: 4px; }
- .terminal-desc { font-size: 12px; color: #f56c6c; margin-bottom: 4px; }
- .terminal-time { font-size: 11px; color: #aaa; }
- .terminal-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 6px;
- }
- .type-tag {
- font-size: 11px;
- color: #909399;
- }
- .detail-panel {
- flex: 1;
- background: white;
- border-radius: 16px;
- padding: 20px;
- overflow-y: auto;
- max-height: calc(100vh - 160px);
- }
- .empty-panel {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- border-radius: 16px;
- }
- .section-title {
- font-weight: 600;
- font-size: 16px;
- margin: 16px 0 12px 0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .info-section { margin-bottom: 20px; }
- .monitor-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 12px;
- }
- .monitor-item {
- background: #f5f7fa;
- border-radius: 8px;
- padding: 12px;
- text-align: center;
- border: 1px solid #e9ecef;
- }
- .monitor-item.alert {
- background: #fef0f0;
- border-color: #f56c6c;
- }
- .monitor-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
- .monitor-value { font-size: 18px; font-weight: 600; color: #303133; }
- .monitor-item.alert .monitor-value { color: #f56c6c; }
- .monitor-extra { font-size: 11px; color: #aaa; margin-top: 2px; }
- .last-upload {
- margin-top: 10px;
- font-size: 12px;
- color: #909399;
- }
- .location-section { margin-bottom: 20px; }
- .location-info {
- display: flex;
- align-items: center;
- gap: 8px;
- background: #f5f7fa;
- border-radius: 8px;
- padding: 16px;
- font-size: 14px;
- color: #303133;
- }
- .location-info .coords { color: #909399; font-size: 12px; }
- .record-content { font-size: 13px; }
- .record-row { margin-bottom: 4px; }
- .pagination-area { padding: 12px; text-align: center; border-top: 1px solid #eee; }
- .batch-list { max-height: 150px; overflow-y: auto; }
- </style>
|