feat: v0.9.0 新增记录日志、设备更换记录及IMC服务集成

- 新增 RecordsLog.vue 操作记录日志页面
- 新增 ReplacementRecords.vue 设备更换记录页面
- 新增 imc_service.py IMC网管系统集成服务
- 新增 datetime.js 前端日期时间工具函数
- 新增 OLT时间同步.md 文档
- 扩展 devices.py API:设备更换记录、批量操作等
- 扩展 ssh_service.py:OLT时间同步功能
- 扩展 olt.py:新增时间同步相关接口
- 更新 DeviceList.vue:增强设备列表功能
- 更新路由和导航菜单
- 将 .claude/ 和 .mcp.json 加入 .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 10:36:06 +08:00
parent eaabebbceb
commit 5aadbc78c6
24 changed files with 2830 additions and 45 deletions
+91 -1
View File
@@ -32,6 +32,9 @@
新增设备
<el-badge v-if="newDeviceCount > 0" :value="newDeviceCount" style="margin-left: 4px" />
</el-button>
<el-button v-if="can('olt.manage')" type="info" plain size="small" @click="openNtpSync" :loading="ntpSyncing">
同步NTP
</el-button>
<el-button v-if="can('olt.loopback')" type="danger" plain size="small" @click="runLoopbackDetection" :loading="loopDetecting">
环路检测
</el-button>
@@ -257,6 +260,67 @@
</template>
</el-dialog>
<!-- NTP 时间同步 -->
<el-dialog v-model="ntpSyncVisible" title="NTP 时间服务器同步" width="560px" destroy-on-close>
<template v-if="ntpPhase === 'config'">
<p style="margin: 0 0 16px; color: var(--text-muted); font-size: 13px">
批量对所有 OLT 执行 NTP 时间服务器配置并强制保存旧服务器若不存在会自动忽略
</p>
<el-form label-width="110px">
<el-form-item label="旧 NTP 服务器">
<el-input v-model="ntpForm.old_server" class="mono-val" placeholder="172.16.0.254" />
</el-form-item>
<el-form-item label="新 NTP 服务器">
<el-input v-model="ntpForm.new_server" class="mono-val" placeholder="172.16.1.252" />
</el-form-item>
</el-form>
<div style="background: var(--bg-subtle); border: 1px solid var(--border-subtle); border-radius: 6px; padding: 10px 14px; font-size: 12px; color: var(--text-muted); font-family: monospace; line-height: 1.7">
<div>system-view</div>
<div>undo ntp-service unicast-server {{ ntpForm.old_server }}</div>
<div>ntp-service unicast-server {{ ntpForm.new_server }}</div>
<div>clock timezone Beijing add 08:00:00</div>
<div>quit</div>
<div>save force</div>
</div>
</template>
<template v-else>
<div class="result-stats" style="margin-bottom: 16px">
<div class="result-stat">
<span class="rs-val" style="color: var(--success)">{{ ntpSyncResult.success || 0 }}</span>
<span class="rs-label">成功</span>
</div>
<div class="result-stat">
<span class="rs-val offline">{{ ntpSyncResult.failed || 0 }}</span>
<span class="rs-label">失败</span>
</div>
<div class="result-stat">
<span class="rs-val muted">{{ ntpSyncResult.total || 0 }}</span>
<span class="rs-label">共计</span>
</div>
</div>
<el-table :data="ntpSyncResult.results || []" border size="small" max-height="320">
<el-table-column prop="olt_location" label="OLT" min-width="150" show-overflow-tooltip />
<el-table-column prop="olt_ip" label="IP" width="140">
<template #default="{ row }"><span class="mono-val">{{ row.olt_ip }}</span></template>
</el-table-column>
<el-table-column label="状态" width="72" align="center">
<template #default="{ row }">
<el-tag :type="row.success ? 'success' : 'danger'" size="small">{{ row.success ? '成功' : '失败' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="error" label="错误信息" min-width="130" show-overflow-tooltip>
<template #default="{ row }">
<span style="color: var(--danger); font-size: 12px">{{ row.error || '—' }}</span>
</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button @click="ntpSyncVisible = false">关闭</el-button>
<el-button v-if="ntpPhase === 'config'" type="primary" @click="runNtpSync" :loading="ntpSyncing">开始同步</el-button>
</template>
</el-dialog>
<!-- 添加/编辑 OLT -->
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑 OLT 设备' : '添加 OLT 设备'" width="480px" destroy-on-close>
<el-form :model="form" label-width="90px">
@@ -443,7 +507,7 @@
<el-table-column label="最后发现" width="155">
<template #default="{ row }">
<span style="font-size: 12px; color: var(--text-muted)">
{{ row.last_seen_at ? new Date(row.last_seen_at).toLocaleString() : '—' }}
{{ fmtTime(row.last_seen_at) }}
</span>
</template>
</el-table-column>
@@ -508,6 +572,7 @@
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from '../utils/message'
import { ElMessageBox } from 'element-plus'
import { fmtTime } from '../utils/datetime'
import request from '../utils/request'
import { useMobile } from '../composables/useMobile'
import { usePermission } from '../composables/usePermission'
@@ -941,6 +1006,31 @@ const openPortManagerFromCard = async (device) => {
await openPortManager()
}
const ntpSyncing = ref(false)
const ntpSyncVisible = ref(false)
const ntpPhase = ref('config') // 'config' | 'results'
const ntpForm = ref({ old_server: '172.16.0.254', new_server: '172.16.1.252' })
const ntpSyncResult = ref({ total: 0, success: 0, failed: 0, results: [] })
const openNtpSync = () => {
ntpPhase.value = 'config'
ntpSyncResult.value = { total: 0, success: 0, failed: 0, results: [] }
ntpSyncVisible.value = true
}
const runNtpSync = async () => {
ntpSyncing.value = true
try {
const { data } = await request.post('/olt/sync-ntp', ntpForm.value)
ntpSyncResult.value = data
ntpPhase.value = 'results'
} catch (error) {
ElMessage.error(error.response?.data?.detail || 'NTP 同步失败')
} finally {
ntpSyncing.value = false
}
}
onMounted(() => {
fetchRegions()
fetchDevices()