为规则、分类、标签管理页新增的「批量操作模式」与多选删除补充使用与开发文档,同时补上此前已完成但未同步的 AI 规则建议可编辑、编辑后实时重算去重等文档,并登记批量模式截图脚本。 变更内容: - guide/categories-tags.md:补充分类与标签的批量删除说明,并回填 tags-batch.png、categories-batch.png 配图。 - guide/rules.md:批量删除与应用匹配账单改为需先进入「批量操作」模式(默认不显示勾选框)。 - dev/api.md:新增 DELETE /api/tag/batch、/api/category/batch,并补齐规则命中账单预览/一键应用、AI 规则去重校验等接口。 - dev/data-model.md:说明标签删除清理交易标签与规则标签关联、分类删除保留逻辑引用(账单显示未分类)。 - dev/rule-engine.md、dev/ai.md、dev/preferences.md、guide/ai.md、guide/transactions.md:同步 AI 规则建议可编辑、编辑后实时重算去重、应用开关默认开启并持久化等说明。 - scripts/ts/capture-elements.ts:新增 captureBatchModes()(标签/分类批量模式截图)并纳入 main()。 说明: - 截图经测试环境(前端 5174 / 后端 8081,虚构数据)生成,已上传 OSS,正文以 OSS 长期地址引用;public/screenshots 目录不入库。
293 lines
22 KiB
Markdown
293 lines
22 KiB
Markdown
# 前端用户偏好与持久化
|
||
|
||
本文梳理前端「用户偏好」的保存逻辑:**为什么保存、保存了哪些偏好、存在哪里、何时读写、目的是什么**。
|
||
|
||
## 为什么保存
|
||
|
||
汇账的不少界面是「多 Tab」或「带筛选条件」的,如果每次进入都重置为默认值,用户每次都要重新选一遍语言、筛选项、分页、排序、上次使用的记账方式等,体验割裂。
|
||
|
||
为此项目在 `ledgerly/AGENTS.md` 中约定:
|
||
|
||
> 多 Tab 或可切换的界面偏好需**持久化记忆**(`localStorage`),下次打开时恢复到上次选择;存储键统一以 `ledgerly.` 前缀命名,读取时对非法值回退默认项,写入时忽略存储异常。
|
||
|
||
据此形成三条实践原则:
|
||
|
||
1. **进入页面即还原**:`onMounted` 或模块初始化时读取上次选择。
|
||
2. **变化即保存**:用 `watch` 监听偏好变量,任一变化自动写入,无需用户点「保存」。
|
||
3. **容错**:读取时校验取值白名单,非法/损坏数据回退默认;写入包在 `try/catch` 中,忽略隐私模式等存储异常。
|
||
|
||
偏好分三层存储:
|
||
|
||
| 存储介质 | 生命周期 | 适用场景 |
|
||
|---|---|---|
|
||
| `localStorage` | 跨会话、跨标签页 | 语言、筛选条件、Tab、历史记录等界面偏好 |
|
||
| `sessionStorage` | 仅当前浏览器会话 | 内容区滚动位置(刷新/切换恢复) |
|
||
| 后端(按当前用户) | 跟随账号 | 需要跨设备/多端一致的外观与功能配置 |
|
||
|
||
> 登录凭证 `token` 也存放在 `localStorage`,但它属于**认证状态**而非界面偏好,见文末说明。
|
||
|
||
## 总览表
|
||
|
||
### localStorage 偏好
|
||
|
||
| 键名 | 偏好内容 | 文件 | 保存时机 | 读取时机 |
|
||
|---|---|---|---|---|
|
||
| `ledgerly.locale` | 界面语言 `zh-CN`/`en-US`/`ja-JP` | `locales/index.ts` | 切换语言 | 模块初始化 |
|
||
| `ledgerly.calendar.view` | 日历视图 `month`/`year` | `views/Calendar.vue` | `useStringPreference` 自动 | 组件初始化 |
|
||
| `ledgerly.calendar.year` | 日历最近浏览年份(四位数字符串) | `views/Calendar.vue` | 变量 `watch` 自动 | 组件初始化 |
|
||
| `ledgerly.calendar.month` | 日历月视图最近浏览月份(`1`-`12`) | `views/Calendar.vue` | 变量 `watch` 自动 | 组件初始化 |
|
||
| `ledgerly.calendar.statsScope` | 日历统计口径 `INCLUDED`/`ALL`/`EXCLUDED` | `views/Calendar.vue` | `useStringPreference` 自动 | 组件初始化 |
|
||
| `ledgerly.dashboard.prefs` | 统计面板时间范围、粒度与多项筛选 | `views/Dashboard.vue` | 变量 `watch` 自动 | `onMounted` |
|
||
| `ledgerly.transaction.filterPrefs` | 收支列表筛选、分页、排序 | `views/TransactionList.vue` | 变量 `watch` 自动 | `onMounted` |
|
||
| `ledgerly.transaction.aiScope` | 批量 AI 建议范围 | `views/TransactionList.vue` | 变量 `watch` 自动 | ref 初始化 |
|
||
| `ledgerly.transaction.aiRecordTab` | AI 记账方式(文字/图片) | `views/TransactionList.vue` | 变量 `watch` 自动 | ref 初始化 |
|
||
| `ledgerly.transaction.aiRuleApplyToSelection` | AI 生成规则时是否同时应用到勾选账单(`true`/`false`,默认 `true`) | `components/AiRuleSuggestModal.vue` | `useStringPreference` 自动 | 弹窗初始化 |
|
||
| `ledgerly.transaction.aiQueryHistory` | AI 自然语言查询历史 | `views/TransactionList.vue` / `components/AiQueryWindow.vue` | 提交查询时 | 浮窗聚焦时 |
|
||
| `ledgerly.dashboard.aiSearchHistory` | 统计面板 AI 查询历史 | `views/Dashboard.vue` | 提交 AI 搜索时 | 浮窗聚焦时 |
|
||
| `ledgerly.transaction.aiQueryWindow` | 收支记录 AI 查询浮窗位置与尺寸 | `components/FloatingPanel.vue` | 拖拽/缩放结束时 | 浮窗初始化 |
|
||
| `ledgerly.dashboard.aiQueryWindow` | 统计面板 AI 搜索浮窗位置与尺寸 | `components/FloatingPanel.vue` | 拖拽/缩放结束时 | 浮窗初始化 |
|
||
| `ledgerly.transaction.remarkHistory` | 备注关键词历史 | `views/TransactionList.vue` | 备注失焦/回车 | `onMounted` |
|
||
| `ledgerly.billImport.billType` | 上次选择的账单来源类型 | `views/BillImportPanel.vue` | 切换类型时 | ref 初始化 |
|
||
| `ledgerly.transaction.relationType` | 账单关系类型偏好 | `components/TransactionRelationEditorModal.vue` | 创建模式切换类型时 | 弹窗初始化 |
|
||
| `ledgerly.pagination.tablePageSizes` | 各表格分别记住的每页条数 | `utils/tablePageSizePrefs.ts` | 切换每页条数时 | 表格初始化 |
|
||
| `ledgerly.emailBill.tab` | 邮箱账单页标签页 | `views/EmailBillManage.vue` | Tab 切换时 | 路由重定向 + 组件 |
|
||
| `ledgerly.logs.tab` | 日志页标签页 | `views/Logs.vue` | Tab 切换时 | 路由重定向 + 组件 |
|
||
|
||
> `locale`、`emailBill.tab`、`logs.tab` 是较早的实现,最初未加前缀,现已统一为 `ledgerly.` 前缀:在 `LEGACY_STORAGE_KEYS` 登记旧键、读取前用 `migrateKey` 一次性迁移;品牌改名(`finance.` → `ledgerly.`)的旧键由 `migrateLegacyPrefixedKeys()` 在模块加载时批量迁移,新键已存在时不覆盖。
|
||
|
||
### sessionStorage 偏好
|
||
|
||
| 键名 | 偏好内容 | 文件 |
|
||
|---|---|---|
|
||
| `ledgerly.scroll.<路由路径>` | 各页面内容区滚动位置 | `views/Layout.vue` |
|
||
| `ledgerly.sqlQuery.session` | 智能查数当前会话的提问、结果与视图(结果超 500 行时只存提问与视图) | `views/SqlQuery.vue` |
|
||
|
||
### 后端按用户同步的偏好
|
||
|
||
| 接口 | 偏好内容 | 前端入口 |
|
||
|---|---|---|
|
||
| `GET/PUT /api/setting/excluded-display-style` | 「不计入统计」记录展示样式 | `components/DisplayConfigPanel.vue` |
|
||
| `GET/PUT /api/setting/show-hidden-data` | 是否显示已隐藏备注的记录 | `components/DisplayConfigPanel.vue` |
|
||
| `GET/PUT /api/setting/chart-click-navigation` | 统计面板图表点击是否跳转明细 | `components/DisplayConfigPanel.vue` / `views/Dashboard.vue` |
|
||
| `GET/PUT /api/setting/pagination-prefs` | 表格分页候选与默认每页条数 | `components/DisplayConfigPanel.vue` |
|
||
| `GET/PUT /api/setting/nav-menu` | 左侧导航顺序与显隐 | `components/NavMenuConfigPanel.vue` |
|
||
| `GET/PUT /api/setting/bill-source-include-in-stats` | 各账单来源「计入统计」默认值 | `components/BillSourceStatsPanel.vue` |
|
||
| `GET/PUT /api/ai/config` | AI 启用开关、密钥、模型 | `components/AiConfigPanel.vue` |
|
||
|
||
## localStorage 偏好详解
|
||
|
||
### 界面语言 `ledgerly.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`。
|
||
|
||
### 日历偏好 `ledgerly.calendar.view` / `ledgerly.calendar.year` / `ledgerly.calendar.month` / `ledgerly.calendar.statsScope`
|
||
|
||
- **文件**:`frontend/src/views/Calendar.vue`
|
||
- **取值**:
|
||
- `ledgerly.calendar.view`:`month`(月视图,默认)/ `year`(年视图);非法值回退 `month`。
|
||
- `ledgerly.calendar.year`:最近浏览的年份(四位数字符串),月视图面板年份与年视图年份共用;非法值回退当前年。
|
||
- `ledgerly.calendar.month`:月视图最近浏览的月份(`1`-`12`);非法值回退当前月。
|
||
- `ledgerly.calendar.statsScope`:`INCLUDED`(仅计入统计,默认)/ `ALL`(全部记录)/ `EXCLUDED`(仅不计入);非法值回退 `INCLUDED`。
|
||
- **读取**:组件初始化时经 `useStringPreference` 读取;URL 中的 `view`/`y`/`m`(如 `?view=year&y=2026`)优先于偏好,无参数时按偏好恢复上次视图、年月与统计口径。
|
||
- **保存**:`view`/`statsScope` 由 `useStringPreference` 内部 `watch` 自动写入;`year`/`month` 由组件内 `watch(panelYear/panelMonth)`(月视图)与 `watch(yearViewYear)`(年视图)写入。
|
||
- **目的**:下次进入日历自动还原上次的月/年视图、年月与统计口径。
|
||
|
||
### 统计面板偏好 `ledgerly.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 行)把统计口径与筛选**合并写入**收支列表的偏好键,并保留分页、排序等既有设置。
|
||
- **目的**:下次进入统计面板自动还原时间范围、粒度与全部过滤条件。
|
||
|
||
### 收支列表筛选偏好 `ledgerly.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 相关偏好(收支列表)
|
||
|
||
| 键名 | 取值 | 说明 |
|
||
|---|---|---|
|
||
| `ledgerly.transaction.aiScope` | `BOTH`/`CATEGORY`/`TAG` | 批量 AI 建议范围,非法值回退 `BOTH`(第 146 行) |
|
||
| `ledgerly.transaction.aiRecordTab` | `text`/`image` | AI 记账方式,非法值回退 `text`(第 206 行) |
|
||
| `ledgerly.transaction.aiQueryHistory` | 字符串数组(≤10) | AI 自然语言查询历史,最新在前 |
|
||
| `ledgerly.transaction.remarkHistory` | 字符串数组(≤10) | 备注关键词历史,失焦/回车记录 |
|
||
|
||
- **`aiRecordTab`** 是 `AGENTS.md` 点名的参考实现:记住用户上次用的记账方式(文字或图片),下次打开 AI 记账弹窗直接落到该 Tab。
|
||
- **关键词历史**统一由 `composables/useKeywordHistory.ts` 管理:去重(忽略大小写)、最新在前、上限 10 条,损坏数据自动忽略,下拉末尾提供「清空历史」项。
|
||
- 读取:`load()`;记录:`record()`;清空:`clear()`。
|
||
- 三处使用:AI 查询历史、备注历史(均在 `TransactionList.vue`)、统计面板 AI 搜索历史(`Dashboard.vue`,键 `ledgerly.dashboard.aiSearchHistory`)。
|
||
|
||
### 账单导入类型 `ledgerly.billImport.billType`
|
||
|
||
- **文件**:`frontend/src/views/BillImportPanel.vue`(第 25 行)
|
||
- **取值**:账单来源编码(`ALIPAY`、`WECHAT`、`CMB` 等)
|
||
- **读取**:`readBillTypePref()`(第 62 行)作为初始值;`loadBillTypes()` 拿到后端列表后校正,非法值回退首个来源。
|
||
- **保存**:`watch(billType)`(第 226 行)在切换类型时写入,同时清空已选文件,避免格式与类型不匹配。
|
||
- **目的**:记住上次选择的账单来源。
|
||
|
||
### 账单关系类型 `ledgerly.transaction.relationType`
|
||
|
||
- **文件**:`frontend/src/components/TransactionRelationEditorModal.vue`(第 14 行)
|
||
- **取值**:`PARENT_CHILD`(默认)、`SIBLING`
|
||
- **读取**:`loadPreferredRelationType()`(第 81 行)在新建弹窗初始化时调用。
|
||
- **保存**:`watch(editorType)`(第 69 行)**仅在创建模式**(`editingGroupId === null`)写入,编辑既有关系组不覆盖偏好。
|
||
- **目的**:新建关系组时默认沿用上次的关系类型。
|
||
|
||
### 页面标签页 `ledgerly.emailBill.tab` / `ledgerly.logs.tab`
|
||
|
||
- **文件**:`views/EmailBillManage.vue`、`views/Logs.vue`
|
||
- **取值**:邮箱页 `configs`/`senderRules`/`messages`;日志页 `http`/`login`/`app`
|
||
- **读取**:路由 `/email-bills`、`/logs` 的 `redirect` 读取偏好并拼接子路径(`router/index.ts`),带白名单校验,无记录回退默认。
|
||
- **保存**:`watch(activeTab, ..., { immediate: true })` 在 Tab 切换时写入。
|
||
- **目的**:下次点击「邮箱账单」「日志」回到上次标签页。
|
||
- **说明**:这两个是 `AGENTS.md` 点名的参考实现,早期未加前缀,现已统一为 `ledgerly.` 前缀并做旧键迁移。
|
||
|
||
### AI 查询浮窗位置 `ledgerly.transaction.aiQueryWindow` / `ledgerly.dashboard.aiQueryWindow`
|
||
|
||
- **文件**:`components/FloatingPanel.vue`(键经 `storageKey` 传入,由 `AiQueryWindow.vue` 复用)
|
||
- **取值**:`{ x, y, width, height }`,非法值回退居中与默认尺寸(默认 560 × 360)。
|
||
- **保存**:拖拽或缩放结束时写入;读取在浮窗初始化时。
|
||
- **目的**:记住 AI 查询浮窗上次的位置与大小,下次打开保持原样。
|
||
|
||
### 各表格每页条数 `ledgerly.pagination.tablePageSizes`
|
||
|
||
- **文件**:`frontend/src/utils/tablePageSizePrefs.ts`、`composables/useTablePageSize.ts`
|
||
- **取值**:`Record<tableKey, number>`,按表格键(`transaction`、`categoryExpense`、`rule`、`logsHttp`、`smart-query` 等)分别记录。
|
||
- **优先级**:本机记录(且须在当前分页候选中)> 全局默认值(后端 `paginationPrefs`)> 20。
|
||
- **保存**:切换某表格每页条数时写入;候选列表与全局默认值在[设置 → 外观设置](/guide/settings#外观设置)中配置(存后端)。
|
||
|
||
## sessionStorage 偏好:滚动位置
|
||
|
||
- **文件**:`frontend/src/views/Layout.vue`(前缀第 83 行,键为 `ledgerly.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` 编辑保存(可点整行或开关切换)。
|
||
- **目的**:按账单来源设置导入时默认是否计入统计,仅影响统计口径,不改变导入结果。
|
||
|
||
### 显示已隐藏备注的记录
|
||
|
||
- **接口**:`GET/PUT /api/setting/show-hidden-data`(`api/setting.ts`)
|
||
- **数据结构**:`{ show: boolean }`,默认 `false`。
|
||
- **存储**:`t_user_preference` 的 `pref_key = showHiddenData`,值以 `"true"`/`"false"` 保存。
|
||
- **前端**:设置页 `components/DisplayConfigPanel.vue` 切换即存;收支列表与 Excel 导出据此过滤(关闭时排除 `remark_hidden = true` 的记录),统计备注搜索不受影响。
|
||
- **目的**:统一控制「已隐藏备注的记录」是否出现在列表与导出中。
|
||
|
||
### 图表点击跳转明细
|
||
|
||
- **接口**:`GET/PUT /api/setting/chart-click-navigation`(`api/setting.ts`)
|
||
- **数据结构**:`{ enabled: boolean }`,默认 `true`(开启)。
|
||
- **存储**:`t_user_preference` 的 `pref_key = chartClickNavigation`,值以 `"true"`/`"false"` 保存;未配置时读取默认开启。
|
||
- **前端**:共享单例 `composables/useChartClickNavigation.ts`(`chartClickEnabled` + `ensureChartClickEnabledLoaded` / `refreshChartClickEnabled`,读取失败回退默认开启);设置页 `components/DisplayConfigPanel.vue` 与统计面板 `views/Dashboard.vue` 筛选区两处开关读写同一状态、即时联动。关闭后 `Dashboard.vue` 的点击跳转入口(`triggerChartNavigation` / `handleTrendBlankClick`)直接返回,并重绘图表以同步光标、隐藏点击提示。
|
||
- **目的**:允许用户关闭统计面板图表的点击下钻交互,跨设备生效。
|
||
|
||
### 表格分页偏好
|
||
|
||
- **接口**:`GET/PUT /api/setting/pagination-prefs`(`api/setting.ts`)
|
||
- **数据结构**:`{ options: number[], defaultSize: number }`,默认候选 `[10, 20, 50, 100]`、默认 20;后端会规范化(去重升序、1–1000、最多 10 项)。
|
||
- **存储**:`t_user_preference` 的 `pref_key = paginationPrefs`(JSON)。
|
||
- **前端**:设置页 `components/DisplayConfigPanel.vue` 编辑、防抖 500ms 自动保存;`composables/usePageSizeOptions.ts` 提供全局单例,`useTablePageSize(tableKey)` 读取各表记忆值。
|
||
- **目的**:统一所有表格的分页候选与默认每页条数。
|
||
|
||
### 导航顺序与显隐
|
||
|
||
- **接口**:`GET/PUT /api/setting/nav-menu`(`api/setting.ts`)
|
||
- **数据结构**:`{ items: [{ key, visible }] }`,未配置时以前端 `utils/navItems.ts` 的默认顺序与可见性为准。
|
||
- **存储**:`t_user_preference` 的 `pref_key = navMenu`(JSON)。
|
||
- **前端**:设置页 `components/NavMenuConfigPanel.vue`(`vuedraggable` 拖拽 + 开关,600ms 防抖保存、可恢复默认);`composables/useNavMenu.ts` 与后端合并,`Layout.vue` 据此渲染。隐藏仅移除菜单入口,路由仍可访问。
|
||
- **目的**:按账号自定义左侧导航的顺序与显隐,跨设备生效。
|
||
|
||
### AI 配置
|
||
|
||
- **接口**:`GET /api/ai/config`、`PUT /api/ai/config`(`api/ai.ts`)
|
||
- **数据结构**:`{ enabled, apiKey, hasApiKey, baseUrl, model, visionModel }`,`apiKey` 留空表示不修改原值。
|
||
- **前端**:设置页 `components/AiConfigPanel.vue` 编辑保存。
|
||
- **目的**:用户级 AI 能力配置;其他页面的 AI 元素统一经 `getAiConfig().enabled` 控制显隐。
|
||
|
||
> 严格说 AI 配置属「功能配置」而非纯界面偏好,但因按用户存储,一并列出。设置页中的数据库配置、注册开关、定时通知为**系统级全局配置**,不在此列。
|
||
|
||
## 未实现的偏好
|
||
|
||
以下常见界面偏好当前**没有**持久化实现,改动时无需假设其存在:
|
||
|
||
- **主题 / 暗色模式**:无主题存储键,界面主题固定。
|
||
- **侧边栏整体折叠 / 展开**:`Layout.vue` 使用固定 `NLayoutSider`,无折叠状态(但菜单项的**顺序与显隐**可配置,见上「导航顺序与显隐」)。
|
||
- **表格列显示 / 隐藏 / 顺序**:各页列配置为固定 `computed`。
|
||
- **货币 / 金额格式**:无货币偏好。
|
||
- **独立日期格式**:仅由语言 `locale` 间接驱动 Naive UI 日期本地化,无单独偏好。
|
||
- **IndexedDB / cookie**:未使用。
|
||
- **Pinia 持久化插件 / vueuse `useStorage`**:未安装、未使用。
|
||
|
||
## 附:登录凭证 `token`
|
||
|
||
- **文件**:`frontend/src/store/user.ts`(第 8 行)
|
||
- **保存**:`setToken()` 写入 `localStorage`;`logout()` 移除。
|
||
- **使用**:`api/request.ts` 请求拦截器读取并写入 `Authorization` 头。
|
||
- **说明**:这是认证状态,**不属于界面偏好**,列出以免混淆。`nickname`、`userId` 不持久化,每次由 `fetchUserInfo()` 从后端获取。
|
||
|
||
## 新增偏好的约定
|
||
|
||
新增一项用户界面偏好时:
|
||
|
||
1. 存储键常量集中登记在 `frontend/src/utils/storageKeys.ts` 的 `STORAGE_KEYS`,以 `ledgerly.` 前缀命名、按页面/模块分段(如 `ledgerly.transaction.xxx`);涉及改名时在 `LEGACY_STORAGE_KEYS` 登记旧键并用 `migrateKey` 迁移。
|
||
2. 读写统一经 `frontend/src/utils/storage.ts`,**禁止**在业务代码中直接调用 `localStorage`/`sessionStorage`。
|
||
3. 优先用 `watch` 自动保存,避免要求用户手动点保存。
|
||
4. 读取时对取值做白名单校验,非法或损坏数据回退默认项;写入包 `try/catch`,忽略存储异常。
|
||
5. 组件内的字符串型偏好用 `useStringPreference`,关键词历史用 `useKeywordHistory`,浮窗位置尺寸用 `FloatingPanel`(键经 `storageKey` 传入)。
|
||
6. 需要跨设备一致时,改存后端并按当前用户隔离,而非 `localStorage`。
|