<!DOCTYPE html>
|
<html lang="zh">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>Gate 策略控制面板</title>
|
<style>
|
:root {
|
--bg: #0f1117; --card: #1a1d27; --border: #2a2d3a;
|
--text: #e1e4e8; --dim: #8b949e; --accent: #58a6ff;
|
--danger: #f85149; --green: #3fb950; --warn: #d29922;
|
--input-bg: #161b22;
|
}
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
body {
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
background: var(--bg); color: var(--text); min-height: 100vh; padding: 20px;
|
}
|
.container { max-width: 960px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
|
.card {
|
background: var(--card); border: 1px solid var(--border);
|
border-radius: 8px; padding: 20px;
|
}
|
.card-header {
|
display: flex; justify-content: space-between; align-items: center;
|
margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border);
|
}
|
h2 { font-size: 18px; display: flex; align-items: center; gap: 10px; }
|
.status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
|
.form-group { display: flex; flex-direction: column; gap: 3px; }
|
.form-group.full { grid-column: span 2; }
|
.form-group label { font-size: 11px; color: var(--dim); font-weight: 500; }
|
.form-group input, .form-group select {
|
background: var(--input-bg); border: 1px solid var(--border);
|
border-radius: 5px; color: var(--text); padding: 7px 10px;
|
font-size: 13px; outline: none;
|
}
|
.form-group input:focus, .form-group select:focus { border-color: var(--accent); }
|
.btn {
|
padding: 7px 16px; border-radius: 5px; border: none;
|
font-size: 12px; cursor: pointer; font-weight: 500;
|
}
|
.btn:hover { opacity: .85; }
|
.btn-primary { background: var(--accent); color: #fff; }
|
.btn-success { background: var(--green); color: #fff; }
|
.btn-danger { background: var(--danger); color: #fff; }
|
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
.btn-group { display: flex; gap: 6px; flex-wrap: wrap; }
|
.api-row { display: flex; gap: 10px; align-items: flex-end; }
|
.api-row .form-group { flex: 1; }
|
|
/* 日志窗口 */
|
.log-panel {
|
height: 280px; overflow-y: auto; background: var(--bg);
|
border: 1px solid var(--border); border-radius: 6px; padding: 10px;
|
font-family: "SF Mono", "Consolas", monospace; font-size: 12px;
|
line-height: 1.6; white-space: pre-wrap; word-break: break-all;
|
}
|
.log-panel .INFO { color: var(--text); }
|
.log-panel .WARN { color: var(--warn); }
|
.log-panel .ERROR { color: var(--danger); }
|
.log-panel .empty { color: var(--dim); text-align: center; padding-top: 60px; }
|
|
.toast {
|
position: fixed; top: 20px; right: 20px; padding: 10px 18px;
|
border-radius: 6px; font-size: 13px; z-index: 999;
|
animation: slideIn .3s ease;
|
}
|
.toast.success { background: #1b3d2b; border:1px solid var(--green); color:var(--green); }
|
.toast.error { background: #3d1b1b; border:1px solid var(--danger); color:var(--danger); }
|
@keyframes slideIn { from{transform:translateX(100px);opacity:0} to{transform:translateX(0);opacity:1} }
|
|
.section-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; color: var(--dim); }
|
.hidden { display: none !important; }
|
</style>
|
</head>
|
<body>
|
<div class="container">
|
<!-- Step 1: apiKey 输入 -->
|
<div class="card" id="step1">
|
<div class="card-header">
|
<h2>🔑 输入 Gate API Key</h2>
|
</div>
|
<div class="api-row">
|
<div class="form-group">
|
<label>API Key</label>
|
<input id="inputApiKey" type="password" placeholder="粘贴你的 Gate API Key">
|
</div>
|
<button class="btn btn-primary" onclick="loadByApiKey()" style="margin-bottom:1px">加载配置</button>
|
</div>
|
</div>
|
|
<!-- Step 2: 配置面板(初始隐藏) -->
|
<div class="card hidden" id="step2">
|
<div class="card-header">
|
<h2>
|
<span class="status-dot" id="statusDot" style="background:var(--dim)"></span>
|
Gate 策略配置
|
<span style="font-size:11px;color:var(--dim);font-weight:400" id="currentApiKey"></span>
|
</h2>
|
<div class="btn-group">
|
<button class="btn btn-outline" onclick="refreshStatus()">刷新状态</button>
|
<button class="btn btn-success" onclick="saveConfig()">保存配置</button>
|
<button class="btn btn-primary" onclick="startStrategy()">启动策略</button>
|
<button class="btn btn-danger" onclick="stopStrategy()">停止策略</button>
|
</div>
|
</div>
|
<form id="configForm" class="form-grid">
|
<div class="form-group"><label>网格间距 (gridRate)</label><input id="gridRate" placeholder="0.005"></div>
|
<div class="form-group"><label>预期收益 USDT</label><input id="expectedProfit" placeholder="0.15"></div>
|
<div class="form-group"><label>最大亏损 USDT</label><input id="maxLoss" placeholder="1.5"></div>
|
<div class="form-group"><label>基底开仓张数</label><input id="baseQuantity" placeholder="2"></div>
|
<div class="form-group"><label>每次下单张数</label><input id="quantity" placeholder="2"></div>
|
<div class="form-group"><label>最大持仓张数</label><input id="maxPositionSize" placeholder="4"></div>
|
<div class="form-group"><label>止损阶梯次数</label><input id="stopLossCount" placeholder="0"></div>
|
</form>
|
</div>
|
|
<!-- 日志面板 -->
|
<div class="card">
|
<div class="card-header">
|
<h2>📋 实时日志</h2>
|
<button class="btn btn-outline btn-sm" onclick="clearLogs()">清屏</button>
|
</div>
|
<div class="log-panel" id="logPanel">
|
<div class="empty">等待日志...</div>
|
</div>
|
</div>
|
</div>
|
<div id="toast"></div>
|
|
<script>
|
const API = '/api/gate';
|
let currentApiKey = '';
|
let logLines = [];
|
const logPanel = document.getElementById('logPanel');
|
|
function $(id) { return document.getElementById(id); }
|
|
function toast(msg, type) {
|
const t = document.getElementById('toast');
|
t.innerHTML = msg; t.className = 'toast ' + type;
|
setTimeout(() => t.innerHTML = '', 3000);
|
}
|
|
function maskKey(k) { return k && k.length > 8 ? k.slice(0, 4) + '****' + k.slice(-4) : '***'; }
|
|
// ========== 日志 ==========
|
function appendLog(line) {
|
logLines.push(line);
|
if (logLines.length > 500) logLines.shift();
|
if (logPanel.querySelector('.empty')) logPanel.innerHTML = '';
|
const div = document.createElement('div');
|
const cls = line.includes('WARN') ? 'WARN' : line.includes('ERROR') ? 'ERROR' : 'INFO';
|
div.className = cls;
|
div.textContent = line;
|
logPanel.appendChild(div);
|
logPanel.scrollTop = logPanel.scrollHeight;
|
}
|
function clearLogs() {
|
logLines = [];
|
logPanel.innerHTML = '<div class="empty">已清屏</div>';
|
}
|
|
// SSE 连接(浏览器自动重连,无需手动 reconnect)
|
(function initSSE() {
|
const es = new EventSource(API + '/logs');
|
es.addEventListener('log', e => appendLog(e.data));
|
es.onerror = () => { appendLog('[SYSTEM] SSE 连接断开,浏览器将自动重连...'); };
|
})();
|
|
// ========== API ==========
|
async function fetchApi(url, opts) {
|
const res = await fetch(url, opts);
|
const data = await res.json();
|
if (data.code !== 0 && data.code !== 200) throw new Error(data.msg || '请求失败');
|
return data.data !== undefined ? data.data : data.msg;
|
}
|
|
// Step 1: 输入 apiKey 加载配置
|
async function loadByApiKey() {
|
const key = document.getElementById('inputApiKey').value.trim();
|
if (!key) return toast('请输入 API Key', 'error');
|
try {
|
const d = await fetchApi(API + '/config/load?apiKey=' + encodeURIComponent(key), { method: 'POST' });
|
fillForm(d);
|
toast('配置已加载', 'success');
|
} catch(e) {
|
// 文件不存在 → 用默认值填充,用户可编辑后保存
|
fillForm({ gridRate:0.005, expectedProfit:0.15, maxLoss:1.5,
|
baseQuantity:'2', quantity:'2', maxPositionSize:4, stopLossCount:0 });
|
toast('未找到配置,已加载默认值,编辑后请保存', 'success');
|
}
|
currentApiKey = key;
|
document.getElementById('currentApiKey').textContent = 'apiKey: ' + maskKey(key);
|
document.getElementById('step2').classList.remove('hidden');
|
refreshStatus();
|
}
|
|
function fillForm(d) {
|
$('#gridRate').value = d.gridRate || '';
|
$('#expectedProfit').value = d.expectedProfit || '';
|
$('#maxLoss').value = d.maxLoss || '';
|
$('#baseQuantity').value = d.baseQuantity || '';
|
$('#quantity').value = d.quantity || '';
|
$('#maxPositionSize').value = d.maxPositionSize ?? '';
|
$('#stopLossCount').value = d.stopLossCount ?? '';
|
}
|
|
function collectForm() {
|
return {
|
apiKey: currentApiKey,
|
gridRate: parseFloat($('#gridRate').value) || 0,
|
expectedProfit: parseFloat($('#expectedProfit').value) || 0,
|
maxLoss: parseFloat($('#maxLoss').value) || 0,
|
baseQuantity: $('#baseQuantity').value,
|
quantity: $('#quantity').value,
|
maxPositionSize: parseInt($('#maxPositionSize').value) || 0,
|
stopLossCount: parseInt($('#stopLossCount').value) || 0
|
};
|
}
|
|
async function saveConfig() {
|
if (!currentApiKey) return toast('请先输入 API Key', 'error');
|
try {
|
const body = collectForm();
|
const msg = await fetchApi(API + '/config', {
|
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)
|
});
|
toast(msg || '保存成功', 'success');
|
} catch(e) { toast(e.message, 'error'); }
|
}
|
|
async function startStrategy() {
|
if (!currentApiKey) return toast('请先输入 API Key', 'error');
|
if (!confirm('确定要启动策略吗?')) return;
|
try {
|
const msg = await fetchApi(API + '/restart?apiKey=' + encodeURIComponent(currentApiKey), { method: 'POST' });
|
toast(msg, 'success');
|
refreshStatus();
|
} catch(e) { toast(e.message, 'error'); }
|
}
|
|
async function stopStrategy() {
|
if (!confirm('确定要停止策略吗?')) return;
|
try {
|
const msg = await fetchApi(API + '/stop', { method: 'POST' });
|
toast(msg, 'success');
|
refreshStatus();
|
} catch(e) { toast(e.message, 'error'); }
|
}
|
|
async function refreshStatus() {
|
try {
|
const s = await fetchApi(API + '/status');
|
const dot = document.getElementById('statusDot');
|
dot.style.background = s === 'ACTIVE' ? 'var(--green)' : 'var(--danger)';
|
} catch(e) {}
|
}
|
</script>
|
</body>
|
</html>
|