refactor: unify three-exchange instance UI with shared templates
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,136 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=4"></script>
|
||||
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码" autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6375,7 +6375,7 @@ def login():
|
||||
return redirect("/")
|
||||
else:
|
||||
flash("账号或密码错误")
|
||||
return render_template("login.html")
|
||||
return render_template("login.html", exchange_display=EXCHANGE_DISPLAY_NAME)
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
@@ -7185,6 +7185,7 @@ def order_focus():
|
||||
selected_order=selected,
|
||||
default_timeframe=KLINE_TIMEFRAME,
|
||||
price_refresh_seconds=PRICE_REFRESH_SECONDS,
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,125 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=4"></script>
|
||||
|
||||
<title>系统登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码" autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,7 +44,16 @@ def order_rule_tips_template(exchange_key: str) -> str:
|
||||
|
||||
|
||||
def include_transfer_block(exchange_key: str) -> bool:
|
||||
return (exchange_key or "").strip().lower() == "gate"
|
||||
"""三所 standalone / embed 壳均在顶栏展示划转区块。"""
|
||||
return (exchange_key or "").strip().lower() in ORDER_RULE_TIPS_BY_EXCHANGE
|
||||
|
||||
|
||||
def ui_open_guard_enabled(exchange_key: str) -> bool:
|
||||
return (exchange_key or "").strip().lower() == "okx"
|
||||
|
||||
|
||||
def ui_orphan_recovery_enabled(exchange_key: str) -> bool:
|
||||
return (exchange_key or "").strip().lower() == "binance"
|
||||
|
||||
|
||||
def path_to_embed_tab(path: str) -> str | None:
|
||||
@@ -144,4 +153,6 @@ def embed_context_extras(exchange_key: str) -> dict:
|
||||
return {
|
||||
"order_rule_tips_tpl": order_rule_tips_template(exchange_key),
|
||||
"include_transfer_block": include_transfer_block(exchange_key),
|
||||
"ui_open_guard_enabled": ui_open_guard_enabled(exchange_key),
|
||||
"ui_orphan_recovery_enabled": ui_orphan_recovery_enabled(exchange_key),
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ funds_fmt(current_capital) }}U</div></div>
|
||||
</div>
|
||||
{% if include_transfer_block %}
|
||||
{% include 'gate_transfer_block.html' %}
|
||||
{% include 'instance_top_bar.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div id="embed-page-root">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{# 三所共用 standalone 主页 — 由 scripts/build_unified_index.py 生成,勿手改三所副本 #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
@@ -15,229 +16,7 @@
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px 20px}
|
||||
.container{width:100%;max-width:min(1440px,94vw);margin:0 auto;padding:0 clamp(8px,1.5vw,20px)}
|
||||
.header{display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:12px}
|
||||
.header h1{font-size:1.75rem;color:#dbe4ff;text-align:center;line-height:1.25}
|
||||
.exchange-tag{font-size:.82rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:5px 14px;border-radius:999px;letter-spacing:.06em}
|
||||
.header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}
|
||||
.top-nav{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:12px}
|
||||
.top-nav a{padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a;color:#8fc8ff;text-decoration:none}
|
||||
.top-nav a.active{background:#2a3f6c;color:#dbe4ff}
|
||||
.stat-box{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-bottom:16px;align-items:stretch}
|
||||
.stat-item{min-width:0;min-height:76px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:6px;background:#151a2a;padding:12px 10px;border-radius:10px;text-align:center;border:1px solid #2a3152}
|
||||
.stat-item .label{font-size:.8rem;color:#aaa;line-height:1.25;max-width:100%}
|
||||
.stat-item .value{font-size:1.25rem;font-weight:600;color:#fff;line-height:1.3;min-height:1.35em;display:flex;align-items:center;justify-content:center}
|
||||
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
||||
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150}
|
||||
.full{grid-column:1/-1}
|
||||
.card h2{font-size:1rem;margin-bottom:10px;color:#d4d9ff}
|
||||
.form-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center}
|
||||
.form-row > input:not([type=checkbox]):not([type=radio]),.form-row > select{flex:0 1 auto;width:10rem;max-width:200px;min-width:7rem}
|
||||
#add-order-form #sltp-mode{min-width:12.5rem;max-width:16rem;width:auto}
|
||||
.order-plan-preview{display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin:4px 0 10px;padding:10px 12px;background:#151a28;border:1px solid #2a3150;border-radius:8px;font-size:.85rem}
|
||||
.order-preview-risk{color:#ff6b6b}.order-preview-risk strong{color:#ff8f8f;font-weight:600}
|
||||
.order-preview-profit{color:#4cd97f}.order-preview-profit strong{color:#6ee7a0;font-weight:600}
|
||||
.order-preview-rr{color:#cfd3ef}.order-preview-rr strong{font-weight:600;color:#dbe4ff}
|
||||
.order-preview-rr.order-preview-rr-low strong{color:#ff8f8f}
|
||||
.order-preview-rr.order-preview-rr-ok strong{color:#8fc8ff}
|
||||
.form-row > button,.form-row > label{flex:0 0 auto}
|
||||
.form-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||||
/* 复盘表单:长下拉文案需可收缩,否则会撑破四列网格 */
|
||||
.journal-card .form-grid{gap:10px}
|
||||
.journal-card .form-grid > input,
|
||||
.journal-card .form-grid > select{
|
||||
min-width:0;
|
||||
width:100%;
|
||||
max-width:100%;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
.journal-card #journal-form textarea[name="note"]{
|
||||
display:block;width:100%;max-width:100%;box-sizing:border-box;margin-top:8px;
|
||||
}
|
||||
input,select,button,textarea{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff;font-size:.88rem;outline:none}
|
||||
button{background:linear-gradient(90deg,#4285f4,#7b42ff);border:none;cursor:pointer}
|
||||
.list{display:flex;flex-direction:column;gap:8px;margin-top:8px;max-height:240px;overflow:auto}
|
||||
.list-item{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:9px;background:#1a2034;border:1px solid #2a3150;border-radius:8px}
|
||||
.btn-del{padding:5px 9px;background:#2f2134;color:#ff7b7b;border-radius:8px;text-decoration:none;font-size:.8rem}
|
||||
.rule-tip{font-size:.8rem;color:#95a2c2;margin-bottom:8px}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{padding:8px;text-align:left;border-bottom:1px solid #25253b;font-size:.85rem}
|
||||
th{color:#a9a9ff}
|
||||
.badge{padding:2px 6px;border-radius:6px;font-size:.72rem}
|
||||
.profit{background:#1e332f;color:#4cd97f}
|
||||
.loss{background:#331e24;color:#ff6666}
|
||||
.miss{background:#29241e;color:#eac147}
|
||||
.direction{background:#1e2533;color:#4cc2ff}
|
||||
.direction-long{background:#1e332f;color:#4cd97f}
|
||||
.direction-short{background:#331e24;color:#ff6666}
|
||||
.pnl-profit{color:#4cd97f;font-weight:600}
|
||||
.pnl-loss{color:#ff6666;font-weight:600}
|
||||
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
|
||||
form.is-form-submitting{opacity:.88;pointer-events:none}
|
||||
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
|
||||
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
|
||||
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
|
||||
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
|
||||
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
|
||||
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
|
||||
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
|
||||
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
|
||||
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
|
||||
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
|
||||
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
|
||||
.price-up{color:#4cd97f}
|
||||
.price-down{color:#ff6666}
|
||||
.price-flat{color:#cfd3ef}
|
||||
.panel-list{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.panel-item{background:#141423;border:1px solid #24243b;border-radius:10px;padding:10px;max-height:260px;overflow:auto}
|
||||
.entry{border-bottom:1px solid #2b2b43;padding:8px 0}
|
||||
.entry:last-child{border-bottom:none}
|
||||
.table-del{padding:4px 8px;background:#2f2134;color:#ff7b7b;border:none;border-radius:6px;cursor:pointer;font-size:.78rem}
|
||||
.mood-grid{display:flex;gap:10px;flex-wrap:wrap;font-size:.82rem;color:#d7d7ea}
|
||||
.mood-grid label{display:flex;align-items:center;gap:3px}
|
||||
.screenshot{width:100px;border-radius:6px;cursor:pointer;margin-top:6px}
|
||||
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1210}
|
||||
.modal img{max-width:90%;max-height:90%;border-radius:8px}
|
||||
.detail-modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1200;padding:20px}
|
||||
.detail-modal .panel{width:min(92vw,980px);max-height:88vh;overflow:auto;background:#121726;border:1px solid #2a3150;border-radius:10px;padding:14px}
|
||||
.detail-modal .panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}
|
||||
.detail-modal .panel-title{font-size:1rem;color:#dbe4ff}
|
||||
.detail-modal .panel-close{padding:6px 10px;background:#2f2134;color:#ffb2b2;border:none;border-radius:8px;cursor:pointer}
|
||||
.detail-modal .panel-body{white-space:pre-wrap;line-height:1.5;font-size:.86rem;color:#e5e9ff}
|
||||
.detail-modal .panel-image{margin-top:10px;max-width:min(100%,680px);border-radius:8px;cursor:pointer;border:1px solid #2a3150}
|
||||
.detail-modal .panel-actions{display:flex;gap:8px;align-items:center;flex-shrink:0}
|
||||
.detail-modal .panel-fs{padding:6px 10px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem}
|
||||
.detail-modal.fullscreen{padding:10px}
|
||||
.detail-modal.fullscreen .panel{width:100%;height:100%;max-width:none;max-height:none;display:flex;flex-direction:column;overflow:hidden}
|
||||
.detail-modal.fullscreen .panel-body{flex:1;overflow:auto;min-height:0;font-size:.9rem}
|
||||
.ai-result-wrap{margin-top:8px}
|
||||
.ai-result-toolbar{display:flex;gap:8px;margin-top:6px}
|
||||
.ai-result-toolbar .btn-fs{padding:4px 10px;font-size:.78rem;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:6px;cursor:pointer}
|
||||
.table-wrap{overflow-x:auto}
|
||||
.dual-panel-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
|
||||
.dual-panel-grid .card{height:100%;display:flex;flex-direction:column}
|
||||
.panel-scroll{flex:1;min-height:280px;max-height:420px;overflow:auto}
|
||||
.records-card{grid-column:1/-1}
|
||||
.review-card{grid-column:1/-1}
|
||||
.review-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap}
|
||||
.review-card-head h2{margin:0}
|
||||
.review-card-fs-btn{padding:6px 12px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;white-space:nowrap}
|
||||
.review-card-fs-btn:hover{filter:brightness(1.08)}
|
||||
body.review-card-fullscreen-open{overflow:hidden}
|
||||
.review-card.is-fullscreen{
|
||||
position:fixed;inset:12px;z-index:1100;margin:0;
|
||||
width:auto !important;max-width:none;height:auto;
|
||||
overflow:auto;display:flex;flex-direction:column;
|
||||
box-shadow:0 12px 48px rgba(0,0,0,.55);
|
||||
}
|
||||
.review-card.is-fullscreen .panel-list{flex:1;min-height:320px}
|
||||
.review-card.is-fullscreen .panel-item{max-height:none;height:auto;min-height:280px}
|
||||
.review-card.is-fullscreen .ai-result{max-height:min(36vh, 320px)}
|
||||
@media (max-width: 1200px){
|
||||
.stat-box{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}
|
||||
}
|
||||
@media (min-width: 1440px){
|
||||
.panel-scroll,.pos-list{max-height:420px}
|
||||
.records-card .table-wrap{max-height:620px;overflow:auto}
|
||||
}
|
||||
@media (min-width: 2200px){
|
||||
.container{max-width:min(1720px,90vw)}
|
||||
}
|
||||
@media (min-width: 2560px){
|
||||
.container{max-width:min(1860px,88vw)}
|
||||
.dual-panel-grid{gap:18px}
|
||||
}
|
||||
@media (min-width: 3000px){
|
||||
.container{max-width:min(1980px,86vw)}
|
||||
.pos-grid{grid-template-columns:repeat(4,minmax(0,1fr))}
|
||||
}
|
||||
@media (max-width: 1100px){
|
||||
.grid{grid-template-columns:1fr}
|
||||
.dual-panel-grid{grid-template-columns:1fr}
|
||||
.records-card,.review-card{grid-column:auto}
|
||||
.panel-list{grid-template-columns:1fr}
|
||||
}
|
||||
@media (max-width: 960px){
|
||||
body{padding:10px}
|
||||
.form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.stat-box{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
}
|
||||
.stats-detail{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-top:10px}
|
||||
.stats-detail .stat-item{min-width:0;min-height:0;display:block;text-align:left;padding:10px 12px;align-items:stretch;gap:4px}
|
||||
.stats-detail .stat-item .value{min-height:0;display:block;font-size:1.05rem}
|
||||
.stats-detail .stat-item .label{font-size:.75rem}
|
||||
.stats-detail .stat-item .value{font-size:1.05rem;word-break:break-all}
|
||||
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
|
||||
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
|
||||
.export-bar a:hover{background:#1f2740}
|
||||
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
|
||||
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
|
||||
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
|
||||
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
|
||||
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||
.key-history .list{max-height:200px}
|
||||
.pos-section{margin-top:12px}
|
||||
.pos-section-title{font-size:.82rem;color:#8892b0;margin-bottom:8px;font-weight:500}
|
||||
.pos-list{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto}
|
||||
.dual-panel-grid .pos-list-live{max-height:none;overflow:visible;flex:1 1 auto}
|
||||
.dual-panel-grid .panel-scroll.pos-list-live{max-height:none;overflow:visible}
|
||||
.pos-card{background:#141923;border:1px solid #2a3348;border-radius:10px;padding:12px 14px}
|
||||
.pos-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.pos-meta{font-size:.74rem;color:#8b95a8;line-height:1.45;margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:4px 0}
|
||||
.pos-meta-item{display:inline-flex;align-items:center}
|
||||
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
|
||||
.pos-meta-on{color:#6eb5ff}
|
||||
.pos-meta-off{color:#7d8799}
|
||||
.pos-breakeven-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:600;background:#1a3d2e;color:#4cd97f}
|
||||
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
||||
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
|
||||
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
|
||||
.pos-side-long{background:#253a6e;color:#6eb5ff}
|
||||
.pos-side-short{background:#4a2230;color:#ff8a8a}
|
||||
.pos-head-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
||||
.pos-entrust-btn{padding:6px 12px;background:#2a4a7a;color:#8fc8ff;border:none;border-radius:8px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap}
|
||||
.pos-entrust-btn:hover{background:#355d96}
|
||||
.pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap;border:none;cursor:pointer;display:inline-block}
|
||||
.pos-close-btn:hover{background:#d66565;color:#fff}
|
||||
.pos-ex-orders{margin-top:10px;padding-top:10px;border-top:1px dashed #2a3348}
|
||||
.pos-ex-orders-title{font-size:.74rem;color:#7d8799;margin-bottom:6px}
|
||||
.pos-ex-order-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.78rem;color:#c5cce0;margin-top:5px}
|
||||
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
||||
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
||||
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
||||
.tpsl-modal-backdrop.open{display:flex}
|
||||
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
||||
.tpsl-modal h3{margin:0 0 12px;font-size:1rem;color:#fff}
|
||||
.tpsl-modal .form-row{margin-bottom:10px}
|
||||
.tpsl-modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
|
||||
.tpsl-modal-actions button{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem}
|
||||
.tpsl-modal-submit{background:#2d6a4f;color:#fff}
|
||||
.tpsl-modal-cancel{background:#3a3f52;color:#ddd}
|
||||
.pos-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;margin-bottom:12px}
|
||||
.pos-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||||
.pos-label{font-size:.72rem;color:#7d8799}
|
||||
.pos-value{font-size:.88rem;color:#e8ecf4;font-weight:500;line-height:1.25}
|
||||
.pos-val-dash{opacity:.75;color:#8b95a8}
|
||||
.pos-value.price-up{color:#4cd97f}
|
||||
.pos-value.price-down{color:#ff6666}
|
||||
.pos-value.price-flat{color:#e8ecf4}
|
||||
.pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689}
|
||||
.pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
|
||||
@media (max-width:520px){.pos-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.stats-card{grid-column:1/-1;margin-top:14px}
|
||||
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
|
||||
.stats-card.collapsed .stats-content{display:none}
|
||||
.stats-period-block{margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid #2a3150}
|
||||
.stats-period-block:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}
|
||||
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
|
||||
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=1">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=53">
|
||||
|
||||
</head>
|
||||
@@ -335,7 +114,7 @@
|
||||
<div class="stat-item"><div class="label">交易日</div><div class="value">{{ trading_day }}</div></div>
|
||||
<div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ funds_fmt(current_capital) }}U</div></div>
|
||||
</div>
|
||||
{% include 'gate_transfer_block.html' %}
|
||||
{% include 'instance_top_bar.html' %}
|
||||
|
||||
<div class="grid">
|
||||
{% if page == 'key_monitor' %}
|
||||
@@ -351,7 +130,7 @@
|
||||
<span class="btn-del" style="background:#2f2f44;color:#9aa;cursor:not-allowed">暂无持仓可放大</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'order_monitor_rule_tips_gate.html' %}
|
||||
{% include order_rule_tips_tpl %}
|
||||
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
|
||||
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
|
||||
{{ trade_policy_symbol('symbol', 'order-symbol') }}
|
||||
@@ -395,6 +174,21 @@
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||
{% if ui_orphan_recovery_enabled %}
|
||||
{% if not order and orphan_live_positions %}
|
||||
{% set o = orphan_live_positions[0] %}
|
||||
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:block;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8">
|
||||
检测到交易所仍有 <strong>{{ o.symbol }}</strong> {{ '空' if o.direction == 'short' else '多' }}仓,但本地监控已中断(误同步时可能无交易记录)。
|
||||
{% if o.recoverable_monitor_id %}
|
||||
<button type="button" class="pos-entrust-btn" onclick="recoverLivePosition({{ o.recoverable_monitor_id }})">恢复监控{% if o.plan_stop_loss and o.plan_take_profit %}并挂止盈止损{% endif %}</button>
|
||||
{% else %}
|
||||
未找到可恢复的监控记录,需在服务器数据库处理。
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:none;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8"></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="panel-scroll pos-list pos-list-live">
|
||||
{% for o in order %}
|
||||
<div class="pos-card" id="order-row-{{ o.id }}"
|
||||
@@ -1633,6 +1427,46 @@ function paintPriceTrend(el, key, value){
|
||||
lastPriceMap[key] = value;
|
||||
}
|
||||
|
||||
function renderOrphanRecoverBanner(orphans){
|
||||
const el = document.getElementById("orphan-position-recover");
|
||||
if(!el) return;
|
||||
const liveCards = document.querySelectorAll(".pos-list-live .pos-card");
|
||||
if(liveCards.length > 0 || !orphans || !orphans.length){
|
||||
el.style.display = "none";
|
||||
el.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const o = orphans[0];
|
||||
const dir = o.direction === "short" ? "空" : "多";
|
||||
const mid = o.recoverable_monitor_id;
|
||||
let html = `检测到交易所仍有 <strong>${o.symbol}</strong> ${dir}仓,但本地监控已中断(误同步时可能无交易记录)。`;
|
||||
if(mid){
|
||||
const tpslHint = (o.plan_stop_loss && o.plan_take_profit) ? "并挂止盈止损" : "";
|
||||
html += ` <button type="button" class="pos-entrust-btn" onclick="recoverLivePosition(${mid})">恢复监控${tpslHint}</button>`;
|
||||
} else {
|
||||
html += " 未找到可恢复的监控记录,需在服务器数据库处理。";
|
||||
}
|
||||
el.innerHTML = html;
|
||||
el.style.display = "block";
|
||||
}
|
||||
|
||||
async function recoverLivePosition(monitorId){
|
||||
const withTpsl = confirm("确认恢复本地实时监控?若原计划有止盈止损,将尝试重新挂到交易所。");
|
||||
if(!withTpsl) return;
|
||||
try{
|
||||
const res = await fetch("/api/recover_live_position", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({monitor_id: monitorId, place_tpsl: true})
|
||||
});
|
||||
const data = await res.json();
|
||||
alert(data.msg || (data.ok ? "已恢复" : "失败"));
|
||||
if(data.ok) location.reload();
|
||||
}catch(e){
|
||||
alert("恢复失败:" + e);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPriceSnapshot(){
|
||||
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
|
||||
const updatedEl = document.getElementById("price-last-updated");
|
||||
@@ -1713,7 +1547,11 @@ function refreshPriceSnapshot(){
|
||||
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
||||
paintPlanTpslDisplay(o.id, o);
|
||||
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o);
|
||||
if(window.TimeCloseUI) TimeCloseUI.paintOrderForceClose(o);
|
||||
});
|
||||
{% if ui_orphan_recovery_enabled %}
|
||||
renderOrphanRecoverBanner(data.orphan_live_positions);
|
||||
{% endif %}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
@@ -1781,20 +1619,17 @@ function refreshAccountSnapshot(){
|
||||
TimeCloseUI.paintForceCloseHeader(data.force_close);
|
||||
}
|
||||
let canTradeText = "可开仓";
|
||||
if (!data.can_trade) {
|
||||
if(!data.can_trade){
|
||||
const parts = [];
|
||||
if (data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason) {
|
||||
parts.push(data.risk_status.reason);
|
||||
}
|
||||
const ac = Number(data.active_count || 0);
|
||||
const max = Number(data.max_active_positions || {{ max_active_positions }});
|
||||
if (ac >= max) parts.push(`持仓 ${ac}/${max}`);
|
||||
if((data.active_count||0) >= (data.max_active_positions||{{ max_active_positions }})) parts.push(`持仓 ${data.active_count}/${data.max_active_positions}`);
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const opens = Number(data.opens_today);
|
||||
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
|
||||
if (!parts.length) parts.push(`未到北京时间 {{ reset_hour }}:00`);
|
||||
else parts.push(`或未到北京时间 {{ reset_hour }}:00`);
|
||||
canTradeText = `不可开仓(${parts.join(";")})`;
|
||||
if(data.open_guard_blocks_now) parts.push(`未到北京时间 ${data.reset_hour||{{ reset_hour }}}:00`);
|
||||
canTradeText = parts.length ? `不可开仓(${parts.join(";")})` : "不可开仓";
|
||||
}
|
||||
const opensToday = Number(data.opens_today);
|
||||
const hardLim = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
@@ -1807,9 +1642,36 @@ function refreshAccountSnapshot(){
|
||||
if(tip){
|
||||
tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${openCntTxt ? openCntTxt + ";" : ""}${canTradeText}${avail};人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1`;
|
||||
}
|
||||
const allowEl = document.getElementById("allow-open-before-reset");
|
||||
const guardStatus = document.getElementById("open-guard-status");
|
||||
const resetH = data.reset_hour != null ? data.reset_hour : {{ reset_hour }};
|
||||
if(allowEl && typeof data.open_guard_enabled !== "undefined"){
|
||||
allowEl.checked = !data.open_guard_enabled;
|
||||
}
|
||||
if(guardStatus && typeof data.open_guard_enabled !== "undefined"){
|
||||
guardStatus.innerText = data.open_guard_enabled
|
||||
? `已限制:${resetH}:00 前不可开仓`
|
||||
: `已放开:${resetH}:00 前允许开仓`;
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
{% if ui_open_guard_enabled %}
|
||||
const allowOpenBeforeResetEl = document.getElementById("allow-open-before-reset");
|
||||
if(allowOpenBeforeResetEl){
|
||||
allowOpenBeforeResetEl.addEventListener("change", function(){
|
||||
const allow = !!this.checked;
|
||||
fetch("/api/settings/open_guard", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({enabled: !allow}),
|
||||
}).then(r=>r.json()).then(data=>{
|
||||
if(!data.ok){ alert(data.msg || "保存失败"); return; }
|
||||
refreshAccountSnapshot();
|
||||
}).catch(()=>alert("保存失败"));
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
const orderSymbolEl = document.getElementById("order-symbol");
|
||||
const orderDirectionEl = document.getElementById("order-direction");
|
||||
const fullMarginEl = document.getElementById("use-full-margin");
|
||||
@@ -2011,6 +1873,9 @@ function refreshPriceSnapshotConditional(){
|
||||
}
|
||||
});
|
||||
tickOrderHoldDurations();
|
||||
{% if ui_orphan_recovery_enabled %}
|
||||
renderOrphanRecoverBanner(data.orphan_live_positions);
|
||||
{% endif %}
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{# 三所统一顶栏:划转 + 手动划转 + 可选整点前开仓开关 #}
|
||||
{% include 'gate_transfer_block.html' %}
|
||||
{% if ui_open_guard_enabled %}
|
||||
<div class="rule-tip" id="open-guard-bar" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#cfd3ef">
|
||||
<input type="checkbox" id="allow-open-before-reset" {% if not open_guard_enabled %}checked{% endif %}>
|
||||
允许北京时间 {{ reset_hour }}:00 前开仓(斐波成交登记、人工下单)
|
||||
</label>
|
||||
<span id="open-guard-status" style="color:#8892b0;font-size:.75rem">
|
||||
{% if open_guard_enabled %}已限制:{{ reset_hour }}:00 前不可开仓{% else %}已放开:{{ reset_hour }}:00 前允许开仓{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,136 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=4"></script>
|
||||
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码" autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="/static/instance_theme.js?v=4"></script>
|
||||
|
||||
<title>登录 · {{ exchange_display }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0a10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.login-box {
|
||||
background: #12121a;
|
||||
padding: 2.5rem;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid #242435;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-box h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #a9a9ff;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2e2e45;
|
||||
background: #1a1a29;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #4cc2ff;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: linear-gradient(90deg, #4285f4, #7b42ff);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.flash {
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #331e24;
|
||||
color: #ff6666;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.exchange-line {
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: #8892b0;
|
||||
margin: -0.5rem 0 1.25rem;
|
||||
}
|
||||
.exchange-line strong {
|
||||
color: #b8f5d0;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
|
||||
|
||||
</head>
|
||||
<div class="login-theme-bar">
|
||||
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
|
||||
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
|
||||
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码" autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="tip-collapse-hint">· 划转规则</span>
|
||||
</summary>
|
||||
<div class="tip-collapse-body rule-tip gate-transfer-tip">
|
||||
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;将 {{ auto_transfer_to }} 调整至 {{ transfer_amount_fmt }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};<strong>持仓中不划转</strong>并微信通知)
|
||||
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;将 {{ auto_transfer_to }} 调整至 {{ transfer_amount_fmt|default(funds_fmt(auto_transfer_amount)) }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};<strong>持仓中不划转</strong>并微信通知)
|
||||
</div>
|
||||
</details>
|
||||
<form action="/manual_transfer" method="post" class="form-row gate-transfer-form">
|
||||
|
||||
@@ -1,33 +1,63 @@
|
||||
"""Build embed_page_fragment.html from gate index.html."""
|
||||
"""Build embed_page_fragment.html from lib/instance/templates/index.html."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
src_lines = (ROOT / "crypto_monitor_gate" / "templates" / "index.html").read_text(
|
||||
encoding="utf-8"
|
||||
).splitlines()
|
||||
SRC = ROOT / "lib" / "instance" / "templates" / "index.html"
|
||||
OUT = ROOT / "lib" / "instance" / "templates" / "embed_page_fragment.html"
|
||||
|
||||
# 1-based line numbers from index.html
|
||||
macro_body = src_lines[243:262] # {% macro %} … {% endmacro %}
|
||||
grid_inner = src_lines[328:736] # inside .grid (exclude outer wrapper)
|
||||
stats_block = src_lines[738:772]
|
||||
GRID_START = ' <div class="grid">'
|
||||
STATS_START = ' <div class="card full stats-card'
|
||||
|
||||
out_lines = [
|
||||
"{# Hub iframe tab fragment — shared via embed_templates #}",
|
||||
*macro_body,
|
||||
'<div class="grid">',
|
||||
*grid_inner,
|
||||
"</div>",
|
||||
*stats_block,
|
||||
]
|
||||
|
||||
out_dir = ROOT / "lib" / "instance" / "templates"
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
text = "\n".join(out_lines) + "\n"
|
||||
text = text.replace(
|
||||
"{% include 'order_monitor_rule_tips_gate.html' %}",
|
||||
"{% include order_rule_tips_tpl %}",
|
||||
)
|
||||
(out_dir / "embed_page_fragment.html").write_text(text, encoding="utf-8")
|
||||
print("wrote", out_dir / "embed_page_fragment.html", "lines", len(out_lines))
|
||||
def _slice_between(lines: list[str], start: str, end: str | None) -> list[str]:
|
||||
try:
|
||||
i = next(idx for idx, line in enumerate(lines) if line == start)
|
||||
except StopIteration:
|
||||
raise SystemExit(f"marker not found: {start!r}")
|
||||
if end is None:
|
||||
return lines[i:]
|
||||
try:
|
||||
j = next(idx for idx, line in enumerate(lines[i + 1 :], i + 1) if line.startswith(end))
|
||||
except StopIteration:
|
||||
raise SystemExit(f"end marker not found: {end!r}")
|
||||
return lines[i:j]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lines = SRC.read_text(encoding="utf-8").splitlines()
|
||||
macro_start = next(i for i, l in enumerate(lines) if l.startswith("{% macro period_stats"))
|
||||
macro_end = next(i for i, l in enumerate(lines) if l.strip() == "{% endmacro %}")
|
||||
macro_body = lines[macro_start : macro_end + 1]
|
||||
|
||||
grid_block = _slice_between(lines, GRID_START, STATS_START)
|
||||
# strip outer .grid wrapper; fragment adds its own
|
||||
if grid_block and grid_block[0] == GRID_START:
|
||||
grid_block = grid_block[1:]
|
||||
if grid_block and grid_block[-1].strip() == "</div>":
|
||||
# only remove closing div if it closes .grid (heuristic: last line before stats)
|
||||
pass
|
||||
|
||||
stats_block = _slice_between(lines, STATS_START, " </div>")
|
||||
|
||||
out_lines = [
|
||||
"{# Hub iframe tab fragment — shared via embed_templates #}",
|
||||
*macro_body,
|
||||
'<div class="grid">',
|
||||
*grid_block,
|
||||
"</div>",
|
||||
*stats_block,
|
||||
]
|
||||
text = "\n".join(out_lines).rstrip() + "\n"
|
||||
if "order_rule_tips_tpl" not in text:
|
||||
text = text.replace(
|
||||
"{% include 'order_monitor_rule_tips_binance.html' %}",
|
||||
"{% include order_rule_tips_tpl %}",
|
||||
)
|
||||
OUT.write_text(text, encoding="utf-8")
|
||||
print("wrote", OUT, "lines", len(out_lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 binance index.html 生成三所共用的 lib/instance/templates/index.html。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "lib" / "instance" / "templates" / "index.html"
|
||||
OUT = ROOT / "lib" / "instance" / "templates" / "index.html"
|
||||
|
||||
TRANSFER_BLOCK = """ <details class="tip-collapse transfer-rule-collapse">
|
||||
<summary class="tip-collapse-summary">划转规则说明</summary>
|
||||
<div class="tip-collapse-body rule-tip">
|
||||
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;将 {{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};<strong>持仓中不划转</strong>并微信通知)
|
||||
</div>
|
||||
</details>
|
||||
<form action="/manual_transfer" method="post" class="form-row">
|
||||
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
|
||||
<select name="from_account">
|
||||
<option value="funding" {% if auto_transfer_from == 'funding' %}selected{% endif %}>from: funding</option>
|
||||
<option value="swap" {% if auto_transfer_from == 'swap' %}selected{% endif %}>from: swap</option>
|
||||
<option value="spot" {% if auto_transfer_from == 'spot' %}selected{% endif %}>from: spot</option>
|
||||
</select>
|
||||
<select name="to_account">
|
||||
<option value="swap" {% if auto_transfer_to == 'swap' %}selected{% endif %}>to: swap</option>
|
||||
<option value="funding" {% if auto_transfer_to == 'funding' %}selected{% endif %}>to: funding</option>
|
||||
<option value="spot" {% if auto_transfer_to == 'spot' %}selected{% endif %}>to: spot</option>
|
||||
</select>
|
||||
<button type="submit">手动划转</button>
|
||||
</form>
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
|
||||
# 外链 CSS 替代内联 style
|
||||
text = re.sub(
|
||||
r" <style>.*?</style>\n",
|
||||
' <link rel="stylesheet" href="/static/instance_page.css?v=1">\n',
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# 顶栏:划转 + 可选 open guard
|
||||
text = text.replace(
|
||||
' <div class="rule-tip">实时价格更新:<span id="price-last-updated">--</span>(北京时间 UTC+8)</div>\n',
|
||||
" {% include 'instance_top_bar.html' %}\n",
|
||||
)
|
||||
|
||||
# 规则条动态 include
|
||||
text = text.replace(
|
||||
"{% include 'order_monitor_rule_tips_binance.html' %}",
|
||||
"{% include order_rule_tips_tpl %}",
|
||||
)
|
||||
|
||||
# 下单面板内划转块移除(已上移到顶栏)
|
||||
if TRANSFER_BLOCK in text:
|
||||
text = text.replace(TRANSFER_BLOCK, "", 1)
|
||||
|
||||
# 孤儿仓恢复 banner
|
||||
orphan_block = """ {% if not order and orphan_live_positions %}
|
||||
{% set o = orphan_live_positions[0] %}
|
||||
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:block;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8">
|
||||
<strong>检测到交易所仍有持仓,本地无对应监控单</strong>
|
||||
<span style="margin-left:8px">{{ o.exchange_symbol or o.symbol }} · {{ '多' if o.direction == 'long' else '空' }}</span>
|
||||
<form action="/recover_orphan_order" method="post" style="display:inline;margin-left:12px">
|
||||
<input type="hidden" name="symbol" value="{{ o.symbol }}">
|
||||
<input type="hidden" name="direction" value="{{ o.direction }}">
|
||||
<button type="submit" style="padding:4px 10px;font-size:.82rem">恢复监控</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:none;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8"></div>
|
||||
{% endif %}"""
|
||||
wrapped = "{% if ui_orphan_recovery_enabled %}\n" + orphan_block + "\n {% endif %}"
|
||||
text = text.replace(orphan_block, wrapped, 1)
|
||||
|
||||
# refreshAccountSnapshot:采用 OKX 版 open_guard 逻辑
|
||||
old_can_trade = """ let canTradeText = "可开仓";
|
||||
if (!data.can_trade) {
|
||||
const parts = [];
|
||||
if (data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason) {
|
||||
parts.push(data.risk_status.reason);
|
||||
}
|
||||
const ac = Number(data.active_count || 0);
|
||||
const max = Number(data.max_active_positions || {{ max_active_positions }});
|
||||
if (ac >= max) parts.push(`持仓 ${ac}/${max}`);
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const opens = Number(data.opens_today);
|
||||
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
|
||||
if (!parts.length) parts.push(`未到北京时间 {{ reset_hour }}:00`);
|
||||
else parts.push(`或未到北京时间 {{ reset_hour }}:00`);
|
||||
canTradeText = `不可开仓(${parts.join(";")})`;
|
||||
}"""
|
||||
new_can_trade = """ let canTradeText = "可开仓";
|
||||
if(!data.can_trade){
|
||||
const parts = [];
|
||||
if (data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason) {
|
||||
parts.push(data.risk_status.reason);
|
||||
}
|
||||
if((data.active_count||0) >= (data.max_active_positions||{{ max_active_positions }})) parts.push(`持仓 ${data.active_count}/${data.max_active_positions}`);
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const opens = Number(data.opens_today);
|
||||
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
|
||||
if(data.open_guard_blocks_now) parts.push(`未到北京时间 ${data.reset_hour||{{ reset_hour }}}:00`);
|
||||
canTradeText = parts.length ? `不可开仓(${parts.join(";")})` : "不可开仓";
|
||||
}"""
|
||||
text = text.replace(old_can_trade, new_can_trade, 1)
|
||||
|
||||
guard_sync = """ const allowEl = document.getElementById("allow-open-before-reset");
|
||||
const guardStatus = document.getElementById("open-guard-status");
|
||||
const resetH = data.reset_hour != null ? data.reset_hour : {{ reset_hour }};
|
||||
if(allowEl && typeof data.open_guard_enabled !== "undefined"){
|
||||
allowEl.checked = !data.open_guard_enabled;
|
||||
}
|
||||
if(guardStatus && typeof data.open_guard_enabled !== "undefined"){
|
||||
guardStatus.innerText = data.open_guard_enabled
|
||||
? `已限制:${resetH}:00 前不可开仓`
|
||||
: `已放开:${resetH}:00 前允许开仓`;
|
||||
}"""
|
||||
insert_after = """ if(tip){
|
||||
tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${openCntTxt ? openCntTxt + ";" : ""}${canTradeText}${avail};人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1`;
|
||||
}
|
||||
}).catch(()=>{});"""
|
||||
if guard_sync not in text:
|
||||
text = text.replace(
|
||||
insert_after,
|
||||
insert_after.replace(" }).catch(()=>{});", guard_sync + "\n }).catch(()=>{});"),
|
||||
1,
|
||||
)
|
||||
|
||||
open_guard_js = """
|
||||
const allowOpenBeforeResetEl = document.getElementById("allow-open-before-reset");
|
||||
if(allowOpenBeforeResetEl){
|
||||
allowOpenBeforeResetEl.addEventListener("change", function(){
|
||||
const allow = !!this.checked;
|
||||
fetch("/api/settings/open_guard", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({enabled: !allow}),
|
||||
}).then(r=>r.json()).then(data=>{
|
||||
if(!data.ok){ alert(data.msg || "保存失败"); return; }
|
||||
refreshAccountSnapshot();
|
||||
}).catch(()=>alert("保存失败"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
marker = "const orderSymbolEl = document.getElementById(\"order-symbol\");"
|
||||
if "allowOpenBeforeResetEl" not in text:
|
||||
text = text.replace(marker, "{% if ui_open_guard_enabled %}" + open_guard_js + "{% endif %}\n" + marker, 1)
|
||||
|
||||
orphan_fn_guard = "{% if ui_orphan_recovery_enabled %}\n renderOrphanRecoverBanner(data.orphan_live_positions);\n {% endif %}"
|
||||
text = text.replace(
|
||||
" renderOrphanRecoverBanner(data.orphan_live_positions);",
|
||||
orphan_fn_guard,
|
||||
)
|
||||
|
||||
header = "{# 三所共用 standalone 主页 — 由 scripts/build_unified_index.py 生成,勿手改三所副本 #}\n"
|
||||
if not text.startswith("{# 三所共用"):
|
||||
text = header + text
|
||||
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(text, encoding="utf-8")
|
||||
print("wrote", OUT, "lines", len(text.splitlines()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,11 @@
|
||||
from lib.instance.instance_embed_lib import (
|
||||
EMBED_TABS,
|
||||
embed_context_extras,
|
||||
include_transfer_block,
|
||||
path_to_embed_tab,
|
||||
rewrite_embed_dest,
|
||||
ui_open_guard_enabled,
|
||||
ui_orphan_recovery_enabled,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,3 +28,15 @@ def test_embed_tabs_cover_main_nav():
|
||||
assert "trade" in EMBED_TABS
|
||||
assert "key_monitor" in EMBED_TABS
|
||||
assert "records" in EMBED_TABS
|
||||
|
||||
|
||||
def test_embed_context_extras_unified_ui_flags():
|
||||
for ex in ("binance", "okx", "gate"):
|
||||
assert include_transfer_block(ex) is True
|
||||
assert ui_open_guard_enabled("okx") is True
|
||||
assert ui_open_guard_enabled("binance") is False
|
||||
assert ui_orphan_recovery_enabled("binance") is True
|
||||
assert ui_orphan_recovery_enabled("gate") is False
|
||||
ctx = embed_context_extras("gate")
|
||||
assert ctx["order_rule_tips_tpl"] == "order_monitor_rule_tips_gate.html"
|
||||
assert ctx["include_transfer_block"] is True
|
||||
|
||||
Reference in New Issue
Block a user