From 10e217d5652988d0f6cfe380beb4c5ab39a6b3cd Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期二, 27 十月 2026 15:45:58 +0800
Subject: [PATCH] feat(环保门禁): 智能巡检 AI 视频分析可视化——顶部统计卡片(任务数/AI识别目标/异常数/平均置信度)、列表新增AI分析列(可点击开回传详情)、详情弹窗新增AI截帧图列;异常判定排除否定句避免误判

---
 src/views/environmentAccess/intelligentInspectionManagement/index.vue |  215 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 211 insertions(+), 4 deletions(-)

diff --git a/src/views/environmentAccess/intelligentInspectionManagement/index.vue b/src/views/environmentAccess/intelligentInspectionManagement/index.vue
index 214f7b0..560d15b 100644
--- a/src/views/environmentAccess/intelligentInspectionManagement/index.vue
+++ b/src/views/environmentAccess/intelligentInspectionManagement/index.vue
@@ -20,9 +20,45 @@
         <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
       </div>
     </div>
+    <!-- AI 瑙嗛鍒嗘瀽缁熻姒傝 -->
+    <div class="ai-stat-row">
+      <div class="ai-stat-card">
+        <div class="ai-stat-label">宸℃浠诲姟</div>
+        <div class="ai-stat-value">{{ aiStat.total }}</div>
+      </div>
+      <div class="ai-stat-card">
+        <div class="ai-stat-label">宸插畬鎴� AI 鍒嗘瀽</div>
+        <div class="ai-stat-value">{{ aiStat.analyzed }}</div>
+      </div>
+      <div class="ai-stat-card">
+        <div class="ai-stat-label">AI 璇嗗埆鐩爣</div>
+        <div class="ai-stat-value accent">{{ aiStat.objectCount }}<span class="unit">椤�</span></div>
+      </div>
+      <div class="ai-stat-card" :class="{ danger: aiStat.abnormalCount > 0 }">
+        <div class="ai-stat-label">鍙戠幇寮傚父</div>
+        <div class="ai-stat-value">{{ aiStat.abnormalCount }}<span class="unit">澶�</span></div>
+      </div>
+      <div class="ai-stat-card">
+        <div class="ai-stat-label">骞冲潎缃俊搴�</div>
+        <div class="ai-stat-value">{{ aiStat.avgConfidence }}<span class="unit">%</span></div>
+      </div>
+    </div>
+
     <div class="table_list">
       <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true"
-        @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"></PIMTable>
+        @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination">
+        <template #aiAnalysis="{ row }">
+          <span v-if="row._ai && row._ai.has" class="ai-cell" title="鐐瑰嚮鏌ョ湅鍥炰紶璇︽儏" @click="openTrace(row)">
+            <el-tag v-if="row._ai.abnormal > 0" type="danger" size="small" effect="dark">
+              {{ row._ai.total }} 椤� 路 {{ row._ai.abnormal }} 寮傚父
+            </el-tag>
+            <el-tag v-else type="success" size="small">
+              {{ row._ai.total }} 椤�
+            </el-tag>
+          </span>
+          <span v-else class="ai-none">鈥� 鏃犲洖浼�</span>
+        </template>
+      </PIMTable>
     </div>
 
     <el-dialog v-model="dialogVisible" :title="operationType === 'add' ? '鏂板宸℃' : '缂栬緫宸℃'" width="70%" @close="closeDia">
@@ -117,6 +153,18 @@
               <template #default="{ row }">{{ row.confidence != null ? (Number(row.confidence) * 100).toFixed(1) + "%" : "-" }}</template>
             </el-table-column>
             <el-table-column prop="frameTime" label="鎴抚鏃堕棿" width="170" />
+            <el-table-column label="AI 鎴抚" width="110" align="center">
+              <template #default="{ row }">
+                <el-image
+                  v-if="row.frameImage"
+                  :src="row.frameImage"
+                  :preview-src-list="[row.frameImage]"
+                  fit="cover"
+                  class="trace-frame"
+                />
+                <span v-else class="trace-frame-none">寰呭洖浼�</span>
+              </template>
+            </el-table-column>
           </el-table>
           <div v-else class="trace-sub-empty">鏆傛棤缁撴瀯鍖栬瘑鍒潯鐩�</div>
         </div>
