Repository Wiki
ChanIok/SpinningMomo

设置页面与引导(Onboarding)流程

设置页面与引导流程是 SpinningMomo Web 前端(web/src)中负责「应用偏好配置」与「首次使用引导」的两个相互协作的功能模块:web/src/features/settings 提供分组式设置界面与全局设置状态(Pinia store),web/src/features/onboarding 提供首次启动的欢迎引导页。两者共享同一份 AppSettings 数据结构与后端设置 RPC,由应用入口 web/src/main.ts 在启动时统一判定是否进入引导。

Purpose and Scope

本文覆盖以下内容(Web 前端视角,端到端):

  • web/src/features/settings 模块:设置 store、外观应用(appearance)、设置页面组件分组(侧栏、菜单、各分组内容组件)。
  • web/src/features/onboarding 模块:引导页 OnboardingPage.vue、引导专用 API、类型定义。
  • 启动判定链路:main.ts 如何结合 onboarding.completed 与 onboarding.flowVersion 决定是否跳转 /welcome。
  • 与后端(C++ 侧)的协同点:features::settings::should_show_onboarding、CURRENT_ONBOARDING_FLOW_VERSION 常量在前后端的镜像定义。
  • 设置模块的 i18n 文案与后台任务(媒体硬链接初始化等一次性重操作)的关联。

以下主题有意留给兄弟页面,本文不展开:

  • 后端设置数据模型、持久化与设置 RPC 端点的完整实现:src/core/rpc/endpoints/settings/、src/core/events/handlers/settings_handlers.cpp、src/features/settings/settings.cpp(C++ 核心设置服务)属于其他页面。
  • 通用 RPC 框架与权限等级(initializeRPC / initializeAccessLevel):见 Web 前端 RPC 通信相关页面。
  • 后台任务系统(web/src/core/tasks 与 initializeMediaHardlinks 任务):见任务编排相关页面。

Overview

背景与问题

SpinningMomo 是一个带有 WebView UI 的桌面应用:C++ 后端负责设置持久化,Vue 3 前端负责设置展示与编辑。因此「设置」不是纯粹的静态页面,而是一条 前端状态 ⇄ RPC ⇄ 后端存储 的双向链路。同样,「首次引导」也不是前端单方面能决定的事——后端启动器(src/core/initializer/initializer.cpp)在打开 WebView 窗口之前就要判断是否处于首启状态,以便决定窗口尺寸(引导页使用临时尺寸)。

关键概念

概念位置说明
AppSettings前端 web/src/features/settings/types.ts / 后端 src/features/settings/types.hpp应用设置聚合结构,前后端各有一份镜像类型定义
app.onboardingAppSettings 内的子结构首次引导状态:completed 与 flowVersion 等字段
CURRENT_ONBOARDING_FLOW_VERSION前后端均为常量 1引导流程版本号;提升该值可强制老用户重新走一遍引导
needsOnboardingweb/src/main.ts 中的局部判定!onboarding.completed || onboarding.flowVersion < CURRENT_ONBOARDING_FLOW_VERSION
should_show_onboardingsrc/features/settings/settings.hpp后端对同一状态的判断,供启动器与 WebView 窗口使用
引导重操作src/extensions/infinity_nikki/task_service.hpp「媒体硬链接初始化」是引导/设置中的一次性后台重任务(initializeMediaHardlinks)

版本化引导的设计意图

引导状态不只是一个布尔值,而是 completed + flowVersion 的组合。这样设计的动机是:当产品迭代出新的引导步骤(例如新增功能需要用户授权)时,只需把 CURRENT_ONBOARDING_FLOW_VERSION 提升,所有 flowVersion 落后的老用户都会在下次启动时被重新带回 /welcome 页,而无需清空他们的全部设置。前后端各自持有同一常量(均为 1),后端用于启动窗口决策,前端用于路由决策,二者必须保持一致。

Architecture

Loading diagram...

