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:
1 parent
58c036c34c
commit
1e2f8ef24f
43 files changed
+6783
-460
No files matched your search
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 在线会话 -->
|
||||
<el-card class="page-card" shadow="never" style="margin-bottom:16px">
|
||||
<template #header>
|
||||
<div class="row-between">
|
||||
<div>
|
||||
<b>在线会话</b>
|
||||
<el-tag size="small" type="success" style="margin-left:8px">{{ sessions.length }} 个在线</el-tag>
|
||||
</div>
|
||||
<el-button link @click="load">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="sessions" size="default" v-loading="loading">
|
||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||
<el-table-column label="协议" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="protocol-tag" :style="{ color: protocolColor(row.protocol) }">{{ protocolDisplay(row.protocol) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="real_address" label="公网地址" min-width="140" />
|
||||
<el-table-column prop="virtual_address" label="VPN IP" min-width="120" />
|
||||
<el-table-column label="已连接时长" width="120">
|
||||
<template #default="{ row }">{{ fmtDuration(row.connected_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="流量 ↓ / ↑" width="140">
|
||||
<template #default="{ row }">
|
||||
<span style="font-size:12px">{{ fmtBytes(row.rx_bytes) }} / {{ fmtBytes(row.tx_bytes) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small" link @click="disconnect(row)">踢下线</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="当前无在线用户" /></template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 异常流量告警 -->
|
||||
<el-card class="page-card" shadow="never" style="margin-bottom:16px" v-if="alerts.length">
|
||||
<template #header><b style="color:var(--color-danger)">⚠ 异常流量告警</b></template>
|
||||
<el-table :data="alerts" size="small">
|
||||
<el-table-column prop="created_at" label="时间" width="170">
|
||||
<template #default="{ row }">{{ new Date(row.created_at).toLocaleString('zh-CN') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户/隧道" width="100" />
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.event === 'rate_alert' ? 'warning' : 'danger'" size="small">
|
||||
{{ { rate_alert: '突发流量', quota_exceed: '配额超额' }[row.event] || row.event }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detail" label="详情" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 连接审计日志 -->
|
||||
<el-card class="page-card" shadow="never">
|
||||
<template #header><b>连接审计日志</b></template>
|
||||
<el-table :data="logs" size="small" max-height="500">
|
||||
<el-table-column prop="created_at" label="时间" width="170">
|
||||
<template #default="{ row }">{{ new Date(row.created_at).toLocaleString('zh-CN') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户" width="100" />
|
||||
<el-table-column label="事件" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="eventTagType(row.event)" size="small">{{ eventLabel(row.event) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ip_address" label="IP 地址" width="140" />
|
||||
<el-table-column prop="detail" label="详情" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '../api'
|
||||
import { protocolColor, protocolDisplay } from '../protocols'
|
||||
|
||||
const sessions = ref([])
|
||||
const logs = ref([])
|
||||
const alerts = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (!n) return '0 B'
|
||||
const u = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let i = 0
|
||||
while (n >= 1024 && i < 4) { n /= 1024; i++ }
|
||||
return n.toFixed(1) + ' ' + u[i]
|
||||
}
|
||||
function fmtDuration(t) {
|
||||
if (!t) return '-'
|
||||
const sec = Math.floor((Date.now() - new Date(t).getTime()) / 1000)
|
||||
if (sec < 60) return `${sec}秒`
|
||||
if (sec < 3600) return `${Math.floor(sec / 60)}分钟`
|
||||
return `${Math.floor(sec / 3600)}小时${Math.floor((sec % 3600) / 60)}分`
|
||||
}
|
||||
function eventLabel(e) {
|
||||
return { connect: '连接', disconnect: '断开', auth_fail: '认证失败', quota_exceed: '配额超额', rate_alert: '突发流量' }[e] || e
|
||||
}
|
||||
function eventTagType(e) {
|
||||
return { connect: 'success', disconnect: 'info', auth_fail: 'danger', quota_exceed: 'warning', rate_alert: 'warning' }[e] || 'info'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [s, l, a] = await Promise.all([
|
||||
api.get('/sessions'),
|
||||
api.get('/sessions/log'),
|
||||
api.get('/traffic/alerts'),
|
||||
])
|
||||
sessions.value = s.data || []
|
||||
logs.value = l.data || []
|
||||
alerts.value = a.data || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function disconnect(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定踢用户「${row.username}」下线?`, '确认', { type: 'warning' })
|
||||
await api.post(`/sessions/${row.id}/disconnect`)
|
||||
ElMessage.success('已踢下线')
|
||||
load()
|
||||
} catch (e) { if (e !== 'cancel') ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
let timer
|
||||
onMounted(() => { load(); timer = setInterval(load, 15000) })
|
||||
onUnmounted(() => clearInterval(timer))
|
||||
</script>
|
||||
Reference in new issue
Block a user