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参数)
140 lines
5.6 KiB
Vue
140 lines
5.6 KiB
Vue
<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>
|