Files
opsx-vpn/frontend/src/views/Sessions.vue
T
Logan.Li 1e2f8ef24f 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参数)
2026-07-23 01:42:36 +08:00

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>