Files
ledgerly-docs/dev/preferences.md
T
rosercode d2e4a51f2b feat: 补充图片记账、交易渠道管理与前端偏好文档
- 新增「交易渠道管理」指南与「前端用户偏好」开发文档,并加入侧边栏导航
- 图片记账:补充多图上传、实时识别进度、结果勾选导入与「来源图片」回看,AI 配置新增视觉模型说明
- 数据模型补充 t_channel、t_ai_config/t_ai_image 等表与「类型+金额+时间」去重键说明,表数更新为 21 张
- API 文档补充 /api/channel、/api/transaction/batch、/api/ai/record/image(/stream)、/api/ai/image、/api/ai/channel 等接口
- 截图脚本新增渠道页、图片记账、来源图片与渠道 AI 辅助截图,并放宽导航超时
2026-09-21 22:19:55 +08:00

14 KiB
Raw Blame History

前端用户偏好与持久化

本文梳理前端「用户偏好」的保存逻辑:为什么保存、保存了哪些偏好、存在哪里、何时读写、目的是什么。

为什么保存

收支管理系统的不少界面是「多 Tab」或「带筛选条件」的,如果每次进入都重置为默认值,用户每次都要重新选一遍语言、筛选项、分页、排序、上次使用的记账方式等,体验割裂。

为此项目在 finance-app/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。