feat: implement VPN platform management and real protocol services

This commit is contained in:
Logan.Li committed 2026-07-22 21:49:13 +08:00
commit 58c036c34c
45 files changed
+9244

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
<template>
<el-card class="page-card" shadow="never" v-loading="loading">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center">
<b>编辑专线</b>
<el-button @click="router.push('/tunnels')">返回列表</el-button>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom:18px"
title="密码、PSK、Token 等敏感字段不会回显;留空表示继续使用原值。" />
<el-form v-if="form" label-width="170px" label-position="left" style="max-width:720px">
<el-form-item label="专线名称" required>
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="协议类型">
<el-tag :color="protocolColor(protocol)" effect="dark" style="border:none">{{ protocolDisplay(protocol) }}</el-tag>
</el-form-item>
<el-form-item v-for="field in visibleFields" :key="field.key" :label="field.label" :required="field.required">
<el-input-number v-if="field.type === 'number'" v-model="form.config[field.key]" :min="0" :max="65535" />
<el-select v-else-if="field.type === 'select'" v-model="form.config[field.key]" style="width:100%">
<el-option v-for="option in field.options" :key="option" :label="option" :value="option" />
</el-select>
<el-input v-else-if="field.type === 'password'" v-model="form.config[field.key]" type="password"
show-password :placeholder="field.placeholder || '留空保持原值'" />
<el-input v-else v-model="form.config[field.key]" :placeholder="field.placeholder || field.default" />
<div v-if="field.tip" class="field-tip">{{ field.tip }}</div>
</el-form-item>
<el-form-item label="到期时间">
<el-date-picker v-model="form.expire_at" type="datetime" clearable placeholder="留空为永久" style="width:100%" />
</el-form-item>
<el-form-item label="自动启动"><el-switch v-model="form.auto_start" /></el-form-item>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" /></el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="save">保存配置</el-button>
<el-button @click="router.push('/tunnels')">取消</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import api from '../api'
import { CUSTOM_FIELDS, PROTOCOL_META, protocolColor, protocolDisplay } from '../protocols'
const route = useRoute()
const router = useRouter()
const loading = ref(true)
const saving = ref(false)
const protocol = ref('')
const form = ref(null)
const fields = computed(() => PROTOCOL_META[protocol.value]?.fields || CUSTOM_FIELDS)
const visibleFields = computed(() => fields.value.filter(field => {
if (!field.showIf) return true
return Object.entries(field.showIf).every(([key, value]) => Array.isArray(value)
? value.includes(form.value.config[key])
: form.value.config[key] === value)
}))
function parseConfig(raw) {
try { return JSON.parse(raw || '{}') } catch { return {} }
}
async function load() {
try {
const { data } = await api.get(`/tunnels/${route.params.id}`)
const tunnel = data.tunnel
protocol.value = tunnel.protocol
const config = parseConfig(tunnel.config)
for (const field of (PROTOCOL_META[protocol.value]?.fields || CUSTOM_FIELDS)) {
if (config[field.key] === undefined && field.default !== undefined) config[field.key] = field.default
}
form.value = {
name: tunnel.name,
remark: tunnel.remark,
auto_start: tunnel.auto_start,
expire_at: tunnel.expire_at,
config,
}
} catch (error) {
ElMessage.error(error.response?.data?.error || '加载专线失败')
router.push('/tunnels')
} finally {
loading.value = false
}
}
async function save() {
if (!form.value.name) return ElMessage.warning('请填写专线名称')
saving.value = true
try {
await api.put(`/tunnels/${route.params.id}`, form.value)
ElMessage.success('配置已保存')
router.push('/tunnels')
} catch (error) {
ElMessage.error(error.response?.data?.error || '保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<style scoped>
.field-tip { margin-top: 4px; color: #909399; font-size: 12px; line-height: 1.4; }
</style>