feat: 后端限速实现 + 前端i18n全面国际化 + 安全与代码质量修复

后端:
- trafficmgr: 实现 tc htb 限速(ApplyRateLimit/RemoveRateLimit),自动推导接口名
- 各协议服务: 代码质量改进与错误处理增强

前端:
- protocols.js: 170个硬编码中文字符串全部转为 i18n key,中英文双语翻译
- 提取 fmtBytes() 到 utils/format.js 公共模块(消除 4 处重复)
- router.js: 添加路由级权限守卫,非管理员无法直接 URL 访问管理页
- Tunnels.vue: SSE 日志流改用 fetch+ReadableStream,token 放 Authorization header
- VPNUsers.vue: 修复 catch 块中文硬编码 fallback
- 新增 TaskProgress 组件、taskmgr 后端任务管理
This commit is contained in:
opsx-tools committed 2026-07-23 12:57:46 +08:00
1 parent 021d012478
commit 91974fc30f
26 files changed
+1977 -377

No files matched your search

+18 -16
View File
@@ -34,7 +34,7 @@
</template>
</el-table-column>
<el-table-column :label="$t('common.traffic')" width="170">
<template #default="{ row }">{{ bytes(row.rx_bytes) }} / {{ bytes(row.tx_bytes) }}</template>
<template #default="{ row }">{{ fmtBytes(row.rx_bytes) }} / {{ fmtBytes(row.tx_bytes) }}</template>
</el-table-column>
<el-table-column :label="$t('vpnUsers.lastOnline')" width="180">
<template #default="{ row }">{{ row.last_seen_at ? new Date(row.last_seen_at).toLocaleString() : '-' }}</template>
@@ -60,7 +60,7 @@
<el-form-item :label="$t('vpnUsers.dialTunnel')" required>
<el-select v-model="form.tunnel_id" style="width:100%" filterable>
<el-option v-for="t in tunnels" :key="t.id" :value="t.id"
:label="`${t.name} (${protocolDisplay(t.protocol)})`" />
:label="`${t.name} (${$t(protocolDisplay(t.protocol))})`" />
</el-select>
</el-form-item>
<el-form-item :label="$t('common.username')" required>
@@ -175,6 +175,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import QRCode from 'qrcode'
import api from '../api'
import { fmtBytes } from '../utils/format'
import { protocolDisplay } from '../protocols'
const { t } = useI18n()
@@ -199,17 +200,10 @@ const mfaSaving = ref(false)
const pinVisible = ref(false)
const pinValue = ref('')
const bytes = 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]}`
}
const tunnelName = id => tunnels.value.find(t => t.id === id)?.name || `#${id}`
const tunnelProto = id => {
const tun = tunnels.value.find(x => x.id === id)
return tun ? protocolDisplay(tun.protocol) : ''
return tun ? t(protocolDisplay(tun.protocol)) : ''
}
const isOpenVPN = id => tunnels.value.find(x => x.id === id)?.protocol === 'openvpn'
const isLocked = row => row.mfa_locked_until && new Date(row.mfa_locked_until) > new Date()
@@ -259,14 +253,22 @@ async function create() {
}
async function toggle(row) {
await api.put(`/vpn-users/${row.id}`, { enabled: !row.enabled, display_name: row.display_name, email: row.email })
load()
try {
await api.put(`/vpn-users/${row.id}`, { enabled: !row.enabled, display_name: row.display_name, email: row.email })
load()
} catch (e) {
ElMessage.error(e.response?.data?.error || t('common.operationFailed'))
}
}
async function remove(row) {
await ElMessageBox.confirm(t('vpnUsers.deleteVpnUserConfirm', { username: row.username }), t('common.confirm'))
await api.delete(`/vpn-users/${row.id}`)
load()
try {
await ElMessageBox.confirm(t('vpnUsers.deleteVpnUserConfirm', { username: row.username }), t('common.confirm'))
await api.delete(`/vpn-users/${row.id}`)
load()
} catch (e) {
if (e !== 'cancel') ElMessage.error(e.response?.data?.error || t('common.deleteFailed'))
}
}
async function download(row) {
@@ -311,7 +313,7 @@ async function refreshOTPStatus() {
mfa_locked: isLocked(otpUser.value),
mfa_locked_until: otpUser.value.mfa_locked_until,
mfa_last_used_at: otpUser.value.mfa_last_used_at,
compatible: 'TOTP 标准(兼容 Google/Microsoft Authenticator)'
compatible: t('vpnUsers.totpCompatible')
}
}
}