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

+128
View File
@@ -0,0 +1,128 @@
<template>
<el-dialog v-model="visible" :title="$t('tunnels.taskProgress')" width="560px" align-center @close="onClose">
<div v-if="!task" style="text-align:center;padding:20px;color:var(--text-secondary)">
{{ $t('tunnels.noTasks') }}
</div>
<template v-else>
<div style="margin-bottom:16px">
<el-tag :type="statusTag" size="small" style="margin-right:8px">{{ statusLabel }}</el-tag>
<span style="font-size:13px;color:var(--text-secondary)">
{{ task.action === 'start' ? $t('tunnels.batchStart') : task.action === 'stop' ? $t('tunnels.batchStop') : $t('tunnels.batchDelete') }}
· {{ task.total }} {{ $t('tunnels.stateTunnel') || 'tunnels' }}
</span>
</div>
<el-progress
:percentage="progress"
:status="task.status === 'done' ? (task.failed > 0 ? 'warning' : 'success') : undefined"
:stroke-width="12"
style="margin-bottom:16px"
/>
<div style="display:flex;gap:16px;margin-bottom:12px;font-size:13px">
<span style="color:#67c23a">✓ {{ task.success }}</span>
<span style="color:#f56c6c">✗ {{ task.failed }}</span>
<span style="color:#909399">⏳ {{ task.total - task.success - task.failed }}</span>
</div>
<el-table :data="task.sub_tasks" size="small" max-height="260">
<el-table-column prop="tunnel_name" :label="$t('common.name')" min-width="120" />
<el-table-column :label="$t('common.status')" width="100">
<template #default="{ row }">
<el-tag :type="subTag(row.status)" size="small">{{ subLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="error" :label="$t('common.error')" min-width="160">
<template #default="{ row }">
<span v-if="row.error" style="color:#f56c6c;font-size:12px">{{ row.error }}</span>
</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button @click="visible = false">{{ $t('common.close') }}</el-button>
<el-button v-if="task?.status !== 'done'" type="primary" @click="refresh">
<el-icon v-if="polling" class="is-loading"><Loading /></el-icon>
{{ $t('tunnels.refresh') || '刷新' }}
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { Loading } from '@element-plus/icons-vue'
import api from '../api'
const { t } = useI18n()
const visible = ref(false)
const task = ref(null)
const taskId = ref('')
const polling = ref(false)
let pollTimer = null
const progress = computed(() => {
if (!task.value || task.value.total === 0) return 0
return Math.round((task.value.success + task.value.failed) * 100 / task.value.total)
})
const statusTag = computed(() => {
if (!task.value) return 'info'
if (task.value.status === 'done') return task.value.failed > 0 ? 'warning' : 'success'
return 'primary'
})
const statusLabel = computed(() => {
if (!task.value) return ''
if (task.value.status === 'pending') return t('tunnels.taskPending')
if (task.value.status === 'running') return t('tunnels.taskInProgress', { done: task.value.success + task.value.failed, total: task.value.total })
if (task.value.status === 'done') return task.value.failed > 0 ? t('tunnels.taskPartialFail') : t('tunnels.taskComplete')
return task.value.status
})
function subTag(s) {
return { pending: 'info', running: 'primary', done: 'success', failed: 'danger' }[s] || 'info'
}
function subLabel(s) {
return { pending: t('tunnels.taskPending'), running: t('tunnels.taskRunning'), done: t('tunnels.taskDone'), failed: t('tunnels.taskFailed') }[s] || s
}
async function open(id) {
taskId.value = id
visible.value = true
await refresh()
startPolling()
}
async function refresh() {
try {
const { data } = await api.get(`/tasks/${taskId.value}`)
task.value = data
if (data.status === 'done') stopPolling()
} catch { /* task may not exist yet */ }
}
function startPolling() {
stopPolling()
polling.value = true
pollTimer = setInterval(refresh, 1000)
}
function stopPolling() {
polling.value = false
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
}
function onClose() {
stopPolling()
task.value = null
taskId.value = ''
}
onUnmounted(stopPolling)
defineExpose({ open })
</script>