tuqin
2026-10-29 54de60fc3e98ad17f280ae1c8c5ee7fcb32891d3
feat(LIMS): 采集清洗 + 监控告警前端

数据采集页:
- 按钮区加「批量清洗」
- 操作列加「详情」「清洗」
- 详情抽屉:采集记录基本信息 + 清洗状态/验证状态标签 + 清洗日志(黑底等宽逐条展示)+ 验证记录表

实时监控页:
- 按钮区加「全量检查」「告警记录」(待处理数用角标显示)
- 操作列加「检查」(单条判定越限)
- 告警记录抽屉:顶部统计条(待处理/处理中/已处理 + 三级告警数)+ 告警表格 + 处理弹窗 + 分页
- onMounted 加载看板数字

新增 cleanRule.js / alertRecord.js 两个 API,dataCollection.js 与 realtimeMonitor.js 各追加清洗与预警方法。
已添加2个文件
已修改4个文件
359 ■■■■■ 文件已修改
src/api/lims/alertRecord.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/lims/cleanRule.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/lims/dataCollection.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/lims/realtimeMonitor.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lims/dataCollection/index.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lims/realtimeMonitor/index.vue 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/lims/alertRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
// å‘Šè­¦è®°å½•接口
import request from '@/utils/request'
export function listAlertRecord(query) {
    return request({ url: '/lims/alertRecord/listPage', method: 'get', params: query })
}
export function getAlertRecord(id) {
    return request({ url: '/lims/alertRecord/' + id, method: 'get' })
}
export function addAlertRecord(data) {
    return request({ url: '/lims/alertRecord/add', method: 'post', data: data })
}
export function updateAlertRecord(data) {
    return request({ url: '/lims/alertRecord/update', method: 'post', data: data })
}
export function delAlertRecord(ids) {
    return request({ url: '/lims/alertRecord/delete', method: 'delete', data: ids })
}
src/api/lims/cleanRule.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
// æ•°æ®æ¸…洗规则接口
import request from '@/utils/request'
export function listCleanRule(query) {
    return request({ url: '/lims/cleanRule/listPage', method: 'get', params: query })
}
export function getCleanRule(id) {
    return request({ url: '/lims/cleanRule/' + id, method: 'get' })
}
export function addCleanRule(data) {
    return request({ url: '/lims/cleanRule/add', method: 'post', data: data })
}
export function updateCleanRule(data) {
    return request({ url: '/lims/cleanRule/update', method: 'post', data: data })
}
export function delCleanRule(ids) {
    return request({ url: '/lims/cleanRule/delete', method: 'delete', data: ids })
}
src/api/lims/dataCollection.js
@@ -44,3 +44,20 @@
        data: ids
    })
}
// ===== â‘¢ æ¸…洗与验证 =====
export function processCollection(id) {
    return request({ url: '/lims/dataCollection/process/' + id, method: 'post' })
}
export function processCollectionBatch(ids) {
    return request({ url: '/lims/dataCollection/processBatch', method: 'post', data: ids })
}
export function processCollectionPending(limit) {
    return request({ url: '/lims/dataCollection/processPending', method: 'post', params: { limit } })
}
export function getCollectionDetail(id) {
    return request({ url: '/lims/dataCollection/detail/' + id, method: 'get' })
}
src/api/lims/realtimeMonitor.js
@@ -44,3 +44,32 @@
        data: ids
    })
}
// ===== â‘¥ é¢„警判定与告警闭环 =====
export function checkMonitor(id) {
    return request({ url: '/lims/realtimeMonitor/check/' + id, method: 'post' })
}
export function checkAllMonitors(limit) {
    return request({ url: '/lims/realtimeMonitor/checkAll', method: 'post', params: { limit } })
}
export function syncMonitorFromCollection(collectionId) {
    return request({ url: '/lims/realtimeMonitor/sync/' + collectionId, method: 'post' })
}
export function listAlerts(query) {
    return request({ url: '/lims/realtimeMonitor/alerts', method: 'get', params: query })
}
export function handleAlert(data) {
    return request({ url: '/lims/realtimeMonitor/alert/handle', method: 'post', data: data })
}
export function getAlertStatistics() {
    return request({ url: '/lims/realtimeMonitor/alertStatistics', method: 'get' })
}
export function getMonitorDashboard() {
    return request({ url: '/lims/realtimeMonitor/dashboard', method: 'get' })
}
src/views/lims/dataCollection/index.vue
@@ -38,6 +38,7 @@
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增采集</el-button>
        <el-button type="warning" plain @click="doCleanBatch">批量清洗</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