@@ -176,6 +224,7 @@
   { label: "璁″垝寮�濮�", prop: "planStartTime", width: 160 },
   { label: "宸℃浜哄憳", prop: "inspector", width: 100 },
   { label: "鐘舵��", prop: "status", width: 90, dataType: "tag", formatData: (p) => statusText(p), formatType: (p) => statusType(p) },
+  { label: "AI 鍒嗘瀽", prop: "aiAnalysis", width: 170, align: "center", dataType: "slot", slot: "aiAnalysis" },
   { label: "宸℃缁撴灉", prop: "inspectionResult", showOverflowTooltip: true },
   {
     dataType: "action", label: "鎿嶄綔", align: "center", fixed: "right", width: 200,
@@ -259,6 +308,90 @@
   traceVisible.value = true;
 };
 
+/* ---------- AI 瑙嗛鍒嗘瀽锛氳В鏋愪笌缁熻 ---------- */
+// 鍒ゅ畾"寮傚父"鐨勫叧閿瘝锛堝悗绔湭鏄惧紡缁� abnormal 鏍囪鏃剁殑鍏滃簳锛�
+const ABNORMAL_WORDS = ["鏈僵鎴�", "鏈寜", "寮傚父", "瓒呮爣", "杩濊", "妯$硦", "瑜壊", "闅句互杈ㄨ", "澶辨晥", "鍋忕Щ", "绉讳綅", "鍗犵敤", "閬楁拻", "娉㈠姩", "婕傜Щ", "涓柇", "鏉惧姩", "娓楁紡", "瓒呮椂", "瓒呴檺", "缂哄け", "鐮存崯"];
+// 鍚﹀畾璇嶏細鍑虹幇杩欎簺璇存槑鏄�"姝e父"鐨勬弿杩帮紝涓嶈兘绠楀紓甯革紙濡�"杩愯骞崇ǔ鏃犲紓甯稿櫔闊�""鏈彂鐜版笚婕�"锛�
+const NEGATIVE_WORDS = ["鏃犲紓甯�", "鏃犲紓鍝�", "鏈彂鐜�", "鏈嚭鐜�", "姝e父", "瀹屽ソ", "绗﹀悎", "鑹ソ", "榻愬叏", "娓呮櫚", "绋冲畾", "鏃犲崱婊�", "杈炬爣", "鏃犳笚婕�"];
+const EMPTY_AI = { has: false, total: 0, abnormal: 0, avgConf: 0, summary: "" };
+
+/**
+ * 瑙f瀽 aiAnalysis 瀛楁锛堝悗绔瓨鐨勬槸 JSON 瀛楃涓诧級
+ * 鍏煎涓夌鎯呭喌锛氣憼 鏍囧噯 JSON 鈶� 绾枃鏈� 鈶� 绌�
+ */
+const parseAi = (raw) => {
+  if (!raw) return { ...EMPTY_AI };
+  let obj = null;
+  try {
+    const v = JSON.parse(raw);
+    obj = v && typeof v === "object" ? v : null;
+  } catch (e) {
+    obj = null;
+  }
+  // 涓嶆槸 JSON 鈫� 褰撶函鏂囨湰澶勭悊锛屾爣璁板凡鍒嗘瀽浣嗘棤缁撴瀯鍖栨潯鐩�
+  if (!obj) {
+    return { has: true, total: 0, abnormal: 0, avgConf: 0, summary: String(raw) };
+  }
+  const list = Array.isArray(obj.objects) ? obj.objects : [];
+  let abnormal = 0;
+  let confSum = 0;
+  let confCnt = 0;
+  list.forEach((it) => {
+    const concl = String(it.conclusion || "");
+    // 鈶� 鍚庣鏄惧紡鏍囪鏈�鍑嗭紝浼樺厛鐢�
+    let isBad;
+    if (it.abnormal === true || it.abnormal === 1 || it.abnormal === "1" || it.abnormal === "true") {
+      isBad = true;
+    } else if (it.abnormal === false || it.abnormal === 0 || it.abnormal === "0" || it.abnormal === "false") {
+      isBad = false;
+    } else {
+      // 鈶� 娌℃湁鏄惧紡鏍囪鏃舵墠璧板叧閿瘝鍏滃簳锛氬懡涓紓甯歌瘝 涓� 涓嶅惈鍚﹀畾璇�
+      const hasNeg = NEGATIVE_WORDS.some((w) => concl.includes(w));
+      isBad = !hasNeg && ABNORMAL_WORDS.some((w) => concl.includes(w));
+    }
+    if (isBad) abnormal++;
+    const c = Number(it.confidence);
+    if (!Number.isNaN(c) && it.confidence !== null && it.confidence !== "") {
+      confSum += c > 1 ? c / 100 : c; // 鍏煎 0.93 鍜� 93 涓ょ鍐欐硶
+      confCnt++;
+    }
+  });
+  return {
+    has: true,
+    total: list.length,
+    abnormal,
+    avgConf: confCnt ? Math.round((confSum / confCnt) * 100) : 0,
+    summary: obj.summary || "",
+  };
+};
+
+const aiStat = ref({ total: 0, analyzed: 0, objectCount: 0, abnormalCount: 0, avgConfidence: 0 });
+const computeAiStat = (rows) => {
+  let analyzed = 0;
+  let objectCount = 0;
+  let abnormalCount = 0;
+  let confSum = 0; // 鎸夋潯鐩姞鏉�
+  let confCnt = 0;
+  rows.forEach((r) => {
+    const a = r._ai;
+    if (!a || !a.has) return;
+    analyzed++;
+    objectCount += a.total;
+    abnormalCount += a.abnormal;
+    if (a.total > 0 && a.avgConf > 0) {
+      confSum += a.avgConf * a.total;
+      confCnt += a.total;
+    }
+  });
+  aiStat.value = {
+    total: rows.length,
+    analyzed,
+    objectCount,
+    abnormalCount,
+    avgConfidence: confCnt ? Math.round(confSum / confCnt) : 0,
+  };
+};
+
 const handleQuery = () => { page.current = 1; getList(); };
 const pagination = (obj) => { page.current = obj.page; page.size = obj.limit; getList(); };
 const handleSelectionChange = (s) => { selectedRows.value = s; };
