const { createApp, ref, reactive, computed, onMounted } = Vue; const app = createApp({ setup() { // ===== 登录态 ===== const loggedIn = ref(false); const loading = ref(false); const loginError = ref(''); const loginForm = reactive({ username: '', password: '', }); // ===== Token ===== const token = ref(''); // ===== Tab ===== const activeTab = ref('config'); // ===== 全局配置 ===== 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 () => { if (!loginForm.username || !loginForm.password) { loginError.value = '请输入用户名和密码'; return; } 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) { token.value = data.data.token; loggedIn.value = true; await loadAllData(); } else { loginError.value = data.msg || '登录失败'; } } catch (e) { loginError.value = '网络错误,请重试'; } loading.value = false; }; const doLogout = () => { loggedIn.value = false; token.value = ''; loginForm.username = ''; loginForm.password = ''; }; // ===== 加载数据 ===== 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; try { const text = await file.text(); const parsed = TOML.parse(text); if (!parsed.proxies || !Array.isArray(parsed.proxies)) { alert('无效的 TOML 文件:缺少 proxies 数组'); return; } // 过滤出有效的隧道条目 const list = parsed.proxies.filter(p => p.name && p.localIP && p.localPort && p.remotePort); if (list.length === 0) { alert('未解析到有效的隧道条目'); return; } if (!confirm(`解析到 ${list.length} 条隧道,将覆盖当前配置,确认导入?`)) return; // 批量导入:先清空,再逐条插入(后端未提供批量接口,用循环) // 简单做法:先删除所有,再逐个创建 for (const p of proxies.value) { await apiFetch('/api/proxy/' + p.id, { method: 'DELETE' }); } for (const p of list) { await apiFetch('/api/proxy', { method: 'POST', body: JSON.stringify({ name: p.name, type: p.type || 'tcp', localIP: p.localIP, localPort: p.localPort, remotePort: p.remotePort, enabled: p.enabled !== undefined ? p.enabled : true, }), }); } await loadProxies(); await loadFrpcStatus(); alert('导入成功!'); } catch (err) { alert('解析 TOML 失败: ' + 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, }; }, }); app.mount('#app');