feat(LIMS): 采集清洗 + 监控告警前端
数据采集页:
- 按钮区加「批量清洗」
- 操作列加「详情」「清洗」
- 详情抽屉:采集记录基本信息 + 清洗状态/验证状态标签 + 清洗日志(黑底等宽逐条展示)+ 验证记录表
实时监控页:
- 按钮区加「全量检查」「告警记录」(待处理数用角标显示)
- 操作列加「检查」(单条判定越限)
- 告警记录抽屉:顶部统计条(待处理/处理中/已处理 + 三级告警数)+ 告警表格 + 处理弹窗 + 分页
- onMounted 加载看板数字
新增 cleanRule.js / alertRecord.js 两个 API,dataCollection.js 与 realtimeMonitor.js 各追加清洗与预警方法。
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // åè¦è®°å½æ¥å£ |
| | | 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 }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ°æ®æ¸
æ´è§åæ¥å£ |
| | | 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 }) |
| | | } |
| | |
| | | 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' }) |
| | | } |
| | |
| | | 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' }) |
| | | } |
| | |
| | | </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> |
| | |
| | | </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> |
| | |
| | | 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(); |
| | |
| | | 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([]); |
| | |
| | | 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(); |
| | | }); |
| | |
| | | </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> |
| | |
| | | </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> |
| | |
| | | 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(); |
| | |
| | | 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([]); |
| | |
| | | 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> |