@@ -282,6 +283,48 @@
      </template>
    </el-dialog>
  </div>
    <!-- ===== é‡‡é›†è¯¦æƒ…:清洗日志 + éªŒè¯è®°å½• ===== -->
    <el-drawer v-model="cleanVisible" title="采集详情" size="760px" destroy-on-close>
      <div v-if="cleanData.collection" style="padding: 0 12px">
        <el-descriptions :column="2" border size="small" title="采集记录">
          <el-descriptions-item label="采集编号">{{ cleanData.collection.collectionCode }}</el-descriptions-item>
          <el-descriptions-item label="设备">{{ cleanData.collection.deviceName }}({{ cleanData.collection.deviceCode }})</el-descriptions-item>
          <el-descriptions-item label="数据类型">{{ cleanData.collection.dataType }}</el-descriptions-item>
          <el-descriptions-item label="采集数值">{{ cleanData.collection.collectionValue }} {{ cleanData.collection.unit }}</el-descriptions-item>
          <el-descriptions-item label="采集时间">{{ cleanData.collection.collectionTime }}</el-descriptions-item>
          <el-descriptions-item label="数据质量">{{ cleanData.collection.dataQuality }}</el-descriptions-item>
          <el-descriptions-item label="清洗状态">
            <el-tag :type="cleanData.collection.cleanStatus === 'cleaned' ? 'success' : cleanData.collection.cleanStatus === 'discarded' ? 'danger' : 'info'" size="small">
              {{ cleanStatusText(cleanData.collection.cleanStatus) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="验证状态">
            <el-tag :type="cleanData.collection.verifyStatus === 'passed' ? 'success' : cleanData.collection.verifyStatus === 'failed' ? 'danger' : 'info'" size="small">
              {{ verifyStatusText(cleanData.collection.verifyStatus) }}
            </el-tag>
          </el-descriptions-item>
        </el-descriptions>
        <el-divider content-position="left">清洗日志</el-divider>
        <div v-if="cleanData.collection.cleanLog" style="background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 4px; font-family: Consolas, monospace; font-size: 12px; white-space: pre-wrap; line-height: 1.7">{{ cleanData.collection.cleanLog }}</div>
        <el-empty v-else description="没有命中任何清洗规则" :image-size="50" />
        <el-divider content-position="left">验证记录({{ (cleanData.verifies || []).length }} æ¡ï¼‰</el-divider>
        <el-table :data="cleanData.verifies || []" border size="small">
          <el-table-column prop="ruleName" label="规则" width="160" show-overflow-tooltip />
          <el-table-column prop="ruleType" label="类型" width="90" />
          <el-table-column prop="passed" label="结果" width="70">
            <template #default="{ row }">
              <el-tag :type="row.passed === '1' ? 'success' : 'danger'" size="small">{{ row.passed === "1" ? "通过" : "不通过" }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="expected" label="规则条件" width="110" />
          <el-table-column prop="actual" label="实际值" width="100" />
          <el-table-column prop="message" label="说明" show-overflow-tooltip />
        </el-table>
      </div>
    </el-drawer>
</template>
<script setup>
@@ -294,8 +337,9 @@
  listDataCollection,
  updateDataCollection,
} from "@/api/lims/dataCollection.js";
import { ElMessageBox } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import { userListNoPage } from "@/api/system/user.js";
import { processCollection, processCollectionBatch, getCollectionDetail } from "@/api/lims/dataCollection";
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
@@ -361,22 +405,16 @@
    width: 100,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        },
        disabled: (row) => {
          return row.maintainer !== userStore.nickName
        }
      },
    ],
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: 'right',
      width: 200,
      operation: [
        { name: "详情", type: "text", clickFun: (row) => { openDetail(row); } },
        { name: "清洗", type: "text", clickFun: (row) => { doClean(row); } },
        { name: "编辑", type: "text", clickFun: (row) => { openForm("edit", row); } },
      ],
  },
]);
const tableData = ref([]);
@@ -553,6 +591,45 @@
  return `${year}-${month}-${day}`;
}
// ==================== â‘¢ æ¸…洗与验证 ====================
const cleanVisible = ref(false);
const cleanData = ref({ collection: null, verifies: [] });
const CLEAN_STATUS = { pending: "待清洗", cleaned: "已清洗", discarded: "已丢弃", failed: "清洗失败" };
const VERIFY_STATUS = { pending: "待验证", passed: "通过", failed: "未通过" };
const cleanStatusText = (s) => CLEAN_STATUS[s] || s || "—";
const verifyStatusText = (s) => VERIFY_STATUS[s] || s || "—";
// å•条清洗
const doClean = (row) => {
  processCollection(row.id).then((res) => {
    const d = res.data || {};
    ElMessage.success(`清洗完成:${d.cleaned || 0} æ¡å·²æ¸…洗,${d.discarded || 0} æ¡ä¸¢å¼ƒï¼Œ${d.verifyFailed || 0} æ¡éªŒè¯ä¸é€šè¿‡`);
    getList();
  });
};
// æ‰¹é‡æ¸…æ´—
const doCleanBatch = () => {
  if (!selectedRows.value.length) { ElMessage.warning("请先选择要清洗的记录"); return; }
  const ids = selectedRows.value.map((r) => r.id);
  ElMessageBox.confirm(`确认清洗选中的 ${ids.length} æ¡è®°å½•?`, "提示", { type: "info" }).then(() => {
    processCollectionBatch(ids).then((res) => {
      const d = res.data || {};
      ElMessage.success(`清洗完成:已清洗 ${d.cleaned || 0},丢弃 ${d.discarded || 0},验证不通过 ${d.verifyFailed || 0}`);
      getList();
    });
  }).catch(() => {});
};
// é‡‡é›†è¯¦æƒ…(含验证记录)
const openDetail = (row) => {
  cleanData.value = { collection: null, verifies: [] };
  cleanVisible.value = true;
  getCollectionDetail(row.id).then((res) => {
    cleanData.value = res.data || { collection: null, verifies: [] };
  });
};
onMounted(() => {
  getList();
});
src/views/lims/realtimeMonitor/index.vue
@@ -38,6 +38,8 @@
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增监控</el-button>
        <el-button type="success" plain @click="doCheckAll">全量检查</el-button>
        <el-button type="warning" plain @click="openAlerts">告警记录 <span v-if="alertStat.pending > 0">({{ alertStat.pending }})</span></el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
