初始化
This commit is contained in:
@@ -0,0 +1,644 @@
|
||||
<template>
|
||||
<div class="olt-manage">
|
||||
<el-button type="primary" @click="openAddDialog">添加 OLT 设备</el-button>
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleImportFile"
|
||||
accept=".xlsx,.xls"
|
||||
style="display: inline-block; margin-left: 10px"
|
||||
>
|
||||
<el-button type="success" :loading="importing">批量导入</el-button>
|
||||
</el-upload>
|
||||
<el-button @click="downloadTemplate">下载模板</el-button>
|
||||
<el-button type="danger" plain @click="openDuplicateMacs" style="margin-left: 10px">
|
||||
重复 MAC 地址
|
||||
<el-badge v-if="duplicateCount > 0" :value="duplicateCount" style="margin-left: 4px" />
|
||||
</el-button>
|
||||
<el-button type="warning" plain @click="openNewDevices" style="margin-left: 10px">
|
||||
新增设备
|
||||
<el-badge v-if="newDeviceCount > 0" :value="newDeviceCount" style="margin-left: 4px" />
|
||||
</el-button>
|
||||
<el-button type="danger" plain @click="runLoopbackDetection" :loading="loopDetecting" style="margin-left: 10px">
|
||||
环路检测
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="runQuickScan" :loading="quickScanning" style="margin-left: 10px">
|
||||
快速扫描
|
||||
</el-button>
|
||||
|
||||
<el-table :data="devices" style="margin-top: 20px">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="ip_address" label="IP 地址" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openDetail(row)">{{ row.ip_address }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="location" label="安装位置" width="150" />
|
||||
<el-table-column prop="slot_command" label="槽位命令" width="150" />
|
||||
<el-table-column prop="description" label="描述" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="环路状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="loopStatusMap[row.id]"
|
||||
type="danger"
|
||||
size="small"
|
||||
style="cursor: pointer"
|
||||
@click="showLoopDetail(row.id)"
|
||||
>
|
||||
环路 {{ loopStatusMap[row.id].length }} 个
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 快速扫描结果对话框 -->
|
||||
<el-dialog v-model="quickScanVisible" title="快速扫描结果" width="700px" destroy-on-close>
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 16px">
|
||||
<el-statistic title="在线设备" :value="quickScanResult.total_online || 0" />
|
||||
<el-statistic title="离线设备" :value="quickScanResult.total_offline || 0" />
|
||||
<el-statistic title="新入库设备" :value="quickScanResult.total_new || 0" />
|
||||
<el-statistic title="失败 OLT" :value="(quickScanResult.errors || []).length" />
|
||||
</div>
|
||||
<el-table :data="quickScanResult.results || []" border size="small" max-height="360">
|
||||
<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 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="离线" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="danger" size="small">{{ row.offline }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="新入库" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.new_discovered > 0" type="warning" size="small">{{ row.new_discovered }}</el-tag>
|
||||
<span v-else style="color: #c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" 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="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.error || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="quickScanVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 环路检测结果对话框 -->
|
||||
<el-dialog v-model="loopVisible" title="环路检测结果" width="860px" destroy-on-close>
|
||||
<div v-for="item in loopResults" :key="item.olt_id" style="margin-bottom: 16px">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px">
|
||||
<span style="font-weight: 600">{{ item.olt_location }}</span>
|
||||
<el-tag v-if="item.error" type="warning" size="small">连接失败</el-tag>
|
||||
<el-tag v-else-if="item.has_loop" type="danger" size="small">检测到环路 {{ item.loop_interfaces.length }} 个</el-tag>
|
||||
<el-tag v-else type="success" size="small">正常</el-tag>
|
||||
<span v-if="item.error" style="color: #e6a23c; font-size: 12px">{{ item.error }}</span>
|
||||
</div>
|
||||
<el-table v-if="item.has_loop" :data="item.loop_interfaces" border size="small">
|
||||
<el-table-column prop="interface" label="端口" width="140" />
|
||||
<el-table-column prop="mac_address" label="MAC 地址" width="160">
|
||||
<template #default="{ row }">{{ row.mac_address || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="region" label="区域" width="100">
|
||||
<template #default="{ row }">{{ row.region || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="school_name" label="学校" min-width="140">
|
||||
<template #default="{ row }">{{ row.school_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="building" label="楼宇" width="100">
|
||||
<template #default="{ row }">{{ row.building || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="room_number" label="房间号" width="90">
|
||||
<template #default="{ row }">{{ row.room_number || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="loopVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 单台 OLT 环路详情对话框 -->
|
||||
<el-dialog v-model="loopDetailVisible" title="环路详情" width="700px" destroy-on-close>
|
||||
<el-table :data="loopDetailInterfaces" border size="small">
|
||||
<el-table-column prop="interface" label="端口" width="140" />
|
||||
<el-table-column prop="mac_address" label="MAC 地址" width="160">
|
||||
<template #default="{ row }">{{ row.mac_address || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="region" label="区域" width="100">
|
||||
<template #default="{ row }">{{ row.region || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="school_name" label="学校" min-width="140">
|
||||
<template #default="{ row }">{{ row.school_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="building" label="楼宇" width="100">
|
||||
<template #default="{ row }">{{ row.building || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="room_number" label="房间号" width="90">
|
||||
<template #default="{ row }">{{ row.room_number || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="loopDetailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑 OLT 设备' : '添加 OLT 设备'" width="500px" destroy-on-close>
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="IP 地址">
|
||||
<el-input v-model="form.ip_address" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.password" type="password" placeholder="不修改请留空" />
|
||||
</el-form-item>
|
||||
<el-form-item label="安装位置">
|
||||
<el-input v-model="form.location" />
|
||||
</el-form-item>
|
||||
<el-form-item label="槽位命令">
|
||||
<el-input v-model="form.slot_command" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveDevice">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailVisible" title="OLT 设备详情" width="500px" destroy-on-close>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="IP 地址" :span="2">{{ selectedDevice.ip_address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">{{ selectedDevice.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="安装位置">{{ selectedDevice.location || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="槽位命令">{{ selectedDevice.slot_command || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">{{ selectedDevice.description || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button type="success" :loading="scanning" @click="scanOlt">扫描发现设备</el-button>
|
||||
<el-button type="warning" :loading="discovering" @click="discoverOlt">扫描并入库</el-button>
|
||||
<el-button type="primary" @click="openEdit">编辑</el-button>
|
||||
<el-button type="danger" @click="confirmDelete">删除</el-button>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 扫描结果对话框 -->
|
||||
<el-dialog v-model="scanVisible" title="扫描结果" width="800px" destroy-on-close>
|
||||
<div style="margin-bottom: 12px">
|
||||
<el-tag>共发现 {{ scanResult.total }} 台</el-tag>
|
||||
<el-tag type="warning" style="margin-left: 8px">新设备 {{ scanResult.new }} 台</el-tag>
|
||||
<el-tag v-if="scanResult.duplicates?.length" type="danger" style="margin-left: 8px">
|
||||
重复 MAC {{ scanResult.duplicates.length }} 个
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-table :data="scanResult.devices" max-height="400">
|
||||
<el-table-column prop="mac_address" label="MAC 地址" width="160" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'online' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 'online' ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="distance_m" label="距离" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ row.distance_m || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="端口" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.port_id || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="loid" label="LOID" width="80" />
|
||||
<el-table-column prop="model" label="型号" />
|
||||
<el-table-column label="是否新设备" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_new" type="warning" size="small">新发现</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button type="warning" :loading="discovering" @click="discoverOlt">确认入库新设备</el-button>
|
||||
<el-button @click="scanVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重复 MAC 地址对话框 -->
|
||||
<el-dialog v-model="dupVisible" title="重复 MAC 地址" width="800px" destroy-on-close>
|
||||
<div style="margin-bottom: 12px; color: #909399; font-size: 13px">
|
||||
以下 MAC 地址在 OLT 上出现在多个端口,可能是设备更换端口后旧记录未清除。对离线的旧端口执行"清除"可恢复其默认配置。
|
||||
</div>
|
||||
<el-table :data="dupList" max-height="450" empty-text="暂无重复 MAC 记录">
|
||||
<el-table-column prop="mac_address" label="MAC 地址" width="160" />
|
||||
<el-table-column label="出现的端口" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<div v-for="p in row.ports" :key="p.port_id" style="display: inline-flex; align-items: center; margin-right: 8px; margin-bottom: 4px">
|
||||
<el-tag
|
||||
:type="p.status === 'online' ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ p.port_id }} ({{ p.status === 'online' ? '在线' : '离线' }})
|
||||
</el-tag>
|
||||
<el-button
|
||||
v-if="p.status !== 'online'"
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
:loading="clearingPort === `${row.id}-${p.port_id}`"
|
||||
style="margin-left: 2px; padding: 0 4px"
|
||||
@click="clearPort(row, p.port_id)"
|
||||
>清除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后发现" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.last_seen_at ? new Date(row.last_seen_at).toLocaleString() : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small" text @click="deleteDupRecord(row.id)">忽略</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="dupVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增设备对话框 -->
|
||||
<el-dialog v-model="newDevVisible" title="新增设备" width="900px" destroy-on-close>
|
||||
<div style="margin-bottom: 12px; color: #909399; font-size: 13px">
|
||||
以下设备是 OLT 扫描新发现的,请补全区域和学校信息后确认入库。
|
||||
</div>
|
||||
<el-table :data="newDevList" max-height="450" empty-text="暂无新增设备">
|
||||
<el-table-column prop="mac_address" label="MAC 地址" width="160" />
|
||||
<el-table-column prop="port_id" label="端口" width="100" />
|
||||
<el-table-column prop="model" label="型号" width="160" />
|
||||
<el-table-column label="区域 *" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.region" size="small" placeholder="必填" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="学校 *" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.school_name" size="small" placeholder="必填" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="楼宇" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.building" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="房间号" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.room_number" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" text :loading="savingDev === row.id" @click="saveNewDevice(row)">保存</el-button>
|
||||
<el-button type="info" size="small" text @click="dismissNewDevice(row.id)">忽略</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="newDevVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
|
||||
const devices = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const scanVisible = ref(false)
|
||||
const dupVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const selectedDevice = ref({})
|
||||
const scanning = ref(false)
|
||||
const discovering = ref(false)
|
||||
const importing = ref(false)
|
||||
const scanResult = ref({ total: 0, new: 0, devices: [], duplicates: [] })
|
||||
const dupList = ref([])
|
||||
const duplicateCount = ref(0)
|
||||
const clearingPort = ref('')
|
||||
const newDevList = ref([])
|
||||
const newDeviceCount = ref(0)
|
||||
const newDevVisible = ref(false)
|
||||
const savingDev = ref(null)
|
||||
const loopDetecting = ref(false)
|
||||
const loopVisible = ref(false)
|
||||
const loopResults = ref([])
|
||||
const loopStatusMap = ref({}) // olt_id -> loop_interfaces[]
|
||||
const loopDetailVisible = ref(false)
|
||||
const loopDetailInterfaces = ref([])
|
||||
const quickScanning = ref(false)
|
||||
const quickScanVisible = ref(false)
|
||||
const quickScanResult = ref({ total_online: 0, total_offline: 0, total_new: 0, results: [], errors: [] })
|
||||
const form = ref({ ip_address: '', username: '', password: '', slot_command: 'display onu slot', location: '', description: '' })
|
||||
|
||||
const fetchDevices = async () => {
|
||||
const { data } = await request.get('/olt/devices')
|
||||
devices.value = data
|
||||
}
|
||||
|
||||
const fetchDuplicateCount = async () => {
|
||||
try {
|
||||
const { data } = await request.get('/olt/duplicate-macs')
|
||||
duplicateCount.value = data.length
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const openAddDialog = () => {
|
||||
isEdit.value = false
|
||||
form.value = { ip_address: '', username: '', password: '', slot_command: 'display onu slot', location: '', description: '' }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openDetail = (row) => {
|
||||
selectedDevice.value = { ...row }
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const openEdit = () => {
|
||||
detailVisible.value = false
|
||||
isEdit.value = true
|
||||
form.value = {
|
||||
ip_address: selectedDevice.value.ip_address,
|
||||
username: selectedDevice.value.username,
|
||||
password: '',
|
||||
slot_command: selectedDevice.value.slot_command || 'display onu slot',
|
||||
location: selectedDevice.value.location || '',
|
||||
description: selectedDevice.value.description || ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const saveDevice = async () => {
|
||||
const payload = { ...form.value }
|
||||
if (!payload.password) {
|
||||
delete payload.password
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await request.put(`/olt/devices/${form.value.ip_address}`, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await request.post('/olt/devices', payload)
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除此 OLT 设备吗?', '确认删除', { type: 'warning' })
|
||||
await request.delete(`/olt/devices/${selectedDevice.value.ip_address}`)
|
||||
ElMessage.success('删除成功')
|
||||
detailVisible.value = false
|
||||
fetchDevices()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.response?.data?.detail || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scanOlt = async () => {
|
||||
scanning.value = true
|
||||
try {
|
||||
const { data } = await request.post(`/check/scan/${selectedDevice.value.id}`)
|
||||
scanResult.value = data
|
||||
detailVisible.value = false
|
||||
scanVisible.value = true
|
||||
fetchDuplicateCount()
|
||||
fetchNewDeviceCount()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '扫描失败')
|
||||
} finally {
|
||||
scanning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const discoverOlt = async () => {
|
||||
discovering.value = true
|
||||
try {
|
||||
const { data } = await request.post(`/check/discover/${selectedDevice.value.id}`)
|
||||
const dupMsg = data.duplicate_macs > 0 ? `,重复 MAC ${data.duplicate_macs} 个` : ''
|
||||
ElMessage.success(`扫描完成:在线 ${data.online},离线 ${data.offline},新入库 ${data.new_discovered} 台${dupMsg}`)
|
||||
scanVisible.value = false
|
||||
detailVisible.value = false
|
||||
fetchDuplicateCount()
|
||||
fetchNewDeviceCount()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '入库失败')
|
||||
} finally {
|
||||
discovering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openDuplicateMacs = async () => {
|
||||
try {
|
||||
const { data } = await request.get('/olt/duplicate-macs')
|
||||
dupList.value = data
|
||||
duplicateCount.value = data.length
|
||||
dupVisible.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error('获取重复 MAC 失败')
|
||||
}
|
||||
}
|
||||
|
||||
const clearPort = async (row, portId) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要清除端口 Onu${portId} 的配置吗?此操作将通过 SSH 执行 default 命令,不可撤销。`,
|
||||
'确认清除',
|
||||
{ type: 'warning', confirmButtonText: '确认清除', confirmButtonClass: 'el-button--danger' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
clearingPort.value = `${row.id}-${portId}`
|
||||
try {
|
||||
const { data } = await request.post(`/olt/duplicate-macs/${row.id}/clear-port`, { port_id: portId })
|
||||
ElMessage.success(data.message)
|
||||
if (data.remaining_ports.length === 0) {
|
||||
dupList.value = dupList.value.filter(r => r.id !== row.id)
|
||||
} else {
|
||||
row.ports = data.remaining_ports
|
||||
}
|
||||
duplicateCount.value = dupList.value.length
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '清除失败')
|
||||
} finally {
|
||||
clearingPort.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const deleteDupRecord = async (id) => {
|
||||
try {
|
||||
await request.delete(`/olt/duplicate-macs/${id}`)
|
||||
dupList.value = dupList.value.filter(r => r.id !== id)
|
||||
duplicateCount.value = dupList.value.length
|
||||
ElMessage.success('已清除')
|
||||
} catch (error) {
|
||||
ElMessage.error('清除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNewDeviceCount = async () => {
|
||||
try {
|
||||
const { data } = await request.get('/olt/new-devices')
|
||||
newDeviceCount.value = data.length
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const openNewDevices = async () => {
|
||||
try {
|
||||
const { data } = await request.get('/olt/new-devices')
|
||||
newDevList.value = data.map(d => ({ ...d, region: d.region || '', school_name: d.school_name || '', building: d.building || '', room_number: d.room_number || '' }))
|
||||
newDeviceCount.value = data.length
|
||||
newDevVisible.value = true
|
||||
} catch {
|
||||
ElMessage.error('获取新增设备失败')
|
||||
}
|
||||
}
|
||||
|
||||
const saveNewDevice = async (row) => {
|
||||
if (!row.region || !row.school_name) {
|
||||
ElMessage.warning('区域和学校为必填项')
|
||||
return
|
||||
}
|
||||
savingDev.value = row.id
|
||||
try {
|
||||
await request.put(`/olt/new-devices/${row.id}`, {
|
||||
region: row.region,
|
||||
school_name: row.school_name,
|
||||
building: row.building,
|
||||
room_number: row.room_number,
|
||||
})
|
||||
ElMessage.success('已保存')
|
||||
newDevList.value = newDevList.value.filter(d => d.id !== row.id)
|
||||
newDeviceCount.value = newDevList.value.length
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '保存失败')
|
||||
} finally {
|
||||
savingDev.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const dismissNewDevice = async (id) => {
|
||||
try {
|
||||
await request.delete(`/olt/new-devices/${id}`)
|
||||
newDevList.value = newDevList.value.filter(d => d.id !== id)
|
||||
newDeviceCount.value = newDevList.value.length
|
||||
ElMessage.success('已忽略')
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const runQuickScan = async () => {
|
||||
quickScanning.value = true
|
||||
try {
|
||||
const { data } = await request.post('/olt/quick-scan')
|
||||
quickScanResult.value = data
|
||||
quickScanVisible.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '快速扫描失败')
|
||||
} finally {
|
||||
quickScanning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const runLoopbackDetection = async () => {
|
||||
loopDetecting.value = true
|
||||
try {
|
||||
const { data } = await request.post('/olt/loopback-detection')
|
||||
loopResults.value = data
|
||||
// 更新列表中的环路标志
|
||||
const map = {}
|
||||
for (const item of data) {
|
||||
if (item.has_loop && item.loop_interfaces.length > 0) {
|
||||
map[item.olt_id] = item.loop_interfaces
|
||||
}
|
||||
}
|
||||
loopStatusMap.value = map
|
||||
loopVisible.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '环路检测失败')
|
||||
} finally {
|
||||
loopDetecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showLoopDetail = (oltId) => {
|
||||
loopDetailInterfaces.value = loopStatusMap.value[oltId] || []
|
||||
loopDetailVisible.value = true
|
||||
}
|
||||
|
||||
const handleImportFile = async (uploadFile) => {
|
||||
importing.value = true
|
||||
const formData = new FormData()
|
||||
formData.append('file', uploadFile.raw)
|
||||
try {
|
||||
const { data } = await request.post('/olt/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
const failMsg = data.failed?.length ? `,${data.failed.length} 条失败` : ''
|
||||
ElMessage.success(`成功导入 ${data.success} 条${failMsg}`)
|
||||
fetchDevices()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '导入失败')
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleImportSuccess = () => {
|
||||
ElMessage.success('导入成功')
|
||||
fetchDevices()
|
||||
}
|
||||
|
||||
const downloadTemplate = () => {
|
||||
window.open('/api/olt/template', '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDevices()
|
||||
fetchDuplicateCount()
|
||||
fetchNewDeviceCount()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.olt-manage {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user