对照 ledgerly 自文档上次更新以来的全部改动,补充新功能文档、修正品牌与版本残留,并新增下载与安装页;配图已重新截取并上传 OSS,本地截图目录不纳入版本管理。 变更内容: - 新增「智能查数」「账单关系」两个使用文档页,并登记侧边栏与总览 - 设置页扩充为八个面板(账号安全、数据库实时进度、外观、导航等)并改为即时保存 - 收支记录补充关系列、批量操作、AI 搜索浮窗、可配置分页与「账单详情」弹窗 - 统计面板补充饼图/渠道图下钻与筛选双向联动;规则管理补充批量删除 - AI 能力补充智能查数与关系命名;新增下载与安装页 - 开发文档同步:AI 13 类调用、数据模型 24 张表、API、前端偏好、构建部署 - 统一「汇账 Ledgerly」品牌与「设置/日志」菜单文案,版本号对齐 0.1 - 截图脚本新增账单详情等场景并更新登记 说明: - download.md 的 OSS 下载链接保持待发布产物回填,暂不调整 - 已通过 npm run docs:build 与 npm run typecheck:scripts
19 KiB
前端用户偏好与持久化
本文梳理前端「用户偏好」的保存逻辑:为什么保存、保存了哪些偏好、存在哪里、何时读写、目的是什么。
为什么保存
汇账的不少界面是「多 Tab」或「带筛选条件」的,如果每次进入都重置为默认值,用户每次都要重新选一遍语言、筛选项、分页、排序、上次使用的记账方式等,体验割裂。
为此项目在 ledgerly/AGENTS.md 中约定:
多 Tab 或可切换的界面偏好需持久化记忆(
localStorage),下次打开时恢复到上次选择;存储键统一以ledgerly.前缀命名,读取时对非法值回退默认项,写入时忽略存储异常。
据此形成三条实践原则:
- 进入页面即还原:
onMounted或模块初始化时读取上次选择。 - 变化即保存:用
watch监听偏好变量,任一变化自动写入,无需用户点「保存」。 - 容错:读取时校验取值白名单,非法/损坏数据回退默认;写入包在
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/custommonthcustomRange自定义起止时间戳 [number, number]nullgranularity趋势粒度 day/week/month/quarter/yeardayfilterStatsScope统计口径 INCLUDED/ALL/EXCLUDEDINCLUDEDfilterType收支类型(多选) []filterCategory分类(多选, 0表示未分类)[]filterChannel交易渠道(多选) []filterTag标签(多选) []filterMinAmount/filterMaxAmount金额区间 nullfilterRemark备注关键字 '' -
读取:
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]nullfilterIncludeInStats计入统计三态 null/'true'/'false'nullfilterRemark备注关键字 ''filterMinAmount/filterMaxAmount金额区间 nullpageSize每页条数 10/20/50/100/200/50020sortValue排序 transactionTime-desc等 4 种transactionTime-descfilterTag标签( 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()从后端获取。
新增偏好的约定
新增一项用户界面偏好时:
- 存储键常量集中登记在
frontend/src/utils/storageKeys.ts的STORAGE_KEYS,以ledgerly.前缀命名、按页面/模块分段(如ledgerly.transaction.xxx);涉及改名时在LEGACY_STORAGE_KEYS登记旧键并用migrateKey迁移。 - 读写统一经
frontend/src/utils/storage.ts,禁止在业务代码中直接调用localStorage/sessionStorage。 - 优先用
watch自动保存,避免要求用户手动点保存。 - 读取时对取值做白名单校验,非法或损坏数据回退默认项;写入包
try/catch,忽略存储异常。 - 组件内的字符串型偏好用
useStringPreference,关键词历史用useKeywordHistory,浮窗位置尺寸用FloatingPanel(键经storageKey传入)。 - 需要跨设备一致时,改存后端并按当前用户隔离,而非
localStorage。