初始化
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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-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>
|
||||
|
||||
<div v-else-if="checkResult">
|
||||
<el-alert
|
||||
:type="checkResult.success ? 'success' : 'error'"
|
||||
:title="checkResult.success ? '更新完成' : '更新失败'"
|
||||
:description="checkResult.error || ''"
|
||||
: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>
|
||||
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success" size="small">{{ row.online || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="离线" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="danger" size="small">{{ row.offline || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.success ? 'success' : 'danger'" size="small">
|
||||
{{ row.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="checkResultVisible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="checkResultVisible = false; loadDevices()" v-if="!checkInProgress">
|
||||
刷新列表
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 设备详情对话框 -->
|
||||
<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>
|
||||
<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="状态">
|
||||
<el-tag :type="selectedDevice.status === 'online' ? 'success' : selectedDevice.status === 'offline' ? 'danger' : 'info'" size="small">
|
||||
{{ selectedDevice.status === 'online' ? '在线' : selectedDevice.status === 'offline' ? '离线' : '未知' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="距离">
|
||||
{{ selectedDevice.distance_m ? selectedDevice.distance_m + ' 米' : '-' }}
|
||||
</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 || '-' }}
|
||||
</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>
|
||||
<el-button type="success" @click="openProvision" :disabled="selectedDevice.status !== 'online' || !selectedDevice.port_id">
|
||||
业务下发
|
||||
</el-button>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</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-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editSaving" @click="saveEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 业务下发对话框 -->
|
||||
<el-dialog v-model="provisionVisible" title="业务下发" width="560px" destroy-on-close>
|
||||
<el-alert
|
||||
v-if="provisionResult"
|
||||
:type="provisionResult.success ? 'success' : 'error'"
|
||||
:title="provisionResult.success ? '下发成功' : '下发失败'"
|
||||
:description="provisionResult.message"
|
||||
:closable="false"
|
||||
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="状态">
|
||||
<el-tag :type="selectedDevice.status === 'online' ? 'success' : selectedDevice.status === 'offline' ? 'danger' : 'info'" size="small">
|
||||
{{ selectedDevice.status === 'online' ? '在线' : selectedDevice.status === 'offline' ? '离线' : '未知' }}
|
||||
</el-tag>
|
||||
</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
|
||||
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>
|
||||
<template #footer>
|
||||
<el-button @click="provisionVisible = false; provisionResult = null">关闭</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="doProvision"
|
||||
:loading="provisionLoading"
|
||||
:disabled="selectedDevice.status !== 'online' || !selectedDevice.port_id"
|
||||
>
|
||||
确认下发
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as deviceApi from '../api/device'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const devices = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
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 {
|
||||
await deviceApi.clearAllStatus()
|
||||
ElMessage.success('已清空所有设备状态')
|
||||
loadDevices()
|
||||
} catch {
|
||||
ElMessage.error('清空失败')
|
||||
} finally {
|
||||
clearing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新所有设备状态
|
||||
const refreshAllStatus = async () => {
|
||||
refreshing.value = true
|
||||
checkResultVisible.value = true
|
||||
checkInProgress.value = true
|
||||
checkResult.value = null
|
||||
|
||||
try {
|
||||
const { data } = await deviceApi.refreshAllStatus()
|
||||
checkResult.value = {
|
||||
success: true,
|
||||
total_online: data.total_online,
|
||||
total_offline: data.total_offline,
|
||||
results: data.results,
|
||||
errors: data.errors,
|
||||
}
|
||||
} catch (error) {
|
||||
checkResult.value = { success: false, error: error.response?.data?.detail || '更新失败' }
|
||||
} finally {
|
||||
checkInProgress.value = false
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化 MAC 地址
|
||||
const formatMac = (mac) => {
|
||||
if (!mac) return ''
|
||||
const clean = mac.toUpperCase().replace(/[:-]/g, '')
|
||||
if (clean.length !== 12) return mac.toLowerCase()
|
||||
return clean.substring(0, 4).toLowerCase() + '-' +
|
||||
clean.substring(4, 8).toLowerCase() + '-' +
|
||||
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 '-'
|
||||
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
|
||||
ElMessage.success(`更新完成:${data.status === 'online' ? '在线' : '离线'}${data.distance_m ? ',距离 ' + data.distance_m + 'm' : ''}`)
|
||||
loadDevices()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '更新失败')
|
||||
} finally {
|
||||
deviceRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑对话框
|
||||
const openEdit = () => {
|
||||
editForm.value = {
|
||||
region: selectedDevice.value.region || '',
|
||||
school_name: selectedDevice.value.school_name || '',
|
||||
building: selectedDevice.value.building || '',
|
||||
room_number: selectedDevice.value.room_number || '',
|
||||
notes: selectedDevice.value.notes || '',
|
||||
}
|
||||
detailVisible.value = false
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
const saveEdit = async () => {
|
||||
editSaving.value = true
|
||||
try {
|
||||
await deviceApi.updateDevice(selectedDevice.value.id, editForm.value)
|
||||
ElMessage.success('保存成功')
|
||||
editVisible.value = false
|
||||
loadDevices()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.detail || '保存失败')
|
||||
} finally {
|
||||
editSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开业务下发对话框
|
||||
const openProvision = () => {
|
||||
provisionResult.value = null
|
||||
provisionVisible.value = true
|
||||
}
|
||||
|
||||
// 执行业务下发
|
||||
const doProvision = async () => {
|
||||
provisionLoading.value = true
|
||||
try {
|
||||
const { data } = await deviceApi.provisionService({ device_id: selectedDevice.value.id })
|
||||
provisionResult.value = { success: true, message: data.message }
|
||||
ElMessage.success(data.message)
|
||||
} catch (error) {
|
||||
const msg = error.response?.data?.detail || '下发失败'
|
||||
provisionResult.value = { success: false, message: msg }
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
provisionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载区域列表
|
||||
const loadRegions = async () => {
|
||||
const { data } = await deviceApi.getRegions()
|
||||
regions.value = data
|
||||
}
|
||||
|
||||
const loadDevices = async () => {
|
||||
const { data } = await deviceApi.getDevices({
|
||||
skip: (page.value - 1) * pageSize.value,
|
||||
limit: pageSize.value,
|
||||
region: filters.value.region || undefined,
|
||||
keyword: filters.value.keyword || undefined,
|
||||
school_name: filters.value.school_name || undefined,
|
||||
status: filters.value.status || undefined,
|
||||
})
|
||||
devices.value = data.items
|
||||
total.value = data.total
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
page.value = 1
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
page.value = 1
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.school_name) {
|
||||
filters.value.keyword = route.query.school_name
|
||||
}
|
||||
loadRegions()
|
||||
loadDevices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user