const { createApp, ref, reactive, computed, onMounted } = Vue; const app = createApp({ setup() { // ===== 登录态 ===== const transitioning = ref(false); // 旋转圈显示 const contentVisible = ref(false); // 主内容淡入 const loggedIn = ref(false); const loading = ref(false); const loginError = ref(""); const loginForm = reactive({ username: "", password: "", }); // ===== Token ===== const token = ref(""); // ===== Tab ===== const activeTab = ref("proxies"); // ===== 全局配置 ===== const globalConfig = reactive({ serverAddr: "frp.whitetop.xyz", serverPort: 9358, token: "Lxh10020328", logLevel: "info", logMaxDays: 3, tcpMux: true, tcpMuxKeepalive: 30, heartbeatInterval: 15, heartbeatTimeout: 70, poolCount: 8, }); const showDetail = ref(false); // ===== 隧道列表 ===== const proxies = ref([]); const searchKeyword = ref(""); // ===== frpc 状态 ===== const frpcRunning = ref(false); // ===== 弹窗 ===== const dialogVisible = ref(false); const dialogMode = ref("add"); // 'add' | 'edit' const dialogForm = reactive({ id: null, name: "", type: "tcp", localIP: "", localPort: 0, remotePort: 0, }); // ===== 计算属性 ===== const filteredProxies = computed(() => { if (!searchKeyword.value) return proxies.value; const kw = searchKeyword.value.toLowerCase(); return proxies.value.filter( (p) => p.name.toLowerCase().includes(kw) || p.localIP.includes(kw) || String(p.remotePort).includes(kw), ); }); // ===== API 基础 ===== const apiHeaders = () => ({ "Content-Type": "application/json", Authorization: "Bearer " + token.value, }); const apiFetch = async (url, opts = {}) => { const res = await fetch(url, { ...opts, headers: { ...apiHeaders(), ...opts.headers }, }); return res.json(); }; // ===== 登录 ===== const doLogin = async () => { // 1. 校验输入 if (!loginForm.username || !loginForm.password) { loginError.value = "请输入用户名和密码"; return; } // 2. 开始登录 loading.value = true; loginError.value = ""; try { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(loginForm), }); const data = await res.json(); if (data.code === 0) { // 3. 保存 token token.value = data.data.token; // 4. 显示旋转圈 transitioning.value = true; // 5. 切换登录态 → 触发容器放大动画(0.5s) loggedIn.value = true; // 6. 等待放大动画完成(0.5s) setTimeout(async () => { // 7. 隐藏旋转圈 transitioning.value = false; // 8. 加载数据 await loadAllData(); // 9. 关闭 loading 状态 loading.value = false; // 10. 再等 0.3s 让内容淡入 setTimeout(() => { contentVisible.value = true; }, 300); }, 500); // 与 CSS 动画时间一致 } else { // 登录失败 loginError.value = data.msg || "登录失败"; loading.value = false; } } catch (e) { loginError.value = "网络错误,请重试"; loading.value = false; } }; const doLogout = () => { contentVisible.value = false; // 先隐藏内容 loggedIn.value = false; token.value = ''; loginForm.username = ''; loginForm.password = ''; loading.value = false; transitioning.value = false; }; // ===== 加载数据 ===== const loadAllData = async () => { await Promise.all([loadConfig(), loadProxies(), loadFrpcStatus()]); }; const loadConfig = async () => { try { const data = await apiFetch("/api/config"); if (data.code === 0) { Object.assign(globalConfig, data.data); } } catch (e) { console.warn("加载配置失败", e); } }; const loadProxies = async () => { try { const data = await apiFetch("/api/proxies"); if (data.code === 0) { proxies.value = data.data || []; } } catch (e) { console.warn("加载隧道列表失败", e); } }; const loadFrpcStatus = async () => { try { const data = await apiFetch("/api/frpc/status"); if (data.code === 0) { frpcRunning.value = data.data.running || false; } } catch (e) { console.warn("获取 frpc 状态失败", e); } }; // ===== 保存配置 ===== const saveConfig = async () => { try { const data = await apiFetch("/api/config", { method: "PUT", body: JSON.stringify(globalConfig), }); if (data.code === 0) { await loadFrpcStatus(); alert("配置已更新并热加载"); } else { alert("保存失败: " + data.msg); } } catch (e) { alert("网络错误"); } }; // ===== 隧道操作 ===== const toggleProxy = async (p) => { try { const data = await apiFetch("/api/proxy/" + p.id, { method: "PUT", body: JSON.stringify(p), }); if (data.code !== 0) { alert("更新失败: " + data.msg); await loadProxies(); } else { await loadFrpcStatus(); } } catch (e) { alert("网络错误"); await loadProxies(); } }; const deleteProxy = async (id) => { if (!confirm("确定要删除这条隧道吗?")) return; try { const data = await apiFetch("/api/proxy/" + id, { method: "DELETE", }); if (data.code === 0) { await loadProxies(); await loadFrpcStatus(); } else { alert("删除失败: " + data.msg); } } catch (e) { alert("网络错误"); } }; // ===== 弹窗 ===== const openAddDialog = () => { dialogMode.value = "add"; dialogForm.id = null; dialogForm.name = ""; dialogForm.type = "tcp"; dialogForm.localIP = ""; dialogForm.localPort = 0; dialogForm.remotePort = 0; dialogVisible.value = true; }; const openEditDialog = (p) => { dialogMode.value = "edit"; Object.assign(dialogForm, p); dialogVisible.value = true; }; const confirmDialog = async () => { if ( !dialogForm.name || !dialogForm.localIP || !dialogForm.localPort || !dialogForm.remotePort ) { alert("请填写完整信息"); return; } const url = dialogMode.value === "add" ? "/api/proxy" : "/api/proxy/" + dialogForm.id; const method = dialogMode.value === "add" ? "POST" : "PUT"; try { const data = await apiFetch(url, { method: method, body: JSON.stringify(dialogForm), }); if (data.code === 0) { dialogVisible.value = false; await loadProxies(); await loadFrpcStatus(); } else { alert("操作失败: " + data.msg); } } catch (e) { alert("网络错误"); } }; // ===== 导入 TOML ===== const triggerImport = () => { document.getElementById("tomlFileInput").click(); }; const handleImport = async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); try { const res = await fetch('/api/import/toml', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token.value, // 不要设置 Content-Type,浏览器会自动加上 boundary }, body: formData, }); const data = await res.json(); if (data.code === 0) { alert(data.msg); await loadAllData(); } else { alert('导入失败: ' + data.msg); } } catch (err) { alert('网络错误: ' + err.message); } e.target.value = ''; }; // ===== 导出 TOML ===== const exportToml = async () => { try { const configData = await apiFetch("/api/config"); if (configData.code !== 0) { alert("获取配置失败"); return; } const proxyData = await apiFetch("/api/proxies"); if (proxyData.code !== 0) { alert("获取隧道列表失败"); return; } const cfg = configData.data; const proxyList = proxyData.data || []; const tomlObj = { serverAddr: cfg.serverAddr, serverPort: cfg.serverPort, auth: { token: cfg.token }, log: { to: "./frpc.log", level: cfg.logLevel, maxDays: cfg.logMaxDays, }, transport: { tcpMux: cfg.tcpMux, tcpMuxKeepaliveInterval: cfg.tcpMuxKeepalive, heartbeatInterval: cfg.heartbeatInterval, heartbeatTimeout: cfg.heartbeatTimeout, poolCount: cfg.poolCount, }, proxies: proxyList.map((p) => ({ name: p.name, type: p.type, localIP: p.localIP, localPort: p.localPort, remotePort: p.remotePort, })), }; const tomlStr = TOML.stringify(tomlObj); const blob = new Blob([tomlStr], { type: "text/plain" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "frpc.toml"; a.click(); URL.revokeObjectURL(a.href); } catch (e) { alert("导出失败: " + e.message); } }; // ===== 键盘快捷键 ===== onMounted(() => { document.addEventListener("keydown", (e) => { if (e.key === "Escape" && dialogVisible.value) { dialogVisible.value = false; } }); }); // ===== 返回所有 ===== return { loggedIn, loading, loginError, loginForm, doLogin, doLogout, activeTab, globalConfig, showDetail, saveConfig, proxies, searchKeyword, filteredProxies, toggleProxy, deleteProxy, frpcRunning, dialogVisible, dialogMode, dialogForm, openAddDialog, openEditDialog, confirmDialog, triggerImport, handleImport, exportToml, transitioning, contentVisible, }; }, }); app.mount("#app");