图解说明:

  • 入口层:main.ts 是两个模块唯一的汇聚点。它先完成基础设施初始化(RPC、权限、路由守卫、任务订阅),再读取设置 store,然后才做引导判定。
  • settings 模块(web/src/features/settings)由四部分组成:store.ts(Pinia 全局状态,负责与后端同步语言等设置)、types.ts(AppSettings 前端镜像类型与版本常量)、appearance.ts(把主题/背景设置落到 DOM,避免首屏闪烁)、以及 components/ 下按功能分组的 Vue 内容组件。UI 结构是「侧栏(SettingsSidebar)→ 菜单/可拖拽列表(SettingsMenuList、DraggableSettingsList)→ 分组内容组件」。
  • onboarding 模块(web/src/features/onboarding)非常小:一个页面 OnboardingPage.vue(对应 /welcome 路由)、api.ts(引导专用 RPC 调用)和 types.ts。
  • 后端协同:C++ 侧 features::settings::should_show_onboarding 同时被 initializer(决定启动时是否直接打开主窗口并打日志 Onboarding required, attempting to open main UI window)和 webview_window(引导期采用 temporary_size 临时窗口尺寸)消费。前后端共享同一份 app.onboarding 数据。

Core Flow: 启动到引导/主界面的完整链路

Loading diagram...

下面是 web/src/main.ts 中承载这条链路的真实代码,这是理解两个模块如何被「拼装」的关键入口:

