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:
opsx-tools committed 2026-07-23 03:01:46 +08:00
1 parent 1e2f8ef24f
commit 021d012478
25 files changed
+2313 -746

No files matched your search

+39 -37
View File
@@ -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