diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b726771..fb47ad2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -41,7 +41,7 @@ jobs:
- id: filter
run: |
REQUESTED="${{ github.event.inputs.platforms }}"
- ALL='[{"os":"windows-latest","artifact":"TickFlowStockPanel-win-x64.zip","platform":"windows"},{"os":"macos-latest","artifact":"TickFlowStockPanel-macos.zip","platform":"macos"},{"os":"ubuntu-latest","artifact":"TickFlowStockPanel-linux-x64.tar.gz","platform":"linux"}]'
+ ALL='[{"os":"windows-latest","artifact":"TickFlowStockPanel-Setup-x64.exe","platform":"windows"},{"os":"macos-latest","artifact":"TickFlowStockPanel-macos.zip","platform":"macos"},{"os":"ubuntu-latest","artifact":"TickFlowStockPanel-linux-x64.tar.gz","platform":"linux"}]'
# 按 input 过滤平台
python3 -c "
import json, sys
@@ -103,12 +103,21 @@ jobs:
- name: 打包 (PyInstaller)
run: uv run pyinstaller packaging/tickflow.spec --noconfirm
- - name: 压缩产物 (Windows)
+ - name: 安装 Inno Setup (仅 Windows)
if: matrix.platform == 'windows'
+ # Inno Setup 6: 把 PyInstaller 产出封装成单文件安装包
+ run: choco install innosetup -y --no-progress
+
+ - name: 生成 Windows 安装包 (Inno Setup)
+ if: matrix.platform == 'windows'
+ # 从 frontend/package.json 读版本号传给 ISCC, 保持与 Release tag 一致
run: |
- cd dist
- 7z a -tzip ../${{ matrix.artifact }} TickFlowStockPanel
- shell: bash
+ $ver = (Get-Content frontend/package.json | ConvertFrom-Json).version
+ echo "版本号: $ver"
+ ISCC.exe "/DMyAppVersion=$ver" packaging/tickflow.iss
+ # 把生成的 Setup.exe 移到工作区根 (Release 上传用)
+ Move-Item packaging/Output/TickFlowStockPanel-Setup-$ver.exe ${{ matrix.artifact }} -Force
+ shell: pwsh
- name: 压缩产物 (macOS)
if: matrix.platform == 'macos'
@@ -139,17 +148,21 @@ jobs:
body: |
## 桌面客户端 ${{ github.event.inputs.version }}
- ### 下载
- 选择对应平台的安装包, 解压后双击运行:
+ ### ⬇️ 下载
- | 平台 | 文件 |
- | :--- | :--- |
- | **Windows** | `TickFlowStockPanel-win-x64.zip` → 解压运行 `TickFlowStockPanel.exe` |
- | **macOS** | `TickFlowStockPanel-macos.zip` → 解压后右键打开 (绕过 Gatekeeper) |
- | **Linux** | `TickFlowStockPanel-linux-x64.tar.gz` → 解压运行 |
+ | 平台 | 文件 | 下载 |
+ | :--- | :--- | :--- |
+ | **Windows x64** | `TickFlowStockPanel-Setup-x64.exe` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TickFlowStockPanel-Setup-x64.exe) |
+ | **macOS** | `TickFlowStockPanel-macos.zip` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TickFlowStockPanel-macos.zip) |
+ | **Linux** | `TickFlowStockPanel-linux-x64.tar.gz` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TickFlowStockPanel-linux-x64.tar.gz) |
- ### 说明
- - 数据存储在用户目录 (Windows `%LOCALAPPDATA%`、mac `~/Library`、Linux `~/.local/share`), 卸载重装不丢数据
- - 含纯 Polars 回测引擎; 不含 vectorbt 回测 (为控制体积)
+ > 💡 点击「立即下载」直接获取, 无需滚动到底部附件区。
+
+ ### 📋 使用说明
+ - **Windows**: 下载后双击运行, 按安装向导操作(无需管理员权限), 自动创建桌面/开始菜单快捷方式
+ - **macOS**: 解压后右键打开(首次绕过 Gatekeeper)
+ - **Linux**: 解压后运行可执行文件
+ - 数据存储在用户目录 (`%LOCALAPPDATA%`), 卸载重装不丢数据
+ - 含纯 Polars 回测引擎; 不含 vectorbt 回测(为控制体积)
- 系统通知: 设置 → 实时监控 → 系统通知
- 检查更新: 设置 → 系统设置 → 关于
diff --git a/.gitignore b/.gitignore
index ba5deea..dbd8d26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,3 +96,8 @@ backend/_desktop_run.log
backend/TickFlowStockPanel-win-x64.zip
backend/TickFlowStockPanel-macos.zip
backend/TickFlowStockPanel-linux-x64.tar.gz
+packaging/Output/
+packaging/icon_preview.png
+packaging/designs/
+packaging/install.log
+packaging/installed_run.log
diff --git a/README.md b/README.md
index 75594a7..98a2260 100644
--- a/README.md
+++ b/README.md
@@ -174,11 +174,11 @@ docker compose up --build
### 方式 B:桌面客户端(免环境,开箱即用)
-下载对应平台的安装包,解压后双击运行即可,**无需 Python / Node 环境**。
+下载对应平台的安装包,运行后按向导安装即可,**无需 Python / Node 环境**。
| 平台 | 文件 |
| :--- | :--- |
-| **Windows** | `TickFlowStockPanel-win-x64.zip` → 解压后运行 `TickFlowStockPanel.exe` |
+| **Windows** | `TickFlowStockPanel-Setup-x64.exe` → 双击安装向导,自动创建桌面/开始菜单快捷方式 |
| **macOS** | `TickFlowStockPanel-macos.zip` → 解压后运行 app(首次打开右键→打开,绕过 Gatekeeper) |
| **Linux** | `TickFlowStockPanel-linux-x64.tar.gz` → 解压后运行可执行文件 |
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
index 219bf01..5084745 100644
--- a/backend/app/__init__.py
+++ b/backend/app/__init__.py
@@ -1,3 +1,3 @@
"""TickFlow Stock Panel backend."""
-__version__ = "0.1.31"
+__version__ = "0.1.35"
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 9a7d287..83a6040 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "tickflow-stock-panel-backend"
-version = "0.1.34"
+version = "0.1.35"
description = "A 股选股 + 监控 + 回测面板 — TickFlow 适配"
readme = "../README.md"
requires-python = ">=3.11"
diff --git a/backend/uv.lock b/backend/uv.lock
index 024839c..c6b9e52 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -2470,7 +2470,7 @@ all = [
[[package]]
name = "tickflow-stock-panel-backend"
-version = "0.1.33"
+version = "0.1.35"
source = { editable = "." }
dependencies = [
{ name = "apscheduler" },
diff --git a/frontend/package.json b/frontend/package.json
index 993c559..a499d57 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "tickflow-stock-panel-frontend",
"private": true,
- "version": "0.1.34",
+ "version": "0.1.35",
"type": "module",
"scripts": {
"dev": "vite",
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/ChineseSimplified.isl b/packaging/ChineseSimplified.isl
new file mode 100644
index 0000000..d84f6cf
--- /dev/null
+++ b/packaging/ChineseSimplified.isl
@@ -0,0 +1,419 @@
+; *** Inno Setup version 6.5.0+ Chinese Simplified messages ***
+;
+; To download user-contributed translations of this file, go to:
+; https://jrsoftware.org/files/istrans/
+;
+; Note: When translating this text, do not add periods (.) to the end of
+; messages that didn't have them already, because on those messages Inno
+; Setup adds the periods automatically (appending a period would result in
+; two periods being displayed).
+;
+; Maintainer: Zhenghan Yang (Kira)
+; Email: 847320916@QQ.com
+; Github: https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
+; Encoding: UTF-8
+; Translation based on network resource
+;
+
+[LangOptions]
+; The following three entries are very important. Be sure to read and
+; understand the '[LangOptions] section' topic in the help file.
+LanguageName=简体中文
+; If Language Name display incorrect, uncomment next line
+; LanguageName=<7B80><4F53><4E2D><6587>
+; About LanguageID, to reference link:
+; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
+LanguageID=$0804
+; LanguageCodePage should always be set if possible, even if this file is Unicode
+; For English it's set to zero anyway because English only uses ASCII characters
+LanguageCodePage=936
+; If the language you are translating to requires special font faces or
+; sizes, uncomment any of the following entries and change them accordingly.
+;DialogFontName=
+;DialogFontSize=9
+;DialogFontBaseScaleWidth=7
+;DialogFontBaseScaleHeight=15
+;WelcomeFontName=Segoe UI
+;WelcomeFontSize=14
+
+[Messages]
+
+; *** Application titles
+SetupAppTitle=安装
+SetupWindowTitle=安装 - %1
+UninstallAppTitle=卸载
+UninstallAppFullTitle=%1 卸载
+
+; *** Misc. common
+InformationTitle=信息
+ConfirmTitle=确认
+ErrorTitle=错误
+
+; *** SetupLdr messages
+SetupLdrStartupMessage=现在将安装 %1。您想要继续吗?
+LdrCannotCreateTemp=无法创建临时文件。安装程序已中止
+LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止
+HelpTextNote=
+
+; *** Startup error messages
+LastErrorMessage=%1。%n%n错误 %2: %3
+SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。
+SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。
+SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。
+InvalidParameter=无效的命令行参数:%n%n%1
+SetupAlreadyRunning=安装程序正在运行。
+WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。
+WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。
+NotOnThisPlatform=此程序不能在 %1 上运行。
+OnlyOnThisPlatform=此程序只能在 %1 上运行。
+OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1
+WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。
+WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。
+AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。
+PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或高级用户组身份登录。
+SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
+UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
+
+; *** Startup questions
+PrivilegesRequiredOverrideTitle=选择安装程序模式
+PrivilegesRequiredOverrideInstruction=选择安装模式
+PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。
+PrivilegesRequiredOverrideText2=%1 可以仅为您安装,或为所有用户安装(需要管理员权限)。
+PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)
+PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A)(建议选项)
+PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M)
+PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M)(建议选项)
+
+; *** Misc. errors
+ErrorCreatingDir=安装程序无法创建目录“%1”
+ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件
+
+; *** Setup common messages
+ExitSetupTitle=退出安装程序
+ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗?
+AboutSetupMenuItem=关于安装程序(&A)...
+AboutSetupTitle=关于安装程序
+AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4
+AboutSetupNote=
+TranslatorNote=简体中文翻译由 Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
+
+; *** Buttons
+ButtonBack=< 上一步(&B)
+ButtonNext=下一步(&N) >
+ButtonInstall=安装(&I)
+ButtonOK=确定
+ButtonCancel=取消
+ButtonYes=是(&Y)
+ButtonYesToAll=全是(&A)
+ButtonNo=否(&N)
+ButtonNoToAll=全否(&O)
+ButtonFinish=完成(&F)
+ButtonBrowse=浏览(&B)...
+ButtonWizardBrowse=浏览(&R)...
+ButtonNewFolder=新建文件夹(&M)
+
+; *** "Select Language" dialog messages
+SelectLanguageTitle=选择安装语言
+SelectLanguageLabel=选择安装时使用的语言。
+
+; *** Common wizard text
+ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。
+BeveledLabel=
+BrowseDialogTitle=浏览文件夹
+BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。
+NewFolderName=新建文件夹
+
+; *** "Welcome" wizard page
+WelcomeLabel1=欢迎使用 [name] 安装向导
+WelcomeLabel2=即将在您的计算机上安装 [name/ver]。%n%n建议您在继续安装前关闭所有其他应用程序。
+
+; *** "Password" wizard page
+WizardPassword=密码
+PasswordLabel1=此安装程序需要密码验证。
+PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。
+PasswordEditLabel=密码(&P):
+IncorrectPassword=您输入的密码不正确,请重新输入。
+
+; *** "License Agreement" wizard page
+WizardLicense=许可协议
+LicenseLabel=请在继续安装前阅读以下重要信息。
+LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。
+LicenseAccepted=我同意此协议(&A)
+LicenseNotAccepted=我不同意此协议(&D)
+
+; *** "Information" wizard pages
+WizardInfoBefore=信息
+InfoBeforeLabel=请在继续安装前阅读以下重要信息。
+InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。
+WizardInfoAfter=信息
+InfoAfterLabel=请在继续安装前阅读以下重要信息。
+InfoAfterClickLabel=准备好继续安装后,点击“下一步”。
+
+; *** "User Information" wizard page
+WizardUserInfo=用户信息
+UserInfoDesc=请输入您的信息。
+UserInfoName=用户名(&U):
+UserInfoOrg=组织(&O):
+UserInfoSerial=序列号(&S):
+UserInfoNameRequired=请输入用户名。
+
+; *** "Select Destination Location" wizard page
+WizardSelectDir=选择目标位置
+SelectDirDesc=您想将 [name] 安装在哪里?
+SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。
+SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
+DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。
+DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。
+CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。
+CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。
+InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\App%n%n或UNC路径:%n%n\\server\share
+InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。
+DiskSpaceWarningTitle=磁盘空间不足
+DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗?
+DirNameTooLong=文件夹名称或路径太长。
+InvalidDirName=文件夹名称无效。
+BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1
+DirExistsTitle=文件夹已存在
+DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗?
+DirDoesntExistTitle=文件夹不存在
+DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗?
+
+; *** "Select Components" wizard page
+WizardSelectComponents=选择组件
+SelectComponentsDesc=您想安装哪些程序组件?
+SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。
+FullInstallation=完全安装
+; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
+CompactInstallation=简洁安装
+CustomInstallation=自定义安装
+NoUninstallWarningTitle=组件已存在
+NoUninstallWarning=安装程序检测到下列组件已安装在您的计算机中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗?
+ComponentSize1=%1 KB
+ComponentSize2=%1 MB
+ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。
+ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。
+
+; *** "Select Additional Tasks" wizard page
+WizardSelectTasks=选择附加任务
+SelectTasksDesc=您想要安装程序执行哪些附加任务?
+SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。
+
+; *** "Select Start Menu Folder" wizard page
+WizardSelectProgramGroup=选择开始菜单文件夹
+SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式?
+SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。
+SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
+MustEnterGroupName=您必须输入一个文件夹名称。
+GroupNameTooLong=文件夹名称或路径太长。
+InvalidGroupName=文件夹名称无效。
+BadGroupName=文件夹名称不能包含下列任何字符:%n%n%1
+NoProgramGroupCheck2=不创建开始菜单文件夹(&D)
+
+; *** "Ready to Install" wizard page
+WizardReady=准备安装
+ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的计算机。
+ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。
+ReadyLabel2b=点击“安装”继续此安装程序。
+ReadyMemoUserInfo=用户信息:
+ReadyMemoDir=目标位置:
+ReadyMemoType=安装类型:
+ReadyMemoComponents=已选择组件:
+ReadyMemoGroup=开始菜单文件夹:
+ReadyMemoTasks=附加任务:
+
+; *** TDownloadWizardPage wizard page and DownloadTemporaryFile
+DownloadingLabel2=正在下载文件...
+ButtonStopDownload=停止下载(&S)
+StopDownload=您确定要停止下载吗?
+ErrorDownloadAborted=下载已中止
+ErrorDownloadFailed=下载失败:%1 %2
+ErrorDownloadSizeFailed=获取大小失败:%1 %2
+ErrorProgress=无效的进度:%1 / %2
+ErrorFileSize=文件大小错误:预期 %1,实际 %2
+
+; *** TExtractionWizardPage wizard page and ExtractArchive
+ExtractingLabel=正在提取文件...
+ButtonStopExtraction=停止提取(&S)
+StopExtraction=您确定要停止提取吗?
+ErrorExtractionAborted=提取已中止
+ErrorExtractionFailed=提取失败:%1
+
+; *** Archive extraction failure details
+ArchiveIncorrectPassword=密码不正确
+ArchiveIsCorrupted=压缩包已损坏
+ArchiveUnsupportedFormat=不支持的压缩包格式
+
+; *** "Preparing to Install" wizard page
+WizardPreparing=正在准备安装
+PreparingDesc=安装程序正在准备安装 [name] 到您的计算机。
+PreviousInstallNotCompleted=先前的程序安装或卸载未完成,需要您重启计算机。%n%n在重启计算机后,再次运行安装程序以完成 [name] 的安装。
+CannotContinue=安装程序不能继续。请点击“取消”退出。
+ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。
+ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。
+CloseApplications=自动关闭应用程序(&A)
+DontCloseApplications=不要关闭应用程序(&D)
+ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。
+PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n要立即重启吗?
+
+; *** "Installing" wizard page
+WizardInstalling=正在安装
+InstallingLabel=安装程序正在安装 [name] 到您的计算机,请稍候。
+
+; *** "Setup Completed" wizard page
+FinishedHeadingLabel=[name] 安装完成
+FinishedLabelNoIcons=安装程序已在您的计算机中安装了 [name]。
+FinishedLabel=安装程序已在您的计算机中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。
+ClickFinish=点击“完成”退出安装程序。
+FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的计算机。要立即重启吗?
+FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的计算机。%n%n要立即重启吗?
+ShowReadmeCheck=是,我想查阅自述文件
+YesRadio=是,立即重启计算机(&Y)
+NoRadio=否,稍后重启计算机(&N)
+; used for example as 'Run MyProg.exe'
+RunEntryExec=运行 %1
+; used for example as 'View Readme.txt'
+RunEntryShellExec=查阅 %1
+
+; *** "Setup Needs the Next Disk" stuff
+ChangeDiskTitle=安装程序需要下一张磁盘
+SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。
+PathLabel=路径(&P):
+FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。
+SelectDirectoryLabel=请指定下一张磁盘的位置。
+
+; *** Installation phase messages
+SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。
+AbortRetryIgnoreSelectAction=选择操作
+AbortRetryIgnoreRetry=重试(&T)
+AbortRetryIgnoreIgnore=忽略错误并继续(&I)
+AbortRetryIgnoreCancel=取消安装
+RetryCancelSelectAction=选择操作
+RetryCancelRetry=重试(&T)
+RetryCancelCancel=取消
+
+; *** Installation status messages
+StatusClosingApplications=正在关闭应用程序...
+StatusCreateDirs=正在创建目录...
+StatusExtractFiles=正在提取文件...
+StatusDownloadFiles=正在下载文件...
+StatusCreateIcons=正在创建快捷方式...
+StatusCreateIniEntries=正在创建 INI 条目...
+StatusCreateRegistryEntries=正在创建注册表条目...
+StatusRegisterFiles=正在注册文件...
+StatusSavingUninstall=正在保存卸载信息...
+StatusRunProgram=正在完成安装...
+StatusRestartingApplications=正在重启应用程序...
+StatusRollback=正在撤销更改...
+
+; *** Misc. errors
+ErrorInternal2=内部错误:%1
+ErrorFunctionFailedNoCode=%1 失败
+ErrorFunctionFailed=%1 失败;错误代码 %2
+ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3
+ErrorExecutingProgram=无法执行文件:%n%1
+
+; *** Registry errors
+ErrorRegOpenKey=打开注册表项时出错:%n%1\%2
+ErrorRegCreateKey=创建注册表项时出错:%n%1\%2
+ErrorRegWriteKey=写入注册表项时出错:%n%1\%2
+
+; *** INI errors
+ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。
+
+; *** File copying errors
+FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S)(不推荐)
+FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I)(不推荐)
+SourceIsCorrupted=源文件已损坏
+SourceDoesntExist=源文件“%1”不存在
+SourceVerificationFailed=源文件验证失败:%1
+VerificationSignatureDoesntExist=签名文件“%1”不存在
+VerificationSignatureInvalid=签名文件“%1”无效
+VerificationKeyNotFound=签名文件“%1”使用了未知的密钥
+VerificationFileNameIncorrect=文件名不正确
+VerificationFileTagIncorrect=文件标签不正确
+VerificationFileSizeIncorrect=文件大小不正确
+VerificationFileHashIncorrect=文件哈希值不正确
+ExistingFileReadOnly2=无法替换现有文件,它是只读的。
+ExistingFileReadOnlyRetry=移除只读属性并重试(&R)
+ExistingFileReadOnlyKeepExisting=保留现有文件(&K)
+ErrorReadingExistingDest=尝试读取现有文件时出错:
+FileExistsSelectAction=选择操作
+FileExists2=文件已经存在。
+FileExistsOverwriteExisting=覆盖已存在的文件(&O)
+FileExistsKeepExisting=保留现有的文件(&K)
+FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
+ExistingFileNewerSelectAction=选择操作
+ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。
+ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)
+ExistingFileNewerKeepExisting=保留现有的文件(&K)(推荐)
+ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
+ErrorChangingAttr=尝试更改下列现有文件的属性时出错:
+ErrorCreatingTemp=尝试在目标目录创建文件时出错:
+ErrorReadingSource=尝试读取下列源文件时出错:
+ErrorCopying=尝试复制下列文件时出错:
+ErrorDownloading=尝试下载文件时出错:
+ErrorExtracting=尝试提取压缩包时出错:
+ErrorReplacingExistingFile=尝试替换现有文件时出错:
+ErrorRestartReplace=重启并替换失败:
+ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错:
+ErrorRegisterServer=无法注册 DLL/OCX:%1
+ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1
+ErrorRegisterTypeLib=无法注册类库:%1
+
+; *** Uninstall display name markings
+; used for example as 'My Program (32-bit)'
+UninstallDisplayNameMark=%1 (%2)
+; used for example as 'My Program (32-bit, All users)'
+UninstallDisplayNameMarks=%1 (%2, %3)
+UninstallDisplayNameMark32Bit=32 位
+UninstallDisplayNameMark64Bit=64 位
+UninstallDisplayNameMarkAllUsers=所有用户
+UninstallDisplayNameMarkCurrentUser=当前用户
+
+; *** Post-installation errors
+ErrorOpeningReadme=尝试打开自述文件时出错。
+ErrorRestartingComputer=安装程序无法重启计算机,请手动重启。
+
+; *** Uninstaller messages
+UninstallNotFound=文件“%1”不存在。无法卸载。
+UninstallOpenError=文件“%1”不能被打开。无法卸载
+UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载
+UninstallUnknownEntry=卸载日志中遇到一个未知条目(%1)
+ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗?
+UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。
+OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。
+UninstallStatusLabel=正在从您的计算机中移除 %1,请稍候。
+UninstalledAll=已顺利从您的计算机中移除 %1。
+UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。
+UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的计算机。%n%n要立即重启吗?
+UninstallDataCorrupted=文件“%1”已损坏。无法卸载
+
+; *** Uninstallation phase messages
+ConfirmDeleteSharedFileTitle=删除共享的文件吗?
+ConfirmDeleteSharedFile2=系统表示下列共享的文件已不再有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,保留该文件在系统中不会造成任何损害。
+SharedFileNameLabel=文件名:
+SharedFileLocationLabel=位置:
+WizardUninstalling=卸载状态
+StatusUninstalling=正在卸载 %1...
+
+; *** Shutdown block reasons
+ShutdownBlockReasonInstallingApp=正在安装 %1。
+ShutdownBlockReasonUninstallingApp=正在卸载 %1。
+
+; The custom messages below aren't used by Setup itself, but if you make
+; use of them in your scripts, you'll want to translate them.
+
+[CustomMessages]
+
+NameAndVersion=%1 版本 %2
+AdditionalIcons=附加快捷方式:
+CreateDesktopIcon=创建桌面快捷方式(&D)
+CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)
+ProgramOnTheWeb=%1 网站
+UninstallProgram=卸载 %1
+LaunchProgram=运行 %1
+AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)
+AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...
+AutoStartProgramGroupDescription=启动:
+AutoStartProgram=自动启动 %1
+AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗?
diff --git a/packaging/generate_icon.py b/packaging/generate_icon.py
new file mode 100644
index 0000000..c9d32d2
--- /dev/null
+++ b/packaging/generate_icon.py
@@ -0,0 +1,90 @@
+"""生成应用图标 icon.ico — 白底圆角 + 紫色 Logo。
+
+设计 (与 frontend Logo.tsx / favicon.svg 一致):
+ 背景: 白色圆角方块
+ 内容: 紫色 #5B21B6 方括号 + K线 (上影短/下影长, bullish 站稳)
+
+蜡烛几何 (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 多尺寸)
+"""
+from __future__ import annotations
+
+from pathlib import Path
+
+from PIL import Image, ImageDraw
+
+OUT = 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:
+ """按尺寸选参数 (小尺寸加粗)。"""
+ 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(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})")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/icon.ico b/packaging/icon.ico
new file mode 100644
index 0000000..ff83526
Binary files /dev/null and b/packaging/icon.ico differ
diff --git a/packaging/tickflow.iss b/packaging/tickflow.iss
new file mode 100644
index 0000000..57cdabd
--- /dev/null
+++ b/packaging/tickflow.iss
@@ -0,0 +1,143 @@
+; ===========================================================================
+; TickFlow Stock Panel — Inno Setup 安装包脚本
+; ===========================================================================
+; 用途: 把 PyInstaller 产出的 dist/TickFlowStockPanel/ 文件夹封装成
+; 单个 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
+;
+; 设计决策:
+; - 装到用户目录 {localappdata}\Programs\ (不弹 UAC, 不需管理员)
+; - 卸载时询问是否删除用户数据 (%LOCALAPPDATA%\TickFlowStockPanel\)
+; - 桌面 + 开始菜单快捷方式
+; - 卸载入口 (控制面板可见)
+; ===========================================================================
+
+#define MyAppName "TickFlow 股票面板"
+#define MyAppNameEN "TickFlow Stock Panel"
+#define MyAppExeName "TickFlowStockPanel.exe"
+#define MyAppPublisher "TickFlow"
+
+; 版本号: 从 frontend/package.json 读取, 与 Release tag 保持一致
+; 手动指定更可靠 (CI 传入 /DMyAppVersion)
+#ifndef MyAppVersion
+ #define MyAppVersion "0.0.0"
+#endif
+
+[Setup]
+; 基本信息
+AppName={#MyAppName}
+AppVerName={#MyAppName} {#MyAppVersion}
+AppVersion={#MyAppVersion}
+AppPublisher={#MyAppPublisher}
+; 默认装到 D 盘 (非系统盘), 用户可在向导中改任意位置
+; 若 D 盘不存在, [Code] 段 InitializeWizard 会自动回退到用户目录
+DefaultDirName=D:\TickFlowStockPanel
+DefaultGroupName={#MyAppName}
+DisableProgramGroupPage=yes
+OutputDir=Output
+OutputBaseFilename=TickFlowStockPanel-Setup-{#MyAppVersion}
+
+; 关键: 不需要管理员权限, 永不弹 UAC
+; 装到 D 盘普通目录 (非 Program Files) 不需要管理员权限
+PrivilegesRequired=lowest
+; 允许用户在向导中自由选择安装目录
+DisableDirPage=no
+
+; 压缩
+Compression=lzma2/ultra64
+SolidCompression=yes
+LZMAUseSeparateProcess=yes
+
+; 界面
+WizardStyle=modern
+DisableWelcomePage=no
+DisableReadyPage=no
+SetupIconFile=icon.ico
+UninstallDisplayIcon={app}\{#MyAppExeName}
+
+; 卸载相关
+Uninstallable=yes
+CreateUninstallRegKey=yes
+
+[Languages]
+; 简中语言包内置在 packaging/ 下 (从 Inno Setup 官方仓库获取),
+; 不依赖安装目录是否含该文件 (CI 友好)。
+Name: "chinesesimp"; MessagesFile: "ChineseSimplified.isl"
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
+
+[Files]
+; 把 PyInstaller 产出的整个文件夹搬进安装目录
+; Source 路径相对于 .iss 文件所在目录
+Source: "..\backend\dist\TickFlowStockPanel\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+
+[Icons]
+; 开始菜单
+Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
+Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
+
+; 桌面 (可选, 由 Task 控制)
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
+
+[Run]
+; 安装完成后启动应用
+Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
+
+[UninstallRun]
+; 卸载前先关闭正在运行的应用 (否则 exe 被占用删不掉)
+Filename: "{cmd}"; Parameters: "/C taskkill /F /IM {#MyAppExeName}"; Flags: runhidden; RunOnceId: "KillApp"
+
+[UninstallDelete]
+; 清理安装目录下的残留 (日志等)
+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\ (策略/选股/回测/监控)
+// 默认保留 (重装不丢), 但给用户彻底清除的选项
+procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
+var
+ DataDir: String;
+begin
+ if CurUninstallStep = usPostUninstall then
+ begin
+ DataDir := ExpandConstant('{localappdata}\TickFlowStockPanel');
+ if DirExists(DataDir) then
+ begin
+ if SuppressibleMsgBox(
+ '是否同时删除用户数据?' + #13#10 + #13#10 +
+ '位置: ' + DataDir + #13#10 +
+ '内容: 策略、选股结果、回测记录、监控规则等' + #13#10 + #13#10 +
+ '选「是」彻底卸载, 选「否」保留数据(重装后可恢复)。',
+ mbConfirmation, MB_YESNO or MB_DEFBUTTON2, IDNO) = IDYES then
+ begin
+ DelTree(DataDir, True, True, True);
+ end;
+ end;
+ end;
+end;
diff --git a/packaging/tickflow.spec b/packaging/tickflow.spec
index 3b6a9e7..6a187dd 100644
--- a/packaging/tickflow.spec
+++ b/packaging/tickflow.spec
@@ -29,6 +29,7 @@ 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")
# ── 收集带原生库的依赖 (.libs/ 目录必须完整, 否则启动崩) ─────────────
# polars / pyarrow / duckdb / fastexcel 都自带共享库子目录
@@ -151,7 +152,7 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
- icon=None, # TODO: 添加应用图标 (后续设计后填路径)
+ icon=APP_ICON, # 应用图标 (与 favicon/logo 一致)
)
coll = COLLECT(
diff --git a/scripts/bump_version.py b/scripts/bump_version.py
index 5e7530a..cd9d161 100644
--- a/scripts/bump_version.py
+++ b/scripts/bump_version.py
@@ -17,6 +17,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PKG = ROOT / "frontend" / "package.json"
PYPROJECT = ROOT / "backend" / "pyproject.toml"
+APP_INIT = ROOT / "backend" / "app" / "__init__.py" # __version__ 写在这里, health 接口读它
def parse_version(v: str) -> tuple[int, int, int]:
@@ -54,6 +55,19 @@ def write_pyproject_version(v: str) -> None:
PYPROJECT.write_text(text, encoding="utf-8")
+def write_app_init_version(v: str) -> None:
+ """更新 backend/app/__init__.py 的 __version__ (health 接口读这里)。"""
+ text = APP_INIT.read_text(encoding="utf-8")
+ text = re.sub(
+ r'^(__version__\s*=\s*)"[^"]+"',
+ rf'\1"{v}"',
+ text,
+ count=1,
+ flags=re.MULTILINE,
+ )
+ APP_INIT.write_text(text, encoding="utf-8")
+
+
def main() -> int:
# 环境变量跳过
if __import__("os").environ.get("SKIP_VERSION_BUMP") == "1":
@@ -62,13 +76,14 @@ def main() -> int:
cur = read_pkg_version()
new = bump(cur)
- # 两个文件版本可能不一致, 统一用 pkg 的为准
+ # 三个文件版本可能不一致, 统一用 pkg 的为准
write_pkg_version(new)
write_pyproject_version(new)
+ write_app_init_version(new)
# 暂存版本文件改动(并入本次 commit)
import subprocess
- subprocess.run(["git", "add", str(PKG), str(PYPROJECT)], check=True, cwd=str(ROOT))
+ subprocess.run(["git", "add", str(PKG), str(PYPROJECT), str(APP_INIT)], check=True, cwd=str(ROOT))
# 输出新版本号供 hook 读用
print(f"[bump] {cur} -> {new}")