复制 local 代码到 serve

This commit is contained in:
2026-07-04 16:59:25 +08:00
parent cee04c1b46
commit 0474e5fb46
302 changed files with 78290 additions and 0 deletions
+419
View File
@@ -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=简体中文翻译由 Kira847320916@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您要继续吗?
+126
View File
@@ -0,0 +1,126 @@
"""生成应用图标 — 白色 squircle 背景 + 紫色 Logo。
设计 (与 frontend Logo.tsx / favicon.svg 一致):
背景: 白色 squircle (macOS Big Sur+ 要求不透明 squircle 背景, 用白色填充)
内容: 紫色 #5B21B6 方括号 + 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, ImageDraw
OUT_ICO = Path(__file__).parent / "icon.ico"
OUT_ICNS = Path(__file__).parent / "icon.icns"
# 背景: 白色 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), (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)))
# 方括号 [ ]
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=LOGO, width=swb, joint="curve")
# wick 影线 (上短下长)
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=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=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.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()
+283
View File
@@ -0,0 +1,283 @@
"""图标设计方案预览 — 生成多个精致方案供挑选。
每个方案以 1024x1024 超清绘制 (4x 超采样抗锯齿), 输出 PNG 预览。
选定后, 用 generate_icon.py 产出最终多尺寸 .ico。
品牌色系 (从 favicon #5B21B6 延伸):
深紫 #2E1065 主紫 #5B21B6 亮紫 #7C3AED 浅紫 #A78BFA
强调金 #FBBF24 (金融上涨感)
"""
from __future__ import annotations
from pathlib import Path
import math
from PIL import Image, ImageDraw, ImageFilter
OUT = Path(__file__).parent / "designs"
OUT.mkdir(exist_ok=True)
# 品牌色
DEEP = (46, 16, 101) # #2E1065
MAIN = (91, 33, 182) # #5B21B6
BRIGHT = (124, 58, 237) # #7C3AED
LIGHT = (167, 139, 250) # #A78BFA
GOLD = (251, 191, 36) # #FBBF24
WHITE = (255, 255, 255)
def _supersample(size: int, factor: int = 4):
"""返回 (大画布, 缩放比, 最终尺寸)。绘制后缩放回 size, 抗锯齿。"""
s = size * factor
return Image.new("RGBA", (s, s), (0, 0, 0, 0)), factor, s
def _finish(img: Image.Image, size: int) -> Image.Image:
"""超采样缩小到目标尺寸。"""
return img.resize((size, size), Image.LANCZOS)
def _v_gradient(size: int, c_top, c_bot, radius_ratio: float = 0.0):
"""竖直渐变填充 (可选圆角)。返回 RGBA Image。"""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
px = img.load()
for y in range(size):
t = y / max(1, size - 1)
r = int(c_top[0] + (c_bot[0] - c_top[0]) * t)
g = int(c_top[1] + (c_bot[1] - c_top[1]) * t)
b = int(c_top[2] + (c_bot[2] - c_top[2]) * t)
for x in range(size):
px[x, y] = (r, g, b, 255)
if radius_ratio > 0:
mask = Image.new("L", (size, size), 0)
md = ImageDraw.Draw(mask)
r = int(size * radius_ratio)
md.rounded_rectangle([0, 0, size - 1, size - 1], radius=r, fill=255)
img.putalpha(mask)
return img
def _glow(size: int, draw_fn, color, blur_ratio: float = 0.04):
"""对绘制内容做柔和光晕: 先画到独立图层, 模糊后叠加。"""
layer = Image.new("RGBA", (size, size), (0, 0, 0, 0))
d = ImageDraw.Draw(layer)
draw_fn(d)
glow = layer.filter(ImageFilter.GaussianBlur(size * blur_ratio))
return glow
# ── 方案 1: 渐变紫底 + 白色发光主体 (现代 App 风格) ──────────────
def design_1(size: int = 1024) -> Image.Image:
img, f, s = _supersample(size)
# 圆角渐变背景 (深紫 → 亮紫)
bg = _v_gradient(s, BRIGHT, DEEP, radius_ratio=0.22)
img.alpha_composite(bg)
d = ImageDraw.Draw(img)
u = s / 32 # 单位 (基于32基准)
# 白色方括号 (圆头粗线)
sw = 2.8 * u
def draw_brackets(dd):
# 左 [
dd.line([(11, 6), (5, 6)], fill=WHITE, width=int(sw), joint="curve")
dd.line([(5, 6), (5, 26)], fill=WHITE, width=int(sw))
dd.line([(5, 26), (11, 26)], fill=WHITE, width=int(sw))
# 右 ]
dd.line([(21, 6), (27, 6)], fill=WHITE, width=int(sw))
dd.line([(27, 6), (27, 26)], fill=WHITE, width=int(sw))
dd.line([(27, 26), (21, 26)], fill=WHITE, width=int(sw))
# 白色 K 线 wick
def draw_wick(dd):
dd.line([(16, 8), (16, 24)], fill=LIGHT, width=int(1.6 * u))
# 白色 K 线 body
def draw_body(dd):
dd.rounded_rectangle(
[12.5 * u, 12 * u, 19.5 * u, 21 * u],
radius=1.2 * u, fill=WHITE,
)
# 光晕层 (放大模糊)
glow = Image.new("RGBA", (s, s), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
draw_body(gd)
glow = glow.filter(ImageFilter.GaussianBlur(s * 0.025))
# 光晕染色为亮紫
tint = Image.new("RGBA", (s, s), BRIGHT + (0,))
glow = Image.composite(tint, Image.new("RGBA", (s, s), (0, 0, 0, 0)),
glow.point(lambda a: min(255, int(a * 0.6))))
img.alpha_composite(glow)
draw_brackets(d)
draw_wick(d)
draw_body(d)
return _finish(img, size)
# ── 方案 2: 透明底 + 渐变紫发光主体 (优雅线条) ──────────────────
def design_2(size: int = 1024) -> Image.Image:
img, f, s = _supersample(size)
d = ImageDraw.Draw(img)
u = s / 32
sw = 3.0 * u
# 渐变括号: 用浅紫到亮紫。Pillow line 不支持渐变, 分段填色模拟。
def gradient_brackets(dd):
# 左括号三段, 从上(浅)到下(亮) — 简化为整体亮紫, 配光晕显层次
col = BRIGHT
# 左 [
dd.line([(11, 5), (5, 5)], fill=col, width=int(sw))
dd.line([(5, 5), (5, 27)], fill=col, width=int(sw))
dd.line([(5, 27), (11, 27)], fill=col, width=int(sw))
# 右 ]
dd.line([(21, 5), (27, 5)], fill=col, width=int(sw))
dd.line([(27, 5), (27, 27)], fill=col, width=int(sw))
dd.line([(27, 27), (21, 27)], fill=col, width=int(sw))
# 主体光晕 (亮紫大模糊)
glow_layer = Image.new("RGBA", (s, s), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow_layer)
gradient_brackets(gd)
gd.rounded_rectangle([12.5 * u, 12 * u, 19.5 * u, 21 * u], radius=1.2 * u, fill=BRIGHT)
glow = glow_layer.filter(ImageFilter.GaussianBlur(s * 0.05))
img.alpha_composite(glow)
# 主体 (亮紫)
gradient_brackets(d)
d.line([(16, 8), (16, 24)], fill=LIGHT, width=int(1.8 * u))
d.rounded_rectangle([12.5 * u, 12 * u, 19.5 * u, 21 * u], radius=1.2 * u, fill=BRIGHT)
# 高光: body 顶部一道浅紫
d.rounded_rectangle([13.5 * u, 12.8 * u, 18.5 * u, 14.5 * u], radius=0.8 * u, fill=LIGHT)
return _finish(img, size)
# ── 方案 3: 深色底 + 金紫上涨 K 线柱 (金融图表感) ────────────────
def design_3(size: int = 1024) -> Image.Image:
img, f, s = _supersample(size)
# 深紫黑圆角底
bg = _v_gradient(s, (30, 27, 75), (15, 12, 41), radius_ratio=0.22)
img.alpha_composite(bg)
d = ImageDraw.Draw(img)
u = s / 32
# 三根上涨 K 线柱 (紫→亮紫→金), 从左到右升高
bars = [
# (x_center, body_top, body_bottom, wick_top, wick_bottom, color)
(10, 17, 24, 14, 26, LIGHT),
(16, 13, 20, 10, 22, BRIGHT),
(22, 8, 15, 5, 17, GOLD),
]
bw = 3.2 * u
for cx, bt, bb, wt, wb, col in bars:
# wick
d.line([(cx * u, wt * u), (cx * u, wb * u)], fill=col, width=int(1.2 * u))
# body (圆角)
d.rounded_rectangle(
[cx * u - bw / 2, bt * u, cx * u + bw / 2, bb * u],
radius=0.8 * u, fill=col,
)
# 金色柱的光晕
glow = Image.new("RGBA", (s, s), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
gd.rounded_rectangle([22 * u - bw / 2, 8 * u, 22 * u + bw / 2, 15 * u], radius=0.8 * u, fill=GOLD)
glow = glow.filter(ImageFilter.GaussianBlur(s * 0.03))
img.alpha_composite(glow)
# 上涨趋势线 (淡, 连接柱顶)
d.line([(10 * u, 17 * u), (16 * u, 13 * u), (22 * u, 8 * u)],
fill=(255, 255, 255, 120), width=int(0.6 * u))
return _finish(img, size)
# ── 方案 4: 渐变底 + K线被方括号"框选" (品牌强调) ────────────────
def design_4(size: int = 1024) -> Image.Image:
img, f, s = _supersample(size)
# 斜向渐变背景 (浅紫→主紫), 圆角
bg = _v_gradient(s, (139, 92, 246), MAIN, radius_ratio=0.22)
img.alpha_composite(bg)
d = ImageDraw.Draw(img)
u = s / 32
# 白色粗括号 (圆角连接)
sw = 2.2 * u
def brackets(dd):
dd.rounded_rectangle(
[5 * u, 6 * u, 11 * u, 26 * u],
radius=2.0 * u, width=int(sw), outline=WHITE,
) # 左括号外形
dd.rounded_rectangle(
[21 * u, 6 * u, 27 * u, 26 * u],
radius=2.0 * u, width=int(sw), outline=WHITE,
) # 右括号外形
# 上面的画法画出的是矩形框, 改回线条式括号但圆头
def brackets2(dd):
cap = int(sw / 2)
# 左 [
dd.line([(10.5, 6), (6, 6)], fill=WHITE, width=int(sw))
dd.rounded_rectangle([6 * u - cap, 6 * u - cap, 6 * u + cap, 26 * u + cap],
radius=cap, fill=WHITE) # 竖干
dd.line([(6, 26), (10.5, 26)], fill=WHITE, width=int(sw))
# 圆角补点
for cx, cy in [(6, 6), (6, 26)]:
dd.ellipse([cx * u - cap, cy * u - cap, cx * u + cap, cy * u + cap], fill=WHITE)
# 右 ]
dd.line([(21.5, 6), (26, 6)], fill=WHITE, width=int(sw))
dd.rounded_rectangle([26 * u - cap, 6 * u - cap, 26 * u + cap, 26 * u + cap],
radius=cap, fill=WHITE)
dd.line([(26, 26), (21.5, 26)], fill=WHITE, width=int(sw))
for cx, cy in [(26, 6), (26, 26)]:
dd.ellipse([cx * u - cap, cy * u - cap, cx * u + cap, cy * u + cap], fill=WHITE)
# K 线光晕
glow = Image.new("RGBA", (s, s), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
gd.rounded_rectangle([12.8 * u, 11.5 * u, 19.2 * u, 21.5 * u], radius=1.4 * u, fill=WHITE)
glow = glow.filter(ImageFilter.GaussianBlur(s * 0.02))
img.alpha_composite(glow)
brackets2(d)
# wick
d.line([(16, 8), (16, 24)], fill=(255, 255, 255, 200), width=int(1.5 * u))
# body (实心白)
d.rounded_rectangle([12.8 * u, 11.5 * u, 19.2 * u, 21.5 * u], radius=1.4 * u, fill=WHITE)
return _finish(img, size)
def main() -> None:
designs = [
("方案1_渐变紫底白色发光", design_1),
("方案2_透明底渐变紫线条", design_2),
("方案3_深色底金紫上涨K线", design_3),
("方案4_浅紫底框选K线", design_4),
]
# 拼一张对比图 (2x2)
cell = 512
grid = Image.new("RGBA", (cell * 2 + 60, cell * 2 + 60), (245, 245, 247, 255))
gd = ImageDraw.Draw(grid)
positions = [(20, 20), (cell + 40, 20), (20, cell + 40), (cell + 40, cell + 40)]
for (name, fn), pos in zip(designs, positions):
img = fn(cell)
# 棋盘格背景透显
grid.alpha_composite(img, pos)
# 单独存
fn(1024).save(OUT / f"{name}.png")
print(f" 生成: {name}.png")
grid.save(OUT / "对比图_2x2.png")
print(f"\n对比图: {OUT / '对比图_2x2.png'}")
print(f"单图目录: {OUT}")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+178
View File
@@ -0,0 +1,178 @@
; ===========================================================================
; 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, 不需管理员)
; - 用户数据存在 {app}\data\ (与程序同处一个总目录, 视觉直观)
; - 卸载时询问是否删除用户数据 ({app}\data\)
; - 覆盖安装(升级)不动 data\: Inno Setup 只写程序文件, data 不在安装清单
; - 桌面 + 开始菜单快捷方式
; - 卸载入口 (控制面板可见)
; ===========================================================================
#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] 故意不删 {app}:
; 用户数据在 {app}\data\, 若这里写 Type: filesandordirs; Name: "{app}" 会连数据一起删。
; 卸载默认行为已足够 —— Inno Setup 会删除它安装清单内的所有程序文件, 只留下运行时
; 生成的 data\ 目录。是否清理 data\ 由下方 [Code] 的卸载询问逻辑决定。
[Code]
// ── 辅助函数: 判断目录是否为空 ─────────────────────────────────
// Inno Setup 内置无 IsDirEmpty, 用 FindFirst/FindNext 自行实现。
// 用于卸载后清理空的 {app} 壳目录。
function IsDirEmpty(const Dir: String): Boolean;
var
FindRec: TFindRec;
begin
Result := True;
if FindFirst(AddBackslash(Dir) + '*', FindRec) then
begin
try
repeat
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
begin
Result := False;
Break;
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
end;
// ── 启动时: 若 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;
// ── 卸载时询问是否删除用户数据 ─────────────────────────────────
// 用户数据在 {app}\data\ (策略/选股/回测/监控/行情), 与程序同处 {app} 总目录。
// Inno Setup 卸载默认只删它装过的程序文件, data\ 会被保留 (覆盖安装/常规卸载都不丢)。
// 这里仅在用户明确「彻底卸载」时, 才询问是否清理 data\ + {app} 空壳。
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
DataDir, AppDir: String;
begin
if CurUninstallStep = usPostUninstall then
begin
// {app}\data = 用户数据目录 (与程序同总目录, 子文件夹)
DataDir := ExpandConstant('{app}\data');
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;
// 清理可能残留的空 {app} 壳目录 (程序文件已被 Inno Setup 删除)
AppDir := ExpandConstant('{app}');
if DirExists(AppDir) and IsDirEmpty(AppDir) then
begin
DelTree(AppDir, True, True, True);
end;
end;
end;
+216
View File
@@ -0,0 +1,216 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller (onedir )
为什么 onedir 而非 onefile:
- onefile 每次启动都解压到临时 _MEIxxxxx, APScheduler/多线程冲突
- onedir 启动更快, 调试更方便 (可看到目录结构), 原生库直接在目录里
- 体积差异通过压缩安装包弥补 (CI zip 打包)
入口: backend/app/desktop.py (桌面版入口, uvicorn + pywebview)
构建 (在项目根目录):
cd frontend && pnpm build # 先构建前端到 frontend/dist
pyinstaller packaging/tickflow.spec # 产物在 dist/TickFlowStockPanel/
"""
import sys
from pathlib import Path
from PyInstaller.utils.hooks import (
collect_all,
collect_submodules,
collect_data_files,
copy_metadata,
)
_IS_MACOS = sys.platform == "darwin"
block_cipher = None
# ── 资源路径基准: 项目根 (spec 文件在 packaging/ 下) ──────────────────
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"))
# ── 收集带原生库的依赖 (.libs/ 目录必须完整, 否则启动崩) ─────────────
# polars / pyarrow / duckdb / fastexcel 都自带共享库子目录
datas = []
binaries = []
hiddenimports = []
for pkg in ("polars", "pyarrow", "duckdb", "fastexcel"):
d, b, h = collect_all(pkg)
datas += d
binaries += b
hiddenimports += h
# polars-runtime-32 (rtcompat 兼容内核): release.yml 用 --extra legacy-cpu 安装。
# 它是独立的伴侣二进制包 (含 .pyd/.so), 与 polars 主包分开发布,
# collect_all("polars") 抓不到它的目录 —— 必须显式收集, 否则老 CPU 用户
# 运行时 rtcompat 加载器找不到兼容库仍会崩 (Illegal instruction)。
# 不存在时 (未装 legacy-cpu) collect_all 返回空, 不影响普通构建。
try:
rt_d, rt_b, rt_h = collect_all("polars_runtime_32")
datas += rt_d
binaries += rt_b
hiddenimports += rt_h
except Exception:
pass
# polars 新 ABI 运行时目录 (_polars_runtime_32) 需显式收集子模块
hiddenimports += collect_submodules("polars")
# ── pywebview 平台后端 (动态导入, PyInstaller 默认抓不到) ────────────
hiddenimports += collect_submodules("webview")
hiddenimports += collect_submodules("webview.platforms")
# ── 系统通知后端 (winotify/plyer 按平台动态导入) ─────────────────────
if sys.platform == "win32":
hiddenimports += collect_submodules("winotify")
hiddenimports += collect_submodules("plyer")
hiddenimports += collect_submodules("plyer.platforms")
# ── uvicorn 动态导入的模块 (loop/protocol/logging 按字符串加载) ──────
hiddenimports += [
"uvicorn.logging",
"uvicorn.loops",
"uvicorn.loops.auto",
"uvicorn.loops.asyncio",
"uvicorn.protocols",
"uvicorn.protocols.http",
"uvicorn.protocols.http.auto",
"uvicorn.protocols.http.h11_impl",
"uvicorn.protocols.websockets",
"uvicorn.protocols.websockets.auto",
"uvicorn.lifespan",
"uvicorn.lifespan.on",
]
# ── fastapi / pydantic 元数据 (版本检测用) ───────────────────────────
# 注意: 任何用 importlib.metadata.version() 读版本的包, 都必须 copy_metadata,
# 否则 frozen 后报 PackageNotFoundError。tickflow 包内部就是这么读的。
# 用容错写法: 不存在的包跳过, 避免不同环境 (有无装某依赖) 导致构建失败。
def _safe_metadata(pkg):
""", """
try:
return copy_metadata(pkg)
except Exception:
return []
for pkg in (
"fastapi", "pydantic", "pydantic_settings", "starlette", "anyio",
"tickflow", # tickflow/__version__.py 用 importlib.metadata 读版本
"uvicorn", "polars", "duckdb", "pyarrow", "httpx", "numpy", "pandas",
"openai", "platformdirs", "winotify", "plyer", "apscheduler",
"python-dotenv", "fastexcel",
):
datas += _safe_metadata(pkg)
# ── 随包资源 (只读, 放进 _MEIPASS) ────────────────────────────────────
# 前端 dist → static/ (config.py frozen 模式读 _MEIPASS/static)
datas += [(FRONTEND_DIST, "static")]
# tiers.yaml → 包根 (config.py frozen 模式读 _MEIPASS/tiers.yaml)
datas += [(TIERS_YAML, ".")]
# 内置策略 → app/strategy/builtin/ (importlib 动态加载, 不能进 PYZ)
datas += [(BUILTIN_STRATEGIES, "app/strategy/builtin")]
# ── 排除不需要的重型依赖 (主包不含 vectorbt 回测链) ──────────────────
excludes = [
"vectorbt",
"numba",
"llvmlite",
"matplotlib",
"plotly",
"ipywidgets",
"nbformat",
"nbconvert",
"jupyter",
"IPython",
"pytest",
"pytest_asyncio",
"ruff",
"mypy",
]
a = Analysis(
[str(ROOT / "backend" / "app" / "desktop.py")],
pathex=[str(ROOT / "backend")],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=excludes,
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="TickFlowStockPanel",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False, # UPX 压缩原生库常导致崩溃, 关闭
console=False, # 桌面应用: 不显示控制台窗口 (调试时临时改 True 抓日志)
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=APP_ICON, # 应用图标 (与 favicon/logo 一致)
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
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",
},
)