Files
ledgerly-docs/dev/preferences.md
T
rosercode 923d6db5c0 docs: 同步汇账最新功能,新增智能查数、账单关系等文档
对照 ledgerly 自文档上次更新以来的全部改动,补充新功能文档、修正品牌与版本残留,并新增下载与安装页;配图已重新截取并上传 OSS,本地截图目录不纳入版本管理。

变更内容:
- 新增「智能查数」「账单关系」两个使用文档页,并登记侧边栏与总览
- 设置页扩充为八个面板(账号安全、数据库实时进度、外观、导航等)并改为即时保存
- 收支记录补充关系列、批量操作、AI 搜索浮窗、可配置分页与「账单详情」弹窗
- 统计面板补充饼图/渠道图下钻与筛选双向联动;规则管理补充批量删除
- AI 能力补充智能查数与关系命名;新增下载与安装页
- 开发文档同步:AI 13 类调用、数据模型 24 张表、API、前端偏好、构建部署
- 统一「汇账 Ledgerly」品牌与「设置/日志」菜单文案,版本号对齐 0.1
- 截图脚本新增账单详情等场景并更新登记

说明:
- download.md 的 OSS 下载链接保持待发布产物回填,暂不调整
- 已通过 npm run docs:build 与 npm run typecheck:scripts
2026-09-26 21:57:38 +08:00

19 KiB
Raw Blame History

前端用户偏好与持久化

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

为什么保存

汇账的不少界面是「多 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.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.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/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.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。
  • 保存:切换某表格每页条数时写入;候选列表与全局默认值在设置 → 外观设置中配置(存后端)。

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/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。