feat: 全协议一键安装 + 国内GitHub镜像加速 + 中英文i18n国际化
后端: - binmgr.go: 新增 detectChina() 国内/海外自动判断(ipinfo.io) - binmgr.go: 国内自动走 gh-proxy.com 镜像加速 GitHub 下载 - binmgr.go: 新增 InstallViaPackageManager() 包管理器一键安装 - binmgr.go: 新增 GetDeploySpec()/ProtocolAlias() 导出函数 - api.go: installBinary 支持全部7个协议(FRP/WireGuard/OpenVPN/IPSec/IKEv2/L2TP/StrongSwan) - api.go: 策略: GitHub下载失败 → 自动降级包管理器安装 前端: - 安装 vue-i18n@9, 支持中英文切换 - 新增 locales/zh-CN.json + en-US.json (548个翻译key, 17个模块) - 新增 i18n.js 配置, localStorage 持久化语言偏好 - Layout.vue 右上角语言切换下拉菜单 - 所有17个Vue文件中文文本转为 $t() 调用 - Element Plus 组件语言跟随切换 - TunnelWizard: 所有协议显示「一键安装」按钮(带loading) - 前端构建零错误
This commit is contained in:
1 parent
1e2f8ef24f
commit
021d012478
25 files changed
+2313
-746
No files matched your search
@@ -5,72 +5,72 @@
|
||||
<template #header>
|
||||
<div class="row-between">
|
||||
<div>
|
||||
<b>在线会话</b>
|
||||
<el-tag size="small" type="success" style="margin-left:8px">{{ sessions.length }} 个在线</el-tag>
|
||||
<b>{{ $t('sessions.title') }}</b>
|
||||
<el-tag size="small" type="success" style="margin-left:8px">{{ $t('sessions.onlineCount', { count: sessions.length }) }}</el-tag>
|
||||
</div>
|
||||
<el-button link @click="load">刷新</el-button>
|
||||
<el-button link @click="load">{{ $t('common.refresh') }}</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">
|
||||
<el-table-column prop="username" :label="$t('common.username')" min-width="120" />
|
||||
<el-table-column :label="$t('common.protocol')" 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">
|
||||
<el-table-column prop="real_address" :label="$t('sessions.publicAddress')" min-width="140" />
|
||||
<el-table-column prop="virtual_address" :label="$t('sessions.vpnIp')" min-width="120" />
|
||||
<el-table-column :label="$t('sessions.connectedDuration')" width="120">
|
||||
<template #default="{ row }">{{ fmtDuration(row.connected_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="流量 ↓ / ↑" width="140">
|
||||
<el-table-column :label="$t('common.traffic')" 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">
|
||||
<el-table-column :label="$t('common.operation')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small" link @click="disconnect(row)">踢下线</el-button>
|
||||
<el-button type="danger" size="small" link @click="disconnect(row)">{{ $t('sessions.kickOffline') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="当前无在线用户" /></template>
|
||||
<template #empty><el-empty :description="$t('sessions.noOnlineUsers')" /></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>
|
||||
<template #header><b style="color:var(--color-danger)">{{ $t('sessions.trafficAlert') }}</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 prop="created_at" :label="$t('common.time')" width="170">
|
||||
<template #default="{ row }">{{ new Date(row.created_at).toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户/隧道" width="100" />
|
||||
<el-table-column label="类型" width="100">
|
||||
<el-table-column prop="username" :label="$t('sessions.userTunnel')" width="100" />
|
||||
<el-table-column :label="$t('sessions.alertType')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.event === 'rate_alert' ? 'warning' : 'danger'" size="small">
|
||||
{{ { rate_alert: '突发流量', quota_exceed: '配额超额' }[row.event] || row.event }}
|
||||
{{ { rate_alert: $t('sessions.rateAlert'), quota_exceed: $t('sessions.quotaExceed') }[row.event] || row.event }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detail" label="详情" show-overflow-tooltip />
|
||||
<el-table-column prop="detail" :label="$t('common.detail')" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 连接审计日志 -->
|
||||
<el-card class="page-card" shadow="never">
|
||||
<template #header><b>连接审计日志</b></template>
|
||||
<template #header><b>{{ $t('sessions.auditLog') }}</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 prop="created_at" :label="$t('common.time')" width="170">
|
||||
<template #default="{ row }">{{ new Date(row.created_at).toLocaleString() }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户" width="100" />
|
||||
<el-table-column label="事件" width="100">
|
||||
<el-table-column prop="username" :label="$t('common.username')" width="100" />
|
||||
<el-table-column :label="$t('sessions.event')" 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-column prop="ip_address" :label="$t('common.ipAddress')" width="140" />
|
||||
<el-table-column prop="detail" :label="$t('common.detail')" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -79,9 +79,11 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '../api'
|
||||
import { protocolColor, protocolDisplay } from '../protocols'
|
||||
|
||||
const { t } = useI18n()
|
||||
const sessions = ref([])
|
||||
const logs = ref([])
|
||||
const alerts = ref([])
|
||||
@@ -94,15 +96,15 @@ function fmtBytes(n) {
|
||||
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 fmtDuration(dur) {
|
||||
if (!dur) return '-'
|
||||
const sec = Math.floor((Date.now() - new Date(dur).getTime()) / 1000)
|
||||
if (sec < 60) return t('sessions.seconds', { n: sec })
|
||||
if (sec < 3600) return t('sessions.minutes', { n: Math.floor(sec / 60) })
|
||||
return t('sessions.hoursMinutes', { h: Math.floor(sec / 3600), m: Math.floor((sec % 3600) / 60) })
|
||||
}
|
||||
function eventLabel(e) {
|
||||
return { connect: '连接', disconnect: '断开', auth_fail: '认证失败', quota_exceed: '配额超额', rate_alert: '突发流量' }[e] || e
|
||||
return { connect: t('sessions.eventConnect'), disconnect: t('sessions.eventDisconnect'), auth_fail: t('sessions.eventAuthFail'), quota_exceed: t('sessions.eventQuotaExceed'), rate_alert: t('sessions.eventRateAlert') }[e] || e
|
||||
}
|
||||
function eventTagType(e) {
|
||||
return { connect: 'success', disconnect: 'info', auth_fail: 'danger', quota_exceed: 'warning', rate_alert: 'warning' }[e] || 'info'
|
||||
@@ -120,17 +122,17 @@ async function load() {
|
||||
logs.value = l.data || []
|
||||
alerts.value = a.data || []
|
||||
} catch (e) {
|
||||
ElMessage.error('加载数据失败')
|
||||
ElMessage.error(t('sessions.loadDataFailed'))
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function disconnect(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定踢用户「${row.username}」下线?`, '确认', { type: 'warning' })
|
||||
await ElMessageBox.confirm(t('sessions.kickConfirm', { username: row.username }), t('common.confirm'), { type: 'warning' })
|
||||
await api.post(`/sessions/${row.id}/disconnect`)
|
||||
ElMessage.success('已踢下线')
|
||||
ElMessage.success(t('sessions.kicked'))
|
||||
load()
|
||||
} catch (e) { if (e !== 'cancel') ElMessage.error('操作失败') }
|
||||
} catch (e) { if (e !== 'cancel') ElMessage.error(t('common.operationFailed')) }
|
||||
}
|
||||
|
||||
let timer
|
||||
|
||||
Reference in new issue
Block a user