feat: 全面优化 — UI/UX重构 + 流量审计与会话管理 + 协议部署

UI/UX 优化:
- 登录页分屏布局 + 可访问性修复 + 内联错误提示
- 侧栏菜单分组(专线运维/资源管理/系统) + 图标修正 + 子路由高亮
- Dashboard 统计卡渐变图标 + 空状态引导 + 统计卡可点击
- 创建向导: 依赖检测门控 + 高级折叠 + 必填校验 + WireGuard推荐标
- 专线列表: 空状态CTA + 操作列「更多」下拉 + 状态筛选 + SSE日志流
- 协议品牌SVG图标(ProtocolIcon组件)替换emoji
- 设计令牌系统(tokens.css)统一颜色/间距/圆角/阴影

功能增强:
- 在线会话管理(sessionmgr): 跨协议会话同步 + 踢下线 + 连接审计日志
- 流量配额引擎(trafficmgr): 配额检查 + 超额动作(block/warn/throttle) + 周期重置
- 异常流量告警: 突发流量检测(>90MB/15s) + 审计记录
- 节点SSH凭证管理: 密码/私钥认证 + 即时测试连接 + AES-GCM加密存储
- 协议二进制管理(binmgr): FRP/OpenVPN GitHub下载 + 系统包安装引导(apt/yum/dnf)
- 右上角修改密码弹窗 + MFA独立验证页 + 密码强度提示
- 用户管理: 批量导入 + LDAP浏览导入 + 平台用户导入VPN拨号用户
- 404路由 + IPSec对接页可达性修复
- MFA备用码持久化弹窗(替代Toast)
- 轮询可见性守卫 + ECharts resize修复

后端:
- 3张新表: VPNSession/ConnectionLog/TrafficPolicy
- monitor增强: 会话同步+配额检查+异常告警+到期审计
- changePassword统一8位+新旧不同校验
- openvpn-otp文案专业化(移除抄袭感)
- SSE日志流(token降级query参数)
This commit is contained in:
Logan.Li committed 2026-07-23 01:42:36 +08:00
1 parent 58c036c34c
commit 1e2f8ef24f
43 files changed
+6783 -460

No files matched your search

