feat(api): 添加OLT端口管理和设备发现功能

- 在check.py中更新discover_olt方法,使用scan_and_discover替代check_olt_devices
- 在olt.py中添加get_olt_ports和toggle_olt_port接口,支持获取和控制OLT端口状态
- 添加TogglePortRequest模型用于端口操作请求验证
- 更新SSHService类,新增get_olt_ports和toggle_olt_port方法
- 优化CheckService中的MAC地址索引逻辑,支持模型信息更新
```
This commit is contained in:
2026-04-04 22:13:22 +08:00
parent 381ea7085d
commit dfa8fa62a8
21 changed files with 4121 additions and 733 deletions
+456 -155
View File
@@ -1,90 +1,155 @@
<template>
<div class="device-list">
<el-card>
<el-form inline>
<el-form-item label="区域">
<el-select v-model="filters.region" placeholder="全部区域" clearable @change="search" style="width: 180px">
<el-option v-for="r in regions" :key="r" :label="r" :value="r" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filters.status" placeholder="全部" clearable @change="search" style="width: 100px">
<el-option label="在线" value="online" />
<el-option label="离线" value="offline" />
<el-option label="未知" value="unknown" />
</el-select>
</el-form-item>
<el-form-item label="搜索">
<el-input
v-model="filters.keyword"
placeholder="MAC/区域/学校/楼宇/房间号"
clearable
@clear="search"
@keyup.enter="search"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
</el-form-item>
<el-form-item style="margin-left: auto">
<el-button type="danger" plain @click="clearStatus" :loading="clearing">清空状态</el-button>
<el-button type="warning" @click="refreshAllStatus" :loading="refreshing" style="margin-left: 8px">
{{ refreshing ? '更新中...' : '全部更新' }}
</el-button>
</el-form-item>
</el-form>
<!-- 页头 -->
<div class="page-header">
<div class="page-title-group">
<h1 class="page-title">设备列表</h1>
<span class="page-subtitle">ONU 终端设备管理</span>
</div>
<div class="header-actions">
<el-button type="danger" plain size="small" @click="clearStatus" :loading="clearing">
清空状态
</el-button>
<el-button type="warning" size="small" @click="refreshAllStatus" :loading="refreshing">
{{ refreshing ? '更新中…' : '全部更新' }}
</el-button>
</div>
</div>
<el-table :data="devices" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="region" label="区域" width="100" />
<el-table-column prop="school_name" label="学校名称" width="180" />
<el-table-column prop="building" label="楼宇" width="130" />
<el-table-column prop="place_type" label="场所类型" width="130" />
<el-table-column prop="room_number" label="房间号" width="100" />
<el-table-column prop="mac_address" label="MAC地址" width="180">
<template #default="{ row }">
<el-link type="primary" @click="openDetail(row)">
{{ formatMac(row.mac_address) }}
</el-link>
<!-- 筛选栏 -->
<div class="filter-bar">
<div class="filter-group">
<label class="filter-label">区域</label>
<el-select
v-model="filters.region"
placeholder="全部区域"
clearable
@change="search"
size="small"
style="width: 160px"
>
<el-option v-for="r in regions" :key="r" :label="r" :value="r" />
</el-select>
</div>
<div class="filter-group">
<label class="filter-label">状态</label>
<el-select
v-model="filters.status"
placeholder="全部"
clearable
@change="search"
size="small"
style="width: 110px"
>
<el-option label="在线" value="online" />
<el-option label="离线" value="offline" />
<el-option label="未知" value="unknown" />
</el-select>
</div>
<div class="filter-group">
<label class="filter-label">搜索</label>
<el-input
v-model="filters.keyword"
placeholder="MAC / 区域 / 学校 / 楼宇 / 房间"
clearable
size="small"
@clear="search"
@keyup.enter="search"
style="width: 280px"
>
<template #prefix>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag
:type="row.status === 'online' ? 'success' : row.status === 'offline' ? 'danger' : 'info'"
size="small"
>
{{ row.status === 'online' ? '在线' : row.status === 'offline' ? '离线' : '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="distance_m" label="距离" width="90" align="right">
<template #default="{ row }">
{{ row.distance_m ? row.distance_m + 'm' : '-' }}
</template>
</el-table-column>
<el-table-column prop="notes" label="备注" min-width="100" show-overflow-tooltip />
</el-table>
</el-input>
</div>
<el-button type="primary" size="small" @click="search">查询</el-button>
</div>
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
@current-change="loadDevices"
@size-change="handleSizeChange"
layout="total, sizes, prev, pager, next"
style="margin-top: 20px; justify-content: flex-end"
/>
</el-card>
<!-- 检查结果对话框 -->
<el-dialog v-model="checkResultVisible" title="全部更新结果" width="700px" destroy-on-close>
<div v-if="checkInProgress" style="text-align: center; padding: 40px 0; color: #909399">
正在扫描所有 OLT请稍候...
<!-- 数据面板 -->
<div class="data-panel">
<!-- 统计行 -->
<div class="data-stats">
<span class="data-total"> <strong>{{ total }}</strong> 条记录</span>
<div class="status-legend">
<span class="legend-item">
<span class="led online"></span>在线
</span>
<span class="legend-item">
<span class="led offline"></span>离线
</span>
<span class="legend-item">
<span class="led unknown"></span>未知
</span>
</div>
</div>
<!-- 表格 -->
<el-table
:data="devices"
border
stripe
size="small"
style="width: 100%"
>
<el-table-column type="index" label="#" width="52" align="center">
<template #default="{ $index }">
<span class="row-index">{{ (page - 1) * pageSize + $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="region" label="区域" width="90" />
<el-table-column prop="school_name" label="学校名称" width="170" show-overflow-tooltip />
<el-table-column prop="building" label="楼宇" width="120" show-overflow-tooltip />
<el-table-column prop="place_type" label="场所类型" width="110" show-overflow-tooltip />
<el-table-column prop="room_number" label="房间号" width="90" />
<el-table-column prop="mac_address" label="MAC 地址" width="160">
<template #default="{ row }">
<span class="mac-link" @click="openDetail(row)">
{{ formatMac(row.mac_address) }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="76" align="center">
<template #default="{ row }">
<span class="status-badge" :class="row.status">
<span class="status-dot-sm"></span>
{{ row.status === 'online' ? '在线' : row.status === 'offline' ? '离线' : '未知' }}
</span>
</template>
</el-table-column>
<el-table-column prop="distance_m" label="距离" width="80" align="right">
<template #default="{ row }">
<span class="mono-val">{{ row.distance_m ? row.distance_m + 'm' : '—' }}</span>
</template>
</el-table-column>
<el-table-column prop="notes" label="备注" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
<span style="color: var(--text-muted); font-size: 12px">{{ row.notes || '—' }}</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrap">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
:total="total"
@current-change="loadDevices"
@size-change="handleSizeChange"
layout="total, sizes, prev, pager, next"
/>
</div>
</div>
<!-- 检查结果对话框 -->
<el-dialog v-model="checkResultVisible" title="全部更新结果" width="680px" destroy-on-close>
<div v-if="checkInProgress" class="dialog-loading">
<div class="loading-spinner"></div>
<span>正在扫描所有 OLT请稍候</span>
</div>
<div v-else-if="checkResult">
<el-alert
:type="checkResult.success ? 'success' : 'error'"
@@ -93,27 +158,34 @@
:closable="false"
style="margin-bottom: 16px"
/>
<template v-if="checkResult.success">
<div style="display: flex; gap: 24px; margin-bottom: 16px">
<el-statistic title="在线设备" :value="checkResult.total_online || 0" />
<el-statistic title="离线设备" :value="checkResult.total_offline || 0" />
<el-statistic title="失败 OLT" :value="(checkResult.errors || []).length" />
<div class="result-stats">
<div class="result-stat">
<span class="rs-val online">{{ checkResult.total_online || 0 }}</span>
<span class="rs-label">在线设备</span>
</div>
<div class="result-stat">
<span class="rs-val offline">{{ checkResult.total_offline || 0 }}</span>
<span class="rs-label">离线设备</span>
</div>
<div class="result-stat">
<span class="rs-val warn">{{ (checkResult.errors || []).length }}</span>
<span class="rs-label">失败 OLT</span>
</div>
</div>
<el-table :data="checkResult.results || []" border stripe size="small" max-height="300">
<el-table-column prop="olt_location" label="OLT名称" min-width="150" show-overflow-tooltip />
<el-table-column label="在线" width="80" align="center">
<el-table :data="checkResult.results || []" border size="small" max-height="280">
<el-table-column prop="olt_location" label="OLT 名称" min-width="150" show-overflow-tooltip />
<el-table-column label="在线" width="70" align="center">
<template #default="{ row }">
<el-tag type="success" size="small">{{ row.online || 0 }}</el-tag>
<span class="mono-val" style="color: var(--success)">{{ row.online || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="离线" width="80" align="center">
<el-table-column label="离线" width="70" align="center">
<template #default="{ row }">
<el-tag type="danger" size="small">{{ row.offline || 0 }}</el-tag>
<span class="mono-val" style="color: var(--danger)">{{ row.offline || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.success ? 'success' : 'danger'" size="small">
{{ row.success ? '成功' : '失败' }}
@@ -123,7 +195,6 @@
</el-table>
</template>
</div>
<template #footer>
<el-button @click="checkResultVisible = false">关闭</el-button>
<el-button type="primary" @click="checkResultVisible = false; loadDevices()" v-if="!checkInProgress">
@@ -135,35 +206,29 @@
<!-- 设备详情对话框 -->
<el-dialog v-model="detailVisible" title="设备详情" width="500px" destroy-on-close>
<el-descriptions :column="2" border>
<el-descriptions-item label="MAC地址" :span="2">
{{ formatMac(selectedDevice.mac_address) }}
<el-descriptions-item label="MAC 地址" :span="2">
<span class="mono-val accent">{{ formatMac(selectedDevice.mac_address) }}</span>
</el-descriptions-item>
<el-descriptions-item label="区域">{{ selectedDevice.region }}</el-descriptions-item>
<el-descriptions-item label="学校">{{ selectedDevice.school_name }}</el-descriptions-item>
<el-descriptions-item label="楼宇">{{ selectedDevice.building || '-' }}</el-descriptions-item>
<el-descriptions-item label="房间号">{{ selectedDevice.room_number || '-' }}</el-descriptions-item>
<el-descriptions-item label="楼宇">{{ selectedDevice.building || '' }}</el-descriptions-item>
<el-descriptions-item label="房间号">{{ selectedDevice.room_number || '' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="selectedDevice.status === 'online' ? 'success' : selectedDevice.status === 'offline' ? 'danger' : 'info'" size="small">
<span class="status-badge" :class="selectedDevice.status">
<span class="status-dot-sm"></span>
{{ selectedDevice.status === 'online' ? '在线' : selectedDevice.status === 'offline' ? '离线' : '未知' }}
</el-tag>
</span>
</el-descriptions-item>
<el-descriptions-item label="距离">
{{ selectedDevice.distance_m ? selectedDevice.distance_m + ' 米' : '-' }}
<span class="mono-val">{{ selectedDevice.distance_m ? selectedDevice.distance_m + ' 米' : '' }}</span>
</el-descriptions-item>
<el-descriptions-item label="端口" :span="2">
{{ formatPort(selectedDevice) }}
</el-descriptions-item>
<el-descriptions-item label="型号" :span="2">
{{ selectedDevice.model || '-' }}
</el-descriptions-item>
<el-descriptions-item label="所属OLT" :span="2">
{{ selectedDevice.olt_location || '-' }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ selectedDevice.notes || '-' }}
<span class="mono-val">{{ formatPort(selectedDevice) }}</span>
</el-descriptions-item>
<el-descriptions-item label="型号" :span="2">{{ selectedDevice.model || '—' }}</el-descriptions-item>
<el-descriptions-item label="所属 OLT" :span="2">{{ selectedDevice.olt_location || '—' }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ selectedDevice.notes || '—' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="info" :loading="deviceRefreshing" @click="doRefreshDevice" :disabled="!selectedDevice.olt_id">更新</el-button>
<el-button type="primary" @click="openEdit">编辑</el-button>
@@ -175,23 +240,13 @@
</el-dialog>
<!-- 编辑对话框 -->
<el-dialog v-model="editVisible" title="编辑设备信息" width="480px" destroy-on-close>
<el-form :model="editForm" label-width="80px">
<el-form-item label="区域">
<el-input v-model="editForm.region" />
</el-form-item>
<el-form-item label="学校">
<el-input v-model="editForm.school_name" />
</el-form-item>
<el-form-item label="楼宇">
<el-input v-model="editForm.building" />
</el-form-item>
<el-form-item label="房间号">
<el-input v-model="editForm.room_number" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="editForm.notes" type="textarea" :rows="2" />
</el-form-item>
<el-dialog v-model="editVisible" title="编辑设备信息" width="460px" destroy-on-close>
<el-form :model="editForm" label-width="70px">
<el-form-item label="区域"><el-input v-model="editForm.region" /></el-form-item>
<el-form-item label="学校"><el-input v-model="editForm.school_name" /></el-form-item>
<el-form-item label="楼宇"><el-input v-model="editForm.building" /></el-form-item>
<el-form-item label="房间号"><el-input v-model="editForm.room_number" /></el-form-item>
<el-form-item label="备注"><el-input v-model="editForm.notes" type="textarea" :rows="2" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
@@ -200,7 +255,7 @@
</el-dialog>
<!-- 业务下发对话框 -->
<el-dialog v-model="provisionVisible" title="业务下发" width="560px" destroy-on-close>
<el-dialog v-model="provisionVisible" title="业务下发" width="540px" destroy-on-close>
<el-alert
v-if="provisionResult"
:type="provisionResult.success ? 'success' : 'error'"
@@ -210,25 +265,30 @@
style="margin-bottom: 16px"
/>
<el-descriptions :column="2" border v-if="selectedDevice.mac_address">
<el-descriptions-item label="MAC地址">{{ formatMac(selectedDevice.mac_address) }}</el-descriptions-item>
<el-descriptions-item label="端口">{{ formatPort(selectedDevice) }}</el-descriptions-item>
<el-descriptions-item label="学校">{{ selectedDevice.school_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="MAC 地址">
<span class="mono-val accent">{{ formatMac(selectedDevice.mac_address) }}</span>
</el-descriptions-item>
<el-descriptions-item label="端口">
<span class="mono-val">{{ formatPort(selectedDevice) }}</span>
</el-descriptions-item>
<el-descriptions-item label="学校">{{ selectedDevice.school_name || '—' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="selectedDevice.status === 'online' ? 'success' : selectedDevice.status === 'offline' ? 'danger' : 'info'" size="small">
<span class="status-badge" :class="selectedDevice.status">
<span class="status-dot-sm"></span>
{{ selectedDevice.status === 'online' ? '在线' : selectedDevice.status === 'offline' ? '离线' : '未知' }}
</el-tag>
</span>
</el-descriptions-item>
</el-descriptions>
<el-divider>将执行的配置</el-divider>
<el-text type="info" size="small">
<pre style="margin: 0; font-size: 12px; line-height: 1.6">system-view
<div class="cli-block">
<pre>system-view
interface {{ formatPort(selectedDevice) }}
uni 1 vlan-mode trunk pvid 4094 2000 to 2000 3000 to 3010
port link-type trunk
undo port trunk permit vlan 1
port trunk permit vlan 2000 3000 to 3010 4094
save force</pre>
</el-text>
</div>
<template #footer>
<el-button @click="provisionVisible = false; provisionResult = null">关闭</el-button>
<el-button
@@ -259,29 +319,24 @@ const pageSize = ref(20)
const regions = ref([])
const filters = ref({ region: '', keyword: '', school_name: '', status: '' })
// 详情对话框
const detailVisible = ref(false)
const selectedDevice = ref({})
const deviceRefreshing = ref(false)
// 业务下发对话框
const provisionVisible = ref(false)
const provisionLoading = ref(false)
const provisionResult = ref(null)
const refreshing = ref(false)
const clearing = ref(false)
// 编辑对话框
const editVisible = ref(false)
const editForm = ref({ region: '', school_name: '', building: '', room_number: '', notes: '' })
const editSaving = ref(false)
// 检查结果对话框
const checkResultVisible = ref(false)
const checkResult = ref(null)
const checkInProgress = ref(false)
// 清空所有设备状态
const clearStatus = async () => {
clearing.value = true
try {
@@ -295,13 +350,11 @@ const clearStatus = async () => {
}
}
// 刷新所有设备状态
const refreshAllStatus = async () => {
refreshing.value = true
checkResultVisible.value = true
checkInProgress.value = true
checkResult.value = null
try {
const { data } = await deviceApi.refreshAllStatus()
checkResult.value = {
@@ -319,7 +372,6 @@ const refreshAllStatus = async () => {
}
}
// 格式化 MAC 地址
const formatMac = (mac) => {
if (!mac) return ''
const clean = mac.toUpperCase().replace(/[:-]/g, '')
@@ -329,27 +381,25 @@ const formatMac = (mac) => {
clean.substring(8, 12).toLowerCase()
}
// 格式化端口显示
const formatPort = (device) => {
if (device.port_id) return `Onu${device.port_id}`
if (!device.slot_number && !device.port_number) return '-'
if (!device.slot_number && !device.port_number) return ''
return `Onu${device.slot_number || '?'}/${device.port_number || '?'}`
}
// 打开详情对话框
const openDetail = (row) => {
selectedDevice.value = { ...row }
provisionResult.value = null
detailVisible.value = true
}
// 单台设备状态更新
const doRefreshDevice = async () => {
deviceRefreshing.value = true
try {
const { data } = await deviceApi.refreshDevice(selectedDevice.value.id)
selectedDevice.value.status = data.status
selectedDevice.value.distance_m = data.distance_m
if (data.model) selectedDevice.value.model = data.model
ElMessage.success(`更新完成:${data.status === 'online' ? '在线' : '离线'}${data.distance_m ? ',距离 ' + data.distance_m + 'm' : ''}`)
loadDevices()
} catch (error) {
@@ -359,7 +409,6 @@ const doRefreshDevice = async () => {
}
}
// 打开编辑对话框
const openEdit = () => {
editForm.value = {
region: selectedDevice.value.region || '',
@@ -386,13 +435,11 @@ const saveEdit = async () => {
}
}
// 打开业务下发对话框
const openProvision = () => {
provisionResult.value = null
provisionVisible.value = true
}
// 执行业务下发
const doProvision = async () => {
provisionLoading.value = true
try {
@@ -408,7 +455,6 @@ const doProvision = async () => {
}
}
// 加载区域列表
const loadRegions = async () => {
const { data } = await deviceApi.getRegions()
regions.value = data
@@ -449,6 +495,261 @@ onMounted(() => {
<style scoped>
.device-list {
padding: 20px;
padding: 24px;
}
/* 页头 */
.page-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 20px;
}
.page-title-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.page-subtitle {
font-size: 12px;
color: var(--text-muted);
}
.header-actions {
display: flex;
gap: 8px;
}
/* 筛选栏 */
.filter-bar {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 18px;
background: var(--bg-card);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
margin-bottom: 16px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.04em;
white-space: nowrap;
}
/* 数据面板 */
.data-panel {
background: var(--bg-card);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
overflow: hidden;
}
.data-stats {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle);
}
.data-total {
font-size: 12px;
color: var(--text-muted);
}
.data-total strong {
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
.status-legend {
display: flex;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--text-muted);
}
.led {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.led.online { background: var(--success); box-shadow: 0 0 5px var(--success); }
.led.offline { background: var(--danger); }
.led.unknown { background: var(--text-muted); }
/* 表格中的元素 */
.row-index {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
}
.mac-link {
font-family: var(--font-mono);
font-size: 12px;
color: var(--accent);
cursor: pointer;
letter-spacing: 0.02em;
transition: color 0.15s;
}
.mac-link:hover {
color: #00f0cc;
text-decoration: underline;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 600;
padding: 2px 7px;
border-radius: 10px;
white-space: nowrap;
}
.status-badge.online {
background: var(--success-dim);
color: var(--success);
border: 1px solid rgba(34,197,94,0.25);
}
.status-badge.offline {
background: var(--danger-dim);
color: var(--danger);
border: 1px solid rgba(239,68,68,0.25);
}
.status-badge.unknown {
background: rgba(255,255,255,0.04);
color: var(--text-muted);
border: 1px solid var(--border-subtle);
}
.status-dot-sm {
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.online .status-dot-sm { box-shadow: 0 0 4px currentColor; }
.mono-val {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-secondary);
}
.mono-val.accent { color: var(--accent); font-size: 13px; }
/* 分页 */
.pagination-wrap {
display: flex;
justify-content: flex-end;
padding: 14px 16px;
border-top: 1px solid var(--border-subtle);
}
/* 对话框内元素 */
.dialog-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 48px 0;
color: var(--text-muted);
font-size: 13px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 2px solid var(--border-default);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.result-stats {
display: flex;
gap: 32px;
padding: 16px 0;
margin-bottom: 16px;
border-bottom: 1px solid var(--border-subtle);
}
.result-stat {
display: flex;
flex-direction: column;
gap: 4px;
}
.rs-val {
font-family: var(--font-mono);
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.rs-val.online { color: var(--success); }
.rs-val.offline { color: var(--danger); }
.rs-val.warn { color: var(--warning); }
.rs-label {
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.04em;
}
/* CLI 预览块 */
.cli-block {
background: #0a0e1a;
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
padding: 14px 16px;
overflow-x: auto;
}
.cli-block pre {
margin: 0;
font-family: var(--font-mono);
font-size: 12px;
color: #a3e6d8;
line-height: 1.7;
white-space: pre;
}
</style>