diff --git a/static/hub.css b/static/hub.css index 123d107..6d91006 100644 --- a/static/hub.css +++ b/static/hub.css @@ -329,7 +329,6 @@ button, .btn { align-items: end; } .check { display: flex !important; align-items: center; gap: 8px; } -.inst-row { margin: 10px 0; border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; } .update-version { font-size: 0.82rem; font-variant-numeric: tabular-nums; @@ -345,6 +344,187 @@ button, .btn { .update-status.ok { color: var(--ok); } .update-status.err { color: var(--bad); } .update-table td { vertical-align: middle; } + +/* 实例页 */ +.inst-page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; +} +.inst-page-head-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} +.inst-add-trigger { + display: inline-flex; + align-items: center; + cursor: pointer; + margin: 0; +} +.inst-add-check { + position: absolute; + opacity: 0; + pointer-events: none; + width: 0; + height: 0; +} +.inst-shell { + max-width: 920px; +} +.inst-add-panel { + display: none; + border-bottom: 1px solid rgba(56, 189, 248, 0.12); +} +#inst-add-toggle:checked ~ .inst-shell .inst-add-panel { + display: block; +} +.inst-list-head { + border-bottom: 1px solid rgba(56, 189, 248, 0.12); +} +.inst-card-list { + padding: 16px 18px 20px; + display: flex; + flex-direction: column; + gap: 12px; +} +.inst-empty { + margin: 8px 4px 12px; + line-height: 1.5; + font-size: 0.88rem; +} +.inst-card { + border: 1px solid rgba(56, 189, 248, 0.16); + border-radius: 14px; + background: rgba(5, 12, 22, 0.45); + overflow: hidden; +} +.inst-card[open] { + border-color: rgba(56, 189, 248, 0.35); + box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.08); +} +.inst-card-summary { + list-style: none; + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + cursor: pointer; + user-select: none; +} +.inst-card-summary::-webkit-details-marker { display: none; } +.inst-card-main { flex: 1; min-width: 0; } +.inst-card-title-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} +.inst-card-name { + font-size: 1rem; + font-weight: 650; + letter-spacing: 0.02em; +} +.inst-card-url { + font-size: 0.8rem; + word-break: break-all; + line-height: 1.35; +} +.inst-card-aside { + display: flex; + align-items: center; + gap: 10px; + flex: 0 0 auto; +} +.inst-card-chevron { + width: 8px; + height: 8px; + border-right: 2px solid var(--muted); + border-bottom: 2px solid var(--muted); + transform: rotate(45deg); + transition: transform 0.15s ease; + margin-top: -4px; +} +.inst-card[open] .inst-card-chevron { + transform: rotate(225deg); + margin-top: 2px; +} +.inst-pill { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.02em; +} +.inst-pill--on { + color: #86efac; + background: rgba(22, 101, 52, 0.35); + border: 1px solid rgba(74, 222, 128, 0.35); +} +.inst-pill--off { + color: #fda4af; + background: rgba(127, 29, 29, 0.35); + border: 1px solid rgba(251, 113, 133, 0.35); +} +.inst-pill--tag { + color: #bae6fd; + background: rgba(14, 116, 144, 0.28); + border: 1px solid rgba(56, 189, 248, 0.28); + max-width: 12rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.inst-card-body { + padding: 0 8px 14px; + border-top: 1px solid rgba(56, 189, 248, 0.1); +} +.inst-card-body .inst-form { + padding-top: 14px; + padding-bottom: 8px; +} +.inst-card-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin-top: 4px; +} +.inst-card-actions .btn, +.inst-card-actions .settings-primary { + margin-top: 0; +} +.inst-delete-form { + padding: 0 12px 4px; +} +.inst-token-wrap { + display: flex; + gap: 8px; + align-items: stretch; + margin-top: 6px; +} +.inst-token-wrap .inst-token-input { + margin-top: 0 !important; + flex: 1; + min-width: 0; +} +.inst-token-wrap .inst-token-toggle { + flex: 0 0 auto; + align-self: center; + margin-top: 0; +} +@media (max-width: 720px) { + .inst-page-head { align-items: flex-start; } + .settings-row-2 { grid-template-columns: 1fr; } + .inst-card-summary { flex-direction: column; align-items: stretch; gap: 8px; } + .inst-card-aside { justify-content: space-between; } +} .overview-table-wrap { overflow-x: auto; margin-top: 12px; diff --git a/static/instances_page.js b/static/instances_page.js new file mode 100644 index 0000000..ff5c6b0 --- /dev/null +++ b/static/instances_page.js @@ -0,0 +1,69 @@ +(function () { + function fmtVer(v) { + if (!v || !v.online) return v && v.msg ? String(v.msg) : "离线"; + const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : ""); + const branch = v.branch ? v.branch + " · " : ""; + if (!short) return "未知版本"; + return branch + short + (v.dirty ? " (dirty)" : ""); + } + + async function refreshVersion(card) { + const iid = card.getAttribute("data-id"); + const verEl = card.querySelector("[data-inst-version]"); + if (!iid || !verEl) return; + verEl.textContent = "…"; + verEl.className = "update-version muted"; + try { + const r = await fetch("/api/instances/" + iid + "/version", { + credentials: "same-origin", + headers: { Accept: "application/json" }, + }); + const v = await r.json(); + if (v && v.online) { + verEl.textContent = fmtVer(v); + verEl.className = "update-version online"; + } else { + verEl.textContent = (v && v.msg) || "离线"; + verEl.className = "update-version offline"; + } + } catch (e) { + verEl.textContent = "探测失败"; + verEl.className = "update-version offline"; + } + } + + function bindTokenToggles(root) { + (root || document).querySelectorAll(".inst-token-toggle").forEach(function (btn) { + if (btn.dataset.bound) return; + btn.dataset.bound = "1"; + btn.addEventListener("click", function () { + const wrap = btn.closest(".inst-token-wrap"); + const input = wrap && wrap.querySelector(".inst-token-input"); + if (!input) return; + const show = input.type === "password"; + input.type = show ? "text" : "password"; + btn.textContent = show ? "隐藏" : "显示"; + }); + }); + } + + function boot() { + bindTokenToggles(document); + document.querySelectorAll(".inst-card[data-id]").forEach(function (card) { + refreshVersion(card); + const btn = card.querySelector("[data-inst-refresh-ver]"); + if (btn) { + btn.addEventListener("click", function (e) { + e.preventDefault(); + refreshVersion(card); + }); + } + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", boot); + } else { + boot(); + } +})(); diff --git a/templates/base.html b/templates/base.html index a5d2a33..6774723 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,7 +4,7 @@ {% block title %}期权中控{% endblock %} - + {% if session.get('logged_in') %} diff --git a/templates/instances.html b/templates/instances.html index c276d02..02376ab 100644 --- a/templates/instances.html +++ b/templates/instances.html @@ -1,55 +1,151 @@ {% extends "base.html" %} {% block title %}实例 · 期权中控{% endblock %} {% block body %} -