+110 -27
View File
@@ -2,28 +2,37 @@
<el-container class="layout">
<el-aside width="220px" class="aside">
<div class="logo">
<span class="logo-icon">🔐</span>
<el-icon :size="22" color="#7dd3fc"><Lock /></el-icon>
<span class="logo-text">VPN 管理平台</span>
</div>
<el-menu :default-active="$route.path" router background-color="#1a2a4a" text-color="#a8b8d8"
<el-menu :default-active="activeMenu" router :background-color="sideBg" text-color="#a8b8d8"
active-text-color="#ffffff" class="menu">
<el-menu-item index="/dashboard"><el-icon><DataAnalysis /></el-icon>总览</el-menu-item>
<el-menu-item index="/tunnels"><el-icon><Connection /></el-icon>专线列表</el-menu-item>
<el-menu-item index="/tunnels/create"><el-icon><CirclePlus /></el-icon>创建专线</el-menu-item>
<el-menu-item index="/ipsec-peer"><el-icon><Link /></el-icon>IPSec 对接</el-menu-item>
<el-menu-item index="/nodes"><el-icon><Share /></el-icon>节点管理</el-menu-item>
<el-menu-item index="/users"><el-icon><UserFilled /></el-icon>用户与 LDAP</el-menu-item>
<el-menu-item index="/vpn-users"><el-icon><User /></el-icon>VPN 拨号用户</el-menu-item>
<el-menu-item index="/plugins"><el-icon><Grid /></el-icon>协议插件</el-menu-item>
<el-menu-item index="/settings"><el-icon><Setting /></el-icon>系统设置</el-menu-item>
<el-menu-item-group title="专线运维">
<el-menu-item index="/dashboard"><el-icon><DataAnalysis /></el-icon>总览</el-menu-item>
<el-menu-item index="/tunnels"><el-icon><Connection /></el-icon>专线列表</el-menu-item>
<el-menu-item index="/sessions"><el-icon><Monitor /></el-icon>在线会话</el-menu-item>
</el-menu-item-group>
<el-menu-item-group v-if="auth.isAdmin()" title="资源管理">
<el-menu-item index="/nodes"><el-icon><Coin /></el-icon>节点管理</el-menu-item>
<el-menu-item index="/vpn-users"><el-icon><Iphone /></el-icon>VPN 拨号用户</el-menu-item>
<el-menu-item index="/plugins"><el-icon><Grid /></el-icon>协议插件</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="系统">
<el-menu-item v-if="auth.isAdmin()" index="/users"><el-icon><UserFilled /></el-icon>用户管理</el-menu-item>
<el-menu-item index="/settings"><el-icon><Setting /></el-icon>系统设置</el-menu-item>
</el-menu-item-group>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<span class="page-title">{{ $route.meta.title }}</span>
<el-dropdown @command="onCommand">
<span class="user">
<el-icon><Avatar /></el-icon> {{ auth.username }} <el-icon><ArrowDown /></el-icon>
<span class="user" role="button" aria-label="账户菜单" tabindex="0">
<el-icon><Avatar /></el-icon> {{ auth.username }}
<el-tag size="small" style="margin-left:6px" :type="roleTagType">
{{ roleLabel }}
</el-tag>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
@@ -37,32 +46,106 @@
<router-view />
</el-main>
</el-container>
<!-- 右上角修改密码弹窗(不必跳转整页) -->
<el-dialog v-model="pwdDialog" title="修改密码" width="420px" align-center>
<el-alert v-if="auth.mustChange" type="warning" :closable="false" show-icon
title="首次登录或密码已被重置,请先修改密码" style="margin-bottom:16px" />
<el-form label-width="90px" @submit.prevent="changePwd">
<el-form-item label="原密码">
<el-input v-model="pwd.old_password" type="password" show-password autocomplete="current-password" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwd.new_password" type="password" show-password autocomplete="new-password"
placeholder="至少 8 位" />
<div class="pwd-hint" v-if="pwd.new_password">强度:{{ pwdStrength }}</div>
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="pwd.confirm" type="password" show-password autocomplete="new-password"
@keyup.enter="changePwd" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="pwdDialog = false">取消</el-button>
<el-button type="primary" :loading="pwdLoading" @click="changePwd">确认修改</el-button>
</template>
</el-dialog>
</el-container>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { DataAnalysis, Connection, CirclePlus, Link, Share, Grid, Setting, Avatar, ArrowDown, UserFilled, User } from '@element-plus/icons-vue'
import { auth } from '../api'
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { DataAnalysis, Connection, Coin, Grid, Setting, Avatar, ArrowDown, UserFilled, Iphone, Lock, Monitor } from '@element-plus/icons-vue'
import api, { auth } from '../api'
const sideBg = 'var(--bg-sidebar)'
const router = useRouter()
const route = useRoute()
const roleLabel = computed(() => ({ admin: '管理员', operator: '操作员', viewer: '只读' }[auth.role] || auth.role))
const roleTagType = computed(() => auth.role === 'admin' ? 'danger' : auth.role === 'operator' ? 'warning' : 'info')
// 子路由(创建/编辑专线)归一到 /tunnels 保持高亮
const activeMenu = computed(() => {
if (route.path.startsWith('/tunnels')) return '/tunnels'
return route.path
})
// ---- 右上角修改密码弹窗 ----
const pwdDialog = ref(false)
const pwdLoading = ref(false)
const pwd = ref({ old_password: '', new_password: '', confirm: '' })
const pwdStrength = computed(() => {
const p = pwd.value.new_password
if (!p) return ''
let score = 0
if (p.length >= 8) score++
if (p.length >= 12) score++
if (/[A-Z]/.test(p)) score++
if (/[0-9]/.test(p)) score++
if (/[^A-Za-z0-9]/.test(p)) score++
return ['很弱', '弱', '一般', '较强', '强'][Math.min(score - 1, 4)] || '很弱'
})
function onCommand(cmd) {
if (cmd === 'logout') { auth.logout(); router.push('/login') }
if (cmd === 'password') router.push('/settings')
if (cmd === 'password') { pwd.value = { old_password: '', new_password: '', confirm: '' }; pwdDialog.value = true }
}
async function changePwd() {
if (pwd.value.new_password !== pwd.value.confirm) return ElMessage.warning('两次新密码不一致')
if (pwd.value.new_password.length < 8) return ElMessage.warning('新密码至少 8 位')
pwdLoading.value = true
try {
await api.post('/password', { old_password: pwd.value.old_password, new_password: pwd.value.new_password })
auth.clearMustChange()
ElMessage.success('密码已修改')
pwdDialog.value = false
} catch (e) {
ElMessage.error(e.response?.data?.error || '修改失败')
} finally { pwdLoading.value = false }
}
</script>
<style scoped>
.layout { height: 100vh; }
.aside { background: #1a2a4a; }
.logo { display: flex; align-items: center; gap: 10px; padding: 18px 20px; color: #fff; font-size: 17px; font-weight: 700; }
.logo-icon { font-size: 22px; }
.layout { height: 100dvh; }
.aside { background: var(--bg-sidebar); }
.logo { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-5); color: #fff; font-size: 17px; font-weight: 700; }
.menu { border-right: none; }
.header {
background: #fff; display: flex; align-items: center; justify-content: space-between;
border-bottom: 1px solid #e8eaee; box-shadow: 0 1px 4px rgba(0,0,0,.04);
.menu :deep(.el-menu-item-group__title) {
color: rgba(255,255,255,0.35); font-size: 11px; padding: 12px 20px 4px;
letter-spacing: 1px; text-transform: uppercase;
}
.page-title { font-size: 16px; font-weight: 600; color: #303133; }
.user { cursor: pointer; color: #606266; display: flex; align-items: center; gap: 4px; }
.main { background: #f5f7fa; padding: 20px; overflow-y: auto; }
.header {
background: var(--bg-card); display: flex; align-items: center; justify-content: space-between;
border-bottom: 1px solid var(--border-base); box-shadow: var(--shadow-sm);
}
.page-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.user { cursor: pointer; color: var(--text-regular); display: flex; align-items: center; gap: var(--space-1); }
.user:focus-visible { outline: 2px solid var(--brand-primary); border-radius: var(--radius-sm); }
.main { background: var(--bg-page); padding: var(--space-5); overflow-y: auto; }
.pwd-hint { font-size: 12px; color: var(--text-secondary); margin-top: var(--space-1); }
</style>