liu
5 天以前 49921150cbdcf2ec681486f36bc78411ea1ff035
Merge remote-tracking branch 'origin/dev_tide_wrzs_xinlan' into dev_tide_wrzs_xinlan

# Conflicts:
# src/api/environmentAccess/accessManagement.js
# src/api/environmentAccess/deviceMonitor.js
# src/api/environmentAccess/unattendedWeighbridgeSystem.js
# src/views/environmentAccess/accessManagement/index.vue
# src/views/environmentAccess/intelligentInspectionManagement/index.vue
# src/views/environmentAccess/reportCenter/index.vue
# src/views/environmentAccess/unattendedWeighbridgeSystem/index.vue
已添加2个文件
已修改8个文件
6352 ■■■■■ 文件已修改
src/api/environmentAccess/accessManagement.js 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/deviceMonitor.js 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/inspection.js 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/reportCenter.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/environmentAccess/unattendedWeighbridgeSystem.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/accessManagement/index.vue 1586 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/deviceMonitor/index.vue 1058 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/intelligentInspectionManagement/index.vue 1386 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/reportCenter/index.vue 457 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/environmentAccess/unattendedWeighbridgeSystem/index.vue 1480 ●●●●● 补丁 | 查看 | 原始文档 | 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="&nbsp;">
                  <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="&nbsp;">
                  <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>