数据被迫合并

This commit is contained in:
2026-07-23 21:00:27 +08:00
parent 5fa1b99c5d
commit 00b769b338
12 changed files with 302 additions and 332 deletions
+289 -15
View File
@@ -1,19 +1,288 @@
// app.js
import { createApp, ref, reactive, computed, onMounted } from 'vue';
// app.js - 普通脚本(非模块)
// 导入各模块
import { apiFetch } from './modules/api.js';
import { login, loadAuthState, clearAuthState, saveAuthState } from './modules/auth.js';
import { loadConfig, saveConfig as saveConfigApi } from './modules/config.js';
import { loadProxies, createProxy, updateProxy, deleteProxy, filterProxies } from './modules/proxies.js';
import { getFrpcStatus, reloadFrpc, startFrpc, stopFrpc } from './modules/frpc.js';
import {
activeTab, switchTab,
dialogVisible, dialogMode, dialogForm,
openAddDialog, openEditDialog, closeDialog,
searchKeyword,
} from './modules/ui.js';
import { importToml, exportToml } from './modules/import.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() {
@@ -32,6 +301,7 @@ const app = createApp({
const globalConfig = reactive({});
const proxies = ref([]);
const frpcRunning = ref(false);
const showDetail = ref(false);
// ===== 初始化 =====
onMounted(() => {
@@ -175,6 +445,7 @@ const app = createApp({
activeTab,
switchTab,
globalConfig,
showDetail,
saveConfig,
proxies,
filteredProxies,
@@ -191,6 +462,9 @@ const app = createApp({
searchKeyword,
transitioning,
contentVisible,
triggerImport,
handleImport,
exportToml,
};
},
});