feat(环保门禁): 智能巡检 AI 视频分析可视化——顶部统计卡片(任务数/AI识别目标/异常数/平均置信度)、列表新增AI分析列(可点击开回传详情)、详情弹窗新增AI截帧图列;异常判定排除否定句避免误判
已修改1个文件
215 ■■■■■ 文件已修改
src/views/environmentAccess/intelligentInspectionManagement/index.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 = ["未佩戴", "未按", "异常", "超标", "违规", "模糊", "褪色", "难以辨认", "失效", "偏移", "移位", "占用", "遗撒", "波动", "漂移", "中断", "松动", "渗漏", "超时", "超限", "缺失", "破损"];
// 否定词:出现这些说明是"正常"的描述,不能算异常(如"运行平稳无异常噪音""未发现渗漏")
const NEGATIVE_WORDS = ["无异常", "无异响", "未发现", "未出现", "正常", "完好", "符合", "良好", "齐全", "清晰", "稳定", "无卡滞", "达标", "无渗漏"];
const EMPTY_AI = { has: false, total: 0, abnormal: 0, avgConf: 0, summary: "" };
/**
 * 解析 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>