部分重型模块的原子化拆解
This commit is contained in:
+119
-340
@@ -1,389 +1,170 @@
|
||||
const { createApp, ref, reactive, computed, onMounted } = Vue;
|
||||
// app.js
|
||||
import { createApp, ref, reactive, computed, onMounted } from 'vue';
|
||||
|
||||
// 导入各模块
|
||||
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 app = createApp({
|
||||
setup() {
|
||||
// ===== 登录态 =====
|
||||
const transitioning = ref(false); // 旋转圈显示
|
||||
const contentVisible = ref(false); // 主内容淡入
|
||||
const loggedIn = ref(false);
|
||||
const loading = ref(false);
|
||||
const loginError = ref("");
|
||||
const loginForm = reactive({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
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;
|
||||
|
||||
// ===== Token =====
|
||||
const token = ref("");
|
||||
|
||||
// ===== Tab =====
|
||||
const activeTab = ref("proxies");
|
||||
|
||||
// ===== 全局配置 =====
|
||||
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 globalConfig = reactive({});
|
||||
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,
|
||||
// ===== 初始化 =====
|
||||
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 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 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 () => {
|
||||
// 1. 校验输入
|
||||
if (!loginForm.username || !loginForm.password) {
|
||||
loginError.value = "请输入用户名和密码";
|
||||
loginError.value = '请输入用户名和密码';
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 开始登录
|
||||
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) {
|
||||
// 3. 保存 token
|
||||
token.value = data.data.token;
|
||||
|
||||
// 4. 显示旋转圈
|
||||
transitioning.value = true;
|
||||
|
||||
// 5. 切换登录态 → 触发容器放大动画(0.5s)
|
||||
loggedIn.value = true;
|
||||
|
||||
// 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 = '';
|
||||
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;
|
||||
}
|
||||
} catch (e) {
|
||||
loginError.value = "网络错误,请重试";
|
||||
setTimeout(() => {
|
||||
contentVisible.value = true;
|
||||
startExpireTimer();
|
||||
}, 300);
|
||||
}, 500);
|
||||
} else {
|
||||
loginError.value = result.error;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const doLogout = () => {
|
||||
contentVisible.value = false; // 先隐藏内容
|
||||
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;
|
||||
};
|
||||
|
||||
// ===== 加载数据 =====
|
||||
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);
|
||||
}
|
||||
clearAuthState();
|
||||
};
|
||||
|
||||
// ===== 保存配置 =====
|
||||
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 result = await saveConfigApi(globalConfig);
|
||||
if (result.code === 0) {
|
||||
frpcRunning.value = await getFrpcStatus();
|
||||
alert('配置已更新并热加载');
|
||||
} else {
|
||||
alert('保存失败: ' + result.msg);
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 隧道操作 =====
|
||||
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 result = await updateProxy(p);
|
||||
if (result.code !== 0) {
|
||||
alert('更新失败: ' + result.msg);
|
||||
proxies.value = await loadProxies();
|
||||
} else {
|
||||
frpcRunning.value = await getFrpcStatus();
|
||||
}
|
||||
};
|
||||
|
||||
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 removeProxy = async (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 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("请填写完整信息");
|
||||
const form = dialogForm.value;
|
||||
if (!form.name || !form.localIP || !form.localPort || !form.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("网络错误");
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 导入 TOML =====
|
||||
const triggerImport = () => {
|
||||
document.getElementById("tomlFileInput").click();
|
||||
};
|
||||
// ===== 计算属性 =====
|
||||
const filteredProxies = computed(() => filterProxies(proxies.value, searchKeyword.value));
|
||||
|
||||
const handleImport = async (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 ' + token.value,
|
||||
// 不要设置 Content-Type,浏览器会自动加上 boundary
|
||||
},
|
||||
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 = '';
|
||||
};
|
||||
|
||||
// ===== 导出 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,
|
||||
@@ -392,28 +173,26 @@ const app = createApp({
|
||||
doLogin,
|
||||
doLogout,
|
||||
activeTab,
|
||||
switchTab,
|
||||
globalConfig,
|
||||
showDetail,
|
||||
saveConfig,
|
||||
proxies,
|
||||
searchKeyword,
|
||||
filteredProxies,
|
||||
toggleProxy,
|
||||
deleteProxy,
|
||||
removeProxy,
|
||||
frpcRunning,
|
||||
dialogVisible,
|
||||
dialogMode,
|
||||
dialogForm,
|
||||
openAddDialog,
|
||||
openEditDialog,
|
||||
closeDialog,
|
||||
confirmDialog,
|
||||
triggerImport,
|
||||
handleImport,
|
||||
exportToml,
|
||||
searchKeyword,
|
||||
transitioning,
|
||||
contentVisible,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
app.mount("#app");
|
||||
app.mount('#app');
|
||||
+3
-1
@@ -13,7 +13,9 @@
|
||||
<!-- TOML 解析库(用于导入/导出) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@iarna/toml@3.0.0/dist/toml.min.js"></script>
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<link rel="stylesheet" href="/static/style-1.css" />
|
||||
<link rel="stylesheet" href="/static/style-2.css" />
|
||||
<link rel="stylesheet" href="/static/style-3.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// modules/api.js
|
||||
const API_BASE = '/api';
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem('frpc_token') || '';
|
||||
}
|
||||
|
||||
export function getHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + getToken(),
|
||||
};
|
||||
}
|
||||
|
||||
export 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();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// modules/auth.js
|
||||
import { apiFetch } from './api.js';
|
||||
|
||||
export 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;
|
||||
}
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export 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 };
|
||||
}
|
||||
|
||||
export function clearAuthState() {
|
||||
localStorage.removeItem('frpc_token');
|
||||
localStorage.removeItem('frpc_token_expire');
|
||||
localStorage.removeItem('frpc_username');
|
||||
}
|
||||
|
||||
export 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 || '登录失败' };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// modules/config.js
|
||||
import { apiFetch } from './api.js';
|
||||
|
||||
const defaultConfig = {
|
||||
serverAddr: 'frp.whitetop.xyz',
|
||||
serverPort: 9358,
|
||||
token: '',
|
||||
logLevel: 'info',
|
||||
logMaxDays: 3,
|
||||
tcpMux: true,
|
||||
tcpMuxKeepalive: 30,
|
||||
heartbeatInterval: 15,
|
||||
heartbeatTimeout: 70,
|
||||
poolCount: 8,
|
||||
};
|
||||
|
||||
export async function loadConfig() {
|
||||
try {
|
||||
const data = await apiFetch('/config');
|
||||
if (data.code === 0) {
|
||||
return { ...defaultConfig, ...data.data };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载配置失败', e);
|
||||
}
|
||||
return { ...defaultConfig };
|
||||
}
|
||||
|
||||
export async function saveConfig(config) {
|
||||
const data = await apiFetch('/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// modules/frpc.js
|
||||
import { apiFetch } from './api.js';
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export async function reloadFrpc() {
|
||||
const data = await apiFetch('/frpc/reload', { method: 'POST' });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function startFrpc() {
|
||||
const data = await apiFetch('/frpc/start', { method: 'POST' });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function stopFrpc() {
|
||||
const data = await apiFetch('/frpc/stop', { method: 'POST' });
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// modules/proxies.js
|
||||
import { apiFetch } from './api.js';
|
||||
|
||||
export async function loadProxies() {
|
||||
try {
|
||||
const data = await apiFetch('/proxies');
|
||||
if (data.code === 0) {
|
||||
return data.data || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载隧道失败', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function createProxy(proxy) {
|
||||
const data = await apiFetch('/proxy', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(proxy),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateProxy(proxy) {
|
||||
const data = await apiFetch('/proxy/' + proxy.id, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(proxy),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteProxy(id) {
|
||||
const data = await apiFetch('/proxy/' + id, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export 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)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// modules/ui.js
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Tab 切换
|
||||
export const activeTab = ref('proxies');
|
||||
export function switchTab(tab) {
|
||||
activeTab.value = tab;
|
||||
}
|
||||
|
||||
// 弹窗控制
|
||||
export const dialogVisible = ref(false);
|
||||
export const dialogMode = ref('add');
|
||||
export const dialogForm = ref({
|
||||
id: null,
|
||||
name: '',
|
||||
type: 'tcp',
|
||||
localIP: '',
|
||||
localPort: 0,
|
||||
remotePort: 0,
|
||||
});
|
||||
|
||||
export function openAddDialog() {
|
||||
dialogMode.value = 'add';
|
||||
dialogForm.value = { id: null, name: '', type: 'tcp', localIP: '', localPort: 0, remotePort: 0 };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
export function openEditDialog(proxy) {
|
||||
dialogMode.value = 'edit';
|
||||
dialogForm.value = { ...proxy };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
export function closeDialog() {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
// 搜索
|
||||
export const searchKeyword = ref('');
|
||||
@@ -0,0 +1,184 @@
|
||||
/* ===== style-1.css - 全局基础 ===== */
|
||||
|
||||
/* ---------- 字体定义 ---------- */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: local('HarmonyOS Sans SC'),
|
||||
local('PingFang SC'),
|
||||
local('Microsoft YaHei'),
|
||||
local('Helvetica Neue');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: local('HarmonyOS Sans SC Medium'),
|
||||
local('PingFang SC Medium');
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: local('HarmonyOS Sans SC Bold'),
|
||||
local('PingFang SC Semibold');
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ---------- 重置 ---------- */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
background: #0a0a0f;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* ---------- 数字专用字体(加粗) ---------- */
|
||||
.digit,
|
||||
.big-number,
|
||||
.remote-port,
|
||||
.proxy-addr,
|
||||
.metric-value .digit,
|
||||
.metric-value .big-number,
|
||||
.login-header h1,
|
||||
.login-btn,
|
||||
.save-btn,
|
||||
.btn-confirm {
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* 数字特别加粗(适用于大数字展示) */
|
||||
.big-number {
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* 代码/端口类数字使用等宽字体,但保持加粗 */
|
||||
.digit,
|
||||
.remote-port,
|
||||
.proxy-addr {
|
||||
font-family: 'HarmonyOS Sans SC', 'JetBrains Mono', monospace;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* ---------- 全局颜色变量 ---------- */
|
||||
:root {
|
||||
--primary-blue: #4a7cff;
|
||||
--primary-blue-dim: rgba(74, 124, 255, 0.15);
|
||||
--primary-blue-glow: rgba(74, 124, 255, 0.3);
|
||||
--primary-blue-border: rgba(74, 124, 255, 0.2);
|
||||
--bg-deep: #0a0a18;
|
||||
--bg-card: rgba(13, 13, 26, 0.6);
|
||||
--bg-metric: rgba(255, 255, 255, 0.03);
|
||||
--border-subtle: rgba(255, 255, 255, 0.04);
|
||||
--text-dim: rgba(255, 255, 255, 0.25);
|
||||
--text-muted: rgba(255, 255, 255, 0.35);
|
||||
--text-secondary: rgba(255, 255, 255, 0.6);
|
||||
--text-primary: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
/* ---------- 滚动条 ---------- */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(74, 124, 255, 0.25);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(74, 124, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ---------- 背景磨砂层 ---------- */
|
||||
.app-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(30, 60, 120, 0.3) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 50%, rgba(80, 30, 120, 0.2) 0%, transparent 60%),
|
||||
#0a0a0f;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ---------- 主容器 ---------- */
|
||||
.app-container {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(40px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(1.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 32px;
|
||||
box-shadow: 0 40px 80px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
transition: all 0.5s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 登录态尺寸 */
|
||||
.app-container:not(.is-logged-in) {
|
||||
width: 600px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
/* 登录后尺寸 */
|
||||
.app-container.is-logged-in {
|
||||
width: min(1600px, 92vw);
|
||||
height: min(900px, 88vh);
|
||||
}
|
||||
|
||||
/* ---------- 旋转圈 ---------- */
|
||||
.loading-ring {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.04);
|
||||
border-top: 3px solid var(--primary-blue);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
z-index: 5;
|
||||
}
|
||||
.loading-ring.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ---------- 主内容淡入 ---------- */
|
||||
.main-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 28px; /* ← 补上内边距 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.main-panel.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/* ===== style-2.css - 登录页 ===== */
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 32px 48px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.login-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
.login-header h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 4px;
|
||||
color: var(--text-primary);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.input-group label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.input-group input {
|
||||
height: 44px;
|
||||
padding: 0 20px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
.input-group input:focus {
|
||||
border-color: var(--primary-blue-border);
|
||||
}
|
||||
.input-group input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 44px;
|
||||
margin-top: 6px;
|
||||
background: linear-gradient(135deg, var(--primary-blue), #2a5adf);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #0a0a0f;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.login-btn:hover:not(:disabled) {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 8px 24px var(--primary-blue-glow);
|
||||
}
|
||||
.login-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: #f87171;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
min-height: 24px;
|
||||
}
|
||||
.login-form .hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
text-align: center;
|
||||
margin-top: -6px;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user