中控支持一键安装到桌面,HTTPS 下可装成独立应用。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-05 18:54:59 +08:00
parent 470c6ba261
commit 0327f49f4e
15 changed files with 220 additions and 5 deletions
+24
View File
@@ -362,6 +362,8 @@ def api_hub_backup_create():
env_path=str(ENV_PATH),
db_path=_resolved_hub_db_path(),
label="manual",
username=hub_username(),
password=hub_password(),
)
return jsonify(result)
except Exception as exc:
@@ -395,6 +397,12 @@ def api_hub_backup_restore():
env_path=str(ENV_PATH),
db_path=_resolved_hub_db_path(),
)
if result.get("ok"):
u = str(result.get("hub_username") or "").strip()
p = result.get("hub_password")
if u or p is not None:
set_hub_credentials(u or hub_username(), "" if p is None else str(p))
result.pop("hub_password", None)
return jsonify(result), (200 if result.get("ok") else 400)
@@ -517,6 +525,7 @@ try:
env_path=str(ENV_PATH),
db_path_fn=_resolved_hub_db_path,
hour=3,
get_credentials=lambda: (hub_username(), hub_password()),
)
except Exception as _hub_bak_err:
print(f"[backup] scheduler: {_hub_bak_err}")
@@ -781,6 +790,21 @@ def api_cond_orders_exit(hub_order_id: str):
return jsonify(post_cond_exit(inst, hub_order_id, payload))
@app.route("/manifest.webmanifest")
def pwa_manifest():
path = ROOT / "static" / "icons" / "manifest.webmanifest"
return send_file(path, mimetype="application/manifest+json; charset=utf-8")
@app.route("/sw.js")
def pwa_service_worker():
path = ROOT / "static" / "sw.js"
resp = send_file(path, mimetype="application/javascript; charset=utf-8")
resp.headers["Cache-Control"] = "no-cache"
resp.headers["Service-Worker-Allowed"] = "/"
return resp
if __name__ == "__main__":
# PM2 下禁止 debug reloader,否则会 Bad file descriptor 崩循环
_debug = (os.getenv("APP_DEBUG") or "").strip().lower() in ("1", "true", "yes", "on")
+39 -1
View File
@@ -169,11 +169,16 @@ a:hover { color: #7dd3fc; }
.nav-spot--flat .nav-spot-last,
.nav-spot--flat .nav-spot-chg,
.nav-spot--flat .nav-spot-pct { color: #9aa3bf; }
.topbar-logout {
.topbar-actions {
position: fixed;
top: 16px;
right: 22px;
z-index: 20;
display: flex;
align-items: center;
gap: 10px;
}
.topbar-logout {
color: var(--muted);
font-size: 0.82rem;
letter-spacing: 0.04em;
@@ -181,6 +186,39 @@ a:hover { color: #7dd3fc; }
.topbar-logout:hover {
color: #e8f1ff;
}
.topbar-install {
appearance: none;
border: 1px solid #304164;
background: #151a2a;
color: #dbe4ff;
font-size: 0.78rem;
border-radius: 8px;
padding: 4px 10px;
cursor: pointer;
font-family: inherit;
}
.topbar-install:hover {
color: #ffffff;
border-color: #3d5278;
}
.topbar-install-hint {
position: fixed;
top: 46px;
right: 22px;
z-index: 20;
margin: 0;
max-width: min(360px, calc(100vw - 40px));
color: var(--muted);
font-size: 0.75rem;
line-height: 1.4;
text-align: right;
}
@media (display-mode: standalone) {
.topbar-install,
.topbar-install-hint {
display: none !important;
}
}
@media (max-width: 1100px) {
.topbar-row {
grid-template-columns: 1fr;
+89
View File
@@ -0,0 +1,89 @@
(function (global) {
var deferredPrompt = null;
function isStandalone() {
return (
(global.matchMedia && global.matchMedia("(display-mode: standalone)").matches) ||
global.navigator.standalone === true
);
}
function btn() {
return document.getElementById("hub-install-btn");
}
function setHint(msg) {
var el = document.getElementById("hub-install-hint");
if (el) el.textContent = msg || "";
}
function showInstall() {
var el = btn();
if (!el || isStandalone()) return;
el.hidden = false;
}
function hideInstall() {
var el = btn();
if (el) el.hidden = true;
setHint("");
}
function downloadShortcut() {
var origin = global.location.origin;
var body = "[InternetShortcut]\r\nURL=" + origin + "/\r\n";
var blob = new Blob([body], { type: "application/internet-shortcut" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "期权中控.url";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () {
URL.revokeObjectURL(a.href);
}, 1000);
}
async function onInstallClick() {
setHint("");
if (deferredPrompt) {
deferredPrompt.prompt();
try {
var choice = await deferredPrompt.userChoice;
deferredPrompt = null;
if (choice && choice.outcome === "accepted") {
hideInstall();
return;
}
} catch (e) {
deferredPrompt = null;
}
}
downloadShortcut();
setHint("已下载快捷方式,拖到桌面即可。Chrome / Edge 也可在地址栏右侧点「安装」。");
}
function boot() {
if (isStandalone()) {
hideInstall();
return;
}
showInstall();
global.addEventListener("beforeinstallprompt", function (ev) {
ev.preventDefault();
deferredPrompt = ev;
showInstall();
});
global.addEventListener("appinstalled", function () {
deferredPrompt = null;
hideInstall();
});
var el = btn();
if (el) el.addEventListener("click", onInstallClick);
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(function () {});
}
}
global.HubPwa = { boot: boot };
})(window);
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#0b0d14"/>
<rect x="36" y="36" width="440" height="440" rx="88" fill="#121828" stroke="#ffffff" stroke-width="10"/>
<circle cx="256" cy="236" r="108" fill="none" stroke="#4285f4" stroke-width="22"/>
<path d="M196 176 L316 296 M316 176 L196 296" stroke="#ffffff" stroke-width="28" stroke-linecap="round"/>
<rect x="186" y="392" width="140" height="18" rx="9" fill="#7b42ff"/>
</svg>

After

Width:  |  Height:  |  Size: 508 B

+37
View File
@@ -0,0 +1,37 @@
{
"name": "加密货币|OKX 期权中控",
"short_name": "期权中控",
"description": "多机期权持仓总览与工作台",
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#070b14",
"theme_color": "#070b14",
"icons": [
{
"src": "/static/icons/icon-192.png?v=1",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/icons/icon-512.png?v=1",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/icons/icon-192-maskable.png?v=1",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/icon-512-maskable.png?v=1",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+7
View File
@@ -0,0 +1,7 @@
/* 期权中控 PWA:仅占位使可安装,请求一律走网络 */
self.addEventListener("install", function (event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener("activate", function (event) {
event.waitUntil(self.clients.claim());
});
+17 -4
View File
@@ -4,12 +4,23 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}加密货币|OKX 期权中控{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=36">
<meta name="theme-color" content="#070b14">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="期权中控">
<link rel="icon" href="{{ url_for('static', filename='icons/favicon.ico') }}?v=1" sizes="any">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='icons/apple-touch-icon.png') }}?v=1">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=37">
</head>
<body class="{% if request.path.startswith('/login') %}page-login{% elif request.path.startswith('/desk') %}page-desk{% elif request.path.startswith('/stats') %}page-stats{% elif request.path.startswith('/instances') %}page-instances{% elif request.path.startswith('/settings') %}page-settings{% elif request.path.startswith('/cond_orders') %}page-cond-orders{% elif request.path == '/' %}page-dashboard{% endif %}">
{% if session.get('logged_in') %}
<a href="/logout" class="topbar-logout">退出</a>
{% endif %}
<div class="topbar-actions">
<button type="button" class="topbar-install" id="hub-install-btn" hidden>安装到桌面</button>
{% if session.get('logged_in') %}
<a href="/logout" class="topbar-logout">退出</a>
{% endif %}
</div>
<p class="topbar-install-hint" id="hub-install-hint"></p>
<main class="main">
{% if session.get('logged_in') %}
<a href="/" class="topbar-brand">加密货币|OKX 期权中控</a>
@@ -54,5 +65,7 @@
{% if session.get('logged_in') and not request.path.startswith('/desk') %}
<script>if (window.HubQuotes) HubQuotes.boot();</script>
{% endif %}
<script src="{{ url_for('static', filename='hub_pwa.js') }}?v=1"></script>
<script>if (window.HubPwa) HubPwa.boot();</script>
</body>
</html>