mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
brand: 品牌统一为 TSP — 应用名/图标(浅色圆角卡, Windows/macOS 双平台)对齐 brand 资产, README 新增官网入口与标题行内嵌 logo
This commit is contained in:
@@ -1,156 +0,0 @@
|
||||
"""生成应用图标 — 白色 squircle 背景 + 品牌 gradient Logo。
|
||||
|
||||
设计 (与 frontend favicon.svg / brand/logo.svg 一致):
|
||||
背景: 白色 squircle (macOS Big Sur+ 要求不透明 squircle 背景, 用白色填充)
|
||||
内容: 品牌渐变 (#A78BFA→#7C3AED, 亮端朝上) 方括号 + K线 (上影短/下影长, bullish 站稳)
|
||||
|
||||
蜡烛几何 (32x32 viewBox):
|
||||
wick: y=7 ~ y=25
|
||||
body: y=9 ~ y=19 (偏上)
|
||||
→ 上影 = 2 (短), 下影 = 6 (长)
|
||||
|
||||
每尺寸独立绘制: 线条占比全尺寸统一 (~9.4%), 小尺寸微调补偿, 不随尺寸递减。
|
||||
运行: python packaging/generate_icon.py
|
||||
产物:
|
||||
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
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageChops, ImageDraw
|
||||
|
||||
OUT_ICO = Path(__file__).parent / "icon.ico"
|
||||
OUT_ICNS = Path(__file__).parent / "icon.icns"
|
||||
|
||||
# 背景: 白色 squircle (不透明, macOS 规范)
|
||||
BG = (255, 255, 255, 255)
|
||||
# logo 线条色: 品牌渐变 #A78BFA(亮端,左上) → #7C3AED(品牌紫,右下), 与 favicon.svg/brand/logo.svg 一致
|
||||
GRAD_A = (167, 139, 250) # #A78BFA
|
||||
GRAD_B = (124, 58, 237) # #7C3AED
|
||||
# wick 影线: 70% 不透明 (与 favicon.svg 一致; 16px 最小档经超采样仍可读)
|
||||
WICK_ALPHA = 0.7
|
||||
|
||||
|
||||
def _grad_image(size_px: int) -> Image.Image:
|
||||
"""32-viewBox 对角渐变 (4,3)→(28,29), 64px 生成后放大 (线性渐变缩放无损)。"""
|
||||
small = 64
|
||||
ax, ay, bx, by = 4 / 32, 3 / 32, 28 / 32, 29 / 32
|
||||
dx, dy = bx - ax, by - ay
|
||||
len2 = dx * dx + dy * dy
|
||||
img = Image.new("RGB", (small, small))
|
||||
px = img.load()
|
||||
for y in range(small):
|
||||
vy = (y + 0.5) / small
|
||||
for x in range(small):
|
||||
vx = (x + 0.5) / small
|
||||
t = max(0.0, min(1.0, ((vx - ax) * dx + (vy - ay) * dy) / len2))
|
||||
px[x, y] = tuple(round(a + (b - a) * t) for a, b in zip(GRAD_A, GRAD_B))
|
||||
return img.resize((size_px, size_px), Image.BILINEAR)
|
||||
|
||||
|
||||
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), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
# 品牌色 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
|
||||
|
||||
swb = max(1, int(round(sw_b * factor)))
|
||||
sww = max(1, int(round(sw_w * factor)))
|
||||
|
||||
# 标记掩膜: 渐变经 alpha 通道贴入, 线条几何与旧版完全一致
|
||||
mark = Image.new("L", (s, s), 0)
|
||||
dm = ImageDraw.Draw(mark)
|
||||
|
||||
# 方括号 [ ]
|
||||
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):
|
||||
dm.line([scaled[i], scaled[i + 1]], fill=255, width=swb, joint="curve")
|
||||
|
||||
# body 实体 (偏上 → 上影短下影长)
|
||||
dm.rounded_rectangle(
|
||||
[p(16 - body_w / 2), p(9), p(16 + body_w / 2), p(19)],
|
||||
radius=p(0.5), fill=255,
|
||||
)
|
||||
|
||||
# wick 影线 (上短下长, 70% 不透明)
|
||||
wick = Image.new("L", (s, s), 0)
|
||||
dw = ImageDraw.Draw(wick)
|
||||
dw.line([(p(16), p(7)), (p(16), p(25))], fill=255, width=sww)
|
||||
wcap = sww // 2 + 1
|
||||
for cy in [7, 25]:
|
||||
dw.ellipse([p(16) - wcap, p(cy) - wcap, p(16) + wcap, p(cy) + wcap], fill=255)
|
||||
|
||||
# 渐变层: alpha = mark + WICK_ALPHA * wick
|
||||
alpha = ImageChops.add(mark, wick.point(lambda v: round(v * WICK_ALPHA)))
|
||||
layer = _grad_image(s).convert("RGBA")
|
||||
layer.putalpha(alpha)
|
||||
img.alpha_composite(layer)
|
||||
|
||||
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.5, sw_w=3.8, body_w=9)
|
||||
elif size <= 32:
|
||||
return _draw(size, sw_b=4.2, sw_w=3.6, body_w=8)
|
||||
else:
|
||||
# ≥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:
|
||||
# 一次绘制所有用到的尺寸 (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__":
|
||||
main()
|
||||
+15
-10
@@ -1,13 +1,13 @@
|
||||
; ===========================================================================
|
||||
; Tick Stock Panel — Inno Setup 安装包脚本
|
||||
; ===========================================================================
|
||||
; 用途: 把 PyInstaller 产出的 dist/TickFlowStockPanel/ 文件夹封装成
|
||||
; 用途: 把 PyInstaller 产出的 dist/TSP/ 文件夹封装成
|
||||
; 单个 Setup.exe 安装程序 (双击→安装向导→快捷方式→可卸载)。
|
||||
;
|
||||
; 构建 (本地):
|
||||
; 1. 先跑 PyInstaller: cd backend && uv run pyinstaller ../packaging/tickflow.spec
|
||||
; 2. 再跑 Inno Setup: ISCC.exe packaging\tickflow.iss
|
||||
; 3. 产物: packaging\Output\TickFlowStockPanel-Setup-x.x.x.exe
|
||||
; 3. 产物: packaging\Output\TSP-Setup-x.x.x.exe
|
||||
;
|
||||
; 设计决策:
|
||||
; - 装到用户目录 {localappdata}\Programs\ (不弹 UAC, 不需管理员)
|
||||
@@ -18,10 +18,10 @@
|
||||
; - 卸载入口 (控制面板可见)
|
||||
; ===========================================================================
|
||||
|
||||
#define MyAppName "TickFlow 股票面板"
|
||||
#define MyAppName "TSP 股票面板"
|
||||
#define MyAppNameEN "Tick Stock Panel"
|
||||
#define MyAppExeName "TickFlowStockPanel.exe"
|
||||
#define MyAppPublisher "TickFlow"
|
||||
#define MyAppExeName "TSP.exe"
|
||||
#define MyAppPublisher "TSP"
|
||||
|
||||
; 版本号: 从 frontend/package.json 读取, 与 Release tag 保持一致
|
||||
; 手动指定更可靠 (CI 传入 /DMyAppVersion)
|
||||
@@ -31,17 +31,20 @@
|
||||
|
||||
[Setup]
|
||||
; 基本信息
|
||||
; AppId 固定 GUID: Inno 默认从 AppName 派生, 一旦改名旧安装会变成重复程序;
|
||||
; 固定后应用名再调整也保持同一卸载/升级身份
|
||||
AppId={{B7F4C9E2-3A51-4D8E-9C60-5E8A2F1D7B43}
|
||||
AppName={#MyAppName}
|
||||
AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
; 默认装到 D 盘 (非系统盘), 用户可在向导中改任意位置
|
||||
; 若 D 盘不存在, [Code] 段 InitializeWizard 会自动回退到用户目录
|
||||
DefaultDirName=D:\TickFlowStockPanel
|
||||
DefaultDirName=D:\TSP
|
||||
DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputDir=Output
|
||||
OutputBaseFilename=TickFlowStockPanel-Setup-{#MyAppVersion}
|
||||
OutputBaseFilename=TSP-Setup-{#MyAppVersion}
|
||||
|
||||
; 关键: 不需要管理员权限, 永不弹 UAC
|
||||
; 装到 D 盘普通目录 (非 Program Files) 不需要管理员权限
|
||||
@@ -58,7 +61,9 @@ LZMAUseSeparateProcess=yes
|
||||
WizardStyle=modern
|
||||
DisableWelcomePage=no
|
||||
DisableReadyPage=no
|
||||
SetupIconFile=icon.ico
|
||||
; 安装向导/卸载图标: 直接引用品牌资产唯一来源 (brand/README 接入指引),
|
||||
; 不再使用 packaging/icon.ico 的旧手绘版本, 避免两处漂移
|
||||
SetupIconFile=..\brand\icon.ico
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
|
||||
; 卸载相关
|
||||
@@ -77,7 +82,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
|
||||
[Files]
|
||||
; 把 PyInstaller 产出的整个文件夹搬进安装目录
|
||||
; Source 路径相对于 .iss 文件所在目录
|
||||
Source: "..\backend\dist\TickFlowStockPanel\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\backend\dist\TSP\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
; 开始菜单
|
||||
@@ -139,7 +144,7 @@ begin
|
||||
// D 盘存在 → 用 D 盘; 否则回退用户目录 (无需管理员权限)
|
||||
if not DirExists('D:\') then
|
||||
begin
|
||||
DefaultDir := ExpandConstant('{localappdata}\Programs\TickFlowStockPanel');
|
||||
DefaultDir := ExpandConstant('{localappdata}\Programs\TSP');
|
||||
WizardForm.DirEdit.Text := DefaultDir;
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
构建 (在项目根目录):
|
||||
cd frontend && pnpm build # 先构建前端到 frontend/dist
|
||||
pyinstaller packaging/tickflow.spec # 产物在 dist/TickFlowStockPanel/
|
||||
pyinstaller packaging/tickflow.spec # 产物在 dist/TSP/
|
||||
"""
|
||||
import sys
|
||||
from importlib.util import find_spec
|
||||
@@ -32,10 +32,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")
|
||||
# 图标按平台选: 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"))
|
||||
# 图标按平台选: Windows 用 brand/icon.ico, macOS 用 brand/icon.icns —
|
||||
# 两者均由 brand/png/logo-tile-light-512.png 派生 (浅色圆角卡版, 品牌资产唯一来源,
|
||||
# 见 brand/README 接入指引), 无需 CI 现场生成。
|
||||
APP_ICON = str((ROOT / "brand" / "icon.icns") if _IS_MACOS else (ROOT / "brand" / "icon.ico"))
|
||||
|
||||
# ── 收集带原生库的依赖 (.libs/ 目录必须完整, 否则启动崩) ─────────────
|
||||
# polars / pyarrow / duckdb / fastexcel 都自带共享库子目录
|
||||
@@ -157,7 +157,7 @@ exe = EXE(
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="TickFlowStockPanel",
|
||||
name="TSP",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
@@ -179,7 +179,7 @@ coll = COLLECT(
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
name="TickFlowStockPanel",
|
||||
name="TSP",
|
||||
)
|
||||
|
||||
# ── macOS: 封装成 .app 包 ────────────────────────────────────────────
|
||||
@@ -200,13 +200,13 @@ if _IS_MACOS:
|
||||
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name="TickFlowStockPanel.app",
|
||||
name="TSP.app",
|
||||
icon=APP_ICON,
|
||||
bundle_identifier="com.tickflow.stockpanel",
|
||||
version=APP_VERSION, # → CFBundleShortVersionString / CFBundleVersion
|
||||
info_plist={
|
||||
"CFBundleName": "Tick Stock Panel",
|
||||
"CFBundleDisplayName": "TickFlow 股票面板",
|
||||
"CFBundleDisplayName": "TSP 股票面板",
|
||||
"CFBundleVersion": APP_VERSION,
|
||||
"NSHighResolutionCapable": True,
|
||||
"LSMinimumSystemVersion": "10.13",
|
||||
|
||||
Reference in New Issue
Block a user