Use per-exchange PWA names and logos for instance app installs.

Show Binance/OKX/Gate 交易系统 with distinct exchange icons in manifests, apple titles, and login branding.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-16 08:44:07 +08:00
parent 717a3d5694
commit 19a4a5194d
62 changed files with 327 additions and 89 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+6
View File
@@ -0,0 +1,6 @@
<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="#F0B90B" stroke-width="12"/>
<polygon points="256,150 362,256 256,362 150,256" fill="#F0B90B"/><polygon points="256,210 302,256 256,302 210,256" fill="#1a160a"/>
</svg>

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+6
View File
@@ -0,0 +1,6 @@
<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="#17E6A1" stroke-width="12"/>
<text x="256" y="310" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="220" font-weight="700" fill="#17E6A1">G</text>
</svg>

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+6
View File
@@ -0,0 +1,6 @@
<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>

After

Width:  |  Height:  |  Size: 515 B

+23
View File
@@ -0,0 +1,23 @@
{
"name": "Binance 交易系统",
"short_name": "Binance 交易系统",
"description": "Binance 永续交易监控与复盘",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
"theme_color": "#F0B90B",
"icons": [
{
"src": "__ICON_PREFIX__/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "__ICON_PREFIX__/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "交易监控复盘",
"short_name": "监控",
"description": "加密货币永续交易监控与复盘",
"name": "交易系统",
"short_name": "交易系统",
"description": "加密货币永续交易监控与复盘(请使用各所独立 manifest)",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
+23
View File
@@ -0,0 +1,23 @@
{
"name": "Gate 交易系统",
"short_name": "Gate 交易系统",
"description": "Gate 永续交易监控与复盘",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
"theme_color": "#17E6A1",
"icons": [
{
"src": "__ICON_PREFIX__/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "__ICON_PREFIX__/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "OKX 交易系统",
"short_name": "OKX 交易系统",
"description": "OKX 永续交易监控与复盘",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
"theme_color": "#FFFFFF",
"icons": [
{
"src": "__ICON_PREFIX__/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "__ICON_PREFIX__/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
+5 -1
View File
@@ -6984,7 +6984,11 @@ def login():
return redirect("/")
else:
flash("账号或密码错误")
return render_template("login.html", exchange_display=EXCHANGE_DISPLAY_NAME)
return render_template(
"login.html",
exchange_display=EXCHANGE_DISPLAY_NAME,
pwa_app_name="Binance 交易系统",
)
@app.route("/logout")
def logout():
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

+2 -13
View File
@@ -1,17 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="100%" stop-color="#34d399"/>
</linearGradient>
</defs>
<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="url(#g)" stroke-width="12"/>
<path d="M120 320 L200 248 L280 272 L392 168" fill="none" stroke="url(#g)" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="392" cy="168" r="18" fill="#34d399"/>
<rect x="168" y="268" width="28" height="64" rx="6" fill="#f87171"/>
<line x1="182" y1="248" x2="182" y2="340" stroke="#f87171" stroke-width="10" stroke-linecap="round"/>
<rect x="268" y="220" width="28" height="96" rx="6" fill="#34d399"/>
<line x1="282" y1="200" x2="282" y2="340" stroke="#34d399" stroke-width="10" stroke-linecap="round"/>
<rect x="36" y="36" width="440" height="440" rx="88" fill="none" stroke="#F0B90B" stroke-width="12"/>
<polygon points="256,150 362,256 256,362 150,256" fill="#F0B90B"/><polygon points="256,210 302,256 256,302 210,256" fill="#1a160a"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 440 B

@@ -1,11 +1,11 @@
{
"name": "交易监控复盘",
"short_name": "监控",
"description": "加密货币永续交易监控与复盘",
"name": "Binance 交易系统",
"short_name": "Binance 交易系统",
"description": "Binance 永续交易监控与复盘",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
"theme_color": "#0b0d14",
"theme_color": "#F0B90B",
"icons": [
{
"src": "/static/icons/icon-192.png",
+5 -1
View File
@@ -6637,7 +6637,11 @@ def login():
return redirect("/")
else:
flash("账号或密码错误")
return render_template("login.html", exchange_display=EXCHANGE_DISPLAY_NAME)
return render_template(
"login.html",
exchange_display=EXCHANGE_DISPLAY_NAME,
pwa_app_name="Gate 交易系统",
)
@app.route("/logout")
def logout():
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

+2 -13
View File
@@ -1,17 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="100%" stop-color="#34d399"/>
</linearGradient>
</defs>
<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="url(#g)" stroke-width="12"/>
<path d="M120 320 L200 248 L280 272 L392 168" fill="none" stroke="url(#g)" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="392" cy="168" r="18" fill="#34d399"/>
<rect x="168" y="268" width="28" height="64" rx="6" fill="#f87171"/>
<line x1="182" y1="248" x2="182" y2="340" stroke="#f87171" stroke-width="10" stroke-linecap="round"/>
<rect x="268" y="220" width="28" height="96" rx="6" fill="#34d399"/>
<line x1="282" y1="200" x2="282" y2="340" stroke="#34d399" stroke-width="10" stroke-linecap="round"/>
<rect x="36" y="36" width="440" height="440" rx="88" fill="none" stroke="#17E6A1" stroke-width="12"/>
<text x="256" y="310" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="220" font-weight="700" fill="#17E6A1">G</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 451 B

@@ -1,11 +1,11 @@
{
"name": "交易监控复盘",
"short_name": "监控",
"description": "加密货币永续交易监控与复盘",
"name": "Gate 交易系统",
"short_name": "Gate 交易系统",
"description": "Gate 永续交易监控与复盘",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
"theme_color": "#0b0d14",
"theme_color": "#17E6A1",
"icons": [
{
"src": "/static/icons/icon-192.png",
+5 -1
View File
@@ -6451,7 +6451,11 @@ def login():
return redirect("/")
else:
flash("账号或密码错误")
return render_template("login.html", exchange_display=EXCHANGE_DISPLAY_NAME)
return render_template(
"login.html",
exchange_display=EXCHANGE_DISPLAY_NAME,
pwa_app_name="OKX 交易系统",
)
@app.route("/logout")
def logout():
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+2 -13
View File
@@ -1,17 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="100%" stop-color="#34d399"/>
</linearGradient>
</defs>
<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="url(#g)" stroke-width="12"/>
<path d="M120 320 L200 248 L280 272 L392 168" fill="none" stroke="url(#g)" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="392" cy="168" r="18" fill="#34d399"/>
<rect x="168" y="268" width="28" height="64" rx="6" fill="#f87171"/>
<line x1="182" y1="248" x2="182" y2="340" stroke="#f87171" stroke-width="10" stroke-linecap="round"/>
<rect x="268" y="220" width="28" height="96" rx="6" fill="#34d399"/>
<line x1="282" y1="200" x2="282" y2="340" stroke="#34d399" stroke-width="10" stroke-linecap="round"/>
<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>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 515 B

@@ -1,11 +1,11 @@
{
"name": "交易监控复盘",
"short_name": "监控",
"description": "加密货币永续交易监控与复盘",
"name": "OKX 交易系统",
"short_name": "OKX 交易系统",
"description": "OKX 永续交易监控与复盘",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
"theme_color": "#0b0d14",
"theme_color": "#FFFFFF",
"icons": [
{
"src": "/static/icons/icon-192.png",
+6 -2
View File
@@ -9,13 +9,17 @@
3. `link rel="icon"` / `favicon.ico`
4. 若都没有 → 灰色地球或网页标题首字
本仓库已在 **中控****三所监控页** 配置统一品牌图标(深色圆角底 + 青绿趋势线 + 简化的 K 线),与页面 UI 一致.PNG/ICO 由 **Pillow** 生成,避免损坏的 favicon 出现花屏.
本仓库已在 **中控****三所监控页** 配置品牌图标.PNG/ICO 由 **Pillow** 生成.
- **中控**:深色圆角底 + 青绿趋势线 + K 线,安装名「复盘系统中控」
- **三所**:各所用交易所标识色与字标(币安菱形 / OKX 方块 / Gate G),安装名分别为「Binance 交易系统」「OKX 交易系统」「Gate 交易系统」
## 文件位置
| 位置 | 访问路径 |
|------|----------|
| 源稿 | `brand/icon.svg`,`brand/icons/*.png` |
| 源稿 | `brand/icon.svg`,`brand/icons/*.png`,`brand/icons/{binance,okx,gate}/` |
| Manifest | `brand/manifest.webmanifest`(中控),`brand/manifest.{binance,okx,gate}.webmanifest` |
| 中控 | `manual_trading_hub/static/icons/``/assets/icons/...` |
| 三所 | `crypto_monitor_*/static/icons/``/static/icons/...` |
+11
View File
@@ -179,10 +179,21 @@ def register_embed_routes(
return jsonify({"ok": True, "page": tab, "html": html})
def pwa_app_name(exchange_key: str) -> str:
"""安装 App / 主屏幕显示名(各所独立标识)."""
ex = (exchange_key or "").strip().lower()
return {
"binance": "Binance 交易系统",
"okx": "OKX 交易系统",
"gate": "Gate 交易系统",
}.get(ex, "交易系统")
def embed_context_extras(exchange_key: str) -> dict:
return {
"order_rule_tips_tpl": order_rule_tips_template(exchange_key),
"include_transfer_block": include_transfer_block(exchange_key),
"ui_open_guard_enabled": ui_open_guard_enabled(exchange_key),
"ui_orphan_recovery_enabled": ui_orphan_recovery_enabled(exchange_key),
"pwa_app_name": pwa_app_name(exchange_key),
}
+1 -1
View File
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/static/instance_theme.css?v=92">
<script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<title>{{ pwa_app_name }}</title>
</head>
<body
data-embed-shell="1"
+2 -2
View File
@@ -10,12 +10,12 @@
<script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控">
<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">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<title>{{ pwa_app_name }}</title>
<link rel="stylesheet" href="/static/instance_page.css?v=3">
<link rel="stylesheet" href="/static/instance_theme.css?v=92">
+9 -3
View File
@@ -4,7 +4,13 @@
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script>
<title>登录 · {{ exchange_display }}</title>
<meta name="theme-color" content="#0b0d14">
<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">
<title>登录 · {{ pwa_app_name }}</title>
<style>
* {
margin: 0;
@@ -113,8 +119,8 @@
</div>
<body>
<div class="login-box">
<h2>交易监控系统登录</h2>
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
<h2>{{ pwa_app_name }}</h2>
<p class="exchange-line">登录 · <strong>{{ exchange_display }}</strong></p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">{{ messages[0] }}</div>
+159 -12
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
"""生成品牌 PNG/ICO(Pillow),供 Chrome 快捷方式与 manifest 使用."""
"""生成品牌 PNG/ICO(Pillow),供 Chrome 快捷方式与 manifest 使用.
中控用通用监控图标;三所各自用交易所标识色+字标.
"""
from __future__ import annotations
import os
@@ -14,6 +17,27 @@ CYAN = (34, 211, 238, 255)
GREEN = (52, 211, 153, 255)
RED = (248, 113, 113, 255)
EXCHANGES = {
"binance": {
"label": "B",
"accent": (240, 185, 11, 255),
"panel": (26, 22, 10, 255),
"svg_fill": "#F0B90B",
},
"okx": {
"label": "OKX",
"accent": (255, 255, 255, 255),
"panel": (18, 18, 18, 255),
"svg_fill": "#FFFFFF",
},
"gate": {
"label": "G",
"accent": (23, 230, 161, 255),
"panel": (10, 28, 24, 255),
"svg_fill": "#17E6A1",
},
}
def _lerp(c1: tuple[int, ...], c2: tuple[int, ...], t: float) -> tuple[int, int, int, int]:
t = max(0.0, min(1.0, t))
@@ -24,6 +48,25 @@ def _rounded_rect(draw, box, radius: int, fill) -> None:
draw.rounded_rectangle(box, radius=radius, fill=fill)
def _font(size: int):
from PIL import ImageFont
candidates = [
os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "arialbd.ttf"),
os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "segoeuib.ttf"),
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
]
for path in candidates:
if path and os.path.isfile(path):
try:
return ImageFont.truetype(path, size=size)
except OSError:
continue
return ImageFont.load_default()
def render_icon(size: int):
from PIL import Image, ImageDraw
@@ -35,7 +78,6 @@ def render_icon(size: int):
inner = m + max(2, size // 28)
_rounded_rect(draw, (inner, inner, size - inner, size - inner), max(6, r - 4), PANEL)
# 渐变描边(四角采样)
border = max(2, size // 42)
for i in range(border):
t0 = i / max(1, border - 1)
@@ -56,7 +98,6 @@ def render_icon(size: int):
def sy(v: float) -> int:
return int(v * size / 512)
# 趋势线
pts = [(120, 320), (200, 248), (280, 272), (392, 168)]
scaled = [(sx(x), sy(y)) for x, y in pts]
draw.line(scaled, fill=CYAN, width=max(2, size // 26), joint="curve")
@@ -66,7 +107,6 @@ def render_icon(size: int):
fill=GREEN,
)
# 蜡烛
def candle(cx, top, bottom, body_top, body_bottom, color):
w = max(1, size // 64)
bh = max(2, size // 32)
@@ -83,29 +123,136 @@ def render_icon(size: int):
return img
def main() -> None:
def render_exchange_icon(size: int, key: str):
from PIL import Image, ImageDraw
cfg = EXCHANGES[key]
accent = cfg["accent"]
panel = cfg["panel"]
label = cfg["label"]
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
m = max(6, size // 12)
r = max(8, size // 6)
_rounded_rect(draw, (m, m, size - m, size - m), r, BG)
inner = m + max(2, size // 28)
_rounded_rect(draw, (inner, inner, size - inner, size - inner), max(6, r - 4), panel)
if size >= 32:
border = max(1, size // 48)
for i in range(border):
x0 = inner + i
y0 = inner + i
x1 = size - inner - 1 - i
y1 = size - inner - 1 - i
if x1 <= x0 or y1 <= y0:
break
draw.rounded_rectangle(
(x0, y0, x1, y1),
radius=max(2, r - 4 - i),
outline=accent,
)
if key == "binance":
# 币安菱形标识
cx = cy = size // 2
s = max(3, int(size * 0.22))
diamond = [(cx, cy - s), (cx + s, cy), (cx, cy + s), (cx - s, cy)]
draw.polygon(diamond, fill=accent)
s2 = max(1, int(s * 0.42))
if s2 < s:
inner_d = [(cx, cy - s2), (cx + s2, cy), (cx, cy + s2), (cx - s2, cy)]
draw.polygon(inner_d, fill=panel)
elif key == "okx":
# OKX 四格方块风格(右下留空)
gap = max(1, size // 48)
cell = max(2, int(size * 0.16))
cx = cy = size // 2
coords = [
(cx - cell - gap // 2, cy - cell - gap // 2),
(cx + gap // 2, cy - cell - gap // 2),
(cx - cell - gap // 2, cy + gap // 2),
]
for x0, y0 in coords:
draw.rectangle((x0, y0, x0 + cell, y0 + cell), fill=accent)
else:
# Gate: 大字 G
font_size = max(10, int(size * 0.42))
font = _font(font_size)
bbox = draw.textbbox((0, 0), label, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
x = (size - tw) // 2 - bbox[0]
y = (size - th) // 2 - bbox[1] - max(0, size // 64)
draw.text((x, y), label, font=font, fill=accent)
return img
def write_exchange_svg(key: str, dest_dir: str) -> None:
cfg = EXCHANGES[key]
fill = cfg["svg_fill"]
if key == "binance":
mark = (
f'<polygon points="256,150 362,256 256,362 150,256" fill="{fill}"/>'
f'<polygon points="256,210 302,256 256,302 210,256" fill="#1a160a"/>'
)
elif key == "okx":
mark = (
f'<rect x="168" y="168" width="72" height="72" rx="10" fill="{fill}"/>'
f'<rect x="272" y="168" width="72" height="72" rx="10" fill="{fill}"/>'
f'<rect x="168" y="272" width="72" height="72" rx="10" fill="{fill}"/>'
)
else:
mark = (
f'<text x="256" y="310" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" '
f'font-size="220" font-weight="700" fill="{fill}">G</text>'
)
svg = f"""<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="{fill}" stroke-width="12"/>
{mark}
</svg>
"""
with open(os.path.join(dest_dir, "icon.svg"), "w", encoding="utf-8", newline="\n") as f:
f.write(svg)
def _save_set(out_dir: str, render_fn) -> None:
from PIL import Image
os.makedirs(OUT, exist_ok=True)
shutil.copy2(os.path.join(REPO, "brand", "icon.svg"), os.path.join(OUT, "icon.svg"))
os.makedirs(out_dir, exist_ok=True)
sizes = [16, 32, 48, 180, 192, 512]
images: dict[int, Image.Image] = {}
for sz in sizes:
im = render_icon(sz)
im = render_fn(sz)
images[sz] = im
name = "apple-touch-icon.png" if sz == 180 else f"icon-{sz}.png"
im.save(os.path.join(OUT, name), format="PNG", optimize=True)
im.save(os.path.join(out_dir, name), format="PNG", optimize=True)
ico_sizes = [16, 32, 48]
ico_imgs = [images[s] for s in ico_sizes]
ico_imgs[0].save(
os.path.join(OUT, "favicon.ico"),
os.path.join(out_dir, "favicon.ico"),
format="ICO",
sizes=[(s, s) for s in ico_sizes],
append_images=ico_imgs[1:],
)
print(f"DONE {OUT}")
def main() -> None:
os.makedirs(OUT, exist_ok=True)
shutil.copy2(os.path.join(REPO, "brand", "icon.svg"), os.path.join(OUT, "icon.svg"))
_save_set(OUT, render_icon)
print(f"DONE hub {OUT}")
for key in EXCHANGES:
dest = os.path.join(OUT, key)
os.makedirs(dest, exist_ok=True)
write_exchange_svg(key, dest)
_save_set(dest, lambda sz, k=key: render_exchange_icon(sz, k))
print(f"DONE {key} {dest}")
if __name__ == "__main__":
+16 -12
View File
@@ -15,10 +15,10 @@ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = os.path.join(REPO, "brand", "icons")
HUB_DEST = os.path.join(REPO, "manual_trading_hub", "static", "icons")
EXCHANGE_DIRS = (
"crypto_monitor_binance",
"crypto_monitor_okx",
"crypto_monitor_gate",
EXCHANGES = (
("crypto_monitor_binance", "binance", "manifest.binance.webmanifest"),
("crypto_monitor_okx", "okx", "manifest.okx.webmanifest"),
("crypto_monitor_gate", "gate", "manifest.gate.webmanifest"),
)
FILES = (
@@ -32,12 +32,15 @@ FILES = (
)
def sync_dir(dest: str, url_prefix: str, manifest_template: str) -> str:
if not os.path.isdir(SRC):
return f"SKIP {dest}: 请先运行 python scripts/generate_brand_icons.py"
def sync_dir(src_dir: str, dest: str, url_prefix: str, manifest_template: str) -> str:
if not os.path.isdir(src_dir):
return f"SKIP {dest}: 缺少 {src_dir},请先运行 python scripts/generate_brand_icons.py"
os.makedirs(dest, exist_ok=True)
for name in FILES:
shutil.copy2(os.path.join(SRC, name), os.path.join(dest, name))
src = os.path.join(src_dir, name)
if not os.path.isfile(src):
return f"SKIP {dest}: 缺少 {src}"
shutil.copy2(src, os.path.join(dest, name))
manifest_src = os.path.join(REPO, "brand", manifest_template)
if os.path.isfile(manifest_src):
with open(manifest_src, encoding="utf-8") as f:
@@ -53,10 +56,11 @@ def sync_dir(dest: str, url_prefix: str, manifest_template: str) -> str:
def main() -> None:
print(sync_dir(HUB_DEST, "/assets/icons", "manifest.webmanifest"))
for d in EXCHANGE_DIRS:
dest = os.path.join(REPO, d, "static", "icons")
print(sync_dir(dest, "/static/icons", "manifest.exchange.webmanifest"))
print(sync_dir(SRC, HUB_DEST, "/assets/icons", "manifest.webmanifest"))
for folder, key, manifest in EXCHANGES:
dest = os.path.join(REPO, folder, "static", "icons")
src_dir = os.path.join(SRC, key)
print(sync_dir(src_dir, dest, "/static/icons", manifest))
if __name__ == "__main__":