优化部分UI动线

This commit is contained in:
2026-07-23 18:40:15 +08:00
parent a35fb8a1f7
commit 17a4557113
+139 -89
View File
@@ -3,26 +3,28 @@ const { createApp, ref, reactive, computed, onMounted } = Vue;
const app = createApp({ const app = createApp({
setup() { setup() {
// ===== 登录态 ===== // ===== 登录态 =====
const transitioning = ref(false); // 旋转圈显示
const contentVisible = ref(false); // 主内容淡入
const loggedIn = ref(false); const loggedIn = ref(false);
const loading = ref(false); const loading = ref(false);
const loginError = ref(''); const loginError = ref("");
const loginForm = reactive({ const loginForm = reactive({
username: '', username: "",
password: '', password: "",
}); });
// ===== Token ===== // ===== Token =====
const token = ref(''); const token = ref("");
// ===== Tab ===== // ===== Tab =====
const activeTab = ref('config'); const activeTab = ref("config");
// ===== 全局配置 ===== // ===== 全局配置 =====
const globalConfig = reactive({ const globalConfig = reactive({
serverAddr: 'frp.whitetop.xyz', serverAddr: "frp.whitetop.xyz",
serverPort: 9358, serverPort: 9358,
token: 'Lxh10020328', token: "Lxh10020328",
logLevel: 'info', logLevel: "info",
logMaxDays: 3, logMaxDays: 3,
tcpMux: true, tcpMux: true,
tcpMuxKeepalive: 30, tcpMuxKeepalive: 30,
@@ -34,19 +36,19 @@ const app = createApp({
// ===== 隧道列表 ===== // ===== 隧道列表 =====
const proxies = ref([]); const proxies = ref([]);
const searchKeyword = ref(''); const searchKeyword = ref("");
// ===== frpc 状态 ===== // ===== frpc 状态 =====
const frpcRunning = ref(false); const frpcRunning = ref(false);
// ===== 弹窗 ===== // ===== 弹窗 =====
const dialogVisible = ref(false); const dialogVisible = ref(false);
const dialogMode = ref('add'); // 'add' | 'edit' const dialogMode = ref("add"); // 'add' | 'edit'
const dialogForm = reactive({ const dialogForm = reactive({
id: null, id: null,
name: '', name: "",
type: 'tcp', type: "tcp",
localIP: '', localIP: "",
localPort: 0, localPort: 0,
remotePort: 0, remotePort: 0,
}); });
@@ -55,17 +57,18 @@ const app = createApp({
const filteredProxies = computed(() => { const filteredProxies = computed(() => {
if (!searchKeyword.value) return proxies.value; if (!searchKeyword.value) return proxies.value;
const kw = searchKeyword.value.toLowerCase(); const kw = searchKeyword.value.toLowerCase();
return proxies.value.filter(p => return proxies.value.filter(
p.name.toLowerCase().includes(kw) || (p) =>
p.localIP.includes(kw) || p.name.toLowerCase().includes(kw) ||
String(p.remotePort).includes(kw) p.localIP.includes(kw) ||
String(p.remotePort).includes(kw),
); );
}); });
// ===== API 基础 ===== // ===== API 基础 =====
const apiHeaders = () => ({ const apiHeaders = () => ({
'Content-Type': 'application/json', "Content-Type": "application/json",
'Authorization': 'Bearer ' + token.value, Authorization: "Bearer " + token.value,
}); });
const apiFetch = async (url, opts = {}) => { const apiFetch = async (url, opts = {}) => {
@@ -78,160 +81,196 @@ const app = createApp({
// ===== 登录 ===== // ===== 登录 =====
const doLogin = async () => { const doLogin = async () => {
// 1. 校验输入
if (!loginForm.username || !loginForm.password) { if (!loginForm.username || !loginForm.password) {
loginError.value = '请输入用户名和密码'; loginError.value = "请输入用户名和密码";
return; return;
} }
// 2. 开始登录
loading.value = true; loading.value = true;
loginError.value = ''; loginError.value = "";
try { try {
const res = await fetch('/api/login', { const res = await fetch("/api/login", {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(loginForm), body: JSON.stringify(loginForm),
}); });
const data = await res.json(); const data = await res.json();
if (data.code === 0) { if (data.code === 0) {
// 3. 保存 token
token.value = data.data.token; token.value = data.data.token;
// 4. 显示旋转圈
transitioning.value = true;
// 5. 切换登录态 → 触发容器放大动画(0.5s)
loggedIn.value = true; 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 { } else {
loginError.value = data.msg || '登录失败'; // 登录失败
loginError.value = data.msg || "登录失败";
loading.value = false;
} }
} catch (e) { } catch (e) {
loginError.value = '网络错误,请重试'; loginError.value = "网络错误,请重试";
loading.value = false;
} }
loading.value = false;
}; };
const doLogout = () => { const doLogout = () => {
contentVisible.value = false; // 先隐藏内容
loggedIn.value = false; loggedIn.value = false;
token.value = ''; token.value = '';
loginForm.username = ''; loginForm.username = '';
loginForm.password = ''; loginForm.password = '';
loading.value = false;
transitioning.value = false;
}; };
// ===== 加载数据 ===== // ===== 加载数据 =====
const loadAllData = async () => { const loadAllData = async () => {
await Promise.all([ await Promise.all([loadConfig(), loadProxies(), loadFrpcStatus()]);
loadConfig(),
loadProxies(),
loadFrpcStatus(),
]);
}; };
const loadConfig = async () => { const loadConfig = async () => {
try { try {
const data = await apiFetch('/api/config'); const data = await apiFetch("/api/config");
if (data.code === 0) { if (data.code === 0) {
Object.assign(globalConfig, data.data); Object.assign(globalConfig, data.data);
} }
} catch (e) { } catch (e) {
console.warn('加载配置失败', e); console.warn("加载配置失败", e);
} }
}; };
const loadProxies = async () => { const loadProxies = async () => {
try { try {
const data = await apiFetch('/api/proxies'); const data = await apiFetch("/api/proxies");
if (data.code === 0) { if (data.code === 0) {
proxies.value = data.data || []; proxies.value = data.data || [];
} }
} catch (e) { } catch (e) {
console.warn('加载隧道列表失败', e); console.warn("加载隧道列表失败", e);
} }
}; };
const loadFrpcStatus = async () => { const loadFrpcStatus = async () => {
try { try {
const data = await apiFetch('/api/frpc/status'); const data = await apiFetch("/api/frpc/status");
if (data.code === 0) { if (data.code === 0) {
frpcRunning.value = data.data.running || false; frpcRunning.value = data.data.running || false;
} }
} catch (e) { } catch (e) {
console.warn('获取 frpc 状态失败', e); console.warn("获取 frpc 状态失败", e);
} }
}; };
// ===== 保存配置 ===== // ===== 保存配置 =====
const saveConfig = async () => { const saveConfig = async () => {
try { try {
const data = await apiFetch('/api/config', { const data = await apiFetch("/api/config", {
method: 'PUT', method: "PUT",
body: JSON.stringify(globalConfig), body: JSON.stringify(globalConfig),
}); });
if (data.code === 0) { if (data.code === 0) {
await loadFrpcStatus(); await loadFrpcStatus();
alert('配置已更新并热加载'); alert("配置已更新并热加载");
} else { } else {
alert('保存失败: ' + data.msg); alert("保存失败: " + data.msg);
} }
} catch (e) { } catch (e) {
alert('网络错误'); alert("网络错误");
} }
}; };
// ===== 隧道操作 ===== // ===== 隧道操作 =====
const toggleProxy = async (p) => { const toggleProxy = async (p) => {
try { try {
const data = await apiFetch('/api/proxy/' + p.id, { const data = await apiFetch("/api/proxy/" + p.id, {
method: 'PUT', method: "PUT",
body: JSON.stringify(p), body: JSON.stringify(p),
}); });
if (data.code !== 0) { if (data.code !== 0) {
alert('更新失败: ' + data.msg); alert("更新失败: " + data.msg);
await loadProxies(); await loadProxies();
} else { } else {
await loadFrpcStatus(); await loadFrpcStatus();
} }
} catch (e) { } catch (e) {
alert('网络错误'); alert("网络错误");
await loadProxies(); await loadProxies();
} }
}; };
const deleteProxy = async (id) => { const deleteProxy = async (id) => {
if (!confirm('确定要删除这条隧道吗?')) return; if (!confirm("确定要删除这条隧道吗?")) return;
try { try {
const data = await apiFetch('/api/proxy/' + id, { const data = await apiFetch("/api/proxy/" + id, {
method: 'DELETE', method: "DELETE",
}); });
if (data.code === 0) { if (data.code === 0) {
await loadProxies(); await loadProxies();
await loadFrpcStatus(); await loadFrpcStatus();
} else { } else {
alert('删除失败: ' + data.msg); alert("删除失败: " + data.msg);
} }
} catch (e) { } catch (e) {
alert('网络错误'); alert("网络错误");
} }
}; };
// ===== 弹窗 ===== // ===== 弹窗 =====
const openAddDialog = () => { const openAddDialog = () => {
dialogMode.value = 'add'; dialogMode.value = "add";
dialogForm.id = null; dialogForm.id = null;
dialogForm.name = ''; dialogForm.name = "";
dialogForm.type = 'tcp'; dialogForm.type = "tcp";
dialogForm.localIP = ''; dialogForm.localIP = "";
dialogForm.localPort = 0; dialogForm.localPort = 0;
dialogForm.remotePort = 0; dialogForm.remotePort = 0;
dialogVisible.value = true; dialogVisible.value = true;
}; };
const openEditDialog = (p) => { const openEditDialog = (p) => {
dialogMode.value = 'edit'; dialogMode.value = "edit";
Object.assign(dialogForm, p); Object.assign(dialogForm, p);
dialogVisible.value = true; dialogVisible.value = true;
}; };
const confirmDialog = async () => { const confirmDialog = async () => {
if (!dialogForm.name || !dialogForm.localIP || !dialogForm.localPort || !dialogForm.remotePort) { if (
alert('请填写完整信息'); !dialogForm.name ||
!dialogForm.localIP ||
!dialogForm.localPort ||
!dialogForm.remotePort
) {
alert("请填写完整信息");
return; return;
} }
const url = dialogMode.value === 'add' ? '/api/proxy' : '/api/proxy/' + dialogForm.id; const url =
const method = dialogMode.value === 'add' ? 'POST' : 'PUT'; dialogMode.value === "add"
? "/api/proxy"
: "/api/proxy/" + dialogForm.id;
const method = dialogMode.value === "add" ? "POST" : "PUT";
try { try {
const data = await apiFetch(url, { const data = await apiFetch(url, {
method: method, method: method,
@@ -242,16 +281,16 @@ const app = createApp({
await loadProxies(); await loadProxies();
await loadFrpcStatus(); await loadFrpcStatus();
} else { } else {
alert('操作失败: ' + data.msg); alert("操作失败: " + data.msg);
} }
} catch (e) { } catch (e) {
alert('网络错误'); alert("网络错误");
} }
}; };
// ===== 导入 TOML ===== // ===== 导入 TOML =====
const triggerImport = () => { const triggerImport = () => {
document.getElementById('tomlFileInput').click(); document.getElementById("tomlFileInput").click();
}; };
const handleImport = async (e) => { const handleImport = async (e) => {
@@ -261,28 +300,33 @@ const app = createApp({
const text = await file.text(); const text = await file.text();
const parsed = TOML.parse(text); const parsed = TOML.parse(text);
if (!parsed.proxies || !Array.isArray(parsed.proxies)) { if (!parsed.proxies || !Array.isArray(parsed.proxies)) {
alert('无效的 TOML 文件:缺少 proxies 数组'); alert("无效的 TOML 文件:缺少 proxies 数组");
return; 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) { if (list.length === 0) {
alert('未解析到有效的隧道条目'); alert("未解析到有效的隧道条目");
return; return;
} }
if (!confirm(`解析到 ${list.length} 条隧道,将覆盖当前配置,确认导入?`)) return; if (
!confirm(`解析到 ${list.length} 条隧道,将覆盖当前配置,确认导入?`)
)
return;
// 批量导入:先清空,再逐条插入(后端未提供批量接口,用循环) // 批量导入:先清空,再逐条插入(后端未提供批量接口,用循环)
// 简单做法:先删除所有,再逐个创建 // 简单做法:先删除所有,再逐个创建
for (const p of proxies.value) { 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) { for (const p of list) {
await apiFetch('/api/proxy', { await apiFetch("/api/proxy", {
method: 'POST', method: "POST",
body: JSON.stringify({ body: JSON.stringify({
name: p.name, name: p.name,
type: p.type || 'tcp', type: p.type || "tcp",
localIP: p.localIP, localIP: p.localIP,
localPort: p.localPort, localPort: p.localPort,
remotePort: p.remotePort, remotePort: p.remotePort,
@@ -292,24 +336,24 @@ const app = createApp({
} }
await loadProxies(); await loadProxies();
await loadFrpcStatus(); await loadFrpcStatus();
alert('导入成功!'); alert("导入成功!");
} catch (err) { } catch (err) {
alert('解析 TOML 失败: ' + err.message); alert("解析 TOML 失败: " + err.message);
} }
e.target.value = ''; e.target.value = "";
}; };
// ===== 导出 TOML ===== // ===== 导出 TOML =====
const exportToml = async () => { const exportToml = async () => {
try { try {
const configData = await apiFetch('/api/config'); const configData = await apiFetch("/api/config");
if (configData.code !== 0) { if (configData.code !== 0) {
alert('获取配置失败'); alert("获取配置失败");
return; return;
} }
const proxyData = await apiFetch('/api/proxies'); const proxyData = await apiFetch("/api/proxies");
if (proxyData.code !== 0) { if (proxyData.code !== 0) {
alert('获取隧道列表失败'); alert("获取隧道列表失败");
return; return;
} }
const cfg = configData.data; const cfg = configData.data;
@@ -319,7 +363,11 @@ const app = createApp({
serverAddr: cfg.serverAddr, serverAddr: cfg.serverAddr,
serverPort: cfg.serverPort, serverPort: cfg.serverPort,
auth: { token: cfg.token }, 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: { transport: {
tcpMux: cfg.tcpMux, tcpMux: cfg.tcpMux,
tcpMuxKeepaliveInterval: cfg.tcpMuxKeepalive, tcpMuxKeepaliveInterval: cfg.tcpMuxKeepalive,
@@ -327,7 +375,7 @@ const app = createApp({
heartbeatTimeout: cfg.heartbeatTimeout, heartbeatTimeout: cfg.heartbeatTimeout,
poolCount: cfg.poolCount, poolCount: cfg.poolCount,
}, },
proxies: proxyList.map(p => ({ proxies: proxyList.map((p) => ({
name: p.name, name: p.name,
type: p.type, type: p.type,
localIP: p.localIP, localIP: p.localIP,
@@ -336,21 +384,21 @@ const app = createApp({
})), })),
}; };
const tomlStr = TOML.stringify(tomlObj); const tomlStr = TOML.stringify(tomlObj);
const blob = new Blob([tomlStr], { type: 'text/plain' }); const blob = new Blob([tomlStr], { type: "text/plain" });
const a = document.createElement('a'); const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.download = 'frpc.toml'; a.download = "frpc.toml";
a.click(); a.click();
URL.revokeObjectURL(a.href); URL.revokeObjectURL(a.href);
} catch (e) { } catch (e) {
alert('导出失败: ' + e.message); alert("导出失败: " + e.message);
} }
}; };
// ===== 键盘快捷键 ===== // ===== 键盘快捷键 =====
onMounted(() => { onMounted(() => {
document.addEventListener('keydown', (e) => { document.addEventListener("keydown", (e) => {
if (e.key === 'Escape' && dialogVisible.value) { if (e.key === "Escape" && dialogVisible.value) {
dialogVisible.value = false; dialogVisible.value = false;
} }
}); });
@@ -383,8 +431,10 @@ const app = createApp({
triggerImport, triggerImport,
handleImport, handleImport,
exportToml, exportToml,
transitioning,
contentVisible,
}; };
}, },
}); });
app.mount('#app'); app.mount("#app");