mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-07 22:05:45 +08:00
515 lines
16 KiB
HTML
515 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>FRP Manager</title>
|
||
<link rel="stylesheet" href="/lib/css/bootstrap.min.css">
|
||
<link rel="stylesheet" href="/lib/css/font-awesome.min.css">
|
||
<style>
|
||
:root {
|
||
--primary-color: #4361ee;
|
||
--success-color: #2e7d32;
|
||
--danger-color: #d32f2f;
|
||
--bg-color: #f0f2f5;
|
||
--card-bg: #ffffff;
|
||
--text-primary: #1a1a1a;
|
||
--border-color: #d1d5db;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', sans-serif;
|
||
margin: 0;
|
||
padding: 20px;
|
||
background-color: var(--bg-color);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20px;
|
||
}
|
||
|
||
.card {
|
||
background: var(--card-bg);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.card-header {
|
||
font-size: 1.25rem;
|
||
font-weight: bold;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
font-size: 0.9rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status-running {
|
||
background: #e8f5e9;
|
||
color: var(--success-color);
|
||
}
|
||
|
||
.status-stopped {
|
||
background: #ffebee;
|
||
color: var(--danger-color);
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.info-label {
|
||
color: #666;
|
||
}
|
||
|
||
.info-value {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.btn:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.btn-success {
|
||
background: var(--success-color);
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: var(--danger-color);
|
||
color: white;
|
||
}
|
||
|
||
.full-width {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.log-container {
|
||
background: #1e1e1e;
|
||
color: #d4d4d4;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
height: 300px;
|
||
overflow-y: auto;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 0.85rem;
|
||
white-space: pre-wrap;
|
||
line-height: 1.4;
|
||
border: 1px solid #333;
|
||
}
|
||
|
||
.floating-help-panel {
|
||
position: fixed;
|
||
right: 20px;
|
||
top: 20px;
|
||
width: 100px;
|
||
background: #e3f2fd;
|
||
border-left: 4px solid #2196f3;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
font-size: 0.9rem;
|
||
line-height: 1.6;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||
z-index: 1000;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.floating-help-panel.collapsed {
|
||
transform: translateX(calc(100% + 20px));
|
||
}
|
||
|
||
.help-toggle-btn {
|
||
position: absolute;
|
||
left: -40px;
|
||
top: 10px;
|
||
width: 40px;
|
||
height: 40px;
|
||
background: #2196f3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px 0 0 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.2rem;
|
||
box-shadow: -2px 2px 5px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.floating-help-panel h3 {
|
||
margin-top: 0;
|
||
color: #1565c0;
|
||
font-size: 1.1rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.floating-help-panel ul {
|
||
padding-left: 20px;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.floating-help-panel li {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
textarea {
|
||
width: 100%;
|
||
height: 400px;
|
||
font-family: monospace;
|
||
padding: 10px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
resize: vertical;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.control-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
input[type="number"] {
|
||
padding: 8px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 4px;
|
||
width: 80px;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
padding: 24px;
|
||
border-radius: 12px;
|
||
width: 600px;
|
||
max-width: 90%;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.modal-header {
|
||
font-size: 1.25rem;
|
||
font-weight: bold;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-body textarea {
|
||
height: 300px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container">
|
||
<!-- Status Card -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
FRP 状态
|
||
<span id="status-badge" class="status-badge status-stopped">未运行</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">进程 ID</span>
|
||
<span class="info-value" id="pid">-</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">启动时间</span>
|
||
<span class="info-value" id="start-time">-</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">下次自动重启</span>
|
||
<span class="info-value" id="next-restart">-</span>
|
||
</div>
|
||
<div class="control-group">
|
||
<button class="btn btn-success" onclick="startFrp()">启动</button>
|
||
<button class="btn btn-danger" onclick="stopFrp()">停止</button>
|
||
<button class="btn btn-primary" onclick="restartFrp()">重启</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Card -->
|
||
<div class="card">
|
||
<div class="card-header">自动重启设置</div>
|
||
<div style="margin-bottom: 20px;">
|
||
<label class="input-group">
|
||
<input type="checkbox" id="auto-restart-check"> 启用自动重启
|
||
</label>
|
||
</div>
|
||
<div class="input-group" style="margin-bottom: 20px;">
|
||
<label>重启间隔 (分钟):</label>
|
||
<input type="number" id="restart-interval" min="1" value="60">
|
||
</div>
|
||
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
|
||
</div>
|
||
|
||
<!-- Logs Card -->
|
||
<div class="card full-width">
|
||
<div class="card-header">
|
||
<div>FRP 实时日志</div>
|
||
<div class="input-group">
|
||
<label style="font-size: 0.9rem; font-weight: normal; margin-right: 15px; cursor: pointer;">
|
||
<input type="checkbox" id="auto-scroll" checked> 自动滚动
|
||
</label>
|
||
<button class="btn btn-danger" style="padding: 5px 15px; font-size: 0.8rem;"
|
||
onclick="clearLogs()">清空日志</button>
|
||
</div>
|
||
</div>
|
||
<div id="log-display" class="log-container">正在获取日志...</div>
|
||
</div>
|
||
|
||
<!-- Config Editor -->
|
||
<div class="card full-width">
|
||
<div class="card-header">
|
||
配置文件 (frpc.ini)
|
||
<div class="control-group" style="margin-top: 0;">
|
||
<button class="btn btn-primary" style="padding: 5px 15px; background-color: #6c757d;"
|
||
onclick="showTemplateModal()">查看示例模板</button>
|
||
<button class="btn btn-primary" style="padding: 5px 15px;" onclick="saveConfig()">保存配置</button>
|
||
</div>
|
||
</div>
|
||
<textarea id="config-editor"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Floating Help Panel -->
|
||
<div class="floating-help-panel" id="help-panel">
|
||
<button class="help-toggle-btn" onclick="toggleHelp()" title="切换帮助面板">
|
||
<i class="fas fa-question"></i>
|
||
</button>
|
||
<h3>
|
||
<i class="fas fa-info-circle"></i> 关于 FRP
|
||
</h3>
|
||
<p>FRP 是一个专注于内网穿透的高性能反向代理应用。</p>
|
||
<h3>用途:</h3>
|
||
<ul>
|
||
<li>使用FRP穿透后可以在不暴漏服务器IP的情况下使用代理服务器访问本面板</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Template Modal -->
|
||
<div id="template-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
示例模板
|
||
<button class="btn btn-danger" style="padding: 5px 10px;" onclick="closeTemplateModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<textarea id="template-content" readonly style="background: #f8f9fa; color: #333;"></textarea>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-success" onclick="copyTemplate()">复制内容</button>
|
||
<button class="btn btn-primary" onclick="closeTemplateModal()">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function toggleHelp() {
|
||
const panel = document.getElementById('help-panel');
|
||
panel.classList.toggle('collapsed');
|
||
}
|
||
|
||
async function loadStatus() {
|
||
try {
|
||
const res = await fetch('/api/frp/status');
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
const badge = document.getElementById('status-badge');
|
||
if (data.isRunning) {
|
||
badge.textContent = '运行中';
|
||
badge.className = 'status-badge status-running';
|
||
document.getElementById('pid').textContent = data.processId;
|
||
document.getElementById('start-time').textContent = new Date(data.startTime).toLocaleString();
|
||
} else {
|
||
badge.textContent = '未运行';
|
||
badge.className = 'status-badge status-stopped';
|
||
document.getElementById('pid').textContent = '-';
|
||
document.getElementById('start-time').textContent = '-';
|
||
}
|
||
|
||
document.getElementById('next-restart').textContent = data.nextRestartTime ? new Date(data.nextRestartTime).toLocaleString() : '未计划';
|
||
|
||
document.getElementById('auto-restart-check').checked = data.autoRestartEnabled;
|
||
document.getElementById('restart-interval').value = data.restartIntervalMinutes || 60;
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
async function startFrp() {
|
||
const res = await fetch('/api/frp/start', { method: 'POST' });
|
||
if (res.ok) { loadStatus(); }
|
||
else alert('启动失败: ' + (await res.json()).error);
|
||
}
|
||
|
||
async function stopFrp() {
|
||
const res = await fetch('/api/frp/stop', { method: 'POST' });
|
||
if (res.ok) { loadStatus(); }
|
||
}
|
||
|
||
async function restartFrp() {
|
||
const res = await fetch('/api/frp/restart', { method: 'POST' });
|
||
if (res.ok) { loadStatus(); }
|
||
else alert('重启失败: ' + (await res.json()).error);
|
||
}
|
||
|
||
async function loadConfig() {
|
||
const res = await fetch('/api/frp/config');
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
document.getElementById('config-editor').value = data.content;
|
||
}
|
||
}
|
||
|
||
async function saveConfig() {
|
||
const content = document.getElementById('config-editor').value;
|
||
const res = await fetch('/api/frp/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ content })
|
||
});
|
||
if (res.ok) alert('配置已保存');
|
||
else alert('保存失败');
|
||
}
|
||
|
||
async function saveSettings() {
|
||
const enabled = document.getElementById('auto-restart-check').checked;
|
||
const interval = parseInt(document.getElementById('restart-interval').value);
|
||
|
||
const res = await fetch('/api/frp/settings', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ autoRestartEnabled: enabled, restartIntervalMinutes: interval })
|
||
});
|
||
if (res.ok) { alert('设置已保存'); loadStatus(); }
|
||
else alert('保存失败');
|
||
}
|
||
|
||
function showTemplateModal() {
|
||
const template = `# frpc.ini
|
||
[common]
|
||
server_addr = 110.42.111.49
|
||
server_port = 15012
|
||
token = token123456
|
||
|
||
[my-service]
|
||
type = tcp
|
||
local_ip = 127.0.0.1
|
||
local_port = 5010
|
||
remote_port = 18880
|
||
use_encryption = true
|
||
use_compression = true`;
|
||
document.getElementById('template-content').value = template;
|
||
document.getElementById('template-modal').style.display = 'flex';
|
||
}
|
||
|
||
function closeTemplateModal() {
|
||
document.getElementById('template-modal').style.display = 'none';
|
||
}
|
||
|
||
function copyTemplate() {
|
||
const content = document.getElementById('template-content');
|
||
content.select();
|
||
document.execCommand('copy');
|
||
alert('已复制到剪贴板');
|
||
closeTemplateModal();
|
||
}
|
||
|
||
async function loadLogs() {
|
||
try {
|
||
const res = await fetch('/api/frp/logs');
|
||
if (res.ok) {
|
||
const logs = await res.json();
|
||
const container = document.getElementById('log-display');
|
||
|
||
if (logs.length === 0) {
|
||
container.textContent = '暂无日志数据';
|
||
} else {
|
||
container.textContent = logs.join('\n');
|
||
}
|
||
|
||
if (document.getElementById('auto-scroll').checked) {
|
||
container.scrollTop = container.scrollHeight;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error("Log fetch error", e);
|
||
}
|
||
}
|
||
|
||
async function clearLogs() {
|
||
if (!confirm('确定要清空日志吗?')) return;
|
||
await fetch('/api/frp/logs/clear', { method: 'POST' });
|
||
loadLogs();
|
||
}
|
||
|
||
loadStatus();
|
||
loadConfig();
|
||
loadLogs();
|
||
setInterval(loadStatus, 5000);
|
||
setInterval(loadLogs, 3000);
|
||
</script>
|
||
</body>
|
||
|
||
</html> |