refactor(vnt-web): 前端重构为 Vite+Vue3+Pinia 工程并重新设计 UI
- 单文件 index.html 拆分为 vnt-web/ui(pnpm+Vite+Vue3 SFC+Pinia+Tailwind v4),构建产物输出到 static/ - UI 重新设计:浅色简洁风(可切换深色模式)、顶部导航、新增总览仪表盘首页 - 自定义弹窗/toast/确认框替换全部原生 alert/confirm,交互与过渡统一 - 响应式布局:移动端汉堡导航,表格与卡片自适应 - 实例卡片移除日志按钮;启动组网默认选中第一个配置
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
import AppModal from "../components/AppModal.vue";
|
||||
import { useUiStore } from "../stores/ui";
|
||||
import { getConfig, saveConfig } from "../api";
|
||||
import { emptyFormData, parseTomlToForm, formToToml, NEW_CONFIG_TEMPLATE } from "../utils/toml";
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
// null 表示新建
|
||||
fileName: { type: String, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "saved"]);
|
||||
|
||||
const ui = useUiStore();
|
||||
|
||||
const editorContent = ref("");
|
||||
const editorFileName = ref("");
|
||||
const editorMode = ref("new"); // 'new' 或 'edit'
|
||||
const editMode = ref("form"); // 'form' 或 'toml'
|
||||
const originalToml = ref(""); // 保存原始TOML内容(包含用户注释)
|
||||
const hasTomlChanges = ref(false);
|
||||
const hasFormChanges = ref(false);
|
||||
const isParsingToml = ref(false);
|
||||
const formData = ref(emptyFormData());
|
||||
|
||||
// 打开时加载内容
|
||||
watch(
|
||||
() => props.show,
|
||||
async (val) => {
|
||||
if (!val) return;
|
||||
editorFileName.value = props.fileName || "";
|
||||
editorMode.value = props.fileName ? "edit" : "new";
|
||||
editMode.value = "form"; // 默认表单模式
|
||||
hasTomlChanges.value = false;
|
||||
hasFormChanges.value = false;
|
||||
|
||||
if (props.fileName) {
|
||||
try {
|
||||
const data = await getConfig(props.fileName);
|
||||
editorContent.value = data;
|
||||
originalToml.value = data; // 保存原始TOML
|
||||
isParsingToml.value = true;
|
||||
formData.value = parseTomlToForm(data);
|
||||
nextTick(() => {
|
||||
isParsingToml.value = false;
|
||||
});
|
||||
} catch (e) {
|
||||
ui.toast.error("获取配置失败: " + e.message);
|
||||
emit("close");
|
||||
}
|
||||
} else {
|
||||
// 新建配置,初始化表单
|
||||
originalToml.value = "";
|
||||
formData.value = emptyFormData();
|
||||
editorContent.value = NEW_CONFIG_TEMPLATE;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 切换到表单模式
|
||||
const switchToFormMode = () => {
|
||||
if (editMode.value === "toml") {
|
||||
editMode.value = "form";
|
||||
// 从TOML解析到表单
|
||||
isParsingToml.value = true;
|
||||
formData.value = parseTomlToForm(editorContent.value);
|
||||
nextTick(() => {
|
||||
isParsingToml.value = false;
|
||||
});
|
||||
} else {
|
||||
editMode.value = "form";
|
||||
}
|
||||
};
|
||||
|
||||
// 切换到TOML模式
|
||||
const switchToTomlMode = () => {
|
||||
if (editMode.value === "form") {
|
||||
// 如果表单被修改过,生成新的TOML
|
||||
if (hasFormChanges.value) {
|
||||
editorContent.value = formToToml(formData.value);
|
||||
hasFormChanges.value = false;
|
||||
} else if (originalToml.value && !hasTomlChanges.value) {
|
||||
// 表单未修改且TOML未修改,使用原始TOML(保留用户注释)
|
||||
editorContent.value = originalToml.value;
|
||||
} else {
|
||||
editorContent.value = formToToml(formData.value);
|
||||
}
|
||||
}
|
||||
editMode.value = "toml";
|
||||
};
|
||||
|
||||
// 监听TOML内容变化(只在TOML模式下)
|
||||
watch(editorContent, (newVal, oldVal) => {
|
||||
if (editMode.value === "toml" && oldVal !== undefined) {
|
||||
hasTomlChanges.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听表单数据变化
|
||||
watch(
|
||||
formData,
|
||||
() => {
|
||||
if (editMode.value === "form" && props.show && !isParsingToml.value) {
|
||||
hasFormChanges.value = true;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 表单模式先转换为TOML
|
||||
let configContent = editorContent.value;
|
||||
if (editMode.value === "form") {
|
||||
// 验证必填项
|
||||
if (!formData.value.network_code.trim()) {
|
||||
ui.toast.error("请填写网络编号");
|
||||
return;
|
||||
}
|
||||
const servers = formData.value.server.filter((s) => s.trim());
|
||||
if (servers.length === 0) {
|
||||
ui.toast.error("请至少填写一个服务器地址");
|
||||
return;
|
||||
}
|
||||
configContent = formToToml(formData.value);
|
||||
}
|
||||
|
||||
await saveConfig(editorFileName.value || null, configContent);
|
||||
ui.toast.success("配置已保存");
|
||||
emit("close");
|
||||
emit("saved");
|
||||
} catch (e) {
|
||||
ui.toast.error("保存失败: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 表单分组内的小型重复控件
|
||||
const checkboxClass =
|
||||
"h-5 w-5 rounded border-slate-300 bg-white text-indigo-600 focus:ring-indigo-500 dark:border-slate-600 dark:bg-slate-700";
|
||||
const toggleLabelClass =
|
||||
"flex cursor-pointer items-center justify-between gap-2 rounded-lg border border-slate-200 bg-slate-50 p-3 transition-colors hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-800/50 dark:hover:bg-slate-800";
|
||||
const removeBtnClass =
|
||||
"shrink-0 rounded-lg bg-red-50 px-3 py-2 text-red-500 transition-colors hover:bg-red-100 dark:bg-red-900/20 dark:text-red-500 dark:hover:bg-red-900/40";
|
||||
const addBtnClass =
|
||||
"flex w-full items-center justify-center gap-1 rounded-lg bg-slate-100 px-3 py-2 text-sm text-slate-600 transition-colors hover:bg-slate-200 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700";
|
||||
const sectionTitleClass = "text-md mb-4 flex items-center font-bold text-slate-900 dark:text-white";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppModal
|
||||
:show="show"
|
||||
:mask-closable="false"
|
||||
panel-class="w-full max-w-6xl h-[85vh]"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white">
|
||||
{{ editorMode === "new" ? "新建配置" : "编辑配置" }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 模式切换按钮 -->
|
||||
<div class="flex rounded-lg border border-slate-300 bg-slate-100 p-1 dark:border-slate-600 dark:bg-slate-800">
|
||||
<button
|
||||
@click="switchToFormMode"
|
||||
:class="editMode === 'form' ? 'bg-white text-slate-900 shadow-sm dark:bg-indigo-600 dark:text-white' : 'text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white'"
|
||||
class="px-4 py-1.5 rounded text-sm font-medium transition-colors flex items-center"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
表单模式
|
||||
</button>
|
||||
<button
|
||||
@click="switchToTomlMode"
|
||||
:class="editMode === 'toml' ? 'bg-white text-slate-900 shadow-sm dark:bg-indigo-600 dark:text-white' : 'text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white'"
|
||||
class="px-4 py-1.5 rounded text-sm font-medium transition-colors flex items-center"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
/>
|
||||
</svg>
|
||||
TOML模式
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm text-slate-500 font-mono hidden md:block" v-if="editorFileName">
|
||||
{{ editorFileName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<!-- 表单模式 -->
|
||||
<div v-show="editMode === 'form'" class="h-full overflow-y-auto scrollbar-hide p-6">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<!-- 基础配置 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
基础配置
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">配置名称</label>
|
||||
<input v-model="formData.config_name" type="text" placeholder="例如: 我的VPN配置" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||
网络编号 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input v-model="formData.network_code" type="text" placeholder="例如: my_network" required class="input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||
服务器地址 <span class="text-red-500">*</span>
|
||||
<span class="text-xs text-slate-500 ml-2">支持 quic:// tcp:// wss:// dynamic://</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(server, idx) in formData.server" :key="idx" class="flex gap-2">
|
||||
<input v-model="formData.server[idx]" type="text" placeholder="例如: quic://1.2.3.4:29872" class="input flex-1" />
|
||||
<button @click="formData.server.splice(idx, 1)" v-if="formData.server.length > 1" :class="removeBtnClass">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="formData.server.push('')"
|
||||
:class="addBtnClass"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
添加服务器
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网络设置 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
|
||||
/>
|
||||
</svg>
|
||||
网络设置
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||
自定义虚拟IP
|
||||
<span class="text-xs text-slate-500 ml-1">(可选)</span>
|
||||
</label>
|
||||
<input v-model="formData.ip" type="text" placeholder="例如: 10.26.0.2" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">MTU</label>
|
||||
<input v-model.number="formData.mtu" type="number" placeholder="1380" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">隧道端口</label>
|
||||
<input v-model.number="formData.tunnel_port" type="number" placeholder="0 (自动分配)" class="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传输优化 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
传输优化
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">QUIC传输优化</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">重传丢包</div>
|
||||
</div>
|
||||
<input v-model="formData.rtx" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">FEC前向纠错</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">损失部分带宽提升稳定性</div>
|
||||
</div>
|
||||
<input v-model="formData.fec" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">LZ4压缩</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">减少传输数据量</div>
|
||||
</div>
|
||||
<input v-model="formData.compress" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">关闭P2P打洞</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">仅通过服务器中转</div>
|
||||
</div>
|
||||
<input v-model="formData.no_punch" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 安全配置 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
安全配置
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300"
|
||||
>组网加密密码(连接公共服务器时建议填写,同一组网密码需要相同)</label
|
||||
>
|
||||
<input v-model="formData.password" type="password" placeholder="留空则不加密" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">服务端证书校验模式</label>
|
||||
<select v-model="formData.cert_mode" class="input">
|
||||
<option value="skip">跳过验证 (默认)</option>
|
||||
<option value="standard">系统证书验证</option>
|
||||
<option value="finger">证书指纹验证</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="formData.cert_mode === 'finger'">
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||
证书指纹
|
||||
<span class="text-xs text-slate-500 ml-1">(服务端启动时日志会输出指纹)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="formData.fingerprint"
|
||||
type="text"
|
||||
placeholder="例如: 3bdd8675606837cdf95d5e13445606315762315a78555f9da652940a25feaec1"
|
||||
class="input font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NAT与路由 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 01-.806-.98L15 9m0 0V7m0 2v6"
|
||||
/>
|
||||
</svg>
|
||||
NAT与路由 (点对网)
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||
入栈网段
|
||||
<span class="text-xs text-slate-500 ml-1">格式: CIDR,目标IP</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(item, idx) in formData.input" :key="idx" class="flex gap-2">
|
||||
<input v-model="formData.input[idx]" type="text" placeholder="例如: 192.168.0.0/24,10.26.0.2" class="input flex-1" />
|
||||
<button @click="formData.input.splice(idx, 1)" :class="removeBtnClass">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="formData.input.push('')"
|
||||
:class="addBtnClass"
|
||||
>
|
||||
+ 添加入栈网段
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||
出栈网段
|
||||
<span class="text-xs text-slate-500 ml-1">格式: CIDR (允许转发的网段)</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(item, idx) in formData.output" :key="idx" class="flex gap-2">
|
||||
<input v-model="formData.output[idx]" type="text" placeholder="例如: 0.0.0.0/0" class="input flex-1" />
|
||||
<button @click="formData.output.splice(idx, 1)" :class="removeBtnClass">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="formData.output.push('')"
|
||||
:class="addBtnClass"
|
||||
>
|
||||
+ 添加出栈网段
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">关闭内置NAT</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">使用系统网卡转发</div>
|
||||
</div>
|
||||
<input v-model="formData.no_nat" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">关闭TUN网卡</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">仅作流量出口或端口映射</div>
|
||||
</div>
|
||||
<input v-model="formData.no_tun" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 端口映射 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
端口映射
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">
|
||||
映射规则
|
||||
<span class="text-xs text-slate-500 ml-1">格式: 协议://监听地址-虚拟IP-目标地址</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(item, idx) in formData.port_mapping" :key="idx" class="flex gap-2">
|
||||
<input
|
||||
v-model="formData.port_mapping[idx]"
|
||||
type="text"
|
||||
placeholder="例如: tcp://0.0.0.0:81-10.0.0.2-10.0.0.2:80"
|
||||
class="input flex-1 font-mono"
|
||||
/>
|
||||
<button @click="formData.port_mapping.splice(idx, 1)" :class="removeBtnClass">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="formData.port_mapping.push('')"
|
||||
:class="addBtnClass"
|
||||
>
|
||||
+ 添加映射规则
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label :class="toggleLabelClass">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-slate-800 dark:text-white">允许作为映射出口</div>
|
||||
<div class="text-xs text-slate-400 mt-0.5">允许其他设备使用本机作跳板</div>
|
||||
</div>
|
||||
<input v-model="formData.allow_mapping" type="checkbox" :class="checkboxClass" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备配置 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
设备配置
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">设备名称</label>
|
||||
<input v-model="formData.device_name" type="text" placeholder="默认为主机名" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">设备ID</label>
|
||||
<input v-model="formData.device_id" type="text" placeholder="自动生成" class="input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">虚拟网卡名</label>
|
||||
<input v-model="formData.tun_name" type="text" placeholder="默认为vnt-tun" class="input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STUN配置 -->
|
||||
<div class="card">
|
||||
<h4 :class="sectionTitleClass">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
STUN配置 (高级)
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">UDP STUN服务器</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(item, idx) in formData.udp_stun" :key="idx" class="flex gap-2">
|
||||
<input v-model="formData.udp_stun[idx]" type="text" placeholder="例如: stun.l.google.com:19302" class="input flex-1" />
|
||||
<button @click="formData.udp_stun.splice(idx, 1)" :class="removeBtnClass">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="formData.udp_stun.push('')"
|
||||
:class="addBtnClass"
|
||||
>
|
||||
+ 添加UDP STUN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-slate-600 dark:text-slate-300">TCP STUN服务器</label>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(item, idx) in formData.tcp_stun" :key="idx" class="flex gap-2">
|
||||
<input v-model="formData.tcp_stun[idx]" type="text" placeholder="例如: stun.nextcloud.com:443" class="input flex-1" />
|
||||
<button @click="formData.tcp_stun.splice(idx, 1)" :class="removeBtnClass">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="formData.tcp_stun.push('')"
|
||||
:class="addBtnClass"
|
||||
>
|
||||
+ 添加TCP STUN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOML模式 -->
|
||||
<div v-show="editMode === 'toml'" class="h-full">
|
||||
<textarea
|
||||
v-model="editorContent"
|
||||
class="h-full w-full resize-none bg-slate-50 p-4 font-mono text-sm text-slate-800 focus:outline-none dark:bg-slate-950 dark:text-slate-200"
|
||||
spellcheck="false"
|
||||
placeholder="# 在此处输入 TOML 配置..."
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex-1 text-left text-xs text-slate-500">
|
||||
<span v-if="editMode === 'form'">填写完成后保存即可生成配置文件</span>
|
||||
<span v-else>* 请使用标准 TOML 格式</span>
|
||||
</div>
|
||||
<button class="btn-ghost" @click="emit('close')">取消</button>
|
||||
<button class="btn-primary" @click="handleSave">保存配置</button>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user