feat: add light/dark theme to exchange instances with hub SSO sync

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 12:52:27 +08:00
parent 6f8f0968c8
commit d14c629778
24 changed files with 3134 additions and 2369 deletions
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32"> <link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -229,6 +231,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .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} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head> </head>
<body data-page="{{ page }}"> <body data-page="{{ page }}">
{% macro period_stats(title, s) %} {% macro period_stats(title, s) %}
@@ -253,7 +257,21 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>加密货币|交易监控 + AI复盘一体化</h1> <h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<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>
</div> </div>
<div class="top-nav"> <div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a> <a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
+277 -260
View File
@@ -1,261 +1,278 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ exchange_display }} | 关键位放大</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>{{ exchange_display }} | 关键位放大</title>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px} #chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
</style> #chart{width:100%;height:100%}
</head> .exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <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">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>币种</label> </svg>
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT"> </button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<label>关键位</label> <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">
<select id="key-id"> <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"/>
<option value="">无(仅看K线)</option> </svg>
{% for k in key_list %} </button>
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option> </div>
{% endfor %}
</select> <div class="row">
<a class="btn" href="/">返回首页</a>
<label>周期</label> <strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
<select id="timeframe"> </div>
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <div class="status">最近刷新:<span id="updated-at">--</span></div>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> </div>
{% endfor %}
</select> <div class="row" style="margin-top:10px">
<label>币种</label>
<label>K线数</label> <input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
<select id="kline-limit">
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option> <label>关键位</label>
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option> <select id="key-id">
</select> <option value="">无(仅看K线)</option>
{% for k in key_list %}
<button id="manual-refresh" type="button">刷新</button> <option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
<span id="load-status" class="status"></span> {% endfor %}
</div> </select>
</div>
<label>周期</label>
<div class="card"> <select id="timeframe">
<div class="meta"> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> </select>
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div> <label>K线数</label>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <select id="kline-limit">
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div> <option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div> <option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
</div> </select>
</div>
<button id="manual-refresh" type="button">刷新</button>
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div> <span id="load-status" class="status"></span>
</div> </div>
</div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script> <div class="card">
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta">
const keySelect = document.getElementById("key-id"); <div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
const symbolInput = document.getElementById("symbol-input"); <div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
const limitSelect = document.getElementById("kline-limit"); <div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const chartHost = document.getElementById("chart"); <div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d); <div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
const fmtSigned = (v,d=4)=>{ </div>
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-"; </div>
const n = Number(v);
return `${n>0?"+":""}${n.toFixed(d)}`; <div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
}; </div>
let chart = null; <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
let candleSeries = null; <script>
let priceLines = []; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
const keyMap = {}; const keySelect = document.getElementById("key-id");
{% for k in key_list %} const symbolInput = document.getElementById("symbol-input");
keyMap["{{ k.id }}"] = "{{ k.symbol }}"; const tfSelect = document.getElementById("timeframe");
{% endfor %} const limitSelect = document.getElementById("kline-limit");
const statusEl = document.getElementById("load-status");
function ensureChart(){ const updatedAtEl = document.getElementById("updated-at");
if(chart && candleSeries) return true; const chartHost = document.getElementById("chart");
if(!window.LightweightCharts){ const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
statusEl.className = "status err"; const fmtSigned = (v,d=4)=>{
statusEl.innerText = "图表库加载失败"; if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
return false; const n = Number(v);
} return `${n>0?"+":""}${n.toFixed(d)}`;
};
if(!chart){
chart = LightweightCharts.createChart(chartHost, { let chart = null;
layout:{background:{color:"#0f1320"},textColor:"#d6deff"}, let candleSeries = null;
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}}, let priceLines = [];
rightPriceScale:{borderColor:"#2a3150"}, const keyMap = {};
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false}, {% for k in key_list %}
crosshair:{mode:0} keyMap["{{ k.id }}"] = "{{ k.symbol }}";
}); {% endfor %}
window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); function ensureChart(){
}); if(chart && candleSeries) return true;
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); if(!window.LightweightCharts){
} statusEl.className = "status err";
statusEl.innerText = "图表库加载失败";
const opts = { return false;
upColor: "#4cd97f", }
downColor: "#ff6666",
borderVisible: false, if(!chart){
wickUpColor: "#4cd97f", chart = LightweightCharts.createChart(chartHost, {
wickDownColor: "#ff6666" layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
}; grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
if (typeof chart.addCandlestickSeries === "function") { rightPriceScale:{borderColor:"#2a3150"},
candleSeries = chart.addCandlestickSeries(opts); timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { crosshair:{mode:0}
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts); });
} window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
if(!candleSeries){ });
statusEl.className = "status err"; chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
statusEl.innerText = "K线序列初始化失败"; }
return false;
} const opts = {
return true; upColor: "#4cd97f",
} downColor: "#ff6666",
borderVisible: false,
function resetPriceLines(){ wickUpColor: "#4cd97f",
if(!candleSeries) return; wickDownColor: "#ff6666"
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} }); };
priceLines = []; if (typeof chart.addCandlestickSeries === "function") {
} candleSeries = chart.addCandlestickSeries(opts);
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
function addLine(price, title, color){ candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
if(!candleSeries || price===null || typeof price==="undefined") return; }
const p = Number(price);
if(Number.isNaN(p) || p<=0) return; if(!candleSeries){
priceLines.push(candleSeries.createPriceLine({ statusEl.className = "status err";
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title statusEl.innerText = "K线序列初始化失败";
})); return false;
} }
return true;
function paintMeta(data){ }
const key = data.key_monitor || null;
document.getElementById("m-symbol").innerText = data.symbol || "-"; function resetPriceLines(){
document.getElementById("m-price").innerText = data.current_price_display || fmt(data.current_price,8); if(!candleSeries) return;
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
if(!key){ priceLines = [];
document.getElementById("m-type").innerText = "未匹配到关键位"; }
document.getElementById("m-direction").innerText = "-";
document.getElementById("m-upper").innerText = "-"; function addLine(price, title, color){
document.getElementById("m-lower").innerText = "-"; if(!candleSeries || price===null || typeof price==="undefined") return;
document.getElementById("m-updiff").innerText = "-"; const p = Number(price);
document.getElementById("m-lowdiff").innerText = "-"; if(Number.isNaN(p) || p<=0) return;
return; priceLines.push(candleSeries.createPriceLine({
} price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
}));
document.getElementById("m-type").innerText = key.monitor_type || "-"; }
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = key.upper_display || fmt(key.upper,8); function paintMeta(data){
document.getElementById("m-lower").innerText = key.lower_display || fmt(key.lower,8); const key = data.key_monitor || null;
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`; document.getElementById("m-symbol").innerText = data.symbol || "-";
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`; document.getElementById("m-price").innerText = data.current_price_display || fmt(data.current_price,8);
}
if(!key){
function syncSymbolByKey(){ document.getElementById("m-type").innerText = "未匹配到关键位";
const keyId = keySelect.value; document.getElementById("m-direction").innerText = "-";
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId]; document.getElementById("m-upper").innerText = "-";
} document.getElementById("m-lower").innerText = "-";
document.getElementById("m-updiff").innerText = "-";
async function loadKeyKline(){ document.getElementById("m-lowdiff").innerText = "-";
if(!ensureChart()) return; return;
const keyId = keySelect.value; }
const symbol = (symbolInput.value || "").trim().toUpperCase();
const timeframe = tfSelect.value; document.getElementById("m-type").innerText = key.monitor_type || "-";
const limit = limitSelect.value; document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = key.upper_display || fmt(key.upper,8);
if(!symbol && !keyId){ document.getElementById("m-lower").innerText = key.lower_display || fmt(key.lower,8);
statusEl.className = "status err"; document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
statusEl.innerText = "请先输入币种或选择关键位"; document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
return; }
}
function syncSymbolByKey(){
statusEl.className = "status"; const keyId = keySelect.value;
statusEl.innerText = "加载中..."; if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
}
try{
const qs = new URLSearchParams(); async function loadKeyKline(){
if(keyId) qs.set("key_id", keyId); if(!ensureChart()) return;
if(symbol) qs.set("symbol", symbol); const keyId = keySelect.value;
qs.set("timeframe", timeframe); const symbol = (symbolInput.value || "").trim().toUpperCase();
qs.set("limit", limit); const timeframe = tfSelect.value;
const limit = limitSelect.value;
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
const data = await resp.json(); if(!symbol && !keyId){
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败"); statusEl.className = "status err";
statusEl.innerText = "请先输入币种或选择关键位";
const candles = Array.isArray(data.candles) ? data.candles : []; return;
if(!candles.length){ }
statusEl.className = "status err";
statusEl.innerText = "暂无K线数据"; statusEl.className = "status";
return; statusEl.innerText = "加载中...";
}
try{
if(!candleSeries) throw new Error("Series init failed"); const qs = new URLSearchParams();
candleSeries.setData(candles); if(keyId) qs.set("key_id", keyId);
resetPriceLines(); if(symbol) qs.set("symbol", symbol);
addLine(data.current_price, "现价", "#42a5f5"); qs.set("timeframe", timeframe);
if(data.key_monitor){ qs.set("limit", limit);
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f"); const resp = await fetch(`/api/key_kline?${qs.toString()}`);
} const data = await resp.json();
chart.timeScale().fitContent(); if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.className = "status"; if(!candles.length){
statusEl.innerText = `已加载 ${candles.length} 根K线`; statusEl.className = "status err";
}catch(err){ statusEl.innerText = "暂无K线数据";
statusEl.className = "status err"; return;
statusEl.innerText = err && err.message ? err.message : "加载失败"; }
}
} if(!candleSeries) throw new Error("Series init failed");
candleSeries.setData(candles);
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline); resetPriceLines();
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); }); addLine(data.current_price, "现价", "#42a5f5");
symbolInput.addEventListener("change", ()=>{ if(data.key_monitor){
if(symbolInput.value.trim()) keySelect.value = ""; addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
loadKeyKline(); addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
}); }
tfSelect.addEventListener("change", loadKeyKline); chart.timeScale().fitContent();
limitSelect.addEventListener("change", loadKeyKline); paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--";
syncSymbolByKey(); statusEl.className = "status";
loadKeyKline(); statusEl.innerText = `已加载 ${candles.length} 根K线`;
setInterval(loadKeyKline, refreshMs); }catch(err){
</script> statusEl.className = "status err";
</body> statusEl.innerText = err && err.message ? err.message : "加载失败";
}
}
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
symbolInput.addEventListener("change", ()=>{
if(symbolInput.value.trim()) keySelect.value = "";
loadKeyKline();
});
tfSelect.addEventListener("change", loadKeyKline);
limitSelect.addEventListener("change", loadKeyKline);
syncSymbolByKey();
loadKeyKline();
setInterval(loadKeyKline, refreshMs);
</script>
</body>
</html> </html>
+136 -118
View File
@@ -1,118 +1,136 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>登录 · {{ exchange_display }}</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
* { <title>登录 · {{ exchange_display }}</title>
margin: 0; <style>
padding: 0; * {
box-sizing: border-box; margin: 0;
} padding: 0;
body { box-sizing: border-box;
background: #0a0a10; }
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; body {
display: flex; background: #0a0a10;
align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
justify-content: center; display: flex;
height: 100vh; align-items: center;
color: #fff; justify-content: center;
} height: 100vh;
.login-box { color: #fff;
background: #12121a; }
padding: 2.5rem; .login-box {
border-radius: 16px; background: #12121a;
width: 100%; padding: 2.5rem;
max-width: 400px; border-radius: 16px;
border: 1px solid #242435; width: 100%;
box-shadow: 0 8px 24px rgba(0,0,0,0.3); max-width: 400px;
} border: 1px solid #242435;
.login-box h2 { box-shadow: 0 8px 24px rgba(0,0,0,0.3);
margin-bottom: 2rem; }
text-align: center; .login-box h2 {
font-size: 1.5rem; margin-bottom: 2rem;
background: linear-gradient(90deg, #4cc2ff, #7b42ff); text-align: center;
-webkit-background-clip: text; font-size: 1.5rem;
-webkit-text-fill-color: transparent; background: linear-gradient(90deg, #4cc2ff, #7b42ff);
} -webkit-background-clip: text;
.form-group { -webkit-text-fill-color: transparent;
margin-bottom: 1.25rem; }
} .form-group {
.form-group label { margin-bottom: 1.25rem;
display: block; }
margin-bottom: 0.5rem; .form-group label {
font-size: 0.9rem; display: block;
color: #a9a9ff; margin-bottom: 0.5rem;
} font-size: 0.9rem;
.form-group input { color: #a9a9ff;
width: 100%; }
padding: 0.85rem 1rem; .form-group input {
border-radius: 10px; width: 100%;
border: 1px solid #2e2e45; padding: 0.85rem 1rem;
background: #1a1a29; border-radius: 10px;
color: #fff; border: 1px solid #2e2e45;
font-size: 0.95rem; background: #1a1a29;
outline: none; color: #fff;
} font-size: 0.95rem;
.form-group input:focus { outline: none;
border-color: #4cc2ff; }
} .form-group input:focus {
button { border-color: #4cc2ff;
width: 100%; }
padding: 0.9rem; button {
border-radius: 10px; width: 100%;
border: none; padding: 0.9rem;
background: linear-gradient(90deg, #4285f4, #7b42ff); border-radius: 10px;
color: #fff; border: none;
font-size: 1rem; background: linear-gradient(90deg, #4285f4, #7b42ff);
font-weight: 500; color: #fff;
cursor: pointer; font-size: 1rem;
transition: 0.2s; font-weight: 500;
} cursor: pointer;
button:hover { transition: 0.2s;
opacity: 0.9; }
} button:hover {
.flash { opacity: 0.9;
padding: 0.8rem; }
margin-bottom: 1rem; .flash {
background: #331e24; padding: 0.8rem;
color: #ff6666; margin-bottom: 1rem;
border-radius: 8px; background: #331e24;
text-align: center; color: #ff6666;
font-size: 0.85rem; border-radius: 8px;
} text-align: center;
.exchange-line { font-size: 0.85rem;
text-align: center; }
font-size: 0.82rem; .exchange-line {
color: #8892b0; text-align: center;
margin: -0.5rem 0 1.25rem; font-size: 0.82rem;
} color: #8892b0;
.exchange-line strong { margin: -0.5rem 0 1.25rem;
color: #b8f5d0; }
font-weight: 600; .exchange-line strong {
} color: #b8f5d0;
</style> font-weight: 600;
</head> }
<body> </style>
<div class="login-box"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<h2>交易监控系统登录</h2>
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p> </head>
{% with messages = get_flashed_messages() %} <div class="login-theme-bar">
{% if messages %} <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<div class="flash">{{ messages[0] }}</div> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% endif %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
{% endwith %} <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<form method="POST"> </svg>
<div class="form-group"> </button>
<label>账号</label> <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<input type="text" name="username" required placeholder="请输入账号"> <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">
</div> <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"/>
<div class="form-group"> </svg>
<label>密码</label> </button>
<input type="password" name="password" required placeholder="请输入密码"> </div>
</div> </div>
<button type="submit">登录</button> <body>
</form> <div class="login-box">
</div> <h2>交易监控系统登录</h2>
</body> <p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
</html> {% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">{{ messages[0] }}</div>
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label>账号</label>
<input type="text" name="username" required placeholder="请输入账号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
@@ -1,214 +1,231 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ exchange_display }} | 实盘下单放大</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>{{ exchange_display }} | 实盘下单放大</title>
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
.empty{padding:18px;color:#95a2c2} #chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px} #chart{width:100%;height:100%}
</style> .empty{padding:18px;color:#95a2c2}
</head> .exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% if orders %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>订单</label> </svg>
<select id="order-id"> </button>
{% for o in orders %} <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}> <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">
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }} <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"/>
</option> </svg>
{% endfor %} </button>
</select> </div>
<label>周期</label>
<select id="timeframe"> <div class="row">
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <a class="btn" href="/">返回首页</a>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> <strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
{% endfor %} </div>
</select> <div class="status">最近刷新:<span id="updated-at">--</span></div>
<button id="manual-refresh" type="button">刷新</button> </div>
<span id="load-status" class="status"></span> {% if orders %}
</div> <div class="row" style="margin-top:10px">
{% else %} <label>订单</label>
<div class="empty">当前没有激活订单,无法展示放大K线。</div> <select id="order-id">
{% endif %} {% for o in orders %}
</div> <option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
{% if orders %} </option>
<div class="card"> {% endfor %}
<div class="meta"> </select>
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <label>周期</label>
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> <select id="timeframe">
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div> </select>
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div> <button id="manual-refresh" type="button">刷新</button>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <span id="load-status" class="status"></span>
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div> </div>
</div> {% else %}
</div> <div class="empty">当前没有激活订单,无法展示放大K线。</div>
{% endif %}
<div class="card"> </div>
<div id="chart-wrap"><div id="chart"></div></div>
</div> {% if orders %}
{% endif %} <div class="card">
</div> <div class="meta">
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
{% if orders %} <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script> <div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
<script> <div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
const orderSelect = document.getElementById("order-id"); <div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
const chartHost = document.getElementById("chart"); </div>
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d); </div>
let chart = null; <div class="card">
let candleSeries = null; <div id="chart-wrap"><div id="chart"></div></div>
let priceLines = []; </div>
{% endif %}
function ensureChart(){ </div>
if(chart){ return true; }
if(!window.LightweightCharts){ {% if orders %}
statusEl.className = "status err"; <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
statusEl.innerText = "图表库加载失败"; <script>
return false; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
} const orderSelect = document.getElementById("order-id");
chart = LightweightCharts.createChart(chartHost, { const tfSelect = document.getElementById("timeframe");
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" }, const statusEl = document.getElementById("load-status");
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } }, const updatedAtEl = document.getElementById("updated-at");
rightPriceScale: { borderColor: "#2a3150" }, const chartHost = document.getElementById("chart");
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false }, const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
crosshair: { mode: 0 }
}); let chart = null;
candleSeries = chart.addCandlestickSeries({ let candleSeries = null;
upColor: "#4cd97f", let priceLines = [];
downColor: "#ff6666",
borderVisible: false, function ensureChart(){
wickUpColor: "#4cd97f", if(chart){ return true; }
wickDownColor: "#ff6666" if(!window.LightweightCharts){
}); statusEl.className = "status err";
window.addEventListener("resize", () => { statusEl.innerText = "图表库加载失败";
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); return false;
}); }
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); chart = LightweightCharts.createChart(chartHost, {
return true; layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
} grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
rightPriceScale: { borderColor: "#2a3150" },
function resetPriceLines(){ timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
if(!candleSeries){ return; } crosshair: { mode: 0 }
priceLines.forEach(line => { });
try { candleSeries.removePriceLine(line); } catch (_) {} candleSeries = chart.addCandlestickSeries({
}); upColor: "#4cd97f",
priceLines = []; downColor: "#ff6666",
} borderVisible: false,
wickUpColor: "#4cd97f",
function addLine(price, title, color){ wickDownColor: "#ff6666"
if(!candleSeries || typeof price === "undefined" || price === null){ return; } });
const p = Number(price); window.addEventListener("resize", () => {
if(Number.isNaN(p) || p <= 0){ return; } chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
priceLines.push(candleSeries.createPriceLine({ });
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
})); return true;
} }
function paintOrder(order){ function resetPriceLines(){
document.getElementById("m-symbol").innerText = order.symbol || "-"; if(!candleSeries){ return; }
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; priceLines.forEach(line => {
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8); try { candleSeries.removePriceLine(line); } catch (_) {}
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8); });
document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8); priceLines = [];
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`; }
document.getElementById("m-breakeven").innerText =
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启"; function addLine(price, title, color){
document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8); if(!candleSeries || typeof price === "undefined" || price === null){ return; }
const pnlEl = document.getElementById("m-pnl"); const p = Number(price);
pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`; if(Number.isNaN(p) || p <= 0){ return; }
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); priceLines.push(candleSeries.createPriceLine({
} price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
}));
async function loadOrderKline(){ }
if(!ensureChart()){ return; }
const orderId = orderSelect.value; function paintOrder(order){
const timeframe = tfSelect.value; document.getElementById("m-symbol").innerText = order.symbol || "-";
if(!orderId){ return; } document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
statusEl.className = "status"; document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
statusEl.innerText = "加载中..."; document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
try{ document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`); document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
const data = await resp.json(); document.getElementById("m-breakeven").innerText =
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); } (order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
const candles = Array.isArray(data.candles) ? data.candles : []; document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
if(!candles.length){ const pnlEl = document.getElementById("m-pnl");
statusEl.className = "status err"; pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
statusEl.innerText = "暂无K线数据"; pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
return; }
}
candleSeries.setData(candles); async function loadOrderKline(){
resetPriceLines(); if(!ensureChart()){ return; }
addLine(data.order.trigger_price, "成交价", "#42a5f5"); const orderId = orderSelect.value;
addLine(data.order.stop_loss, "止损", "#ff6666"); const timeframe = tfSelect.value;
addLine(data.order.take_profit, "止盈", "#4cd97f"); if(!orderId){ return; }
chart.timeScale().fitContent(); statusEl.className = "status";
paintOrder(data.order || {}); statusEl.innerText = "加载中...";
updatedAtEl.innerText = data.updated_at || "--"; try{
statusEl.className = "status"; const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
statusEl.innerText = `已加载 ${candles.length} 根K线`; const data = await resp.json();
}catch(err){ if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
statusEl.className = "status err"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.innerText = err && err.message ? err.message : "加载失败"; if(!candles.length){
} statusEl.className = "status err";
} statusEl.innerText = "暂无K线数据";
return;
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline); }
orderSelect.addEventListener("change", loadOrderKline); candleSeries.setData(candles);
tfSelect.addEventListener("change", loadOrderKline); resetPriceLines();
loadOrderKline(); addLine(data.order.trigger_price, "成交价", "#42a5f5");
setInterval(loadOrderKline, refreshMs); addLine(data.order.stop_loss, "止损", "#ff6666");
</script> addLine(data.order.take_profit, "止盈", "#4cd97f");
{% endif %} chart.timeScale().fitContent();
<script> paintOrder(data.order || {});
(function(){ updatedAtEl.innerText = data.updated_at || "--";
if (typeof ensureChart !== 'function') return; statusEl.className = "status";
const oldEnsureChart = ensureChart; statusEl.innerText = `已加载 ${candles.length} 根K线`;
ensureChart = function(){ }catch(err){
if (chart && candleSeries) return true; statusEl.className = "status err";
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {} statusEl.innerText = err && err.message ? err.message : "加载失败";
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { }
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' }; }
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries; document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
} orderSelect.addEventListener("change", loadOrderKline);
return !!candleSeries; tfSelect.addEventListener("change", loadOrderKline);
}; loadOrderKline();
})(); setInterval(loadOrderKline, refreshMs);
</script> </script>
</body> {% endif %}
</html> <script>
(function(){
if (typeof ensureChart !== 'function') return;
const oldEnsureChart = ensureChart;
ensureChart = function(){
if (chart && candleSeries) return true;
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries;
}
return !!candleSeries;
};
})();
</script>
</body>
</html>
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32"> <link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -229,6 +231,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .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} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head> </head>
<body data-page="{{ page }}"> <body data-page="{{ page }}">
{% macro period_stats(title, s) %} {% macro period_stats(title, s) %}
@@ -253,7 +257,21 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>加密货币|交易监控 + AI复盘一体化</h1> <h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<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>
</div> </div>
<div class="top-nav"> <div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a> <a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
+277 -260
View File
@@ -1,261 +1,278 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ exchange_display }} | 关键位放大</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>{{ exchange_display }} | 关键位放大</title>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px} #chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
</style> #chart{width:100%;height:100%}
</head> .exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <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">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>币种</label> </svg>
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT"> </button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<label>关键位</label> <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">
<select id="key-id"> <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"/>
<option value="">无(仅看K线)</option> </svg>
{% for k in key_list %} </button>
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option> </div>
{% endfor %}
</select> <div class="row">
<a class="btn" href="/">返回首页</a>
<label>周期</label> <strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
<select id="timeframe"> </div>
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <div class="status">最近刷新:<span id="updated-at">--</span></div>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> </div>
{% endfor %}
</select> <div class="row" style="margin-top:10px">
<label>币种</label>
<label>K线数</label> <input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
<select id="kline-limit">
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option> <label>关键位</label>
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option> <select id="key-id">
</select> <option value="">无(仅看K线)</option>
{% for k in key_list %}
<button id="manual-refresh" type="button">刷新</button> <option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
<span id="load-status" class="status"></span> {% endfor %}
</div> </select>
</div>
<label>周期</label>
<div class="card"> <select id="timeframe">
<div class="meta"> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> </select>
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div> <label>K线数</label>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <select id="kline-limit">
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div> <option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div> <option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
</div> </select>
</div>
<button id="manual-refresh" type="button">刷新</button>
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div> <span id="load-status" class="status"></span>
</div> </div>
</div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script> <div class="card">
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta">
const keySelect = document.getElementById("key-id"); <div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
const symbolInput = document.getElementById("symbol-input"); <div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
const limitSelect = document.getElementById("kline-limit"); <div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const chartHost = document.getElementById("chart"); <div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d); <div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
const fmtSigned = (v,d=4)=>{ </div>
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-"; </div>
const n = Number(v);
return `${n>0?"+":""}${n.toFixed(d)}`; <div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
}; </div>
let chart = null; <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
let candleSeries = null; <script>
let priceLines = []; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
const keyMap = {}; const keySelect = document.getElementById("key-id");
{% for k in key_list %} const symbolInput = document.getElementById("symbol-input");
keyMap["{{ k.id }}"] = "{{ k.symbol }}"; const tfSelect = document.getElementById("timeframe");
{% endfor %} const limitSelect = document.getElementById("kline-limit");
const statusEl = document.getElementById("load-status");
function ensureChart(){ const updatedAtEl = document.getElementById("updated-at");
if(chart && candleSeries) return true; const chartHost = document.getElementById("chart");
if(!window.LightweightCharts){ const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
statusEl.className = "status err"; const fmtSigned = (v,d=4)=>{
statusEl.innerText = "图表库加载失败"; if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
return false; const n = Number(v);
} return `${n>0?"+":""}${n.toFixed(d)}`;
};
if(!chart){
chart = LightweightCharts.createChart(chartHost, { let chart = null;
layout:{background:{color:"#0f1320"},textColor:"#d6deff"}, let candleSeries = null;
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}}, let priceLines = [];
rightPriceScale:{borderColor:"#2a3150"}, const keyMap = {};
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false}, {% for k in key_list %}
crosshair:{mode:0} keyMap["{{ k.id }}"] = "{{ k.symbol }}";
}); {% endfor %}
window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); function ensureChart(){
}); if(chart && candleSeries) return true;
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); if(!window.LightweightCharts){
} statusEl.className = "status err";
statusEl.innerText = "图表库加载失败";
const opts = { return false;
upColor: "#4cd97f", }
downColor: "#ff6666",
borderVisible: false, if(!chart){
wickUpColor: "#4cd97f", chart = LightweightCharts.createChart(chartHost, {
wickDownColor: "#ff6666" layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
}; grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
if (typeof chart.addCandlestickSeries === "function") { rightPriceScale:{borderColor:"#2a3150"},
candleSeries = chart.addCandlestickSeries(opts); timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { crosshair:{mode:0}
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts); });
} window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
if(!candleSeries){ });
statusEl.className = "status err"; chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
statusEl.innerText = "K线序列初始化失败"; }
return false;
} const opts = {
return true; upColor: "#4cd97f",
} downColor: "#ff6666",
borderVisible: false,
function resetPriceLines(){ wickUpColor: "#4cd97f",
if(!candleSeries) return; wickDownColor: "#ff6666"
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} }); };
priceLines = []; if (typeof chart.addCandlestickSeries === "function") {
} candleSeries = chart.addCandlestickSeries(opts);
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
function addLine(price, title, color){ candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
if(!candleSeries || price===null || typeof price==="undefined") return; }
const p = Number(price);
if(Number.isNaN(p) || p<=0) return; if(!candleSeries){
priceLines.push(candleSeries.createPriceLine({ statusEl.className = "status err";
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title statusEl.innerText = "K线序列初始化失败";
})); return false;
} }
return true;
function paintMeta(data){ }
const key = data.key_monitor || null;
document.getElementById("m-symbol").innerText = data.symbol || "-"; function resetPriceLines(){
document.getElementById("m-price").innerText = fmt(data.current_price,8); if(!candleSeries) return;
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
if(!key){ priceLines = [];
document.getElementById("m-type").innerText = "未匹配到关键位"; }
document.getElementById("m-direction").innerText = "-";
document.getElementById("m-upper").innerText = "-"; function addLine(price, title, color){
document.getElementById("m-lower").innerText = "-"; if(!candleSeries || price===null || typeof price==="undefined") return;
document.getElementById("m-updiff").innerText = "-"; const p = Number(price);
document.getElementById("m-lowdiff").innerText = "-"; if(Number.isNaN(p) || p<=0) return;
return; priceLines.push(candleSeries.createPriceLine({
} price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
}));
document.getElementById("m-type").innerText = key.monitor_type || "-"; }
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = fmt(key.upper,8); function paintMeta(data){
document.getElementById("m-lower").innerText = fmt(key.lower,8); const key = data.key_monitor || null;
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`; document.getElementById("m-symbol").innerText = data.symbol || "-";
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`; document.getElementById("m-price").innerText = fmt(data.current_price,8);
}
if(!key){
function syncSymbolByKey(){ document.getElementById("m-type").innerText = "未匹配到关键位";
const keyId = keySelect.value; document.getElementById("m-direction").innerText = "-";
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId]; document.getElementById("m-upper").innerText = "-";
} document.getElementById("m-lower").innerText = "-";
document.getElementById("m-updiff").innerText = "-";
async function loadKeyKline(){ document.getElementById("m-lowdiff").innerText = "-";
if(!ensureChart()) return; return;
const keyId = keySelect.value; }
const symbol = (symbolInput.value || "").trim().toUpperCase();
const timeframe = tfSelect.value; document.getElementById("m-type").innerText = key.monitor_type || "-";
const limit = limitSelect.value; document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = fmt(key.upper,8);
if(!symbol && !keyId){ document.getElementById("m-lower").innerText = fmt(key.lower,8);
statusEl.className = "status err"; document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
statusEl.innerText = "请先输入币种或选择关键位"; document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
return; }
}
function syncSymbolByKey(){
statusEl.className = "status"; const keyId = keySelect.value;
statusEl.innerText = "加载中..."; if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
}
try{
const qs = new URLSearchParams(); async function loadKeyKline(){
if(keyId) qs.set("key_id", keyId); if(!ensureChart()) return;
if(symbol) qs.set("symbol", symbol); const keyId = keySelect.value;
qs.set("timeframe", timeframe); const symbol = (symbolInput.value || "").trim().toUpperCase();
qs.set("limit", limit); const timeframe = tfSelect.value;
const limit = limitSelect.value;
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
const data = await resp.json(); if(!symbol && !keyId){
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败"); statusEl.className = "status err";
statusEl.innerText = "请先输入币种或选择关键位";
const candles = Array.isArray(data.candles) ? data.candles : []; return;
if(!candles.length){ }
statusEl.className = "status err";
statusEl.innerText = "暂无K线数据"; statusEl.className = "status";
return; statusEl.innerText = "加载中...";
}
try{
if(!candleSeries) throw new Error("Series init failed"); const qs = new URLSearchParams();
candleSeries.setData(candles); if(keyId) qs.set("key_id", keyId);
resetPriceLines(); if(symbol) qs.set("symbol", symbol);
addLine(data.current_price, "现价", "#42a5f5"); qs.set("timeframe", timeframe);
if(data.key_monitor){ qs.set("limit", limit);
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f"); const resp = await fetch(`/api/key_kline?${qs.toString()}`);
} const data = await resp.json();
chart.timeScale().fitContent(); if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.className = "status"; if(!candles.length){
statusEl.innerText = `已加载 ${candles.length} 根K线`; statusEl.className = "status err";
}catch(err){ statusEl.innerText = "暂无K线数据";
statusEl.className = "status err"; return;
statusEl.innerText = err && err.message ? err.message : "加载失败"; }
}
} if(!candleSeries) throw new Error("Series init failed");
candleSeries.setData(candles);
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline); resetPriceLines();
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); }); addLine(data.current_price, "现价", "#42a5f5");
symbolInput.addEventListener("change", ()=>{ if(data.key_monitor){
if(symbolInput.value.trim()) keySelect.value = ""; addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
loadKeyKline(); addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
}); }
tfSelect.addEventListener("change", loadKeyKline); chart.timeScale().fitContent();
limitSelect.addEventListener("change", loadKeyKline); paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--";
syncSymbolByKey(); statusEl.className = "status";
loadKeyKline(); statusEl.innerText = `已加载 ${candles.length} 根K线`;
setInterval(loadKeyKline, refreshMs); }catch(err){
</script> statusEl.className = "status err";
</body> statusEl.innerText = err && err.message ? err.message : "加载失败";
}
}
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
symbolInput.addEventListener("change", ()=>{
if(symbolInput.value.trim()) keySelect.value = "";
loadKeyKline();
});
tfSelect.addEventListener("change", loadKeyKline);
limitSelect.addEventListener("change", loadKeyKline);
syncSymbolByKey();
loadKeyKline();
setInterval(loadKeyKline, refreshMs);
</script>
</body>
</html> </html>
+136 -118
View File
@@ -1,118 +1,136 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>登录 · {{ exchange_display }}</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
* { <title>登录 · {{ exchange_display }}</title>
margin: 0; <style>
padding: 0; * {
box-sizing: border-box; margin: 0;
} padding: 0;
body { box-sizing: border-box;
background: #0a0a10; }
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; body {
display: flex; background: #0a0a10;
align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
justify-content: center; display: flex;
height: 100vh; align-items: center;
color: #fff; justify-content: center;
} height: 100vh;
.login-box { color: #fff;
background: #12121a; }
padding: 2.5rem; .login-box {
border-radius: 16px; background: #12121a;
width: 100%; padding: 2.5rem;
max-width: 400px; border-radius: 16px;
border: 1px solid #242435; width: 100%;
box-shadow: 0 8px 24px rgba(0,0,0,0.3); max-width: 400px;
} border: 1px solid #242435;
.login-box h2 { box-shadow: 0 8px 24px rgba(0,0,0,0.3);
margin-bottom: 2rem; }
text-align: center; .login-box h2 {
font-size: 1.5rem; margin-bottom: 2rem;
background: linear-gradient(90deg, #4cc2ff, #7b42ff); text-align: center;
-webkit-background-clip: text; font-size: 1.5rem;
-webkit-text-fill-color: transparent; background: linear-gradient(90deg, #4cc2ff, #7b42ff);
} -webkit-background-clip: text;
.form-group { -webkit-text-fill-color: transparent;
margin-bottom: 1.25rem; }
} .form-group {
.form-group label { margin-bottom: 1.25rem;
display: block; }
margin-bottom: 0.5rem; .form-group label {
font-size: 0.9rem; display: block;
color: #a9a9ff; margin-bottom: 0.5rem;
} font-size: 0.9rem;
.form-group input { color: #a9a9ff;
width: 100%; }
padding: 0.85rem 1rem; .form-group input {
border-radius: 10px; width: 100%;
border: 1px solid #2e2e45; padding: 0.85rem 1rem;
background: #1a1a29; border-radius: 10px;
color: #fff; border: 1px solid #2e2e45;
font-size: 0.95rem; background: #1a1a29;
outline: none; color: #fff;
} font-size: 0.95rem;
.form-group input:focus { outline: none;
border-color: #4cc2ff; }
} .form-group input:focus {
button { border-color: #4cc2ff;
width: 100%; }
padding: 0.9rem; button {
border-radius: 10px; width: 100%;
border: none; padding: 0.9rem;
background: linear-gradient(90deg, #4285f4, #7b42ff); border-radius: 10px;
color: #fff; border: none;
font-size: 1rem; background: linear-gradient(90deg, #4285f4, #7b42ff);
font-weight: 500; color: #fff;
cursor: pointer; font-size: 1rem;
transition: 0.2s; font-weight: 500;
} cursor: pointer;
button:hover { transition: 0.2s;
opacity: 0.9; }
} button:hover {
.flash { opacity: 0.9;
padding: 0.8rem; }
margin-bottom: 1rem; .flash {
background: #331e24; padding: 0.8rem;
color: #ff6666; margin-bottom: 1rem;
border-radius: 8px; background: #331e24;
text-align: center; color: #ff6666;
font-size: 0.85rem; border-radius: 8px;
} text-align: center;
.exchange-line { font-size: 0.85rem;
text-align: center; }
font-size: 0.82rem; .exchange-line {
color: #8892b0; text-align: center;
margin: -0.5rem 0 1.25rem; font-size: 0.82rem;
} color: #8892b0;
.exchange-line strong { margin: -0.5rem 0 1.25rem;
color: #b8f5d0; }
font-weight: 600; .exchange-line strong {
} color: #b8f5d0;
</style> font-weight: 600;
</head> }
<body> </style>
<div class="login-box"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<h2>交易监控系统登录</h2>
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p> </head>
{% with messages = get_flashed_messages() %} <div class="login-theme-bar">
{% if messages %} <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<div class="flash">{{ messages[0] }}</div> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% endif %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
{% endwith %} <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<form method="POST"> </svg>
<div class="form-group"> </button>
<label>账号</label> <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<input type="text" name="username" required placeholder="请输入账号"> <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">
</div> <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"/>
<div class="form-group"> </svg>
<label>密码</label> </button>
<input type="password" name="password" required placeholder="请输入密码"> </div>
</div> </div>
<button type="submit">登录</button> <body>
</form> <div class="login-box">
</div> <h2>交易监控系统登录</h2>
</body> <p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
</html> {% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">{{ messages[0] }}</div>
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label>账号</label>
<input type="text" name="username" required placeholder="请输入账号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
+231 -214
View File
@@ -1,214 +1,231 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ exchange_display }} | 实盘下单放大</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>{{ exchange_display }} | 实盘下单放大</title>
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
.empty{padding:18px;color:#95a2c2} #chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px} #chart{width:100%;height:100%}
</style> .empty{padding:18px;color:#95a2c2}
</head> .exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% if orders %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>订单</label> </svg>
<select id="order-id"> </button>
{% for o in orders %} <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}> <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">
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }} <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"/>
</option> </svg>
{% endfor %} </button>
</select> </div>
<label>周期</label>
<select id="timeframe"> <div class="row">
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <a class="btn" href="/">返回首页</a>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> <strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
{% endfor %} </div>
</select> <div class="status">最近刷新:<span id="updated-at">--</span></div>
<button id="manual-refresh" type="button">刷新</button> </div>
<span id="load-status" class="status"></span> {% if orders %}
</div> <div class="row" style="margin-top:10px">
{% else %} <label>订单</label>
<div class="empty">当前没有激活订单,无法展示放大K线。</div> <select id="order-id">
{% endif %} {% for o in orders %}
</div> <option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
{% if orders %} </option>
<div class="card"> {% endfor %}
<div class="meta"> </select>
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <label>周期</label>
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> <select id="timeframe">
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div> </select>
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div> <button id="manual-refresh" type="button">刷新</button>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <span id="load-status" class="status"></span>
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div> </div>
</div> {% else %}
</div> <div class="empty">当前没有激活订单,无法展示放大K线。</div>
{% endif %}
<div class="card"> </div>
<div id="chart-wrap"><div id="chart"></div></div>
</div> {% if orders %}
{% endif %} <div class="card">
</div> <div class="meta">
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
{% if orders %} <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script> <div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
<script> <div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
const orderSelect = document.getElementById("order-id"); <div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
const chartHost = document.getElementById("chart"); </div>
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d); </div>
let chart = null; <div class="card">
let candleSeries = null; <div id="chart-wrap"><div id="chart"></div></div>
let priceLines = []; </div>
{% endif %}
function ensureChart(){ </div>
if(chart){ return true; }
if(!window.LightweightCharts){ {% if orders %}
statusEl.className = "status err"; <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
statusEl.innerText = "图表库加载失败"; <script>
return false; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
} const orderSelect = document.getElementById("order-id");
chart = LightweightCharts.createChart(chartHost, { const tfSelect = document.getElementById("timeframe");
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" }, const statusEl = document.getElementById("load-status");
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } }, const updatedAtEl = document.getElementById("updated-at");
rightPriceScale: { borderColor: "#2a3150" }, const chartHost = document.getElementById("chart");
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false }, const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
crosshair: { mode: 0 }
}); let chart = null;
candleSeries = chart.addCandlestickSeries({ let candleSeries = null;
upColor: "#4cd97f", let priceLines = [];
downColor: "#ff6666",
borderVisible: false, function ensureChart(){
wickUpColor: "#4cd97f", if(chart){ return true; }
wickDownColor: "#ff6666" if(!window.LightweightCharts){
}); statusEl.className = "status err";
window.addEventListener("resize", () => { statusEl.innerText = "图表库加载失败";
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); return false;
}); }
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); chart = LightweightCharts.createChart(chartHost, {
return true; layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
} grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
rightPriceScale: { borderColor: "#2a3150" },
function resetPriceLines(){ timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
if(!candleSeries){ return; } crosshair: { mode: 0 }
priceLines.forEach(line => { });
try { candleSeries.removePriceLine(line); } catch (_) {} candleSeries = chart.addCandlestickSeries({
}); upColor: "#4cd97f",
priceLines = []; downColor: "#ff6666",
} borderVisible: false,
wickUpColor: "#4cd97f",
function addLine(price, title, color){ wickDownColor: "#ff6666"
if(!candleSeries || typeof price === "undefined" || price === null){ return; } });
const p = Number(price); window.addEventListener("resize", () => {
if(Number.isNaN(p) || p <= 0){ return; } chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
priceLines.push(candleSeries.createPriceLine({ });
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
})); return true;
} }
function paintOrder(order){ function resetPriceLines(){
document.getElementById("m-symbol").innerText = order.symbol || "-"; if(!candleSeries){ return; }
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; priceLines.forEach(line => {
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8); try { candleSeries.removePriceLine(line); } catch (_) {}
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8); });
document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8); priceLines = [];
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`; }
document.getElementById("m-breakeven").innerText =
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启"; function addLine(price, title, color){
document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8); if(!candleSeries || typeof price === "undefined" || price === null){ return; }
const pnlEl = document.getElementById("m-pnl"); const p = Number(price);
pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`; if(Number.isNaN(p) || p <= 0){ return; }
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); priceLines.push(candleSeries.createPriceLine({
} price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
}));
async function loadOrderKline(){ }
if(!ensureChart()){ return; }
const orderId = orderSelect.value; function paintOrder(order){
const timeframe = tfSelect.value; document.getElementById("m-symbol").innerText = order.symbol || "-";
if(!orderId){ return; } document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
statusEl.className = "status"; document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
statusEl.innerText = "加载中..."; document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
try{ document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`); document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
const data = await resp.json(); document.getElementById("m-breakeven").innerText =
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); } (order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
const candles = Array.isArray(data.candles) ? data.candles : []; document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
if(!candles.length){ const pnlEl = document.getElementById("m-pnl");
statusEl.className = "status err"; pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
statusEl.innerText = "暂无K线数据"; pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
return; }
}
candleSeries.setData(candles); async function loadOrderKline(){
resetPriceLines(); if(!ensureChart()){ return; }
addLine(data.order.trigger_price, "成交价", "#42a5f5"); const orderId = orderSelect.value;
addLine(data.order.stop_loss, "止损", "#ff6666"); const timeframe = tfSelect.value;
addLine(data.order.take_profit, "止盈", "#4cd97f"); if(!orderId){ return; }
chart.timeScale().fitContent(); statusEl.className = "status";
paintOrder(data.order || {}); statusEl.innerText = "加载中...";
updatedAtEl.innerText = data.updated_at || "--"; try{
statusEl.className = "status"; const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
statusEl.innerText = `已加载 ${candles.length} 根K线`; const data = await resp.json();
}catch(err){ if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
statusEl.className = "status err"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.innerText = err && err.message ? err.message : "加载失败"; if(!candles.length){
} statusEl.className = "status err";
} statusEl.innerText = "暂无K线数据";
return;
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline); }
orderSelect.addEventListener("change", loadOrderKline); candleSeries.setData(candles);
tfSelect.addEventListener("change", loadOrderKline); resetPriceLines();
loadOrderKline(); addLine(data.order.trigger_price, "成交价", "#42a5f5");
setInterval(loadOrderKline, refreshMs); addLine(data.order.stop_loss, "止损", "#ff6666");
</script> addLine(data.order.take_profit, "止盈", "#4cd97f");
{% endif %} chart.timeScale().fitContent();
<script> paintOrder(data.order || {});
(function(){ updatedAtEl.innerText = data.updated_at || "--";
if (typeof ensureChart !== 'function') return; statusEl.className = "status";
const oldEnsureChart = ensureChart; statusEl.innerText = `已加载 ${candles.length} 根K线`;
ensureChart = function(){ }catch(err){
if (chart && candleSeries) return true; statusEl.className = "status err";
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {} statusEl.innerText = err && err.message ? err.message : "加载失败";
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { }
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' }; }
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries; document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
} orderSelect.addEventListener("change", loadOrderKline);
return !!candleSeries; tfSelect.addEventListener("change", loadOrderKline);
}; loadOrderKline();
})(); setInterval(loadOrderKline, refreshMs);
</script> </script>
</body> {% endif %}
</html> <script>
(function(){
if (typeof ensureChart !== 'function') return;
const oldEnsureChart = ensureChart;
ensureChart = function(){
if (chart && candleSeries) return true;
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries;
}
return !!candleSeries;
};
})();
</script>
</body>
</html>
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32"> <link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -209,6 +211,8 @@
.stats-split-row{grid-template-columns:1fr} .stats-split-row{grid-template-columns:1fr}
} }
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head> </head>
<body> <body>
{% macro period_metrics_cells(s) %} {% macro period_metrics_cells(s) %}
@@ -243,7 +247,21 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>加密货币|Gate 机器人交易监控</h1> <h1>加密货币|Gate 机器人交易监控</h1>
<div class="exchange-tag">{{ exchange_display }}</div> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<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>
</div> </div>
<div class="top-nav"> <div class="top-nav">
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a> <a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
@@ -1,261 +1,278 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ exchange_display }} | 关键位放大</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>{{ exchange_display }} | 关键位放大</title>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px} #chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
</style> #chart{width:100%;height:100%}
</head> .exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <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">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>币种</label> </svg>
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT"> </button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<label>关键位</label> <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">
<select id="key-id"> <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"/>
<option value="">无(仅看K线)</option> </svg>
{% for k in key_list %} </button>
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option> </div>
{% endfor %}
</select> <div class="row">
<a class="btn" href="/">返回首页</a>
<label>周期</label> <strong style="color:#dbe4ff">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
<select id="timeframe"> </div>
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <div class="status">最近刷新:<span id="updated-at">--</span></div>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> </div>
{% endfor %}
</select> <div class="row" style="margin-top:10px">
<label>币种</label>
<label>K线数</label> <input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
<select id="kline-limit">
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option> <label>关键位</label>
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option> <select id="key-id">
</select> <option value="">无(仅看K线)</option>
{% for k in key_list %}
<button id="manual-refresh" type="button">刷新</button> <option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
<span id="load-status" class="status"></span> {% endfor %}
</div> </select>
</div>
<label>周期</label>
<div class="card"> <select id="timeframe">
<div class="meta"> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> </select>
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div> <label>K线数</label>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <select id="kline-limit">
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div> <option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div> <option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
</div> </select>
</div>
<button id="manual-refresh" type="button">刷新</button>
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div> <span id="load-status" class="status"></span>
</div> </div>
</div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script> <div class="card">
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta">
const keySelect = document.getElementById("key-id"); <div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
const symbolInput = document.getElementById("symbol-input"); <div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
const limitSelect = document.getElementById("kline-limit"); <div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const chartHost = document.getElementById("chart"); <div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d); <div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
const fmtSigned = (v,d=4)=>{ </div>
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-"; </div>
const n = Number(v);
return `${n>0?"+":""}${n.toFixed(d)}`; <div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
}; </div>
let chart = null; <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
let candleSeries = null; <script>
let priceLines = []; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
const keyMap = {}; const keySelect = document.getElementById("key-id");
{% for k in key_list %} const symbolInput = document.getElementById("symbol-input");
keyMap["{{ k.id }}"] = "{{ k.symbol }}"; const tfSelect = document.getElementById("timeframe");
{% endfor %} const limitSelect = document.getElementById("kline-limit");
const statusEl = document.getElementById("load-status");
function ensureChart(){ const updatedAtEl = document.getElementById("updated-at");
if(chart && candleSeries) return true; const chartHost = document.getElementById("chart");
if(!window.LightweightCharts){ const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
statusEl.className = "status err"; const fmtSigned = (v,d=4)=>{
statusEl.innerText = "图表库加载失败"; if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
return false; const n = Number(v);
} return `${n>0?"+":""}${n.toFixed(d)}`;
};
if(!chart){
chart = LightweightCharts.createChart(chartHost, { let chart = null;
layout:{background:{color:"#0f1320"},textColor:"#d6deff"}, let candleSeries = null;
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}}, let priceLines = [];
rightPriceScale:{borderColor:"#2a3150"}, const keyMap = {};
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false}, {% for k in key_list %}
crosshair:{mode:0} keyMap["{{ k.id }}"] = "{{ k.symbol }}";
}); {% endfor %}
window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); function ensureChart(){
}); if(chart && candleSeries) return true;
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); if(!window.LightweightCharts){
} statusEl.className = "status err";
statusEl.innerText = "图表库加载失败";
const opts = { return false;
upColor: "#4cd97f", }
downColor: "#ff6666",
borderVisible: false, if(!chart){
wickUpColor: "#4cd97f", chart = LightweightCharts.createChart(chartHost, {
wickDownColor: "#ff6666" layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
}; grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
if (typeof chart.addCandlestickSeries === "function") { rightPriceScale:{borderColor:"#2a3150"},
candleSeries = chart.addCandlestickSeries(opts); timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { crosshair:{mode:0}
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts); });
} window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
if(!candleSeries){ });
statusEl.className = "status err"; chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
statusEl.innerText = "K线序列初始化失败"; }
return false;
} const opts = {
return true; upColor: "#4cd97f",
} downColor: "#ff6666",
borderVisible: false,
function resetPriceLines(){ wickUpColor: "#4cd97f",
if(!candleSeries) return; wickDownColor: "#ff6666"
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} }); };
priceLines = []; if (typeof chart.addCandlestickSeries === "function") {
} candleSeries = chart.addCandlestickSeries(opts);
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
function addLine(price, title, color){ candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
if(!candleSeries || price===null || typeof price==="undefined") return; }
const p = Number(price);
if(Number.isNaN(p) || p<=0) return; if(!candleSeries){
priceLines.push(candleSeries.createPriceLine({ statusEl.className = "status err";
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title statusEl.innerText = "K线序列初始化失败";
})); return false;
} }
return true;
function paintMeta(data){ }
const key = data.key_monitor || null;
document.getElementById("m-symbol").innerText = data.symbol || "-"; function resetPriceLines(){
document.getElementById("m-price").innerText = fmt(data.current_price,8); if(!candleSeries) return;
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
if(!key){ priceLines = [];
document.getElementById("m-type").innerText = "未匹配到关键位"; }
document.getElementById("m-direction").innerText = "-";
document.getElementById("m-upper").innerText = "-"; function addLine(price, title, color){
document.getElementById("m-lower").innerText = "-"; if(!candleSeries || price===null || typeof price==="undefined") return;
document.getElementById("m-updiff").innerText = "-"; const p = Number(price);
document.getElementById("m-lowdiff").innerText = "-"; if(Number.isNaN(p) || p<=0) return;
return; priceLines.push(candleSeries.createPriceLine({
} price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
}));
document.getElementById("m-type").innerText = key.monitor_type || "-"; }
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = fmt(key.upper,8); function paintMeta(data){
document.getElementById("m-lower").innerText = fmt(key.lower,8); const key = data.key_monitor || null;
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`; document.getElementById("m-symbol").innerText = data.symbol || "-";
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`; document.getElementById("m-price").innerText = fmt(data.current_price,8);
}
if(!key){
function syncSymbolByKey(){ document.getElementById("m-type").innerText = "未匹配到关键位";
const keyId = keySelect.value; document.getElementById("m-direction").innerText = "-";
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId]; document.getElementById("m-upper").innerText = "-";
} document.getElementById("m-lower").innerText = "-";
document.getElementById("m-updiff").innerText = "-";
async function loadKeyKline(){ document.getElementById("m-lowdiff").innerText = "-";
if(!ensureChart()) return; return;
const keyId = keySelect.value; }
const symbol = (symbolInput.value || "").trim().toUpperCase();
const timeframe = tfSelect.value; document.getElementById("m-type").innerText = key.monitor_type || "-";
const limit = limitSelect.value; document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = fmt(key.upper,8);
if(!symbol && !keyId){ document.getElementById("m-lower").innerText = fmt(key.lower,8);
statusEl.className = "status err"; document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
statusEl.innerText = "请先输入币种或选择关键位"; document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
return; }
}
function syncSymbolByKey(){
statusEl.className = "status"; const keyId = keySelect.value;
statusEl.innerText = "加载中..."; if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
}
try{
const qs = new URLSearchParams(); async function loadKeyKline(){
if(keyId) qs.set("key_id", keyId); if(!ensureChart()) return;
if(symbol) qs.set("symbol", symbol); const keyId = keySelect.value;
qs.set("timeframe", timeframe); const symbol = (symbolInput.value || "").trim().toUpperCase();
qs.set("limit", limit); const timeframe = tfSelect.value;
const limit = limitSelect.value;
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
const data = await resp.json(); if(!symbol && !keyId){
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败"); statusEl.className = "status err";
statusEl.innerText = "请先输入币种或选择关键位";
const candles = Array.isArray(data.candles) ? data.candles : []; return;
if(!candles.length){ }
statusEl.className = "status err";
statusEl.innerText = "暂无K线数据"; statusEl.className = "status";
return; statusEl.innerText = "加载中...";
}
try{
if(!candleSeries) throw new Error("Series init failed"); const qs = new URLSearchParams();
candleSeries.setData(candles); if(keyId) qs.set("key_id", keyId);
resetPriceLines(); if(symbol) qs.set("symbol", symbol);
addLine(data.current_price, "现价", "#42a5f5"); qs.set("timeframe", timeframe);
if(data.key_monitor){ qs.set("limit", limit);
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f"); const resp = await fetch(`/api/key_kline?${qs.toString()}`);
} const data = await resp.json();
chart.timeScale().fitContent(); if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.className = "status"; if(!candles.length){
statusEl.innerText = `已加载 ${candles.length} 根K线`; statusEl.className = "status err";
}catch(err){ statusEl.innerText = "暂无K线数据";
statusEl.className = "status err"; return;
statusEl.innerText = err && err.message ? err.message : "加载失败"; }
}
} if(!candleSeries) throw new Error("Series init failed");
candleSeries.setData(candles);
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline); resetPriceLines();
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); }); addLine(data.current_price, "现价", "#42a5f5");
symbolInput.addEventListener("change", ()=>{ if(data.key_monitor){
if(symbolInput.value.trim()) keySelect.value = ""; addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
loadKeyKline(); addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
}); }
tfSelect.addEventListener("change", loadKeyKline); chart.timeScale().fitContent();
limitSelect.addEventListener("change", loadKeyKline); paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--";
syncSymbolByKey(); statusEl.className = "status";
loadKeyKline(); statusEl.innerText = `已加载 ${candles.length} 根K线`;
setInterval(loadKeyKline, refreshMs); }catch(err){
</script> statusEl.className = "status err";
</body> statusEl.innerText = err && err.message ? err.message : "加载失败";
}
}
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
symbolInput.addEventListener("change", ()=>{
if(symbolInput.value.trim()) keySelect.value = "";
loadKeyKline();
});
tfSelect.addEventListener("change", loadKeyKline);
limitSelect.addEventListener("change", loadKeyKline);
syncSymbolByKey();
loadKeyKline();
setInterval(loadKeyKline, refreshMs);
</script>
</body>
</html> </html>
+136 -118
View File
@@ -1,118 +1,136 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>登录 · {{ exchange_display }}</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
* { <title>登录 · {{ exchange_display }}</title>
margin: 0; <style>
padding: 0; * {
box-sizing: border-box; margin: 0;
} padding: 0;
body { box-sizing: border-box;
background: #0a0a10; }
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; body {
display: flex; background: #0a0a10;
align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
justify-content: center; display: flex;
height: 100vh; align-items: center;
color: #fff; justify-content: center;
} height: 100vh;
.login-box { color: #fff;
background: #12121a; }
padding: 2.5rem; .login-box {
border-radius: 16px; background: #12121a;
width: 100%; padding: 2.5rem;
max-width: 400px; border-radius: 16px;
border: 1px solid #242435; width: 100%;
box-shadow: 0 8px 24px rgba(0,0,0,0.3); max-width: 400px;
} border: 1px solid #242435;
.login-box h2 { box-shadow: 0 8px 24px rgba(0,0,0,0.3);
margin-bottom: 2rem; }
text-align: center; .login-box h2 {
font-size: 1.5rem; margin-bottom: 2rem;
background: linear-gradient(90deg, #4cc2ff, #7b42ff); text-align: center;
-webkit-background-clip: text; font-size: 1.5rem;
-webkit-text-fill-color: transparent; background: linear-gradient(90deg, #4cc2ff, #7b42ff);
} -webkit-background-clip: text;
.form-group { -webkit-text-fill-color: transparent;
margin-bottom: 1.25rem; }
} .form-group {
.form-group label { margin-bottom: 1.25rem;
display: block; }
margin-bottom: 0.5rem; .form-group label {
font-size: 0.9rem; display: block;
color: #a9a9ff; margin-bottom: 0.5rem;
} font-size: 0.9rem;
.form-group input { color: #a9a9ff;
width: 100%; }
padding: 0.85rem 1rem; .form-group input {
border-radius: 10px; width: 100%;
border: 1px solid #2e2e45; padding: 0.85rem 1rem;
background: #1a1a29; border-radius: 10px;
color: #fff; border: 1px solid #2e2e45;
font-size: 0.95rem; background: #1a1a29;
outline: none; color: #fff;
} font-size: 0.95rem;
.form-group input:focus { outline: none;
border-color: #4cc2ff; }
} .form-group input:focus {
button { border-color: #4cc2ff;
width: 100%; }
padding: 0.9rem; button {
border-radius: 10px; width: 100%;
border: none; padding: 0.9rem;
background: linear-gradient(90deg, #4285f4, #7b42ff); border-radius: 10px;
color: #fff; border: none;
font-size: 1rem; background: linear-gradient(90deg, #4285f4, #7b42ff);
font-weight: 500; color: #fff;
cursor: pointer; font-size: 1rem;
transition: 0.2s; font-weight: 500;
} cursor: pointer;
button:hover { transition: 0.2s;
opacity: 0.9; }
} button:hover {
.flash { opacity: 0.9;
padding: 0.8rem; }
margin-bottom: 1rem; .flash {
background: #331e24; padding: 0.8rem;
color: #ff6666; margin-bottom: 1rem;
border-radius: 8px; background: #331e24;
text-align: center; color: #ff6666;
font-size: 0.85rem; border-radius: 8px;
} text-align: center;
.exchange-line { font-size: 0.85rem;
text-align: center; }
font-size: 0.82rem; .exchange-line {
color: #8892b0; text-align: center;
margin: -0.5rem 0 1.25rem; font-size: 0.82rem;
} color: #8892b0;
.exchange-line strong { margin: -0.5rem 0 1.25rem;
color: #b8f5d0; }
font-weight: 600; .exchange-line strong {
} color: #b8f5d0;
</style> font-weight: 600;
</head> }
<body> </style>
<div class="login-box"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<h2>交易监控系统登录</h2>
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p> </head>
{% with messages = get_flashed_messages() %} <div class="login-theme-bar">
{% if messages %} <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<div class="flash">{{ messages[0] }}</div> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% endif %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
{% endwith %} <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<form method="POST"> </svg>
<div class="form-group"> </button>
<label>账号</label> <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<input type="text" name="username" required placeholder="请输入账号"> <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">
</div> <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"/>
<div class="form-group"> </svg>
<label>密码</label> </button>
<input type="password" name="password" required placeholder="请输入密码"> </div>
</div> </div>
<button type="submit">登录</button> <body>
</form> <div class="login-box">
</div> <h2>交易监控系统登录</h2>
</body> <p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
</html> {% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">{{ messages[0] }}</div>
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label>账号</label>
<input type="text" name="username" required placeholder="请输入账号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
@@ -1,214 +1,231 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ exchange_display }} | 实盘下单放大</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>{{ exchange_display }} | 实盘下单放大</title>
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
.empty{padding:18px;color:#95a2c2} #chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
.exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px} #chart{width:100%;height:100%}
</style> .empty{padding:18px;color:#95a2c2}
</head> .exchange-tag{font-size:.72rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:4px 10px;border-radius:999px;margin-left:8px}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% if orders %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>订单</label> </svg>
<select id="order-id"> </button>
{% for o in orders %} <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}> <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">
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }} <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"/>
</option> </svg>
{% endfor %} </button>
</select> </div>
<label>周期</label>
<select id="timeframe"> <div class="row">
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <a class="btn" href="/">返回首页</a>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> <strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
{% endfor %} </div>
</select> <div class="status">最近刷新:<span id="updated-at">--</span></div>
<button id="manual-refresh" type="button">刷新</button> </div>
<span id="load-status" class="status"></span> {% if orders %}
</div> <div class="row" style="margin-top:10px">
{% else %} <label>订单</label>
<div class="empty">当前没有激活订单,无法展示放大K线。</div> <select id="order-id">
{% endif %} {% for o in orders %}
</div> <option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
{% if orders %} </option>
<div class="card"> {% endfor %}
<div class="meta"> </select>
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <label>周期</label>
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> <select id="timeframe">
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div> </select>
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div> <button id="manual-refresh" type="button">刷新</button>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <span id="load-status" class="status"></span>
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div> </div>
</div> {% else %}
</div> <div class="empty">当前没有激活订单,无法展示放大K线。</div>
{% endif %}
<div class="card"> </div>
<div id="chart-wrap"><div id="chart"></div></div>
</div> {% if orders %}
{% endif %} <div class="card">
</div> <div class="meta">
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
{% if orders %} <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script> <div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
<script> <div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
const orderSelect = document.getElementById("order-id"); <div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
const chartHost = document.getElementById("chart"); </div>
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d); </div>
let chart = null; <div class="card">
let candleSeries = null; <div id="chart-wrap"><div id="chart"></div></div>
let priceLines = []; </div>
{% endif %}
function ensureChart(){ </div>
if(chart){ return true; }
if(!window.LightweightCharts){ {% if orders %}
statusEl.className = "status err"; <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
statusEl.innerText = "图表库加载失败"; <script>
return false; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
} const orderSelect = document.getElementById("order-id");
chart = LightweightCharts.createChart(chartHost, { const tfSelect = document.getElementById("timeframe");
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" }, const statusEl = document.getElementById("load-status");
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } }, const updatedAtEl = document.getElementById("updated-at");
rightPriceScale: { borderColor: "#2a3150" }, const chartHost = document.getElementById("chart");
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false }, const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
crosshair: { mode: 0 }
}); let chart = null;
candleSeries = chart.addCandlestickSeries({ let candleSeries = null;
upColor: "#4cd97f", let priceLines = [];
downColor: "#ff6666",
borderVisible: false, function ensureChart(){
wickUpColor: "#4cd97f", if(chart){ return true; }
wickDownColor: "#ff6666" if(!window.LightweightCharts){
}); statusEl.className = "status err";
window.addEventListener("resize", () => { statusEl.innerText = "图表库加载失败";
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); return false;
}); }
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); chart = LightweightCharts.createChart(chartHost, {
return true; layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
} grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
rightPriceScale: { borderColor: "#2a3150" },
function resetPriceLines(){ timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
if(!candleSeries){ return; } crosshair: { mode: 0 }
priceLines.forEach(line => { });
try { candleSeries.removePriceLine(line); } catch (_) {} candleSeries = chart.addCandlestickSeries({
}); upColor: "#4cd97f",
priceLines = []; downColor: "#ff6666",
} borderVisible: false,
wickUpColor: "#4cd97f",
function addLine(price, title, color){ wickDownColor: "#ff6666"
if(!candleSeries || typeof price === "undefined" || price === null){ return; } });
const p = Number(price); window.addEventListener("resize", () => {
if(Number.isNaN(p) || p <= 0){ return; } chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
priceLines.push(candleSeries.createPriceLine({ });
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
})); return true;
} }
function paintOrder(order){ function resetPriceLines(){
document.getElementById("m-symbol").innerText = order.symbol || "-"; if(!candleSeries){ return; }
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; priceLines.forEach(line => {
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8); try { candleSeries.removePriceLine(line); } catch (_) {}
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8); });
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8); priceLines = [];
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`; }
document.getElementById("m-breakeven").innerText =
(order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启"; function addLine(price, title, color){
document.getElementById("m-price").innerText = fmt(order.current_price, 8); if(!candleSeries || typeof price === "undefined" || price === null){ return; }
const pnlEl = document.getElementById("m-pnl"); const p = Number(price);
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`; if(Number.isNaN(p) || p <= 0){ return; }
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); priceLines.push(candleSeries.createPriceLine({
} price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
}));
async function loadOrderKline(){ }
if(!ensureChart()){ return; }
const orderId = orderSelect.value; function paintOrder(order){
const timeframe = tfSelect.value; document.getElementById("m-symbol").innerText = order.symbol || "-";
if(!orderId){ return; } document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
statusEl.className = "status"; document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
statusEl.innerText = "加载中..."; document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
try{ document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`); document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
const data = await resp.json(); document.getElementById("m-breakeven").innerText =
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); } (order.breakeven_enabled === false || order.breakeven_enabled === 0) ? "关闭" : "开启";
const candles = Array.isArray(data.candles) ? data.candles : []; document.getElementById("m-price").innerText = fmt(order.current_price, 8);
if(!candles.length){ const pnlEl = document.getElementById("m-pnl");
statusEl.className = "status err"; pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`;
statusEl.innerText = "暂无K线数据"; pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
return; }
}
candleSeries.setData(candles); async function loadOrderKline(){
resetPriceLines(); if(!ensureChart()){ return; }
addLine(data.order.trigger_price, "成交价", "#42a5f5"); const orderId = orderSelect.value;
addLine(data.order.stop_loss, "止损", "#ff6666"); const timeframe = tfSelect.value;
addLine(data.order.take_profit, "止盈", "#4cd97f"); if(!orderId){ return; }
chart.timeScale().fitContent(); statusEl.className = "status";
paintOrder(data.order || {}); statusEl.innerText = "加载中...";
updatedAtEl.innerText = data.updated_at || "--"; try{
statusEl.className = "status"; const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
statusEl.innerText = `已加载 ${candles.length} 根K线`; const data = await resp.json();
}catch(err){ if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
statusEl.className = "status err"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.innerText = err && err.message ? err.message : "加载失败"; if(!candles.length){
} statusEl.className = "status err";
} statusEl.innerText = "暂无K线数据";
return;
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline); }
orderSelect.addEventListener("change", loadOrderKline); candleSeries.setData(candles);
tfSelect.addEventListener("change", loadOrderKline); resetPriceLines();
loadOrderKline(); addLine(data.order.trigger_price, "成交价", "#42a5f5");
setInterval(loadOrderKline, refreshMs); addLine(data.order.stop_loss, "止损", "#ff6666");
</script> addLine(data.order.take_profit, "止盈", "#4cd97f");
{% endif %} chart.timeScale().fitContent();
<script> paintOrder(data.order || {});
(function(){ updatedAtEl.innerText = data.updated_at || "--";
if (typeof ensureChart !== 'function') return; statusEl.className = "status";
const oldEnsureChart = ensureChart; statusEl.innerText = `已加载 ${candles.length} 根K线`;
ensureChart = function(){ }catch(err){
if (chart && candleSeries) return true; statusEl.className = "status err";
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {} statusEl.innerText = err && err.message ? err.message : "加载失败";
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { }
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' }; }
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries; document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
} orderSelect.addEventListener("change", loadOrderKline);
return !!candleSeries; tfSelect.addEventListener("change", loadOrderKline);
}; loadOrderKline();
})(); setInterval(loadOrderKline, refreshMs);
</script> </script>
</body> {% endif %}
</html> <script>
(function(){
if (typeof ensureChart !== 'function') return;
const oldEnsureChart = ensureChart;
ensureChart = function(){
if (chart && candleSeries) return true;
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries;
}
return !!candleSeries;
};
})();
</script>
</body>
</html>
+20 -2
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=1"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32"> <link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
@@ -229,6 +231,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .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} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=1">
</head> </head>
<body data-page="{{ page }}"> <body data-page="{{ page }}">
{% macro period_stats(title, s) %} {% macro period_stats(title, s) %}
@@ -253,7 +257,21 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>加密货币|交易监控 + AI复盘一体化</h1> <h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
<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>
</div> </div>
<div class="top-nav"> <div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a> <a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
+276 -259
View File
@@ -1,260 +1,277 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>关键位放大 | K线查看</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>关键位放大 | K线查看</title>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} input,select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
</style> #chart-wrap{height:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
</head> #chart{width:100%;height:100%}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">关键位放大(可输入币种)</strong> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <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">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>币种</label> </svg>
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT"> </button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<label>关键位</label> <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">
<select id="key-id"> <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"/>
<option value="">无(仅看K线)</option> </svg>
{% for k in key_list %} </button>
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option> </div>
{% endfor %}
</select> <div class="row">
<a class="btn" href="/">返回首页</a>
<label>周期</label> <strong style="color:#dbe4ff">关键位放大(可输入币种)</strong>
<select id="timeframe"> </div>
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <div class="status">最近刷新:<span id="updated-at">--</span></div>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> </div>
{% endfor %}
</select> <div class="row" style="margin-top:10px">
<label>币种</label>
<label>K线数</label> <input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
<select id="kline-limit">
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option> <label>关键位</label>
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option> <select id="key-id">
</select> <option value="">无(仅看K线)</option>
{% for k in key_list %}
<button id="manual-refresh" type="button">刷新</button> <option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
<span id="load-status" class="status"></span> {% endfor %}
</div> </select>
</div>
<label>周期</label>
<div class="card"> <select id="timeframe">
<div class="meta"> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> </select>
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div> <label>K线数</label>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <select id="kline-limit">
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div> <option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div> <option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
</div> </select>
</div>
<button id="manual-refresh" type="button">刷新</button>
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div> <span id="load-status" class="status"></span>
</div> </div>
</div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script> <div class="card">
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta">
const keySelect = document.getElementById("key-id"); <div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
const symbolInput = document.getElementById("symbol-input"); <div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
const limitSelect = document.getElementById("kline-limit"); <div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const chartHost = document.getElementById("chart"); <div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d); <div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
const fmtSigned = (v,d=4)=>{ </div>
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-"; </div>
const n = Number(v);
return `${n>0?"+":""}${n.toFixed(d)}`; <div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
}; </div>
let chart = null; <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
let candleSeries = null; <script>
let priceLines = []; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
const keyMap = {}; const keySelect = document.getElementById("key-id");
{% for k in key_list %} const symbolInput = document.getElementById("symbol-input");
keyMap["{{ k.id }}"] = "{{ k.symbol }}"; const tfSelect = document.getElementById("timeframe");
{% endfor %} const limitSelect = document.getElementById("kline-limit");
const statusEl = document.getElementById("load-status");
function ensureChart(){ const updatedAtEl = document.getElementById("updated-at");
if(chart && candleSeries) return true; const chartHost = document.getElementById("chart");
if(!window.LightweightCharts){ const fmt = (v,d=6)=>(v===null||typeof v==="undefined"||Number.isNaN(Number(v)))?"-":Number(v).toFixed(d);
statusEl.className = "status err"; const fmtSigned = (v,d=4)=>{
statusEl.innerText = "图表库加载失败"; if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
return false; const n = Number(v);
} return `${n>0?"+":""}${n.toFixed(d)}`;
};
if(!chart){
chart = LightweightCharts.createChart(chartHost, { let chart = null;
layout:{background:{color:"#0f1320"},textColor:"#d6deff"}, let candleSeries = null;
grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}}, let priceLines = [];
rightPriceScale:{borderColor:"#2a3150"}, const keyMap = {};
timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false}, {% for k in key_list %}
crosshair:{mode:0} keyMap["{{ k.id }}"] = "{{ k.symbol }}";
}); {% endfor %}
window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); function ensureChart(){
}); if(chart && candleSeries) return true;
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight}); if(!window.LightweightCharts){
} statusEl.className = "status err";
statusEl.innerText = "图表库加载失败";
const opts = { return false;
upColor: "#4cd97f", }
downColor: "#ff6666",
borderVisible: false, if(!chart){
wickUpColor: "#4cd97f", chart = LightweightCharts.createChart(chartHost, {
wickDownColor: "#ff6666" layout:{background:{color:"#0f1320"},textColor:"#d6deff"},
}; grid:{vertLines:{color:"#1e263d"},horzLines:{color:"#1e263d"}},
if (typeof chart.addCandlestickSeries === "function") { rightPriceScale:{borderColor:"#2a3150"},
candleSeries = chart.addCandlestickSeries(opts); timeScale:{borderColor:"#2a3150",timeVisible:true,secondsVisible:false},
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { crosshair:{mode:0}
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts); });
} window.addEventListener("resize",()=>{
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
if(!candleSeries){ });
statusEl.className = "status err"; chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
statusEl.innerText = "K线序列初始化失败"; }
return false;
} const opts = {
return true; upColor: "#4cd97f",
} downColor: "#ff6666",
borderVisible: false,
function resetPriceLines(){ wickUpColor: "#4cd97f",
if(!candleSeries) return; wickDownColor: "#ff6666"
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} }); };
priceLines = []; if (typeof chart.addCandlestickSeries === "function") {
} candleSeries = chart.addCandlestickSeries(opts);
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
function addLine(price, title, color){ candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
if(!candleSeries || price===null || typeof price==="undefined") return; }
const p = Number(price);
if(Number.isNaN(p) || p<=0) return; if(!candleSeries){
priceLines.push(candleSeries.createPriceLine({ statusEl.className = "status err";
price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title statusEl.innerText = "K线序列初始化失败";
})); return false;
} }
return true;
function paintMeta(data){ }
const key = data.key_monitor || null;
document.getElementById("m-symbol").innerText = data.symbol || "-"; function resetPriceLines(){
document.getElementById("m-price").innerText = fmt(data.current_price,8); if(!candleSeries) return;
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
if(!key){ priceLines = [];
document.getElementById("m-type").innerText = "未匹配到关键位"; }
document.getElementById("m-direction").innerText = "-";
document.getElementById("m-upper").innerText = "-"; function addLine(price, title, color){
document.getElementById("m-lower").innerText = "-"; if(!candleSeries || price===null || typeof price==="undefined") return;
document.getElementById("m-updiff").innerText = "-"; const p = Number(price);
document.getElementById("m-lowdiff").innerText = "-"; if(Number.isNaN(p) || p<=0) return;
return; priceLines.push(candleSeries.createPriceLine({
} price:p,color,lineWidth:1,lineStyle:0,axisLabelVisible:true,title
}));
document.getElementById("m-type").innerText = key.monitor_type || "-"; }
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = fmt(key.upper,8); function paintMeta(data){
document.getElementById("m-lower").innerText = fmt(key.lower,8); const key = data.key_monitor || null;
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`; document.getElementById("m-symbol").innerText = data.symbol || "-";
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`; document.getElementById("m-price").innerText = fmt(data.current_price,8);
}
if(!key){
function syncSymbolByKey(){ document.getElementById("m-type").innerText = "未匹配到关键位";
const keyId = keySelect.value; document.getElementById("m-direction").innerText = "-";
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId]; document.getElementById("m-upper").innerText = "-";
} document.getElementById("m-lower").innerText = "-";
document.getElementById("m-updiff").innerText = "-";
async function loadKeyKline(){ document.getElementById("m-lowdiff").innerText = "-";
if(!ensureChart()) return; return;
const keyId = keySelect.value; }
const symbol = (symbolInput.value || "").trim().toUpperCase();
const timeframe = tfSelect.value; document.getElementById("m-type").innerText = key.monitor_type || "-";
const limit = limitSelect.value; document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
document.getElementById("m-upper").innerText = fmt(key.upper,8);
if(!symbol && !keyId){ document.getElementById("m-lower").innerText = fmt(key.lower,8);
statusEl.className = "status err"; document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
statusEl.innerText = "请先输入币种或选择关键位"; document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
return; }
}
function syncSymbolByKey(){
statusEl.className = "status"; const keyId = keySelect.value;
statusEl.innerText = "加载中..."; if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
}
try{
const qs = new URLSearchParams(); async function loadKeyKline(){
if(keyId) qs.set("key_id", keyId); if(!ensureChart()) return;
if(symbol) qs.set("symbol", symbol); const keyId = keySelect.value;
qs.set("timeframe", timeframe); const symbol = (symbolInput.value || "").trim().toUpperCase();
qs.set("limit", limit); const timeframe = tfSelect.value;
const limit = limitSelect.value;
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
const data = await resp.json(); if(!symbol && !keyId){
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败"); statusEl.className = "status err";
statusEl.innerText = "请先输入币种或选择关键位";
const candles = Array.isArray(data.candles) ? data.candles : []; return;
if(!candles.length){ }
statusEl.className = "status err";
statusEl.innerText = "暂无K线数据"; statusEl.className = "status";
return; statusEl.innerText = "加载中...";
}
try{
if(!candleSeries) throw new Error("Series init failed"); const qs = new URLSearchParams();
candleSeries.setData(candles); if(keyId) qs.set("key_id", keyId);
resetPriceLines(); if(symbol) qs.set("symbol", symbol);
addLine(data.current_price, "现价", "#42a5f5"); qs.set("timeframe", timeframe);
if(data.key_monitor){ qs.set("limit", limit);
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f"); const resp = await fetch(`/api/key_kline?${qs.toString()}`);
} const data = await resp.json();
chart.timeScale().fitContent(); if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.className = "status"; if(!candles.length){
statusEl.innerText = `已加载 ${candles.length} 根K线`; statusEl.className = "status err";
}catch(err){ statusEl.innerText = "暂无K线数据";
statusEl.className = "status err"; return;
statusEl.innerText = err && err.message ? err.message : "加载失败"; }
}
} if(!candleSeries) throw new Error("Series init failed");
candleSeries.setData(candles);
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline); resetPriceLines();
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); }); addLine(data.current_price, "现价", "#42a5f5");
symbolInput.addEventListener("change", ()=>{ if(data.key_monitor){
if(symbolInput.value.trim()) keySelect.value = ""; addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
loadKeyKline(); addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
}); }
tfSelect.addEventListener("change", loadKeyKline); chart.timeScale().fitContent();
limitSelect.addEventListener("change", loadKeyKline); paintMeta(data);
updatedAtEl.innerText = data.updated_at || "--";
syncSymbolByKey(); statusEl.className = "status";
loadKeyKline(); statusEl.innerText = `已加载 ${candles.length} 根K线`;
setInterval(loadKeyKline, refreshMs); }catch(err){
</script> statusEl.className = "status err";
</body> statusEl.innerText = err && err.message ? err.message : "加载失败";
}
}
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
symbolInput.addEventListener("change", ()=>{
if(symbolInput.value.trim()) keySelect.value = "";
loadKeyKline();
});
tfSelect.addEventListener("change", loadKeyKline);
limitSelect.addEventListener("change", loadKeyKline);
syncSymbolByKey();
loadKeyKline();
setInterval(loadKeyKline, refreshMs);
</script>
</body>
</html> </html>
+125 -107
View File
@@ -1,107 +1,125 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>系统登录</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
* { <title>系统登录</title>
margin: 0; <style>
padding: 0; * {
box-sizing: border-box; margin: 0;
} padding: 0;
body { box-sizing: border-box;
background: #0a0a10; }
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; body {
display: flex; background: #0a0a10;
align-items: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
justify-content: center; display: flex;
height: 100vh; align-items: center;
color: #fff; justify-content: center;
} height: 100vh;
.login-box { color: #fff;
background: #12121a; }
padding: 2.5rem; .login-box {
border-radius: 16px; background: #12121a;
width: 100%; padding: 2.5rem;
max-width: 400px; border-radius: 16px;
border: 1px solid #242435; width: 100%;
box-shadow: 0 8px 24px rgba(0,0,0,0.3); max-width: 400px;
} border: 1px solid #242435;
.login-box h2 { box-shadow: 0 8px 24px rgba(0,0,0,0.3);
margin-bottom: 2rem; }
text-align: center; .login-box h2 {
font-size: 1.5rem; margin-bottom: 2rem;
background: linear-gradient(90deg, #4cc2ff, #7b42ff); text-align: center;
-webkit-background-clip: text; font-size: 1.5rem;
-webkit-text-fill-color: transparent; background: linear-gradient(90deg, #4cc2ff, #7b42ff);
} -webkit-background-clip: text;
.form-group { -webkit-text-fill-color: transparent;
margin-bottom: 1.25rem; }
} .form-group {
.form-group label { margin-bottom: 1.25rem;
display: block; }
margin-bottom: 0.5rem; .form-group label {
font-size: 0.9rem; display: block;
color: #a9a9ff; margin-bottom: 0.5rem;
} font-size: 0.9rem;
.form-group input { color: #a9a9ff;
width: 100%; }
padding: 0.85rem 1rem; .form-group input {
border-radius: 10px; width: 100%;
border: 1px solid #2e2e45; padding: 0.85rem 1rem;
background: #1a1a29; border-radius: 10px;
color: #fff; border: 1px solid #2e2e45;
font-size: 0.95rem; background: #1a1a29;
outline: none; color: #fff;
} font-size: 0.95rem;
.form-group input:focus { outline: none;
border-color: #4cc2ff; }
} .form-group input:focus {
button { border-color: #4cc2ff;
width: 100%; }
padding: 0.9rem; button {
border-radius: 10px; width: 100%;
border: none; padding: 0.9rem;
background: linear-gradient(90deg, #4285f4, #7b42ff); border-radius: 10px;
color: #fff; border: none;
font-size: 1rem; background: linear-gradient(90deg, #4285f4, #7b42ff);
font-weight: 500; color: #fff;
cursor: pointer; font-size: 1rem;
transition: 0.2s; font-weight: 500;
} cursor: pointer;
button:hover { transition: 0.2s;
opacity: 0.9; }
} button:hover {
.flash { opacity: 0.9;
padding: 0.8rem; }
margin-bottom: 1rem; .flash {
background: #331e24; padding: 0.8rem;
color: #ff6666; margin-bottom: 1rem;
border-radius: 8px; background: #331e24;
text-align: center; color: #ff6666;
font-size: 0.85rem; border-radius: 8px;
} text-align: center;
</style> font-size: 0.85rem;
</head> }
<body> </style>
<div class="login-box"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<h2>交易监控系统登录</h2>
{% with messages = get_flashed_messages() %} </head>
{% if messages %} <div class="login-theme-bar">
<div class="flash">{{ messages[0] }}</div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
{% endif %} <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% endwith %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<form method="POST"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<div class="form-group"> </svg>
<label>账号</label> </button>
<input type="text" name="username" required placeholder="请输入账号"> <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
</div> <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">
<div class="form-group"> <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"/>
<label>密码</label> </svg>
<input type="password" name="password" required placeholder="请输入密码"> </button>
</div> </div>
<button type="submit">登录</button> </div>
</form> <body>
</div> <div class="login-box">
</body> <h2>交易监控系统登录</h2>
</html> {% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">{{ messages[0] }}</div>
{% endif %}
{% endwith %}
<form method="POST">
<div class="form-group">
<label>账号</label>
<input type="text" name="username" required placeholder="请输入账号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
+228 -211
View File
@@ -1,211 +1,228 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>实盘下单放大 | 100根K线</title> <script src="/static/instance_theme.js?v=1"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box} <title>实盘下单放大 | 100根K线</title>
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px} <style>
.container{width:min(98vw,1900px);margin:0 auto} *{margin:0;padding:0;box-sizing:border-box}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px} body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .container{width:min(98vw,1900px);margin:0 auto}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer} .card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.btn:hover{background:#1f2740} .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff} .btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px} .btn:hover{background:#1f2740}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px} select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta-item .k{font-size:.76rem;color:#9fb0d8} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all} .meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.status{font-size:.84rem;color:#95a2c2} .meta-item .k{font-size:.76rem;color:#9fb0d8}
.status.err{color:#ff8080} .meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px} .status{font-size:.84rem;color:#95a2c2}
#chart{width:100%;height:100%} .status.err{color:#ff8080}
.empty{padding:18px;color:#95a2c2} #chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
</style> #chart{width:100%;height:100%}
</head> .empty{padding:18px;color:#95a2c2}
<body> </style>
<div class="container"> <link rel="stylesheet" href="/static/instance_theme.css?v=1">
<div class="card">
<div class="row" style="justify-content:space-between"> </head>
<div class="row"> <body>
<a class="btn" href="/">返回首页</a> <div class="container">
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong> <div class="card">
</div> <div class="row" style="justify-content:space-between">
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
</div> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
{% if orders %} <svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<div class="row" style="margin-top:10px"> <path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
<label>订单</label> </svg>
<select id="order-id"> </button>
{% for o in orders %} <button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}> <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">
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }} <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"/>
</option> </svg>
{% endfor %} </button>
</select> </div>
<label>周期</label>
<select id="timeframe"> <div class="row">
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %} <a class="btn" href="/">返回首页</a>
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option> <strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
{% endfor %} </div>
</select> <div class="status">最近刷新:<span id="updated-at">--</span></div>
<button id="manual-refresh" type="button">刷新</button> </div>
<span id="load-status" class="status"></span> {% if orders %}
</div> <div class="row" style="margin-top:10px">
{% else %} <label>订单</label>
<div class="empty">当前没有激活订单,无法展示放大K线。</div> <select id="order-id">
{% endif %} {% for o in orders %}
</div> <option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
{% if orders %} </option>
<div class="card"> {% endfor %}
<div class="meta"> </select>
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div> <label>周期</label>
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div> <select id="timeframe">
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div> {% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div> <option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div> {% endfor %}
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div> </select>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div> <button id="manual-refresh" type="button">刷新</button>
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div> <span id="load-status" class="status"></span>
</div> </div>
</div> {% else %}
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
<div class="card"> {% endif %}
<div id="chart-wrap"><div id="chart"></div></div> </div>
</div>
{% endif %} {% if orders %}
</div> <div class="card">
<div class="meta">
{% if orders %} <div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script> <div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
<script> <div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000); <div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
const orderSelect = document.getElementById("order-id"); <div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
const tfSelect = document.getElementById("timeframe"); <div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
const statusEl = document.getElementById("load-status"); <div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
const updatedAtEl = document.getElementById("updated-at"); <div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
const chartHost = document.getElementById("chart"); </div>
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d); </div>
let chart = null; <div class="card">
let candleSeries = null; <div id="chart-wrap"><div id="chart"></div></div>
let priceLines = []; </div>
{% endif %}
function ensureChart(){ </div>
if(chart){ return true; }
if(!window.LightweightCharts){ {% if orders %}
statusEl.className = "status err"; <script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
statusEl.innerText = "图表库加载失败"; <script>
return false; const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
} const orderSelect = document.getElementById("order-id");
chart = LightweightCharts.createChart(chartHost, { const tfSelect = document.getElementById("timeframe");
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" }, const statusEl = document.getElementById("load-status");
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } }, const updatedAtEl = document.getElementById("updated-at");
rightPriceScale: { borderColor: "#2a3150" }, const chartHost = document.getElementById("chart");
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false }, const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
crosshair: { mode: 0 }
}); let chart = null;
candleSeries = chart.addCandlestickSeries({ let candleSeries = null;
upColor: "#4cd97f", let priceLines = [];
downColor: "#ff6666",
borderVisible: false, function ensureChart(){
wickUpColor: "#4cd97f", if(chart){ return true; }
wickDownColor: "#ff6666" if(!window.LightweightCharts){
}); statusEl.className = "status err";
window.addEventListener("resize", () => { statusEl.innerText = "图表库加载失败";
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); return false;
}); }
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); chart = LightweightCharts.createChart(chartHost, {
return true; layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
} grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
rightPriceScale: { borderColor: "#2a3150" },
function resetPriceLines(){ timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
if(!candleSeries){ return; } crosshair: { mode: 0 }
priceLines.forEach(line => { });
try { candleSeries.removePriceLine(line); } catch (_) {} candleSeries = chart.addCandlestickSeries({
}); upColor: "#4cd97f",
priceLines = []; downColor: "#ff6666",
} borderVisible: false,
wickUpColor: "#4cd97f",
function addLine(price, title, color){ wickDownColor: "#ff6666"
if(!candleSeries || typeof price === "undefined" || price === null){ return; } });
const p = Number(price); window.addEventListener("resize", () => {
if(Number.isNaN(p) || p <= 0){ return; } chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
priceLines.push(candleSeries.createPriceLine({ });
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
})); return true;
} }
function paintOrder(order){ function resetPriceLines(){
document.getElementById("m-symbol").innerText = order.symbol || "-"; if(!candleSeries){ return; }
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多"; priceLines.forEach(line => {
document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8); try { candleSeries.removePriceLine(line); } catch (_) {}
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8); });
document.getElementById("m-tp").innerText = fmt(order.take_profit, 8); priceLines = [];
const rr = order.rr_ratio; }
document.getElementById("m-rr").innerText = (rr === null || typeof rr === "undefined") ? "-:1" : `${Number(rr).toFixed(2)}:1`;
document.getElementById("m-price").innerText = fmt(order.current_price, 8); function addLine(price, title, color){
const pnlEl = document.getElementById("m-pnl"); if(!candleSeries || typeof price === "undefined" || price === null){ return; }
pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`; const p = Number(price);
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff"); if(Number.isNaN(p) || p <= 0){ return; }
} priceLines.push(candleSeries.createPriceLine({
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
async function loadOrderKline(){ }));
if(!ensureChart()){ return; } }
const orderId = orderSelect.value;
const timeframe = tfSelect.value; function paintOrder(order){
if(!orderId){ return; } document.getElementById("m-symbol").innerText = order.symbol || "-";
statusEl.className = "status"; document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
statusEl.innerText = "加载中..."; document.getElementById("m-entry").innerText = fmt(order.trigger_price, 8);
try{ document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`); document.getElementById("m-tp").innerText = fmt(order.take_profit, 8);
const data = await resp.json(); const rr = order.rr_ratio;
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); } document.getElementById("m-rr").innerText = (rr === null || typeof rr === "undefined") ? "-:1" : `${Number(rr).toFixed(2)}:1`;
const candles = Array.isArray(data.candles) ? data.candles : []; document.getElementById("m-price").innerText = fmt(order.current_price, 8);
if(!candles.length){ const pnlEl = document.getElementById("m-pnl");
statusEl.className = "status err"; pnlEl.innerText = `${fmt(order.float_pnl, 4)}U (${fmt(order.float_pct, 2)}%)`;
statusEl.innerText = "暂无K线数据"; pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
return; }
}
candleSeries.setData(candles); async function loadOrderKline(){
resetPriceLines(); if(!ensureChart()){ return; }
addLine(data.order.trigger_price, "成交价", "#42a5f5"); const orderId = orderSelect.value;
addLine(data.order.stop_loss, "止损", "#ff6666"); const timeframe = tfSelect.value;
addLine(data.order.take_profit, "止盈", "#4cd97f"); if(!orderId){ return; }
chart.timeScale().fitContent(); statusEl.className = "status";
paintOrder(data.order || {}); statusEl.innerText = "加载中...";
updatedAtEl.innerText = data.updated_at || "--"; try{
statusEl.className = "status"; const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
statusEl.innerText = `已加载 ${candles.length} 根K线`; const data = await resp.json();
}catch(err){ if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
statusEl.className = "status err"; const candles = Array.isArray(data.candles) ? data.candles : [];
statusEl.innerText = err && err.message ? err.message : "加载失败"; if(!candles.length){
} statusEl.className = "status err";
} statusEl.innerText = "暂无K线数据";
return;
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline); }
orderSelect.addEventListener("change", loadOrderKline); candleSeries.setData(candles);
tfSelect.addEventListener("change", loadOrderKline); resetPriceLines();
loadOrderKline(); addLine(data.order.trigger_price, "成交价", "#42a5f5");
setInterval(loadOrderKline, refreshMs); addLine(data.order.stop_loss, "止损", "#ff6666");
</script> addLine(data.order.take_profit, "止盈", "#4cd97f");
{% endif %} chart.timeScale().fitContent();
<script> paintOrder(data.order || {});
(function(){ updatedAtEl.innerText = data.updated_at || "--";
if (typeof ensureChart !== 'function') return; statusEl.className = "status";
const oldEnsureChart = ensureChart; statusEl.innerText = `已加载 ${candles.length} 根K线`;
ensureChart = function(){ }catch(err){
if (chart && candleSeries) return true; statusEl.className = "status err";
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {} statusEl.innerText = err && err.message ? err.message : "加载失败";
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) { }
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' }; }
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries; document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
} orderSelect.addEventListener("change", loadOrderKline);
return !!candleSeries; tfSelect.addEventListener("change", loadOrderKline);
}; loadOrderKline();
})(); setInterval(loadOrderKline, refreshMs);
</script> </script>
</body> {% endif %}
</html> <script>
(function(){
if (typeof ensureChart !== 'function') return;
const oldEnsureChart = ensureChart;
ensureChart = function(){
if (chart && candleSeries) return true;
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
return !!candleSeries;
}
return !!candleSeries;
};
})();
</script>
</body>
</html>
+62 -4
View File
@@ -28,6 +28,46 @@ from hub_sso import (
) )
def _merge_query_into_path(path: str, **params: str) -> str:
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
split = urlsplit(path or "/")
q = list(parse_qsl(split.query, keep_blank_values=True))
keys = {k for k, _ in q}
for k, v in params.items():
if not v or k in keys:
continue
q.append((k, str(v)))
return urlunsplit((split.scheme, split.netloc, split.path, urlencode(q), split.fragment))
def install_instance_theme_static(app) -> None:
"""仓库根 static/instance_theme.* 供四所页面共用。"""
import os
from flask import Response, send_file
repo_static = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
assets = {
"instance_theme.js": "application/javascript; charset=utf-8",
"instance_theme.css": "text/css; charset=utf-8",
}
for name, mime in assets.items():
path = os.path.join(repo_static, name)
def _view(p=path, m=mime):
if not os.path.isfile(p):
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
return send_file(p, mimetype=m)
app.add_url_rule(
f"/static/{name}",
endpoint=f"repo_static_{name.replace('.', '_')}",
view_func=_view,
)
def _hub_auth_required(f): def _hub_auth_required(f):
@wraps(f) @wraps(f)
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
@@ -149,6 +189,7 @@ def install_on_app(
} }
install_hub_embed_headers(app) install_hub_embed_headers(app)
configure_hub_embed_session(app) configure_hub_embed_session(app)
install_instance_theme_static(app)
register_hub_routes(app) register_hub_routes(app)
@@ -421,11 +462,22 @@ def register_hub_routes(app):
if _sso_wants_embed_auth() and request.is_secure: if _sso_wants_embed_auth() and request.is_secure:
boot = mint_hub_embed_bootstrap(ex, next_path) boot = mint_hub_embed_bootstrap(ex, next_path)
if boot: if boot:
q = urlencode({"t": boot, "next": next_path, "embed": "1"}) from urllib.parse import urlencode as _ue
return redirect(f"/hub-embed-auth?{q}")
qdict = {"t": boot, "next": next_path, "embed": "1"}
ht0 = (request.args.get("hub_theme") or "").strip().lower()
if ht0 in ("light", "dark"):
qdict["hub_theme"] = ht0
return redirect(f"/hub-embed-auth?{_ue(qdict)}")
session["logged_in"] = True session["logged_in"] = True
session.modified = True session.modified = True
return redirect(next_path) dest = next_path
if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
dest = _merge_query_into_path(dest, embed="1")
ht = (request.args.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
dest = _merge_query_into_path(dest, hub_theme=ht)
return redirect(dest)
hint = err or "校验失败" hint = err or "校验失败"
flash( flash(
f"中控 SSO 未生效({hint})。" f"中控 SSO 未生效({hint})。"
@@ -449,7 +501,13 @@ def register_hub_routes(app):
if ok: if ok:
session["logged_in"] = True session["logged_in"] = True
session.modified = True session.modified = True
return redirect(next_path) dest = next_path
if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
dest = _merge_query_into_path(dest, embed="1")
ht = (request.args.get("hub_theme") or "").strip().lower()
if ht in ("light", "dark"):
dest = _merge_query_into_path(dest, hub_theme=ht)
return redirect(dest)
hint = err or "校验失败" hint = err or "校验失败"
flash(f"iframe 登录未生效({hint})。可点本地导航工具栏「实例免密」重试。") flash(f"iframe 登录未生效({hint})。可点本地导航工具栏「实例免密」重试。")
return redirect("/login") return redirect("/login")
+8 -1
View File
@@ -1057,7 +1057,11 @@ def _require_hub_logged_in(request: Request) -> None:
@app.get("/api/instance/open-url") @app.get("/api/instance/open-url")
def api_instance_open_url( def api_instance_open_url(
request: Request, exchange_id: str, next: str = "/", embed: str = "" request: Request,
exchange_id: str,
next: str = "/",
embed: str = "",
hub_theme: str = "",
): ):
"""已登录中控时生成实例 SSO 打开链接(2h 有效、单次使用,复用 HUB_BRIDGE_TOKEN)。""" """已登录中控时生成实例 SSO 打开链接(2h 有效、单次使用,复用 HUB_BRIDGE_TOKEN)。"""
_require_hub_logged_in(request) _require_hub_logged_in(request)
@@ -1079,6 +1083,9 @@ def api_instance_open_url(
params = {"token": token, "next": nxt} params = {"token": token, "next": nxt}
if (embed or "").strip().lower() in ("1", "true", "yes", "on"): if (embed or "").strip().lower() in ("1", "true", "yes", "on"):
params["embed"] = "1" params["embed"] = "1"
ht = (hub_theme or "").strip().lower()
if ht in ("light", "dark"):
params["hub_theme"] = ht
q = urlencode(params) q = urlencode(params)
return { return {
"ok": True, "ok": True,
+13
View File
@@ -56,6 +56,9 @@
const next = nextPath || "/"; const next = nextPath || "/";
const q = new URLSearchParams({ exchange_id: String(exchangeId), next }); const q = new URLSearchParams({ exchange_id: String(exchangeId), next });
if (options.embed) q.set("embed", "1"); if (options.embed) q.set("embed", "1");
if (globalThis.HubTheme && typeof HubTheme.get === "function") {
q.set("hub_theme", HubTheme.get());
}
const r = await apiFetch("/api/instance/open-url?" + q.toString()); const r = await apiFetch("/api/instance/open-url?" + q.toString());
const j = await r.json(); const j = await r.json();
if (!j.ok || !j.url) { if (!j.ok || !j.url) {
@@ -135,6 +138,16 @@
shell.classList.remove("hidden"); shell.classList.remove("hidden");
shell.setAttribute("aria-hidden", "false"); shell.setAttribute("aria-hidden", "false");
document.body.classList.add("hub-instance-frame-open"); document.body.classList.add("hub-instance-frame-open");
frame.addEventListener("load", function syncInstanceFrameTheme() {
try {
if (globalThis.HubTheme && typeof HubTheme.get === "function" && frame.contentWindow) {
frame.contentWindow.postMessage(
{ type: "hub-theme-sync", theme: HubTheme.get() },
"*"
);
}
} catch (_) {}
});
} }
function closeInstanceFrame() { function closeInstanceFrame() {
+3 -3
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN" data-theme="dark"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script src="/assets/theme.js?v=20260604-hub-theme4"></script> <script src="/assets/theme.js?v=20260604-hub-inst-theme"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0b0e18" /> <meta name="theme-color" content="#0b0e18" />
<meta name="apple-mobile-web-app-title" content="中控" /> <meta name="apple-mobile-web-app-title" content="中控" />
@@ -248,7 +248,7 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-hub-chart-sse"></script> <script src="/assets/chart.js?v=20260604-hub-inst-theme"></script>
<script src="/assets/app.js?v=20260604-hub-chart-sse"></script> <script src="/assets/app.js?v=20260604-hub-inst-theme"></script>
</body> </body>
</html> </html>
+10
View File
@@ -15,6 +15,15 @@
} }
} }
function broadcastThemeToInstances() {
const msg = { type: "hub-theme-sync", theme: get() };
document.querySelectorAll("iframe#instance-frame, iframe.instance-frame").forEach((frame) => {
try {
if (frame.contentWindow) frame.contentWindow.postMessage(msg, "*");
} catch (_) {}
});
}
function apply(theme) { function apply(theme) {
const t = normalize(theme); const t = normalize(theme);
const root = document.documentElement; const root = document.documentElement;
@@ -26,6 +35,7 @@
if (meta) meta.setAttribute("content", META[t]); if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t; root.style.colorScheme = t;
document.dispatchEvent(new CustomEvent("hub-theme-change", { detail: { theme: t } })); document.dispatchEvent(new CustomEvent("hub-theme-change", { detail: { theme: t } }));
broadcastThemeToInstances();
return t; return t;
} }
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""为四所 templates 注入 instance_theme 脚本/样式与切换按钮。"""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EXCHANGES = ("crypto_monitor_binance", "crypto_monitor_okx", "crypto_monitor_gate", "crypto_monitor_gate_bot")
FILES = ("index.html", "login.html", "key_focus_v2.html", "order_focus_v2.html")
SCRIPT_TAG = ' <script src="/static/instance_theme.js?v=1"></script>\n'
CSS_LINK = ' <link rel="stylesheet" href="/static/instance_theme.css?v=1">\n'
THEME_TOGGLE = """ <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>
"""
INDEX_HEADER_OLD = """ <div class="header">
<h1>加密货币交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
</div>"""
INDEX_HEADER_NEW = """ <div class="header">
<h1>加密货币交易监控 + AI复盘一体化</h1>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
""" + THEME_TOGGLE + """ </div>
</div>"""
def patch_file(path: Path) -> bool:
text = path.read_text(encoding="utf-8")
orig = text
if 'data-theme="dark"' not in text:
text = text.replace('<html lang="zh-CN">', '<html lang="zh-CN" data-theme="dark">', 1)
if "/static/instance_theme.js" not in text:
text = text.replace(
"<meta charset=\"UTF-8\">",
"<meta charset=\"UTF-8\">\n" + SCRIPT_TAG.strip() + "\n",
1,
)
if "/static/instance_theme.css" not in text:
text = text.replace("</style>", "</style>\n" + CSS_LINK, 1)
if path.name == "index.html" and INDEX_HEADER_OLD in text and "instance-theme-toggle" not in text:
text = text.replace(INDEX_HEADER_OLD, INDEX_HEADER_NEW)
if path.name == "login.html" and "instance-theme-toggle" not in text:
text = text.replace(
"<body>",
'<div class="login-theme-bar">\n' + THEME_TOGGLE + "</div>\n<body>",
1,
)
if path.name == "key_focus_v2.html" and "instance-theme-toggle" not in text:
marker = '<div class="row" style="justify-content:space-between">'
if marker in text:
text = text.replace(
marker,
marker + "\n " + THEME_TOGGLE.replace("\n", "\n "),
1,
)
if path.name == "order_focus_v2.html" and "instance-theme-toggle" not in text:
marker = '<div class="row" style="justify-content:space-between">'
if marker in text:
text = text.replace(
marker,
marker + "\n " + THEME_TOGGLE.replace("\n", "\n "),
1,
)
if text != orig:
path.write_text(text, encoding="utf-8")
return True
return False
def main() -> None:
n = 0
for ex in EXCHANGES:
for fn in FILES:
p = ROOT / ex / "templates" / fn
if p.is_file() and patch_file(p):
print("patched", p.relative_to(ROOT))
n += 1
print("done", n, "files")
if __name__ == "__main__":
main()
+160
View File
@@ -0,0 +1,160 @@
/* 实例页亮色主题(覆盖模板内联暗色样式) */
html[data-theme="light"] {
color-scheme: light;
}
html[data-theme="light"] body {
background: #d8e2ec !important;
color: #1a2838 !important;
}
html[data-theme="light"] .header h1 {
color: #142232 !important;
}
html[data-theme="light"] .exchange-tag {
color: #087a50 !important;
background: rgba(10, 143, 92, 0.12) !important;
border-color: rgba(10, 143, 92, 0.35) !important;
}
html[data-theme="light"] .top-nav a {
background: #fff !important;
color: #006e9a !important;
border-color: rgba(0, 95, 140, 0.22) !important;
}
html[data-theme="light"] .top-nav a.active {
background: rgba(0, 110, 154, 0.12) !important;
color: #142232 !important;
}
html[data-theme="light"] .stat-item,
html[data-theme="light"] .card,
html[data-theme="light"] .meta-item,
html[data-theme="light"] .list-item {
background: #fff !important;
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .stat-item .label,
html[data-theme="light"] .status,
html[data-theme="light"] .rule-tip {
color: #4a6078 !important;
}
html[data-theme="light"] .stat-item .value,
html[data-theme="light"] .card h2 {
color: #142232 !important;
}
html[data-theme="light"] input,
html[data-theme="light"] select,
html[data-theme="light"] textarea {
background: #f6f9fc !important;
color: #142232 !important;
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .flash {
background: rgba(0, 110, 154, 0.1) !important;
color: #006e9a !important;
border-color: rgba(0, 95, 140, 0.22) !important;
}
html[data-theme="light"] th {
color: #4a6078 !important;
}
html[data-theme="light"] td {
color: #142232 !important;
border-bottom-color: #d0dae4 !important;
}
html[data-theme="light"] .ai-result,
html[data-theme="light"] .login-box {
background: #fff !important;
border-color: #b8c8d8 !important;
color: #142232 !important;
}
html[data-theme="light"] #chart-wrap {
background: #f0f4f9 !important;
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .btn {
background: #fff !important;
color: #006e9a !important;
border-color: rgba(0, 95, 140, 0.22) !important;
}
html[data-theme="light"] .btn:hover {
background: #eef3f8 !important;
}
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
border-radius: 8px;
border: 1px solid #304164;
background: #151a2a;
}
html[data-theme="light"] .theme-toggle {
background: #fff;
border-color: #b8c8d8;
}
.theme-toggle.is-hub-linked {
display: none !important;
}
.theme-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 30px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: #8fc8ff;
cursor: pointer;
}
html[data-theme="light"] .theme-toggle-btn {
color: #4a6078;
}
.theme-toggle-btn.is-active {
color: #dbe4ff;
background: rgba(79, 121, 255, 0.2);
box-shadow: inset 0 0 0 1px #304164;
}
html[data-theme="light"] .theme-toggle-btn.is-active {
color: #006e9a;
background: rgba(0, 110, 154, 0.12);
box-shadow: inset 0 0 0 1px #b8c8d8;
}
.header-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 6px;
}
.login-theme-bar {
display: flex;
justify-content: flex-end;
width: 100%;
max-width: 400px;
margin: 0 auto 10px;
}
+141
View File
@@ -0,0 +1,141 @@
/**
* 四所实例主题默认暗色单独登录用 instance-theme中控 iframe/SSO hub-theme 联动
*/
(function (global) {
const STANDALONE_KEY = "instance-theme";
const META = { dark: "#0b0d14", light: "#d8e2ec" };
function normalize(theme) {
return theme === "light" ? "light" : "dark";
}
function isHubLinked() {
try {
const p = new URLSearchParams(location.search);
if (p.get("embed") === "1") return true;
const ht = p.get("hub_theme");
if (ht === "light" || ht === "dark") return true;
} catch (_) {}
try {
if (window.self !== window.top) return true;
} catch (_) {
return true;
}
return false;
}
function themeFromUrl() {
try {
const t = new URLSearchParams(location.search).get("hub_theme");
if (t === "light" || t === "dark") return t;
} catch (_) {}
return null;
}
function getStandalone() {
try {
return normalize(localStorage.getItem(STANDALONE_KEY));
} catch (_) {
return "dark";
}
}
function setStandalone(theme) {
try {
localStorage.setItem(STANDALONE_KEY, normalize(theme));
} catch (_) {}
}
function get() {
if (isHubLinked()) {
return themeFromUrl() || _linkedTheme || "dark";
}
return getStandalone();
}
let _linkedTheme = null;
function apply(theme, opts) {
const options = opts || {};
const linked = isHubLinked();
const t = normalize(theme);
if (linked) {
_linkedTheme = t;
} else if (!options.skipStore) {
setStandalone(t);
}
const root = document.documentElement;
root.setAttribute("data-theme", t);
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t;
syncToggleUI();
document.dispatchEvent(
new CustomEvent("instance-theme-change", { detail: { theme: t, hubLinked: linked } })
);
return t;
}
function syncToggleUI(root) {
const scope = root || document;
const linked = isHubLinked();
const toggle = scope.querySelector(".instance-theme-toggle");
if (toggle) {
toggle.classList.toggle("is-hub-linked", linked);
toggle.setAttribute("aria-hidden", linked ? "true" : "false");
}
if (linked) return;
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
const on = btn.getAttribute("data-theme-value") === getStandalone();
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-pressed", on ? "true" : "false");
});
}
function initToggleUI(root) {
const scope = root || document;
syncToggleUI(scope);
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
if (btn.dataset.themeBound === "1") return;
btn.dataset.themeBound = "1";
btn.addEventListener("click", () => {
if (isHubLinked()) return;
apply(btn.getAttribute("data-theme-value"));
});
});
}
function initFromHubMessage(data) {
if (!data || data.type !== "hub-theme-sync") return;
if (!isHubLinked()) return;
apply(data.theme, { skipStore: true });
}
function boot() {
if (isHubLinked()) {
apply(themeFromUrl() || "dark", { skipStore: true });
window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
try {
window.parent.postMessage({ type: "instance-theme-ready" }, "*");
} catch (_) {}
} else {
apply(getStandalone());
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initToggleUI());
} else {
initToggleUI();
}
}
boot();
global.InstanceTheme = {
STANDALONE_KEY,
isHubLinked,
get,
apply,
initToggleUI,
syncToggleUI,
};
})(typeof window !== "undefined" ? window : globalThis);