Add proper PWA install icons and sim-aware app manifest.
Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -4953,10 +4953,19 @@ def login():
|
||||
return redirect("/")
|
||||
else:
|
||||
flash("账号或密码错误")
|
||||
is_sim = False
|
||||
try:
|
||||
from lib.sim.mode_lib import is_sim_mode as _is_sim_mode_fn
|
||||
|
||||
is_sim = bool(_is_sim_mode_fn(get_db))
|
||||
except Exception:
|
||||
is_sim = False
|
||||
from lib.instance.instance_embed_lib import pwa_app_name
|
||||
|
||||
return render_template(
|
||||
"login.html",
|
||||
exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
pwa_app_name="OKX 交易系统",
|
||||
pwa_app_name=pwa_app_name("okx", is_sim=is_sim),
|
||||
)
|
||||
|
||||
@app.route("/logout")
|
||||
@@ -5319,7 +5328,7 @@ def render_main_page(page="options", embed_mode=None):
|
||||
funding_usdt=funding_usdt,
|
||||
exchange_pnl_sync=exchange_pnl_sync,
|
||||
**strategy_extra,
|
||||
**embed_context_extras("okx"),
|
||||
**embed_context_extras("okx", is_sim=_sim_mode_for_header),
|
||||
**_display_ctx,
|
||||
**settings_page_context(
|
||||
page,
|
||||
@@ -7016,6 +7025,64 @@ def delete_journal(jid):
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/manifest.webmanifest")
|
||||
@app.route("/static/icons/manifest.webmanifest")
|
||||
def pwa_manifest():
|
||||
"""安装 App 用 manifest;模拟盘 short_name=OKX_sim."""
|
||||
is_sim = False
|
||||
try:
|
||||
from lib.sim.mode_lib import is_sim_mode as _is_sim_mode_fn
|
||||
|
||||
is_sim = bool(_is_sim_mode_fn(get_db))
|
||||
except Exception:
|
||||
is_sim = False
|
||||
from lib.instance.instance_embed_lib import pwa_app_name
|
||||
|
||||
name = pwa_app_name("okx", is_sim=is_sim)
|
||||
short = "OKX_sim" if is_sim else "OKX"
|
||||
payload = {
|
||||
"name": name,
|
||||
"short_name": short,
|
||||
"description": "OKX 期权与对冲交易监控",
|
||||
"id": "/",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0b0d14",
|
||||
"theme_color": "#0b0d14",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/icon-192.png?v=2",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-512.png?v=2",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-192-maskable.png?v=2",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable",
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-512-maskable.png?v=2",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable",
|
||||
},
|
||||
],
|
||||
}
|
||||
resp = jsonify(payload)
|
||||
resp.headers["Content-Type"] = "application/manifest+json; charset=utf-8"
|
||||
resp.headers["Cache-Control"] = "no-cache"
|
||||
return resp
|
||||
|
||||
|
||||
_REPO_STATIC_DIR = common_static_dir(_REPO_ROOT)
|
||||
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
||||
|
||||
@@ -201,9 +201,18 @@ def register_embed_routes(
|
||||
return resp
|
||||
|
||||
|
||||
def pwa_app_name(exchange_key: str) -> str:
|
||||
"""安装 App / 主屏幕显示名(各所独立标识)."""
|
||||
def pwa_app_name(exchange_key: str, *, is_sim: bool | None = None) -> str:
|
||||
"""安装 App / 主屏幕显示名(各所独立标识;模拟盘带 _sim)."""
|
||||
ex = (exchange_key or "").strip().lower()
|
||||
base = {
|
||||
"binance": "Binance",
|
||||
"okx": "OKX",
|
||||
"gate": "Gate",
|
||||
}.get(ex, "Crypto")
|
||||
if is_sim is True:
|
||||
return f"{base}_sim"
|
||||
if is_sim is False:
|
||||
return f"{base}_live"
|
||||
return {
|
||||
"binance": "Binance 交易系统",
|
||||
"okx": "OKX 交易系统",
|
||||
@@ -211,11 +220,11 @@ def pwa_app_name(exchange_key: str) -> str:
|
||||
}.get(ex, "交易系统")
|
||||
|
||||
|
||||
def embed_context_extras(exchange_key: str) -> dict:
|
||||
def embed_context_extras(exchange_key: str, *, is_sim: bool | None = None) -> dict:
|
||||
return {
|
||||
"order_rule_tips_tpl": order_rule_tips_template(exchange_key),
|
||||
"include_transfer_block": include_transfer_block(exchange_key),
|
||||
"ui_open_guard_enabled": ui_open_guard_enabled(exchange_key),
|
||||
"ui_orphan_recovery_enabled": ui_orphan_recovery_enabled(exchange_key),
|
||||
"pwa_app_name": pwa_app_name(exchange_key),
|
||||
"pwa_app_name": pwa_app_name(exchange_key, is_sim=is_sim),
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||
<script src="/static/open_submit_gate.js?v=1"></script>
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ pwa_app_name }}">
|
||||
<link rel="icon" href="/static/icons/favicon.ico?v=2" sizes="any">
|
||||
<link rel="icon" href="/static/icons/icon-32.png?v=2" type="image/png" sizes="32x32">
|
||||
<link rel="icon" href="/static/icons/icon.svg?v=2" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png?v=2">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<title>{{ pwa_app_name }}</title>
|
||||
</head>
|
||||
<body
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
<script src="/static/open_submit_gate.js?v=1"></script>
|
||||
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ pwa_app_name }}">
|
||||
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
|
||||
<link rel="icon" href="/static/icons/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icons/favicon.ico?v=2" sizes="any">
|
||||
<link rel="icon" href="/static/icons/icon-32.png?v=2" type="image/png" sizes="32x32">
|
||||
<link rel="icon" href="/static/icons/icon.svg?v=2" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png?v=2">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<title>{{ pwa_app_name }}</title>
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=17">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=121">
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<script src="/static/instance_theme.js?v=4"></script>
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ pwa_app_name }}">
|
||||
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32">
|
||||
<link rel="icon" href="/static/icons/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icons/favicon.ico?v=2" sizes="any">
|
||||
<link rel="icon" href="/static/icons/icon-32.png?v=2" type="image/png" sizes="32x32">
|
||||
<link rel="icon" href="/static/icons/icon.svg?v=2" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png?v=2">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<title>登录 · {{ pwa_app_name }}</title>
|
||||
<style>
|
||||
* {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Generate PWA / favicon assets under static/icons."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
BASE = os.path.join(os.path.dirname(__file__), "..", "static", "icons")
|
||||
BASE = os.path.abspath(BASE)
|
||||
|
||||
BG = (11, 13, 20, 255)
|
||||
PANEL = (18, 24, 40, 255)
|
||||
ACCENT = (66, 133, 244, 255)
|
||||
ACCENT2 = (123, 66, 255, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
|
||||
|
||||
def thick_line(draw: ImageDraw.ImageDraw, p1, p2, t: float, color) -> None:
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
length = math.hypot(dx, dy) or 1.0
|
||||
ux, uy = -dy / length, dx / length
|
||||
hx, hy = ux * t / 2, uy * t / 2
|
||||
draw.polygon(
|
||||
[
|
||||
(x1 + hx, y1 + hy),
|
||||
(x2 + hx, y2 + hy),
|
||||
(x2 - hx, y2 - hy),
|
||||
(x1 - hx, y1 - hy),
|
||||
],
|
||||
fill=color,
|
||||
)
|
||||
|
||||
|
||||
def make_icon(size: int, *, maskable: bool = False) -> Image.Image:
|
||||
im = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(im)
|
||||
pad = int(size * (0.08 if maskable else 0.0))
|
||||
outer = [pad, pad, size - 1 - pad, size - 1 - pad]
|
||||
r = int((size - 2 * pad) * 0.22)
|
||||
d.rounded_rectangle(outer, radius=r, fill=BG)
|
||||
inset = int(size * 0.07)
|
||||
inner = [pad + inset, pad + inset, size - 1 - pad - inset, size - 1 - pad - inset]
|
||||
r2 = max(8, int((size - 2 * pad - 2 * inset) * 0.18))
|
||||
d.rounded_rectangle(inner, radius=r2, fill=PANEL, outline=WHITE, width=max(2, size // 64))
|
||||
cx = cy = size // 2
|
||||
ring_r = int(size * 0.22)
|
||||
stroke = max(3, size // 28)
|
||||
bbox = [
|
||||
cx - ring_r,
|
||||
cy - ring_r - int(size * 0.02),
|
||||
cx + ring_r,
|
||||
cy + ring_r - int(size * 0.02),
|
||||
]
|
||||
d.ellipse(bbox, outline=ACCENT, width=stroke)
|
||||
arm = int(size * 0.11)
|
||||
thick = max(3, size // 26)
|
||||
ox = cx
|
||||
oy = cy - int(size * 0.02)
|
||||
thick_line(d, (ox - arm, oy - arm), (ox + arm, oy + arm), thick, WHITE)
|
||||
thick_line(d, (ox + arm, oy - arm), (ox - arm, oy + arm), thick, WHITE)
|
||||
bar_h = max(3, size // 28)
|
||||
bar_w = int(size * 0.28)
|
||||
by = int(size * (0.74 if maskable else 0.78))
|
||||
d.rounded_rectangle(
|
||||
[cx - bar_w // 2, by, cx + bar_w // 2, by + bar_h],
|
||||
radius=max(1, bar_h // 2),
|
||||
fill=ACCENT2,
|
||||
)
|
||||
return im
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(BASE, exist_ok=True)
|
||||
mapping = {
|
||||
"icon-16.png": 16,
|
||||
"icon-32.png": 32,
|
||||
"apple-touch-icon.png": 180,
|
||||
"icon-192.png": 192,
|
||||
"icon-512.png": 512,
|
||||
}
|
||||
for name, sz in mapping.items():
|
||||
path = os.path.join(BASE, name)
|
||||
make_icon(sz).save(path, format="PNG", optimize=True)
|
||||
print("wrote", name, os.path.getsize(path))
|
||||
|
||||
for name, sz in (("icon-192-maskable.png", 192), ("icon-512-maskable.png", 512)):
|
||||
path = os.path.join(BASE, name)
|
||||
make_icon(sz, maskable=True).save(path, format="PNG", optimize=True)
|
||||
print("wrote", name, os.path.getsize(path))
|
||||
|
||||
ico_sizes = [16, 32, 48]
|
||||
frames = [make_icon(s) for s in ico_sizes]
|
||||
ico_path = os.path.join(BASE, "favicon.ico")
|
||||
frames[0].save(
|
||||
ico_path,
|
||||
format="ICO",
|
||||
sizes=[(s, s) for s in ico_sizes],
|
||||
append_images=frames[1:],
|
||||
)
|
||||
print("wrote favicon.ico", os.path.getsize(ico_path))
|
||||
|
||||
svg = """<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>
|
||||
"""
|
||||
with open(os.path.join(BASE, "icon.svg"), "w", encoding="utf-8") as f:
|
||||
f.write(svg)
|
||||
print("wrote icon.svg")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 133 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 108 B After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 254 B After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -1,6 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="108" fill="#0c1019"/>
|
||||
<rect x="36" y="36" width="440" height="440" rx="88" fill="#141b2d"/>
|
||||
<rect x="36" y="36" width="440" height="440" rx="88" fill="none" stroke="#FFFFFF" stroke-width="12"/>
|
||||
<rect x="168" y="168" width="72" height="72" rx="10" fill="#FFFFFF"/><rect x="272" y="168" width="72" height="72" rx="10" fill="#FFFFFF"/><rect x="168" y="272" width="72" height="72" rx="10" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 508 B |
@@ -1,23 +1,37 @@
|
||||
{
|
||||
"name": "OKX 交易系统",
|
||||
"short_name": "OKX 交易系统",
|
||||
"description": "OKX 永续交易监控与复盘",
|
||||
"short_name": "OKX",
|
||||
"description": "OKX 期权与对冲交易监控",
|
||||
"id": "/",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0b0d14",
|
||||
"theme_color": "#FFFFFF",
|
||||
"theme_color": "#0b0d14",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/icon-192.png",
|
||||
"src": "/static/icons/icon-192.png?v=2",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-512.png",
|
||||
"src": "/static/icons/icon-512.png?v=2",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-192-maskable.png?v=2",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-512-maskable.png?v=2",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||