fix(ipam): 修复IP管理页面网段筛选不生效及下拉框过窄

- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄
- Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致)
- Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效
- enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
This commit is contained in:
Your Name
2026-07-18 09:45:09 +08:00
commit 1e1e5957e8
103 changed files with 13196 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
<template>
<el-container class="layout-container">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '220px'" class="sidebar">
<div class="logo">
<el-icon :size="32" color="#409eff">
<Monitor />
</el-icon>
<span v-if="!isCollapse" class="logo-text">IPAM管理系统</span>
</div>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:collapse-transition="false"
router
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409eff"
class="sidebar-menu"
>
<el-menu-item index="/dashboard">
<el-icon><Odometer /></el-icon>
<template #title>仪表盘</template>
</el-menu-item>
<el-menu-item index="/networks">
<el-icon><Connection /></el-icon>
<template #title>网段管理</template>
</el-menu-item>
<el-menu-item index="/ips">
<el-icon><Cpu /></el-icon>
<template #title>IP地址</template>
</el-menu-item>
<el-menu-item index="/scan">
<el-icon><Search /></el-icon>
<template #title>扫描管理</template>
</el-menu-item>
<el-menu-item index="/snmp">
<el-icon><Setting /></el-icon>
<template #title>SNMP管理</template>
</el-menu-item>
<el-menu-item index="/alerts">
<el-icon><WarningFilled /></el-icon>
<template #title>告警中心</template>
<el-badge v-if="alertCount > 0" :value="alertCount" class="menu-badge" />
</el-menu-item>
<el-menu-item v-if="isAdmin" index="/users">
<el-icon><UserFilled /></el-icon>
<template #title>用户管理</template>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container class="main-container">
<!-- 顶部导航 -->
<el-header class="header">
<div class="header-left">
<el-icon class="collapse-btn" @click="toggleCollapse">
<Fold v-if="!isCollapse" />
<Expand v-else />
</el-icon>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/dashboard' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ currentPageTitle }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<el-tooltip content="刷新数据" placement="bottom">
<el-icon class="header-icon" @click="refreshData">
<RefreshRight />
</el-icon>
</el-tooltip>
<el-dropdown trigger="click" @command="onUserMenuCommand">
<span class="user-info">
<el-icon :size="18"><User /></el-icon>
<span>{{ displayName }}</span>
<el-tag v-if="userInfo" size="small" :type="roleTagType" effect="plain" class="role-tag">
{{ roleLabel }}
</el-tag>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><UserFilled /></el-icon> 个人中心
</el-dropdown-item>
<el-dropdown-item v-if="isAdmin" command="users" divided>
<el-icon><Setting /></el-icon> 用户管理
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<el-icon><SwitchButton /></el-icon> 退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 内容区 -->
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { alertApi } from '@/api'
import { authStore } from '@/utils/auth'
import { logout as doLogout } from '@/api/auth'
const router = useRouter()
const route = useRoute()
const isCollapse = ref(false)
const alertCount = ref(0)
const userInfo = ref(authStore.getUserInfo())
const isAdmin = computed(() => authStore.hasRole('super_admin', 'admin'))
const displayName = computed(() => {
const u = userInfo.value
if (!u) return '未登录'
return u.real_name || u.username || '用户'
})
const roleLabel = computed(() => {
const map = {
super_admin: '超级管理员',
admin: '管理员',
operator: '操作员',
viewer: '访客',
}
return map[userInfo.value?.role] || '用户'
})
const roleTagType = computed(() => {
const map = {
super_admin: 'danger',
admin: 'warning',
operator: 'success',
viewer: 'info',
}
return map[userInfo.value?.role] || ''
})
const activeMenu = computed(() => route.path)
const currentPageTitle = computed(() => {
return route.meta.title || 'IPAM管理系统'
})
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
const refreshData = () => {
router.go(0)
}
async function onUserMenuCommand(cmd) {
if (cmd === 'profile') {
router.push('/profile')
} else if (cmd === 'users') {
router.push('/users')
} else if (cmd === 'logout') {
try {
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '退出',
cancelButtonText: '取消',
type: 'warning',
})
} catch (_) {
return
}
doLogout()
ElMessage.success('已退出登录')
router.push('/login')
}
}
const loadAlertCount = async () => {
try {
const stats = await alertApi.getStats()
alertCount.value = stats.by_status?.active || 0
} catch (error) {
// 401 已在 axios 拦截器处理,这里静默
console.error('加载告警数量失败:', error)
}
}
onMounted(() => {
loadAlertCount()
})
</script>
<style scoped>
.layout-container {
height: 100vh;
background-color: #f0f2f5;
}
.sidebar {
background-color: #304156;
transition: width 0.3s;
}
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 0 16px;
border-bottom: 1px solid #1f2d3d;
}
.logo-text {
color: #fff;
font-size: 16px;
font-weight: 600;
white-space: nowrap;
}
.sidebar-menu {
border-right: none;
}
.menu-badge {
margin-left: 8px;
}
.main-container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.header {
background-color: #fff;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 60px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.collapse-btn {
font-size: 20px;
cursor: pointer;
transition: color 0.3s;
}
.collapse-btn:hover {
color: #409eff;
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
}
.header-icon {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
}
.header-icon:hover {
color: #409eff;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.role-tag {
margin-left: 4px;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
background-color: #f0f2f5;
}
</style>