@@ -0,0 +1,155 @@
< template >
< BasicLayout >
< template # wrapper >
< el-card class = "box-card" >
< div class = "page-header" >
< div >
< h3 > 本地事件候选 < / h3 >
< p > 查看 Sense 在本机形成的匿名事件候选及证据处理状态 。 < / p >
< / div >
< el-button :icon = "Refresh" @click ="loadEvents" > 刷新 < / el -button >
< / div >
< el-alert type = "info" :closable = "false" show -icon class = "boundary-alert" >
< template # title > 本页不依赖 Bell < / template >
这里展示的是 Sense 本地事件候选 , 不代表已生成或已送达 Bell 预警 ; Bell 未安装或未启动时仍可正常查看 。
< / el-alert >
< el-row :gutter = "12" class = "summary-row" aria -label = " 当前页事件概览 " >
< el-col :xs = "24" :sm = "8" > < div class = "summary-item" > < span > 当前页候选 < / span > < strong > { { pageSummary . candidate } } < / strong > < / div > < / el-col >
< el-col :xs = "24" :sm = "8" > < div class = "summary-item" > < span > 当前页已确认 < / span > < strong > { { pageSummary . confirmed } } < / strong > < / div > < / el-col >
< el-col :xs = "24" :sm = "8" > < div class = "summary-item" > < span > 当前页证据失败 < / span > < strong > { { pageSummary . failed } } < / strong > < / div > < / el-col >
< / el-row >
< el-form :model = "query" label -width = " 78px " class = "filter-form" @submit.prevent ="search" >
< el -form -item label = "发生时间" class = "time-field" >
< el-date-picker v-model = "occurredRange" type="datetimerange" start-placeholder="开始时间" end-placeholder="结束时间" range-separator="至" :clearable="true" / >
< / el-form-item >
< el-form-item label = "候选状态" >
< el-select v-model = "query.candidateState" clearable placeholder="全部" >
< el -option label = "候选" value = "candidate" / > < el-option label = "已确认事件" value = "confirmed" / >
< / el-select >
< / el-form-item >
< el-form-item label = "证据状态" >
< el-select v-model = "query.evidenceState" clearable placeholder="全部" >
< el -option label = "处理中" value = "pending" / > < el-option label = "成功" value = "success" / > < el-option label = "失败" value = "failed" / >
< / el-select >
< / el-form-item >
< el-form-item label = "规则来源" > < el-input v-model = "query.ruleRef" clearable placeholder="规则编号" / > < / el-form-item >
< el-form-item label = "关键词" > < el-input v-model = "query.keyword" clearable placeholder="事件 / 摄像头 / 规则" @keyup.enter="search" / > < / el-form-item >
< el-form-item class = "filter-actions" >
< el-button type = "primary" :icon = "Search" native -type = " submit " > 查询 < / el-button >
< el-button :icon = "RefreshLeft" @click ="reset" > 重置 < / el -button >
< / el-form-item >
< / el-form >
< el-table v-loading = "loading" :data="events" border stripe empty-text="没有符合条件的本地事件候选" >
< el -table -column prop = "occurredAt" label = "发生时间" width = "176" >
< template # default = "scope" > { { parseTime ( scope . row . occurredAt ) } } < / template >
< / el-table-column >
< el-table-column prop = "id" label = "事件编号" min -width = " 205 " show -overflow -tooltip / >
< el-table-column label = "摄像头" min -width = " 145 " >
< template # default = "scope" > < div > { { scope . row . sourceRef } } < / div > < small > { { scope . row . sourceLabel || '未命名位置' } } < / small > < / template >
< / el-table-column >
< el-table-column label = "规则来源" min -width = " 160 " >
< template # default = "scope" > < div > { { scope . row . ruleName } } < / div > < small > { { scope . row . ruleRef } } { { scope . row . ruleVersion } } < / small > < / template >
< / el-table-column >
< el-table-column label = "候选状态" width = "118" align = "center" >
< template # default = "scope" > < el-tag :type = "candidateType(scope.row.candidateState)" > { { candidateLabel ( scope . row . candidateState ) } } < / el-tag > < / template >
< / el-table-column >
< el-table-column label = "证据状态" width = "104" align = "center" >
< template # default = "scope" > < el-tag :type = "evidenceType(scope.row.evidenceState)" > { { evidenceLabel ( scope . row . evidenceState ) } } < / el-tag > < / template >
< / el-table-column >
< el-table-column label = "保留期限" width = "142" >
< template # default = "scope" > < span : class = "{ 'retention-warning': scope.row.retentionState === 'expiring' }" > { { retentionLabel ( scope . row ) } } < / span > < / template >
< / el-table-column >
< el-table-column label = "操作" width = "94" fixed = "right" >
< template # default = "scope" > < el-button v-permisaction = "['sense:local-event:detail']" type="primary" link @click="openDetail(scope.row.id)" > 查看详情 < / el -button > < / template >
< / el-table-column >
< / el-table >
< pagination v-show = "total > 0" v-model:current-page="query.pageIndex" v-model:page-size="query.pageSize" :total="total" @pagination="loadEvents" / >
< p class = "retention-help" > 保留策略由 Sense 服务端配置 ; 列表显示每条记录的实际到期时间 , 到期清理策略不由本页面修改 。 列表和详情读取均受权限控制并进入系统操作审计 。 < / p >
< / el-card >
< el-dialog v-model = "detailOpen" title="本地事件详情" width="min(760px, calc(100vw - 24px))" :close-on-click-modal="false" >
< el -descriptions v-if = "selected" :column="2" border class="detail-descriptions" >
< el -descriptions -item label = "事件编号" > { { selected . id } } < / el-descriptions-item >
< el-descriptions-item label = "发生时间" > { { parseTime ( selected . occurredAt ) } } < / el-descriptions-item >
< el-descriptions-item label = "摄像头" > { { selected . sourceRef } } / { { selected . sourceLabel || '未命名位置' } } < / el-descriptions-item >
< el-descriptions-item label = "规则来源" > { { selected . ruleName } } { { selected . ruleVersion } } < / el-descriptions-item >
< el-descriptions-item label = "区域引用" > { { selected . zoneRef || '-' } } < / el-descriptions-item >
< el-descriptions-item label = "保留期限" > { { retentionLabel ( selected ) } } < / el-descriptions-item >
< el-descriptions-item label = "候选状态" > < el-tag :type = "candidateType(selected.candidateState)" > { { candidateLabel ( selected . candidateState ) } } < / el-tag > < / el-descriptions-item >
< el-descriptions-item label = "证据状态" > < el-tag :type = "evidenceType(selected.evidenceState)" > { { evidenceLabel ( selected . evidenceState ) } } < / el-tag > < / el-descriptions-item >
< el-descriptions-item label = "Bell 送达状态" :span = "2" > < strong > 不适用 < / strong > — 本地候选未发送到 Bell , 本页不推断 Bell Alert 状态 。 < / el-descriptions-item >
< / el-descriptions >
< el-alert v-if = "selected" :title="evidenceTitle" :type="evidenceType(selected.evidenceState)" :closable="false" show-icon class="evidence-alert" >
{{ selected.evidenceDetail | | ' 暂无更多证据处理说明 ' }} < template v-if = "selected.evidenceRef" > < br > 本地证据引用 : { { selected . evidenceRef } } < / template >
< / el-alert >
< template # footer > < el-button @click ="detailOpen = false" > 关闭 < / el -button > < / template >
< / el-dialog >
< / template >
< / BasicLayout >
< / template >
< script setup >
import { computed , onMounted , reactive , ref } from 'vue'
import { Refresh , RefreshLeft , Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getLocalEvent , listLocalEvents } from '@/api/sense/local-event'
import { buildLocalEventQuery , candidateLabel , candidateType , evidenceLabel , evidenceType , retentionLabel } from './localEventState'
defineOptions ( { name : 'SenseLocalEvent' } )
const loading = ref ( false )
const events = ref ( [ ] )
const total = ref ( 0 )
const detailOpen = ref ( false )
const selected = ref ( null )
const now = Date . now ( )
const occurredRange = ref ( [ new Date ( now - 24 * 60 * 60 * 1000 ) , new Date ( now ) ] )
const query = reactive ( { pageIndex : 1 , pageSize : 10 , candidateState : '' , evidenceState : '' , ruleRef : '' , keyword : '' } )
const pageSummary = computed ( ( ) => events . value . reduce ( ( value , item ) => {
if ( item . candidateState === 'candidate' ) value . candidate += 1
if ( item . candidateState === 'confirmed' ) value . confirmed += 1
if ( item . evidenceState === 'failed' ) value . failed += 1
return value
} , { candidate : 0 , confirmed : 0 , failed : 0 } ) )
const evidenceTitle = computed ( ( ) => ` 证据处理: ${ evidenceLabel ( selected . value ? . evidenceState ) } ` )
function unwrap ( response ) { return response ? . data ? . data ? ? response ? . data ? ? response }
async function loadEvents ( ) {
loading . value = true
try {
const payload = unwrap ( await listLocalEvents ( buildLocalEventQuery ( query , occurredRange . value ) ) )
events . value = payload ? . list || payload ? . data || [ ]
total . value = payload ? . count || 0
} catch ( error ) {
ElMessage . error ( error . message || '本地事件查询失败' )
} finally {
loading . value = false
}
}
function search ( ) { query . pageIndex = 1 ; loadEvents ( ) }
function reset ( ) {
query . pageIndex = 1 ; query . candidateState = '' ; query . evidenceState = '' ; query . ruleRef = '' ; query . keyword = ''
const current = Date . now ( ) ; occurredRange . value = [ new Date ( current - 24 * 60 * 60 * 1000 ) , new Date ( current ) ]
loadEvents ( )
}
async function openDetail ( id ) {
try {
selected . value = unwrap ( await getLocalEvent ( id ) )
detailOpen . value = true
} catch ( error ) {
ElMessage . error ( error . message || '本地事件详情查询失败' )
}
}
onMounted ( loadEvents )
< / script >
< style scoped >
. page - header { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px } . page - header h3 { margin : 0 0 6 px } . page - header p { margin : 0 ; color : # 909399 } . boundary - alert { margin : 16 px 0 } . summary - row { margin - bottom : 16 px } . summary - item { display : flex ; align - items : center ; justify - content : space - between ; min - height : 68 px ; padding : 12 px 16 px ; border : 1 px solid # ebeef5 ; border - radius : 4 px } . summary - item span { color : # 606266 } . summary - item strong { font - size : 24 px } . filter - form { display : flex ; align - items : flex - end ; flex - wrap : wrap ; gap : 0 12 px ; margin - bottom : 2 px } . filter - form . el - form - item { width : 210 px } . filter - form . time - field { width : 420 px } . filter - form . filter - actions { width : auto } . filter - form : deep ( . el - select ) , . filter - form : deep ( . el - date - editor ) { width : 100 % } small { color : # 909399 } . retention - warning { color : # e6a23c } . retention - help { margin : 12 px 0 0 ; color : # 909399 ; font - size : 12 px } . evidence - alert { margin - top : 16 px } @ media ( max - width : 768 px ) { . filter - form . el - form - item , . filter - form . time - field { width : 100 % } . detail - descriptions : deep ( . el - descriptions _ _body . el - descriptions _ _table ) { display : block ; overflow - x : auto } }
< / style >