ui优化
This commit is contained in:
@@ -112,67 +112,74 @@
|
||||
inner = `<div class="err">${esc(row.error || "子代理不可用")}</div>`;
|
||||
} else if (!agOk) {
|
||||
inner = `<div class="err">${esc(agErr || "子代理返回失败")}</div>`;
|
||||
inner += `<div class="rule-tip">请检查:PM2 子代理是否在对应 crypto_monitor_* 目录加载了 .env;<code>curl ${esc(row.agent_url || "")}/status</code></div>`;
|
||||
inner += `<div class="empty-hint">请检查 PM2 子代理与 <code>${esc(row.agent_url || "")}/status</code></div>`;
|
||||
} else {
|
||||
const posRows = pos
|
||||
.map(
|
||||
(x) =>
|
||||
`<tr><td>${esc(x.symbol)}</td><td>${esc(x.side)}</td><td>${fmt(x.contracts, 4)}</td><td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
inner = `<div class="rule-tip">余额 ${fmt(ag.balance_usdt, 2)} U · 浮盈合计 <span class="${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</span></div>`;
|
||||
inner += pos.length
|
||||
? `<table><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th></tr>${posRows}</table>`
|
||||
: `<div style="color:var(--muted);padding:6px 0">交易所无持仓</div>`;
|
||||
inner = `<div class="stat-row">
|
||||
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
||||
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</div></div>
|
||||
</div>`;
|
||||
inner += `<div class="section-title">交易所持仓</div>`;
|
||||
if (pos.length) {
|
||||
const posRows = pos
|
||||
.map(
|
||||
(x) =>
|
||||
`<tr><td>${esc(x.symbol)}</td><td>${esc(x.side)}</td><td>${fmt(x.contracts, 4)}</td><td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
inner += `<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th></tr></thead><tbody>${posRows}</tbody></table>`;
|
||||
} else {
|
||||
inner += `<div class="empty-hint">无持仓</div>`;
|
||||
}
|
||||
if (orders.length) {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#b8c4ff">机器人持仓 ${orders.length} 笔</div>`;
|
||||
inner += `<div class="section-title">机器人单 · ${orders.length}</div>`;
|
||||
orders.forEach((o) => {
|
||||
inner += `<div class="rule-tip">${esc(o.symbol)} ${o.direction} 成交${o.trigger_price}</div>`;
|
||||
inner += `<div class="list-line">${esc(o.symbol)} · ${esc(o.direction)} · 触发 ${o.trigger_price}</div>`;
|
||||
});
|
||||
}
|
||||
if ((row.capabilities || []).includes("key")) {
|
||||
inner += `<div class="section-title">关键位</div>`;
|
||||
if (!flaskOk) {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#f85149">关键位/机器人:策略 Flask 未连通</div>`;
|
||||
const fe = row.flask_error || hm.msg || hm.error || "";
|
||||
const short =
|
||||
fe ||
|
||||
(hm.status === 404
|
||||
? "HTTP 404:请 git pull 并重启各 crypto_* Flask(hub_bridge 路由未注册)"
|
||||
: "请确认实例已启动,且 HUB_BRIDGE_TOKEN 与实例一致或 APP_AUTH_DISABLED=true");
|
||||
inner += `<div class="rule-tip">${esc(short)}</div>`;
|
||||
? "HTTP 404:请重启各 crypto_* Flask"
|
||||
: "策略 Flask 未连通");
|
||||
inner += `<div class="err">${esc(short)}</div>`;
|
||||
} else if (!keys.length) {
|
||||
inner += `<div style="margin-top:8px;color:var(--muted);font-size:12px">关键位:当前无记录(在下单区或实例首页添加)</div>`;
|
||||
inner += `<div class="empty-hint">当前无记录</div>`;
|
||||
} else {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#b8c4ff">关键位 ${keys.length} 条</div>`;
|
||||
keys.slice(0, 8).forEach((k) => {
|
||||
const kp = kmap[k.id] || kmap[String(k.id)] || {};
|
||||
const mt = k.monitor_type || k.type || "";
|
||||
inner += `<div class="rule-tip">${esc(k.symbol)} ${esc(mt)} 上${k.upper}/下${k.lower}`;
|
||||
let line = `${esc(k.symbol)} · ${esc(mt)} · ${k.upper} / ${k.lower}`;
|
||||
if (kp.price_display != null || kp.price != null) {
|
||||
inner += ` · 现价 ${esc(kp.price_display != null ? kp.price_display : kp.price)}`;
|
||||
line += ` · ${esc(kp.price_display != null ? kp.price_display : kp.price)}`;
|
||||
}
|
||||
inner += ` · 门控 ${esc(kp.gate_summary || "-")}</div>`;
|
||||
line += ` · ${esc(kp.gate_summary || "-")}`;
|
||||
inner += `<div class="list-line">${line}</div>`;
|
||||
});
|
||||
}
|
||||
} else if ((row.capabilities || []).includes("trend")) {
|
||||
inner += `<div style="margin-top:6px;color:var(--muted);font-size:12px">该账户为趋势户,无关键位(见趋势计划或下单区)</div>`;
|
||||
}
|
||||
if (trends.length) {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#b8c4ff">趋势计划 ${trends.length} 个运行中</div>`;
|
||||
inner += `<div class="section-title">趋势计划 · ${trends.length}</div>`;
|
||||
trends.forEach((t) => {
|
||||
inner += `<div class="rule-tip">#${t.id} ${esc(t.symbol)} ${t.direction} SL${t.stop_loss} TP${t.take_profit}</div>`;
|
||||
inner += `<div class="list-line">#${t.id} ${esc(t.symbol)} ${t.direction} · SL ${t.stop_loss} · TP ${t.take_profit}</div>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
const review = row.review_url
|
||||
? `<a href="${esc(row.review_url)}" target="_blank" rel="noopener" title="打开该实例的交易记录与复盘页(不在中控内操作)">交易复盘</a>`
|
||||
? `<a class="btn-link" href="${esc(row.review_url)}" target="_blank" rel="noopener">复盘</a>`
|
||||
: "";
|
||||
return `<div class="card">
|
||||
<div class="card-head">
|
||||
<div><strong>${esc(row.name)}</strong><div class="rule-tip">${esc(row.flask_url_browser || row.flask_url || "")}</div></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<div>
|
||||
<div class="card-title">${esc(row.name)}</div>
|
||||
<div class="card-sub">${esc(row.flask_url_browser || row.flask_url || "")}</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${review}
|
||||
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">该户全平</button>
|
||||
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">${inner}</div>
|
||||
@@ -273,16 +280,17 @@
|
||||
const data = await r.json();
|
||||
tradeMeta = data.meta?.meta || data.meta || {};
|
||||
const el = document.getElementById("trade-meta");
|
||||
if (tradeMeta.key_gate_rule_text) {
|
||||
el.textContent = tradeMeta.key_gate_rule_text;
|
||||
} else if (tradeMeta.trend_pullback_preview_ttl) {
|
||||
el.textContent =
|
||||
`预览有效期 ${tradeMeta.trend_pullback_preview_ttl}s · 补仓档 ${tradeMeta.trend_pullback_dca_legs} · 余额偏差≤${tradeMeta.trend_preview_max_drift_pct}%`;
|
||||
} else {
|
||||
el.textContent = "";
|
||||
let txt = "";
|
||||
if (tradeMeta.key_gate_rule_text) txt = tradeMeta.key_gate_rule_text;
|
||||
else if (tradeMeta.trend_pullback_preview_ttl) {
|
||||
txt = `预览 ${tradeMeta.trend_pullback_preview_ttl}s · 补仓 ${tradeMeta.trend_pullback_dca_legs} 档 · 余额偏差 ≤${tradeMeta.trend_preview_max_drift_pct}%`;
|
||||
}
|
||||
el.textContent = txt;
|
||||
el.style.display = txt ? "block" : "none";
|
||||
} catch (e) {
|
||||
document.getElementById("trade-meta").textContent = "";
|
||||
const el = document.getElementById("trade-meta");
|
||||
el.textContent = "";
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,9 +320,9 @@
|
||||
.map((r) => `<tr><td>${r.i}</td><td>${r.price}</td><td>${r.contracts}</td></tr>`)
|
||||
.join("");
|
||||
box.innerHTML = `
|
||||
<div class="rule-tip">预览 #${esc(p.id || trendPreviewId)} 剩余 ${p.expires_in_sec ?? "?"}s</div>
|
||||
<div class="rule-tip">${esc(p.symbol)} ${esc(p.direction)} ${p.leverage}x · 快照 ${fmt(p.snapshot_available_usdt, 2)} U</div>
|
||||
<table><tr><th>#</th><th>补仓价</th><th>张数</th></tr>${levels}</table>
|
||||
<div class="section-title">预览 #${esc(p.id || trendPreviewId)} · ${p.expires_in_sec ?? "?"}s</div>
|
||||
<div class="list-line">${esc(p.symbol)} ${esc(p.direction)} · ${p.leverage}x · 快照 ${fmt(p.snapshot_available_usdt, 2)} U</div>
|
||||
<table class="data-table"><thead><tr><th>#</th><th>补仓价</th><th>张数</th></tr></thead><tbody>${levels}</tbody></table>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<button type="button" id="btn-trend-exec">确认执行(实盘)</button>
|
||||
</div>`;
|
||||
@@ -355,8 +363,8 @@
|
||||
const parts = [];
|
||||
if (m.hub_bridge_token_set) parts.push("中控已配置 HUB_BRIDGE_TOKEN");
|
||||
else parts.push("中控未设 HUB_BRIDGE_TOKEN(实例需 APP_AUTH_DISABLED 或同令牌)");
|
||||
if (m.public_origin) parts.push("复盘外链: " + m.public_origin);
|
||||
else parts.push("未设 HUB_PUBLIC_ORIGIN(复盘 127.0.0.1 仅服务器本机可开)");
|
||||
if (m.public_origin) parts.push("浏览器外链基址: " + m.public_origin);
|
||||
else parts.push("未设 HUB_PUBLIC_ORIGIN(复盘链接仅本机可开)");
|
||||
if ((m.env_disabled_ids || []).length)
|
||||
parts.push("环境强制关闭 id: " + m.env_disabled_ids.join(", "));
|
||||
el.textContent = parts.join(" · ");
|
||||
@@ -366,11 +374,11 @@
|
||||
function loadSettingsUI() {
|
||||
loadSettingsMetaLine();
|
||||
loadSettings().then((data) => {
|
||||
const tbody = document.getElementById("settings-tbody");
|
||||
tbody.innerHTML = (data.exchanges || [])
|
||||
.map((ex, idx) => renderSettingsRow(ex, idx))
|
||||
const list = document.getElementById("settings-list");
|
||||
list.innerHTML = (data.exchanges || [])
|
||||
.map((ex, idx) => renderSettingsCard(ex, idx))
|
||||
.join("");
|
||||
tbody.querySelectorAll(".btn-del-ex").forEach((btn) => {
|
||||
list.querySelectorAll(".btn-del-ex").forEach((btn) => {
|
||||
btn.onclick = () => {
|
||||
const i = Number(btn.dataset.idx);
|
||||
data.exchanges.splice(i, 1);
|
||||
@@ -381,46 +389,53 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderSettingsRow(ex, idx) {
|
||||
function renderSettingsCard(ex, idx) {
|
||||
const caps = ex.capabilities || [];
|
||||
const envOff = ex.env_disabled
|
||||
? ' <span class="badge">环境变量强制关</span>'
|
||||
? '<span class="badge">环境变量强制关</span>'
|
||||
: "";
|
||||
return `<tr data-idx="${idx}" data-key="${esc(ex.key || ex.id || "")}">
|
||||
<td><input type="checkbox" class="ex-enabled" ${ex.enabled ? "checked" : ""} ${ex.env_disabled ? "disabled" : ""}/>${envOff}</td>
|
||||
<td><input class="ex-name" value="${esc(ex.name || "")}" /></td>
|
||||
<td><input class="ex-flask" value="${esc(ex.flask_url || "")}" /></td>
|
||||
<td><input class="ex-agent" value="${esc(ex.agent_url || "")}" /></td>
|
||||
<td><input class="ex-review" value="${esc(ex.review_url || "")}" /></td>
|
||||
<td class="chk-row">
|
||||
<label><input type="checkbox" class="cap-order" ${caps.includes("order") ? "checked" : ""}/>下单</label>
|
||||
<label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/>关键位</label>
|
||||
<label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/>趋势</label>
|
||||
</td>
|
||||
<td><input class="ex-id" value="${esc(ex.id || "")}" style="width:48px" /></td>
|
||||
<td><button type="button" class="btn-del-ex" data-idx="${idx}">删</button></td>
|
||||
</tr>`;
|
||||
return `<div class="settings-card" data-idx="${idx}" data-key="${esc(ex.key || ex.id || "")}">
|
||||
<div class="settings-card-head">
|
||||
<label class="chk-label"><input type="checkbox" class="ex-enabled" ${ex.enabled ? "checked" : ""} ${ex.env_disabled ? "disabled" : ""}/> 启用</label>
|
||||
${envOff}
|
||||
<input class="ex-name" value="${esc(ex.name || "")}" placeholder="显示名称" />
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="field"><label>Flask URL</label><input class="ex-flask" value="${esc(ex.flask_url || "")}" /></div>
|
||||
<div class="field"><label>Agent URL</label><input class="ex-agent" value="${esc(ex.agent_url || "")}" /></div>
|
||||
<div class="field field-wide"><label>复盘链接(可空)</label><input class="ex-review" value="${esc(ex.review_url || "")}" placeholder="留空则自动生成 /records" /></div>
|
||||
</div>
|
||||
<div class="cap-chips">
|
||||
<label><input type="checkbox" class="cap-order" ${caps.includes("order") ? "checked" : ""}/> 下单</label>
|
||||
<label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/> 关键位</label>
|
||||
<label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/> 趋势</label>
|
||||
</div>
|
||||
<div class="settings-card-foot">
|
||||
<div class="field"><label>id</label><input class="ex-id" value="${esc(ex.id || "")}" /></div>
|
||||
<button type="button" class="danger btn-del-ex" data-idx="${idx}">删除账户</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function collectSettingsFromUI() {
|
||||
const rows = [...document.querySelectorAll("#settings-tbody tr")];
|
||||
const rows = [...document.querySelectorAll("#settings-list .settings-card")];
|
||||
return {
|
||||
version: 1,
|
||||
exchanges: rows.map((tr) => {
|
||||
exchanges: rows.map((card) => {
|
||||
const caps = [];
|
||||
if (tr.querySelector(".cap-order").checked) caps.push("order");
|
||||
if (tr.querySelector(".cap-key").checked) caps.push("key");
|
||||
if (tr.querySelector(".cap-trend").checked) caps.push("trend");
|
||||
const id = tr.querySelector(".ex-id").value.trim();
|
||||
const stableKey = (tr.dataset.key || id).trim();
|
||||
if (card.querySelector(".cap-order").checked) caps.push("order");
|
||||
if (card.querySelector(".cap-key").checked) caps.push("key");
|
||||
if (card.querySelector(".cap-trend").checked) caps.push("trend");
|
||||
const id = card.querySelector(".ex-id").value.trim();
|
||||
const stableKey = (card.dataset.key || id).trim();
|
||||
return {
|
||||
id: id,
|
||||
key: stableKey,
|
||||
name: tr.querySelector(".ex-name").value.trim(),
|
||||
flask_url: tr.querySelector(".ex-flask").value.trim(),
|
||||
agent_url: tr.querySelector(".ex-agent").value.trim(),
|
||||
review_url: tr.querySelector(".ex-review").value.trim(),
|
||||
enabled: tr.querySelector(".ex-enabled").checked,
|
||||
name: card.querySelector(".ex-name").value.trim(),
|
||||
flask_url: card.querySelector(".ex-flask").value.trim(),
|
||||
agent_url: card.querySelector(".ex-agent").value.trim(),
|
||||
review_url: card.querySelector(".ex-review").value.trim(),
|
||||
enabled: card.querySelector(".ex-enabled").checked,
|
||||
capabilities: caps,
|
||||
};
|
||||
}),
|
||||
@@ -471,18 +486,20 @@
|
||||
};
|
||||
document.getElementById("order-sltp-mode").onchange = function () {
|
||||
const pct = this.value === "pct";
|
||||
document.getElementById("order-sl").style.display = pct ? "none" : "";
|
||||
document.getElementById("order-tp").style.display = pct ? "none" : "";
|
||||
document.getElementById("order-sl-pct").style.display = pct ? "" : "none";
|
||||
document.getElementById("order-tp-pct").style.display = pct ? "" : "none";
|
||||
const slField = document.getElementById("order-sl").closest(".field");
|
||||
const tpField = document.getElementById("order-tp").closest(".field");
|
||||
if (slField) slField.style.display = pct ? "none" : "";
|
||||
if (tpField) tpField.style.display = pct ? "none" : "";
|
||||
document.getElementById("wrap-sl-pct").style.display = pct ? "" : "none";
|
||||
document.getElementById("wrap-tp-pct").style.display = pct ? "" : "none";
|
||||
};
|
||||
document.getElementById("key-sl-tp-mode").onchange = function () {
|
||||
const manual = this.value === "trend_manual";
|
||||
document.getElementById("key-manual-tp").style.display = manual ? "" : "none";
|
||||
document.getElementById("wrap-key-manual-tp").style.display = manual ? "" : "none";
|
||||
};
|
||||
document.getElementById("trend-direction").onchange = function () {
|
||||
const inp = document.getElementById("trend-add-upper");
|
||||
inp.placeholder = this.value === "short" ? "补仓下沿价" : "补仓上沿价";
|
||||
const lbl = document.getElementById("trend-add-label");
|
||||
if (lbl) lbl.textContent = this.value === "short" ? "补仓下沿价" : "补仓上沿价";
|
||||
};
|
||||
document.getElementById("btn-settings-save").onclick = saveSettings;
|
||||
document.getElementById("btn-settings-reload").onclick = loadSettingsUI;
|
||||
|
||||
Reference in New Issue
Block a user