feat(import): 添加区域字段支持和改进模板路径处理

- 在OLT设备导入功能中添加region字段支持,从Excel模板读取区域信息
- 修复模板文件路径问题,使用相对路径动态构建模板文件路径
- 更新导入服务中的验证错误格式,包含行号和MAC地址信息

feat(check): 增强设备检测服务的端口信息同步

- 在CheckService中添加端口和OLT归属信息的同步逻辑
- 改进设备状态检查时的端口信息更新策略,避免用None覆盖现有值
- 扩展返回数据结构,包含端口ID、槽位号、端口号和OLT位置信息

feat(frontend): 添加设备列表刷新冷却机制和端口验证

- 实现设备状态刷新的60秒冷却时间限制,防止频繁操作
- 改进业务下发按钮的启用条件,同时支持port_id或slot_number+port_number组合
- 优化导入结果显示,显示具体的MAC地址信息

feat(olt): 添加重复MAC记录批量清除功能

- 新增批量清除重复MAC记录的功能,支持按OLT分组处理
- 实现SSH端口清除和自动忽略的批量操作流程
- 添加批量操作的进度提示和错误处理机制
```
This commit is contained in:
2026-04-09 11:33:04 +08:00
parent dcb2435514
commit d222978ae4
9 changed files with 1016 additions and 15 deletions
+21 -5
View File
@@ -10,8 +10,8 @@
<el-button v-if="!isMobile && can('device.delete')" type="danger" plain size="small" @click="clearStatus" :loading="clearing">
清空状态
</el-button>
<el-button v-if="can('device.check')" type="warning" size="small" @click="refreshAllStatus" :loading="refreshing">
{{ refreshing ? '更新中' : '全部更新' }}
<el-button v-if="can('device.check')" type="warning" size="small" @click="refreshAllStatus" :loading="refreshing" :disabled="refreshing || refreshCooldown > 0">
{{ refreshing ? '更新中' : refreshCooldown > 0 ? `冷却 ${refreshCooldown}s` : '全部更新' }}
</el-button>
<el-button v-if="can('device.import')" type="success" size="small" @click="importDialogVisible = true">
数据导入
@@ -203,7 +203,7 @@
v-if="can('device.edit')"
class="card-action-btn card-action-btn--primary"
@click.stop="openProvisionFromCard(device)"
:disabled="device.status !== 'online' || !device.port_id"
:disabled="device.status !== 'online' || (!device.port_id && !(device.slot_number && device.port_number))"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
@@ -347,7 +347,7 @@
<el-button v-if="can('device.check')" type="info" :loading="deviceRefreshing" @click="doRefreshDevice" :disabled="!selectedDevice.olt_id">更新</el-button>
<el-button v-if="can('device.edit')" type="warning" @click="openReplace">更换</el-button>
<el-button v-if="can('device.edit')" type="primary" @click="openEdit">编辑</el-button>
<el-button v-if="can('device.edit')" type="success" @click="openProvision" :disabled="selectedDevice.status !== 'online' || !selectedDevice.port_id">
<el-button v-if="can('device.edit')" type="success" @click="openProvision" :disabled="selectedDevice.status !== 'online' || (!selectedDevice.port_id && !(selectedDevice.slot_number && selectedDevice.port_number))">
业务下发
</el-button>
<el-button @click="detailVisible = false">关闭</el-button>
@@ -480,7 +480,7 @@ save force</pre>
/>
<div v-if="importResult.failed?.length" style="margin-top: 10px; max-height: 160px; overflow-y: auto">
<div v-for="f in importResult.failed" :key="f.row" style="font-size: 12px; color: var(--danger); padding: 2px 0">
第 {{ f.row }} 行:{{ f.reason }}
第 {{ f.row }} 行<template v-if="f.mac">{{ f.mac }}</template>{{ f.reason }}
</div>
</div>
</div>
@@ -521,6 +521,8 @@ const provisionVisible = ref(false)
const provisionLoading = ref(false)
const provisionResult = ref(null)
const refreshing = ref(false)
const refreshCooldown = ref(0)
let cooldownTimer = null
const clearing = ref(false)
const editVisible = ref(false)
@@ -636,6 +638,7 @@ const clearStatus = async () => {
}
const refreshAllStatus = async () => {
if (refreshCooldown.value > 0) return
refreshing.value = true
checkResultVisible.value = true
checkInProgress.value = true
@@ -654,6 +657,15 @@ const refreshAllStatus = async () => {
} finally {
checkInProgress.value = false
refreshing.value = false
clearInterval(cooldownTimer)
refreshCooldown.value = 60
cooldownTimer = setInterval(() => {
refreshCooldown.value--
if (refreshCooldown.value <= 0) {
clearInterval(cooldownTimer)
refreshCooldown.value = 0
}
}, 1000)
}
}
@@ -691,6 +703,10 @@ const doRefreshDevice = async () => {
selectedDevice.value.status = data.status
selectedDevice.value.distance_m = data.distance_m
if (data.model) selectedDevice.value.model = data.model
if (data.port_id !== undefined) selectedDevice.value.port_id = data.port_id
if (data.slot_number !== undefined) selectedDevice.value.slot_number = data.slot_number
if (data.port_number !== undefined) selectedDevice.value.port_number = data.port_number
if (data.olt_location !== undefined) selectedDevice.value.olt_location = data.olt_location
ElMessage.success(`更新完成:${data.status === 'online' ? '在线' : '离线'}${data.distance_m ? ',距离 ' + formatDistance(data.distance_m) : ''}`)
loadDevices()
} catch (error) {
+52
View File
@@ -454,6 +454,7 @@
</el-table-column>
</el-table>
<template #footer>
<el-button v-if="can('olt.manage') && dupList.length > 0" type="danger" :loading="batchClearing" @click="batchClearAll">批量清除并忽略</el-button>
<el-button @click="dupVisible = false">关闭</el-button>
</template>
</el-dialog>
@@ -533,6 +534,7 @@ const scanResult = ref({ total: 0, new: 0, devices: [], duplicates: [] })
const dupList = ref([])
const duplicateCount = ref(0)
const clearingPort = ref('')
const batchClearing = ref(false)
const newDevList = ref([])
const newDeviceCount = ref(0)
const newDevVisible = ref(false)
@@ -766,6 +768,56 @@ const deleteDupRecord = async (id) => {
}
}
const batchClearAll = async () => {
const total = dupList.value.length
if (total === 0) return
try {
await ElMessageBox.confirm(
`将对 ${total} 条重复 MAC 记录的所有离线端口执行 SSH 清除,然后自动忽略全部记录。此操作不可撤销,确认继续?`,
'批量清除确认',
{ type: 'warning', confirmButtonText: '确认清除', confirmButtonClass: 'el-button--danger' }
)
} catch {
return
}
batchClearing.value = true
// 按 olt_id 分组
const groups = {}
for (const row of dupList.value) {
const key = row.olt_id ?? '__unknown__'
if (!groups[key]) groups[key] = []
groups[key].push(row)
}
let cleared = 0, failed = 0
// 每个 OLT 一个并发任务,组内串行处理完所有重复 MAC 再结束
await Promise.all(
Object.values(groups).map(async (rows) => {
for (const row of rows) {
const offlinePorts = (row.ports || []).filter(p => p.status !== 'online')
for (const p of offlinePorts) {
try {
await request.post(`/olt/duplicate-macs/${row.id}/clear-port`, { port_id: p.port_id })
cleared++
} catch {
failed++
}
}
try {
await request.delete(`/olt/duplicate-macs/${row.id}`)
} catch {}
}
})
)
dupList.value = []
duplicateCount.value = 0
batchClearing.value = false
ElMessage.success(`批量清除完成:清除 ${cleared} 个端口${failed ? `${failed} 个失败` : ''},已忽略全部 ${total} 条记录`)
}
const fetchNewDeviceCount = async () => {
try {
const { data } = await request.get('/olt/new-devices')