首次引入全新WebUI
This commit is contained in:
+390
@@ -0,0 +1,390 @@
|
||||
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');
|
||||
Reference in New Issue
Block a user