| src/api/environmentAccess/accessManagement.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/environmentAccess/deviceMonitor.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/environmentAccess/inspection.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/environmentAccess/reportCenter.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/environmentAccess/unattendedWeighbridgeSystem.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/accessManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/deviceMonitor/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/intelligentInspectionManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/reportCenter/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/unattendedWeighbridgeSystem/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/environmentAccess/accessManagement.js
@@ -1,15 +1,65 @@ import request from "@/utils/request"; export const listAccessGates = (params) => request({ url: "/environmentAccess/access/gate/page", method: "get", params }); export const addAccessGate = (data) => request({ url: "/environmentAccess/access/gate/add", method: "post", data }); export const updateAccessGate = (data) => request({ url: "/environmentAccess/access/gate/update", method: "put", data }); export const deleteAccessGates = (data) => request({ url: "/environmentAccess/access/gate/delete", method: "delete", data }); export const verifyAccess = (data) => request({ url: "/environmentAccess/access/verify", method: "post", data }); export const listAccessRecords = (params) => request({ url: "/environmentAccess/access/record/page", method: "get", params }); // ==================== é¨ç¦ç¹ ==================== export function listAccessGate(query) { return request({ url: "/environmentAccess/accessGatePage", method: "get", params: query }); } export function listAllAccessGate() { return request({ url: "/environmentAccess/accessGateList", method: "get" }); } export function addAccessGate(data) { return request({ url: "/environmentAccess/accessGateAdd", method: "post", data }); } export function updateAccessGate(data) { return request({ url: "/environmentAccess/accessGateUpdate", method: "put", data }); } export function delAccessGate(ids) { return request({ url: "/environmentAccess/accessGateDelete", method: "delete", data: ids }); } // 车è¾éè¡ææï¼è½¦è¾æ¡£æ¡ï¼ export const listAccessVehicles = (params) => request({ url: "/environmentAccess/access/vehicle/page", method: "get", params }); export const addAccessVehicle = (data) => request({ url: "/environmentAccess/access/vehicle/add", method: "post", data }); export const updateAccessVehicle = (data) => request({ url: "/environmentAccess/access/vehicle/update", method: "put", data }); export const deleteAccessVehicles = (data) => request({ url: "/environmentAccess/access/vehicle/delete", method: "delete", data }); // ==================== è½¦è¾ææ ==================== export function listAccessVehicle(query) { return request({ url: "/environmentAccess/accessVehiclePage", method: "get", params: query }); } export function addAccessVehicle(data) { return request({ url: "/environmentAccess/accessVehicleAdd", method: "post", data }); } export function updateAccessVehicle(data) { return request({ url: "/environmentAccess/accessVehicleUpdate", method: "put", data }); } export function delAccessVehicle(ids) { return request({ url: "/environmentAccess/accessVehicleDelete", method: "delete", data: ids }); } // ==================== äººåææ ==================== export function listAccessPerson(query) { return request({ url: "/environmentAccess/accessPersonPage", method: "get", params: query }); } export function addAccessPerson(data) { return request({ url: "/environmentAccess/accessPersonAdd", method: "post", data }); } export function updateAccessPerson(data) { return request({ url: "/environmentAccess/accessPersonUpdate", method: "put", data }); } export function delAccessPerson(ids) { return request({ url: "/environmentAccess/accessPersonDelete", method: "delete", data: ids }); } // ==================== è¯å«æ¾è¡ ==================== export function vehicleRecognize(params) { return request({ url: "/environmentAccess/accessVehicleRecognize", method: "post", params }); } export function personRecognize(params) { return request({ url: "/environmentAccess/accessPersonRecognize", method: "post", params }); } // ==================== éè¡è®°å½ ==================== export function listAccessRecord(query) { return request({ url: "/environmentAccess/accessRecordPage", method: "get", params: query }); } export function delAccessRecord(ids) { return request({ url: "/environmentAccess/accessRecordDelete", method: "delete", data: ids }); } export function getAccessOverview() { return request({ url: "/environmentAccess/accessOverview", method: "get" }); } src/api/environmentAccess/deviceMonitor.js
@@ -1,77 +1,69 @@ import request from "@/utils/request"; import { getToken } from "@/utils/auth"; export const listDeviceMonitor = () => request({ url: "/environmentAccess/deviceMonitor/list", method: "get" }); export const collectTelemetry = () => request({ url: "/environmentAccess/deviceMonitor/collect", method: "post" }); export const listDeviceAlarms = () => request({ url: "/environmentAccess/deviceMonitor/alarms", method: "get" }); export const controlDevice = (deviceId, action) => request({ url: "/environmentAccess/deviceMonitor/control", method: "post", data: { deviceId, action }, }); /** * 使ç¨å¸¦ Authorization 请æ±å¤´ç fetch 订é SSE æ¥è¦æµã * åç EventSource æ æ³æºå¸¦æ¬é¡¹ç®ç Bearer Tokenï¼å æ¤å¨è¿éè§£æäºä»¶æµã */ export function subscribeDeviceAlarms(onAlarm, onState = () => {}) { let stopped = false; let controller; let retryTimer; const processBlock = (block) => { let event = "message"; const data = []; block.split("\n").forEach((line) => { if (line.startsWith("event:")) event = line.slice(6).trim(); if (line.startsWith("data:")) data.push(line.slice(5).trim()); }); if (event !== "alarm" || !data.length) return; try { onAlarm(JSON.parse(data.join("\n"))); } catch (error) { console.warn("æ¥è¦æ°æ®è§£æå¤±è´¥", error); } }; const connect = async () => { if (stopped) return; controller = new AbortController(); onState("connecting"); try { const response = await fetch(`${import.meta.env.VITE_APP_BASE_API}/environmentAccess/deviceMonitor/alarm-stream`, { headers: { Authorization: `Bearer ${getToken()}` }, signal: controller.signal, cache: "no-store", }); if (!response.ok || !response.body) throw new Error(`SSE ${response.status}`); onState("connected"); const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; while (!stopped) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }).replace(/\r/g, ""); let boundary; while ((boundary = buffer.indexOf("\n\n")) >= 0) { processBlock(buffer.slice(0, boundary)); buffer = buffer.slice(boundary + 2); } } } catch (error) { if (!stopped && error?.name !== "AbortError") console.warn("æ¥è¦æ¨éè¿æ¥ä¸æ", error); } if (!stopped) { onState("reconnecting"); retryTimer = window.setTimeout(connect, 3000); } }; connect(); return { close() { stopped = true; window.clearTimeout(retryTimer); controller?.abort(); onState("closed"); }, }; // ==================== çæ§è®¾å¤ ==================== export function listMonitorDevice(query) { return request({ url: "/environmentAccess/monitorDevicePage", method: "get", params: query }); } export function listAllMonitorDevice() { return request({ url: "/environmentAccess/monitorDeviceList", method: "get" }); } export function getMonitorDevice(id) { return request({ url: "/environmentAccess/monitorDeviceInfo", method: "get", params: { id } }); } export function addMonitorDevice(data) { return request({ url: "/environmentAccess/monitorDeviceAdd", method: "post", data }); } export function updateMonitorDevice(data) { return request({ url: "/environmentAccess/monitorDeviceUpdate", method: "put", data }); } export function delMonitorDevice(ids) { return request({ url: "/environmentAccess/monitorDeviceDelete", method: "delete", data: ids }); } // ==================== çæµææ ==================== export function listMonitorMetric(deviceId) { return request({ url: "/environmentAccess/monitorMetricList", method: "get", params: { deviceId } }); } export function addMonitorMetric(data) { return request({ url: "/environmentAccess/monitorMetricAdd", method: "post", data }); } export function updateMonitorMetric(data) { return request({ url: "/environmentAccess/monitorMetricUpdate", method: "put", data }); } export function delMonitorMetric(ids) { return request({ url: "/environmentAccess/monitorMetricDelete", method: "delete", data: ids }); } // ==================== æ°æ®éé ==================== export function reportMonitorData(params) { return request({ url: "/environmentAccess/monitorDataReport", method: "post", params }); } // çæµå岿°æ®(è¶å¿æ²çº¿) export function listMonitorDataHistory(params) { return request({ url: "/environmentAccess/monitorDataHistory", method: "get", params }); } // ==================== æ¥è¦ ==================== export function listMonitorAlarm(query) { return request({ url: "/environmentAccess/monitorAlarmPage", method: "get", params: query }); } export function listUnhandledAlarm() { return request({ url: "/environmentAccess/monitorAlarmUnhandled", method: "get" }); } export function handleMonitorAlarm(id, handleRemark) { return request({ url: "/environmentAccess/monitorAlarmHandle", method: "put", params: { id, handleRemark } }); } export function delMonitorAlarm(ids) { return request({ url: "/environmentAccess/monitorAlarmDelete", method: "delete", data: ids }); } // ==================== è¿ç¨æ§å¶ ==================== export function remoteControlDevice(params) { return request({ url: "/environmentAccess/monitorControl", method: "post", params }); } export function listMonitorControl(query) { return request({ url: "/environmentAccess/monitorControlPage", method: "get", params: query }); } export function getMonitorOverview() { return request({ url: "/environmentAccess/monitorOverview", method: "get" }); } src/api/environmentAccess/inspection.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,83 @@ import request from "@/utils/request"; // ==================== å·¡æ£ç¹ä½ ==================== export function listInspectionPoint(query) { return request({ url: "/environmentAccess/inspectionPointPage", method: "get", params: query }); } export function listAllInspectionPoint() { return request({ url: "/environmentAccess/inspectionPointList", method: "get" }); } export function addInspectionPoint(data) { return request({ url: "/environmentAccess/inspectionPointAdd", method: "post", data }); } export function updateInspectionPoint(data) { return request({ url: "/environmentAccess/inspectionPointUpdate", method: "put", data }); } export function delInspectionPoint(ids) { return request({ url: "/environmentAccess/inspectionPointDelete", method: "delete", data: ids }); } // ==================== å·¡æ£è·¯å¾ ==================== export function listInspectionRoute(query) { return request({ url: "/environmentAccess/inspectionRoutePage", method: "get", params: query }); } export function listAllInspectionRoute() { return request({ url: "/environmentAccess/inspectionRouteList", method: "get" }); } export function getInspectionRoute(id) { return request({ url: "/environmentAccess/inspectionRouteInfo", method: "get", params: { id } }); } export function addInspectionRoute(data) { return request({ url: "/environmentAccess/inspectionRouteAdd", method: "post", data }); } export function updateInspectionRoute(data) { return request({ url: "/environmentAccess/inspectionRouteUpdate", method: "put", data }); } export function delInspectionRoute(ids) { return request({ url: "/environmentAccess/inspectionRouteDelete", method: "delete", data: ids }); } // ==================== å·¡æ£ä»»å¡ ==================== export function listInspectionTask(query) { return request({ url: "/environmentAccess/inspectionTaskPage", method: "get", params: query }); } export function getInspectionTask(id) { return request({ url: "/environmentAccess/inspectionTaskInfo", method: "get", params: { id } }); } export function addInspectionTask(data) { return request({ url: "/environmentAccess/inspectionTaskAdd", method: "post", data }); } export function startInspectionTask(id) { return request({ url: "/environmentAccess/inspectionTaskStart", method: "put", params: { id } }); } export function analyzeInspectionTask(id) { return request({ url: "/environmentAccess/inspectionTaskAnalyze", method: "post", params: { id } }); } export function finishInspectionTask(id) { return request({ url: "/environmentAccess/inspectionTaskFinish", method: "post", params: { id } }); } export function delInspectionTask(ids) { return request({ url: "/environmentAccess/inspectionTaskDelete", method: "delete", data: ids }); } // ==================== å·¡æ£æç» ==================== export function listInspectionRecord(taskId) { return request({ url: "/environmentAccess/inspectionRecordList", method: "get", params: { taskId } }); } export function addInspectionRecord(data) { return request({ url: "/environmentAccess/inspectionRecordAdd", method: "post", data }); } export function addInspectionRecordBatch(data) { return request({ url: "/environmentAccess/inspectionRecordBatchAdd", method: "post", data }); } // ==================== å·¡æ£æ¥å ==================== export function listInspectionReport(query) { return request({ url: "/environmentAccess/inspectionReportPage", method: "get", params: query }); } export function getInspectionReport(id) { return request({ url: "/environmentAccess/inspectionReportInfo", method: "get", params: { id } }); } export function delInspectionReport(ids) { return request({ url: "/environmentAccess/inspectionReportDelete", method: "delete", data: ids }); } src/api/environmentAccess/reportCenter.js
@@ -1,14 +1,38 @@ import request from "@/utils/request"; /** * æ¯æ¥è¿ç£ ç»è®¡æ¥è¡¨ï¼è½¦æ¬¡ãè´§éãåå·®ç * @param {Object} params - { startDate, endDate } */ export function getDailyWeighbridgeReport(params) { // æ¯æ¥è¿ç£ ç»è®¡æ¥è¡¨(车次/è´§é/åå·®ç) export function getDailyWeighbridgeReport(query) { return request({ url: "/environmentAccess/reportCenterDaily", method: "get", params: query, }); } // æ¿è¿å绩æåæ(åæ¶ç/ä½å¼æ¬¡æ°) export function getCarrierPerformance(query) { return request({ url: "/environmentAccess/reportCenterCarrier", method: "get", params: query, }); } // æ¥è¡¨åºé´æ»è§ export function getReportOverview(query) { return request({ url: "/environmentAccess/reportCenterOverview", method: "get", params: query, }); } // å岿工è¿ç£ ç»è®¡(æ§æ¥å£ï¼ä¿çå ¼å®¹) export function getLegacyDailyReport(query) { return request({ url: "/environmentAccess/weighbridgeDailyReport", method: "get", params, params: query, }); } src/api/environmentAccess/unattendedWeighbridgeSystem.js
@@ -1,5 +1,6 @@ import request from "@/utils/request"; // ==================== æå·¥è¿ç£ è®°å½(åå²) ==================== // æ¥è¯¢æ 人å¼å®å°ç£ ç³»ç»å页å表 export function listWeighbridgeSystem(query) { return request({ @@ -45,10 +46,55 @@ }); } export function autoWeigh(data) { return request({ url: "/environmentAccess/weighbridgeAutoWeigh", method: "post", data }); // ==================== èªå¨ç§°éè®°å½ ==================== export function listWeighbridgeRecord(query) { return request({ url: "/environmentAccess/weighbridgeRecordPage", method: "get", params: query }); } export function pushWeighbridgeToErp(id) { return request({ url: `/environmentAccess/weighbridgePushErp/${id}`, method: "post" }); export function getWeighbridgeRecord(id) { return request({ url: "/environmentAccess/weighbridgeRecordInfo", method: "get", params: { id } }); } // ç¬¬ä¸æ¬¡ç§°é(ç®é) export function firstWeighbridgeWeight(data) { return request({ url: "/environmentAccess/weighbridgeFirstWeight", method: "post", data }); } // ç¬¬äºæ¬¡ç§°é(æ¯é) export function secondWeighbridgeWeight(data) { return request({ url: "/environmentAccess/weighbridgeSecondWeight", method: "post", data }); } // 红å¤åç§»æ£æµ export function infraredCheck(params) { return request({ url: "/environmentAccess/weighbridgeInfraredCheck", method: "post", params }); } // 订åèªå¨å¹é export function matchWeighbridgeOrder(orderNo) { return request({ url: "/environmentAccess/weighbridgeMatchOrder", method: "get", params: { orderNo } }); } // ä¸ä¼ ERP export function syncWeighbridgeErp(id) { return request({ url: "/environmentAccess/weighbridgeSyncErp", method: "post", params: { id } }); } // ç£ åæå°æ°æ®(å«äºç»´ç ) export function getWeighbridgePrintInfo(id) { return request({ url: "/environmentAccess/weighbridgePrintInfo", method: "get", params: { id } }); } // æ«ç æ ¸éªç£ å export function verifyWeighbridgeQr(code) { return request({ url: "/environmentAccess/weighbridgeVerifyQr", method: "get", params: { code } }); } export function addWeighbridgeRecord(data) { return request({ url: "/environmentAccess/weighbridgeRecordAdd", method: "post", data }); } export function updateWeighbridgeRecord(data) { return request({ url: "/environmentAccess/weighbridgeRecordUpdate", method: "put", data }); } export function delWeighbridgeRecord(ids) { return request({ url: "/environmentAccess/weighbridgeRecordDelete", method: "delete", data: ids }); } // ERPä¸ä¼ æ¥å¿ export function listWeighbridgeErpLog(query) { return request({ url: "/environmentAccess/weighbridgeErpLogPage", method: "get", params: query }); } // 仿¥æ¦è§ export function getWeighbridgeOverview() { return request({ url: "/environmentAccess/weighbridgeOverview", method: "get" }); } src/views/environmentAccess/accessManagement/index.vue
@@ -1,815 +1,921 @@ <template> <div class="app-container access-page"> <!-- 页头 --> <div class="page-head"> <div class="head-text"> <h2>é¨ç¦ç®¡ç</h2> <p>车è¾åå åï¼è½¦ç + RFIDï¼ã人ååå åï¼äººè¸ + å·¥å¡ï¼æ ¸éªï¼å¹¶ä¸å°ç£ è卿¾è¡ã</p> <!-- 仿¥éè¡ç»è®¡ --> <div class="overview-row"> <div class="overview-card"> <div class="label">仿¥è½¦è¾è¿åº</div> <div class="value">{{ overview.vehicleIn || 0 }}</div> </div> <el-button type="primary" :icon="Plus" @click="openGate()">æ°å¢é¨ç¦</el-button> </div> <!-- æ¦è§ --> <div class="stat-row"> <div class="stat tone-brand"> <div class="stat-top"><span>é¨ç¦è®¾å¤</span><el-icon class="stat-icon"><Lock /></el-icon></div> <div class="stat-value">{{ gates.length }}<i>å°</i></div> <div class="overview-card"> <div class="label">仿¥è½¦è¾åºåº</div> <div class="value">{{ overview.vehicleOut || 0 }}</div> </div> <div class="stat"> <div class="stat-top"><span>è¿åºè®°å½</span><el-icon class="stat-icon"><Tickets /></el-icon></div> <div class="stat-value">{{ recordTotal }}<i>æ¡</i></div> <div class="overview-card"> <div class="label">仿¥äººåéè¡</div> <div class="value">{{ (overview.personIn || 0) + (overview.personOut || 0) }}</div> </div> <div class="stat tone-signal"> <div class="stat-top"><span>æ¬é¡µå·²æ¾è¡</span><el-icon class="stat-icon"><CircleCheck /></el-icon></div> <div class="stat-value">{{ releasedCount }}<i>æ¡</i></div> </div> <div class="stat tone-alert"> <div class="stat-top"><span>æ¬é¡µå·²æ¦æª</span><el-icon class="stat-icon"><CircleClose /></el-icon></div> <div class="stat-value">{{ blockedCount }}<i>æ¡</i></div> <div class="overview-card danger"> <div class="label">仿¥æç»æ¾è¡</div> <div class="value">{{ overview.reject || 0 }}</div> </div> </div> <el-tabs v-model="active" @tab-change="loadActive"> <!-- é¨ç¦è®¾å¤ --> <el-tab-pane label="é¨ç¦è®¾å¤" name="gates"> <div class="table-card"> <el-table :data="gates" v-loading="loading" border stripe row-key="id"> <el-table-column prop="gateName" label="é¨ç¦åç§°" min-width="160" /> <el-table-column prop="area" label="åºå" width="130" /> <el-table-column prop="location" label="ä½ç½®" min-width="160" show-overflow-tooltip /> <el-table-column prop="weighbridgeNumber" label="èå¨å°ç£ " width="120" /> <el-table-column label="ç±»å" width="100"> <template #default="{ row }">{{ typeText(row.gateType) }}</template> </el-table-column> <el-table-column label="ç¶æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.deviceStatus === 'online' ? 'success' : 'info'" size="small"> {{ row.deviceStatus === 'online' ? 'å¨çº¿' : '离线' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="160" fixed="right" align="center"> <template #default="{ row }"> <el-button link type="primary" :icon="Edit" @click="openGate(row)">ç¼è¾</el-button> <el-button link type="danger" :icon="Delete" @click="removeGate(row)">å é¤</el-button> </template> </el-table-column> <template #empty><div class="table-empty">ææ é¨ç¦è®¾å¤</div></template> </el-table> </div> </el-tab-pane> <!-- è½¦è¾æ¡£æ¡ --> <el-tab-pane label="è½¦è¾æ¡£æ¡" name="vehicles"> <div class="filter-card"> <el-input v-model="vehicleQuery.plateNumber" placeholder="车çå·" clearable :prefix-icon="Search" style="width: 180px" @keyup.enter="loadVehicles" /> <el-input v-model="vehicleQuery.carrierName" placeholder="æ¿è¿å" clearable style="width: 180px" @keyup.enter="loadVehicles" /> <el-button type="primary" :icon="Search" @click="loadVehicles">æ¥è¯¢</el-button> <el-button :icon="Plus" @click="openVehicle()">æ°å¢è½¦è¾</el-button> </div> <div class="table-card"> <el-table :data="vehicles" v-loading="loading" border stripe row-key="id"> <el-table-column prop="plateNumber" label="车çå·" width="130" /> <el-table-column prop="plateColor" label="车çé¢è²" width="100" /> <el-table-column prop="rfidTag" label="RFIDæ ç¾" width="140" /> <el-table-column prop="carrierName" label="æ¿è¿å" min-width="150" show-overflow-tooltip /> <el-table-column prop="vehicleType" label="车è¾ç±»å" min-width="140" show-overflow-tooltip /> <el-table-column prop="driverName" label="叿º" width="100" /> <el-table-column prop="driverPhone" label="çµè¯" width="130" /> <el-table-column label="ç¶æ" width="90" align="center"> <template #default="{ row }"> <el-tag :type="row.status === 'valid' ? 'success' : 'info'" size="small"> {{ row.status === 'valid' ? 'ææ' : 'åç¨' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="150" fixed="right" align="center"> <template #default="{ row }"> <el-button link type="primary" :icon="Edit" @click="openVehicle(row)">ç¼è¾</el-button> <el-button link type="danger" :icon="Delete" @click="removeVehicle(row)">å é¤</el-button> </template> </el-table-column> <template #empty><div class="table-empty">ææ è½¦è¾ææï¼ç¹å»âæ°å¢è½¦è¾âç»´æ¤</div></template> </el-table> </div> </el-tab-pane> <!-- èº«ä»½æ ¸éª --> <el-tab-pane label="èº«ä»½æ ¸éª" name="verify"> <div class="verify-layout"> <div class="verify-card"> <div class="verify-head"> <h3>æ§è¡æ ¸éª</h3> <span>è¯å«å°è½¦ç + RFID 峿¾è¡ï¼äººå人è¸éè¿ + å·¥å¡å³æ¾è¡</span> <el-tabs v-model="activeTab" @tab-change="handleTabChange"> <!-- ==================== 车è¾éè¡æ ¸éª ==================== --> <el-tab-pane label="车è¾éè¡æ ¸éª" name="vehicleCheck"> <el-card shadow="never" class="check-card"> <template #header> <div class="card-title"> 车çè¯å« + RFID æ ç¾æ ¸éª <span class="card-desc">æ ¸éªéè¿åä¸å°ç£ è卿¾è¡ï¼æªå®æè¿ç£ ç车è¾ç¦æ¢åºåº</span> </div> <el-form ref="verifyRef" :model="verifyForm" :rules="verifyRules" label-width="100px"> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="é¨ç¦" prop="gateId"> <el-select v-model="verifyForm.gateId" placeholder="éæ©é¨ç¦" style="width: 100%"> <el-option v-for="g in gates" :key="g.id" :label="g.gateName" :value="g.id" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="è¯å«å¯¹è±¡"> <el-radio-group v-model="verifyForm.accessType" @change="onTypeChange"> <el-radio-button value="vehicle">车è¾</el-radio-button> <el-radio-button value="person">人å</el-radio-button> </el-radio-group> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="è¿åºæ¹å"> <el-radio-group v-model="verifyForm.direction"> <el-radio-button value="in">è¿åº</el-radio-button> <el-radio-button value="out">åºåº</el-radio-button> </el-radio-group> </el-form-item> </el-col> </el-row> </template> <el-form :model="vehicleCheckForm" label-position="top"> <el-row :gutter="20"> <el-col :span="6"> <el-form-item label="é¨ç¦ç¹ï¼"> <el-select v-model="vehicleCheckForm.gateId" style="width: 100%" placeholder="è¯·éæ©é¨ç¦ç¹"> <el-option v-for="item in gateList" :key="item.id" :label="`${item.gateName}ï¼${item.gateType || '车è¾éé¸'}ï¼`" :value="item.id" /> </el-select> </el-form-item> </el-col> <el-col :span="5"> <el-form-item label="è¯å«è½¦çå·ï¼"> <el-input v-model="vehicleCheckForm.plateNumber" placeholder="å¦ æA12345" clearable /> </el-form-item> </el-col> <el-col :span="5"> <el-form-item label="RFID æ ç¾ï¼"> <el-input v-model="vehicleCheckForm.rfidTag" placeholder="RFID æ ç¾å·" clearable /> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label="éè¡æ¹åï¼"> <el-radio-group v-model="vehicleCheckForm.direction"> <el-radio value="0">è¿åº</el-radio> <el-radio value="1">åºåº</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label=" "> <el-button type="primary" :loading="checkLoading" @click="doVehicleCheck">èº«ä»½æ ¸éªæ¾è¡</el-button> </el-form-item> </el-col> </el-row> </el-form> <el-alert v-if="vehicleCheckResult.message" :title="vehicleCheckResult.message" show-icon :closable="false" :type="vehicleCheckResult.allow ? 'success' : 'error'" style="margin-bottom: 12px" /> <el-descriptions v-if="vehicleCheckResult.record" :column="4" border size="small"> <el-descriptions-item label="车çå·">{{ vehicleCheckResult.record.plateNumber || '-' }}</el-descriptions-item> <el-descriptions-item label="æ¿è¿å">{{ vehicleCheckResult.record.carrierName || '-' }}</el-descriptions-item> <el-descriptions-item label="è¯å«æ¹å¼">{{ vehicleCheckResult.record.identifyMode }}</el-descriptions-item> <el-descriptions-item label="éè¡æ¹å"> {{ vehicleCheckResult.record.direction === '0' ? 'è¿åº' : 'åºåº' }} </el-descriptions-item> <el-descriptions-item label="æ¾è¡ç»æ"> <el-tag :type="vehicleCheckResult.record.allowFlag === '0' ? 'success' : 'danger'" size="small"> {{ vehicleCheckResult.record.allowFlag === '0' ? 'å·²æ¾è¡' : 'å·²æç»' }} </el-tag> </el-descriptions-item> <el-descriptions-item label="å°ç£ èå¨">{{ vehicleCheckResult.record.linkageStatus || '-' }}</el-descriptions-item> <el-descriptions-item label="å ³èç£ å">{{ vehicleCheckResult.record.weightRecordNo || '-' }}</el-descriptions-item> <el-descriptions-item label="éè¡æ¶é´"> {{ formatDateTime(vehicleCheckResult.record.passTime) }} </el-descriptions-item> </el-descriptions> </el-card> <el-table :data="recentRecords" border stripe style="width: 100%; margin-top: 14px" max-height="360"> <el-table-column prop="passTime" label="éè¡æ¶é´" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.passTime) }}</template> </el-table-column> <el-table-column prop="gateName" label="é¨ç¦ç¹" width="130" /> <el-table-column prop="plateNumber" label="车çå·" width="120" /> <el-table-column prop="carrierName" label="æ¿è¿å" width="140" /> <el-table-column prop="identifyMode" label="è¯å«æ¹å¼" width="150" align="center" /> <el-table-column label="æ¹å" width="80" align="center"> <template #default="{ row }">{{ row.direction === '0' ? 'è¿åº' : 'åºåº' }}</template> </el-table-column> <el-table-column label="ç»æ" width="90" align="center"> <template #default="{ row }"> <el-tag :type="row.allowFlag === '0' ? 'success' : 'danger'" size="small"> {{ row.allowFlag === '0' ? 'æ¾è¡' : 'æç»' }} </el-tag> </template> </el-table-column> <el-table-column prop="rejectReason" label="æç»åå " min-width="200" show-overflow-tooltip /> </el-table> </el-tab-pane> <template v-if="verifyForm.accessType === 'vehicle'"> <el-row :gutter="20"> <el-col :span="24"> <el-form-item label="éæ©è½¦è¾"> <el-select v-model="pickedVehicleId" placeholder="ä»è½¦è¾æ¡£æ¡éæ©ï¼èªå¨å¸¦åºè½¦ç / RFID / æ¿è¿å" filterable clearable style="width: 100%" @change="onVehiclePick" > <el-option v-for="v in vehicles" :key="v.id" :label="`${v.plateNumber} · ${v.rfidTag || 'æ RFID'} · ${v.carrierName || 'â'}`" :value="v.id" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="车çå·" prop="plateNumber"> <el-input v-model="verifyForm.plateNumber" placeholder="å¦ï¼èA·D8231" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="RFIDæ ç¾" prop="rfidTag"> <el-input v-model="verifyForm.rfidTag" placeholder="å¦ï¼RFID-88213" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ¿è¿å"> <el-input v-model="verifyForm.carrierName" placeholder="èå¨å°ç£ æ¶èªå¨å¸¦åº" /> </el-form-item> </el-col> </el-row> </template> <!-- ==================== 人åéè¡æ ¸éª ==================== --> <el-tab-pane label="人åéè¡æ ¸éª" name="personCheck"> <el-card shadow="never" class="check-card"> <template #header> <div class="card-title"> 人è¸è¯å« + å·¥å¡å·å¡æ ¸éª <span class="card-desc">人è¸ä¸å·¥å¡ä¿¡æ¯ä¸è´ä¸ææå¨æææå æ¹å¯æ¾è¡</span> </div> </template> <el-form :model="personCheckForm" label-position="top"> <el-row :gutter="20"> <el-col :span="6"> <el-form-item label="é¨ç¦ç¹ï¼"> <el-select v-model="personCheckForm.gateId" style="width: 100%" placeholder="è¯·éæ©é¨ç¦ç¹"> <el-option v-for="item in gateList" :key="item.id" :label="`${item.gateName}ï¼${item.gateType || '人è¡éé'}ï¼`" :value="item.id" /> </el-select> </el-form-item> </el-col> <el-col :span="5"> <el-form-item label="å·¥å¡å·ï¼"> <el-input v-model="personCheckForm.cardNo" placeholder="å·å¡è·åå·¥å¡å·" clearable /> </el-form-item> </el-col> <el-col :span="5"> <el-form-item label="人è¸è¯å«å§åï¼"> <el-input v-model="personCheckForm.personName" placeholder="人è¸è¯å«ç»æ" clearable /> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label="éè¡æ¹åï¼"> <el-radio-group v-model="personCheckForm.direction"> <el-radio value="0">è¿åº</el-radio> <el-radio value="1">åºåº</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="4"> <el-form-item label=" "> <el-button type="primary" :loading="checkLoading" @click="doPersonCheck">èº«ä»½æ ¸éªæ¾è¡</el-button> </el-form-item> </el-col> </el-row> </el-form> <el-alert v-if="personCheckResult.message" :title="personCheckResult.message" show-icon :closable="false" :type="personCheckResult.allow ? 'success' : 'error'" /> </el-card> </el-tab-pane> <template v-else> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="人åå§å" prop="personName"> <el-input v-model="verifyForm.personName" placeholder="å¦ï¼å¼ ä¼" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="äººè¸æ ¸éª" prop="idCardFace"> <el-select v-model="verifyForm.idCardFace" style="width: 100%"> <el-option label="äººè¸æ¯å¯¹éè¿" value="matched" /> <el-option label="äººè¸æ¯å¯¹å¤±è´¥" value="mismatch" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å·¥å¡å·" prop="workCardNo"> <el-input v-model="verifyForm.workCardNo" placeholder="å¦ï¼CARD-10086" /> </el-form-item> </el-col> </el-row> </template> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="å°ç£ èå¨"> <el-switch v-model="verifyForm.weighbridgeLinked" @change="onLinkChange" /> </el-form-item> </el-col> <el-col v-if="verifyForm.weighbridgeLinked" :span="16"> <el-form-item label="å°ç£ è®°å½" prop="weighbridgeRecordId"> <el-select v-model="verifyForm.weighbridgeRecordId" placeholder="éæ©å°ç£ è®°å½" filterable style="width: 100%" @change="onWeighSelect" > <el-option v-for="w in weighList" :key="w.id" :label="weighLabel(w)" :value="w.id" /> </el-select> </el-form-item> </el-col> </el-row> <div v-if="selectedWeigh" class="weigh-hint" :class="{ danger: isWeighAbnormal(selectedWeigh) }"> <el-icon><Warning /></el-icon> <span> {{ selectedWeigh.plateNumber || 'â' }} · åé {{ selectedWeigh.netWeight ?? 'â' }} kg · 红å¤ï¼{{ selectedWeigh.infraredStatus === 'normal' ? 'æ£å¸¸' : 'åç§»æ¥è¦' }} <b v-if="isWeighAbnormal(selectedWeigh)">ï¼å¼å¸¸å°ç£ è®°å½ä¼è¢«æ¦æªï¼</b> </span> </div> <div class="verify-actions"> <el-button type="primary" :loading="verifying" :icon="CircleCheck" @click="submitVerify"> æ§è¡åå åæ ¸éªå¹¶è卿¾è¡ </el-button> <el-button :icon="Refresh" @click="resetVerifyForm">éç½®</el-button> </div> </el-form> <!-- æ ¸éªç»æ --> <transition name="fade"> <div v-if="lastResult" class="verify-result" :class="lastResult.releaseStatus"> <el-icon class="result-icon"> <CircleCheck v-if="lastResult.releaseStatus === 'released'" /> <CircleClose v-else /> </el-icon> <div class="result-body"> <strong>{{ lastResult.releaseStatus === 'released' ? 'æ ¸éªéè¿ï¼é¸æºå·²æ¾è¡' : 'æ ¸éªæªéè¿ï¼å·²æ¦æª' }}</strong> <p>æ ¸éªæ¹å¼ï¼{{ lastResult.verifyMethod || 'â' }}</p> <p v-if="lastResult.rejectReason">æ¦æªåå ï¼{{ lastResult.rejectReason }}</p> <p>éè¡æ¶é´ï¼{{ lastResult.accessTime || 'â' }}</p> </div> <el-tag :type="lastResult.releaseStatus === 'released' ? 'success' : 'danger'" effect="dark"> {{ lastResult.releaseStatus === 'released' ? 'æ¾è¡' : 'æ¦æª' }} </el-tag> </div> </transition> <!-- ==================== è½¦è¾ææ ==================== --> <el-tab-pane label="车è¾éè¡ææ" name="vehicleAuth"> <div class="search_form"> <div> <span class="search_title">车çå·ï¼</span> <el-input v-model="vehicleQuery.plateNumber" style="width: 160px" placeholder="请è¾å ¥" clearable @keyup.enter="loadVehicle" /> <span class="search_title ml10">æ¿è¿åï¼</span> <el-input v-model="vehicleQuery.carrierName" style="width: 160px" placeholder="请è¾å ¥" clearable @keyup.enter="loadVehicle" /> <el-button type="primary" style="margin-left: 10px" @click="loadVehicle">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openVehicleForm()">æ°å¢è½¦è¾ææ</el-button> </div> </div> <el-table :data="vehicleList" border stripe v-loading="vehicleLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="plateNumber" label="车çå·" width="120" align="center" /> <el-table-column prop="plateColor" label="车çé¢è²" width="100" align="center" /> <el-table-column prop="rfidTag" label="RFIDæ ç¾" width="150" align="center" /> <el-table-column prop="carrierName" label="æ¿è¿å" min-width="150" show-overflow-tooltip /> <el-table-column prop="vehicleType" label="车è¾ç±»å" width="120" /> <el-table-column prop="driverName" label="叿º" width="100" align="center" /> <el-table-column prop="driverPhone" label="èç³»çµè¯" width="130" align="center" /> <el-table-column label="æææ" width="200" align="center"> <template #default="{ row }">{{ row.validStart || '-' }} è³ {{ row.validEnd || '-' }}</template> </el-table-column> <el-table-column label="ç¶æ" width="80" align="center"> <template #default="{ row }"> <el-tag :type="row.status === '0' ? 'success' : 'info'" size="small"> {{ row.status === '0' ? 'ææ' : 'ç¦ç¨' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="140" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openVehicleForm(row)">ç¼è¾</el-button> <el-button link type="danger" @click="removeVehicle(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- è¿åºè®°å½ --> <el-tab-pane label="è¿åºè®°å½" name="records"> <div class="filter-card"> <el-input v-model="recordQuery.plateNumber" placeholder="车çå·" clearable :prefix-icon="Search" style="width: 180px" @keyup.enter="handleRecordSearch" /> <el-select v-model="recordQuery.accessType" placeholder="ç±»å" clearable style="width: 130px" @change="handleRecordSearch"> <el-option label="车è¾" value="vehicle" /> <el-option label="人å" value="person" /> </el-select> <el-button type="primary" :icon="Search" @click="handleRecordSearch">æ¥è¯¢</el-button> <el-button :icon="Refresh" @click="resetRecordQuery">éç½®</el-button> <!-- ==================== äººåææ ==================== --> <el-tab-pane label="人åéè¡ææ" name="personAuth"> <div class="search_form"> <div> <span class="search_title">å§åï¼</span> <el-input v-model="personQuery.personName" style="width: 150px" placeholder="请è¾å ¥" clearable @keyup.enter="loadPerson" /> <span class="search_title ml10">å·¥å¡å·ï¼</span> <el-input v-model="personQuery.cardNo" style="width: 150px" placeholder="请è¾å ¥" clearable @keyup.enter="loadPerson" /> <el-button type="primary" style="margin-left: 10px" @click="loadPerson">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openPersonForm()">æ°å¢äººåææ</el-button> </div> </div> <div class="table-card"> <el-table :data="records" v-loading="loading" border stripe> <el-table-column prop="accessTime" label="è¿åºæ¶é´" width="170" /> <el-table-column prop="gateName" label="é¨ç¦" min-width="130" /> <el-table-column label="è¯å«å¯¹è±¡" min-width="130"> <template #default="{ row }">{{ row.accessType === 'vehicle' ? row.plateNumber : row.personName }}</template> </el-table-column> <el-table-column prop="carrierName" label="æ¿è¿å" min-width="140" show-overflow-tooltip /> <el-table-column prop="verifyMethod" label="æ ¸éªæ¹å¼" min-width="150" /> <el-table-column label="å°ç£ èå¨" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.weighbridgeLinked ? 'success' : 'info'" size="small" effect="plain"> {{ row.weighbridgeLinked ? 'å·²èå¨' : 'æªèå¨' }} </el-tag> </template> </el-table-column> <el-table-column label="ç»æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.releaseStatus === 'released' ? 'success' : 'danger'" size="small"> {{ row.releaseStatus === 'released' ? 'å·²æ¾è¡' : 'å·²æ¦æª' }} </el-tag> </template> </el-table-column> <el-table-column prop="rejectReason" label="æ¦æªåå " min-width="160" show-overflow-tooltip /> </el-table> <el-pagination v-model:current-page="recordQuery.current" v-model:page-size="recordQuery.size" :total="recordTotal" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" class="pager" @current-change="loadRecords" @size-change="handleRecordSearch" /> <el-table :data="personList" border stripe v-loading="personLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="personName" label="å§å" width="110" align="center" /> <el-table-column prop="jobNumber" label="å·¥å·" width="110" align="center" /> <el-table-column prop="cardNo" label="å·¥å¡å·" width="140" align="center" /> <el-table-column prop="deptName" label="æå±é¨é¨" width="140" /> <el-table-column prop="personType" label="人åç±»å" width="110" align="center" /> <el-table-column prop="phone" label="èç³»çµè¯" width="130" align="center" /> <el-table-column label="æææ" width="200" align="center"> <template #default="{ row }">{{ row.validStart || '-' }} è³ {{ row.validEnd || '-' }}</template> </el-table-column> <el-table-column label="ç¶æ" width="80" align="center"> <template #default="{ row }"> <el-tag :type="row.status === '0' ? 'success' : 'info'" size="small"> {{ row.status === '0' ? 'ææ' : 'ç¦ç¨' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="140" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openPersonForm(row)">ç¼è¾</el-button> <el-button link type="danger" @click="removePerson(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- ==================== é¨ç¦ç¹ ==================== --> <el-tab-pane label="é¨ç¦ç¹ç®¡ç" name="gate"> <div class="search_form"> <div> <span class="search_title">é¨ç¦åç§°ï¼</span> <el-input v-model="gateQuery.gateName" style="width: 180px" placeholder="请è¾å ¥" clearable @keyup.enter="loadGate" /> <el-button type="primary" style="margin-left: 10px" @click="loadGate">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openGateForm()">æ°å¢é¨ç¦ç¹</el-button> </div> </div> <el-table :data="gatePageList" border stripe v-loading="gateLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="gateCode" label="é¨ç¦ç¼å·" width="130" align="center" /> <el-table-column prop="gateName" label="é¨ç¦åç§°" min-width="150" /> <el-table-column prop="gateType" label="é¨ç¦ç±»å" width="120" align="center" /> <el-table-column prop="areaName" label="æå±åºå" width="120" /> <el-table-column prop="deviceIp" label="设å¤IP" width="140" align="center" /> <el-table-column prop="identifyMode" label="è¯å«æ¹å¼" min-width="180" show-overflow-tooltip /> <el-table-column prop="weighbridgeCode" label="èå¨å°ç£ " width="120" align="center" /> <el-table-column label="ç¶æ" width="80" align="center"> <template #default="{ row }"> <el-tag :type="row.status === '0' ? 'success' : 'info'" size="small"> {{ row.status === '0' ? 'å¯ç¨' : 'åç¨' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="140" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openGateForm(row)">ç¼è¾</el-button> <el-button link type="danger" @click="removeGate(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- ==================== éè¡è®°å½ ==================== --> <el-tab-pane label="éè¡è®°å½" name="record"> <div class="search_form"> <div> <span class="search_title">车çå·ï¼</span> <el-input v-model="recordQuery.plateNumber" style="width: 130px" placeholder="请è¾å ¥" clearable @keyup.enter="loadRecord" /> <span class="search_title ml10">æ¿è¿åï¼</span> <el-input v-model="recordQuery.carrierName" style="width: 130px" placeholder="请è¾å ¥" clearable @keyup.enter="loadRecord" /> <span class="search_title ml10">éè¡å¯¹è±¡ï¼</span> <el-select v-model="recordQuery.recordType" style="width: 110px" placeholder="å ¨é¨" clearable @change="loadRecord"> <el-option label="车è¾" value="0" /> <el-option label="人å" value="1" /> </el-select> <span class="search_title ml10">ç»æï¼</span> <el-select v-model="recordQuery.allowFlag" style="width: 110px" placeholder="å ¨é¨" clearable @change="loadRecord"> <el-option label="æ¾è¡" value="0" /> <el-option label="æç»" value="1" /> </el-select> <el-button type="primary" style="margin-left: 10px" @click="loadRecord">æç´¢</el-button> </div> </div> <el-table :data="recordList" border stripe v-loading="recordLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="passTime" label="éè¡æ¶é´" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.passTime) }}</template> </el-table-column> <el-table-column prop="gateName" label="é¨ç¦ç¹" width="130" /> <el-table-column label="éè¡å¯¹è±¡" width="90" align="center"> <template #default="{ row }">{{ row.recordType === '1' ? '人å' : '车è¾' }}</template> </el-table-column> <el-table-column label="车çå·/å§å" width="130" align="center"> <template #default="{ row }">{{ row.plateNumber || row.personName || '-' }}</template> </el-table-column> <el-table-column prop="carrierName" label="æ¿è¿å" width="150" show-overflow-tooltip /> <el-table-column prop="identifyMode" label="è¯å«æ¹å¼" width="150" align="center" /> <el-table-column label="æ¹å" width="80" align="center"> <template #default="{ row }">{{ row.direction === '0' ? 'è¿åº' : 'åºåº' }}</template> </el-table-column> <el-table-column label="æ¾è¡ç»æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.allowFlag === '0' ? 'success' : 'danger'" size="small"> {{ row.allowFlag === '0' ? 'æ¾è¡' : 'æç»' }} </el-tag> </template> </el-table-column> <el-table-column prop="linkageStatus" label="å°ç£ èå¨ç»æ" min-width="220" show-overflow-tooltip /> <el-table-column prop="rejectReason" label="æç»åå " min-width="200" show-overflow-tooltip /> </el-table> </el-tab-pane> </el-tabs> <!-- é¨ç¦è®¾å¤å¼¹çª --> <el-dialog v-model="gateDialog" :title="gateForm.id ? 'ç¼è¾é¨ç¦' : 'æ°å¢é¨ç¦'" width="620px" destroy-on-close> <el-form ref="gateRef" :model="gateForm" :rules="gateRules" label-width="100px"> <el-form-item label="é¨ç¦åç§°" prop="gateName"> <el-input v-model="gateForm.gateName" /> </el-form-item> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="åºå"><el-input v-model="gateForm.area" /></el-form-item> </el-col> <el-col :span="12"> <el-form-item label="ä½ç½®"><el-input v-model="gateForm.location" /></el-form-item> </el-col> </el-row> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="é¨ç¦ç±»å"> <el-select v-model="gateForm.gateType" style="width: 100%"> <el-option label="车è¾" value="vehicle" /> <el-option label="人å" value="person" /> <el-option label="æ··å" value="all" /> </el-select> <!-- ==================== è½¦è¾ææå¼¹çª ==================== --> <el-dialog v-model="vehicleDialog" :title="vehicleForm.id ? 'ç¼è¾è½¦è¾ææ' : 'æ°å¢è½¦è¾ææ'" width="720px" @close="resetVehicleForm"> <el-form ref="vehicleFormRef" :model="vehicleForm" :rules="vehicleRules" label-position="top"> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="车çå·ï¼" prop="plateNumber"> <el-input v-model="vehicleForm.plateNumber" placeholder="请è¾å ¥è½¦çå·" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="èå¨å°ç£ "><el-input v-model="gateForm.weighbridgeNumber" /></el-form-item> <el-col :span="8"> <el-form-item label="车çé¢è²ï¼"> <el-input v-model="vehicleForm.plateColor" placeholder="å¦ èè²" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="RFID æ ç¾å·ï¼"> <el-input v-model="vehicleForm.rfidTag" placeholder="请è¾å ¥ RFID æ ç¾å·" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="设å¤ç¶æ"> <el-radio-group v-model="gateForm.deviceStatus"> <el-radio-button value="online">å¨çº¿</el-radio-button> <el-radio-button value="offline">离线</el-radio-button> </el-radio-group> </el-form-item> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="æ¿è¿åï¼"> <el-input v-model="vehicleForm.carrierName" placeholder="请è¾å ¥æ¿è¿å" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="车è¾ç±»åï¼"> <el-input v-model="vehicleForm.vehicleType" placeholder="å¦ éå货车" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="叿ºå§åï¼"> <el-input v-model="vehicleForm.driverName" placeholder="请è¾å ¥å¸æºå§å" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="叿ºçµè¯ï¼"> <el-input v-model="vehicleForm.driverPhone" placeholder="请è¾å ¥èç³»çµè¯" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æææå¼å§ï¼"> <el-date-picker v-model="vehicleForm.validStart" type="date" style="width: 100%" value-format="YYYY-MM-DD" placeholder="éæ©æ¥æ" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æææç»æï¼"> <el-date-picker v-model="vehicleForm.validEnd" type="date" style="width: 100%" value-format="YYYY-MM-DD" placeholder="éæ©æ¥æ" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="ç¶æï¼"> <el-radio-group v-model="vehicleForm.status"> <el-radio value="0">ææ</el-radio> <el-radio value="1">ç¦ç¨</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="16"> <el-form-item label="夿³¨ï¼"> <el-input v-model="vehicleForm.remark" placeholder="请è¾å ¥å¤æ³¨" clearable /> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <el-button @click="gateDialog = false">åæ¶</el-button> <el-button type="primary" :loading="saving" @click="saveGate">ä¿å</el-button> <el-button @click="vehicleDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitVehicle">确认</el-button> </template> </el-dialog> <!-- è½¦è¾æ¡£æ¡å¼¹çª --> <el-dialog v-model="vehicleDialog" :title="vehicleForm.id ? 'ç¼è¾è½¦è¾' : 'æ°å¢è½¦è¾'" width="680px" destroy-on-close> <el-form ref="vehicleRef" :model="vehicleForm" :rules="vehicleRules" label-width="100px"> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="车çå·" prop="plateNumber"><el-input v-model="vehicleForm.plateNumber" placeholder="å¦ï¼èA·D8231" /></el-form-item> <!-- ==================== äººåææå¼¹çª ==================== --> <el-dialog v-model="personDialog" :title="personForm.id ? 'ç¼è¾äººåææ' : 'æ°å¢äººåææ'" width="720px" @close="resetPersonForm"> <el-form ref="personFormRef" :model="personForm" :rules="personRules" label-position="top"> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="å§åï¼" prop="personName"> <el-input v-model="personForm.personName" placeholder="请è¾å ¥å§å" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="车çé¢è²"><el-input v-model="vehicleForm.plateColor" placeholder="èè²/é»è²" /></el-form-item> <el-col :span="8"> <el-form-item label="å·¥å·ï¼"> <el-input v-model="personForm.jobNumber" placeholder="请è¾å ¥å·¥å·" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å·¥å¡å·ï¼"> <el-input v-model="personForm.cardNo" placeholder="请è¾å ¥å·¥å¡å·" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="RFIDæ ç¾" prop="rfidTag"><el-input v-model="vehicleForm.rfidTag" placeholder="å¦ï¼RFID-88213" /></el-form-item> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="æå±é¨é¨ï¼"> <el-input v-model="personForm.deptName" placeholder="请è¾å ¥é¨é¨" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="æ¿è¿å"><el-input v-model="vehicleForm.carrierName" /></el-form-item> <el-col :span="8"> <el-form-item label="èç³»çµè¯ï¼"> <el-input v-model="personForm.phone" placeholder="请è¾å ¥çµè¯" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="人åç±»åï¼"> <el-select v-model="personForm.personType" style="width: 100%" placeholder="è¯·éæ©" clearable> <el-option label="å é¨åå·¥" value="å é¨åå·¥" /> <el-option label="夿¥äººå" value="夿¥äººå" /> <el-option label="æ¿å å" value="æ¿å å" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="车è¾ç±»å"><el-input v-model="vehicleForm.vehicleType" /></el-form-item> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="æææå¼å§ï¼"> <el-date-picker v-model="personForm.validStart" type="date" style="width: 100%" value-format="YYYY-MM-DD" placeholder="éæ©æ¥æ" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="叿ºå§å"><el-input v-model="vehicleForm.driverName" /></el-form-item> <el-col :span="8"> <el-form-item label="æææç»æï¼"> <el-date-picker v-model="personForm.validEnd" type="date" style="width: 100%" value-format="YYYY-MM-DD" placeholder="éæ©æ¥æ" /> </el-form-item> </el-col> </el-row> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="叿ºçµè¯"><el-input v-model="vehicleForm.driverPhone" /></el-form-item> </el-col> <el-col :span="12"> <el-form-item label="ç¶æ"> <el-radio-group v-model="vehicleForm.status"> <el-radio-button value="valid">ææ</el-radio-button> <el-radio-button value="disabled">åç¨</el-radio-button> <el-col :span="8"> <el-form-item label="ç¶æï¼"> <el-radio-group v-model="personForm.status"> <el-radio value="0">ææ</el-radio> <el-radio value="1">ç¦ç¨</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> <el-form-item label="夿³¨"><el-input v-model="vehicleForm.remark" type="textarea" :rows="2" /></el-form-item> <el-form-item label="人è¸ç §çå°å(å¯é)ï¼"> <el-input v-model="personForm.faceImage" placeholder="人è¸ç §çå°å" clearable /> </el-form-item> <el-form-item label="夿³¨ï¼"> <el-input v-model="personForm.remark" placeholder="请è¾å ¥å¤æ³¨" clearable /> </el-form-item> </el-form> <template #footer> <el-button @click="vehicleDialog = false">åæ¶</el-button> <el-button type="primary" :loading="savingVehicle" @click="saveVehicle">ä¿å</el-button> <el-button @click="personDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitPerson">确认</el-button> </template> </el-dialog> <!-- ==================== é¨ç¦ç¹å¼¹çª ==================== --> <el-dialog v-model="gateDialog" :title="gateForm.id ? 'ç¼è¾é¨ç¦ç¹' : 'æ°å¢é¨ç¦ç¹'" width="720px" @close="resetGateForm"> <el-form ref="gateFormRef" :model="gateForm" :rules="gateRules" label-position="top"> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="é¨ç¦åç§°ï¼" prop="gateName"> <el-input v-model="gateForm.gateName" placeholder="å¦ ä¸å·é¨è½¦è¾éé¸" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="é¨ç¦ç±»åï¼"> <el-select v-model="gateForm.gateType" style="width: 100%" placeholder="è¯·éæ©"> <el-option label="车è¾éé¸" value="车è¾éé¸" /> <el-option label="人è¡éé" value="人è¡éé" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æå±åºåï¼"> <el-input v-model="gateForm.areaName" placeholder="请è¾å ¥åºå" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="å®è£ ä½ç½®ï¼"> <el-input v-model="gateForm.location" placeholder="请è¾å ¥ä½ç½®" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="设å¤IPï¼"> <el-input v-model="gateForm.deviceIp" placeholder="å¦ 192.168.1.20" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="èå¨å°ç£ ç¼å·ï¼"> <el-input v-model="gateForm.weighbridgeCode" placeholder="å¦ DB-01" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="è¯å«æ¹å¼(å¯å¤é)ï¼"> <el-checkbox-group v-model="gateIdentifyModes"> <el-checkbox value="车çè¯å«">车çè¯å«</el-checkbox> <el-checkbox value="RFIDæ ç¾">RFIDæ ç¾</el-checkbox> <el-checkbox value="人è¸è¯å«">人è¸è¯å«</el-checkbox> <el-checkbox value="å·¥å¡å·å¡">å·¥å¡å·å¡</el-checkbox> </el-checkbox-group> </el-form-item> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="æ¯å¦èå¨å°ç£ æ¾è¡ï¼"> <el-radio-group v-model="gateForm.linkageEnabled"> <el-radio value="1">æ¯</el-radio> <el-radio value="0">å¦</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ¾è¡æ¹å¼ï¼"> <el-select v-model="gateForm.openMode" style="width: 100%" placeholder="è¯·éæ©"> <el-option label="èªå¨æ¾è¡" value="èªå¨æ¾è¡" /> <el-option label="人工确认" value="人工确认" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="ç¶æï¼"> <el-radio-group v-model="gateForm.status"> <el-radio value="0">å¯ç¨</el-radio> <el-radio value="1">åç¨</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> <el-form-item label="夿³¨ï¼"> <el-input v-model="gateForm.remark" placeholder="请è¾å ¥å¤æ³¨" clearable /> </el-form-item> </el-form> <template #footer> <el-button @click="gateDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitGate">确认</el-button> </template> </el-dialog> </div> </template> <script setup> import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue"; import { ElMessageBox } from "element-plus"; import { Plus, Search, Refresh, Edit, Delete, Lock, Tickets, CircleCheck, CircleClose, Warning } from "@element-plus/icons-vue"; import { addAccessGate, addAccessVehicle, deleteAccessGates, deleteAccessVehicles, listAccessGates, listAccessRecords, listAccessVehicles, updateAccessGate, updateAccessVehicle, verifyAccess } from "@/api/environmentAccess/accessManagement"; import { listWeighbridgeSystem } from "@/api/environmentAccess/unattendedWeighbridgeSystem"; addAccessGate, addAccessPerson, addAccessVehicle, delAccessGate, delAccessPerson, delAccessVehicle, getAccessOverview, listAccessGate, listAccessPerson, listAccessRecord, listAccessVehicle, listAllAccessGate, personRecognize, updateAccessGate, updateAccessPerson, updateAccessVehicle, vehicleRecognize, } from "@/api/environmentAccess/accessManagement.js"; const { proxy } = getCurrentInstance(); const active = ref("gates"); const loading = ref(false); const verifying = ref(false); const saving = ref(false); const gates = ref([]); const records = ref([]); const recordTotal = ref(0); const weighList = ref([]); const gateDialog = ref(false); const gateRef = ref(null); const verifyRef = ref(null); const lastResult = ref(null); const selectedWeigh = ref(null); const gateForm = reactive({ id: null, gateName: "", area: "", location: "", gateType: "all", weighbridgeNumber: "", deviceStatus: "online", }); const verifyForm = reactive({ gateId: null, accessType: "vehicle", direction: "in", plateNumber: "", rfidTag: "", carrierName: "", personName: "", idCardFace: "matched", workCardNo: "", weighbridgeLinked: false, weighbridgeRecordId: null, }); const recordQuery = reactive({ plateNumber: "", accessType: "", current: 1, size: 10 }); const activeTab = ref("vehicleCheck"); const overview = ref({}); const gateList = ref([]); const checkLoading = ref(false); const vehicles = ref([]); const vehicleDialog = ref(false); const vehicleRef = ref(null); const savingVehicle = ref(false); const pickedVehicleId = ref(null); const vehicleQuery = reactive({ plateNumber: "", carrierName: "" }); const emptyVehicle = { id: null, plateNumber: "", plateColor: "èè²", rfidTag: "", carrierName: "", vehicleType: "", driverName: "", driverPhone: "", status: "valid", remark: "", const formatDateTime = (val) => { if (!val) return "-"; return String(val).replace("T", " ").substring(0, 19); }; const vehicleForm = reactive({ ...emptyVehicle }); // ==================== æ ¸éª ==================== const vehicleCheckForm = reactive({ gateId: null, plateNumber: "", rfidTag: "", direction: "0" }); const vehicleCheckResult = ref({}); const personCheckForm = reactive({ gateId: null, cardNo: "", personName: "", direction: "0" }); const personCheckResult = ref({}); const recentRecords = ref([]); const loadOverview = () => { getAccessOverview().then((res) => { overview.value = res.data || {}; }); }; const loadGateOptions = () => { listAllAccessGate().then((res) => { gateList.value = res.data || []; }); }; const loadRecent = () => { listAccessRecord({ current: 1, size: 10 }).then((res) => { recentRecords.value = res.data?.records || []; }); }; const doVehicleCheck = () => { if (!vehicleCheckForm.gateId) { proxy.$modal.msgWarning("è¯·éæ©é¨ç¦ç¹"); return; } if (!vehicleCheckForm.plateNumber && !vehicleCheckForm.rfidTag) { proxy.$modal.msgWarning("请è¾å ¥è½¦çå·æ RFID æ ç¾"); return; } checkLoading.value = true; vehicleRecognize({ ...vehicleCheckForm }) .then((res) => { vehicleCheckResult.value = res.data || {}; if (res.data?.allow) { proxy.$modal.msgSuccess(res.data.message); } else { proxy.$modal.msgError(res.data?.message || "æ ¸éªæªéè¿"); } loadRecent(); loadOverview(); }) .finally(() => { checkLoading.value = false; }); }; const doPersonCheck = () => { if (!personCheckForm.gateId) { proxy.$modal.msgWarning("è¯·éæ©é¨ç¦ç¹"); return; } if (!personCheckForm.cardNo && !personCheckForm.personName) { proxy.$modal.msgWarning("请è¾å ¥å·¥å¡å·æäººè¸è¯å«å§å"); return; } checkLoading.value = true; personRecognize({ ...personCheckForm }) .then((res) => { personCheckResult.value = res.data || {}; if (res.data?.allow) { proxy.$modal.msgSuccess(res.data.message); } else { proxy.$modal.msgError(res.data?.message || "æ ¸éªæªéè¿"); } loadOverview(); }) .finally(() => { checkLoading.value = false; }); }; // ==================== è½¦è¾ææ ==================== const vehicleQuery = reactive({ plateNumber: "", carrierName: "" }); const vehicleList = ref([]); const vehicleLoading = ref(false); const vehicleDialog = ref(false); const vehicleFormRef = ref(); const vehicleForm = ref({}); const vehicleRules = { plateNumber: [{ required: true, message: "请è¾å ¥è½¦çå·", trigger: "blur" }], rfidTag: [{ required: true, message: "请è¾å ¥RFIDæ ç¾", trigger: "blur" }], }; const gateRules = { gateName: [{ required: true, message: "请è¾å ¥é¨ç¦åç§°", trigger: "blur" }] }; const verifyRules = computed(() => { const base = { gateId: [{ required: true, message: "è¯·éæ©é¨ç¦", trigger: "change" }] }; if (verifyForm.accessType === "person") { return { ...base, personName: [{ required: true, message: "请è¾å ¥äººåå§å", trigger: "blur" }], idCardFace: [{ required: true, message: "è¯·éæ©äººè¸æ ¸éªç»æ", trigger: "change" }], workCardNo: [{ required: true, message: "请è¾å ¥å·¥å¡å·", trigger: "blur" }], }; } return { ...base, plateNumber: [{ required: true, message: "请è¾å ¥è½¦çå·", trigger: "blur" }], rfidTag: [{ required: true, message: "请è¾å ¥RFIDæ ç¾", trigger: "blur" }], }; }); const rows = r => r?.data?.records || r?.records || r?.data || []; const releasedCount = computed(() => records.value.filter(r => r.releaseStatus === "released").length); const blockedCount = computed(() => records.value.filter(r => r.releaseStatus !== "released").length); async function loadGates() { loading.value = true; try { gates.value = rows(await listAccessGates({ current: 1, size: 100 })); } finally { loading.value = false; } } async function loadRecords() { loading.value = true; try { const res = await listAccessRecords({ ...recordQuery }); records.value = rows(res); recordTotal.value = Number(res?.data?.total ?? res?.total ?? records.value.length) || 0; } finally { loading.value = false; } } async function loadWeighList() { try { weighList.value = rows(await listWeighbridgeSystem({ current: 1, size: 100 })); } catch (e) { weighList.value = []; } } function loadActive(name) { if (name === "records") loadRecords(); else if (name === "verify") { loadGates(); loadWeighList(); loadVehicles(); } else if (name === "vehicles") loadVehicles(); else loadGates(); } function openGate(row = {}) { Object.assign(gateForm, { id: null, gateName: "", area: "", location: "", gateType: "all", weighbridgeNumber: "", deviceStatus: "online", }, row); gateDialog.value = true; } async function saveGate() { try { await gateRef.value.validate(); } catch (e) { return; } saving.value = true; try { await (gateForm.id ? updateAccessGate(gateForm) : addAccessGate(gateForm)); gateDialog.value = false; proxy.$modal.msgSuccess("ä¿åæå"); loadGates(); } catch (e) { proxy.$modal.msgError("ä¿å失败"); } finally { saving.value = false; } } async function removeGate(row) { try { await proxy.$modal.confirm(`确认å é¤é¨ç¦â${row.gateName}âï¼`); } catch (e) { return; } await deleteAccessGates([row.id]); proxy.$modal.msgSuccess("å 餿å"); loadGates(); } /* ---------- è½¦è¾æ¡£æ¡ ---------- */ async function loadVehicles() { try { vehicles.value = rows(await listAccessVehicles({ ...vehicleQuery, current: 1, size: 100 })); } catch (e) { vehicles.value = []; } } function openVehicle(row = {}) { Object.assign(vehicleForm, emptyVehicle, row); const loadVehicle = () => { vehicleLoading.value = true; listAccessVehicle({ ...vehicleQuery, current: 1, size: 500 }) .then((res) => { vehicleList.value = res.data?.records || []; }) .finally(() => { vehicleLoading.value = false; }); }; const openVehicleForm = (row) => { vehicleForm.value = row ? { ...row } : { status: "0" }; vehicleDialog.value = true; } async function saveVehicle() { try { await vehicleRef.value.validate(); } catch (e) { return; } savingVehicle.value = true; try { await (vehicleForm.id ? updateAccessVehicle(vehicleForm) : addAccessVehicle(vehicleForm)); vehicleDialog.value = false; proxy.$modal.msgSuccess("ä¿åæå"); loadVehicles(); } catch (e) { proxy.$modal.msgError("ä¿å失败"); } finally { savingVehicle.value = false; } } async function removeVehicle(row) { try { await proxy.$modal.confirm(`确认å é¤è½¦è¾â${row.plateNumber}âï¼`); } catch (e) { return; } await deleteAccessVehicles([row.id]); proxy.$modal.msgSuccess("å 餿å"); loadVehicles(); } function onVehiclePick(id) { const v = vehicles.value.find(item => item.id === id); if (!v) return; verifyForm.plateNumber = v.plateNumber || ""; verifyForm.rfidTag = v.rfidTag || ""; verifyForm.carrierName = v.carrierName || ""; } function onTypeChange() { pickedVehicleId.value = null; verifyForm.plateNumber = ""; verifyForm.rfidTag = ""; verifyForm.personName = ""; verifyForm.idCardFace = "matched"; verifyForm.workCardNo = ""; nextTick(() => verifyRef.value?.clearValidate()); } function onLinkChange(val) { if (!val) { verifyForm.weighbridgeRecordId = null; selectedWeigh.value = null; } else { loadWeighList(); } } function onWeighSelect(id) { const rec = weighList.value.find(w => w.id === id); selectedWeigh.value = rec || null; if (rec && rec.carrierName) { verifyForm.carrierName = rec.carrierName; } } function isWeighAbnormal(w) { return !!w && (Number(w.cheatFlag) === 1 || (w.infraredStatus && w.infraredStatus !== "normal")); } function weighLabel(w) { return `${w.plateNumber || "â"} · ${w.weighNo || "æ ç£ åå·"} · åé ${w.netWeight ?? "â"}`; } function resetVerifyForm() { Object.assign(verifyForm, { gateId: verifyForm.gateId, accessType: "vehicle", direction: "in", plateNumber: "", rfidTag: "", carrierName: "", personName: "", idCardFace: "matched", workCardNo: "", weighbridgeLinked: false, weighbridgeRecordId: null, }; const resetVehicleForm = () => { vehicleFormRef.value?.resetFields(); vehicleForm.value = {}; }; const submitVehicle = () => { vehicleFormRef.value.validate((valid) => { if (!valid) return; const api = vehicleForm.value.id ? updateAccessVehicle : addAccessVehicle; api(vehicleForm.value).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); vehicleDialog.value = false; loadVehicle(); }); }); selectedWeigh.value = null; pickedVehicleId.value = null; lastResult.value = null; nextTick(() => verifyRef.value?.clearValidate()); } }; const removeVehicle = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤è½¦çã${row.plateNumber}ãçææåï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delAccessVehicle([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadVehicle(); }); }).catch(() => {}); }; async function submitVerify() { try { await verifyRef.value.validate(); } catch (e) { return; } verifying.value = true; try { const payload = { ...verifyForm }; if (!payload.weighbridgeLinked) { payload.weighbridgeRecordId = null; } const res = await verifyAccess(payload); const data = res?.data || {}; lastResult.value = data; if (data.releaseStatus === "released") { proxy.$modal.msgSuccess("æ ¸éªéè¿ï¼é¸æºå·²æ¾è¡"); } else { proxy.$modal.msgWarning(data.rejectReason || "æ ¸éªæªéè¿ï¼å·²æ¦æª"); } if (active.value === "records") loadRecords(); } catch (e) { proxy.$modal.msgError("æ ¸éªè¯·æ±å¤±è´¥"); } finally { verifying.value = false; } } // ==================== äººåææ ==================== const personQuery = reactive({ personName: "", cardNo: "" }); const personList = ref([]); const personLoading = ref(false); const personDialog = ref(false); const personFormRef = ref(); const personForm = ref({}); const personRules = { personName: [{ required: true, message: "请è¾å ¥å§å", trigger: "blur" }], }; function handleRecordSearch() { recordQuery.current = 1; loadRecords(); } const loadPerson = () => { personLoading.value = true; listAccessPerson({ ...personQuery, current: 1, size: 500 }) .then((res) => { personList.value = res.data?.records || []; }) .finally(() => { personLoading.value = false; }); }; const openPersonForm = (row) => { personForm.value = row ? { ...row } : { status: "0" }; personDialog.value = true; }; const resetPersonForm = () => { personFormRef.value?.resetFields(); personForm.value = {}; }; const submitPerson = () => { personFormRef.value.validate((valid) => { if (!valid) return; const api = personForm.value.id ? updateAccessPerson : addAccessPerson; api(personForm.value).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); personDialog.value = false; loadPerson(); }); }); }; const removePerson = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤ã${row.personName}ãçéè¡ææåï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delAccessPerson([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadPerson(); }); }).catch(() => {}); }; function resetRecordQuery() { recordQuery.plateNumber = ""; recordQuery.accessType = ""; handleRecordSearch(); } // ==================== é¨ç¦ç¹ ==================== const gateQuery = reactive({ gateName: "" }); const gatePageList = ref([]); const gateLoading = ref(false); const gateDialog = ref(false); const gateFormRef = ref(); const gateForm = ref({}); const gateIdentifyModes = ref([]); const gateRules = { gateName: [{ required: true, message: "请è¾å ¥é¨ç¦åç§°", trigger: "blur" }], }; const typeText = v => ({ vehicle: "车è¾", person: "人å", all: "æ··å" }[v] || v); const loadGate = () => { gateLoading.value = true; listAccessGate({ ...gateQuery, current: 1, size: 500 }) .then((res) => { gatePageList.value = res.data?.records || []; }) .finally(() => { gateLoading.value = false; }); }; const openGateForm = (row) => { gateForm.value = row ? { ...row } : { status: "0", gateType: "车è¾éé¸", linkageEnabled: "1", openMode: "èªå¨æ¾è¡" }; gateIdentifyModes.value = row?.identifyMode ? row.identifyMode.split(",").filter(Boolean) : ["车çè¯å«", "RFIDæ ç¾"]; gateDialog.value = true; }; const resetGateForm = () => { gateFormRef.value?.resetFields(); gateForm.value = {}; gateIdentifyModes.value = []; }; const submitGate = () => { gateFormRef.value.validate((valid) => { if (!valid) return; const payload = { ...gateForm.value }; payload.identifyMode = gateIdentifyModes.value.join(","); const api = payload.id ? updateAccessGate : addAccessGate; api(payload).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); gateDialog.value = false; loadGate(); loadGateOptions(); }); }); }; const removeGate = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤é¨ç¦ç¹ã${row.gateName}ãåï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delAccessGate([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadGate(); loadGateOptions(); }); }).catch(() => {}); }; // ==================== éè¡è®°å½ ==================== const recordQuery = reactive({ plateNumber: "", carrierName: "", recordType: "", allowFlag: "" }); const recordList = ref([]); const recordLoading = ref(false); const loadRecord = () => { recordLoading.value = true; listAccessRecord({ ...recordQuery, current: 1, size: 300 }) .then((res) => { recordList.value = res.data?.records || []; }) .finally(() => { recordLoading.value = false; }); }; const handleTabChange = (name) => { if (name === "vehicleAuth") loadVehicle(); if (name === "personAuth") loadPerson(); if (name === "gate") loadGate(); if (name === "record") loadRecord(); if (name === "vehicleCheck") loadRecent(); }; onMounted(() => { loadGates(); loadVehicles(); loadOverview(); loadGateOptions(); loadRecent(); loadVehicle(); loadPerson(); loadGate(); loadRecord(); }); </script> <style scoped lang="scss"> $line: #e3e8ed; $paper: #f4f6f8; $text: #1b2733; $muted: #5c6b7a; $brand: #2c51d9; $alert: #e0a02e; $signal: #2fbfa0; .access-page { background: $paper; min-height: calc(100vh - 84px); } .page-head { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 20px 22px; background: linear-gradient(150deg, #14191f 0%, #1e252c 100%); border-radius: 6px; margin-bottom: 16px; h2 { margin: 0 0 6px; font-size: 20px; font-weight: 700; color: #fff; letter-spacing: .03em; } p { margin: 0; font-size: 13px; color: rgba(234,240,245,.66); } :deep(.el-button--primary) { background: #fff; border-color: #fff; color: #14191f; font-weight: 600; &:hover { background: #e9eef5; border-color: #e9eef5; color: #14191f; } .access-page { .overview-row { display: flex; gap: 14px; margin-bottom: 16px; .overview-card { flex: 1; background: #f7f9fc; border-radius: 6px; padding: 14px 18px; .label { color: #909399; font-size: 13px; } .value { font-size: 24px; font-weight: 600; color: #303133; margin-top: 6px; } &.danger .value { color: #f56c6c; } } } } .stat-row { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 14px; margin-bottom: 16px; } .stat { position: relative; background: #fff; border: 1px solid $line; border-radius: 6px; padding: 14px 18px 12px; &::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: #c7d0da; } &.tone-brand::before { background: $brand; } &.tone-signal::before { background: $signal; } &.tone-alert::before { background: $alert; } } .stat-top { display: flex; align-items: center; justify-content: space-between; font-size: 12.5px; color: $muted; } .stat-icon { font-size: 16px; color: #b4bfca; } .stat-value { margin-top: 8px; font-family: "MyCustomFont", "Helvetica Neue", Arial, sans-serif; font-size: 26px; font-weight: 700; color: $text; line-height: 1; i { font-style: normal; font-size: 12px; color: #98a4b0; margin-left: 4px; font-family: "Microsoft YaHei", Arial, sans-serif; font-weight: 400; } } .filter-card { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 14px 18px; background: #fff; border: 1px solid $line; border-radius: 6px; margin-bottom: 16px; } .table-card { background: #fff; border: 1px solid $line; border-radius: 6px; padding: 14px 16px; } .table-empty { padding: 26px 0; color: #a7b2bd; font-size: 13px; } .pager { justify-content: flex-end; margin-top: 14px; } /* æ ¸éª */ .verify-layout { max-width: 1080px; } .verify-card { background: #fff; border: 1px solid $line; border-radius: 6px; padding: 20px 24px; } .verify-head { display: flex; align-items: baseline; gap: 12px; padding-bottom: 14px; margin-bottom: 18px; border-bottom: 1px solid #eef2f5; h3 { margin: 0; font-size: 15px; font-weight: 600; color: $text; } span { font-size: 12.5px; color: #98a4b0; } } .weigh-hint { display: flex; align-items: center; gap: 8px; margin: 0 0 6px 100px; padding: 9px 12px; background: #f2f7f5; border: 1px solid #d8ebe4; border-radius: 4px; font-size: 12.5px; color: #2f6f5f; &.danger { background: #fbf1f1; border-color: #f0d4d4; color: #b04545; } b { margin-left: 4px; } } .verify-actions { display: flex; gap: 10px; margin-top: 14px; } .verify-result { display: flex; align-items: flex-start; gap: 14px; margin-top: 20px; padding: 16px 18px; border-radius: 6px; border: 1px solid #d8ebe4; background: #f2f7f5; &.blocked { border-color: #f0d4d4; background: #fbf1f1; } .result-icon { font-size: 26px; color: #2f9e83; margin-top: 2px; } &.blocked .result-icon { color: #c14b4b; } .result-body { flex: 1; strong { font-size: 14px; color: $text; } p { margin: 6px 0 0; font-size: 12.5px; color: $muted; } .ml10 { margin-left: 10px; } } .fade-enter-active, .fade-leave-active { transition: opacity .2s; } .fade-enter-from, .fade-leave-to { opacity: 0; } @media (max-width: 900px) { .stat-row { grid-template-columns: repeat(2, minmax(0,1fr)); } .page-head { flex-direction: column; align-items: flex-start; } .weigh-hint { margin-left: 0; } .check-card { margin-bottom: 10px; .card-title { font-weight: 600; .card-desc { margin-left: 10px; color: #909399; font-size: 12px; font-weight: 400; } } } } </style> src/views/environmentAccess/deviceMonitor/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,1058 @@ <template> <div class="app-container monitor-page"> <!-- æ¦è§ --> <div class="overview-row"> <div class="overview-card"> <div class="label">è®¾å¤æ»æ°</div> <div class="value">{{ overview.total || 0 }}</div> </div> <div class="overview-card"> <div class="label">å¨çº¿</div> <div class="value success">{{ overview.online || 0 }}</div> </div> <div class="overview-card"> <div class="label">离线</div> <div class="value info">{{ overview.offline || 0 }}</div> </div> <div class="overview-card"> <div class="label">æ¥è¦ä¸</div> <div class="value danger">{{ overview.alarming || 0 }}</div> </div> <div class="overview-card"> <div class="label">æªå¤çæ¥è¦</div> <div class="value danger">{{ overview.unhandledAlarm || 0 }}</div> </div> <div class="overview-card"> <div class="label">䏿¯æå¨çº¿çæµ</div> <div class="value"> {{ (overview.total || 0) - (overview.monitorEnabled || 0) }} </div> </div> <div class="overview-card"> <div class="label">æ¯æè¿ç¨æ§å¶</div> <div class="value">{{ overview.controlEnabled || 0 }}</div> </div> </div> <el-tabs v-model="activeTab" @tab-change="handleTabChange"> <!-- ==================== 设å¤å®æ¶çæ§ ==================== --> <el-tab-pane label="设å¤å®æ¶çæ§" name="device"> <div class="search_form"> <div> <span class="search_title">设å¤åç§°ï¼</span> <el-input v-model="deviceQuery.deviceName" style="width: 160px" placeholder="请è¾å ¥" clearable @keyup.enter="loadDevice" /> <span class="search_title ml10">设å¤ç±»åï¼</span> <el-select v-model="deviceQuery.deviceType" style="width: 150px" placeholder="å ¨é¨" clearable @change="loadDevice"> <el-option v-for="item in deviceTypeList" :key="item" :label="item" :value="item" /> </el-select> <span class="search_title ml10">å¨çº¿ç¶æï¼</span> <el-select v-model="deviceQuery.onlineStatus" style="width: 130px" placeholder="å ¨é¨" clearable @change="loadDevice"> <el-option label="å¨çº¿" value="0" /> <el-option label="离线" value="1" /> <el-option label="æ¥è¦" value="2" /> </el-select> <el-button type="primary" style="margin-left: 10px" @click="loadDevice">æç´¢</el-button> <el-button @click="refreshAll">å·æ°</el-button> </div> <div> <el-button type="primary" @click="openDeviceForm()">æ°å¢è®¾å¤</el-button> </div> </div> <el-table :data="deviceList" border stripe v-loading="deviceLoading" style="width: 100%"> <el-table-column type="expand"> <template #default="{ row }"> <div class="metric-panel"> <div v-if="row.monitorEnabled !== '1'" class="no-monitor"> è¯¥è®¾å¤æªæ¥å ¥å¨çº¿çæµåè·¯ï¼æ æ³èªå¨ééåæ°ï¼è¯·äººå·¥æå½åå¨ä¸æ¹ææ ä¸å½å ¥ï¼æ°æ®æ¥æºæ 记为人工å½å ¥ï¼ã </div> <el-table :data="row.metrics || []" border size="small" style="width: 100%"> <el-table-column prop="metricName" label="çæµææ " width="140" /> <el-table-column prop="metricCode" label="ææ ç¼ç " width="150" /> <el-table-column label="å½åå¼" width="160"> <template #default="{ row: metric }"> <span :class="metricAlarmClass(metric)"> {{ metric.currentValue == null ? 'ææ æ°æ®' : metric.currentValue }} {{ metric.unit || '' }} </span> </template> </el-table-column> <el-table-column label="æ¥è¦éå¼" width="220"> <template #default="{ row: metric }"> ä¸é {{ metric.thresholdMin ?? '-' }} ï½ ä¸é {{ metric.thresholdMax ?? '-' }} </template> </el-table-column> <el-table-column label="ééæ¹å¼" width="110" align="center"> <template #default="{ row: metric }"> {{ metric.collectType === '1' ? '人工å½å ¥' : 'èªå¨éé' }} </template> </el-table-column> <el-table-column label="æåééæ¶é´" width="180" align="center"> <template #default="{ row: metric }">{{ formatDateTime(metric.lastCollectTime) }}</template> </el-table-column> <el-table-column label="æä½" width="200" align="center"> <template #default="{ row: metric }"> <el-button link type="primary" @click="openReportDialog(row, metric)">䏿¥æ°æ®</el-button> <el-button link type="primary" @click="openTrendDialog(row, metric)">è¶å¿</el-button> </template> </el-table-column> </el-table> <div v-if="!row.metrics || row.metrics.length === 0" class="no-monitor"> å°æªé ç½®çæµææ ï¼è¯·ç¹å»å³ä¾§ãææ é ç½®ãæ°å¢æ¸©åº¦/åå/è½èçææ ååå½å ¥æ°æ®ã </div> </div> </template> </el-table-column> <el-table-column prop="deviceCode" label="设å¤ç¼å·" width="130" align="center" /> <el-table-column prop="deviceName" label="设å¤åç§°" min-width="160" show-overflow-tooltip /> <el-table-column prop="deviceType" label="设å¤ç±»å" width="120" align="center" /> <el-table-column prop="location" label="å®è£ ä½ç½®" width="130" /> <el-table-column prop="commProtocol" label="éä¿¡æ¹å¼" width="110" align="center" /> <el-table-column label="å¨çº¿ç¶æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="onlineStatusType(row.onlineStatus)" size="small"> {{ onlineStatusText(row.onlineStatus) }} </el-tag> </template> </el-table-column> <el-table-column label="è¿è¡ç¶æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.runStatus === '1' ? 'success' : 'info'" size="small"> {{ row.runStatus === '1' ? 'è¿è¡' : 'åæº' }} </el-tag> </template> </el-table-column> <el-table-column label="宿¶åæ°" min-width="220"> <template #default="{ row }"> <template v-if="row.metrics && row.metrics.length"> <el-tag v-for="metric in row.metrics" :key="metric.id" size="small" style="margin-right: 6px" :type="isMetricAlarm(metric) ? 'danger' : 'info'"> {{ metric.metricName }} {{ metric.currentValue ?? '-' }}{{ metric.unit || '' }} </el-tag> </template> <span v-else class="sub-text"> {{ row.monitorEnabled === '1' ? 'ææ ééæ°æ®' : '䏿¯æå¨çº¿çæµ' }} </span> </template> </el-table-column> <el-table-column label="æåå¿è·³" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.lastHeartbeat) }}</template> </el-table-column> <el-table-column label="æä½" width="260" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openDeviceForm(row)">ç¼è¾</el-button> <el-button link type="primary" @click="openMetricDialog(row)">ææ é ç½®</el-button> <el-button v-if="row.controlEnabled === '1'" link type="primary" @click="controlDevice(row, row.runStatus === '1' ? 'stop' : 'start')"> {{ row.runStatus === '1' ? 'è¿ç¨åæº' : 'è¿ç¨å¯å¨' }} </el-button> <el-tooltip v-else content="è¯¥è®¾å¤æªæ¥å ¥è¿ç¨æ§å¶åè·¯ï¼éç°åºæä½" placement="top"> <span class="disabled-btn">è¿ç¨å¯å</span> </el-tooltip> <el-button link type="danger" @click="removeDevice(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- ==================== æ¥è¦ç®¡ç ==================== --> <el-tab-pane label="æ¥è¦ç®¡ç" name="alarm"> <div class="search_form"> <div> <span class="search_title">设å¤åç§°ï¼</span> <el-input v-model="alarmQuery.deviceName" style="width: 160px" placeholder="请è¾å ¥" clearable @keyup.enter="loadAlarm" /> <span class="search_title ml10">å¤çç¶æï¼</span> <el-select v-model="alarmQuery.alarmStatus" style="width: 130px" placeholder="å ¨é¨" clearable @change="loadAlarm"> <el-option label="æªå¤ç" value="0" /> <el-option label="å·²å¤ç" value="1" /> <el-option label="å·²æ¢å¤" value="2" /> </el-select> <span class="search_title ml10">æ¥è¦çº§å«ï¼</span> <el-select v-model="alarmQuery.alarmLevel" style="width: 130px" placeholder="å ¨é¨" clearable @change="loadAlarm"> <el-option label="æç¤º" value="0" /> <el-option label="è¦å" value="1" /> <el-option label="严é" value="2" /> </el-select> <el-button type="primary" style="margin-left: 10px" @click="loadAlarm">æç´¢</el-button> </div> <div class="alarm-tip"> <el-badge :value="overview.unhandledAlarm || 0" :hidden="!overview.unhandledAlarm" type="danger"> <span>æªå¤çæ¥è¦</span> </el-badge> </div> </div> <el-table :data="alarmList" border stripe v-loading="alarmLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="alarmTime" label="æ¥è¦æ¶é´" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.alarmTime) }}</template> </el-table-column> <el-table-column prop="deviceName" label="设å¤" width="150" show-overflow-tooltip /> <el-table-column prop="location" label="ä½ç½®" width="130" /> <el-table-column label="ææ " width="120" align="center"> <template #default="{ row }">{{ row.metricName }}</template> </el-table-column> <el-table-column label="æ¥è¦å¼" width="140" align="center"> <template #default="{ row }"> <span class="danger-text">{{ row.alarmValue }}{{ row.unit || '' }}</span> </template> </el-table-column> <el-table-column label="éå¼" width="180" align="center"> <template #default="{ row }"> ä¸é {{ row.thresholdMin ?? '-' }} / ä¸é {{ row.thresholdMax ?? '-' }} </template> </el-table-column> <el-table-column label="级å«" width="90" align="center"> <template #default="{ row }"> <el-tag :type="alarmLevelType(row.alarmLevel)" size="small">{{ alarmLevelText(row.alarmLevel) }}</el-tag> </template> </el-table-column> <el-table-column prop="alarmContent" label="æ¥è¦å 容" min-width="240" show-overflow-tooltip /> <el-table-column label="ç¶æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.alarmStatus === '0' ? 'danger' : 'success'" size="small"> {{ row.alarmStatus === '0' ? 'æªå¤ç' : (row.alarmStatus === '1' ? 'å·²å¤ç' : 'å·²æ¢å¤') }} </el-tag> </template> </el-table-column> <el-table-column prop="handleUser" label="å¤ç人" width="100" align="center" /> <el-table-column label="æä½" width="140" align="center" fixed="right"> <template #default="{ row }"> <el-button v-if="row.alarmStatus === '0'" link type="primary" @click="handleAlarm(row)">å¤ç</el-button> <el-button link type="danger" @click="removeAlarm(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- ==================== è¿ç¨æ§å¶è®°å½ ==================== --> <el-tab-pane label="è¿ç¨æ§å¶è®°å½" name="control"> <el-table :data="controlList" border stripe v-loading="controlLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="operateTime" label="ä¸åæ¶é´" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.operateTime) }}</template> </el-table-column> <el-table-column prop="deviceName" label="设å¤" width="150" /> <el-table-column label="æ§å¶æä»¤" width="100" align="center"> <template #default="{ row }">{{ controlCommandText(row.controlCommand) }}</template> </el-table-column> <el-table-column prop="controlType" label="æ§å¶ç±»å" width="100" align="center" /> <el-table-column prop="operator" label="æä½äºº" width="100" align="center" /> <el-table-column label="æ§è¡ç»æ" width="140" align="center"> <template #default="{ row }"> <!-- æ§è¡ç»æä¸æï¼æå / å·²åç(模æ) / 失败 --> <el-tag v-if="row.execResult === '0' && isSimulate(row)" type="warning" size="small"> å·²åçï¼æ¨¡æï¼ </el-tag> <el-tag v-else-if="row.execResult === '0'" type="success" size="small">æ§è¡æå</el-tag> <el-tag v-else-if="row.execResult === '2'" type="info" size="small">ä¸åä¸</el-tag> <el-tag v-else type="danger" size="small">æ§è¡å¤±è´¥</el-tag> </template> </el-table-column> <el-table-column label="èæ¶" width="90" align="center"> <template #default="{ row }">{{ row.execDuration != null ? row.execDuration + " ms" : "-" }}</template> </el-table-column> <el-table-column prop="resultMsg" label="ç»æè¯´æ" min-width="220" show-overflow-tooltip /> <el-table-column label="æä½" width="90" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" size="small" @click="showControlDetail(row)">é讯æ¥å¿</el-button> </template> </el-table-column> </el-table> </el-tab-pane> </el-tabs> <!-- ==================== è¿ç¨æ§å¶æ§è¡ç»æ ==================== --> <el-dialog v-model="controlResultVisible" title="è¿ç¨æ§å¶æ§è¡ç»æ" width="700px" top="8vh"> <div class="ctrl-result"> <div class="ctrl-result-head" :class="controlResult.execResult === '0' ? 'ok' : 'fail'"> <span class="ctrl-result-icon">{{ controlResult.execResult === '0' ? 'â' : 'â' }}</span> <span>{{ controlResult.deviceName }} · {{ controlResult.action }}</span> </div> <el-descriptions :column="2" border size="small"> <el-descriptions-item label="æ§è¡ç»æ"> <el-tag v-if="controlResult.execResult === '0' && controlResult.simulate" type="warning" size="small"> æä»¤å·²åçï¼æ¨¡ææ¨¡å¼ï¼ </el-tag> <el-tag v-else-if="controlResult.execResult === '0'" type="success" size="small">æ§è¡æå</el-tag> <el-tag v-else-if="controlResult.execResult === '2'" type="info" size="small">ä¸åä¸</el-tag> <el-tag v-else type="danger" size="small">æ§è¡å¤±è´¥</el-tag> </el-descriptions-item> <el-descriptions-item label="æ§è¡èæ¶">{{ controlResult.duration }} ms</el-descriptions-item> <el-descriptions-item label="æ§è¡æ¨¡å¼" :span="2"> <el-tag v-if="controlResult.simulate" type="warning" size="small">æ¨¡ææ¨¡å¼ ââ æªæ¥å ¥ç°åºè®¾å¤ï¼æä»¤æªå®é åå ¥è®¾å¤</el-tag> <el-tag v-else type="success" size="small">çå®ä¸å ââ å·²åå ¥è®¾å¤æ§å¶å¯åå¨</el-tag> </el-descriptions-item> <el-descriptions-item label="ç»æè¯´æ" :span="2">{{ controlResult.resultMsg || "-" }}</el-descriptions-item> </el-descriptions> <div class="ctrl-log-title">é讯æ¥å¿</div> <pre class="ctrl-log">{{ controlResult.detail || "ï¼æ æ¥å¿ï¼" }}</pre> </div> <template #footer> <el-button type="primary" @click="controlResultVisible = false">ç¥éäº</el-button> </template> </el-dialog> <!-- ==================== 设å¤å¼¹çª ==================== --> <el-dialog v-model="deviceDialog" :title="deviceForm.id ? 'ç¼è¾çæ§è®¾å¤' : 'æ°å¢çæ§è®¾å¤'" width="760px" @close="resetDeviceForm"> <el-form ref="deviceFormRef" :model="deviceForm" :rules="deviceRules" label-position="top"> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="设å¤ç¼å·ï¼" prop="deviceCode"> <el-input v-model="deviceForm.deviceCode" placeholder="å¦ WD-001" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="设å¤åç§°ï¼" prop="deviceName"> <el-input v-model="deviceForm.deviceName" placeholder="请è¾å ¥è®¾å¤åç§°" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="设å¤ç±»åï¼"> <el-select v-model="deviceForm.deviceType" style="width: 100%" placeholder="è¯·éæ©"> <el-option v-for="item in deviceTypeList" :key="item" :label="item" :value="item" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="å®è£ ä½ç½®ï¼"> <el-input v-model="deviceForm.location" placeholder="请è¾å ¥ä½ç½®" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="ç产åå®¶ï¼"> <el-input v-model="deviceForm.manufacturer" placeholder="请è¾å ¥åå®¶" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="设å¤åå·ï¼"> <el-input v-model="deviceForm.deviceModel" placeholder="请è¾å ¥åå·" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="éä¿¡æ¹å¼ï¼"> <el-select v-model="deviceForm.commProtocol" style="width: 100%" placeholder="è¯·éæ©" clearable> <el-option label="Modbus" value="Modbus" /> <el-option label="MQTT" value="MQTT" /> <el-option label="OPC UA" value="OPC UA" /> <el-option label="HTTP" value="HTTP" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="éä¿¡å°å(IP)ï¼"> <el-input v-model="deviceForm.ipAddress" placeholder="å¦ 192.168.1.31" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å®è£ æ¥æï¼"> <el-date-picker v-model="deviceForm.installDate" type="date" style="width: 100%" value-format="YYYY-MM-DD" placeholder="éæ©æ¥æ" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="æ¯å¦æ¯æå¨çº¿çæµï¼"> <el-radio-group v-model="deviceForm.monitorEnabled"> <el-radio value="1">æ¯æ</el-radio> <el-radio value="0">䏿¯æ</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ¯å¦æ¯æè¿ç¨æ§å¶ï¼"> <el-radio-group v-model="deviceForm.controlEnabled"> <el-radio value="1">æ¯æ</el-radio> <el-radio value="0">䏿¯æ</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ§å¶ç±»åï¼"> <el-select v-model="deviceForm.controlType" style="width: 100%" placeholder="è¯·éæ©" clearable> <el-option label="å¯å" value="å¯å" /> <el-option label="è°é" value="è°é" /> <el-option label="å¤ä½" value="å¤ä½" /> </el-select> </el-form-item> </el-col> </el-row> <el-form-item label="夿³¨ï¼"> <el-input v-model="deviceForm.remark" type="textarea" :rows="2" placeholder="䏿¯æçæµ/æ§å¶ç设å¤è¯·æ³¨æåå " /> </el-form-item> </el-form> <template #footer> <el-button @click="deviceDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitDevice">确认</el-button> </template> </el-dialog> <!-- ==================== ææ é ç½®å¼¹çª ==================== --> <el-dialog v-model="metricDialog" title="çæµææ é ç½®" width="860px"> <div class="metric-actions"> <el-button type="primary" size="small" @click="addMetricRow">æ°å¢ææ </el-button> </div> <el-table :data="metricRows" border size="small" style="width: 100%"> <el-table-column label="ææ åç§°" width="130"> <template #default="{ row }"> <el-input v-model="row.metricName" placeholder="温度/åå/è½è" /> </template> </el-table-column> <el-table-column label="ææ ç¼ç " width="150"> <template #default="{ row }"> <el-input v-model="row.metricCode" placeholder="temperature" /> </template> </el-table-column> <el-table-column label="åä½" width="90"> <template #default="{ row }"> <el-input v-model="row.unit" placeholder="â" /> </template> </el-table-column> <el-table-column label="æ¥è¦ä¸é" width="120"> <template #default="{ row }"> <el-input-number v-model="row.thresholdMin" :controls="false" style="width: 100%" /> </template> </el-table-column> <el-table-column label="æ¥è¦ä¸é" width="120"> <template #default="{ row }"> <el-input-number v-model="row.thresholdMax" :controls="false" style="width: 100%" /> </template> </el-table-column> <el-table-column label="å¯ç¨æ¥è¦" width="90" align="center"> <template #default="{ row }"> <el-switch v-model="row.alarmEnabled" active-value="1" inactive-value="0" /> </template> </el-table-column> <el-table-column label="æä½" width="80" align="center"> <template #default="{ $index }"> <el-button link type="danger" @click="removeMetricRow($index)">å é¤</el-button> </template> </el-table-column> </el-table> <template #footer> <el-button @click="metricDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitMetrics">ä¿åææ </el-button> </template> </el-dialog> <!-- ==================== æ°æ®ä¸æ¥å¼¹çª ==================== --> <el-dialog v-model="reportDialog" title="忰䏿¥" width="480px"> <el-form label-position="top"> <el-form-item label="设å¤ï¼"> <el-input :model-value="reportForm.deviceName" disabled /> </el-form-item> <el-form-item label="çæµææ ï¼"> <el-input :model-value="reportForm.metricName" disabled /> </el-form-item> <el-form-item label="ééæ°å¼ï¼"> <el-input-number v-model="reportForm.value" :precision="2" style="width: 100%" /> </el-form-item> <el-form-item label="æ°æ®æ¥æºï¼"> <el-radio-group v-model="reportForm.dataSource"> <el-radio value="0">èªå¨éé</el-radio> <el-radio value="1">人工å½å ¥</el-radio> </el-radio-group> </el-form-item> </el-form> <template #footer> <el-button @click="reportDialog = false">åæ¶</el-button> <el-button type="primary" :loading="reportLoading" @click="submitReportData">䏿¥</el-button> </template> </el-dialog> <!-- ==================== çæµè¶å¿ ==================== --> <el-dialog v-model="trendDialog" title="çæµè¶å¿" width="880px"> <div class="trend-header"> <span class="trend-device">{{ trendForm.deviceName }} · {{ trendForm.metricName }}</span> <el-radio-group v-model="trendRange" size="small" @change="loadTrend"> <el-radio-button value="1">è¿1天</el-radio-button> <el-radio-button value="7">è¿7天</el-radio-button> <el-radio-button value="30">è¿30天</el-radio-button> </el-radio-group> </div> <div v-if="trendData.length" style="width: 100%"> <Echarts :chartStyle="trendChartStyle" :grid="trendGrid" :xAxis="trendXAxis" :yAxis="trendYAxis" :series="trendSeries" :tooltip="trendTooltip" style="height: 340px" /> </div> <el-empty v-else description="该æ¶é´æ®µææ ééæ°æ®ï¼å¯å¨ä¸æ¹ææ éå 䏿¥ä¸æ¡åçè¶å¿" /> <template #footer> <el-button @click="trendDialog = false">å ³é</el-button> </template> </el-dialog> </div> </template> <script setup> import { onMounted, onUnmounted, reactive, ref } from "vue"; import { ElLoading, ElMessageBox, ElNotification } from "element-plus"; import Echarts from "@/components/Echarts/echarts.vue"; import { addMonitorDevice, addMonitorMetric, delMonitorAlarm, delMonitorDevice, delMonitorMetric, getMonitorOverview, handleMonitorAlarm, listMonitorAlarm, listMonitorControl, listMonitorDataHistory, listMonitorDevice, listMonitorMetric, reportMonitorData, remoteControlDevice, updateMonitorDevice, updateMonitorMetric, } from "@/api/environmentAccess/deviceMonitor.js"; const { proxy } = getCurrentInstance(); const activeTab = ref("device"); const overview = ref({}); const deviceTypeList = ["æ¸©åº¦çæµ", "ååçæµ", "è½èçæµ", "综åçæµ", "å ¶ä»"]; const formatDateTime = (val) => { if (!val) return "-"; return String(val).replace("T", " ").substring(0, 19); }; const onlineStatusText = (status) => ({ "0": "å¨çº¿", "1": "离线", "2": "æ¥è¦" }[status] || "æªç¥"); const onlineStatusType = (status) => ({ "0": "success", "1": "info", "2": "danger" }[status] || "info"); const alarmLevelText = (level) => ({ "0": "æç¤º", "1": "è¦å", "2": "严é" }[level] || "-"); const alarmLevelType = (level) => ({ "0": "info", "1": "warning", "2": "danger" }[level] || "info"); const controlCommandText = (command) => ({ start: "å¯å¨", stop: "åæº", restart: "éå¯" }[command] || command); // ==================== è®¾å¤ ==================== const deviceQuery = reactive({ deviceName: "", deviceType: "", onlineStatus: "" }); const deviceList = ref([]); const deviceLoading = ref(false); const deviceDialog = ref(false); const deviceFormRef = ref(); const deviceForm = ref({}); const deviceRules = { deviceCode: [{ required: true, message: "请è¾å ¥è®¾å¤ç¼å·", trigger: "blur" }], deviceName: [{ required: true, message: "请è¾å ¥è®¾å¤åç§°", trigger: "blur" }], }; const loadOverview = () => { getMonitorOverview().then((res) => { overview.value = res.data || {}; }); }; const loadDevice = () => { deviceLoading.value = true; listMonitorDevice({ ...deviceQuery, current: 1, size: 300 }) .then(async (res) => { const records = res.data?.records || []; await Promise.all( records.map(async (item) => { try { const metricRes = await listMonitorMetric(item.id); item.metrics = metricRes.data || []; } catch (e) { item.metrics = []; } }) ); deviceList.value = records; }) .finally(() => { deviceLoading.value = false; }); }; const refreshAll = () => { loadOverview(); loadDevice(); loadAlarm(); }; const openDeviceForm = (row) => { deviceForm.value = row ? { ...row } : { monitorEnabled: "1", controlEnabled: "0", runStatus: "0", onlineStatus: "1" }; deviceDialog.value = true; }; const resetDeviceForm = () => { deviceFormRef.value?.resetFields(); deviceForm.value = {}; }; const submitDevice = () => { deviceFormRef.value.validate((valid) => { if (!valid) return; const api = deviceForm.value.id ? updateMonitorDevice : addMonitorDevice; api(deviceForm.value).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); deviceDialog.value = false; loadDevice(); loadOverview(); }); }); }; const removeDevice = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤è®¾å¤ã${row.deviceName}ãåå ¶ææ åï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delMonitorDevice([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadDevice(); loadOverview(); }); }).catch(() => {}); }; const controlDevice = (row, command) => { const actionText = command === "stop" ? "è¿ç¨åæº" : command === "restart" ? "è¿ç¨éå¯" : "è¿ç¨å¯å¨"; const runText = row.runStatus === "1" ? "è¿è¡ä¸" : "å·²åæº"; ElMessageBox.confirm( `<div style="line-height:1.9"> <div>设å¤ï¼<b>${row.deviceName || "-"}</b></div> <div>é讯åè®®ï¼${row.commProtocol || "æªé ç½®"}</div> <div>设å¤å°åï¼${row.ipAddress || "æªé ç½®"}</div> <div>å½åç¶æï¼${runText}</div> <div style="margin-top:8px">æ§è¡å¨ä½ï¼<b style="color:#e6a23c">${actionText}</b></div> <div style="margin-top:8px;color:#909399;font-size:12px">ç³»ç»å°å°è¯è¿æ¥è®¾å¤å¹¶ä¸åæä»¤ï¼æ§è¡è¿ç¨ä¼è®°å½å¨é讯æ¥å¿ä¸</div> </div>`, "è¿ç¨æ§å¶ç¡®è®¤", { type: "warning", dangerouslyUseHTMLString: true, confirmButtonText: "确认æ§è¡", cancelButtonText: "åæ¶", } ).then(() => { const loading = ElLoading.service({ lock: true, text: "æ£å¨è¿æ¥è®¾å¤å¹¶ä¸åæä»¤â¦", background: "rgba(0,0,0,0.4)" }); const startAt = Date.now(); remoteControlDevice({ deviceId: row.id, command }) .then((res) => { const data = (res && res.data) || {}; showControlResult({ deviceName: row.deviceName, action: actionText, execResult: data.execResult, resultMsg: data.resultMsg || "", duration: data.execDuration != null ? data.execDuration : Date.now() - startAt, detail: data.execDetail || "", }); loadDevice(); loadOverview(); loadControl(); }) .catch(() => {}) .finally(() => loading.close()); }).catch(() => {}); }; // ==================== è¿ç¨æ§å¶æ§è¡ç»æå¼¹çª ==================== const controlResultVisible = ref(false); const controlResult = ref({}); const isSimulate = (row) => /æ¨¡ææ¨¡å¼/.test(row.resultMsg || "") || /æ¨¡ææ¨¡å¼/.test(row.execDetail || ""); const showControlResult = (data) => { controlResult.value = { ...data, simulate: isSimulate(data) }; controlResultVisible.value = true; }; /** 仿§å¶è®°å½éæ¥çé讯æ¥å¿ */ const showControlDetail = (row) => { showControlResult({ deviceName: row.deviceName, action: controlCommandText(row.controlCommand), execResult: row.execResult, resultMsg: row.resultMsg || "", duration: row.execDuration != null ? row.execDuration : 0, detail: row.execDetail || "", }); }; // ==================== ææ ==================== const metricDialog = ref(false); const metricRows = ref([]); const currentDeviceId = ref(null); const openMetricDialog = (row) => { currentDeviceId.value = row.id; listMonitorMetric(row.id).then((res) => { metricRows.value = (res.data || []).map((item) => ({ ...item })); if (metricRows.value.length === 0) { addMetricRow(); } metricDialog.value = true; }); }; const addMetricRow = () => { metricRows.value.push({ metricName: "", metricCode: "", unit: "", alarmEnabled: "1", collectType: "0" }); }; const removeMetricRow = (index) => { const row = metricRows.value[index]; if (row.id) { delMonitorMetric([row.id]).then(() => { metricRows.value.splice(index, 1); proxy.$modal.msgSuccess("å·²å é¤"); loadDevice(); }); } else { metricRows.value.splice(index, 1); } }; const submitMetrics = () => { if (!currentDeviceId.value) return; const tasks = metricRows.value.map((row) => { const payload = { ...row, deviceId: currentDeviceId.value }; return payload.id ? updateMonitorMetric(payload) : addMonitorMetric(payload); }); Promise.all(tasks).then(() => { proxy.$modal.msgSuccess("ææ å·²ä¿å"); metricDialog.value = false; loadDevice(); }); }; const isMetricAlarm = (metric) => { if (metric.currentValue === null || metric.currentValue === undefined) return false; const value = Number(metric.currentValue); if (metric.thresholdMin !== null && metric.thresholdMin !== undefined && value < Number(metric.thresholdMin)) { return true; } return metric.thresholdMax !== null && metric.thresholdMax !== undefined && value > Number(metric.thresholdMax); }; const metricAlarmClass = (metric) => (isMetricAlarm(metric) ? "danger-text" : ""); // ==================== çæµè¶å¿ ==================== const trendDialog = ref(false); const trendRange = ref("1"); const trendData = ref([]); const trendForm = reactive({ deviceId: null, deviceName: "", metricCode: "", metricName: "", unit: "" }); const trendChartStyle = { width: "100%", height: "100%" }; const trendGrid = { left: "3%", right: "4%", bottom: "10%", top: "12%", containLabel: true }; const trendTooltip = { trigger: "axis" }; const trendXAxis = ref([{ type: "category", data: [], axisLabel: { rotate: 20 } }]); const trendYAxis = ref([{ type: "value", scale: true }]); const trendSeries = ref([]); const toDateTimeStr = (d) => { const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; const openTrendDialog = (device, metric) => { trendForm.deviceId = device.id; trendForm.deviceName = device.deviceName; trendForm.metricCode = metric.metricCode; trendForm.metricName = metric.metricName; trendForm.unit = metric.unit || ""; trendRange.value = "1"; trendDialog.value = true; loadTrend(); }; const loadTrend = () => { const days = Number(trendRange.value || 1); const end = new Date(); const start = new Date(end.getTime() - days * 24 * 3600 * 1000); listMonitorDataHistory({ deviceId: trendForm.deviceId, metricCode: trendForm.metricCode, startTime: toDateTimeStr(start), endTime: toDateTimeStr(end), }).then((res) => { const list = res.data || []; trendData.value = list; trendXAxis.value[0].data = list.map((item) => formatDateTime(item.collectTime).substring(5, 16)); trendSeries.value = [ { name: trendForm.metricName + (trendForm.unit ? `(${trendForm.unit})` : ""), type: "line", smooth: true, symbol: "circle", symbolSize: 6, data: list.map((item) => Number(item.metricValue)), itemStyle: { color: "#409EFF" }, areaStyle: { opacity: 0.12 }, }, ]; }); }; // ==================== æ°æ®ä¸æ¥ ==================== const reportDialog = ref(false); const reportLoading = ref(false); const reportForm = reactive({ deviceId: null, deviceName: "", metricCode: "", metricName: "", value: 0, dataSource: "0" }); const openReportDialog = (device, metric) => { reportForm.deviceId = device.id; reportForm.deviceName = device.deviceName; reportForm.metricCode = metric.metricCode; reportForm.metricName = metric.metricName; reportForm.value = metric.currentValue == null ? 0 : Number(metric.currentValue); reportForm.dataSource = metric.collectType === "1" ? "1" : "0"; reportDialog.value = true; }; const submitReportData = () => { reportLoading.value = true; reportMonitorData({ deviceId: reportForm.deviceId, metricCode: reportForm.metricCode, value: reportForm.value, dataSource: reportForm.dataSource, }) .then((res) => { reportDialog.value = false; if (res.data?.alarmFlag) { ElNotification({ title: "设å¤åæ°è¶éæ¥è¦", message: res.data.alarm?.alarmContent || "çæµåæ°è¶ åºéå¼", type: "error", duration: 8000, }); } else { proxy.$modal.msgSuccess("æ°æ®ä¸æ¥æå"); } loadDevice(); loadAlarm(); loadOverview(); }) .finally(() => { reportLoading.value = false; }); }; // ==================== æ¥è¦ ==================== const alarmQuery = reactive({ deviceName: "", alarmStatus: "", alarmLevel: "" }); const alarmList = ref([]); const alarmLoading = ref(false); const loadAlarm = () => { alarmLoading.value = true; listMonitorAlarm({ ...alarmQuery, current: 1, size: 300 }) .then((res) => { alarmList.value = res.data?.records || []; }) .finally(() => { alarmLoading.value = false; }); }; const handleAlarm = (row) => { ElMessageBox.prompt("请è¾å ¥å¤ç说æ", "å¤çæ¥è¦", { confirmButtonText: "确认", cancelButtonText: "åæ¶", inputPlaceholder: "å¦ï¼å·²ç°åºææ¥å¹¶æ¢å¤æ£å¸¸", }) .then(({ value }) => { handleMonitorAlarm(row.id, value).then(() => { proxy.$modal.msgSuccess("å·²å¤ç"); loadAlarm(); loadOverview(); loadDevice(); }); }) .catch(() => {}); }; const removeAlarm = (row) => { ElMessageBox.confirm("ç¡®å®å é¤è¯¥æ¥è¦è®°å½åï¼", "å é¤æç¤º", { type: "warning" }).then(() => { delMonitorAlarm([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadAlarm(); loadOverview(); }); }).catch(() => {}); }; // ==================== è¿ç¨æ§å¶è®°å½ ==================== const controlList = ref([]); const controlLoading = ref(false); const loadControl = () => { controlLoading.value = true; listMonitorControl({ current: 1, size: 200 }) .then((res) => { controlList.value = res.data?.records || []; }) .finally(() => { controlLoading.value = false; }); }; const handleTabChange = (name) => { if (name === "device") loadDevice(); if (name === "alarm") loadAlarm(); if (name === "control") loadControl(); }; // æ¥è¦æ¨é轮询 let alarmTimer = null; let lastAlarmCount = 0; const pollAlarm = () => { getMonitorOverview().then((res) => { const data = res.data || {}; overview.value = data; const count = Number(data.unhandledAlarm || 0); if (count > lastAlarmCount && lastAlarmCount >= 0) { ElNotification({ title: "è®¾å¤æ¥è¦æé", message: `å½åæ ${count} æ¡æªå¤çè®¾å¤æ¥è¦ï¼è¯·åæ¶å¤ç`, type: "warning", duration: 8000, }); } lastAlarmCount = count; }); }; onMounted(() => { refreshAll(); loadControl(); alarmTimer = setInterval(pollAlarm, 30000); }); onUnmounted(() => { if (alarmTimer) clearInterval(alarmTimer); }); </script> <style scoped lang="scss"> /* ============ è¿ç¨æ§å¶æ§è¡ç»æå¼¹çª ============ */ .ctrl-result { font-size: 13px; } .ctrl-result-head { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; padding: 10px 14px; border-radius: 6px; margin-bottom: 14px; &.ok { color: #067d3c; background: #eafaf1; border: 1px solid #b7ebd0; } &.fail { color: #c0392b; background: #fef0f0; border: 1px solid #fbc4c4; } } .ctrl-result-icon { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; color: #fff; font-size: 13px; background: #67c23a; .ctrl-result-head.fail & { background: #f56c6c; } } .ctrl-log-title { font-size: 14px; font-weight: 600; color: #303133; border-left: 3px solid #409eff; padding-left: 8px; margin: 16px 0 8px; } .ctrl-log { margin: 0; padding: 12px 14px; background: #1e1e1e; color: #d4d4d4; border-radius: 6px; font-family: Consolas, Monaco, "Courier New", monospace; font-size: 12px; line-height: 1.7; max-height: 260px; overflow: auto; white-space: pre-wrap; word-break: break-all; } .monitor-page { .overview-row { display: flex; gap: 12px; margin-bottom: 16px; .overview-card { flex: 1; background: #f7f9fc; border-radius: 6px; padding: 12px 16px; .label { color: #909399; font-size: 13px; } .value { font-size: 22px; font-weight: 600; margin-top: 6px; color: #303133; &.success { color: #67c23a; } &.danger { color: #f56c6c; } &.info { color: #909399; } } } } .ml10 { margin-left: 10px; } .sub-text { color: #909399; font-size: 12px; } .danger-text { color: #f56c6c; font-weight: 600; } .metric-panel { padding: 10px 20px; .no-monitor { color: #909399; font-size: 13px; padding: 6px 0; } } .alarm-tip { color: #909399; } .disabled-btn { color: #c0c4cc; font-size: 13px; cursor: not-allowed; margin: 0 8px; } .metric-actions { margin-bottom: 10px; } .trend-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; .trend-device { font-weight: 600; color: #303133; } } } </style> src/views/environmentAccess/intelligentInspectionManagement/index.vue
@@ -1,575 +1,931 @@ <template> <div class="app-container inspection-page"> <!-- 页头 --> <div class="page-head"> <div class="head-text"> <h2>æºè½å·¡æ£ç®¡ç</h2> <p>è§åå·¡æ£è·¯å¾ï¼èå¨è§é¢ãæ äººæºææºå¨äººåä¼ ï¼å®æåèªå¨çæå·¡æ£æ¥åã</p> </div> <el-button type="primary" :icon="Plus" @click="openForm()">æ°å»ºå·¡æ£ä»»å¡</el-button> </div> <!-- æ¦è§ --> <div class="stat-row"> <div v-for="s in summary" :key="s.label" class="stat" :class="s.tone"> <div class="stat-top"> <span class="stat-label">{{ s.label }}</span> <el-icon class="stat-icon"><component :is="s.icon" /></el-icon> <el-tabs v-model="activeTab" @tab-change="handleTabChange"> <!-- ==================== å·¡æ£è·¯å¾è§å ==================== --> <el-tab-pane label="å·¡æ£è·¯å¾è§å" name="route"> <div class="search_form"> <div> <span class="search_title">路线åç§°ï¼</span> <el-input v-model="routeQuery.routeName" style="width: 200px" placeholder="请è¾å ¥è·¯çº¿åç§°" clearable @keyup.enter="loadRoute" /> <span class="search_title ml10">æ§è¡æ¹å¼ï¼</span> <el-select v-model="routeQuery.inspectMode" style="width: 160px" placeholder="å ¨é¨" clearable @change="loadRoute"> <el-option v-for="item in inspectModeList" :key="item" :label="item" :value="item" /> </el-select> <el-button type="primary" style="margin-left: 10px" @click="loadRoute">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openRouteForm()">æ°å¢å·¡æ£è·¯å¾</el-button> </div> </div> <div class="stat-value">{{ s.value }}<i>项</i></div> </div> </div> <el-table :data="routeList" border stripe v-loading="routeLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="routeCode" label="路线ç¼å·" width="140" align="center" /> <el-table-column prop="routeName" label="路线åç§°" min-width="160" show-overflow-tooltip /> <el-table-column prop="routeType" label="å·¡æ£ç±»å" width="110" align="center" /> <el-table-column prop="inspectMode" label="æ§è¡æ¹å¼" width="110" align="center" /> <el-table-column prop="areaName" label="å·¡æ£åºå" width="120" /> <el-table-column prop="pointTotal" label="ç¹ä½æ°" width="80" align="center" /> <el-table-column prop="pointNames" label="å·¡æ£é¡ºåº" min-width="240" show-overflow-tooltip /> <el-table-column prop="cycleType" label="å·¡æ£å¨æ" width="100" align="center" /> <el-table-column prop="cycleTime" label="æ§è¡æ¶é´" width="100" align="center" /> <el-table-column prop="executorName" label="å·¡æ£äººå" width="100" /> <el-table-column label="ç¶æ" width="80" align="center"> <template #default="{ row }"> <el-tag :type="row.status === '0' ? 'success' : 'info'" size="small"> {{ row.status === '0' ? 'å¯ç¨' : 'åç¨' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="200" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openRouteForm(row)">ç¼è¾</el-button> <el-button link type="primary" @click="createTaskFromRoute(row)">ä¸åä»»å¡</el-button> <el-button link type="danger" @click="removeRoute(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- æ¥è¯¢ --> <div class="filter-card"> <el-input v-model="query.taskName" placeholder="ä»»å¡åç§°" clearable :prefix-icon="Search" style="width: 200px" @keyup.enter="handleSearch" /> <el-input v-model="query.inspectionLocation" placeholder="å·¡æ£åºå" clearable style="width: 180px" @keyup.enter="handleSearch" /> <el-select v-model="query.taskStatus" placeholder="ç¶æ" clearable style="width: 140px"> <el-option label="å¾ æ§è¡" value="pending" /> <el-option label="æ§è¡ä¸" value="running" /> <el-option label="已宿" value="finished" /> </el-select> <el-select v-model="query.deviceSource" placeholder="ééç»ç«¯" clearable style="width: 150px"> <el-option label="è§é¢åæ" value="video" /> <el-option label="æ 人æº" value="drone" /> <el-option label="å·¡æ£æºå¨äºº" value="robot" /> <el-option label="人工ç»ç«¯" value="manual" /> </el-select> <el-button type="primary" :icon="Search" @click="handleSearch">æ¥è¯¢</el-button> <el-button :icon="Refresh" @click="resetQuery">éç½®</el-button> </div> <!-- ==================== å·¡æ£ä»»å¡ ==================== --> <el-tab-pane label="å·¡æ£ä»»å¡" name="task"> <div class="search_form"> <div> <span class="search_title">路线åç§°ï¼</span> <el-input v-model="taskQuery.routeName" style="width: 180px" placeholder="请è¾å ¥" clearable @keyup.enter="loadTask" /> <span class="search_title ml10">ä»»å¡ç¶æï¼</span> <el-select v-model="taskQuery.taskStatus" style="width: 140px" placeholder="å ¨é¨" clearable @change="loadTask"> <el-option label="å¾ æ§è¡" value="0" /> <el-option label="æ§è¡ä¸" value="1" /> <el-option label="已宿" value="2" /> <el-option label="å·²ç»æ¢" value="3" /> </el-select> <el-button type="primary" style="margin-left: 10px" @click="loadTask">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openTaskForm">ä¸åå·¡æ£ä»»å¡</el-button> </div> </div> <el-table :data="taskList" border stripe v-loading="taskLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="taskCode" label="ä»»å¡ç¼å·" width="170" align="center" /> <el-table-column prop="routeName" label="å·¡æ£è·¯çº¿" min-width="150" show-overflow-tooltip /> <el-table-column prop="inspectMode" label="æ§è¡æ¹å¼" width="110" align="center" /> <el-table-column prop="deviceCode" label="æ§è¡è®¾å¤" width="120" align="center" /> <el-table-column prop="executorName" label="å·¡æ£äººå" width="100" align="center" /> <el-table-column label="è®¡åæ¶é´" width="180" align="center"> <template #default="{ row }"> <div>{{ formatDateTime(row.planStartTime) }}</div> <div class="sub-text">è³ {{ formatDateTime(row.planEndTime) }}</div> </template> </el-table-column> <el-table-column label="ç¹ä½æ°" width="120" align="center"> <template #default="{ row }"> <span>{{ row.pointNormal || 0 }}/{{ row.pointTotal || 0 }}</span> <el-tag v-if="row.pointAbnormal > 0" type="danger" size="small" style="margin-left: 6px"> å¼å¸¸{{ row.pointAbnormal }} </el-tag> </template> </el-table-column> <el-table-column label="ä»»å¡ç¶æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="taskStatusType(row.taskStatus)" size="small">{{ taskStatusText(row.taskStatus) }}</el-tag> </template> </el-table-column> <el-table-column label="æä½" width="300" align="center" fixed="right"> <template #default="{ row }"> <el-button v-if="row.taskStatus === '0'" link type="primary" @click="startTask(row)">å¼å§</el-button> <el-button link type="primary" @click="openRecordDialog(row)">å·¡æ£æç»</el-button> <el-button link type="primary" @click="analyzeTask(row)">è§é¢åæçææ¥å</el-button> <el-button v-if="row.taskStatus !== '2'" link type="primary" @click="finishTask(row)">å®æå¹¶åºæ¥å</el-button> <el-button link type="danger" @click="removeTask(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- è¡¨æ ¼ --> <div class="table-card"> <el-table :data="tasks" v-loading="loading" border stripe row-key="id"> <el-table-column prop="taskName" label="ä»»å¡åç§°" min-width="160" show-overflow-tooltip /> <el-table-column prop="inspectionLocation" label="å·¡æ£åºå" min-width="120" show-overflow-tooltip /> <el-table-column label="è§åè·¯å¾" min-width="240" show-overflow-tooltip> <template #default="{ row }"> <span class="route-text">{{ row.inspectionRoute || 'â' }}</span> </template> </el-table-column> <el-table-column label="ééç»ç«¯" width="130"> <template #default="{ row }"> <el-tag effect="plain" size="small" :type="sourceType(row.deviceSource)"> {{ sourceText(row.deviceSource) }} </el-tag> </template> </el-table-column> <el-table-column prop="inspector" label="å·¡æ£äºº" width="110" /> <el-table-column label="ç¶æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="statusType(row.taskStatus)" size="small">{{ statusText(row.taskStatus) }}</el-tag> </template> </el-table-column> <el-table-column prop="actualStartTime" label="å¼å§æ¶é´" width="165" /> <el-table-column prop="reportTime" label="æ¥åæ¶é´" width="165" /> <el-table-column label="æä½" width="230" fixed="right" align="center"> <template #default="{ row }"> <el-button link type="primary" :icon="Edit" :disabled="row.taskStatus === 'running' || row.taskStatus === 'finished'" @click="openForm(row)">ç¼è¾</el-button> <el-button v-if="!row.taskStatus || row.taskStatus === 'pending'" link type="success" :icon="VideoPlay" @click="start(row)">å¼å§</el-button> <el-button v-if="row.taskStatus === 'running'" link type="warning" :icon="CircleCheck" @click="finish(row)">宿</el-button> <el-button v-if="row.reportContent" link :icon="Document" @click="showReport(row)">æ¥å</el-button> <el-button link type="danger" :icon="Delete" @click="remove(row)">å é¤</el-button> </template> </el-table-column> <template #empty> <div class="table-empty">ææ å·¡æ£ä»»å¡ï¼ç¹å»å³ä¸è§âæ°å»ºå·¡æ£ä»»å¡âå¼å§è§å</div> </template> </el-table> <!-- ==================== å·¡æ£æ¥å ==================== --> <el-tab-pane label="å·¡æ£æ¥å" name="report"> <div class="search_form"> <div> <span class="search_title">æ¥åæ é¢ï¼</span> <el-input v-model="reportQuery.reportTitle" style="width: 200px" placeholder="请è¾å ¥" clearable @keyup.enter="loadReport" /> <el-button type="primary" style="margin-left: 10px" @click="loadReport">æç´¢</el-button> </div> <div class="tip-text">æ¥åç±è§é¢åæ / æ äººæº / æºå¨äººåä¼ ç»æèªå¨æ±æ»çæ</div> </div> <el-table :data="reportList" border stripe v-loading="reportLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="reportCode" label="æ¥åç¼å·" width="170" align="center" /> <el-table-column prop="reportTitle" label="æ¥åæ é¢" min-width="180" show-overflow-tooltip /> <el-table-column prop="routeName" label="å·¡æ£è·¯çº¿" width="150" show-overflow-tooltip /> <el-table-column prop="inspectMode" label="æ§è¡æ¹å¼" width="110" align="center" /> <el-table-column label="ç¹ä½æ°" width="110" align="center"> <template #default="{ row }"> {{ row.pointNormal }}/{{ row.pointTotal }} </template> </el-table-column> <el-table-column label="æ£å¸¸ç" width="100" align="center"> <template #default="{ row }"> <span :class="Number(row.normalRate) >= 90 ? 'text-success' : 'text-warning'"> {{ row.normalRate }}% </span> </template> </el-table-column> <el-table-column label="å¼å¸¸ç¹ä½" width="90" align="center"> <template #default="{ row }"> <el-tag :type="row.pointAbnormal > 0 ? 'danger' : 'success'" size="small"> {{ row.pointAbnormal || 0 }} </el-tag> </template> </el-table-column> <el-table-column prop="generateType" label="çææ¹å¼" width="110" align="center"> <template #default="{ row }"> {{ row.generateType === '0' ? 'èªå¨çæ' : 'æå¨çæ' }} </template> </el-table-column> <el-table-column prop="reportTime" label="çææ¶é´" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.reportTime) }}</template> </el-table-column> <el-table-column label="æä½" width="140" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="viewReport(row)">æ¥çæ¥å</el-button> <el-button link type="danger" @click="removeReport(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <el-pagination v-model:current-page="query.current" v-model:page-size="query.size" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" class="pager" @current-change="load" @size-change="handleSearch" /> </div> <!-- ==================== å·¡æ£ç¹ä½ ==================== --> <el-tab-pane label="å·¡æ£ç¹ä½" name="point"> <div class="search_form"> <div> <span class="search_title">ç¹ä½åç§°ï¼</span> <el-input v-model="pointQuery.pointName" style="width: 180px" placeholder="请è¾å ¥" clearable @keyup.enter="loadPoint" /> <span class="search_title ml10">æå±åºåï¼</span> <el-input v-model="pointQuery.areaName" style="width: 150px" placeholder="请è¾å ¥" clearable @keyup.enter="loadPoint" /> <el-button type="primary" style="margin-left: 10px" @click="loadPoint">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openPointForm()">æ°å¢ç¹ä½</el-button> </div> </div> <el-table :data="pointList" border stripe v-loading="pointLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="pointCode" label="ç¹ä½ç¼å·" width="140" align="center" /> <el-table-column prop="pointName" label="ç¹ä½åç§°" min-width="150" /> <el-table-column prop="areaName" label="æå±åºå" width="130" /> <el-table-column prop="deviceType" label="éé设å¤ç±»å" width="140" align="center" /> <el-table-column prop="deviceCode" label="éé设å¤ç¼å·" width="140" align="center" /> <el-table-column prop="analyzeItem" label="è§é¢åæè¯å«é¡¹" min-width="200" show-overflow-tooltip /> <el-table-column label="ç¶æ" width="80" align="center"> <template #default="{ row }"> <el-tag :type="row.status === '0' ? 'success' : 'info'" size="small"> {{ row.status === '0' ? 'å¯ç¨' : 'åç¨' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="140" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openPointForm(row)">ç¼è¾</el-button> <el-button link type="danger" @click="removePoint(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> </el-tabs> <!-- æ°å»º/ç¼è¾ --> <el-dialog v-model="dialog" :title="form.id ? 'ç¼è¾å·¡æ£ä»»å¡' : 'æ°å»ºå·¡æ£ä»»å¡'" width="700px" destroy-on-close> <el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-form-item label="ä»»å¡åç§°" prop="taskName"> <el-input v-model="form.taskName" placeholder="ä¾å¦ï¼åæåºæ¥å¸¸å·¡æ£" /> </el-form-item> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="å·¡æ£åºå" prop="inspectionLocation"> <el-input v-model="form.inspectionLocation" placeholder="ä¾å¦ï¼åæåº" /> <!-- ==================== è·¯å¾ç¼è¾å¼¹çª ==================== --> <el-dialog v-model="routeDialog" :title="routeForm.id ? 'ç¼è¾å·¡æ£è·¯å¾' : 'æ°å¢å·¡æ£è·¯å¾'" width="70%" @close="resetRouteForm"> <el-form ref="routeFormRef" :model="routeForm" :rules="routeRules" label-position="top"> <el-row :gutter="24"> <el-col :span="8"> <el-form-item label="路线åç§°ï¼" prop="routeName"> <el-input v-model="routeForm.routeName" placeholder="请è¾å ¥è·¯çº¿åç§°" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="ééç»ç«¯" prop="deviceSource"> <el-select v-model="form.deviceSource" style="width: 100%"> <el-option label="è§é¢åæ" value="video" /> <el-option label="æ 人æº" value="drone" /> <el-option label="å·¡æ£æºå¨äºº" value="robot" /> <el-option label="人工ç»ç«¯" value="manual" /> <el-col :span="8"> <el-form-item label="å·¡æ£ç±»åï¼" prop="routeType"> <el-select v-model="routeForm.routeType" style="width: 100%" placeholder="è¯·éæ©"> <el-option v-for="item in routeTypeList" :key="item" :label="item" :value="item" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ§è¡æ¹å¼ï¼" prop="inspectMode"> <el-select v-model="routeForm.inspectMode" style="width: 100%" placeholder="è¯·éæ©"> <el-option v-for="item in inspectModeList" :key="item" :label="item" :value="item" /> </el-select> </el-form-item> </el-col> </el-row> <el-form-item label="è§åè·¯å¾" prop="inspectionRoute"> <el-input v-model="form.inspectionRoute" type="textarea" :rows="3" placeholder="ä¾å¦ï¼åé¨ â åæåº â 1å·äº§çº¿ â å°ç£ æ¿ â åé¨" /> <div class="route-templates"> <span>å¿«æ·æ¨¡æ¿ï¼</span> <el-button v-for="t in routeTemplates" :key="t.label" link type="primary" size="small" @click="form.inspectionRoute = t.value">{{ t.label }}</el-button> <el-row :gutter="24"> <el-col :span="8"> <el-form-item label="å·¡æ£åºåï¼"> <el-input v-model="routeForm.areaName" placeholder="请è¾å ¥å·¡æ£åºå" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å·¡æ£å¨æï¼"> <el-select v-model="routeForm.cycleType" style="width: 100%" placeholder="è¯·éæ©" clearable> <el-option label="æ¯å¤©" value="æ¯å¤©" /> <el-option label="æ¯å¨" value="æ¯å¨" /> <el-option label="æ¯æ" value="æ¯æ" /> <el-option label="ä¸éå¤" value="ä¸éå¤" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ§è¡æ¶é´ï¼"> <el-time-picker v-model="routeForm.cycleTime" style="width: 100%" value-format="HH:mm" format="HH:mm" placeholder="éæ©æ¶é´" /> </el-form-item> </el-col> </el-row> <el-row :gutter="24"> <el-col :span="8"> <el-form-item label="é¢è®¡èæ¶(åé)ï¼"> <el-input-number v-model="routeForm.planDuration" :min="0" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="é»è®¤å·¡æ£äººåï¼"> <el-input v-model="routeForm.executorName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ§è¡è®¾å¤(æ 人æº/æºå¨äººç¼å·)ï¼"> <el-input v-model="routeForm.deviceCode" placeholder="å¦ WRJ-001 / JQR-002" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="å·¡æ£ç¹ä½(æåºå·ä¾æ¬¡å·¡æ£ï¼å¯ä¸ä¸è°æ´é¡ºåº)ï¼"> <el-select v-model="routePointIds" multiple filterable style="width: 100%" placeholder="è¯·éæ©å·¡æ£ç¹ä½"> <el-option v-for="item in allPointList" :key="item.id" :label="`${item.pointName}ï¼${item.areaName || 'æªååº'}ï¼`" :value="item.id" /> </el-select> <div class="point-order-editor"> <div v-for="(name, index) in selectedPointNames" :key="index" class="order-item"> <span class="seq">{{ index + 1 }}</span> <span class="name">{{ name }}</span> <span class="ops"> <el-button link type="primary" :disabled="index === 0" @click="movePoint(index, -1)">ä¸ç§»</el-button> <el-button link type="primary" :disabled="index === routePointIds.length - 1" @click="movePoint(index, 1)">ä¸ç§»</el-button> <el-button link type="danger" @click="removePointAt(index)">ç§»é¤</el-button> </span> </div> <span v-if="selectedPointNames.length === 0" class="sub-text">å°æªéæ©ç¹ä½</span> </div> </el-form-item> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="æ§è¡é¢æ¬¡"> <el-select v-model="form.frequencyType" style="width: 100%"> <el-option label="忬¡" value="once" /> <el-option label="æ¯æ¥" value="daily" /> <el-option label="æ¯å¨" value="weekly" /> </el-select> <el-row :gutter="24"> <el-col :span="8"> <el-form-item label="ç¶æï¼"> <el-radio-group v-model="routeForm.status"> <el-radio value="0">å¯ç¨</el-radio> <el-radio value="1">åç¨</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="å·¡æ£äººå"> <el-input v-model="form.inspector" placeholder="æ§è¡äººå§å" /> <el-col :span="16"> <el-form-item label="夿³¨ï¼"> <el-input v-model="routeForm.remark" placeholder="请è¾å ¥å¤æ³¨" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="è®¡åæ¶é´"> <el-input v-model="form.frequencyDetail" placeholder="å¦ï¼æ¯æ¥ 08:00" /> </el-form-item> <el-form-item label="夿³¨"> <el-input v-model="form.remarks" type="textarea" :rows="2" /> </el-form-item> </el-form> <template #footer> <el-button @click="dialog = false">åæ¶</el-button> <el-button type="primary" :loading="saving" @click="save">ä¿å</el-button> <el-button @click="routeDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitRoute">确认</el-button> </template> </el-dialog> <!-- æ¥å --> <el-dialog v-model="reportDialog" title="èªå¨å·¡æ£æ¥å" width="680px"> <el-descriptions :column="2" border> <el-descriptions-item label="ä»»å¡">{{ current.taskName }}</el-descriptions-item> <el-descriptions-item label="ééç»ç«¯">{{ sourceText(current.deviceSource) }}</el-descriptions-item> <el-descriptions-item label="å·¡æ£åºå">{{ current.inspectionLocation || 'â' }}</el-descriptions-item> <el-descriptions-item label="å·¡æ£äººå">{{ current.inspector || 'â' }}</el-descriptions-item> <el-descriptions-item label="å¼å§æ¶é´">{{ current.actualStartTime || 'â' }}</el-descriptions-item> <el-descriptions-item label="ç»ææ¶é´">{{ current.actualEndTime || 'â' }}</el-descriptions-item> </el-descriptions> <pre class="report-content">{{ current.reportContent }}</pre> <!-- ==================== ä»»å¡ä¸åå¼¹çª ==================== --> <el-dialog v-model="taskDialog" title="ä¸åå·¡æ£ä»»å¡" width="620px" @close="resetTaskForm"> <el-form ref="taskFormRef" :model="taskForm" :rules="taskRules" label-position="top"> <el-form-item label="å·¡æ£è·¯çº¿ï¼" prop="routeId"> <el-select v-model="taskForm.routeId" style="width: 100%" placeholder="è¯·éæ©å·¡æ£è·¯çº¿" @change="onTaskRouteChange"> <el-option v-for="item in routeOptions" :key="item.id" :label="item.routeName" :value="item.id" /> </el-select> </el-form-item> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="计åå¼å§æ¶é´ï¼" prop="planStartTime"> <el-date-picker v-model="taskForm.planStartTime" type="datetime" style="width: 100%" value-format="YYYY-MM-DD HH:mm:ss" placeholder="éæ©æ¶é´" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="计åç»ææ¶é´ï¼"> <el-date-picker v-model="taskForm.planEndTime" type="datetime" style="width: 100%" value-format="YYYY-MM-DD HH:mm:ss" placeholder="éæ©æ¶é´" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="æ§è¡æ¹å¼ï¼"> <el-select v-model="taskForm.inspectMode" style="width: 100%" placeholder="è¯·éæ©"> <el-option v-for="item in inspectModeList" :key="item" :label="item" :value="item" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="å·¡æ£äººåï¼"> <el-input v-model="taskForm.executorName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="æ§è¡è®¾å¤(æ 人æº/æºå¨äººç¼å·)ï¼"> <el-input v-model="taskForm.deviceCode" placeholder="å¦ WRJ-001 / JQR-002" clearable /> </el-form-item> <el-form-item label="夿³¨ï¼"> <el-input v-model="taskForm.remark" type="textarea" :rows="2" placeholder="请è¾å ¥å¤æ³¨" /> </el-form-item> </el-form> <template #footer> <el-button :icon="CopyDocument" @click="copyReport">å¤å¶æ¥å</el-button> <el-button type="primary" @click="reportDialog = false">å ³é</el-button> <el-button @click="taskDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitTask">确认ä¸å</el-button> </template> </el-dialog> <!-- ==================== å·¡æ£æç»å¼¹çª ==================== --> <el-dialog v-model="recordDialog" title="å·¡æ£æç»(è§é¢åæ / 设å¤åä¼ )" width="80%"> <div class="record-summary"> ä»»å¡ï¼{{ currentTask.taskCode }} ï½ è·¯çº¿ï¼{{ currentTask.routeName }} ï½ æ§è¡æ¹å¼ï¼{{ currentTask.inspectMode }} </div> <el-table :data="recordList" border stripe style="width: 100%" max-height="440"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="pointName" label="ç¹ä½" width="140" /> <el-table-column prop="areaName" label="åºå" width="110" /> <el-table-column prop="collectSource" label="æ°æ®æ¥æº" width="110" align="center" /> <el-table-column prop="collectDevice" label="éé设å¤" width="120" align="center" /> <el-table-column prop="collectTime" label="ééæ¶é´" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.collectTime) }}</template> </el-table-column> <el-table-column prop="analyzeItem" label="è¯å«é¡¹" width="140" show-overflow-tooltip /> <el-table-column label="åæç»æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.analyzeResult === '1' ? 'danger' : 'success'" size="small"> {{ row.analyzeResult === '1' ? 'å¼å¸¸' : 'æ£å¸¸' }} </el-tag> </template> </el-table-column> <el-table-column prop="confidence" label="置信度" width="90" align="center" /> <el-table-column prop="description" label="è¯å«æè¿°" min-width="220" show-overflow-tooltip /> </el-table> <template #footer> <el-button @click="recordDialog = false">å ³é</el-button> </template> </el-dialog> <!-- ==================== å·¡æ£æ¥åå¼¹çª ==================== --> <el-dialog v-model="reportDialog" title="å·¡æ£æ¥å" width="80%"> <div class="report-head"> <h3>{{ currentReport.reportTitle }}</h3> <div class="report-meta"> <span>æ¥åç¼å·ï¼{{ currentReport.reportCode }}</span> <span>å·¡æ£è·¯çº¿ï¼{{ currentReport.routeName }}</span> <span>æ§è¡æ¹å¼ï¼{{ currentReport.inspectMode }}</span> <span>çææ¶é´ï¼{{ formatDateTime(currentReport.reportTime) }}</span> </div> <el-alert :title="currentReport.summary" type="info" :closable="false" show-icon /> <el-alert v-if="currentReport.pointAbnormal > 0" :title="currentReport.abnormalDesc" type="error" :closable="false" show-icon style="margin-top: 10px" /> </div> <el-table :data="reportDetails" border stripe style="width: 100%" max-height="380"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="pointName" label="ç¹ä½" width="150" /> <el-table-column prop="areaName" label="åºå" width="120" /> <el-table-column prop="collectSource" label="æ°æ®æ¥æº" width="110" align="center" /> <el-table-column prop="analyzeResult" label="åæç»æ" width="100" align="center" /> <el-table-column prop="analyzeItem" label="è¯å«é¡¹" width="140" show-overflow-tooltip /> <el-table-column prop="collectTime" label="ééæ¶é´" width="170" align="center" /> <el-table-column prop="description" label="è¯å«æè¿°" min-width="220" show-overflow-tooltip /> </el-table> <template #footer> <el-button @click="reportDialog = false">å ³é</el-button> </template> </el-dialog> <!-- ==================== ç¹ä½ç¼è¾å¼¹çª ==================== --> <el-dialog v-model="pointDialog" :title="pointForm.id ? 'ç¼è¾å·¡æ£ç¹ä½' : 'æ°å¢å·¡æ£ç¹ä½'" width="640px" @close="resetPointForm"> <el-form ref="pointFormRef" :model="pointForm" :rules="pointRules" label-position="top"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="ç¹ä½åç§°ï¼" prop="pointName"> <el-input v-model="pointForm.pointName" placeholder="请è¾å ¥ç¹ä½åç§°" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="æå±åºåï¼"> <el-input v-model="pointForm.areaName" placeholder="请è¾å ¥æå±åºå" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="éé设å¤ç±»åï¼"> <el-select v-model="pointForm.deviceType" style="width: 100%" placeholder="è¯·éæ©" clearable> <el-option v-for="item in deviceTypeList" :key="item" :label="item" :value="item" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="éé设å¤ç¼å·ï¼"> <el-input v-model="pointForm.deviceCode" placeholder="æå头/æºå¨äºº/æ 人æºç¼å·" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="è§é¢åæè¯å«é¡¹ï¼"> <el-input v-model="pointForm.analyzeItem" placeholder="å¦ ç®å¸¦è·å,äººåæªæ´å®å ¨å¸½,è®¾å¤æ¼æ²¹" clearable /> </el-form-item> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="å¹³é¢åæ Xï¼"> <el-input v-model="pointForm.positionX" placeholder="å¯é" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="å¹³é¢åæ Yï¼"> <el-input v-model="pointForm.positionY" placeholder="å¯é" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="ç¶æï¼"> <el-radio-group v-model="pointForm.status"> <el-radio value="0">å¯ç¨</el-radio> <el-radio value="1">åç¨</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="夿³¨ï¼"> <el-input v-model="pointForm.remark" placeholder="请è¾å ¥å¤æ³¨" clearable /> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <el-button @click="pointDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitPoint">确认</el-button> </template> </el-dialog> </div> </template> <script setup> import { computed, getCurrentInstance, onBeforeUnmount, onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue"; import { ElMessageBox, ElNotification } from "element-plus"; import { Plus, Search, Refresh, Edit, Delete, VideoPlay, CircleCheck, Document, CopyDocument, List, Clock, Loading, Tickets } from "@element-plus/icons-vue"; import { deleteInspectionTasks, finishInspectionTask, listInspectionTasks, saveInspectionTask, startInspectionTask } from "@/api/environmentAccess/intelligentInspection"; addInspectionPoint, addInspectionTask, addInspectionRoute, analyzeInspectionTask, delInspectionPoint, delInspectionReport, delInspectionRoute, delInspectionTask, finishInspectionTask, getInspectionReport, listAllInspectionPoint, listAllInspectionRoute, listInspectionPoint, listInspectionRecord, listInspectionReport, listInspectionRoute, listInspectionTask, startInspectionTask, updateInspectionPoint, updateInspectionRoute, } from "@/api/environmentAccess/inspection.js"; const { proxy } = getCurrentInstance(); const tasks = ref([]); const total = ref(0); const loading = ref(false); const saving = ref(false); const dialog = ref(false); const reportDialog = ref(false); const current = ref({}); const formRef = ref(null); const query = reactive({ taskName: "", inspectionLocation: "", taskStatus: "", deviceSource: "", current: 1, size: 10, const activeTab = ref("route"); const inspectModeList = ["è§é¢åæ", "æ 人æº", "å·¡æ£æºå¨äºº", "人工"]; const routeTypeList = ["æ¥å¸¸å·¡æ£", "ä¸é¡¹å·¡æ£", "临æ¶å·¡æ£"]; const deviceTypeList = ["æå头", "æ¸©åº¦ä¼ æå¨", "ååä¼ æå¨", "人工ç¹ä½"]; const formatDateTime = (val) => { if (!val) return "-"; return String(val).replace("T", " ").substring(0, 19); }; const taskStatusText = (status) => { return { "0": "å¾ æ§è¡", "1": "æ§è¡ä¸", "2": "已宿", "3": "å·²ç»æ¢" }[status] || "-"; }; const taskStatusType = (status) => { return { "0": "info", "1": "warning", "2": "success", "3": "danger" }[status] || "info"; }; // ==================== å·¡æ£ç¹ä½ ==================== const pointQuery = reactive({ pointName: "", areaName: "" }); const pointList = ref([]); const pointLoading = ref(false); const allPointList = ref([]); const pointDialog = ref(false); const pointFormRef = ref(); const pointForm = ref({}); const pointRules = { pointName: [{ required: true, message: "请è¾å ¥ç¹ä½åç§°", trigger: "blur" }], }; const loadPoint = () => { pointLoading.value = true; listInspectionPoint({ ...pointQuery, current: 1, size: 500 }) .then((res) => { pointList.value = res.data?.records || []; }) .finally(() => { pointLoading.value = false; }); }; const loadAllPoint = () => { listAllInspectionPoint().then((res) => { allPointList.value = res.data || []; }); }; const openPointForm = (row) => { pointForm.value = row ? { ...row } : { status: "0" }; pointDialog.value = true; }; const resetPointForm = () => { pointFormRef.value?.resetFields(); pointForm.value = {}; }; const submitPoint = () => { pointFormRef.value.validate((valid) => { if (!valid) return; const api = pointForm.value.id ? updateInspectionPoint : addInspectionPoint; api(pointForm.value).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); pointDialog.value = false; loadPoint(); loadAllPoint(); }); }); }; const removePoint = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤ç¹ä½ã${row.pointName}ãåï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delInspectionPoint([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadPoint(); loadAllPoint(); }); }).catch(() => {}); }; // ==================== å·¡æ£è·¯å¾ ==================== const routeQuery = reactive({ routeName: "", inspectMode: "" }); const routeList = ref([]); const routeOptions = ref([]); const routeLoading = ref(false); const routeDialog = ref(false); const routeFormRef = ref(); const routeForm = ref({}); const routePointIds = ref([]); const routeRules = { routeName: [{ required: true, message: "请è¾å ¥è·¯çº¿åç§°", trigger: "blur" }], inspectMode: [{ required: true, message: "è¯·éæ©æ§è¡æ¹å¼", trigger: "change" }], }; const selectedPointNames = computed(() => { return routePointIds.value.map((id) => { const target = allPointList.value.find((item) => item.id === id); return target ? target.pointName : id; }); }); const empty = { id: null, taskName: "", inspectionLocation: "", inspectionRoute: "", deviceSource: "video", frequencyType: "once", frequencyDetail: "", inspector: "", remarks: "", taskStatus: "pending", }; const form = reactive({ ...empty }); const rules = { taskName: [{ required: true, message: "请è¾å ¥ä»»å¡åç§°", trigger: "blur" }], inspectionLocation: [{ required: true, message: "请è¾å ¥å·¡æ£åºå", trigger: "blur" }], inspectionRoute: [{ required: true, message: "请è§åå·¡æ£è·¯å¾", trigger: "blur" }], deviceSource: [{ required: true, message: "è¯·éæ©ééç»ç«¯", trigger: "change" }], /** è°æ´å·¡æ£é¡ºåºï¼direction = -1 ä¸ç§» / 1 ä¸ç§» */ const movePoint = (index, direction) => { const list = [...routePointIds.value]; const target = index + direction; if (target < 0 || target >= list.length) { return; } const tmp = list[index]; list[index] = list[target]; list[target] = tmp; routePointIds.value = list; }; const routeTemplates = [ { label: "åæåº", value: "åé¨ â åæåº â 1å·äº§çº¿ â å°ç£ æ¿ â åé¨" }, { label: "产线", value: "æ§å¶å®¤ â 1å·äº§çº¿ â é çµæ¿ â 1å·äº§çº¿" }, { label: "å°ç£ åº", value: "å°ç£ æ¿ â ç§°éå° â 红å¤å¯¹å° â åºå£éé¸" }, { label: "ç½åº", value: "å¨ç½åº â æ³µæ¿ â æ¶é²æ â å¨ç½åº" }, ]; /** ä»å·¡æ£é¡ºåºä¸ç§»é¤æä¸ªç¹ä½ */ const removePointAt = (index) => { const list = [...routePointIds.value]; list.splice(index, 1); routePointIds.value = list; }; const summary = computed(() => [ { label: "å ¨é¨ä»»å¡", value: total.value, icon: List, tone: "tone-brand" }, { label: "å¾ æ§è¡", value: tasks.value.filter(x => !x.taskStatus || x.taskStatus === "pending").length, icon: Clock, tone: "" }, { label: "æ§è¡ä¸", value: tasks.value.filter(x => x.taskStatus === "running").length, icon: Loading, tone: "tone-warn" }, { label: "å·²çææ¥å", value: tasks.value.filter(x => x.reportContent).length, icon: Tickets, tone: "tone-signal" }, ]); const rows = r => r?.data?.records || r?.records || r?.data || []; let timer = null; async function load() { loading.value = true; try { const res = await listInspectionTasks({ ...query }); tasks.value = rows(res); total.value = Number(res?.data?.total ?? res?.total ?? tasks.value.length) || tasks.value.length; } catch (e) { tasks.value = []; total.value = 0; } finally { loading.value = false; } } function handleSearch() { query.current = 1; load(); } function resetQuery() { query.taskName = ""; query.inspectionLocation = ""; query.taskStatus = ""; query.deviceSource = ""; handleSearch(); } function openForm(row = {}) { Object.assign(form, empty, row); dialog.value = true; } async function save() { try { await formRef.value.validate(); } catch (e) { return; } saving.value = true; try { await saveInspectionTask({ ...form }); dialog.value = false; proxy.$modal.msgSuccess("ä»»å¡å·²ä¿å"); load(); } catch (e) { proxy.$modal.msgError("ä¿å失败"); } finally { saving.value = false; } } async function start(row) { try { await startInspectionTask(row.id); proxy.$modal.msgSuccess("å·¡æ£å·²å¯å¨ï¼ééç»ç«¯å¼å§åä¼ "); load(); } catch (e) { // é误信æ¯å·²ç±è¯·æ±æ¦æªå¨æç¤º } } async function finish(row) { try { await finishInspectionTask(row.id); proxy.$modal.msgSuccess("å·¡æ£å®æï¼æ¥åå·²èªå¨çæ"); load(); } catch (e) { // é误信æ¯å·²ç±è¯·æ±æ¦æªå¨æç¤º } } async function remove(row) { try { await proxy.$modal.confirm(`确认å é¤â${row.taskName}âï¼`); } catch (e) { return; } try { await deleteInspectionTasks([row.id]); proxy.$modal.msgSuccess("å 餿å"); load(); } catch (e) { proxy.$modal.msgError("å é¤å¤±è´¥"); } } function showReport(row) { current.value = row; reportDialog.value = true; } async function copyReport() { const text = String(current.value.reportContent || ""); if (!text) { proxy.$modal.msgWarning("ææ å¯å¤å¶çæ¥åå 容"); return; } if (await copyText(text)) { proxy.$modal.msgSuccess("æ¥åå·²å¤å¶"); const loadRoute = () => { routeLoading.value = true; listInspectionRoute({ ...routeQuery, current: 1, size: 500 }) .then((res) => { routeList.value = res.data?.records || []; }) .finally(() => { routeLoading.value = false; }); }; const loadRouteOptions = () => { listAllInspectionRoute().then((res) => { routeOptions.value = res.data || []; }); }; const openRouteForm = (row) => { loadAllPoint(); if (row) { routeForm.value = { ...row }; routePointIds.value = row.pointIds ? row.pointIds.split(",").filter(Boolean).map(Number) : []; } else { proxy.$modal.msgError("å¤å¶å¤±è´¥ï¼è¯·æ£æ¥æµè§å¨åªè´´æ¿æé"); routeForm.value = { status: "0", inspectMode: "è§é¢åæ", routeType: "æ¥å¸¸å·¡æ£" }; routePointIds.value = []; } } async function copyText(text) { // HTTPS/localhost ä¼å ä½¿ç¨æ ååªè´´æ¿æ¥å£ã if (window.isSecureContext && navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return true; } catch (e) { // æé被æç»æ¶ç»§ç»ä½¿ç¨å ¼å®¹æ¹æ¡ã routeDialog.value = true; }; const resetRouteForm = () => { routeFormRef.value?.resetFields(); routeForm.value = {}; routePointIds.value = []; }; const submitRoute = () => { routeFormRef.value.validate((valid) => { if (!valid) return; if (routePointIds.value.length === 0) { proxy.$modal.msgWarning("请è³å°éæ©ä¸ä¸ªå·¡æ£ç¹ä½"); return; } } const payload = { ...routeForm.value }; payload.pointIds = routePointIds.value.join(","); payload.pointNames = selectedPointNames.value.join(" -> "); payload.pointTotal = routePointIds.value.length; const api = payload.id ? updateInspectionRoute : addInspectionRoute; api(payload).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); routeDialog.value = false; loadRoute(); loadRouteOptions(); }); }); }; const removeRoute = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤è·¯çº¿ã${row.routeName}ãåï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delInspectionRoute([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadRoute(); loadRouteOptions(); }); }).catch(() => {}); }; const textarea = document.createElement("textarea"); const activeElement = document.activeElement; textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.left = "-9999px"; textarea.style.top = "0"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.focus(); textarea.select(); textarea.setSelectionRange(0, textarea.value.length); let copied = false; try { copied = document.execCommand("copy"); } catch (e) { copied = false; } finally { document.body.removeChild(textarea); activeElement?.focus?.(); } return copied; } // ==================== å·¡æ£ä»»å¡ ==================== const taskQuery = reactive({ routeName: "", taskStatus: "" }); const taskList = ref([]); const taskLoading = ref(false); const taskDialog = ref(false); const taskFormRef = ref(); const taskForm = ref({}); const taskRules = { routeId: [{ required: true, message: "è¯·éæ©å·¡æ£è·¯çº¿", trigger: "change" }], planStartTime: [{ required: true, message: "è¯·éæ©è®¡åå¼å§æ¶é´", trigger: "change" }], }; const sourceText = v => ({ video: "è§é¢åæ", drone: "æ 人æº", robot: "å·¡æ£æºå¨äºº", manual: "人工ç»ç«¯" }[v] || "人工ç»ç«¯"); const sourceType = v => ({ video: "primary", drone: "success", robot: "warning", manual: "info" }[v] || "info"); const statusText = v => ({ pending: "å¾ æ§è¡", running: "æ§è¡ä¸", finished: "已宿" }[v] || "å¾ æ§è¡"); const statusType = v => ({ pending: "info", running: "warning", finished: "success" }[v] || "info"); const loadTask = () => { taskLoading.value = true; listInspectionTask({ ...taskQuery, current: 1, size: 500 }) .then((res) => { taskList.value = res.data?.records || []; }) .finally(() => { taskLoading.value = false; }); }; const openTaskForm = () => { loadRouteOptions(); taskForm.value = { inspectMode: "è§é¢åæ" }; taskDialog.value = true; }; const createTaskFromRoute = (row) => { loadRouteOptions(); taskForm.value = { routeId: row.id, inspectMode: row.inspectMode, executorName: row.executorName, deviceCode: row.deviceCode, }; taskDialog.value = true; }; const resetTaskForm = () => { taskFormRef.value?.resetFields(); taskForm.value = {}; }; const onTaskRouteChange = (routeId) => { const route = routeOptions.value.find((item) => item.id === routeId); if (route) { taskForm.value.inspectMode = route.inspectMode; taskForm.value.executorName = route.executorName; taskForm.value.deviceCode = route.deviceCode; } }; const submitTask = () => { taskFormRef.value.validate((valid) => { if (!valid) return; addInspectionTask(taskForm.value).then(() => { proxy.$modal.msgSuccess("å·¡æ£ä»»å¡å·²ä¸å"); taskDialog.value = false; activeTab.value = "task"; loadTask(); }); }); }; const startTask = (row) => { startInspectionTask(row.id).then(() => { proxy.$modal.msgSuccess("å·¡æ£å·²å¼å§"); loadTask(); }); }; const analyzeTask = (row) => { ElMessageBox.confirm( "å°æå·¡æ£è·¯å¾éç¹ä½æ§è¡è§é¢åæ/设å¤åä¼ ï¼å¹¶èªå¨æ±æ»çæå·¡æ£æ¥åï¼æ¯å¦ç»§ç»ï¼", "è§é¢åæ", { type: "info" } ).then(() => { analyzeInspectionTask(row.id).then((res) => { const report = res.data || {}; ElNotification({ title: "å·¡æ£æ¥åå·²çæ", message: `æ£å¸¸ ${report.pointNormal || 0} 个ç¹ä½ï¼å¼å¸¸ ${report.pointAbnormal || 0} 个ç¹ä½`, type: report.pointAbnormal > 0 ? "warning" : "success", }); loadTask(); loadReport(); }); }).catch(() => {}); }; const finishTask = (row) => { ElMessageBox.confirm("ç¡®è®¤å®æè¯¥å·¡æ£ä»»å¡å¹¶çæå·¡æ£æ¥åï¼", "æç¤º", { type: "info" }).then(() => { finishInspectionTask(row.id).then(() => { proxy.$modal.msgSuccess("å·¡æ£æ¥åå·²çæ"); loadTask(); loadReport(); }); }).catch(() => {}); }; const removeTask = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤ä»»å¡ã${row.taskCode}ãåï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delInspectionTask([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadTask(); }); }).catch(() => {}); }; // ==================== å·¡æ£æç» ==================== const recordDialog = ref(false); const recordList = ref([]); const currentTask = ref({}); const openRecordDialog = (row) => { currentTask.value = { ...row }; listInspectionRecord(row.id).then((res) => { recordList.value = res.data || []; recordDialog.value = true; }); }; // ==================== å·¡æ£æ¥å ==================== const reportQuery = reactive({ reportTitle: "" }); const reportList = ref([]); const reportLoading = ref(false); const reportDialog = ref(false); const currentReport = ref({}); const reportDetails = ref([]); const loadReport = () => { reportLoading.value = true; listInspectionReport({ ...reportQuery, current: 1, size: 500 }) .then((res) => { reportList.value = res.data?.records || []; }) .finally(() => { reportLoading.value = false; }); }; const viewReport = (row) => { getInspectionReport(row.id).then((res) => { currentReport.value = res.data || {}; try { reportDetails.value = JSON.parse(currentReport.value.reportContent || "[]"); } catch (e) { reportDetails.value = []; } reportDialog.value = true; }); }; const removeReport = (row) => { ElMessageBox.confirm(`ç¡®å®å 餿¥åã${row.reportTitle}ãåï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delInspectionReport([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadReport(); }); }).catch(() => {}); }; const handleTabChange = (name) => { if (name === "route") loadRoute(); if (name === "task") loadTask(); if (name === "report") loadReport(); if (name === "point") loadPoint(); }; onMounted(() => { load(); // ææ§è¡ä¸ä»»å¡æ¶å®æ¶å·æ°ï¼è§å¯èªå¨åä¼ timer = window.setInterval(() => { if (tasks.value.some(t => t.taskStatus === "running")) load(); }, 30000); loadRoute(); loadTask(); loadReport(); loadPoint(); loadAllPoint(); loadRouteOptions(); }); onBeforeUnmount(() => { if (timer) clearInterval(timer); }); </script> <style scoped lang="scss"> $line: #e3e8ed; $paper: #f4f6f8; $text: #1b2733; $muted: #5c6b7a; $brand: #2c51d9; $alert: #e0a02e; $signal: #2fbfa0; .inspection-page { background: $paper; min-height: calc(100vh - 84px); } /* 页头 */ .page-head { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 20px 22px; background: linear-gradient(150deg, #14191f 0%, #1e252c 100%); border-radius: 6px; margin-bottom: 16px; h2 { margin: 0 0 6px; font-size: 20px; font-weight: 700; color: #fff; letter-spacing: 0.03em; .ml10 { margin-left: 10px; } p { margin: 0; .sub-text { color: #909399; font-size: 12px; } .tip-text { color: #909399; font-size: 13px; color: rgba(234, 240, 245, 0.66); } :deep(.el-button--primary) { background: #fff; border-color: #fff; color: #14191f; font-weight: 600; &:hover { background: #e9eef5; border-color: #e9eef5; color: #14191f; .point-order-editor { margin-top: 8px; .order-item { display: flex; align-items: center; gap: 10px; padding: 4px 8px; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 6px; background: #fafafa; .seq { width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #409eff; color: #fff; font-size: 12px; flex: 0 0 auto; } .name { flex: 1; color: #303133; font-size: 13px; } .ops { flex: 0 0 auto; } } } } /* æ¦è§ */ .stat-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 16px; } .stat { position: relative; background: #fff; border: 1px solid $line; border-radius: 6px; padding: 14px 18px 12px; &::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: #c7d0da; .record-summary { margin-bottom: 12px; color: #606266; } &.tone-brand::before { background: $brand; } &.tone-warn::before { background: $alert; } &.tone-signal::before { background: $signal; } } .stat-top { display: flex; align-items: center; justify-content: space-between; } .stat-label { font-size: 12.5px; color: $muted; } .stat-icon { font-size: 16px; color: #b4bfca; } .stat-value { margin-top: 8px; font-family: "MyCustomFont", "Helvetica Neue", Arial, sans-serif; font-size: 26px; font-weight: 700; color: $text; line-height: 1; i { font-style: normal; font-size: 12px; color: #98a4b0; margin-left: 4px; font-family: "Microsoft YaHei", Arial, sans-serif; font-weight: 400; .report-head { margin-bottom: 14px; h3 { margin: 0 0 8px; } .report-meta { display: flex; flex-wrap: wrap; gap: 18px; color: #606266; font-size: 13px; margin-bottom: 10px; } } } /* æ¥è¯¢ */ .filter-card { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 14px 18px; background: #fff; border: 1px solid $line; border-radius: 6px; margin-bottom: 16px; } /* è¡¨æ ¼ */ .table-card { background: #fff; border: 1px solid $line; border-radius: 6px; padding: 14px 16px; } .route-text { color: $muted; font-size: 12.5px; } .pager { justify-content: flex-end; margin-top: 14px; } .table-empty { padding: 26px 0; color: #a7b2bd; font-size: 13px; } /* 表å */ .route-templates { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 12px; color: #98a4b0; } /* æ¥å */ .report-content { margin: 18px 0 0; padding: 18px; line-height: 1.9; background: #f5f7fa; border: 1px solid #e8edf2; border-radius: 6px; font-size: 13px; color: $text; white-space: pre-wrap; font-family: "Microsoft YaHei", Arial, sans-serif; } @media (max-width: 900px) { .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } .page-head { flex-direction: column; align-items: flex-start; } .filter-card { flex-wrap: wrap; } .text-success { color: #67c23a; } .text-warning { color: #e6a23c; } } </style> src/views/environmentAccess/reportCenter/index.vue
@@ -1,17 +1,450 @@ <template> <div class="app-container"> <div class="page-head"><div><h2>æ¥è¡¨ä¸å¿</h2><p>æ¯æ¥è¿ç£ ç»è¥ç»è®¡ä¸æ¿è¿å履约ãä½å¼é£é©åæã</p></div><div><el-date-picker v-model="range" type="daterange" value-format="YYYY-MM-DD" start-placeholder="å¼å§æ¥æ" end-placeholder="ç»ææ¥æ"/><el-button type="primary" :loading="loading" @click="load" style="margin-left:10px">æ¥è¯¢</el-button></div></div> <el-row :gutter="16" class="summary"><el-col v-for="s in cards" :key="s.label" :span="6"><el-card shadow="never"><span>{{s.label}}</span><b>{{s.value}}</b><small>{{s.unit}}</small></el-card></el-col></el-row> <el-tabs v-model="tab"> <el-tab-pane label="æ¯æ¥è¿ç£ ç»è®¡" name="daily"><el-table :data="daily" v-loading="loading" border show-summary><el-table-column prop="date" label="æ¥æ"/><el-table-column prop="tripCount" label="车次"/><el-table-column label="è´§é(å¨)"><template #default="{row}">{{kgToTon(row.cargoVolume)}}</template></el-table-column><el-table-column prop="abnormalCount" label="å¼å¸¸è½¦æ¬¡"/><el-table-column prop="cheatCount" label="ä½å¼æ¬¡æ°"/><el-table-column label="åå·®ç"><template #default="{row}"><el-progress :percentage="Number(row.deviationRate||0)" :status="Number(row.deviationRate)>5?'exception':undefined" /></template></el-table-column></el-table></el-tab-pane> <el-tab-pane label="æ¿è¿å绩æåæ" name="carrier"><el-table :data="carriers" v-loading="loading" border><el-table-column type="index" label="æå" width="70"/><el-table-column prop="carrierName" label="æ¿è¿å" min-width="160"/><el-table-column prop="tripCount" label="æ¿è¿è½¦æ¬¡"/><el-table-column label="è´§é(å¨)"><template #default="{row}">{{kgToTon(row.cargoVolume)}}</template></el-table-column><el-table-column label="åæ¶ç"><template #default="{row}"><el-tag :type="Number(row.onTimeRate)>=95?'success':'warning'">{{row.onTimeRate}}%</el-tag></template></el-table-column><el-table-column prop="cheatCount" label="ä½å¼æ¬¡æ°"/><el-table-column label="绩æè¯å"><template #default="{row}"><b :class="Number(row.score)<80?'danger':''">{{row.score}}</b></template></el-table-column></el-table></el-tab-pane> </el-tabs> <div class="app-container report-center"> <!-- çéåº --> <div class="report-header"> <div class="header-left"> <span class="search_title">ç»è®¡æ¥æï¼</span> <el-date-picker v-model="dateRange" type="datetimerange" range-separator="è³" start-placeholder="å¼å§æ¥ææ¶é´" end-placeholder="ç»ææ¥ææ¶é´" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" :shortcuts="dateShortcuts" style="width: 360px" /> <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æ¥è¯¢</el-button> <el-button @click="handleReset">éç½®</el-button> </div> </div> <!-- åºé´æ»è§ --> <div class="overview-row"> <div class="overview-card"> <div class="label">è¿ç£ 车次</div> <div class="value">{{ overview.totalTrips || 0 }}</div> </div> <div class="overview-card"> <div class="label">è´§é(å¨)</div> <div class="value">{{ overview.totalNetWeight || 0 }}</div> </div> <div class="overview-card"> <div class="label">å¹³ååå·®ç</div> <div class="value">{{ overview.avgDeviation || 0 }}%</div> </div> <div class="overview-card"> <div class="label">ä½å¼/红å¤å¼å¸¸</div> <div class="value danger">{{ overview.cheatCount || 0 }}</div> </div> <div class="overview-card"> <div class="label">åæ¶ç</div> <div class="value success">{{ overview.onTimeRate || 0 }}%</div> </div> <div class="overview-card"> <div class="label">å·²ä¸ä¼ ERP</div> <div class="value">{{ overview.erpSyncedCount || 0 }}</div> </div> </div> <!-- ä¸ãæ¯æ¥è¿ç£ ç»è®¡æ¥è¡¨ï¼è½¦æ¬¡/è´§é/åå·®ç --> <el-card class="report-card" shadow="hover"> <template #header> <div class="card-header"> <span>æ¯æ¥è¿ç£ ç»è®¡æ¥è¡¨</span> <span class="card-desc">车次ãè´§éãåå·®çã红å¤å¼å¸¸æ¬¡æ°</span> </div> </template> <div class="chart-row"> <div class="chart-wrap" style="flex: 1; min-width: 0;"> <Echarts :chartStyle="dailyChartStyle" :grid="dailyGrid" :xAxis="dailyXAxis" :yAxis="dailyYAxis" :series="dailySeries" :tooltip="dailyTooltip" :legend="dailyLegend" style="height: 320px" /> </div> </div> <div class="table-wrap"> <el-table :data="dailyTableData" border stripe v-loading="dailyLoading" style="width: 100%" max-height="360" > <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="statDate" label="æ¥æ" width="140" align="center" /> <el-table-column prop="tripCount" label="车次" align="center" /> <el-table-column prop="cargoVolume" label="è´§é(å¨)" align="center" /> <el-table-column prop="deviationRate" label="åå·®ç(%)" align="center"> <template #default="{ row }"> <span :class="getDeviationClass(row.deviationRate)">{{ row.deviationRate }}%</span> </template> </el-table-column> <el-table-column prop="cheatCount" label="红å¤å¼å¸¸/ä½å¼æ¬¡æ°" align="center"> <template #default="{ row }"> <el-tag :type="Number(row.cheatCount) > 0 ? 'danger' : 'success'" size="small"> {{ row.cheatCount || 0 }} </el-tag> </template> </el-table-column> </el-table> </div> </el-card> <!-- äºãæ¿è¿å绩æåæï¼åæ¶ç/ä½å¼æ¬¡æ° --> <el-card class="report-card" shadow="hover"> <template #header> <div class="card-header"> <span>æ¿è¿å绩æåæ</span> <span class="card-desc">åæ¶çãä½å¼æ¬¡æ°ãå¹³ååå·®çä¸ç»¼åè¯å</span> </div> </template> <div class="chart-row"> <div class="chart-wrap" style="flex: 1; min-width: 0;"> <Echarts :chartStyle="carrierChartStyle" :grid="carrierGrid" :xAxis="carrierXAxis" :yAxis="carrierYAxis" :series="carrierSeries" :tooltip="carrierTooltip" :legend="carrierLegend" style="height: 320px" /> </div> </div> <div class="table-wrap"> <el-table :data="carrierTableData" border stripe v-loading="carrierLoading" style="width: 100%" max-height="420" > <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="carrierName" label="æ¿è¿å" min-width="160" show-overflow-tooltip /> <el-table-column prop="totalTrips" label="æ»è½¦æ¬¡" width="90" align="center" /> <el-table-column prop="onTimeTrips" label="åæ¶è½¦æ¬¡" width="100" align="center" /> <el-table-column prop="lateTrips" label="è¿å°è½¦æ¬¡" width="100" align="center"> <template #default="{ row }"> <span :class="Number(row.lateTrips) > 0 ? 'text-warning' : ''">{{ row.lateTrips || 0 }}</span> </template> </el-table-column> <el-table-column label="åæ¶ç" width="110" align="center"> <template #default="{ row }"> <span :class="getOnTimeClass(row.onTimeRate)">{{ row.onTimeRate }}%</span> </template> </el-table-column> <el-table-column label="ä½å¼æ¬¡æ°" width="110" align="center"> <template #default="{ row }"> <el-tag :type="Number(row.cheatCount) > 0 ? 'danger' : 'success'" size="small"> {{ row.cheatCount || 0 }} </el-tag> </template> </el-table-column> <el-table-column label="ä½å¼ç" width="100" align="center"> <template #default="{ row }"> <span :class="Number(row.cheatRate) > 0 ? 'text-danger' : ''">{{ row.cheatRate }}%</span> </template> </el-table-column> <el-table-column label="å¹³ååå·®ç" width="110" align="center"> <template #default="{ row }">{{ row.avgDeviation }}%</template> </el-table-column> <el-table-column label="æ»è´§é(å¨)" width="110" align="center"> <template #default="{ row }">{{ row.totalNetWeight }}</template> </el-table-column> <el-table-column label="综åè¯å" width="110" align="center"> <template #default="{ row }"> <span class="score">{{ row.score }}</span> </template> </el-table-column> <el-table-column label="绩æç级" width="100" align="center"> <template #default="{ row }"> <el-tag :type="gradeType(row.grade)" size="small">{{ row.grade }}</el-tag> </template> </el-table-column> </el-table> <div v-if="!carrierLoading && carrierTableData.length === 0" class="empty-tip"> ææ æ¿è¿åç»©ææ°æ®ï¼è¯·å¨ãæ 人å¼å®å°ç£ 管ç-èªå¨ç§°éãä¸å®æè¿ç£ ï¼å«æ¿è¿åä¸é¢çº¦å°åºæ¶é´ï¼åæ¥ç </div> </div> </el-card> </div> </template> <script setup> import { computed, onMounted, ref } from "vue"; import { getCarrierPerformance, getDailyWeighbridgeReport } from "@/api/environmentAccess/reportCenter"; const tab=ref("daily"),range=ref([]),daily=ref([]),carriers=ref([]),loading=ref(false); const fmt=d=>`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; function init(){const e=new Date(),s=new Date();s.setDate(s.getDate()-29);range.value=[fmt(s),fmt(e)];} const payload=r=>r?.data?.records||r?.data||r?.records||[]; async function load(){if(!range.value?.length)init();loading.value=true;const params={startDate:`${range.value[0]} 00:00:00`,endDate:`${range.value[1]} 23:59:59`};try{const [d,c]=await Promise.all([getDailyWeighbridgeReport(params),getCarrierPerformance(params)]);daily.value=payload(d);carriers.value=payload(c);}finally{loading.value=false;}} const cards=computed(()=>{const trips=daily.value.reduce((n,x)=>n+Number(x.tripCount||0),0),kg=daily.value.reduce((n,x)=>n+Number(x.cargoVolume||0),0),cheats=daily.value.reduce((n,x)=>n+Number(x.cheatCount||0),0),score=carriers.value.length?carriers.value.reduce((n,x)=>n+Number(x.score||0),0)/carriers.value.length:0;return[{label:"è¿ç£ æ»è½¦æ¬¡",value:trips,unit:"车"},{label:"累计货é",value:(kg/1000).toFixed(2),unit:"å¨"},{label:"ä½å¼æ¥è¦",value:cheats,unit:"次"},{label:"æ¿è¿åå¹³åå",value:score.toFixed(1),unit:"å"}]}); const kgToTon=v=>(Number(v||0)/1000).toFixed(2); onMounted(()=>{init();load();}); import { ref, onMounted, getCurrentInstance } from "vue"; import Echarts from "@/components/Echarts/echarts.vue"; import { getCarrierPerformance, getDailyWeighbridgeReport, getReportOverview, } from "@/api/environmentAccess/reportCenter.js"; const { proxy } = getCurrentInstance(); // æ¥æèå´ const dateRange = ref([]); const dateShortcuts = [ { text: "æè¿7天", value: () => { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); return [start, end]; }}, { text: "æè¿30天", value: () => { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); return [start, end]; }}, { text: "æ¬æ", value: () => { const d = new Date(); return [new Date(d.getFullYear(), d.getMonth(), 1, 0, 0, 0), d]; }}, ]; // é»è®¤æè¿30天 function getDefaultDateRange() { const end = new Date(); const start = new Date(); start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); return [formatDateTime(start), formatDateTime(end)]; } function formatDateTime(d) { const y = d.getFullYear(), m = String(d.getMonth() + 1).padStart(2, "0"), day = String(d.getDate()).padStart(2, "0"), hh = String(d.getHours()).padStart(2, "0"), mm = String(d.getMinutes()).padStart(2, "0"), ss = String(d.getSeconds()).padStart(2, "0"); return `${y}-${m}-${day} ${hh}:${mm}:${ss}`; } // ==================== æ»è§ ==================== const overview = ref({}); function loadOverview() { const [start, end] = getQueryRange(); getReportOverview({ startDate: start, endDate: end }).then((res) => { overview.value = res.data || {}; }); } // ==================== æ¯æ¥è¿ç£ ç»è®¡ ==================== const dailyLoading = ref(false); const dailyTableData = ref([]); const dailyChartStyle = { width: "100%", height: "100%" }; const dailyGrid = { left: "3%", right: "4%", bottom: "12%", top: "10%", containLabel: true }; const dailyLegend = { show: true, bottom: 0, data: ["车次", "è´§é(å¨)", "åå·®ç(%)", "红å¤å¼å¸¸æ¬¡æ°"] }; const dailyTooltip = { trigger: "axis", axisPointer: { type: "shadow" }, }; const dailyXAxis = ref([{ type: "category", data: [], axisLabel: { rotate: 35 } }]); const dailyYAxis = ref([ { type: "value", name: "车次/è´§é/次æ°", position: "left", min: 0 }, { type: "value", name: "åå·®ç%", position: "right", min: 0, max: 100, axisLabel: { formatter: "{value}%" } }, ]); const dailySeries = ref([]); function getDeviationClass(rate) { const v = parseFloat(rate); if (v <= 1) return "text-success"; if (v <= 3) return "text-warning"; return "text-danger"; } function buildDailyChart(arr) { if (!arr || !arr.length) return; dailyXAxis.value[0].data = arr.map((r) => r.statDate); dailySeries.value = [ { name: "车次", type: "bar", data: arr.map((r) => r.tripCount || 0), itemStyle: { color: "#409EFF" } }, { name: "è´§é(å¨)", type: "bar", data: arr.map((r) => Number(r.cargoVolume || 0)), itemStyle: { color: "#67C23A" } }, { name: "红å¤å¼å¸¸æ¬¡æ°", type: "bar", data: arr.map((r) => Number(r.cheatCount || 0)), itemStyle: { color: "#F56C6C" } }, { name: "åå·®ç(%)", type: "line", yAxisIndex: 1, data: arr.map((r) => Number(r.deviationRate || 0)), itemStyle: { color: "#E6A23C" }, symbol: "circle", symbolSize: 8 }, ]; } function loadDailyReport() { dailyLoading.value = true; const [start, end] = getQueryRange(); getDailyWeighbridgeReport({ startDate: start, endDate: end }) .then((res) => { const list = res.data || []; dailyTableData.value = Array.isArray(list) ? list : []; buildDailyChart(dailyTableData.value); }) .finally(() => { dailyLoading.value = false; }); } // ==================== æ¿è¿å绩æåæ ==================== const carrierLoading = ref(false); const carrierTableData = ref([]); const carrierChartStyle = { width: "100%", height: "100%" }; const carrierGrid = { left: "3%", right: "4%", bottom: "16%", top: "10%", containLabel: true }; const carrierLegend = { show: true, bottom: 0, data: ["åæ¶ç(%)", "ä½å¼æ¬¡æ°", "æ»è½¦æ¬¡"] }; const carrierTooltip = { trigger: "axis", axisPointer: { type: "shadow" } }; const carrierXAxis = ref([{ type: "category", data: [], axisLabel: { rotate: 30 } }]); const carrierYAxis = ref([ { type: "value", name: "åæ¶ç%", position: "left", min: 0, max: 100, axisLabel: { formatter: "{value}%" } }, { type: "value", name: "ä½å¼æ¬¡æ°/车次", position: "right", min: 0 }, ]); const carrierSeries = ref([]); function getOnTimeClass(rate) { const v = parseFloat(rate); if (v >= 95) return "text-success"; if (v >= 80) return "text-warning"; return "text-danger"; } function gradeType(grade) { if (grade === "A") return "success"; if (grade === "B") return "primary"; if (grade === "C") return "warning"; if (grade === "D") return "danger"; return "info"; } function buildCarrierChart(arr) { if (!arr || !arr.length) return; carrierXAxis.value[0].data = arr.map((r) => r.carrierName || "æªç¥æ¿è¿å"); carrierSeries.value = [ { name: "åæ¶ç(%)", type: "bar", data: arr.map((r) => Number(r.onTimeRate || 0)), itemStyle: { color: "#409EFF" } }, { name: "ä½å¼æ¬¡æ°", type: "bar", yAxisIndex: 1, data: arr.map((r) => Number(r.cheatCount || 0)), itemStyle: { color: "#F56C6C" } }, { name: "æ»è½¦æ¬¡", type: "line", yAxisIndex: 1, data: arr.map((r) => Number(r.totalTrips || 0)), itemStyle: { color: "#67C23A" }, symbol: "circle", symbolSize: 8 }, ]; } function loadCarrierPerformance() { carrierLoading.value = true; const [start, end] = getQueryRange(); getCarrierPerformance({ startDate: start, endDate: end }) .then((res) => { const list = res.data || []; carrierTableData.value = Array.isArray(list) ? list : []; buildCarrierChart(carrierTableData.value); }) .finally(() => { carrierLoading.value = false; }); } // ==================== æ¥è¯¢æ§å¶ ==================== function getQueryRange() { if (dateRange.value && dateRange.value.length === 2) { return dateRange.value; } return getDefaultDateRange(); } function handleQuery() { if (!dateRange.value || dateRange.value.length !== 2) { dateRange.value = getDefaultDateRange(); } loadOverview(); loadDailyReport(); loadCarrierPerformance(); } function handleReset() { dateRange.value = getDefaultDateRange(); handleQuery(); } onMounted(() => { dateRange.value = getDefaultDateRange(); handleQuery(); }); </script> <style scoped lang="scss">.page-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}h2{margin:0 0 6px}p{margin:0;color:#909399}.summary{margin-bottom:14px}.summary .el-card :deep(.el-card__body){display:flex;align-items:baseline;gap:8px}.summary span{flex:1;color:#909399}.summary b{font-size:28px}.summary small{color:#909399}.danger{color:#f56c6c}@media(max-width:900px){.page-head{align-items:flex-start;gap:12px;flex-direction:column}}</style> <style scoped lang="scss"> .report-center { .overview-row { display: flex; gap: 12px; margin-bottom: 16px; .overview-card { flex: 1; background: #f7f9fc; border-radius: 6px; padding: 12px 16px; .label { color: #909399; font-size: 13px; } .value { font-size: 22px; font-weight: 600; margin-top: 6px; color: #303133; &.success { color: #67c23a; } &.danger { color: #f56c6c; } } } } .report-header { display: flex; align-items: center; margin-bottom: 16px; .header-left { display: flex; align-items: center; } .search_title { margin-right: 8px; white-space: nowrap; } } .report-card { margin-bottom: 20px; .card-header { display: flex; align-items: center; gap: 12px; .card-desc { color: #909399; font-size: 13px; } } .chart-row { display: flex; gap: 16px; margin-bottom: 16px; .chart-wrap { flex: 1; min-width: 0; } } .table-wrap { margin-top: 12px; } .empty-tip { color: #909399; font-size: 13px; padding: 10px 4px; } .score { font-weight: 600; color: #303133; } } .text-success { color: #67c23a; } .text-warning { color: #e6a23c; } .text-danger { color: #f56c6c; } } </style> src/views/environmentAccess/unattendedWeighbridgeSystem/index.vue
@@ -1,616 +1,990 @@ <template> <div class="app-container weighbridge-page"> <!-- 页头 --> <div class="page-head"> <div class="head-text"> <h2>æ 人å¼å®å°ç£ </h2> <p>èªå¨ééç®éãæ¯éï¼çº¢å¤é²ä½å¼ï¼å¹é éè´/éå®è®¢åå¹¶æ¨é ERPã</p> </div> <el-button type="primary" :icon="VideoPlay" @click="openWeigh">å¼å§èªå¨ç§°é</el-button> </div> <!-- æ¦è§ --> <div class="stat-row"> <div class="stat"> <span class="stat-label">è¿ç£ è®°å½</span> <span class="stat-value">{{ total }}<i>æ¡</i></span> <div class="overview-row"> <div class="overview-card"> <div class="label">仿¥è¿ç£ 车次</div> <div class="value">{{ overview.todayCount || 0 }}</div> </div> <div class="stat"> <span class="stat-label">æ¬é¡µå¼å¸¸</span> <span class="stat-value warn">{{ pageAbnormal }}<i>æ¡</i></span> <div class="overview-card"> <div class="label">å¾ å®æç§°é</div> <div class="value warning">{{ overview.pendingCount || 0 }}</div> </div> <div class="stat"> <span class="stat-label">æ¬é¡µæªå¹é 订å</span> <span class="stat-value">{{ pageUnmatched }}<i>æ¡</i></span> <div class="overview-card"> <div class="label">仿¥çº¢å¤å¼å¸¸/ä½å¼</div> <div class="value danger">{{ overview.cheatCount || 0 }}</div> </div> <div class="stat"> <span class="stat-label">æ¬é¡µå¾ æ¨é ERP</span> <span class="stat-value">{{ pagePendingErp }}<i>æ¡</i></span> <div class="overview-card"> <div class="label">å¾ ä¸ä¼ ERP</div> <div class="value">{{ overview.erpPendingCount || 0 }}</div> </div> </div> <!-- æ¥è¯¢ --> <div class="filter-card"> <span class="filter-label">车çå·</span> <el-input v-model="query.plateNumber" placeholder="请è¾å ¥è½¦çå·" clearable :prefix-icon="Search" style="width: 240px" @keyup.enter="handleSearch" @clear="handleSearch" /> <el-button type="primary" :icon="Search" @click="handleSearch">æ¥è¯¢</el-button> <el-button :icon="Refresh" @click="resetQuery">éç½®</el-button> </div> <!-- è¡¨æ ¼ --> <div class="table-card"> <el-table :data="rows" v-loading="loading" border stripe row-key="id"> <el-table-column prop="weighNo" label="ç£ åå·" min-width="170" /> <el-table-column prop="plateNumber" label="车çå·" width="110" /> <el-table-column prop="carrierName" label="æ¿è¿å" min-width="130" show-overflow-tooltip /> <el-table-column prop="weightTime" label="ç§°éæ¶é´" width="170" /> <el-table-column prop="tareWeight" label="ç®é(kg)" width="100" align="right" /> <el-table-column prop="grossWeight" label="æ¯é(kg)" width="100" align="right" /> <el-table-column label="åé(kg)" width="100" align="right"> <template #default="{ row }">{{ netOf(row) }}</template> </el-table-column> <el-table-column label="çº¢å¤æ£æµ" width="110" align="center"> <template #default="{ row }"> <el-tag v-if="row.infraredStatus" :type="row.infraredStatus === 'normal' ? 'success' : 'danger'" effect="plain" size="small"> {{ row.infraredStatus === 'normal' ? 'æ£å¸¸' : 'åç§»æ¥è¦' }} </el-tag> <span v-else class="muted-cell">â</span> </template> </el-table-column> <el-table-column label="订åå¹é " width="120" align="center"> <template #default="{ row }"> <el-tag :type="row.orderMatched ? 'success' : 'info'" effect="plain" size="small"> {{ row.orderMatched ? (row.orderNo || 'å·²å¹é ') : 'æªå¹é ' }} </el-tag> </template> </el-table-column> <el-table-column label="ERP" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.erpPushStatus === 'success' ? 'success' : 'info'" effect="plain" size="small"> {{ row.erpPushStatus === 'success' ? 'å·²ä¸ä¼ ' : 'å¾ ä¸ä¼ ' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="190" fixed="right" align="center"> <template #default="{ row }"> <el-button link type="primary" :icon="Printer" @click="printTicket(row)">ç£ å</el-button> <el-button v-if="row.erpPushStatus !== 'success'" link type="success" :icon="Upload" :loading="pushingId === row.id" @click="pushErp(row)" >æ¨é ERP</el-button> </template> </el-table-column> <template #empty> <div class="table-empty">ææ è¿ç£ è®°å½ï¼ç¹å»å³ä¸è§âå¼å§èªå¨ç§°éâåå»ºç¬¬ä¸æ¡</div> </template> </el-table> <el-pagination v-model:current-page="query.current" v-model:page-size="query.size" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" class="pager" @current-change="load" @size-change="handleSearch" /> </div> <!-- èªå¨ç§°é --> <el-dialog v-model="dialog" title="èªå¨ç§°é" width="640px" destroy-on-close> <el-alert title="çº¢å¤æ£æµéå¼ä¸º 5%ï¼è½¦è¾æªå®å ¨ä¸ç£ ä¼èªå¨æ¥è¦å¹¶æ è®°å¼å¸¸" type="info" :closable="false" show-icon class="weigh-tip" /> <el-form ref="formRef" :model="form" :rules="rules" label-width="110px" class="weigh-form"> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="车çå·" prop="plateNumber"> <el-input v-model="form.plateNumber" placeholder="请è¾å ¥è½¦çå·" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="æ¿è¿å"> <el-input v-model="form.carrierName" placeholder="ç¨äºå¹é 订åä¸ç»©æ" /> </el-form-item> </el-col> </el-row> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="ç®é(kg)" prop="tareWeight"> <el-input-number v-model="form.tareWeight" :min="0" :controls="false" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="æ¯é(kg)" prop="grossWeight"> <el-input-number v-model="form.grossWeight" :min="0" :controls="false" style="width: 100%" /> </el-form-item> </el-col> </el-row> <el-row :gutter="16"> <el-col :span="12"> <el-form-item label="车è¾åç§»(%)"> <el-input-number v-model="form.positionOffset" :min="0" :max="100" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="å°ç£ ç¼å·"> <el-input v-model="form.weighbridgeNumber" /> </el-form-item> </el-col> </el-row> <el-form-item label="è´§ç©åç§°"> <el-input v-model="form.cargoName" /> </el-form-item> <div class="net-preview"> é¢è®¡åéï¼<b>{{ netPreview }}</b> kg <el-tabs v-model="activeTab" @tab-change="handleTabChange"> <!-- ==================== èªå¨ç§°é ==================== --> <el-tab-pane label="èªå¨ç§°é" name="auto"> <div class="search_form"> <div> <span class="search_title">车çå·ï¼</span> <el-input v-model="recordQuery.plateNumber" style="width: 130px" placeholder="请è¾å ¥" clearable @keyup.enter="loadRecord" /> <span class="search_title ml10">ç£ åå·ï¼</span> <el-input v-model="recordQuery.recordNo" style="width: 150px" placeholder="请è¾å ¥" clearable @keyup.enter="loadRecord" /> <span class="search_title ml10">æ¿è¿åï¼</span> <el-input v-model="recordQuery.carrierName" style="width: 130px" placeholder="请è¾å ¥" clearable @keyup.enter="loadRecord" /> <span class="search_title ml10">ç¶æï¼</span> <el-select v-model="recordQuery.weighStatus" style="width: 120px" placeholder="å ¨é¨" clearable @change="loadRecord"> <el-option label="å¾ ç®é" value="0" /> <el-option label="å¾ æ¯é" value="1" /> <el-option label="已宿" value="2" /> </el-select> <span class="search_title ml10">ä½å¼æ è®°ï¼</span> <el-select v-model="recordQuery.cheatFlag" style="width: 110px" placeholder="å ¨é¨" clearable @change="loadRecord"> <el-option label="æ£å¸¸" value="0" /> <el-option label="ä½å¼å«ç" value="1" /> </el-select> <el-button type="primary" style="margin-left: 10px" @click="loadRecord">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openFirstWeight">ç®éç§°é</el-button> <el-button @click="openVerifyDialog">æ«ç æ ¸éªç£ å</el-button> </div> </div> <el-table :data="recordList" border stripe v-loading="recordLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="recordNo" label="ç£ åå·" width="160" align="center" /> <el-table-column prop="plateNumber" label="车çå·" width="110" align="center" /> <el-table-column prop="carrierName" label="æ¿è¿å" width="140" show-overflow-tooltip /> <el-table-column prop="cargoName" label="è´§ç©" width="110" show-overflow-tooltip /> <el-table-column label="å ³è订å" min-width="180"> <template #default="{ row }"> <template v-if="row.orderNo"> <el-tag size="small" :type="row.orderType === '0' ? 'success' : 'warning'"> {{ row.orderType === '0' ? 'éè´' : (row.orderType === '1' ? 'éå®' : 'æªå¹é ') }} </el-tag> <span class="order-no">{{ row.orderNo }}</span> <div v-if="row.orderParty" class="sub-text">{{ row.orderParty }}</div> </template> <span v-else class="sub-text">-</span> </template> </el-table-column> <el-table-column prop="tareWeight" label="ç®é(kg)" width="110" align="center" /> <el-table-column prop="grossWeight" label="æ¯é(kg)" width="110" align="center" /> <el-table-column prop="netWeight" label="åé(kg)" width="110" align="center" /> <el-table-column label="åå·®ç" width="100" align="center"> <template #default="{ row }"> <span v-if="row.orderDeviation != null" :class="deviationClass(row.orderDeviation)"> {{ row.orderDeviation }}% </span> <span v-else class="sub-text">-</span> </template> </el-table-column> <el-table-column label="红å¤/ä½å¼" width="120" align="center"> <template #default="{ row }"> <el-tag v-if="row.cheatFlag === '1'" type="danger" size="small">ä½å¼å«ç</el-tag> <el-tag v-else-if="row.irStatus === '2'" type="warning" size="small">ä½ç½®åç§»</el-tag> <el-tag v-else type="success" size="small">æ£å¸¸</el-tag> <div v-if="row.positionOffset != null" class="sub-text">åç§»{{ row.positionOffset }}cm</div> </template> </el-table-column> <el-table-column label="ERP" width="110" align="center"> <template #default="{ row }"> <el-tag :type="row.erpSyncStatus === '1' ? 'success' : 'info'" size="small"> {{ row.erpSyncStatus === '1' ? 'å·²ä¸ä¼ ' : 'å¾ ä¸ä¼ ' }} </el-tag> </template> </el-table-column> <el-table-column label="ç§°éç¶æ" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.weighStatus === '2' ? 'success' : 'warning'" size="small"> {{ weighStatusText(row.weighStatus) }} </el-tag> </template> </el-table-column> <el-table-column label="ç§°éæ¶é´" width="170" align="center"> <template #default="{ row }">{{ formatDateTime(row.secondWeightTime || row.firstWeightTime) }}</template> </el-table-column> <el-table-column label="æä½" width="300" align="center" fixed="right"> <template #default="{ row }"> <el-button v-if="row.weighStatus !== '2'" link type="primary" @click="openSecondWeight(row)"> æ¯éç§°é </el-button> <el-button link type="primary" @click="openInfraredDialog(row)">çº¢å¤æ£æµ</el-button> <el-button link type="primary" @click="openPrintDialog(row)">æå°ç£ å</el-button> <el-button v-if="row.erpSyncStatus !== '1'" link type="primary" @click="syncErp(row)">ä¸ä¼ ERP</el-button> <el-button link type="primary" @click="openDetail(row)">详æ </el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- ==================== æå·¥è¿ç£ è®°å½ ==================== --> <el-tab-pane label="æå·¥è¿ç£ è®°å½" name="manual"> <div class="search_form"> <div> <span class="search_title">车çå·ï¼</span> <el-input v-model="manualQuery.plateNumber" style="width: 200px" placeholder="请è¾å ¥è½¦çå·" clearable @keyup.enter="loadManual" /> <el-button type="primary" style="margin-left: 10px" @click="loadManual">æç´¢</el-button> </div> <div> <el-button type="primary" @click="openManualForm()">æ°å¢è®°å½</el-button> </div> </div> <el-table :data="manualList" border stripe v-loading="manualLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="plateNumber" label="车çå·" width="130" align="center" /> <el-table-column prop="weightTime" label="ç§°éæ¶é´" width="180" align="center"> <template #default="{ row }">{{ formatDateTime(row.weightTime) }}</template> </el-table-column> <el-table-column prop="tareWeight" label="ç®é(kg)" width="110" align="center" /> <el-table-column prop="grossWeight" label="æ¯é(kg)" width="110" align="center" /> <el-table-column prop="positionOffset" label="å离度" width="100" align="center" /> <el-table-column prop="driverName" label="叿º" width="110" align="center" /> <el-table-column prop="contactPhone" label="èç³»çµè¯" width="130" align="center" /> <el-table-column prop="cargoName" label="è´§ç©åç§°" width="130" /> <el-table-column prop="weighbridgeNumber" label="å°ç£ ç¼å·" width="110" align="center" /> <el-table-column label="æä½" width="140" align="center" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openManualForm(row)">ç¼è¾</el-button> <el-button link type="danger" @click="removeManual(row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!-- ==================== ERPä¸ä¼ æ¥å¿ ==================== --> <el-tab-pane label="ERPä¸ä¼ æ¥å¿" name="erpLog"> <el-table :data="erpLogList" border stripe v-loading="erpLogLoading" style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="recordNo" label="ç£ åå·" width="170" align="center" /> <el-table-column label="åæ¥ç¶æ" width="110" align="center"> <template #default="{ row }"> <el-tag :type="row.syncStatus === '1' ? 'success' : 'warning'" size="small"> {{ row.syncStatus === '1' ? 'æå' : 'å¾ ä¸ä¼ ' }} </el-tag> </template> </el-table-column> <el-table-column prop="syncTime" label="忥æ¶é´" width="180" align="center"> <template #default="{ row }">{{ formatDateTime(row.syncTime) }}</template> </el-table-column> <el-table-column prop="remark" label="说æ" min-width="220" show-overflow-tooltip /> <el-table-column prop="responseBody" label="ERPè¿å" min-width="240" show-overflow-tooltip /> <el-table-column label="æä½" width="100" align="center"> <template #default="{ row }"> <el-button link type="primary" @click="viewErpPayload(row)">æ¥çæ¥æ</el-button> </template> </el-table-column> </el-table> </el-tab-pane> </el-tabs> <!-- ==================== ç®éç§°é ==================== --> <el-dialog v-model="firstWeightDialog" title="ç¬¬ä¸æ¬¡ç§°éï¼ç®éï¼" width="720px" @close="resetFirstWeight"> <el-form ref="firstWeightRef" :model="firstWeightForm" :rules="firstWeightRules" label-position="top"> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="车çå·(车çè¯å«)ï¼" prop="plateNumber"> <el-input v-model="firstWeightForm.plateNumber" placeholder="请è¾å ¥è½¦çå·" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å°ç£ ç¼å·ï¼"> <el-input v-model="firstWeightForm.weighbridgeCode" placeholder="å¦ DB-01" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="ç®éèªå¨éé(kg)ï¼" prop="tareWeight"> <el-input-number v-model="firstWeightForm.tareWeight" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="æ¿è¿åï¼"> <el-input v-model="firstWeightForm.carrierName" placeholder="å¯ç空èªå¨å¸¦åº" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="叿ºï¼"> <el-input v-model="firstWeightForm.driverName" placeholder="叿ºå§å" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="èç³»çµè¯ï¼"> <el-input v-model="firstWeightForm.driverPhone" placeholder="èç³»çµè¯" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="è´§ç©åç§°ï¼"> <el-input v-model="firstWeightForm.cargoName" placeholder="å¦ åç ¤" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="订åå·(éè´/éå®ååå·)ï¼"> <el-input v-model="firstWeightForm.orderNo" placeholder="è¾å ¥åèªå¨å¹é 订å" clearable @blur="autoMatchOrder" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="订åéé(kg)ï¼"> <el-input-number v-model="firstWeightForm.orderWeight" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="é¢çº¦å°åºæ¶é´(ç¨äºåæ¶çç»è®¡)ï¼"> <el-date-picker v-model="firstWeightForm.appointmentTime" type="datetime" style="width: 100%" value-format="YYYY-MM-DD HH:mm:ss" placeholder="计åå°åºæ¶é´" /> </el-form-item> </el-col> </el-row> <el-alert v-if="orderMatchMessage" :title="orderMatchMessage" :type="orderMatchSuccess ? 'success' : 'warning'" :closable="false" show-icon style="margin-bottom: 12px" /> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="红å¤å¯¹å°ç¶æï¼"> <el-select v-model="firstWeightForm.irStatus" style="width: 100%"> <el-option label="æ£å¸¸" value="0" /> <el-option label="鮿¡" value="1" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="车è¾åç§»é(cm)ï¼"> <el-input-number v-model="firstWeightForm.positionOffset" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å 许åç§»éå¼(cm)ï¼"> <el-input-number v-model="firstWeightForm.offsetLimit" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> </el-row> <el-alert title="è½¦è¾æªå®å ¨ä¸ç£ æåç§»è¶ éæ¶ï¼ç³»ç»å°æ è®°ä½å¼å«çå¹¶æç¤ºæ¥è¦ï¼é²æ¢åç£ ä½å¼ã" type="info" :closable="false" show-icon /> </el-form> <template #footer> <el-button @click="dialog = false">åæ¶</el-button> <el-button type="primary" :loading="saving" @click="submit">å®æç§°é</el-button> <el-button @click="firstWeightDialog = false">åæ¶</el-button> <el-button type="primary" :loading="submitLoading" @click="submitFirstWeight">确认ééç®é</el-button> </template> </el-dialog> <!-- çµåç£ å --> <el-dialog v-model="ticketDialog" title="çµåç£ å" width="520px"> <div class="ticket"> <h2>æ 人å¼å®çµåç£ å</h2> <p class="ticket-no">ç£ åå·ï¼{{ ticket.weighNo }}</p> <el-descriptions :column="2" border> <el-descriptions-item label="车ç">{{ ticket.plateNumber }}</el-descriptions-item> <el-descriptions-item label="æ¿è¿å">{{ ticket.carrierName || 'â' }}</el-descriptions-item> <el-descriptions-item label="ç®é">{{ ticket.tareWeight }} kg</el-descriptions-item> <el-descriptions-item label="æ¯é">{{ ticket.grossWeight }} kg</el-descriptions-item> <el-descriptions-item label="åé">{{ ticket.netWeight }} kg</el-descriptions-item> <el-descriptions-item label="订å">{{ ticket.orderNo || 'æªå¹é ' }}</el-descriptions-item> </el-descriptions> <img v-if="qrUrl" :src="qrUrl" class="qr" alt="ç£ åäºç»´ç " /> <p class="qr-tip">æ«æäºç»´ç 坿 ¸éªæ¬æ¬¡ç£ å</p> <!-- ==================== æ¯éç§°é ==================== --> <el-dialog v-model="secondWeightDialog" title="ç¬¬äºæ¬¡ç§°éï¼æ¯éï¼" width="620px"> <el-descriptions :column="2" border size="small" style="margin-bottom: 14px"> <el-descriptions-item label="ç£ åå·">{{ currentRecord.recordNo }}</el-descriptions-item> <el-descriptions-item label="车çå·">{{ currentRecord.plateNumber }}</el-descriptions-item> <el-descriptions-item label="ç®é">{{ currentRecord.tareWeight }} kg</el-descriptions-item> <el-descriptions-item label="æ¿è¿å">{{ currentRecord.carrierName || '-' }}</el-descriptions-item> </el-descriptions> <el-form label-position="top"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="æ¯éèªå¨éé(kg)ï¼"> <el-input-number v-model="secondWeightForm.grossWeight" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="红å¤å¯¹å°ç¶æï¼"> <el-select v-model="secondWeightForm.irStatus" style="width: 100%"> <el-option label="æ£å¸¸" value="0" /> <el-option label="鮿¡" value="1" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="车è¾åç§»é(cm)ï¼"> <el-input-number v-model="secondWeightForm.positionOffset" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="åé(kg)ï¼"> <el-input :model-value="computedNetWeight" disabled /> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <el-button @click="secondWeightDialog = false">åæ¶</el-button> <el-button type="primary" :loading="submitLoading" @click="submitSecondWeight">确认é鿝é并宿è¿ç£ </el-button> </template> </el-dialog> <!-- ==================== çº¢å¤æ£æµ ==================== --> <el-dialog v-model="infraredDialog" title="红å¤åç§»æ£æµ" width="480px"> <el-form label-position="top"> <el-form-item label="ç£ åå·ï¼"> <el-input :model-value="currentRecord.recordNo" disabled /> </el-form-item> <el-form-item label="红å¤å¯¹å°ç¶æï¼"> <el-select v-model="infraredForm.irStatus" style="width: 100%"> <el-option label="æ£å¸¸" value="0" /> <el-option label="鮿¡" value="1" /> <el-option label="ä½ç½®åç§»" value="2" /> </el-select> </el-form-item> <el-form-item label="车è¾åç§»é(cm)ï¼"> <el-input-number v-model="infraredForm.positionOffset" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-form> <el-alert v-if="infraredResult.message" :title="infraredResult.message" :type="infraredResult.alarm ? 'error' : 'success'" :closable="false" show-icon /> <template #footer> <el-button @click="infraredDialog = false">å ³é</el-button> <el-button type="primary" @click="submitInfrared">æ£æµ</el-button> </template> </el-dialog> <!-- ==================== æå°ç£ å ==================== --> <el-dialog v-model="printDialog" title="ç£ åæå°" width="520px"> <div ref="printArea" class="ticket"> <div class="ticket-title">æ 人å¼å®å°ç£ ç§°éå</div> <div class="ticket-no">ç£ åå·ï¼{{ printData.recordNo }}</div> <div class="ticket-body"> <div class="ticket-left"> <p><span>车çå·ï¼</span>{{ printData.plateNumber }}</p> <p><span>æ¿è¿åï¼</span>{{ printData.carrierName || '-' }}</p> <p><span>è´§ç©ï¼</span>{{ printData.cargoName || '-' }}</p> <p><span>订åå·ï¼</span>{{ printData.orderNo || '-' }}</p> <p><span>ç®éï¼</span>{{ printData.tareWeight }} kg</p> <p><span>æ¯éï¼</span>{{ printData.grossWeight }} kg</p> <p class="net"><span>åéï¼</span>{{ printData.netWeight }} kg</p> <p><span>ç§°éæ¶é´ï¼</span>{{ formatDateTime(printData.secondWeightTime) }}</p> <p><span>å°ç£ ç¼å·ï¼</span>{{ printData.weighbridgeCode || '-' }}</p> </div> <div class="ticket-right"> <img v-if="qrCodeUrl" :src="qrCodeUrl" alt="ç£ åäºç»´ç " class="qr-img" /> <div class="qr-tip">æ«ç æ ¸éªç£ å</div> </div> </div> <div class="ticket-footer">æ¬ç£ åç±æ 人å¼å®ç³»ç»èªå¨çæï¼æ«æäºç»´ç 坿 ¸éªç伪</div> </div> <template #footer> <el-button @click="ticketDialog = false">å ³é</el-button> <el-button type="primary" :icon="Printer" @click="print">æå°ç£ å</el-button> <el-button @click="printDialog = false">å ³é</el-button> <el-button type="primary" @click="doPrint">æå°ç£ å</el-button> </template> </el-dialog> <!-- ==================== æ«ç æ ¸éª ==================== --> <el-dialog v-model="verifyDialog" title="æ«ç æ ¸éªç£ å" width="560px"> <el-form label-position="top"> <el-form-item label="äºç»´ç å 容ï¼"> <el-input v-model="verifyCode" type="textarea" :rows="3" placeholder="æ«ç æªæ«æææå·¥ç²è´´äºç»´ç å 容" /> </el-form-item> </el-form> <el-alert v-if="verifyResult.message" :title="verifyResult.message" :type="verifyResult.valid ? 'success' : 'error'" :closable="false" show-icon /> <el-descriptions v-if="verifyResult.record" :column="2" border size="small" style="margin-top: 12px"> <el-descriptions-item label="ç£ åå·">{{ verifyResult.record.recordNo }}</el-descriptions-item> <el-descriptions-item label="车çå·">{{ verifyResult.record.plateNumber }}</el-descriptions-item> <el-descriptions-item label="åé">{{ verifyResult.record.netWeight }} kg</el-descriptions-item> <el-descriptions-item label="æ¿è¿å">{{ verifyResult.record.carrierName || '-' }}</el-descriptions-item> </el-descriptions> <template #footer> <el-button @click="verifyDialog = false">å ³é</el-button> <el-button type="primary" @click="doVerify">æ ¸éª</el-button> </template> </el-dialog> <!-- ==================== 详æ ==================== --> <el-dialog v-model="detailDialog" title="è¿ç£ 详æ " width="760px"> <el-descriptions :column="3" border size="small"> <el-descriptions-item label="ç£ åå·">{{ detail.recordNo }}</el-descriptions-item> <el-descriptions-item label="车çå·">{{ detail.plateNumber }}</el-descriptions-item> <el-descriptions-item label="æ¿è¿å">{{ detail.carrierName || '-' }}</el-descriptions-item> <el-descriptions-item label="è´§ç©">{{ detail.cargoName || '-' }}</el-descriptions-item> <el-descriptions-item label="订åç±»å"> {{ detail.orderType === '0' ? 'éè´' : (detail.orderType === '1' ? 'éå®' : '-') }} </el-descriptions-item> <el-descriptions-item label="订åå·">{{ detail.orderNo || '-' }}</el-descriptions-item> <el-descriptions-item label="æ¥å¾åä½">{{ detail.orderParty || '-' }}</el-descriptions-item> <el-descriptions-item label="订åéé">{{ detail.orderWeight || '-' }}</el-descriptions-item> <el-descriptions-item label="åå·®ç">{{ detail.orderDeviation != null ? detail.orderDeviation + '%' : '-' }}</el-descriptions-item> <el-descriptions-item label="ç®é">{{ detail.tareWeight }}</el-descriptions-item> <el-descriptions-item label="æ¯é">{{ detail.grossWeight }}</el-descriptions-item> <el-descriptions-item label="åé">{{ detail.netWeight }}</el-descriptions-item> <el-descriptions-item label="红å¤ç¶æ">{{ irStatusText(detail.irStatus) }}</el-descriptions-item> <el-descriptions-item label="åç§»é">{{ detail.positionOffset != null ? detail.positionOffset + ' cm' : '-' }}</el-descriptions-item> <el-descriptions-item label="ä½å¼æ è®°"> <el-tag :type="detail.cheatFlag === '1' ? 'danger' : 'success'" size="small"> {{ detail.cheatFlag === '1' ? 'ä½å¼å«ç' : 'æ£å¸¸' }} </el-tag> </el-descriptions-item> <el-descriptions-item label="ä½å¼åå " :span="3">{{ detail.cheatReason || '-' }}</el-descriptions-item> <el-descriptions-item label="ç®éæ¶é´">{{ formatDateTime(detail.firstWeightTime) }}</el-descriptions-item> <el-descriptions-item label="æ¯éæ¶é´">{{ formatDateTime(detail.secondWeightTime) }}</el-descriptions-item> <el-descriptions-item label="ERPç¶æ">{{ detail.erpSyncStatus === '1' ? 'å·²ä¸ä¼ ' : 'å¾ ä¸ä¼ ' }}</el-descriptions-item> <el-descriptions-item label="æå°æ¬¡æ°">{{ detail.printCount || 0 }}</el-descriptions-item> <el-descriptions-item label="äºç»´ç å 容" :span="2">{{ detail.qrCode || '-' }}</el-descriptions-item> </el-descriptions> <template #footer> <el-button @click="detailDialog = false">å ³é</el-button> </template> </el-dialog> <!-- ==================== æå·¥è®°å½å¼¹çª ==================== --> <el-dialog v-model="manualDialog" :title="manualForm.id ? 'ç¼è¾è¿ç£ è®°å½' : 'æ°å¢è¿ç£ è®°å½'" width="720px" @close="resetManualForm"> <el-form ref="manualFormRef" :model="manualForm" :rules="manualRules" label-position="top"> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="车çå·ï¼" prop="plateNumber"> <el-input v-model="manualForm.plateNumber" placeholder="请è¾å ¥è½¦çå·" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="ç§°éæ¶é´ï¼" prop="weightTime"> <el-date-picker v-model="manualForm.weightTime" type="datetime" style="width: 100%" value-format="YYYY-MM-DD HH:mm:ss" placeholder="éæ©æ¶é´" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å°ç£ ç¼å·ï¼"> <el-input v-model="manualForm.weighbridgeNumber" placeholder="请è¾å ¥å°ç£ ç¼å·" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="ç®é(kg)ï¼" prop="tareWeight"> <el-input-number v-model="manualForm.tareWeight" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="æ¯é(kg)ï¼" prop="grossWeight"> <el-input-number v-model="manualForm.grossWeight" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å离度ï¼"> <el-input-number v-model="manualForm.positionOffset" :precision="2" :min="0" style="width: 100%" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="叿ºå§åï¼"> <el-input v-model="manualForm.driverName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="èç³»çµè¯ï¼"> <el-input v-model="manualForm.contactPhone" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="è´§ç©åç§°ï¼"> <el-input v-model="manualForm.cargoName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="夿³¨ï¼"> <el-input v-model="manualForm.remark" type="textarea" :rows="2" placeholder="请è¾å ¥å¤æ³¨" /> </el-form-item> </el-form> <template #footer> <el-button @click="manualDialog = false">åæ¶</el-button> <el-button type="primary" @click="submitManual">确认</el-button> </template> </el-dialog> </div> </template> <script setup> import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue"; import { ElMessage, ElMessageBox } from "element-plus"; import QRCode from "qrcode"; import { Search, Refresh, Printer, Upload, VideoPlay } from "@element-plus/icons-vue"; import { autoWeigh, listWeighbridgeSystem, pushWeighbridgeToErp } from "@/api/environmentAccess/unattendedWeighbridgeSystem"; import { addWeighbridgeSystem, delWeighbridgeSystem, firstWeighbridgeWeight, getWeighbridgeOverview, getWeighbridgePrintInfo, getWeighbridgeRecord, getWeighbridgeSystem, infraredCheck, listWeighbridgeErpLog, listWeighbridgeRecord, listWeighbridgeSystem, matchWeighbridgeOrder, secondWeighbridgeWeight, syncWeighbridgeErp, updateWeighbridgeRecord, updateWeighbridgeSystem, verifyWeighbridgeQr, } from "@/api/environmentAccess/unattendedWeighbridgeSystem.js"; const { proxy } = getCurrentInstance(); const rows = ref([]); const loading = ref(false); const saving = ref(false); const pushingId = ref(null); const total = ref(0); const dialog = ref(false); const ticketDialog = ref(false); const ticket = ref({}); const qrUrl = ref(""); const formRef = ref(null); const query = reactive({ plateNumber: "", current: 1, size: 20 }); const emptyForm = { plateNumber: "", carrierName: "", tareWeight: null, grossWeight: null, positionOffset: 0, weighbridgeNumber: "WB-01", cargoName: "", const activeTab = ref("auto"); const overview = ref({}); const submitLoading = ref(false); const formatDateTime = (val) => { if (!val) return "-"; return String(val).replace("T", " ").substring(0, 19); }; const form = reactive({ ...emptyForm }); const weighStatusText = (status) => ({ "0": "å¾ ç®é", "1": "å¾ æ¯é", "2": "已宿" }[status] || "-"); const irStatusText = (status) => ({ "0": "æ£å¸¸", "1": "鮿¡", "2": "ä½ç½®åç§»" }[status] || "-"); const deviationClass = (value) => { const num = Number(value); if (num <= 1) return "text-success"; if (num <= 3) return "text-warning"; return "text-danger"; }; const rules = { // ==================== èªå¨ç§°éè®°å½ ==================== const recordQuery = reactive({ plateNumber: "", recordNo: "", carrierName: "", weighStatus: "", cheatFlag: "" }); const recordList = ref([]); const recordLoading = ref(false); const loadOverview = () => { getWeighbridgeOverview().then((res) => { overview.value = res.data || {}; }); }; const loadRecord = () => { recordLoading.value = true; listWeighbridgeRecord({ ...recordQuery, current: 1, size: 300 }) .then((res) => { recordList.value = res.data?.records || []; }) .finally(() => { recordLoading.value = false; }); }; // ==================== ç®éç§°é ==================== const firstWeightDialog = ref(false); const firstWeightRef = ref(); const firstWeightForm = ref({}); const firstWeightRules = { plateNumber: [{ required: true, message: "请è¾å ¥è½¦çå·", trigger: "blur" }], tareWeight: [{ required: true, message: "请è¾å ¥ç®é", trigger: "change" }], grossWeight: [{ required: true, message: "请è¾å ¥æ¯é", trigger: "change" }], tareWeight: [{ required: true, message: "请è¾å ¥ç®é", trigger: "blur" }], }; const orderMatchMessage = ref(""); const orderMatchSuccess = ref(false); const openFirstWeight = () => { firstWeightForm.value = { irStatus: "0", offsetLimit: 30, positionOffset: 0 }; orderMatchMessage.value = ""; orderMatchSuccess.value = false; firstWeightDialog.value = true; }; const resetFirstWeight = () => { firstWeightRef.value?.resetFields(); firstWeightForm.value = {}; }; const autoMatchOrder = () => { const orderNo = firstWeightForm.value.orderNo; if (!orderNo) { orderMatchMessage.value = ""; return; } matchWeighbridgeOrder(orderNo).then((res) => { const data = res.data || {}; orderMatchSuccess.value = !!data.matched; orderMatchMessage.value = data.matched ? `${data.message}ï¼${data.orderParty || ''}ï¼${data.projectName || 'æ 项ç®'}ï¼` : data.message; }); }; const submitFirstWeight = () => { firstWeightRef.value.validate((valid) => { if (!valid) return; submitLoading.value = true; firstWeighbridgeWeight(firstWeightForm.value) .then((res) => { const record = res.data || {}; if (record.cheatFlag === "1") { ElMessage({ message: `çº¢å¤æ£æµå¼å¸¸ï¼${record.cheatReason}`, type: "error", duration: 6000 }); } else { proxy.$modal.msgSuccess(`ç®éééæåï¼ç£ åå· ${record.recordNo}`); } firstWeightDialog.value = false; loadRecord(); loadOverview(); if (record.cheatFlag !== "1") { openSecondWeight(record); } }) .finally(() => { submitLoading.value = false; }); }); }; const netPreview = computed(() => { const t = Number(form.tareWeight); const g = Number(form.grossWeight); if (!t || !g || g <= t) return "â"; return (g - t).toFixed(2); // ==================== æ¯éç§°é ==================== const secondWeightDialog = ref(false); const secondWeightForm = reactive({ grossWeight: null, irStatus: "0", positionOffset: 0 }); const currentRecord = ref({}); const computedNetWeight = computed(() => { const tare = Number(currentRecord.value.tareWeight || 0); const gross = Number(secondWeightForm.grossWeight || 0); if (!gross) return ""; return (gross - tare).toFixed(2); }); function netOf(row) { if (row.netWeight !== null && row.netWeight !== undefined) return Number(row.netWeight).toFixed(2); if (row.grossWeight !== null && row.tareWeight !== null) { return (Number(row.grossWeight) - Number(row.tareWeight)).toFixed(2); } return "â"; } const pageAbnormal = computed(() => rows.value.filter(r => r.infraredStatus && r.infraredStatus !== "normal").length); const pageUnmatched = computed(() => rows.value.filter(r => !r.orderMatched).length); const pagePendingErp = computed(() => rows.value.filter(r => r.erpPushStatus !== "success").length); async function load() { loading.value = true; try { const res = await listWeighbridgeSystem({ ...query }); const data = res?.data ?? res; rows.value = data?.records || []; total.value = Number(data?.total) || 0; } catch (e) { rows.value = []; total.value = 0; } finally { loading.value = false; } } function handleSearch() { query.current = 1; load(); } function resetQuery() { query.plateNumber = ""; handleSearch(); } function openWeigh() { Object.assign(form, emptyForm); dialog.value = true; } async function submit() { try { await formRef.value.validate(); } catch (e) { const openSecondWeight = (row) => { currentRecord.value = { ...row }; secondWeightForm.grossWeight = null; secondWeightForm.irStatus = "0"; secondWeightForm.positionOffset = row.positionOffset != null ? Number(row.positionOffset) : 0; secondWeightDialog.value = true; }; const submitSecondWeight = () => { if (!secondWeightForm.grossWeight) { proxy.$modal.msgWarning("请è¾å ¥æ¯é"); return; } if (Number(form.grossWeight) <= Number(form.tareWeight)) { proxy.$modal.msgError("æ¯éå¿ é¡»å¤§äºç®é"); submitLoading.value = true; secondWeighbridgeWeight({ id: currentRecord.value.id, grossWeight: secondWeightForm.grossWeight, irStatus: secondWeightForm.irStatus, positionOffset: secondWeightForm.positionOffset, }) .then((res) => { const record = res.data || {}; if (record.cheatFlag === "1") { ElMessage({ message: `çº¢å¤æ£æµå¼å¸¸ï¼${record.cheatReason}`, type: "error", duration: 6000 }); } else { proxy.$modal.msgSuccess(`è¿ç£ 宿ï¼åé ${record.netWeight} kg`); } secondWeightDialog.value = false; loadRecord(); loadOverview(); if (record.weighStatus === "2") { openPrintDialog(record); } }) .finally(() => { submitLoading.value = false; }); }; // ==================== çº¢å¤æ£æµ ==================== const infraredDialog = ref(false); const infraredForm = reactive({ irStatus: "0", positionOffset: 0 }); const infraredResult = ref({}); const openInfraredDialog = (row) => { currentRecord.value = { ...row }; infraredForm.irStatus = row.irStatus || "0"; infraredForm.positionOffset = row.positionOffset != null ? Number(row.positionOffset) : 0; infraredResult.value = {}; infraredDialog.value = true; }; const submitInfrared = () => { infraredCheck({ recordId: currentRecord.value.id, positionOffset: infraredForm.positionOffset, irStatus: infraredForm.irStatus, }).then((res) => { infraredResult.value = res.data || {}; loadRecord(); loadOverview(); }); }; // ==================== æå°ç£ å ==================== const printDialog = ref(false); const printData = ref({}); const qrCodeUrl = ref(""); const printArea = ref(); const openPrintDialog = (row) => { getWeighbridgePrintInfo(row.id).then(async (res) => { const data = res.data || {}; printData.value = data.record || {}; qrCodeUrl.value = await QRCode.toDataURL(data.qrCode || "", { width: 180, margin: 1 }); printDialog.value = true; loadRecord(); }); }; const doPrint = () => { const html = printArea.value?.innerHTML || ""; const win = window.open("", "_blank", "width=520,height=680"); if (!win) { proxy.$modal.msgWarning("æµè§å¨æ¦æªäºæå°çªå£ï¼è¯·å 许弹çªåéè¯"); return; } saving.value = true; try { const res = await autoWeigh({ ...form }); const result = res?.data || {}; dialog.value = false; if (result.cheatFlag) { proxy.$modal.msgWarning("ç§°é宿ï¼çº¢å¤æ£æµåç°è½¦è¾åç§»ï¼å·²æ è®°å¼å¸¸"); } else { proxy.$modal.msgSuccess("ç§°é宿ï¼ç£ åå·²çæ"); } load(); } catch (e) { proxy.$modal.msgError("ç§°é失败ï¼è¯·ç¨åéè¯"); } finally { saving.value = false; } } win.document.write(`<!DOCTYPE html><html><head><title>ç£ åæå°</title> <style> body{font-family:"Microsoft YaHei",Arial,sans-serif;padding:16px;} .ticket{border:1px solid #333;padding:14px;} .ticket-title{text-align:center;font-size:20px;font-weight:bold;margin-bottom:6px;} .ticket-no{text-align:right;font-size:12px;margin-bottom:8px;} .ticket-body{display:flex;justify-content:space-between;} .ticket-left p{margin:4px 0;font-size:13px;} .ticket-left p span{display:inline-block;width:76px;color:#555;} .ticket-left p.net{font-weight:bold;font-size:15px;} .qr-img{width:150px;height:150px;} .qr-tip{text-align:center;font-size:12px;color:#666;} .ticket-footer{margin-top:10px;font-size:12px;color:#666;text-align:center;} </style></head><body><div class="ticket">${html}</div></body></html>`); win.document.close(); win.focus(); setTimeout(() => { win.print(); }, 300); }; async function pushErp(row) { pushingId.value = row.id; try { await pushWeighbridgeToErp(row.id); proxy.$modal.msgSuccess("ç§°éæ°æ®å·²æ¨éè³ ERP"); load(); } catch (e) { proxy.$modal.msgError("ERP æ¨é失败"); } finally { pushingId.value = null; } } // ==================== æ«ç æ ¸éª ==================== const verifyDialog = ref(false); const verifyCode = ref(""); const verifyResult = ref({}); async function printTicket(row) { const netWeight = row.netWeight ?? ( row.grossWeight != null && row.tareWeight != null ? Number(row.grossWeight) - Number(row.tareWeight) : "" const openVerifyDialog = () => { verifyCode.value = ""; verifyResult.value = {}; verifyDialog.value = true; }; const doVerify = () => { if (!verifyCode.value) { proxy.$modal.msgWarning("è¯·æ«ææç²è´´äºç»´ç å 容"); return; } verifyWeighbridgeQr(verifyCode.value.trim()).then((res) => { verifyResult.value = res.data || {}; }); }; // ==================== ERP ==================== const syncErp = (row) => { ElMessageBox.confirm(`确认å°ç£ å ${row.recordNo} ä¸ä¼ è³ ERPï¼`, "ä¸ä¼ ERP", { type: "info" }).then(() => { syncWeighbridgeErp(row.id).then((res) => { const data = res.data || {}; if (data.success) { proxy.$modal.msgSuccess(data.message); } else { ElMessage({ message: data.message, type: "warning", duration: 6000 }); } loadRecord(); loadErpLog(); loadOverview(); }); }).catch(() => {}); }; const erpLogList = ref([]); const erpLogLoading = ref(false); const loadErpLog = () => { erpLogLoading.value = true; listWeighbridgeErpLog({ current: 1, size: 200 }) .then((res) => { erpLogList.value = res.data?.records || []; }) .finally(() => { erpLogLoading.value = false; }); }; const viewErpPayload = (row) => { ElMessageBox.alert( `<div style="max-height:400px;overflow:auto;white-space:pre-wrap;font-size:12px">${row.requestBody || ""}</div>`, "ä¸ä¼ æ¥æ", { dangerouslyUseHTMLString: true } ); const weighNo = cleanQrValue(row.weighNo) || `WB${formatQrTime(row.weightTime)}${String(row.id || 0).padStart(3, "0")}`; ticket.value = { ...row, weighNo, netWeight }; // å§ç»æ ¹æ®å½åè®°å½é建äºç»´ç ï¼é¿å åå² qrCode 䏿®çå符串 "null"ã const content = [ weighNo, cleanQrValue(row.plateNumber), cleanQrValue(netWeight), cleanQrValue(row.weightTime), cleanQrValue(row.carrierName), cleanQrValue(row.weighbridgeNumber), ].join("|"); qrUrl.value = await QRCode.toDataURL(content, { width: 180, margin: 1 }); ticketDialog.value = true; } }; function cleanQrValue(value) { return value == null || String(value).toLowerCase() === "null" ? "" : String(value); } // ==================== 详æ ==================== const detailDialog = ref(false); const detail = ref({}); const openDetail = (row) => { getWeighbridgeRecord(row.id).then((res) => { detail.value = res.data || {}; detailDialog.value = true; }); }; function formatQrTime(value) { const digits = cleanQrValue(value).replace(/\D/g, ""); return (digits || String(Date.now())).slice(0, 14).padEnd(14, "0"); } // ==================== æå·¥è¿ç£ è®°å½ ==================== const manualQuery = reactive({ plateNumber: "" }); const manualList = ref([]); const manualLoading = ref(false); const manualDialog = ref(false); const manualFormRef = ref(); const manualForm = ref({}); const manualRules = { plateNumber: [{ required: true, message: "请è¾å ¥è½¦çå·", trigger: "blur" }], weightTime: [{ required: true, message: "è¯·éæ©ç§°éæ¶é´", trigger: "change" }], tareWeight: [{ required: true, message: "请è¾å ¥ç®é", trigger: "blur" }], grossWeight: [{ required: true, message: "请è¾å ¥æ¯é", trigger: "blur" }], }; function print() { const t = ticket.value; const rowsHtml = [ ["车ç", t.plateNumber], ["æ¿è¿å", t.carrierName || "â"], ["ç®é", `${t.tareWeight ?? "â"} kg`], ["æ¯é", `${t.grossWeight ?? "â"} kg`], ["åé", `${t.netWeight ?? "â"} kg`], ["订å", t.orderNo || "æªå¹é "], ["ç§°éæ¶é´", t.weightTime || "â"], ].map(([k, v]) => `<tr><th>${k}</th><td>${v}</td></tr>`).join(""); const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>çµåç£ å ${t.weighNo || ""}</title> <style> body{font-family:"Microsoft YaHei",Arial,sans-serif;padding:32px;color:#1b2733} h1{text-align:center;font-size:22px;letter-spacing:4px;margin:0 0 8px} .no{text-align:center;color:#666;font-size:13px;margin-bottom:22px} table{width:100%;border-collapse:collapse;font-size:14px} th,td{border:1px solid #d9dde3;padding:10px 12px;text-align:left} th{width:28%;background:#f5f7fa;font-weight:500;color:#5c6b7a} .qr{text-align:center;margin-top:24px} .qr img{width:170px;height:170px} .tip{text-align:center;color:#98a4b0;font-size:12px;margin-top:6px} </style></head><body> <h1>æ 人å¼å®çµåç£ å</h1> <div class="no">ç£ åå·ï¼${t.weighNo || "â"}</div> <table>${rowsHtml}</table> <div class="qr"><img src="${qrUrl.value}" alt="äºç»´ç "></div> <div class="tip">æ«æäºç»´ç 坿 ¸éªæ¬æ¬¡ç£ å</div> </body></html>`; const w = window.open("", "_blank", "width=760,height=860"); if (!w) { proxy.$modal.msgWarning("请å 许弹åºçªå£åéè¯æå°"); return; const loadManual = () => { manualLoading.value = true; listWeighbridgeSystem({ ...manualQuery, current: 1, size: 300 }) .then((res) => { manualList.value = res.data?.records || []; }) .finally(() => { manualLoading.value = false; }); }; const openManualForm = (row) => { if (row) { getWeighbridgeSystem(row.id).then((res) => { manualForm.value = { ...res.data }; manualDialog.value = true; }); } else { const now = new Date(); const pad = (n) => String(n).padStart(2, "0"); manualForm.value = { weightTime: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`, }; manualDialog.value = true; } w.document.write(html); w.document.close(); w.focus(); setTimeout(() => w.print(), 300); } }; const resetManualForm = () => { manualFormRef.value?.resetFields(); manualForm.value = {}; }; const submitManual = () => { manualFormRef.value.validate((valid) => { if (!valid) return; if (Number(manualForm.value.grossWeight) <= Number(manualForm.value.tareWeight)) { proxy.$modal.msgError("æ¯éå¿ é¡»å¤§äºç®é"); return; } const api = manualForm.value.id ? updateWeighbridgeSystem : addWeighbridgeSystem; api(manualForm.value).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); manualDialog.value = false; loadManual(); }); }); }; const removeManual = (row) => { ElMessageBox.confirm(`ç¡®å®å é¤è½¦çã${row.plateNumber}ãçè¿ç£ è®°å½åï¼`, "å é¤æç¤º", { type: "warning" }).then(() => { delWeighbridgeSystem([row.id]).then(() => { proxy.$modal.msgSuccess("å 餿å"); loadManual(); }); }).catch(() => {}); }; onMounted(load); const handleTabChange = (name) => { if (name === "auto") loadRecord(); if (name === "manual") loadManual(); if (name === "erpLog") loadErpLog(); }; onMounted(() => { loadOverview(); loadRecord(); loadManual(); loadErpLog(); }); </script> <style scoped lang="scss"> $line: #e3e8ed; $paper: #f4f6f8; $text: #1b2733; $muted: #5c6b7a; $brand: #2c51d9; $alert: #e0a02e; .weighbridge-page { background: $paper; min-height: calc(100vh - 84px); } /* 页头 */ .page-head { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 20px 22px; background: linear-gradient(150deg, #14191f 0%, #1e252c 100%); border-radius: 6px; margin-bottom: 16px; h2 { margin: 0 0 6px; font-size: 20px; font-weight: 700; color: #fff; letter-spacing: 0.03em; } p { margin: 0; font-size: 13px; color: rgba(234, 240, 245, 0.66); } :deep(.el-button--primary) { background: #fff; border-color: #fff; color: #14191f; font-weight: 600; &:hover { background: #e9eef5; border-color: #e9eef5; color: #14191f; .overview-row { display: flex; gap: 14px; margin-bottom: 16px; .overview-card { flex: 1; background: #f7f9fc; border-radius: 6px; padding: 14px 18px; .label { color: #909399; font-size: 13px; } .value { font-size: 24px; font-weight: 600; color: #303133; margin-top: 6px; &.warning { color: #e6a23c; } &.danger { color: #f56c6c; } } } } } /* æ¦è§ */ .stat-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 16px; } .stat { position: relative; background: #fff; border: 1px solid $line; border-radius: 6px; padding: 14px 18px 12px; &::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: #c7d0da; .ml10 { margin-left: 10px; } } .stat-label { display: block; font-size: 12.5px; color: $muted; margin-bottom: 8px; } .stat-value { font-family: "MyCustomFont", "Helvetica Neue", Arial, sans-serif; font-size: 26px; font-weight: 700; color: $text; line-height: 1; i { font-style: normal; .sub-text { color: #909399; font-size: 12px; color: #98a4b0; margin-left: 4px; font-family: "Microsoft YaHei", Arial, sans-serif; font-weight: 400; } &.warn { color: $alert; } } /* æ¥è¯¢ */ .filter-card { display: flex; align-items: center; gap: 10px; padding: 14px 18px; background: #fff; border: 1px solid $line; border-radius: 6px; margin-bottom: 16px; .filter-label { .order-no { margin-left: 6px; font-size: 13px; color: $muted; } } /* è¡¨æ ¼ */ .table-card { background: #fff; border: 1px solid $line; border-radius: 6px; padding: 14px 16px; } .pager { justify-content: flex-end; margin-top: 14px; } .table-empty { padding: 26px 0; color: #a7b2bd; font-size: 13px; } .muted-cell { color: #a7b2bd; } /* å¼¹çª */ .weigh-tip { margin-bottom: 16px; } .weigh-form { :deep(.el-form-item) { margin-bottom: 16px; .text-success { color: #67c23a; } } .net-preview { margin: 4px 0 0 110px; padding: 10px 14px; background: #f3f6fa; border: 1px dashed #d5dde4; border-radius: 4px; font-size: 13px; color: $muted; b { font-family: "MyCustomFont", "Helvetica Neue", Arial, sans-serif; font-size: 18px; color: $brand; margin: 0 4px; .text-warning { color: #e6a23c; } } /* ç£ å */ .ticket { text-align: center; h2 { margin: 0 0 8px; font-size: 18px; letter-spacing: 0.16em; color: $text; .text-danger { color: #f56c6c; } .ticket-no { margin: 0 0 16px; font-size: 12.5px; color: $muted; } :deep(.el-descriptions__label) { width: 90px; color: $muted; } } .qr { width: 170px; height: 170px; margin: 20px auto 0; display: block; } .qr-tip { margin: 6px 0 0; font-size: 12px; color: #98a4b0; } @media (max-width: 900px) { .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } .page-head { flex-direction: column; align-items: flex-start; .ticket { border: 1px solid #333; padding: 14px; .ticket-title { text-align: center; font-size: 20px; font-weight: bold; margin-bottom: 6px; } .ticket-no { text-align: right; font-size: 12px; margin-bottom: 8px; } .ticket-body { display: flex; justify-content: space-between; .ticket-left p { margin: 4px 0; font-size: 13px; span { display: inline-block; width: 76px; color: #555; } &.net { font-weight: bold; font-size: 15px; } } .ticket-right { text-align: center; .qr-img { width: 150px; height: 150px; } .qr-tip { font-size: 12px; color: #666; } } } .ticket-footer { margin-top: 10px; font-size: 12px; color: #666; text-align: center; } } } </style>