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:
1 parent
021d012478
commit
91974fc30f
26 files changed
+1977
-377
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user