@@ -0,0 +1,170 @@
< template >
< BasicLayout >
< template # wrapper >
< el-card class = "box-card" >
< div class = "page-header" >
< div >
< h3 > 运维中心 < / h3 >
< p > 集中查看设备 、 媒体和可选本地推理的期望态 、 实际态与未收敛问题 。 < / p >
< / div >
< el-button :icon = "Refresh" :loading = "loading" @click ="load" > 刷新状态 < / el -button >
< / div >
< el-alert type = "info" :closable = "false" show -icon class = "boundary-alert" >
< template # title > 本地推理是可选能力 < / template >
Brain 未安装时显示 “ 不可用 ( 未安装 ) ” , 不会导致页面失败 , 也不会阻断设备和媒体运维 。 运维异常不会发送到 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 > { { summary . convergedCount } } / { { summary . managedCount } } < / strong > < / div >
< / el-col >
< el-col :xs = "24" :sm = "8" >
< div class = "summary-item" > < span > 待处理问题 < / span > < strong class = "warning-number" > { { summary . actionableProblems } } < / strong > < / div >
< / el-col >
< el-col :xs = "24" :sm = "8" >
< div class = "summary-item" > < span > 本地推理适配器 < / span > < strong class = "adapter-state" > { { inferenceState } } < / strong > < / div >
< / el-col >
< / el-row >
< div class = "section-header" >
< div > < h4 > 问题队列 < / h4 > < p > 默认只读诊断 ; 只有明确可重试的项目才显示受控重试 。 < / p > < / div >
< / div >
< el-form :model = "query" label -width = " 76px " class = "filter-form" @submit.prevent ="search" >
< el -form -item label = "对象类型" >
< el-select v-model = "query.objectType" clearable placeholder="全部" >
< el -option label = "设备" value = "device" / > < el-option label = "媒体" value = "media" / > < el-option label = "本地推理" value = "local_inference" / >
< / el-select >
< / el-form-item >
< el-form-item label = "问题类型" >
< el-select v-model = "query.problemType" clearable placeholder="全部" >
< el -option label = "认证失败" value = "authentication_failed" / > < el-option label = "退避等待" value = "backoff_wait" / >
< el-option label = "时间漂移" value = "clock_drift" / > < el-option label = "孤儿安全闸" value = "orphan_safety_gate" / >
< el-option label = "能力未安装" value = "capability_unavailable" / > < el-option label = "尚未收敛" value = "not_converged" / >
< / el-select >
< / el-form-item >
< el-form-item label = "级别" >
< el-select v-model = "query.severity" clearable placeholder="全部" >
< el -option label = "高" value = "high" / > < el-option label = "中" value = "medium" / > < el-option label = "提示" value = "info" / >
< / el-select >
< / 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="problems" border stripe empty-text="当前筛选条件下没有未收敛问题" >
< el -table -column label = "对象" min -width = " 170 " >
< template # default = "scope" > < strong > { { scope . row . objectName } } < / strong > < div class = "muted" > { { objectLabel ( scope . row . objectType ) } } · { { scope . row . location || scope . row . objectId } } < / div > < / template >
< / el-table-column >
< el-table-column label = "问题" min -width = " 138 " > < template # default = "scope" > < div > { { problemLabel ( scope . row . problemType ) } } < / div > < small > { { scope . row . id } } < / small > < / template > < / el-table-column >
< el-table-column label = "期望态" min -width = " 110 " > < template # default = "scope" > { { stateLabel ( scope . row . expected ) } } < / template > < / el-table-column >
< el-table-column label = "实际态" min -width = " 120 " > < template # default = "scope" > < el-tag size = "small" :type = "severityType(scope.row.severity)" > { { stateLabel ( scope . row . actual ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column prop = "difference" label = "未收敛差异" min -width = " 210 " show -overflow -tooltip / >
< el-table-column label = "下次动作" min -width = " 190 " > < template # default = "scope" > < div > { { scope . row . nextAction } } < / div > < small v-if = "scope.row.nextRetryAt" > 自动重试 : {{ formatTime ( scope.row.nextRetryAt ) }} < / small > < / template > < / el -table -column >
< el-table-column label = "级别" width = "78" align = "center" > < template # default = "scope" > < el-tag size = "small" :type = "severityType(scope.row.severity)" > { { severityLabel ( scope . row . severity ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "操作" width = "156" fixed = "right" >
< template # default = "scope" >
< el-button v-permisaction = "['sense:operations:detail']" type="primary" link @click="openDetail(scope.row.id)" > 详情 < / el -button >
< el-button v-if = "canRetry(scope.row)" v-permisaction="['sense:operations:retry']" type="warning" link @click="confirmRetry(scope.row)" > 受控重试 < / el -button >
< span v-else-if = "scope.row.retryInProgress" class="muted" > 重试已排队 < / span >
< / 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="load" / >
< p class = "safety-help" > 受控重试会校验权限 、 当前版本和并发占用 , 并写入操作审计 。 孤儿资源仅隔离和提示 , 本页没有删除操作 。 < / 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 >
< el-descriptions-item label = "问题编号" > { { selected . id } } < / el-descriptions-item >
< el-descriptions-item label = "对象" > { { selected . objectName } } < / el-descriptions-item >
< el-descriptions-item label = "期望态" > { { stateLabel ( selected . expected ) } } < / el-descriptions-item >
< el-descriptions-item label = "实际态" > { { stateLabel ( selected . actual ) } } < / el-descriptions-item >
< el-descriptions-item label = "未收敛差异" :span = "2" > { { selected . difference } } < / el-descriptions-item >
< el-descriptions-item label = "退避 / 下次重试" > { { selected . nextRetryAt ? formatTime ( selected . nextRetryAt ) : '无退避' } } < / el-descriptions-item >
< el-descriptions-item label = "尝试次数" > { { selected . attemptCount } } < / el-descriptions-item >
< el-descriptions-item label = "建议处理" :span = "2" > { { selected . nextAction } } < / el-descriptions-item >
< / el-descriptions >
< el-alert v-if = "selected" title="安全闸已启用" type="warning" :closable="false" show-icon class="safety-alert" > {{ selected.safetyGate }} < / el -alert >
< p class = "boundary-text" > Brain / Bell 状态不会参与本地设备和媒体重试判定 。 < / p >
< template # footer >
< el-button @click ="detailOpen = false" > 关闭 < / el -button >
< el-button v-if = "canRetry(selected)" v-permisaction="['sense:operations:retry']" type="warning" @click="confirmRetry(selected)" > 受控重试 < / el -button >
< / template >
< / el-dialog >
< / template >
< / BasicLayout >
< / template >
< script setup >
import { onMounted , reactive , ref } from 'vue'
import { Refresh , RefreshLeft , Search } from '@element-plus/icons-vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { getOperationProblem , listOperationProblems , retryOperationProblem } from '@/api/sense/operations'
import { buildOperationsQuery , canRetry , objectLabel , problemLabel , severityLabel , severityType , stateLabel } from './operationsState'
defineOptions ( { name : 'SenseOperations' } )
const loading = ref ( false )
const problems = ref ( [ ] )
const total = ref ( 0 )
const summary = reactive ( { managedCount : 0 , convergedCount : 0 , actionableProblems : 0 , localInference : 'unavailable' } )
const query = reactive ( { pageIndex : 1 , pageSize : 10 , objectType : '' , problemType : '' , severity : '' , keyword : '' } )
const detailOpen = ref ( false )
const selected = ref ( null )
const inferenceState = ref ( '不可用(未安装)' )
function unwrap ( response ) { return response ? . data ? . data ? ? response ? . data ? ? response }
function formatTime ( value ) { return value ? new Date ( value ) . toLocaleString ( 'zh-CN' , { hour12 : false } ) : '—' }
async function load ( ) {
loading . value = true
try {
const payload = unwrap ( await listOperationProblems ( buildOperationsQuery ( query ) ) ) || { }
problems . value = payload . list || [ ]
total . value = payload . count || 0
Object . assign ( summary , payload . summary || { managedCount : 0 , convergedCount : 0 , actionableProblems : 0 } )
inferenceState . value = summary . localInference === 'configured' ? '已配置' : '不可用(未安装)'
} catch ( error ) {
ElMessage . error ( error . message || '运维状态加载失败' )
} finally {
loading . value = false
}
}
function search ( ) { query . pageIndex = 1 ; load ( ) }
function reset ( ) { Object . assign ( query , { pageIndex : 1 , objectType : '' , problemType : '' , severity : '' , keyword : '' } ) ; load ( ) }
async function openDetail ( id ) {
try {
selected . value = unwrap ( await getOperationProblem ( id ) )
detailOpen . value = true
} catch ( error ) {
ElMessage . error ( error . message || '状态详情加载失败' )
}
}
async function confirmRetry ( item ) {
try {
await ElMessageBox . confirm ( ` 将对“ ${ item . objectName } ”发起一次重试。此操作不会修改凭据,也不会删除媒体资源。 ` , '确认受控重试' , {
type : 'warning' , confirmButtonText : '确认重试' , cancelButtonText : '取消'
} )
await retryOperationProblem ( item . id , item . version )
ElMessage . success ( '重试任务已排队' )
detailOpen . value = false
await load ( )
} catch ( error ) {
if ( error !== 'cancel' && error !== 'close' ) ElMessage . warning ( error . message || '重试未排队,请刷新状态后再试' )
}
}
onMounted ( load )
< / script >
< style scoped >
. page - header , . section - header { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px } . page - header h3 , . section - header h4 { margin : 0 0 6 px } . page - header p , . section - header p { margin : 0 ; color : # 909399 } . boundary - alert { margin : 16 px 0 } . summary - row { margin - bottom : 20 px } . summary - item { display : flex ; align - items : center ; justify - content : space - between ; min - height : 72 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 : 22 px } . summary - item . warning - number { color : # e6a23c } . summary - item . adapter - state { font - size : 15 px ; color : # 909399 } . filter - form { display : flex ; align - items : flex - end ; flex - wrap : wrap ; gap : 0 12 px ; margin : 16 px 0 2 px } . filter - form . el - form - item { width : 210 px } . filter - form . filter - actions { width : auto } . filter - form : deep ( . el - select ) { width : 100 % } . muted , small { color : # 909399 ; font - size : 12 px } . safety - help , . boundary - text { margin : 12 px 0 0 ; color : # 909399 ; font - size : 12 px } . safety - alert { margin - top : 16 px } @ media ( max - width : 768 px ) { . filter - form . el - form - item { width : 100 % } . page - header { align - items : stretch ; flex - direction : column } . summary - item { margin - bottom : 8 px } }
< / style >