- 技术栈
- 框架:Avalonia
- UI:FluentAvalonia
- 服务主机:Microsoft.Extensions.Hosting(DI 以 Host 为准)
- 项目结构
SecRandom:UI 主项目(Views / ViewModels / Langs / Models / Services)SecRandom.Core:核心通用(抽象、扩展、控件、工具、通用服务)SecRandom.Desktop:桌面启动壳(入口Program.cs)
- ViewModels 必须注册到 Host(强约定)。
- 导航页面必须:
- 类上标注
[PageInfo(...)] - 在
SecRandom/App.axaml.cs的BuildHost()里用services.AddMainPage<T>() / AddSettingsPage<T>()注册
- 类上标注
- 桌面主/设置子页和移动业务/设置子页都是普通
UserControl,不得继承ViewBase。MVE 只承载独立逻辑视图(桌面MainView/SettingsView和崩溃恢复);平台 UI 差异在对应 Host 的条件 DI 注册阶段决定,不通过 MVE 路由表替换页面类型。 - 本地化必须按“每页一个文件夹”拆分,不要混在一起。
- 文件路径统一用
Utils.GetFilePath(...)。桌面和便携包数据落在 package root 的data/...;移动端由共享SecRandom.App在任何路径首次读取前调用一次Utils.ConfigureMobileDataRoot(),选择 app-privateLocalApplicationData/SecRandom/data,其他代码不得运行中改写根目录。 - 不要在页面里随意
new可复用服务;需要复用/单例/可测试的服务必须进 Host。 - 平台功能必须使用
SecRandom.Platforms.Abstractions的窄接口,经App.BuildHost()注册后调用。窗口类只能声明所需特性,不得直接添加 Win32/X11/AppKit 调用或散落的OperatingSystem.Is*分支。 - 课程联动的数据源固定为
0=关闭、1=CSES、2=ClassIsland。CSES 文件由 app 层服务管理在data/CSES/cses_schedule.yml,ClassIsland IPC 仅能在 app 层适配器中引用。数据源失效或状态未知时必须允许抽取;只有确认的课间状态可触发限制、浮窗隐藏或课前重置。
- Host 构建与注册入口:
SecRandom/App.axaml.cs的BuildHost()。共享App按生命周期注册互斥的桌面或 SingleView 移动服务;移动前端源码位于SecRandom/Mobile/、Views/Mobile/、Controls/Mobile/、Services/Mobile/与Langs/Mobile/,不保留独立移动 UI 程序集。SecRandom.Android/SecRandom.iOS只提供平台入口与原生实现。 - 取服务统一走静态入口:
IAppHost.GetService<T>()(拿不到会抛异常)IAppHost.TryGetService<T>()(拿不到返回 null)
- 常见生命周期选择(按项目现有用法对齐):
AddSingleton:配置 Handler、核心业务服务(例如 list service)AddTransient:ViewModel、主容器 View(MainView/SettingsView)、非共享页面实例
导航不是“手写菜单项”,而是“注册页面 → 生成菜单项 → keyed service 实例化页面”。
- 进入设置导航的标准写法:
services.AddSettingsPage<LotteryTablePreviewPage>(
Langs.SettingsPages.ListManagementPage.Resources.LotteryTableTitle);- 分组(侧边栏折叠组):
services.AddGroup(new GroupInfo(name, groupId, iconGlyph));- 页面
[PageInfo(..., groupId: "settings.listManagement")]加入该组
- 设置页内部跳转(最常用):
SettingsView.Current?.SelectNavigationItemById("settings.xxx");
- 主界面内部跳转:
MainView.Current?.SelectNavigationItemById("main.xxx");
- 注意:导航页面是 keyed service 取出来的,没注册就会显示“页面未找到”的占位控件。
- 主界面:
main.xxx - 设置页:
settings.xxx - 设置子页:
settings.group.xxx
- 每个页面的本地化拆分到独立文件夹:
- 必需:
Resources.resx(zh-hans)和Resources.Designer.cs - 可选:
Resources.en-US.resx和Resources.ja-JP.resx,文件名必须保持现有的精确大小写
- 必需:
SecRandom/SecRandom.csproj只需要注册Resources.resx和Resources.Designer.cs(照现有条目追加,不要把所有语言文件都注册进去)。- 注意必须使用
PublicResXFileCodeGenerator - 页面标题/菜单标题优先直接用
Langs.*.Resources.*。 - 大部分情况无需处理 en-US 和 ja-JP 的创建和本地化,由 Crowdin 处理。
- 中文 i18n 文案不得使用中文句号
。 - 设置页面说明资源(
*_D,包含S_*_D和C_*_D)不得使用中文句号或英文句点,文件名、域名、进程名、版本号等技术标识中的英文点号保留
- 配置文件路径由
ConfigBase.ConfigFilePath决定(因此天然支持“可变路径/档案切换”的设计)。 ConfigHandlerBase默认监听PropertyChanged自动保存;MainConfigHandler还会对语言/主题/字体等变更触发 UI 行为。- 保存/读取 JSON 由
SecRandom.Core/Services/Config/FileConfigService.cs实现,并从 desktop/mobile Host 注册;它不是插件 API。 - 安全凭据不是普通配置:只能由
SecRandom/Services/Security/SecurityCredentialStore写入data/config/security/credentials.json。格式版本写在内容的FormatVersion字段,密码用 Argon2id 派生 AES-256-GCM 密钥;不得使用平台密钥库或读取旧凭据文件。
Dictionary内部增删改不会触发PropertyChanged,不会自动保存。- 正确姿势:在 Unloaded 等方法调用 Save 方法,或更新完毕就调用。
- Toast:
- 页面里直接
this.ShowWarningToast(...) / ShowErrorToast(...) - 不需要自己管理容器,MainView/SettingsView Loaded 时会注入
AppToastAdorner
- 页面里直接
- 新增设置页
- 添加页面类 +
[PageInfo] - 新增本地化文件夹(Resources 三件套)
BuildHost()里services.AddSettingsPage<>()注册(必要时先AddGroup)- 需要语言切换刷新标题:补
App.Consts.cs的PageNameProviders - 页面跳转使用
SettingsView.Current?.SelectNavigationItemById(...)
- 添加页面类 +
- 新增 ViewModel
- 在
BuildHost()里注册到 Host - 页面/容器通过
IAppHost.GetService<>()或构造注入(以现有风格为准)
- 在
- 新增服务
- 优先放
SecRandom.Core/Services(通用)或SecRandom/Services(UI 专属) - 在
BuildHost()注册(需要复用的一律不要new)
- 优先放
- 平台原生实现放在对应
SecRandom.Platforms.<OS>项目;平台抽象、启动上下文和 Stub 不进入Core/Shared的插件或 IPC 公开面