@@ -296,6 +298,52 @@
      </template>
    </el-dialog>
  </div>
    <!-- ===== å‘Šè­¦è®°å½•抽屉 ===== -->
    <el-drawer v-model="alertVisible" title="告警记录" size="960px" destroy-on-close>
      <div style="margin-bottom: 12px; display: flex; gap: 16px; font-size: 14px">
        <span>待处理 <el-tag type="danger" size="small">{{ alertStat.pending || 0 }}</el-tag></span>
        <span>处理中 <el-tag type="warning" size="small">{{ alertStat.handling || 0 }}</el-tag></span>
        <span>已处理 <el-tag type="success" size="small">{{ alertStat.resolved || 0 }}</el-tag></span>
        <span style="margin-left: auto">紧急 {{ alertStat.levelHigh || 0 }} / é‡è¦ {{ alertStat.levelMedium || 0 }} / ä¸€èˆ¬ {{ alertStat.levelLow || 0 }}</span>
      </div>
      <el-table :data="alertData" :loading="alertLoading" border size="small" max-height="560">
        <el-table-column prop="alertTime" label="告警时间" width="160" />
        <el-table-column prop="monitorTarget" label="监控对象" width="150" show-overflow-tooltip />
        <el-table-column prop="alertLevel" label="级别" width="80">
          <template #default="{ row }">
            <el-tag :type="row.alertLevel === 'high' ? 'danger' : row.alertLevel === 'medium' ? 'warning' : 'info'" size="small">{{ alertLevelText(row.alertLevel) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="alertValue" label="触发值" width="100">
          <template #default="{ row }">{{ row.alertValue }}{{ row.unit || "" }}</template>
        </el-table-column>
        <el-table-column prop="alertMessage" label="告警描述" show-overflow-tooltip />
        <el-table-column prop="status" label="状态" width="90">
          <template #default="{ row }">
            <el-tag :type="alertStatusTag(row.status)" size="small">{{ alertStatusText(row.status) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="handler" label="处理人" width="90" />
        <el-table-column label="操作" width="90" align="center">
          <template #default="{ row }">
            <el-button v-if="row.status === 'pending' || row.status === 'handling'" link type="primary" @click="doHandleAlert(row)">处理</el-button>
            <span v-else style="color: #909399">—</span>
          </template>
        </el-table-column>
      </el-table>
      <div style="margin-top: 12px; text-align: right">
        <el-pagination
          v-model:current-page="alertQuery.pageNum"
          v-model:page-size="alertQuery.pageSize"
          :total="alertTotal"
          :page-sizes="[20, 50, 100]"
          layout="total, sizes, prev, pager, next"
          @current-change="loadAlerts"
          @size-change="loadAlerts"
        />
      </div>
    </el-drawer>
</template>
<script setup>
@@ -308,8 +356,9 @@
  listRealtimeMonitor,
  updateRealtimeMonitor,
} from "@/api/lims/realtimeMonitor.js";
import { ElMessageBox } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import { userListNoPage } from "@/api/system/user.js";
import { checkMonitor, checkAllMonitors, listAlerts, handleAlert, getMonitorDashboard } from "@/api/lims/realtimeMonitor";
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
@@ -380,22 +429,15 @@
    width: 100,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        },
        disabled: (row) => {
          return row.maintainer !== userStore.nickName
        }
      },
    ],
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: 'right',
      width: 200,
      operation: [
        { name: "检查", type: "text", clickFun: (row) => { doCheck(row); } },
        { name: "编辑", type: "text", clickFun: (row) => { openForm("edit", row); } },
      ],
  },
]);
const tableData = ref([]);
@@ -574,7 +616,89 @@
  return `${year}-${month}-${day}`;
}
// ==================== â‘¥ é¢„警判定与告警闭环 ====================
const alertVisible = ref(false);
const alertData = ref([]);
const alertLoading = ref(false);
const alertTotal = ref(0);
const alertQuery = ref({ pageNum: 1, pageSize: 20 });
const alertStat = ref({ pending: 0, handling: 0, resolved: 0, levelHigh: 0, levelMedium: 0, levelLow: 0, total: 0 });
const dashboard = ref({ total: 0, alerting: 0, warning: 0, normal: 0 });
const ALERT_STATUS = { pending: "待处理", handling: "处理中", resolved: "已处理", ignored: "已忽略" };
const ALERT_LEVEL = { high: "紧急", medium: "重要", low: "一般" };
const alertStatusText = (s) => ALERT_STATUS[s] || s || "—";
const alertLevelText = (s) => ALERT_LEVEL[s] || s || "—";
const alertStatusTag = (s) => (s === "pending" ? "danger" : s === "handling" ? "warning" : s === "resolved" ? "success" : "info");
// æ£€æŸ¥å•个监控项
const doCheck = (row) => {
  checkMonitor(row.id).then((res) => {
    const d = res.data || {};
    if (d.alerts > 0) {
      ElMessage.warning("检测到越限,已生成告警");
    } else if (d.suppressed > 0) {
      ElMessage.info("越限但已有未处理告警,未重复生成");
    } else {
      ElMessage.success("当前值正常");
    }
    getList();
    loadDashboard();
  });
};
// å…¨é‡æ£€æŸ¥
const doCheckAll = () => {
  checkAllMonitors(500).then((res) => {
    const d = res.data || {};
    ElMessage.success(`扫描 ${d.scanned || 0} ä¸ªç›‘控项,新增告警 ${d.alerts || 0} æ¡`);
    getList();
    loadDashboard();
  });
};
// å‘Šè­¦åˆ—表
const openAlerts = () => {
  alertQuery.value = { pageNum: 1, pageSize: 20 };
  alertVisible.value = true;
  loadAlerts();
};
const loadAlerts = () => {
  alertLoading.value = true;
  listAlerts(alertQuery.value).then((res) => {
    alertLoading.value = false;
    alertData.value = res.data.records || [];
    alertTotal.value = res.data.total || 0;
  }).catch(() => (alertLoading.value = false));
};
// çœ‹æ¿ä¸Žç»Ÿè®¡
const loadDashboard = () => {
  getMonitorDashboard().then((res) => {
    const d = res.data || {};
    dashboard.value = d;
    if (d.alerts) alertStat.value = d.alerts;
  });
};
// å¤„理告警
const doHandleAlert = (row) => {
  ElMessageBox.prompt("请填写处理措施(选填)", `处理告警:${row.monitorTarget || row.monitorCode}`, {
    inputPlaceholder: "如 å·²é€šçŸ¥è¿ç»´è°ƒæ•´å‚æ•°",
    confirmButtonText: "确认已处理",
    cancelButtonText: "取消",
  }).then(({ value }) => {
    handleAlert({ alertId: row.id, status: "resolved", measure: value }).then((res) => {
      ElMessage.success(res.msg || "已处理");
      loadAlerts();
      loadDashboard();
      getList();
    });
  }).catch(() => {});
};
onMounted(() => {
  loadDashboard();
  getList();
});
</script>