typescript
1;(async () => { 2 // 首先初始化 i18n(使用默认语言) 3 await initI18n('zh-CN') 4 5 // 先确定调用者访问等级,再初始化会根据权限显示不同内容的页面。 6 await initializeAccessLevel() 7 8 // 守卫必须在安装 Router 前注册;app.use(router) 会立即启动首次导航。 9 setupRouterGuards(router) 10 setupDocumentTitle(router) 11 app.use(router) 12 await router.isReady() 13 14 // 然后初始化 settings store,它会自动同步后端的语言设置 15 const settingsStore = useSettingsStore() 16 await settingsStore.init() 17 18 // 初始化后台任务订阅 19 const taskStore = useTaskStore() 20 await taskStore.initialize() 21 22 const onboarding = settingsStore.appSettings.app.onboarding 23 const needsOnboarding = 24 !onboarding.completed || onboarding.flowVersion < CURRENT_ONBOARDING_FLOW_VERSION 25 if (needsOnboarding && router.currentRoute.value.name !== 'welcome') { 26 await router.replace('/welcome') 27 } 28 29 // 在挂载前应用主题和背景,避免首屏闪烁 30 applyAppearanceToDocument(settingsStore.appSettings) 31 32 // 监听设置变化,实时同步外观 33 watch( 34 () => [ 35 settingsStore.appSettings.ui.webTheme.mode, 36 settingsStore.appSettings.ui.webTheme.customCss, 37 settingsStore.appSettings.ui.webTheme.menuBlur, 38 settingsStore.appSettings.ui.background.type, 39 settingsStore.appSettings.ui.background.imageFileName, 40 settingsStore.appSettings.ui.background.backgroundBlurAmount, 41 settingsStore.appSettings.ui.background.backgroundOpacity, 42 settingsStore.appSettings.ui.background.overlayColors.join('|'), 43 settingsStore.appSettings.ui.background.primaryColor, 44 settingsStore.appSettings.ui.background.overlayOpacity, 45 settingsStore.appSettings.ui.background.surfaceOpacity, 46 ], 47 () => { 48 applyAppearanceToDocument(settingsStore.appSettings) 49 } 50 ) 51 52 // 最后挂载应用 53 app.mount('#app') 54})()

Source: main.ts

为什么是这个顺序

这段启动序列的每个 await 都有明确的依赖理由,顺序不可随意调换:

  1. createPinia() 必须先于一切(第 17-22 行):注释明确说明「Pinia 必须先安装,后续权限探测和路由守卫会读取其状态」。设置 store、任务 store 都依赖 Pinia 已安装。
  2. initI18n 在最前:后续所有 UI(包括引导页文案)都需要语言就绪。注意默认值硬编码为 zh-CN,真正的语言随后由 settingsStore.init() 从后端同步覆盖。
  3. initializeAccessLevel 在页面渲染前:不同调用者(本地/远程)看到不同的设置项可见性,必须在路由导航发生前确定。
  4. 守卫先注册、Router 后安装:app.use(router) 会立即触发首次导航,若守卫未注册,首次导航会绕过权限逻辑——这是一类典型的时序 bug,代码用注释显式防住了。
  5. 引导判定在 store 与任务订阅之后:needsOnboarding 需要读 appSettings.app.onboarding,而该数据来自后端,必须等 settingsStore.init() 完成。任务订阅先行初始化则保证引导页若触发后台重任务(如媒体硬链接初始化),进度可被正确上报。
  6. applyAppearanceToDocument 在 app.mount('#app') 之前调用:注释写明目的是「避免首屏闪烁」——先把主题与背景写进 document,再挂载 DOM,用户就不会看到默认样式闪一下再切换。
  7. watch 的依赖数组是「浅字段拼接」:overlayColors.join('|') 把数组降级成字符串参与依赖比较,这是 Vue watch 对数组内容的经典处理方式(直接传数组引用无法侦测元素变化)。

引导判定逻辑详解

判定表达式:

typescript
1const onboarding = settingsStore.appSettings.app.onboarding 2const needsOnboarding = 3 !onboarding.completed || onboarding.flowVersion < CURRENT_ONBOARDING_FLOW_VERSION 4if (needsOnboarding && router.currentRoute.value.name !== 'welcome') { 5 await router.replace('/welcome') 6}

Source: main.ts

三个细节体现了防御性设计:

  • 短路顺序:!completed 在前,flowVersion 比较在后。已完成的用户若版本落后仍会重新引导(升级新流程),但未完成引导的用户即使 flowVersion 异常地等于当前值也会被引导。
  • 双条件跳转:needsOnboarding && route.name !== 'welcome' 避免了用户已在 /welcome(例如刷新页面时 Router 首次导航已落在 welcome)时重复 replace,防止导航冗余/循环。
  • router.replace 而非 push:引导页不应出现在历史栈里,用户不能「后退」回到一个未初始化的应用状态。

前后端镜像的版本常量

前端常量从设置模块类型文件导入:

typescript
import { useSettingsStore } from '@/features/settings/store' import { CURRENT_ONBOARDING_FLOW_VERSION } from '@/features/settings/types' import { applyAppearanceToDocument } from '@/features/settings/appearance'

Source: main.ts

后端在同一子系统中有对应定义(C++ 侧):

cpp
// 当前欢迎流程版本(用于控制是否需要重新引导) constexpr int CURRENT_ONBOARDING_FLOW_VERSION = 1;

Source: types.hpp

AppSettings 中的引导子结构在后端类型中显式声明:

cpp
// 首次引导设置 struct Onboarding {

Source: types.hpp

以及供启动器使用的判定函数签名:

cpp
// 判断当前配置是否需要显示首次引导页 auto should_show_onboarding(const AppSettings& settings) -> bool;

Source: settings.hpp

这种「前后端各持常量、语义靠约定对齐」的做法有一个隐含的维护约束:修改任何一端的 CURRENT_ONBOARDING_FLOW_VERSION 都必须同步另一端,否则会出现后端认为需要引导(采用临时窗口尺寸)而前端不跳转 /welcome、或相反的窗口/页面不一致。

设置页面组件体系

设置 UI 采用「侧栏 + 分组内容」的两栏结构,所有组件位于 web/src/features/settings/components/。骨架组件负责导航与布局,内容组件按设置域拆分:

组件职责(依据文件清单与模块结构)
SettingsSidebar.vue设置页左侧栏,承载入口导航
SettingsMenuList.vue设置菜单列表,展示各分组入口
DraggableSettingsList.vue可拖拽排序的列表,用于用户自定义项顺序
GeneralSettingsContent.vue通用设置(语言等基础项)
AppearanceContent.vue外观设置(主题模式、自定义 CSS、菜单模糊、背景类型/图片/模糊/透明度/叠加色等)
CaptureSettingsContent.vue采集(截图/录屏)设置
HotkeySettingsContent.vue + HotkeyRecorder.vue快捷键设置与按键录制控件
BackupSettingsContent.vue备份设置(配合 backupApi.ts)
NetworkAccessContent.vue远程访问(网络)设置
AdbModeContent.vueADB 模式设置
FloatingWindowContent.vue悬浮窗设置
WindowSceneContent.vue窗口场景设置
ExtensionsContent.vue扩展(如 infinity_nikki)设置
OverlayPaletteEditor.vue叠加层调色板编辑器
ResetSettingsDialog.vue重置设置确认对话框

这样按域拆分(而不是一个巨型设置页)的设计意图:每个内容组件只依赖自己那部分 AppSettings 子树,减少不必要的重渲染面,也让各设置域可以独立测试与懒加载。

外观应用链路(appearance.ts)

设置页里「外观」类改动(AppearanceContent.vue)的落点不在页面内,而是全局的:main.ts 通过 watch 监听 11 个外观相关字段并调用 applyAppearanceToDocument(settingsStore.appSettings),把主题/背景直接应用到 document。这解释了为什么在设置页改主题会即时影响整个应用(包括 /welcome 引导页)——外观是文档级全局效果,而不是路由级组件效果。

i18n 文案

设置页文案集中在 web/src/core/i18n/locales/{zh-CN,en-US}/settings.json,默认语言为 zh-CN(main.ts 中 initI18n('zh-CN')),随后由 settingsStore.init() 从后端同步用户偏好的语言。

Onboarding 模块结构

web/src/features/onboarding 只包含三个文件,是最小化的功能模块:

Loading diagram...
  • pages/OnboardingPage.vue:引导页本体,挂载在 /welcome 路由。它消费引导步骤的状态并在完成时把 app.onboarding 写回(经由 api.ts 的 RPC,最终落到后端 AppSettings)。
  • api.ts:引导专用 RPC 封装,与 settings/api.ts 平行——两者都走 core/rpc,但关注点分离:引导的写操作与设置的写操作各自成文件,避免引导逻辑混入通用设置 API。
  • types.ts:引导侧类型定义。

说明:本页源码读取预算已耗尽,OnboardingPage.vue、store.ts、api.ts 的逐行实现细节未在本文逐条摘录;其组件清单与协作关系来自模块文件结构与 main.ts 的调用点证据。需要逐行细节时请直接查阅上述文件。

引导与后台重任务

引导不止收集偏好,还承担一次性重操作的触发入口。后端任务服务注释明确了这一点:

cpp
// 媒体硬链接初始化(引导 / 设置里的一次性重操作)。任务类型 // initializeMediaHardlinks。

Source: task_service.hpp

设计意图:硬链接初始化是磁盘密集型操作,放在引导期(用户预期「正在准备应用」)比放在使用期更合理;同时设置页也保留同一入口(ExtensionsContent.vue 所在的扩展设置域),便于用户日后手动重跑。这要求前端在引导判定前先 await taskStore.initialize() 完成任务订阅,与 main.ts 的初始化顺序一致。

后端启动器协同

C++ 启动器在打开 WebView 前做同样的引导判断:

cpp
1// 先显示启动 UI,避免 Gallery 目录探测、远程根检查等非首屏工作阻塞用户看到窗口。 2const bool should_open_onboarding = 3 features::settings::should_show_onboarding(state.settings->raw); 4if (should_open_onboarding) { 5 Logger().info("Onboarding required, attempting to open main UI window");

Source: initializer.cpp

以及 WebView 窗口在引导期使用临时尺寸:

cpp
if (!temporary_size && window.temporary_size && features::settings::should_show_onboarding(state.settings->raw)) { temporary_size = window.temporary_size;

Source: webview_window.cpp

即:引导状态下窗口使用 window.temporary_size(适合引导页的较小尺寸),完成引导后恢复正常尺寸。前端 /welcome 路由与后端临时窗口尺寸由同一份 app.onboarding 状态驱动,这是双端一致性的关键。

配置与状态参考

引导相关状态字段

字段类型默认语义说明
app.onboarding.completedboolean未完成时触发引导引导是否已完成
app.onboarding.flowVersionnumber初始落后于当前版本则引导已完成的引导流程版本
CURRENT_ONBOARDING_FLOW_VERSIONnumber(常量)1当前引导流程版本;前后端镜像定义,需同步修改
window.temporary_size尺寸结构后端字段引导期 WebView 窗口采用的临时尺寸

外观实时同步字段(watch 依赖清单)

以下字段任一变化都会触发 applyAppearanceToDocument(摘自 main.ts 的 watch 依赖数组):

字段路径所属域
ui.webTheme.mode主题模式
ui.webTheme.customCss自定义 CSS
ui.webTheme.menuBlur菜单模糊
ui.background.type背景类型
ui.background.imageFileName背景图片
ui.background.backgroundBlurAmount背景模糊量
ui.background.backgroundOpacity背景透明度
ui.background.overlayColors(`join('')`)
ui.background.primaryColor主色
ui.background.overlayOpacity叠加层透明度
ui.background.surfaceOpacity表面透明度

模块文件地图

路径角色
web/src/features/settings/store.ts设置 Pinia store,init() 同步后端语言
web/src/features/settings/types.ts前端 AppSettings 镜像类型 + CURRENT_ONBOARDING_FLOW_VERSION
web/src/features/settings/appearance.tsapplyAppearanceToDocument 文档级外观应用
web/src/features/settings/api.ts / backupApi.ts设置/备份 RPC 封装
web/src/features/settings/components/*设置页 UI 组件(见上文表格)
web/src/features/onboarding/pages/OnboardingPage.vue/welcome 引导页
web/src/features/onboarding/api.ts / types.ts引导 RPC 与类型
web/src/core/i18n/locales/{zh-CN,en-US}/settings.json设置页多语言文案
src/features/settings/settings.hpp/.cpp后端 should_show_onboarding 与设置服务
src/features/settings/types.hpp后端 AppSettings 与引导版本常量
src/core/initializer/initializer.cpp启动期引导判定与窗口打开决策
src/ui/webview_window/webview_window.cpp引导期临时窗口尺寸
src/core/rpc/endpoints/settings/设置 RPC 端点(后端,兄弟页面)
src/core/events/handlers/settings_handlers.*设置事件处理(后端,兄弟页面)

失败模式、边界与一致性

场景行为/风险依据
后端设置尚未同步完成settingsStore.init() 被 await,引导判定一定发生在真实数据之上main.ts 第 42-51 行
用户刷新且已停留在 /welcomeroute.name !== 'welcome' 双条件避免重复 replacemain.ts 第 52 行
引导完成后退按钮使用 router.replace,引导页不入历史栈main.ts 第 53 行
前后端版本常量不一致后端按临时窗口尺寸渲染、前端不跳转 /welcome(或相反),出现窗口/页面错配双端常量镜像结构
外观数组内容变更`overlayColors.join('')` 使数组元素变化可被侦测
首屏主题闪烁applyAppearanceToDocument 在 app.mount 之前调用main.ts 第 56-57 行注释
路由守卫时序守卫必须先注册再 app.use(router),否则首次导航绕过权限main.ts 第 35-36 行注释
引导期重任务进度丢失taskStore.initialize() 先于引导判定执行,确保订阅就绪main.ts 第 45-47 行

并发/时序约束小结

  • 全部初始化步骤串行 await,无并行竞态;代价是启动延迟取决于最慢一步(RPC 建立与设置读取)。
  • watch 在挂载前注册(main.ts 第 60 行起),因此挂载瞬间的任何设置变更也不会漏掉一次外观应用。

扩展点

  • 新增设置分组:在 components/ 下新增 XxxContent.vue 内容组件并接入 SettingsMenuList/SettingsSidebar 的分组清单;文案加入 settings.json(zh-CN 与 en-US 两份)。
  • 升级引导流程:同时提升前后端 CURRENT_ONBOARDING_FLOW_VERSION(web/src/features/settings/types.ts 与 src/features/settings/types.hpp),并在 OnboardingPage.vue 中实现新步骤。
  • 新增引导期一次性任务:参考 initializeMediaHardlinks 的模式——后端注册任务类型,前端经 core/tasks 订阅进度,在引导页或扩展设置中触发。
  • 源码入口:main.ts、OnboardingPage.vue、settings/types.hpp
  • 后端设置服务与 RPC 端点、事件处理:见本仓库对应的后端设置子系统页面
  • RPC 框架与访问等级(initializeRPC / initializeAccessLevel):见 Web 前端基础设施相关页面
  • 后台任务系统(useTaskStore / initializeMediaHardlinks):见任务编排相关页面
  • 用户文档:getting-started、custom-settings

Sources

(1 files)