Files
ledgerly-docs/dev/preferences.md
T
rosercode b979c6e498 refactor: 文档站品牌改名为汇账 Ledgerly,站点与脚本标识统一为 ledgerly
- 站点标题、首页与文档正文品牌名由「收支管理系统」改为「汇账 Ledgerly」
- package.json 包名改为 ledgerly-docs
- 维护脚本环境变量由 FINANCE_* 改为 LEDGERLY_*,测试库路径与示例同步
- 项目结构、构建部署、开发环境等文档中的目录名与 jar 名改为 ledgerly
- 保留在线体验域名与 OSS 截图前缀不变
2026-09-23 17:33:59 +08:00

222 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端用户偏好与持久化
本文梳理前端「用户偏好」的保存逻辑:**为什么保存、保存了哪些偏好、存在哪里、何时读写、目的是什么**。
## 为什么保存
汇账的不少界面是「多 Tab」或「带筛选条件」的,如果每次进入都重置为默认值,用户每次都要重新选一遍语言、筛选项、分页、排序、上次使用的记账方式等,体验割裂。
为此项目在 `ledgerly/AGENTS.md` 中约定:
> 多 Tab 或可切换的界面偏好需**持久化记忆**(`localStorage`),下次打开时恢复到上次选择;存储键统一以 `finance.` 前缀命名,读取时对非法值回退默认项,写入时忽略存储异常。
据此形成三条实践原则:
1. **进入页面即还原**:`onMounted` 或模块初始化时读取上次选择。
2. **变化即保存**:用 `watch` 监听偏好变量,任一变化自动写入,无需用户点「保存」。
3. **容错**:读取时校验取值白名单,非法/损坏数据回退默认;写入包在 `try/catch` 中,忽略隐私模式等存储异常。
偏好分三层存储:
| 存储介质 | 生命周期 | 适用场景 |
|---|---|---|
| `localStorage` | 跨会话、跨标签页 | 语言、筛选条件、Tab、历史记录等界面偏好 |
| `sessionStorage` | 仅当前浏览器会话 | 内容区滚动位置(刷新/切换恢复) |
| 后端(按当前用户) | 跟随账号 | 需要跨设备/多端一致的外观与功能配置 |
> 登录凭证 `token` 也存放在 `localStorage`,但它属于**认证状态**而非界面偏好,见文末说明。
## 总览表
### localStorage 偏好
| 键名 | 偏好内容 | 文件 | 保存时机 | 读取时机 |
|---|---|---|---|---|
| `locale` | 界面语言 `zh-CN`/`en-US`/`ja-JP` | `locales/index.ts:16` | 切换语言 | 模块初始化 |
| `finance.dashboard.prefs` | 统计面板时间范围、粒度与 11 项筛选 | `views/Dashboard.vue:470` | 变量 `watch` 自动 | `onMounted` |
| `finance.transaction.filterPrefs` | 收支列表筛选、分页、排序 | `views/TransactionList.vue:369` | 变量 `watch` 自动 | `onMounted` |
| `finance.transaction.aiScope` | 批量 AI 建议范围 | `views/TransactionList.vue:146` | 变量 `watch` 自动 | ref 初始化 |
| `finance.transaction.aiRecordTab` | AI 记账方式(文字/图片) | `views/TransactionList.vue:206` | 变量 `watch` 自动 | ref 初始化 |
| `finance.transaction.aiQueryHistory` | AI 自然语言查询历史 | `views/TransactionList.vue:195` | 提交查询时 | `onMounted` |
| `finance.transaction.remarkHistory` | 备注关键词历史 | `views/TransactionList.vue:300` | 备注失焦/回车 | `onMounted` |
| `finance.dashboard.aiSearchHistory` | 统计面板 AI 查询历史 | `views/Dashboard.vue:91` | 提交 AI 搜索时 | `onMounted` |
| `finance.billImport.billType` | 上次选择的账单来源类型 | `views/BillImportPanel.vue:25` | 切换类型时 | ref 初始化 |
| `finance.transaction.relationType` | 账单关系类型偏好 | `components/TransactionRelationEditorModal.vue:14` | 创建模式切换类型时 | 弹窗初始化 |
| `emailBill.tab` | 邮箱账单页标签页 | `views/EmailBillManage.vue:43` | Tab 切换时 | 路由重定向 + 组件 |
| `logs.tab` | 系统日志页标签页 | `views/Logs.vue:25` | Tab 切换时 | 路由重定向 + 组件 |
> `locale`、`emailBill.tab`、`logs.tab` 是较早的实现,未使用 `finance.` 前缀;前两者分别为国际化与路由重定向的既有约定,改动时需保持键名稳定。
### sessionStorage 偏好
| 键名 | 偏好内容 | 文件 |
|---|---|---|
| `finance.scroll.<路由路径>` | 各页面内容区滚动位置 | `views/Layout.vue:83` |
### 后端按用户同步的偏好
| 接口 | 偏好内容 | 前端入口 |
|---|---|---|
| `GET/PUT /api/setting/excluded-display-style` | 「不计入统计」记录展示样式 | `components/DisplayConfigPanel.vue` |
| `GET/PUT /api/setting/bill-source-include-in-stats` | 各账单来源「计入统计」默认值 | `components/BillSourceStatsPanel.vue` |
| `GET/PUT /api/ai/config` | AI 启用开关、密钥、模型 | `components/AiConfigPanel.vue` |
## localStorage 偏好详解
### 界面语言 `locale`
- **文件**:`frontend/src/locales/index.ts`
- **取值**:`zh-CN`、`en-US`、`ja-JP`
- **读取**:`resolveInitialLocale()`(第 42 行)按「`localStorage` → `navigator.language` → `zh-CN`」优先级解析,作为 `createI18n` 初始语言。
- **保存**:`setLocale()`(第 75 行)在切换语言时写入,并同步 `document.documentElement.lang`。
- **目的**:记住用户语言选择,应用启动与切换即时生效。
- **入口**:`components/LocaleSwitch.vue`。
### 统计面板偏好 `finance.dashboard.prefs`
- **文件**:`frontend/src/views/Dashboard.vue`(键名第 470 行)
- **数据结构**(JSON 对象,保存见 `savePrefs()` 第 499 行):
| 字段 | 含义 | 默认值 |
|---|---|---|
| `rangeType` | 时间范围 `month`/`3month`/`12month`/`custom` | `month` |
| `customRange` | 自定义起止时间戳 `[number, number]` | `null` |
| `granularity` | 趋势粒度 `day`/`week`/`month`/`quarter`/`year` | `day` |
| `filterStatsScope` | 统计口径 `INCLUDED`/`ALL`/`EXCLUDED` | `INCLUDED` |
| `filterType` | 收支类型(多选) | `[]` |
| `filterCategory` | 分类(多选,`0` 表示未分类) | `[]` |
| `filterChannel` | 交易渠道(多选) | `[]` |
| `filterTag` | 标签(多选) | `[]` |
| `filterMinAmount` / `filterMaxAmount` | 金额区间 | `null` |
| `filterRemark` | 备注关键字 | `''` |
- **读取**:`loadPrefs()`(第 474 行)在 `onMounted`(第 699 行)调用,逐项校验白名单;兼容旧字段 `filterPaymentMethod`(第 486 行)。
- **保存**:`watch` 监听上述变量,任一变化调用 `savePrefs()`。
- **跨页写入**:点击趋势图跳转收支列表时,`applyTrendNavigation()`(第 381 行)把统计口径与筛选**合并写入**收支列表的偏好键,并保留分页、排序等既有设置。
- **目的**:下次进入统计面板自动还原时间范围、粒度与全部过滤条件。
### 收支列表筛选偏好 `finance.transaction.filterPrefs`
- **文件**:`frontend/src/views/TransactionList.vue`(键名第 369 行)
- **数据结构**(保存见 `saveFilterPrefs()` 第 396 行):
| 字段 | 含义 | 默认值 |
|---|---|---|
| `filterType` | 收支类型 | `[]` |
| `filterCategory` | 分类(`0` 表示未分类) | `[]` |
| `filterChannel` | 交易渠道 | `[]` |
| `filterDateRange` | 日期范围 `[number, number]` | `null` |
| `filterIncludeInStats` | 计入统计三态 `null`/`'true'`/`'false'` | `null` |
| `filterRemark` | 备注关键字 | `''` |
| `filterMinAmount` / `filterMaxAmount` | 金额区间 | `null` |
| `pageSize` | 每页条数 `10`/`20`/`50`/`100`/`200`/`500` | `20` |
| `sortValue` | 排序 `transactionTime-desc` 等 4 种 | `transactionTime-desc` |
| `filterTag` | 标签(`0` 表示无标签) | `[]` |
- **读取**:`loadFilterPrefs()`(第 372 行)在 `onMounted`(第 2030 行)调用;对 `pageSize` 校验白名单、对 `sortValue` 校验选项,兼容旧字段 `filterPaymentMethod`。
- **保存**:`watch` 深度监听 11 个变量,任一变化自动保存(第 417 行)。
- **目的**:记住筛选条件、每页条数与排序方式,下次进入自动还原。
### AI 相关偏好(收支列表)
| 键名 | 取值 | 说明 |
|---|---|---|
| `finance.transaction.aiScope` | `BOTH`/`CATEGORY`/`TAG` | 批量 AI 建议范围,非法值回退 `BOTH`(第 146 行) |
| `finance.transaction.aiRecordTab` | `text`/`image` | AI 记账方式,非法值回退 `text`(第 206 行) |
| `finance.transaction.aiQueryHistory` | 字符串数组(≤10) | AI 自然语言查询历史,最新在前 |
| `finance.transaction.remarkHistory` | 字符串数组(≤10) | 备注关键词历史,失焦/回车记录 |
- **`aiRecordTab`** 是 `AGENTS.md` 点名的参考实现:记住用户上次用的记账方式(文字或图片),下次打开 AI 记账弹窗直接落到该 Tab。
- **关键词历史**统一由 `composables/useKeywordHistory.ts` 管理:去重(忽略大小写)、最新在前、上限 10 条,损坏数据自动忽略,下拉末尾提供「清空历史」项。
- 读取:`load()`;记录:`record()`;清空:`clear()`。
- 三处使用:AI 查询历史、备注历史(均在 `TransactionList.vue`)、统计面板 AI 搜索历史(`Dashboard.vue`,键 `finance.dashboard.aiSearchHistory`)。
### 账单导入类型 `finance.billImport.billType`
- **文件**:`frontend/src/views/BillImportPanel.vue`(第 25 行)
- **取值**:账单来源编码(`ALIPAY`、`WECHAT`、`CMB` 等)
- **读取**:`readBillTypePref()`(第 62 行)作为初始值;`loadBillTypes()` 拿到后端列表后校正,非法值回退首个来源。
- **保存**:`watch(billType)`(第 226 行)在切换类型时写入,同时清空已选文件,避免格式与类型不匹配。
- **目的**:记住上次选择的账单来源。
### 账单关系类型 `finance.transaction.relationType`
- **文件**:`frontend/src/components/TransactionRelationEditorModal.vue`(第 14 行)
- **取值**:`PARENT_CHILD`(默认)、`SIBLING`
- **读取**:`loadPreferredRelationType()`(第 81 行)在新建弹窗初始化时调用。
- **保存**:`watch(editorType)`(第 69 行)**仅在创建模式**(`editingGroupId === null`)写入,编辑既有关系组不覆盖偏好。
- **目的**:新建关系组时默认沿用上次的关系类型。
### 页面标签页 `emailBill.tab` / `logs.tab`
- **文件**:`views/EmailBillManage.vue:43`、`views/Logs.vue:25`
- **取值**:邮箱页 `configs`/`senderRules`/`messages`;日志页 `http`/`login`/`app`
- **读取**:路由 `/email-bills`、`/logs` 的 `redirect` 读取偏好并拼接子路径(`router/index.ts:69`、第 93 行),带白名单校验,无记录回退默认。
- **保存**:`watch(activeTab, ..., { immediate: true })` 在 Tab 切换时写入。
- **目的**:下次点击「邮箱账单」「系统日志」回到上次标签页。
- **说明**:这两个是 `AGENTS.md` 点名的参考实现,但键名未加 `finance.` 前缀,属早期实现。
## sessionStorage 偏好:滚动位置
- **文件**:`frontend/src/views/Layout.vue`(前缀第 83 行,键为 `finance.scroll.<路由路径>`)
- **保存**:内容区 `scroll` 事件经 `requestAnimationFrame` 节流写入;路由切换前保存离开页;`beforeunload` 兜底(第 172 行)。
- **读取**:`restoreScroll()`(第 139 行)在 `onMounted` 与路由切换后恢复;因页面内容异步加载、切换后高度可能不足,用 `rAF` 轮询等待内容高度足够再定位,2 秒超时后按当前可达最大值定位。
- **目的**:刷新或页面切换后恢复阅读位置;仅当前会话有效,关闭浏览器即失效。
## 后端按用户同步的偏好
以下偏好需要跟随账号(换设备/多端一致),因此存后端,按当前用户隔离,前端通过设置页读写。
### 「不计入统计」展示样式
- **接口**:`GET /api/setting/excluded-display-style`、`PUT /api/setting/excluded-display-style`(`api/setting.ts:14`)
- **取值**:`strikethrough`(默认,整行删除线)、`muted`(灰色标签)
- **前端**:设置页 `components/DisplayConfigPanel.vue` 编辑保存;收支列表 `TransactionList.vue:647` 在 `onMounted` 读取并应用到「不计入统计」记录(失败保持默认删除线)。
- **目的**:统一用户对「不计入统计」记录的外观选择。
### 账单来源「计入统计」默认值
- **接口**:`GET/PUT /api/setting/bill-source-include-in-stats`(`api/setting.ts:4`)
- **数据结构**:`{ [账单来源编码]: boolean }`
- **前端**:设置页 `components/BillSourceStatsPanel.vue` 编辑保存(可点整行或开关切换)。
- **目的**:按账单来源设置导入时默认是否计入统计,仅影响统计口径,不改变导入结果。
### AI 配置
- **接口**:`GET /api/ai/config`、`PUT /api/ai/config`(`api/ai.ts:37`)
- **数据结构**:`{ enabled, apiKey, hasApiKey, baseUrl, model, visionModel }`,`apiKey` 留空表示不修改原值。
- **前端**:设置页 `components/AiConfigPanel.vue` 编辑保存。
- **目的**:用户级 AI 能力配置;其他页面的 AI 元素统一经 `getAiConfig().enabled` 控制显隐。
> 严格说 AI 配置属「功能配置」而非纯界面偏好,但因按用户存储,一并列出。设置页中的数据库配置、注册开关、定时通知为**系统级全局配置**,不在此列。
## 未实现的偏好
以下常见界面偏好当前**没有**持久化实现,改动时无需假设其存在:
- **主题 / 暗色模式**:无主题存储键,界面主题固定。
- **侧边栏折叠 / 展开**:`Layout.vue` 使用固定 `NLayoutSider`,无折叠状态。
- **表格列显示 / 隐藏 / 顺序**:各页列配置为固定 `computed`。
- **货币 / 金额格式**:无货币偏好。
- **独立日期格式**:仅由语言 `locale` 间接驱动 Naive UI 日期本地化,无单独偏好。
- **全局分页大小**:仅收支列表的 `pageSize` 持久化;日志、规则、邮箱账单等页面分页大小不持久化。
- **IndexedDB / cookie**:未使用。
- **Pinia 持久化插件 / vueuse `useStorage`**:未安装、未使用。
## 附:登录凭证 `token`
- **文件**:`frontend/src/store/user.ts`(第 8 行)
- **保存**:`setToken()` 写入 `localStorage`;`logout()` 移除。
- **使用**:`api/request.ts` 请求拦截器读取并写入 `Authorization` 头。
- **说明**:这是认证状态,**不属于界面偏好**,列出以免混淆。`nickname`、`userId` 不持久化,每次由 `fetchUserInfo()` 从后端获取。
## 新增偏好的约定
新增一项用户界面偏好时:
1. 存储键以 `finance.` 前缀命名,按页面/模块分段(如 `finance.transaction.xxx`)。
2. 优先用 `watch` 自动保存,避免要求用户手动点保存。
3. 读取时对取值做白名单校验,非法或损坏数据回退默认项。
4. 写入包 `try/catch`,忽略存储异常。
5. 需要跨设备一致时,改存后端并按当前用户隔离,而非 `localStorage`。