// 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() { try { const configData = await apiFetch('/config'); if (configData.code !== 0) { alert('获取配置失败'); return; } const proxyData = await apiFetch('/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); } } // ============================================================ // 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');