build(packaging): macOS 双架构 dmg 打包 + 新增 icns 图标 + README 下载说明

- README: 下载说明区分 macOS Apple Silicon(arm64) / Intel(x64) 两个 dmg
- generate_icon.py: 支持生成 .icns,重构图标生成逻辑
- 新增 packaging/icon.icns (macOS 图标资源)
- 更新 icon.ico (Windows 图标)
- tickflow.spec: 适配 macOS dmg 双架构打包配置
This commit is contained in:
shy3130
2026-06-24 18:31:47 +08:00
parent b28355aacf
commit 2ea2844917
5 changed files with 104 additions and 31 deletions
+2 -1
View File
@@ -179,7 +179,8 @@ docker compose up --build
| 平台 | 文件 |
| :--- | :--- |
| **Windows** | `TickFlowStockPanel-Setup-x64.exe` → 双击安装向导,自动创建桌面/开始菜单快捷方式 |
| **macOS** | `TickFlowStockPanel-macos.zip` → 解压后运行 app(首次打开右键→打开,绕过 Gatekeeper) |
| **macOS (Apple Silicon)** | `TickFlowStockPanel-macos-arm64.dmg` → 双击打开,拖入「应用程序」(M1/M2/M3/M4 芯片选这个) |
| **macOS (Intel)** | `TickFlowStockPanel-macos-x64.dmg` → 双击打开,拖入「应用程序」(老款 Intel 芯片选这个) |
| **Linux** | `TickFlowStockPanel-linux-x64.tar.gz` → 解压后运行可执行文件 |
下载地址:**[GitHub Releases](https://github.com/shy3130/tickflow-stock-panel/releases/latest)**
+65 -29
View File
@@ -1,7 +1,7 @@
"""生成应用图标 icon.ico — 白底圆角 + 紫色 Logo。
"""生成应用图标 — 白色 squircle 背景 + 紫色 Logo。
设计 (与 frontend Logo.tsx / favicon.svg 一致):
背景: 白色圆角方块
背景: 白色 squircle (macOS Big Sur+ 要求不透明 squircle 背景, 用白色填充)
内容: 紫色 #5B21B6 方括号 + K线 (上影短/下影长, bullish 站稳)
蜡烛几何 (32x32 viewBox):
@@ -9,9 +9,11 @@
body: y=9 ~ y=19 (偏上)
→ 上影 = 2 (短), 下影 = 6 (长)
每尺寸独立绘制: 小尺寸加粗线条, 大尺寸精细
每尺寸独立绘制: 线条占比全尺寸统一 (~9.4%), 小尺寸微调补偿, 不随尺寸递减
运行: python packaging/generate_icon.py
产物: packaging/icon.ico (16/32/48/64/128/256 多尺寸)
产物:
packaging/icon.ico — Windows (16/32/48/64/128/256)
packaging/icon.icns — macOS (16/32/64/128/256/512, 含 @2x)
"""
from __future__ import annotations
@@ -19,21 +21,27 @@ from pathlib import Path
from PIL import Image, ImageDraw
OUT = Path(__file__).parent / "icon.ico"
OUT_ICO = Path(__file__).parent / "icon.ico"
OUT_ICNS = Path(__file__).parent / "icon.icns"
BRAND = (91, 33, 182, 255) # #5B21B6
BRAND_WICK = (91, 33, 182, 153) # 0.6 opacity
# 背景: 白色 squircle (不透明, macOS 规范)
BG = (255, 255, 255, 255)
# logo 线条色: 紫色 #5B21B6 (与 Logo.tsx / favicon.svg 一致)
LOGO = (91, 33, 182, 255) # #5B21B6
# wick 影线: 同色不透明 (半透明在小尺寸会糊掉)
LOGO_WICK = (91, 33, 182, 255)
def _draw(size: int, sw_b: float, sw_w: float, body_w: float) -> Image.Image:
"""绘制单尺寸图标 (像素直接映射 32-viewBox)。"""
factor = max(8, 2048 // size)
s = size * factor
img = Image.new("RGBA", (s, s), (255, 255, 255, 0))
img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
# 白色圆角背景
d.rounded_rectangle([0, 0, s - 1, s - 1], radius=int(s * 0.18), fill=(255, 255, 255, 255))
# 品牌色 squircle 背景 (圆角方块, macOS Big Sur+ 规范形状, 不透明)
# 纯色填充, 不用渐变 — 渐变在 16px 小尺寸下看不出, 反而易引入 alpha 合成 bug
d.rounded_rectangle([0, 0, s - 1, s - 1], radius=int(s * 0.22), fill=BG)
def p(v):
return v * s / 32
@@ -45,45 +53,73 @@ def _draw(size: int, sw_b: float, sw_w: float, body_w: float) -> Image.Image:
for pts in [[(10, 4), (4, 4), (4, 28), (10, 28)], [(22, 4), (28, 4), (28, 28), (22, 28)]]:
scaled = [(p(x), p(y)) for x, y in pts]
for i in range(len(scaled) - 1):
d.line([scaled[i], scaled[i + 1]], fill=BRAND, width=swb, joint="curve")
d.line([scaled[i], scaled[i + 1]], fill=LOGO, width=swb, joint="curve")
# wick 影线 (上短下长)
d.line([(p(16), p(7)), (p(16), p(25))], fill=BRAND_WICK, width=sww)
d.line([(p(16), p(7)), (p(16), p(25))], fill=LOGO_WICK, width=sww)
wcap = sww // 2 + 1
for cy in [7, 25]:
d.ellipse([p(16) - wcap, p(cy) - wcap, p(16) + wcap, p(cy) + wcap], fill=BRAND_WICK)
d.ellipse([p(16) - wcap, p(cy) - wcap, p(16) + wcap, p(cy) + wcap], fill=LOGO_WICK)
# body 实体 (偏上 → 上影短下影长)
d.rounded_rectangle(
[p(16 - body_w / 2), p(9), p(16 + body_w / 2), p(19)],
radius=p(0.5), fill=BRAND,
radius=p(0.5), fill=LOGO,
)
return img.resize((size, size), Image.LANCZOS)
def draw_logo(size: int) -> Image.Image:
"""按尺寸选参数 (小尺寸加粗)。"""
"""按尺寸选参数, 保证各尺寸视觉粗细一致。
核心原则: 所有尺寸线条占图标的视觉比例统一, 不能出现"大尺寸反而显细"
基准定为 sw=4.0/32 (12.5%) — 经多轮对比选定, Dock 放大时线条仍清晰有力。
小尺寸在基准上略加粗, 抵消像素化糊掉; 大尺寸统一用基准, 不递减。
"""
if size <= 16:
return _draw(size, sw_b=4.0, sw_w=3.5, body_w=9)
return _draw(size, sw_b=4.5, sw_w=3.8, body_w=9)
elif size <= 32:
return _draw(size, sw_b=3.0, sw_w=2.5, body_w=8)
elif size <= 48:
return _draw(size, sw_b=2.5, sw_w=2.0, body_w=7)
return _draw(size, sw_b=4.2, sw_w=3.6, body_w=8)
else:
return _draw(size, sw_b=2.0, sw_w=1.5, body_w=6)
# ≥48px 统一用基准 4.0, 不随尺寸递减
return _draw(size, sw_b=4.0, sw_w=3.4, body_w=8)
def _save_ico(images_by_size: dict[int, Image.Image]) -> None:
"""Windows .ico (16/32/48/64/128/256)。主图 256 优先, 资源管理器大图清晰。"""
sizes = [16, 32, 48, 64, 128, 256]
images = [images_by_size[s] for s in sizes]
images[-1].save(
OUT_ICO, format="ICO",
sizes=[(s, s) for s in sizes],
append_images=images[:-1],
)
print(f"生成: {OUT_ICO} (尺寸 {sizes})")
def _save_icns(images_by_size: dict[int, Image.Image]) -> None:
"""macOS .icns (16/32/64/128/256/512)。
Apple 要求 .icns 至少含一张大图 (512 或 1024) 才合法, 否则 Finder/Dock
不显示。这里用 512 作主图 (Pillow 12 写 1024 需额外编码, 512 已覆盖
Retina @2x)。@2x 高清由系统从大图自动缩放, 无需单独提供。
"""
sizes = [16, 32, 64, 128, 256, 512]
images = [images_by_size[s] for s in sizes]
images[-1].save(
OUT_ICNS, format="ICNS",
append_images=images[:-1],
)
print(f"生成: {OUT_ICNS} (尺寸 {sizes})")
def main() -> None:
sizes = [16, 32, 48, 64, 128, 256]
images = [draw_logo(sz) for sz in sizes]
# 主图 256 (大尺寸优先, 资源管理器大图清晰)
images[-1].save(
OUT, format="ICO",
sizes=[(sz, sz) for sz in sizes],
append_images=images[:-1],
)
print(f"生成: {OUT} (尺寸 {sizes})")
# 一次绘制所有用到的尺寸 (ico 和 icns 取并集), 避免重复绘制
all_sizes = {16, 32, 48, 64, 128, 256, 512}
images_by_size = {sz: draw_logo(sz) for sz in all_sizes}
_save_ico(images_by_size)
_save_icns(images_by_size)
if __name__ == "__main__":
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+37 -1
View File
@@ -22,6 +22,8 @@ from PyInstaller.utils.hooks import (
copy_metadata,
)
_IS_MACOS = sys.platform == "darwin"
block_cipher = None
# ── 资源路径基准: 项目根 (spec 文件在 packaging/ 下) ──────────────────
@@ -29,7 +31,10 @@ ROOT = Path(SPECPATH).parent
FRONTEND_DIST = str(ROOT / "frontend" / "dist")
TIERS_YAML = str(ROOT / "tiers.yaml")
BUILTIN_STRATEGIES = str(ROOT / "backend" / "app" / "strategy" / "builtin")
APP_ICON = str(ROOT / "packaging" / "icon.ico")
# 图标按平台选: Windows 用 .ico, macOS 用 .icns (PyInstaller 对 .ico 在
# mac 上静默忽略, 不换格式 Dock/Finder 会显示通用图标)。两者都由
# packaging/generate_icon.py 一并生成。
APP_ICON = str(ROOT / "packaging" / ("icon.icns" if _IS_MACOS else "icon.ico"))
# ── 收集带原生库的依赖 (.libs/ 目录必须完整, 否则启动崩) ─────────────
# polars / pyarrow / duckdb / fastexcel 都自带共享库子目录
@@ -165,3 +170,34 @@ coll = COLLECT(
upx_exclude=[],
name="TickFlowStockPanel",
)
# ── macOS: 封装成 .app 包 ────────────────────────────────────────────
# Windows/Linux: 上面 COLLECT 产出的 onedir 目录即是最终产物。
# macOS: 额外加 BUNDLE, 把目录包装成标准 .app (Contents/MacOS/...),
# 这样 Dock/Finder 能识别, 用户可双击启动, 并显示自定义 .icns 图标。
# BUNDLE 必须引用上面的 COLLECT (coll), 它会把 coll 的产物搬进 .app。
#
# PyInstaller 6 BUNDLE 关键参数 (见 building/osx.py):
# - version: → CFBundleShortVersionString (默认 0.0.0, 必须显式传)
# - info_plist (单数!): 用户自定义键, update 合并进默认 plist, 可覆盖任意字段
if _IS_MACOS:
import json
# 版本号从 frontend/package.json 读, 与 Release tag 对齐
_pkg_json = json.loads((ROOT / "frontend" / "package.json").read_text(encoding="utf-8"))
APP_VERSION = _pkg_json["version"]
app = BUNDLE(
coll,
name="TickFlowStockPanel.app",
icon=APP_ICON,
bundle_identifier="com.tickflow.stockpanel",
version=APP_VERSION, # → CFBundleShortVersionString / CFBundleVersion
info_plist={
"CFBundleName": "TickFlow Stock Panel",
"CFBundleDisplayName": "TickFlow ",
"CFBundleVersion": APP_VERSION,
"NSHighResolutionCapable": True,
"LSMinimumSystemVersion": "10.13",
},
)