// app.js - 普通脚本(非模块) const { createApp, ref, reactive, computed, onMounted } = Vue; // ============================================================ // 1. 把 modules/*.js 的内容直接合并进来(因为是普通脚本,不能 import) // ============================================================ // ---------- api ---------- const API_BASE = "/api"; function getToken() { return localStorage.getItem("frpc_token") || ""; } function getHeaders() { return { "Content-Type": "application/json", Authorization: "Bearer " + getToken(), }; } async function apiFetch(endpoint, options = {}) { const url = API_BASE + endpoint; const headers = options.body instanceof FormData ? { Authorization: "Bearer " + getToken() } : getHeaders(); const res = await fetch(url, { ...options, headers: { ...headers, ...(options.headers || {}) }, }); return res.json(); } // ---------- auth ---------- function parseJwtExpire(token) { try { const payload = JSON.parse(atob(token.split(".")[1])); return payload.exp * 1000; } catch { return Date.now() + 7 * 24 * 60 * 60 * 1000; } } function saveAuthState(token, username) { const expire = parseJwtExpire(token); localStorage.setItem("frpc_token", token); localStorage.setItem("frpc_token_expire", String(expire)); localStorage.setItem("frpc_username", username); return expire; } function loadAuthState() { const token = localStorage.getItem("frpc_token"); const expire = parseInt(localStorage.getItem("frpc_token_expire") || "0", 10); const username = localStorage.getItem("frpc_username") || ""; if (token && expire > Date.now()) { return { token, expire, username, valid: true }; } clearAuthState(); return { token: "", expire: 0, username: "", valid: false }; } function clearAuthState() { localStorage.removeItem("frpc_token"); localStorage.removeItem("frpc_token_expire"); localStorage.removeItem("frpc_username"); } async function login(username, password) { const data = await apiFetch("/login", { method: "POST", body: JSON.stringify({ username, password }), }); if (data.code === 0) { const token = data.data.token; const expire = saveAuthState(token, username); return { success: true, token, expire }; } return { success: false, error: data.msg || "登录失败" }; } // ---------- config ---------- const defaultConfig = { serverAddr: "frp.whitetop.xyz", serverPort: 9358, token: "", logLevel: "info", logMaxDays: 3, tcpMux: true, tcpMuxKeepalive: 30, heartbeatInterval: 15, heartbeatTimeout: 70, poolCount: 8, }; async function loadConfig() { try { const data = await apiFetch("/config"); if (data.code === 0) { return { ...defaultConfig, ...data.data }; } } catch (e) { console.warn("加载配置失败", e); } return { ...defaultConfig }; } async function saveConfigApi(config) { const data = await apiFetch("/config", { method: "PUT", body: JSON.stringify(config), }); return data; } // ---------- proxies ---------- async function loadProxies() { try { const data = await apiFetch("/proxies"); if (data.code === 0) { return data.data || []; } } catch (e) { console.warn("加载隧道失败", e); } return []; } async function createProxy(proxy) { const data = await apiFetch("/proxy", { method: "POST", body: JSON.stringify(proxy), }); return data; } async function updateProxy(proxy) { const data = await apiFetch("/proxy/" + proxy.id, { method: "PUT", body: JSON.stringify(proxy), }); return data; } async function deleteProxy(id) { const data = await apiFetch("/proxy/" + id, { method: "DELETE", }); return data; } function filterProxies(list, keyword) { if (!keyword) return list; const kw = keyword.toLowerCase(); return list.filter( (p) => p.name.toLowerCase().includes(kw) || p.localIP.includes(kw) || String(p.remotePort).includes(kw), ); } // ---------- frpc ---------- async function getFrpcStatus() { try { const data = await apiFetch("/frpc/status"); if (data.code === 0) { return data.data.running || false; } } catch (e) { console.warn("获取 frpc 状态失败", e); } return false; } async function reloadFrpc() { const data = await apiFetch("/frpc/reload", { method: "POST" }); return data; } async function startFrpc() { const data = await apiFetch("/frpc/start", { method: "POST" }); return data; } async function stopFrpc() { const data = await apiFetch("/frpc/stop", { method: "POST" }); return data; } // ---------- ui ---------- // Tab 切换 const activeTab = ref("proxies"); function switchTab(tab) { activeTab.value = tab; } // 弹窗控制 const dialogVisible = ref(false); const dialogMode = ref("add"); const dialogForm = ref({ id: null, name: "", type: "tcp", localIP: "", localPort: 0, remotePort: 0, }); function openAddDialog() { dialogMode.value = "add"; dialogForm.value = { id: null, name: "", type: "tcp", localIP: "", localPort: 0, remotePort: 0, }; dialogVisible.value = true; } function openEditDialog(proxy) { dialogMode.value = "edit"; dialogForm.value = { ...proxy }; dialogVisible.value = true; } function closeDialog() { dialogVisible.value = false; } const searchKeyword = ref(""); // ---------- import/export 功能 ---------- async function triggerImport() { document.getElementById("tomlFileInput").click(); } async function handleImport(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 " + getToken() }, 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 = ""; } async function exportToml() { console.log('🔍 exportToml 被调用了'); try { const res = await fetch('/api/export/toml', { method: 'GET', headers: { 'Authorization': 'Bearer ' + getToken() }, }); if (!res.ok) { const data = await res.json(); alert('导出失败: ' + (data.msg || '未知错误')); return; } const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'frpc.toml'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); console.log('✅ 导出成功'); } catch (e) { console.error('导出失败:', e); alert('导出失败: ' + e.message); } } // ============================================================ // 2. Vue 应用 // ============================================================ const app = createApp({ setup() { // ===== 登录态 ===== const loggedIn = ref(false); const loading = ref(false); const loginError = ref(""); const loginForm = reactive({ username: "", password: "" }); const token = ref(""); const tokenExpireTime = ref(0); const transitioning = ref(false); const contentVisible = ref(false); let expireTimer = null; // ===== 数据 ===== const globalConfig = reactive({}); const proxies = ref([]); const frpcRunning = ref(false); const showDetail = ref(false); // ===== 初始化 ===== onMounted(() => { const saved = loadAuthState(); if (saved.valid) { token.value = saved.token; tokenExpireTime.value = saved.expire; loginForm.username = saved.username; loggedIn.value = true; loadAllData().then(() => { contentVisible.value = true; startExpireTimer(); }); } }); const startExpireTimer = () => { if (expireTimer) clearInterval(expireTimer); expireTimer = setInterval(() => { if (Date.now() >= tokenExpireTime.value) { doLogout(); alert("登录已超时,请重新登录"); } }, 5000); }; const loadAllData = async () => { const [cfg, list, running] = await Promise.all([ loadConfig(), loadProxies(), getFrpcStatus(), ]); Object.assign(globalConfig, cfg); proxies.value = list; frpcRunning.value = running; }; // ===== 登录/退出 ===== const doLogin = async () => { if (!loginForm.username || !loginForm.password) { loginError.value = "请输入用户名和密码"; return; } loading.value = true; loginError.value = ""; const result = await login(loginForm.username, loginForm.password); if (result.success) { token.value = result.token; tokenExpireTime.value = result.expire; transitioning.value = true; loggedIn.value = true; setTimeout(async () => { transitioning.value = false; await loadAllData(); loading.value = false; setTimeout(() => { contentVisible.value = true; startExpireTimer(); }, 300); }, 500); } else { loginError.value = result.error; loading.value = false; } }; const doLogout = () => { if (expireTimer) clearInterval(expireTimer); contentVisible.value = false; loggedIn.value = false; token.value = ""; tokenExpireTime.value = 0; loginForm.username = ""; loginForm.password = ""; loading.value = false; transitioning.value = false; clearAuthState(); }; // ===== 保存配置 ===== const saveConfig = async () => { const result = await saveConfigApi(globalConfig); if (result.code === 0) { frpcRunning.value = await getFrpcStatus(); alert("配置已更新并热加载"); } else { alert("保存失败: " + result.msg); } }; // ===== 隧道操作 ===== const toggleProxy = async (p) => { const result = await updateProxy(p); if (result.code !== 0) { alert("更新失败: " + result.msg); proxies.value = await loadProxies(); } else { frpcRunning.value = await getFrpcStatus(); } }; const removeProxy = async (id) => { console.log("🔍 removeProxy 被调用, id:", id); // ← 加这行 if (!confirm("确定要删除这条隧道吗?")) return; const result = await deleteProxy(id); if (result.code === 0) { proxies.value = await loadProxies(); frpcRunning.value = await getFrpcStatus(); } else { alert("删除失败: " + result.msg); } }; const confirmDialog = async () => { const form = dialogForm.value; if (!form.name || !form.localIP || !form.localPort || !form.remotePort) { alert("请填写完整信息"); return; } const result = dialogMode.value === "add" ? await createProxy(form) : await updateProxy(form); if (result.code === 0) { closeDialog(); proxies.value = await loadProxies(); frpcRunning.value = await getFrpcStatus(); } else { alert("操作失败: " + result.msg); } }; // ===== 计算属性 ===== const filteredProxies = computed(() => filterProxies(proxies.value, searchKeyword.value), ); return { loggedIn, loading, loginError, loginForm, doLogin, doLogout, activeTab, switchTab, globalConfig, showDetail, saveConfig, proxies, filteredProxies, toggleProxy, removeProxy, frpcRunning, dialogVisible, dialogMode, dialogForm, openAddDialog, openEditDialog, closeDialog, confirmDialog, searchKeyword, transitioning, contentVisible, triggerImport, handleImport, exportToml, }; }, }); app.mount("#app");