客户端实例

- -
-

添加实例

-
- - - - - - - - -
+{% set open_add = (not instances) or ((request.args.get('add') or '') == '1') %} +
+
+

Instances

+

客户端实例

+

登记各交易机地址与 Bridge Token;远程更新请到系统设置。

+
+
+ 系统更新 + +
-
-

已登记 ({{ instances|length }})

- {% if not instances %} -

暂无实例.先在 crypto_okx 配置 HUB_BRIDGE_TOKEN 并公网可访问.

- {% endif %} - {% for inst in instances %} -
- - {{ inst.name }} - {{ inst.base_url }} - {% if inst.enabled %}启用{% else %}停用{% endif %} - -
- - - - - - - - - + + +
+
+
+ New +

添加实例

+

Token 需与客户端 HUB_BRIDGE_TOKEN 一致;公网 URL 需可被中控访问。

+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
-
- - - -
-

- 内嵌打开 - 系统更新 -

-
- {% endfor %} + + +
+ Registry +

已登记 · {{ instances|length }}

+

点击卡片展开编辑;Token 默认隐藏。

+
+ +
+ {% if not instances %} +

暂无实例。先在客户端配置 HUB_BRIDGE_TOKEN 并保证公网可访问,再点右上角「添加实例」。

+ {% endif %} + {% for inst in instances %} +
+ +
+
+ {{ inst.name }} + {% if inst.enabled %} + 启用 + {% else %} + 停用 + {% endif %} + {% if inst.tags %} + {{ inst.tags }} + {% endif %} +
+
{{ inst.base_url }}
+
+
+ — + +
+
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ + 内嵌打开 + 系统更新 + +
+
+
+ + + +
+
+
+ {% endfor %} +
{% endblock %} +{% block scripts %} + +{% endblock %}