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\ (策略/选股/回测/监控)
// 默认保留 (重装不丢), 但给用户彻底清除的选项