From 17a45571137b4a94eaa1288f398d7f1b38eb981b Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Thu, 23 Jul 2026 18:40:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=83=A8=E5=88=86UI=E5=8A=A8?= =?UTF-8?q?=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/app.js | 228 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 139 insertions(+), 89 deletions(-) diff --git a/static/app.js b/static/app.js index ac5e0bd..dd9060c 100644 --- a/static/app.js +++ b/static/app.js @@ -3,26 +3,28 @@ 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 loginError = ref(""); const loginForm = reactive({ - username: '', - password: '', + username: "", + password: "", }); // ===== Token ===== - const token = ref(''); + const token = ref(""); // ===== Tab ===== - const activeTab = ref('config'); + const activeTab = ref("config"); // ===== 全局配置 ===== const globalConfig = reactive({ - serverAddr: 'frp.whitetop.xyz', + serverAddr: "frp.whitetop.xyz", serverPort: 9358, - token: 'Lxh10020328', - logLevel: 'info', + token: "Lxh10020328", + logLevel: "info", logMaxDays: 3, tcpMux: true, tcpMuxKeepalive: 30, @@ -34,19 +36,19 @@ const app = createApp({ // ===== 隧道列表 ===== const proxies = ref([]); - const searchKeyword = ref(''); + const searchKeyword = ref(""); // ===== frpc 状态 ===== const frpcRunning = ref(false); // ===== 弹窗 ===== const dialogVisible = ref(false); - const dialogMode = ref('add'); // 'add' | 'edit' + const dialogMode = ref("add"); // 'add' | 'edit' const dialogForm = reactive({ id: null, - name: '', - type: 'tcp', - localIP: '', + name: "", + type: "tcp", + localIP: "", localPort: 0, remotePort: 0, }); @@ -55,17 +57,18 @@ const app = createApp({ 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) + 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, + "Content-Type": "application/json", + Authorization: "Bearer " + token.value, }); const apiFetch = async (url, opts = {}) => { @@ -78,160 +81,196 @@ const app = createApp({ // ===== 登录 ===== const doLogin = async () => { + // 1. 校验输入 if (!loginForm.username || !loginForm.password) { - loginError.value = '请输入用户名和密码'; + loginError.value = "请输入用户名和密码"; return; } + + // 2. 开始登录 loading.value = true; - loginError.value = ''; + loginError.value = ""; + try { - const res = await fetch('/api/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + 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; - await loadAllData(); + + // 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 || '登录失败'; + // 登录失败 + loginError.value = data.msg || "登录失败"; + loading.value = false; } } catch (e) { - loginError.value = '网络错误,请重试'; + loginError.value = "网络错误,请重试"; + loading.value = false; } - 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(), - ]); + await Promise.all([loadConfig(), loadProxies(), loadFrpcStatus()]); }; const loadConfig = async () => { try { - const data = await apiFetch('/api/config'); + const data = await apiFetch("/api/config"); if (data.code === 0) { Object.assign(globalConfig, data.data); } } catch (e) { - console.warn('加载配置失败', e); + console.warn("加载配置失败", e); } }; const loadProxies = async () => { try { - const data = await apiFetch('/api/proxies'); + const data = await apiFetch("/api/proxies"); if (data.code === 0) { proxies.value = data.data || []; } } catch (e) { - console.warn('加载隧道列表失败', e); + console.warn("加载隧道列表失败", e); } }; const loadFrpcStatus = async () => { try { - const data = await apiFetch('/api/frpc/status'); + const data = await apiFetch("/api/frpc/status"); if (data.code === 0) { frpcRunning.value = data.data.running || false; } } catch (e) { - console.warn('获取 frpc 状态失败', e); + console.warn("获取 frpc 状态失败", e); } }; // ===== 保存配置 ===== const saveConfig = async () => { try { - const data = await apiFetch('/api/config', { - method: 'PUT', + const data = await apiFetch("/api/config", { + method: "PUT", body: JSON.stringify(globalConfig), }); if (data.code === 0) { await loadFrpcStatus(); - alert('配置已更新并热加载'); + alert("配置已更新并热加载"); } else { - alert('保存失败: ' + data.msg); + alert("保存失败: " + data.msg); } } catch (e) { - alert('网络错误'); + alert("网络错误"); } }; // ===== 隧道操作 ===== const toggleProxy = async (p) => { try { - const data = await apiFetch('/api/proxy/' + p.id, { - method: 'PUT', + const data = await apiFetch("/api/proxy/" + p.id, { + method: "PUT", body: JSON.stringify(p), }); if (data.code !== 0) { - alert('更新失败: ' + data.msg); + alert("更新失败: " + data.msg); await loadProxies(); } else { await loadFrpcStatus(); } } catch (e) { - alert('网络错误'); + alert("网络错误"); await loadProxies(); } }; const deleteProxy = async (id) => { - if (!confirm('确定要删除这条隧道吗?')) return; + if (!confirm("确定要删除这条隧道吗?")) return; try { - const data = await apiFetch('/api/proxy/' + id, { - method: 'DELETE', + const data = await apiFetch("/api/proxy/" + id, { + method: "DELETE", }); if (data.code === 0) { await loadProxies(); await loadFrpcStatus(); } else { - alert('删除失败: ' + data.msg); + alert("删除失败: " + data.msg); } } catch (e) { - alert('网络错误'); + alert("网络错误"); } }; // ===== 弹窗 ===== const openAddDialog = () => { - dialogMode.value = 'add'; + dialogMode.value = "add"; dialogForm.id = null; - dialogForm.name = ''; - dialogForm.type = 'tcp'; - dialogForm.localIP = ''; + dialogForm.name = ""; + dialogForm.type = "tcp"; + dialogForm.localIP = ""; dialogForm.localPort = 0; dialogForm.remotePort = 0; dialogVisible.value = true; }; const openEditDialog = (p) => { - dialogMode.value = 'edit'; + dialogMode.value = "edit"; Object.assign(dialogForm, p); dialogVisible.value = true; }; const confirmDialog = async () => { - if (!dialogForm.name || !dialogForm.localIP || !dialogForm.localPort || !dialogForm.remotePort) { - alert('请填写完整信息'); + 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'; + 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, @@ -242,16 +281,16 @@ const app = createApp({ await loadProxies(); await loadFrpcStatus(); } else { - alert('操作失败: ' + data.msg); + alert("操作失败: " + data.msg); } } catch (e) { - alert('网络错误'); + alert("网络错误"); } }; // ===== 导入 TOML ===== const triggerImport = () => { - document.getElementById('tomlFileInput').click(); + document.getElementById("tomlFileInput").click(); }; const handleImport = async (e) => { @@ -261,28 +300,33 @@ const app = createApp({ const text = await file.text(); const parsed = TOML.parse(text); if (!parsed.proxies || !Array.isArray(parsed.proxies)) { - alert('无效的 TOML 文件:缺少 proxies 数组'); + alert("无效的 TOML 文件:缺少 proxies 数组"); return; } // 过滤出有效的隧道条目 - const list = parsed.proxies.filter(p => p.name && p.localIP && p.localPort && p.remotePort); + const list = parsed.proxies.filter( + (p) => p.name && p.localIP && p.localPort && p.remotePort, + ); if (list.length === 0) { - alert('未解析到有效的隧道条目'); + alert("未解析到有效的隧道条目"); return; } - if (!confirm(`解析到 ${list.length} 条隧道,将覆盖当前配置,确认导入?`)) return; + if ( + !confirm(`解析到 ${list.length} 条隧道,将覆盖当前配置,确认导入?`) + ) + return; // 批量导入:先清空,再逐条插入(后端未提供批量接口,用循环) // 简单做法:先删除所有,再逐个创建 for (const p of proxies.value) { - await apiFetch('/api/proxy/' + p.id, { method: 'DELETE' }); + await apiFetch("/api/proxy/" + p.id, { method: "DELETE" }); } for (const p of list) { - await apiFetch('/api/proxy', { - method: 'POST', + await apiFetch("/api/proxy", { + method: "POST", body: JSON.stringify({ name: p.name, - type: p.type || 'tcp', + type: p.type || "tcp", localIP: p.localIP, localPort: p.localPort, remotePort: p.remotePort, @@ -292,24 +336,24 @@ const app = createApp({ } await loadProxies(); await loadFrpcStatus(); - alert('导入成功!'); + alert("导入成功!"); } catch (err) { - alert('解析 TOML 失败: ' + err.message); + alert("解析 TOML 失败: " + err.message); } - e.target.value = ''; + e.target.value = ""; }; // ===== 导出 TOML ===== const exportToml = async () => { try { - const configData = await apiFetch('/api/config'); + const configData = await apiFetch("/api/config"); if (configData.code !== 0) { - alert('获取配置失败'); + alert("获取配置失败"); return; } - const proxyData = await apiFetch('/api/proxies'); + const proxyData = await apiFetch("/api/proxies"); if (proxyData.code !== 0) { - alert('获取隧道列表失败'); + alert("获取隧道列表失败"); return; } const cfg = configData.data; @@ -319,7 +363,11 @@ const app = createApp({ serverAddr: cfg.serverAddr, serverPort: cfg.serverPort, auth: { token: cfg.token }, - log: { to: './frpc.log', level: cfg.logLevel, maxDays: cfg.logMaxDays }, + log: { + to: "./frpc.log", + level: cfg.logLevel, + maxDays: cfg.logMaxDays, + }, transport: { tcpMux: cfg.tcpMux, tcpMuxKeepaliveInterval: cfg.tcpMuxKeepalive, @@ -327,7 +375,7 @@ const app = createApp({ heartbeatTimeout: cfg.heartbeatTimeout, poolCount: cfg.poolCount, }, - proxies: proxyList.map(p => ({ + proxies: proxyList.map((p) => ({ name: p.name, type: p.type, localIP: p.localIP, @@ -336,21 +384,21 @@ const app = createApp({ })), }; const tomlStr = TOML.stringify(tomlObj); - const blob = new Blob([tomlStr], { type: 'text/plain' }); - const a = document.createElement('a'); + const blob = new Blob([tomlStr], { type: "text/plain" }); + const a = document.createElement("a"); a.href = URL.createObjectURL(blob); - a.download = 'frpc.toml'; + a.download = "frpc.toml"; a.click(); URL.revokeObjectURL(a.href); } catch (e) { - alert('导出失败: ' + e.message); + alert("导出失败: " + e.message); } }; // ===== 键盘快捷键 ===== onMounted(() => { - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && dialogVisible.value) { + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && dialogVisible.value) { dialogVisible.value = false; } }); @@ -383,8 +431,10 @@ const app = createApp({ triggerImport, handleImport, exportToml, + transitioning, + contentVisible, }; }, }); -app.mount('#app'); \ No newline at end of file +app.mount("#app");