diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index b329447..504f453 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1,11 +1,12 @@ + - + - + - + - + diff --git a/frontend/src/components/Logo.tsx b/frontend/src/components/Logo.tsx index a39e266..cc4baff 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/components/Logo.tsx @@ -3,7 +3,7 @@ // 概念: // - 外层 brackets:终端 / 代码 / 引用边界 — 赛博 + quant 气质 // - 中央 wick+body:一根标准 K 线 — 直接的金融指代 -// - body 在垂直中线偏下:体重感,bullish 暗示(上影长 > 下影) +// - body 偏上 + 下影长:bullish 站稳感 (上影短 / 下影长) // // 用 currentColor,继承父级 color 设定,方便切换品牌色。 interface LogoProps { @@ -48,9 +48,9 @@ export function Logo({ className, size = 32, style }: LogoProps) { strokeLinecap="round" strokeOpacity="0.6" /> - {/* K 线 body — 偏下,意味"上影长 / 反弹站住" */} + {/* K 线 body — 偏上,上影短/下影长, bullish 站稳感 */} diff --git a/packaging/generate_icon.py b/packaging/generate_icon.py index c7fbaad..c9d32d2 100644 --- a/packaging/generate_icon.py +++ b/packaging/generate_icon.py @@ -1,16 +1,17 @@ -"""生成应用图标 .ico — 与前端 favicon.svg / Logo.tsx 保持一致。 +"""生成应用图标 icon.ico — 白底圆角 + 紫色 Logo。 -几何 (32x32 基准, 按比例缩放到各尺寸): - - 紫色方括号 [ ] (brand color #5B21B6) - - 中间一根带 wick 的 K 线 body (实心矩形) +设计 (与 frontend Logo.tsx / favicon.svg 一致): + 背景: 白色圆角方块 + 内容: 紫色 #5B21B6 方括号 + K线 (上影短/下影长, bullish 站稳) -用途: - - exe 图标 (PyInstaller icon=) - - 安装包图标 (Inno Setup SetupIconFile=) - - 快捷方式图标 +蜡烛几何 (32x32 viewBox): + wick: y=7 ~ y=25 + body: y=9 ~ y=19 (偏上) + → 上影 = 2 (短), 下影 = 6 (长) +每尺寸独立绘制: 小尺寸加粗线条, 大尺寸精细。 运行: python packaging/generate_icon.py -产物: packaging/icon.ico (含 16/32/48/64/128/256 多尺寸) +产物: packaging/icon.ico (16/32/48/64/128/256 多尺寸) """ from __future__ import annotations @@ -18,76 +19,71 @@ from pathlib import Path from PIL import Image, ImageDraw -# 品牌色 (与 frontend/public/favicon.svg 一致) -BRAND = (91, 33, 182, 255) # #5B21B6 -BRAND_DIM = (91, 33, 182, 166) # stroke-opacity 0.65 (wick) +OUT = Path(__file__).parent / "icon.ico" -OUTPUT = Path(__file__).parent / "icon.ico" +BRAND = (91, 33, 182, 255) # #5B21B6 +BRAND_WICK = (91, 33, 182, 153) # 0.6 opacity + + +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)) + d = ImageDraw.Draw(img) + + # 白色圆角背景 + d.rounded_rectangle([0, 0, s - 1, s - 1], radius=int(s * 0.18), fill=(255, 255, 255, 255)) + + def p(v): + return v * s / 32 + + swb = max(1, int(round(sw_b * factor))) + sww = max(1, int(round(sw_w * factor))) + + # 方括号 [ ] + 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") + + # wick 影线 (上短下长) + d.line([(p(16), p(7)), (p(16), p(25))], fill=BRAND_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) + + # body 实体 (偏上 → 上影短下影长) + d.rounded_rectangle( + [p(16 - body_w / 2), p(9), p(16 + body_w / 2), p(19)], + radius=p(0.5), fill=BRAND, + ) + + return img.resize((size, size), Image.LANCZOS) def draw_logo(size: int) -> Image.Image: - """按 favicon.svg 的几何绘制指定尺寸的图标。 - - 坐标基于 32x32 viewBox, 按比例缩放到 size。 - """ - # 用 4x 超采样再缩放, 抗锯齿 - scale = max(4, 256 // size) - s = size * scale - img = Image.new("RGBA", (s, s), (0, 0, 0, 0)) - d = ImageDraw.Draw(img) - - # 把 32-viewBox 坐标缩放到 s - def x(v: float) -> float: - return v * s / 32 - - def y(v: float) -> float: - return v * s / 32 - - sw = 2.5 # 括号线宽 - bw = 2.0 # wick 线宽 - w = max(1, int(round(sw * scale))) - ww = max(1, int(round(bw * scale))) - - # 左方括号 [ : M10,4 L4,4 L4,28 L10,28 - d.line([(x(10), y(4)), (x(4), y(4))], fill=BRAND, width=w) - d.line([(x(4), y(4)), (x(4), y(28))], fill=BRAND, width=w) - d.line([(x(4), y(28)), (x(10), y(28))], fill=BRAND, width=w) - # 括号转角处用圆点补强 (line 转角有缝隙) - r = w // 2 + 1 - for cx, cy in [(x(4), y(4)), (x(4), y(28))]: - d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=BRAND) - - # 右方括号 ] : M22,4 L28,4 L28,28 L22,28 - d.line([(x(22), y(4)), (x(28), y(4))], fill=BRAND, width=w) - d.line([(x(28), y(4)), (x(28), y(28))], fill=BRAND, width=w) - d.line([(x(28), y(28)), (x(22), y(28))], fill=BRAND, width=w) - for cx, cy in [(x(28), y(4)), (x(28), y(28))]: - d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=BRAND) - - # K 线 wick: x=16, y=7..25 - d.line([(x(16), y(7)), (x(16), y(25))], fill=BRAND_DIM, width=ww) - - # K 线 body: rect (12.5, 11.5) 7x11, rx=0.8 - bx0, by0 = x(12.5), y(11.5) - bx1, by1 = x(12.5) + x(7), y(11.5) + y(11) - radius = x(0.8) - d.rounded_rectangle([bx0, by0, bx1, by1], radius=radius, fill=BRAND) - - # 缩放到目标尺寸 (高质量抗锯齿) - return img.resize((size, size), Image.LANCZOS) + """按尺寸选参数 (小尺寸加粗)。""" + if size <= 16: + return _draw(size, sw_b=4.0, sw_w=3.5, 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) + else: + return _draw(size, sw_b=2.0, sw_w=1.5, body_w=6) def main() -> None: sizes = [16, 32, 48, 64, 128, 256] - images = [draw_logo(s) for s in sizes] - # 第一个作为主图, 其余作为变体 - images[0].save( - OUTPUT, - format="ICO", - sizes=[(s, s) for s in sizes], - append_images=images[1:], + 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"生成图标: {OUTPUT} (尺寸 {sizes})") + print(f"生成: {OUT} (尺寸 {sizes})") if __name__ == "__main__": diff --git a/packaging/icon.ico b/packaging/icon.ico index 5c167db..ff83526 100644 Binary files a/packaging/icon.ico and b/packaging/icon.ico differ diff --git a/packaging/tickflow.iss b/packaging/tickflow.iss index 6fbd75b..57cdabd 100644 --- a/packaging/tickflow.iss +++ b/packaging/tickflow.iss @@ -33,16 +33,19 @@ AppName={#MyAppName} AppVerName={#MyAppName} {#MyAppVersion} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} -DefaultDirName={localappdata}\Programs\TickFlowStockPanel +; 默认装到 D 盘 (非系统盘), 用户可在向导中改任意位置 +; 若 D 盘不存在, [Code] 段 InitializeWizard 会自动回退到用户目录 +DefaultDirName=D:\TickFlowStockPanel DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes OutputDir=Output OutputBaseFilename=TickFlowStockPanel-Setup-{#MyAppVersion} -; 关键: 不需要管理员权限 (用户目录安装) -; PrivilegesRequired=lowest 固定走用户安装, 永不弹 UAC -; 不用 PrivilegesRequiredOverridesAllowed (它在静默模式下路径解析不确定) +; 关键: 不需要管理员权限, 永不弹 UAC +; 装到 D 盘普通目录 (非 Program Files) 不需要管理员权限 PrivilegesRequired=lowest +; 允许用户在向导中自由选择安装目录 +DisableDirPage=no ; 压缩 Compression=lzma2/ultra64 @@ -95,6 +98,25 @@ Filename: "{cmd}"; Parameters: "/C taskkill /F /IM {#MyAppExeName}"; Flags: runh Type: filesandordirs; Name: "{app}" [Code] +// ── 启动时: 若 D 盘不存在, 回退默认路径到用户目录 ─────────────── +// 避免默认 D:\... 但系统没 D 盘时向导显示无效路径 +function InitializeSetup(): Boolean; +begin + Result := True; +end; + +procedure InitializeWizard(); +var + DefaultDir: String; +begin + // D 盘存在 → 用 D 盘; 否则回退用户目录 (无需管理员权限) + if not DirExists('D:\') then + begin + DefaultDir := ExpandConstant('{localappdata}\Programs\TickFlowStockPanel'); + WizardForm.DirEdit.Text := DefaultDir; + end; +end; + // ── 卸载时询问是否删除用户数据 ───────────────────────────────── // 用户数据在 %LOCALAPPDATA%\TickFlowStockPanel\ (策略/选股/回测/监控) // 默认保留 (重装不丢), 但给用户彻底清除的选项