Administrator
7 hours ago f41c863d623a44f1bfb3d8a45e1ebab372f8bf38
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<!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 byId(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) {
    byId('gridRate').value = d.gridRate || '';
    byId('expectedProfit').value = d.expectedProfit || '';
    byId('maxLoss').value = d.maxLoss || '';
    byId('baseQuantity').value = d.baseQuantity || '';
    byId('quantity').value = d.quantity || '';
    byId('maxPositionSize').value = d.maxPositionSize ?? '';
    byId('stopLossCount').value = d.stopLossCount ?? '';
}
 
function collectForm() {
    return {
        apiKey: currentApiKey,
        gridRate: parseFloat(byId('gridRate').value) || 0,
        expectedProfit: parseFloat(byId('expectedProfit').value) || 0,
        maxLoss: parseFloat(byId('maxLoss').value) || 0,
        baseQuantity: byId('baseQuantity').value,
        quantity: byId('quantity').value,
        maxPositionSize: parseInt(byId('maxPositionSize').value) || 0,
        stopLossCount: parseInt(byId('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>