@@ -266,8 +399,14 @@
   tableLoading.value = true;
   listInspection({ ...searchForm.value, ...page }).then((res) => {
     tableLoading.value = false;
-    tableData.value = res.data.records;
-    page.total = res.data.total;
+    const rows = (res.data && res.data.records) || [];
+    // 棰勮В鏋� AI 鍒嗘瀽缁撴灉锛岄伩鍏嶆ā鏉块噷閲嶅 JSON.parse
+    rows.forEach((r) => {
+      r._ai = parseAi(r.aiAnalysis);
+    });
+    tableData.value = rows;
+    page.total = (res.data && res.data.total) || 0;
+    computeAiStat(rows);
   }).catch(() => { tableLoading.value = false; });
 };
 const openForm = (type, row) => {
@@ -299,7 +438,64 @@
 
 <style scoped lang="scss">
 .ml10 { margin-left: 10px; }
-.table_list { margin-top: unset; }
+.table_list { margin-top: 12px; }
+
+/* ============ AI 瑙嗛鍒嗘瀽缁熻鍗$墖 ============ */
+.ai-stat-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin: 14px 0 4px;
+}
+.ai-stat-card {
+  flex: 1;
+  min-width: 130px;
+  padding: 12px 16px;
+  background: #f7f9fc;
+  border: 1px solid #e4e8ee;
+  border-radius: 6px;
+  box-sizing: border-box;
+
+  &.danger {
+    background: #fef0f0;
+    border-color: #fbc4c4;
+  }
+}
+.ai-stat-label {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 6px;
+}
+.ai-stat-value {
+  font-size: 22px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.2;
+
+  &.accent {
+    color: #409eff;
+  }
+  .ai-stat-card.danger & {
+    color: #f56c6c;
+  }
+  .unit {
+    font-size: 13px;
+    font-weight: 400;
+    margin-left: 2px;
+    color: #909399;
+  }
+}
+
+/* 鍒楄〃閲岀殑 AI 鍒嗘瀽鍒� */
+.ai-cell {
+  display: inline-block;
+  cursor: pointer;
+}
+.ai-none {
+  font-size: 13px;
+  color: #c0c4cc;
+}
+
 .trace-block { margin-bottom: 18px; }
 .trace-title {
   font-size: 14px;
@@ -336,4 +532,15 @@
   border: 1px solid #ebeef5;
   cursor: pointer;
 }
+.trace-frame {
+  width: 80px;
+  height: 60px;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  cursor: pointer;
+}
+.trace-frame-none {
+  font-size: 12px;
+  color: #c0c4cc;
+}
 </style>

--
Gitblit v1.9.3