Files
squad-rain-ops-mini/wwwroot/frp-manager.html

515 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>