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
+100
-102
@@ -3,28 +3,28 @@
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<div>
|
||||
<el-input v-model="keyword" placeholder="搜索名称" clearable style="width:200px" @change="load" />
|
||||
<el-select v-model="protocolFilter" placeholder="全部协议" clearable style="width:160px;margin-left:10px" @change="load">
|
||||
<el-input v-model="keyword" :placeholder="$t('tunnels.searchPlaceholder')" clearable style="width:200px" @change="load" />
|
||||
<el-select v-model="protocolFilter" :placeholder="$t('tunnels.allProtocols')" clearable style="width:160px;margin-left:10px" @change="load">
|
||||
<el-option v-for="p in protocolOptions" :key="p" :label="protocolDisplay(p)" :value="p" />
|
||||
</el-select>
|
||||
<el-select v-model="stateFilter" placeholder="全部状态" clearable style="width:140px;margin-left:10px" @change="load">
|
||||
<el-option label="运行中" value="running" />
|
||||
<el-option label="已停止" value="stopped" />
|
||||
<el-option label="异常" value="error" />
|
||||
<el-select v-model="stateFilter" :placeholder="$t('tunnels.allStates')" clearable style="width:140px;margin-left:10px" @change="load">
|
||||
<el-option :label="$t('tunnels.stateRunning')" value="running" />
|
||||
<el-option :label="$t('tunnels.stateStopped')" value="stopped" />
|
||||
<el-option :label="$t('tunnels.stateError')" value="error" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="$router.push('/tunnels/create')">创建专线</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="$router.push('/tunnels/create')">{{ $t('tunnels.createTunnel') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="list" v-loading="loading" size="large">
|
||||
<el-table-column prop="name" label="名称" min-width="140">
|
||||
<el-table-column prop="name" :label="$t('common.name')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<b>{{ row.name }}</b>
|
||||
<div class="remark" v-if="row.remark">{{ row.remark }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="协议" width="150">
|
||||
<el-table-column :label="$t('common.protocol')" width="150">
|
||||
<template #default="{ row }">
|
||||
<span class="proto-cell">
|
||||
<ProtocolIcon :protocol="row.protocol" :size="16" :style="{ color: protocolColor(row.protocol) }" />
|
||||
@@ -32,7 +32,7 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<el-table-column :label="$t('common.status')" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-badge is-dot :type="dotType(row)" style="padding-left:12px">
|
||||
<span :style="{ color: stateColor(row) }">{{ stateText(row) }}</span>
|
||||
@@ -42,42 +42,42 @@
|
||||
</el-tooltip>
|
||||
</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="150">
|
||||
<el-table-column :label="$t('tunnels.expireTime')" width="150">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: expiringSoon(row) ? '#e6a23c' : '' }">{{ row.expire_at ? fmtTime(row.expire_at) : '永久' }}</span>
|
||||
<span :style="{ color: expiringSoon(row) ? '#e6a23c' : '' }">{{ row.expire_at ? fmtTime(row.expire_at) : $t('common.permanent') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="节点" width="110">
|
||||
<template #default="{ row }">{{ row.node?.name || '本机节点' }}</template>
|
||||
<el-table-column :label="$t('tunnels.node')" width="110">
|
||||
<template #default="{ row }">{{ row.node?.name || $t('tunnels.localNode') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<el-table-column :label="$t('common.operation')" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="stateOf(row) !== 'running'" type="success" size="small" link @click="start(row)">启动</el-button>
|
||||
<el-button v-else type="warning" size="small" link @click="stop(row)">停止</el-button>
|
||||
<el-button size="small" link @click="showDetail(row)">详情</el-button>
|
||||
<el-button size="small" link @click="$router.push(`/tunnels/${row.id}/edit`)">编辑</el-button>
|
||||
<el-button v-if="stateOf(row) !== 'running'" type="success" size="small" link @click="start(row)">{{ $t('tunnels.start') }}</el-button>
|
||||
<el-button v-else type="warning" size="small" link @click="stop(row)">{{ $t('tunnels.stop') }}</el-button>
|
||||
<el-button size="small" link @click="showDetail(row)">{{ $t('tunnels.detail') }}</el-button>
|
||||
<el-button size="small" link @click="$router.push(`/tunnels/${row.id}/edit`)">{{ $t('common.edit') }}</el-button>
|
||||
<el-dropdown trigger="click" @command="cmd => onMoreCommand(cmd, row)" style="margin-left:8px">
|
||||
<el-button size="small" link>更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||||
<el-button size="small" link>{{ $t('tunnels.more') }}<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logs">查看日志</el-dropdown-item>
|
||||
<el-dropdown-item command="download">下载配置</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.protocol === 'wireguard'" command="qrcode">显示二维码</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.protocol === 'ipsec'" command="ipsec-peer">IPSec 对接测试</el-dropdown-item>
|
||||
<el-dropdown-item command="logs">{{ $t('tunnels.viewLogs') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="download">{{ $t('tunnels.downloadConfig') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.protocol === 'wireguard'" command="qrcode">{{ $t('tunnels.showQrCode') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="row.protocol === 'ipsec'" command="ipsec-peer">{{ $t('tunnels.ipsecTest') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button type="danger" size="small" link style="margin-left:12px" @click="remove(row)">删除</el-button>
|
||||
<el-button type="danger" size="small" link style="margin-left:12px" @click="remove(row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="还没有专线">
|
||||
<el-button type="primary" :icon="Plus" @click="$router.push('/tunnels/create')">创建第一条专线</el-button>
|
||||
<el-empty :description="$t('tunnels.noTunnels')">
|
||||
<el-button type="primary" :icon="Plus" @click="$router.push('/tunnels/create')">{{ $t('tunnels.createFirst') }}</el-button>
|
||||
</el-empty>
|
||||
</template>
|
||||
</el-table>
|
||||
@@ -86,69 +86,69 @@
|
||||
<el-drawer v-model="detailVisible" :title="detail?.tunnel?.name" size="560px">
|
||||
<template v-if="detail">
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="协议">{{ protocolDisplay(detail.tunnel.protocol) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ stateText(detail.tunnel) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ fmtTime(detail.tunnel.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="自动启动">{{ detail.tunnel.auto_start ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('common.protocol')">{{ protocolDisplay(detail.tunnel.protocol) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('common.status')">{{ stateText(detail.tunnel) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('tunnels.createTime')">{{ fmtTime(detail.tunnel.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('tunnels.autoStart')">{{ detail.tunnel.auto_start ? $t('common.yes') : $t('common.no') }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<h4>运行参数</h4>
|
||||
<h4>{{ $t('tunnels.runningParams') }}</h4>
|
||||
<pre class="mono conf-box">{{ prettyConfig(detail.tunnel.config) }}</pre>
|
||||
<h4>客户端配置</h4>
|
||||
<pre class="mono conf-box">{{ detail.client_config || '该协议暂无客户端配置' }}</pre>
|
||||
<el-button type="primary" style="width:100%;margin-bottom:8px" @click="download(detail.tunnel)">下载客户端配置</el-button>
|
||||
<h4>{{ $t('tunnels.clientConfig') }}</h4>
|
||||
<pre class="mono conf-box">{{ detail.client_config || $t('tunnels.noClientConfig') }}</pre>
|
||||
<el-button type="primary" style="width:100%;margin-bottom:8px" @click="download(detail.tunnel)">{{ $t('tunnels.downloadClientConfig') }}</el-button>
|
||||
<el-button v-if="detail.tunnel.protocol === 'ipsec'" type="warning" plain style="width:100%;margin-bottom:16px"
|
||||
@click="$router.push('/ipsec-peer')">IPSec 参数检查与连通性测试</el-button>
|
||||
@click="$router.push('/ipsec-peer')">{{ $t('tunnels.ipsecParamCheck') }}</el-button>
|
||||
|
||||
<!-- 流量策略 -->
|
||||
<h4>流量策略</h4>
|
||||
<h4>{{ $t('tunnels.trafficPolicy') }}</h4>
|
||||
<el-form label-width="90px" size="small" v-loading="policyLoading">
|
||||
<el-form-item label="配额(GB)">
|
||||
<el-form-item :label="$t('tunnels.quotaGB')">
|
||||
<el-input-number v-model="policy.quota_gb" :min="0" :precision="1" :step="10" style="width:100%" />
|
||||
<span style="font-size:12px;color:var(--text-secondary);margin-left:8px">0 = 不限。已用:{{ fmtBytes(policy.used_bytes || 0) }}</span>
|
||||
<span style="font-size:12px;color:var(--text-secondary);margin-left:8px">{{ $t('tunnels.quotaUnlimited', { used: fmtBytes(policy.used_bytes || 0) }) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="限速(Mbps)">
|
||||
<el-form-item :label="$t('tunnels.rateLimitMbps')">
|
||||
<el-input-number v-model="policy.rate_limit_mbps" :min="0" :max="10000" style="width:100%" />
|
||||
<span style="font-size:12px;color:var(--text-secondary);margin-left:8px">0 = 不限</span>
|
||||
<span style="font-size:12px;color:var(--text-secondary);margin-left:8px">{{ $t('tunnels.rateUnlimited') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="超额动作">
|
||||
<el-form-item :label="$t('tunnels.exceedAction')">
|
||||
<el-select v-model="policy.action_on_exceed" style="width:100%">
|
||||
<el-option value="warn" label="仅告警" />
|
||||
<el-option value="block" label="自动停止" />
|
||||
<el-option value="throttle" label="限速至1Mbps" />
|
||||
<el-option value="warn" :label="$t('tunnels.actionWarn')" />
|
||||
<el-option value="block" :label="$t('tunnels.actionBlock')" />
|
||||
<el-option value="throttle" :label="$t('tunnels.actionThrottle')" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="重置周期">
|
||||
<el-form-item :label="$t('tunnels.resetCycle')">
|
||||
<el-select v-model="policy.reset_cycle" style="width:100%">
|
||||
<el-option value="none" label="不重置" />
|
||||
<el-option value="daily" label="每天" />
|
||||
<el-option value="monthly" label="每月" />
|
||||
<el-option value="none" :label="$t('tunnels.resetNone')" />
|
||||
<el-option value="daily" :label="$t('tunnels.resetDaily')" />
|
||||
<el-option value="monthly" :label="$t('tunnels.resetMonthly')" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" size="small" :loading="policySaving" @click="savePolicy(detail.tunnel.id)">保存策略</el-button>
|
||||
<el-button type="primary" size="small" :loading="policySaving" @click="savePolicy(detail.tunnel.id)">{{ $t('tunnels.savePolicy') }}</el-button>
|
||||
</el-form>
|
||||
|
||||
<!-- WireGuard Peer 管理 -->
|
||||
<template v-if="detail.tunnel.protocol === 'wireguard'">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin:8px 0">
|
||||
<h4 style="margin:0">Peer 列表</h4>
|
||||
<el-button type="primary" size="small" @click="openAddPeer">添加 Peer</el-button>
|
||||
<h4 style="margin:0">{{ $t('tunnels.peerList') }}</h4>
|
||||
<el-button type="primary" size="small" @click="openAddPeer">{{ $t('tunnels.addPeer') }}</el-button>
|
||||
</div>
|
||||
<el-table :data="wgPeers" size="small" v-loading="peersLoading">
|
||||
<el-table-column prop="id" label="ID" width="100" />
|
||||
<el-table-column prop="name" label="名称" width="100" />
|
||||
<el-table-column prop="client_address" label="地址" min-width="110" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<el-table-column prop="name" :label="$t('common.name')" width="100" />
|
||||
<el-table-column prop="client_address" :label="$t('tunnels.address')" min-width="110" />
|
||||
<el-table-column :label="$t('common.status')" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="peerOnline(row) ? 'success' : 'info'">
|
||||
{{ peerOnline(row) ? '在线' : '离线' }}
|
||||
{{ peerOnline(row) ? $t('tunnels.peerOnline') : $t('tunnels.peerOffline') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<el-table-column :label="$t('common.operation')" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.managed !== false" link size="small" @click="downloadPeer(row)">配置</el-button>
|
||||
<el-button v-if="row.managed !== false" link size="small" @click="qrPeer(row)">二维码</el-button>
|
||||
<el-button type="danger" link size="small" @click="removePeer(row)">删除</el-button>
|
||||
<el-button v-if="row.managed !== false" link size="small" @click="downloadPeer(row)">{{ $t('tunnels.config') }}</el-button>
|
||||
<el-button v-if="row.managed !== false" link size="small" @click="qrPeer(row)">{{ $t('tunnels.qrCode') }}</el-button>
|
||||
<el-button type="danger" link size="small" @click="removePeer(row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -157,58 +157,59 @@
|
||||
</el-drawer>
|
||||
|
||||
<!-- WireGuard 二维码 -->
|
||||
<el-dialog v-model="qrVisible" title="扫码导入 WireGuard 客户端" width="360px" align-center>
|
||||
<el-dialog v-model="qrVisible" :title="$t('tunnels.scanQrTitle')" width="360px" align-center>
|
||||
<div style="text-align:center">
|
||||
<canvas ref="qrCanvas"></canvas>
|
||||
<p style="color:#909399;font-size:12px">使用 WireGuard App「从二维码添加」</p>
|
||||
<p style="color:#909399;font-size:12px">{{ $t('tunnels.scanQrHint') }}</p>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 添加 Peer -->
|
||||
<el-dialog v-model="peerDialog" title="添加 WireGuard Peer" width="440px">
|
||||
<el-dialog v-model="peerDialog" :title="$t('tunnels.addPeerTitle')" width="440px">
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="Peer ID">
|
||||
<el-input v-model="peerForm.id" placeholder="留空自动生成 client-N" />
|
||||
<el-form-item :label="$t('tunnels.peerId')">
|
||||
<el-input v-model="peerForm.id" :placeholder="$t('tunnels.autoGenId')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="peerForm.name" placeholder="手机 / 笔记本" />
|
||||
<el-form-item :label="$t('common.name')">
|
||||
<el-input v-model="peerForm.name" :placeholder="$t('tunnels.peerNamePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端地址" required>
|
||||
<el-form-item :label="$t('tunnels.clientAddress')" required>
|
||||
<el-input v-model="peerForm.client_address" placeholder="10.8.0.3/32" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AllowedIPs">
|
||||
<el-input v-model="peerForm.allowed_ips" placeholder="留空使用隧道默认" />
|
||||
<el-input v-model="peerForm.allowed_ips" :placeholder="$t('tunnels.defaultAllowedIPs')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="外部公钥">
|
||||
<el-form-item :label="$t('tunnels.externalPublicKey')">
|
||||
<el-input v-model="peerForm.public_key" type="textarea" :rows="2"
|
||||
placeholder="留空则平台托管生成密钥;填写则为外部设备" />
|
||||
:placeholder="$t('tunnels.externalKeyHint')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="peerDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="peerSaving" @click="addPeer">添加</el-button>
|
||||
<el-button @click="peerDialog = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="peerSaving" @click="addPeer">{{ $t('common.add') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志实时查看弹窗 -->
|
||||
<el-dialog v-model="logVisible" :title="`实例日志 — ${logTunnel?.name || ''}`" width="780px" align-center
|
||||
<el-dialog v-model="logVisible" :title="$t('tunnels.logTitle', { name: logTunnel?.name || '' })" width="780px" align-center
|
||||
@close="closeLogStream">
|
||||
<div class="log-toolbar">
|
||||
<el-tag size="small" :type="logStreaming ? 'success' : 'info'">
|
||||
{{ logStreaming ? '● 实时输出中' : '已停止' }}
|
||||
{{ logStreaming ? $t('tunnels.liveOutput') : $t('tunnels.logStopped') }}
|
||||
</el-tag>
|
||||
<el-button v-if="logStreaming" size="small" @click="stopLogStream">暂停</el-button>
|
||||
<el-button v-else size="small" type="primary" @click="resumeLogStream">继续</el-button>
|
||||
<el-button size="small" @click="logContent = ''">清屏</el-button>
|
||||
<el-button v-if="logStreaming" size="small" @click="stopLogStream">{{ $t('tunnels.pause') }}</el-button>
|
||||
<el-button v-else size="small" type="primary" @click="resumeLogStream">{{ $t('tunnels.resume') }}</el-button>
|
||||
<el-button size="small" @click="logContent = ''">{{ $t('tunnels.clearScreen') }}</el-button>
|
||||
</div>
|
||||
<div class="log-view mono" ref="logView">{{ logContent || '等待日志输出...' }}</div>
|
||||
<div class="log-view mono" ref="logView">{{ logContent || $t('tunnels.waitingLog') }}</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Plus, InfoFilled, ArrowDown } from '@element-plus/icons-vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -216,6 +217,7 @@ import api from '../api'
|
||||
import ProtocolIcon from '../components/ProtocolIcon.vue'
|
||||
import { protocolDisplay, protocolColor } from '../protocols'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -245,12 +247,12 @@ function fmtBytes(n) {
|
||||
while (n >= 1024 && i < 4) { n /= 1024; i++ }
|
||||
return n.toFixed(1) + ' ' + u[i]
|
||||
}
|
||||
function fmtTime(t) { return t ? new Date(t).toLocaleString('zh-CN') : '-' }
|
||||
function fmtTime(time) { return time ? new Date(time).toLocaleString() : '-' }
|
||||
function expiringSoon(row) {
|
||||
return row.expire_at && new Date(row.expire_at) - Date.now() < 7 * 864e5
|
||||
}
|
||||
function stateText(row) {
|
||||
return { running: '运行中', stopped: '已停止', error: '异常', undeployed: '未部署' }[stateOf(row)] || stateOf(row)
|
||||
return { running: t('tunnels.stateRunning'), stopped: t('tunnels.stateStopped'), error: t('tunnels.stateError'), undeployed: t('tunnels.stateUndeployed') }[stateOf(row)] || stateOf(row)
|
||||
}
|
||||
function stateColor(row) {
|
||||
return { running: '#67c23a', stopped: '#909399', error: '#f56c6c', undeployed: '#e6a23c' }[stateOf(row)]
|
||||
@@ -272,7 +274,6 @@ async function load() {
|
||||
try {
|
||||
const { data } = await api.get('/tunnels', { params: { keyword: keyword.value, protocol: protocolFilter.value } })
|
||||
let result = data || []
|
||||
// 前端状态筛选(后端无 state 参数)
|
||||
if (stateFilter.value) {
|
||||
result = result.filter(r => stateOf(r) === stateFilter.value)
|
||||
}
|
||||
@@ -307,16 +308,13 @@ function resumeLogStream() {
|
||||
if (!logTunnel.value) return
|
||||
stopLogStream()
|
||||
const token = localStorage.getItem('vpn_token')
|
||||
// SSE 不支持自定义 header,通过 query 传 token
|
||||
logEventSource = new EventSource(`/api/tunnels/${logTunnel.value.id}/logs?token=${token}`)
|
||||
logStreaming.value = true
|
||||
logEventSource.onmessage = (e) => {
|
||||
logContent.value += e.data + '\n'
|
||||
// 自动滚动到底部
|
||||
nextTick(() => {
|
||||
if (logView.value) logView.value.scrollTop = logView.value.scrollHeight
|
||||
})
|
||||
// 限制日志长度防止内存爆炸
|
||||
if (logContent.value.length > 100000) {
|
||||
logContent.value = logContent.value.slice(-50000)
|
||||
}
|
||||
@@ -364,30 +362,30 @@ async function savePolicy(tunnelId) {
|
||||
action_on_exceed: policy.value.action_on_exceed,
|
||||
reset_cycle: policy.value.reset_cycle,
|
||||
})
|
||||
ElMessage.success('流量策略已保存')
|
||||
ElMessage.success(t('tunnels.policySaved'))
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.error || '保存失败')
|
||||
ElMessage.error(e.response?.data?.error || t('common.saveFailed'))
|
||||
} finally { policySaving.value = false }
|
||||
}
|
||||
|
||||
async function start(row) {
|
||||
try {
|
||||
await api.post(`/tunnels/${row.id}/start`)
|
||||
ElMessage.success('已启动')
|
||||
ElMessage.success(t('tunnels.started'))
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.error || '启动失败')
|
||||
ElMessage.error(e.response?.data?.error || t('tunnels.startFailed'))
|
||||
}
|
||||
load()
|
||||
}
|
||||
async function stop(row) {
|
||||
await api.post(`/tunnels/${row.id}/stop`)
|
||||
ElMessage.success('已停止')
|
||||
ElMessage.success(t('tunnels.stopped'))
|
||||
load()
|
||||
}
|
||||
async function remove(row) {
|
||||
await ElMessageBox.confirm(`确定删除专线「${row.name}」?配置与密钥将一并清除。`, '删除确认', { type: 'warning' })
|
||||
await ElMessageBox.confirm(t('tunnels.deleteTunnelConfirm', { name: row.name }), t('common.deleteConfirm'), { type: 'warning' })
|
||||
await api.delete(`/tunnels/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
ElMessage.success(t('common.deleteSuccess'))
|
||||
load()
|
||||
}
|
||||
async function showDetail(row) {
|
||||
@@ -422,7 +420,7 @@ function openAddPeer() {
|
||||
}
|
||||
|
||||
async function addPeer() {
|
||||
if (!peerForm.value.client_address) return ElMessage.warning('请填写客户端地址')
|
||||
if (!peerForm.value.client_address) return ElMessage.warning(t('tunnels.fillClientAddress'))
|
||||
peerSaving.value = true
|
||||
try {
|
||||
const body = { ...peerForm.value }
|
||||
@@ -430,28 +428,28 @@ async function addPeer() {
|
||||
if (!body.public_key) delete body.public_key
|
||||
if (!body.allowed_ips) delete body.allowed_ips
|
||||
await api.post(`/tunnels/${detail.value.tunnel.id}/wireguard/peers`, body)
|
||||
ElMessage.success('Peer 已添加')
|
||||
ElMessage.success(t('tunnels.peerAdded'))
|
||||
peerDialog.value = false
|
||||
await loadPeers(detail.value.tunnel.id)
|
||||
const { data } = await api.get(`/tunnels/${detail.value.tunnel.id}`)
|
||||
detail.value = data
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.error || '添加失败')
|
||||
ElMessage.error(e.response?.data?.error || t('tunnels.addFailed'))
|
||||
} finally {
|
||||
peerSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removePeer(row) {
|
||||
await ElMessageBox.confirm(`删除 Peer「${row.id}」?`, '确认', { type: 'warning' })
|
||||
await ElMessageBox.confirm(t('tunnels.deletePeerConfirm', { id: row.id }), t('common.confirm'), { type: 'warning' })
|
||||
try {
|
||||
await api.delete(`/tunnels/${detail.value.tunnel.id}/wireguard/peers/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
ElMessage.success(t('common.deleteSuccess'))
|
||||
await loadPeers(detail.value.tunnel.id)
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e.response?.data?.error || '删除失败')
|
||||
ElMessage.error(e.response?.data?.error || t('common.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +467,7 @@ function downloadPeer(row) {
|
||||
a.href = URL.createObjectURL(b)
|
||||
a.download = `wireguard-${row.id}.conf`
|
||||
a.click()
|
||||
}).catch(e => ElMessage.error(e.message || '下载失败'))
|
||||
}).catch(e => ElMessage.error(e.message || t('tunnels.downloadFailed')))
|
||||
}
|
||||
|
||||
async function qrPeer(row) {
|
||||
@@ -486,7 +484,7 @@ async function qrPeer(row) {
|
||||
await nextTick()
|
||||
QRCode.toCanvas(qrCanvas.value, conf, { width: 280, margin: 1 })
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || '生成二维码失败')
|
||||
ElMessage.error(e.message || t('tunnels.qrFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +501,7 @@ function download(row) {
|
||||
}
|
||||
async function showQr(row) {
|
||||
const { data } = await api.get(`/tunnels/${row.id}`)
|
||||
if (!data.client_config) return ElMessage.warning('无客户端配置')
|
||||
if (!data.client_config) return ElMessage.warning(t('tunnels.noClientConfigWarn'))
|
||||
qrVisible.value = true
|
||||
await nextTick()
|
||||
QRCode.toCanvas(qrCanvas.value, data.client_config, { width: 280, margin: 1 })
|
||||
|
||||
Reference in new issue
Block a user