- 站点标题、首页与文档正文品牌名由「收支管理系统」改为「汇账 Ledgerly」 - package.json 包名改为 ledgerly-docs - 维护脚本环境变量由 FINANCE_* 改为 LEDGERLY_*,测试库路径与示例同步 - 项目结构、构建部署、开发环境等文档中的目录名与 jar 名改为 ledgerly - 保留在线体验域名与 OSS 截图前缀不变
14 KiB
前端用户偏好与持久化
本文梳理前端「用户偏好」的保存逻辑:为什么保存、保存了哪些偏好、存在哪里、何时读写、目的是什么。
为什么保存
汇账的不少界面是「多 Tab」或「带筛选条件」的,如果每次进入都重置为默认值,用户每次都要重新选一遍语言、筛选项、分页、排序、上次使用的记账方式等,体验割裂。
为此项目在 ledgerly/AGENTS.md 中约定:
多 Tab 或可切换的界面偏好需持久化记忆(
localStorage),下次打开时恢复到上次选择;存储键统一以finance.前缀命名,读取时对非法值回退默认项,写入时忽略存储异常。
据此形成三条实践原则:
- 进入页面即还原:
onMounted或模块初始化时读取上次选择。 - 变化即保存:用
watch监听偏好变量,任一变化自动写入,无需用户点「保存」。 - 容错:读取时校验取值白名单,非法/损坏数据回退默认;写入包在
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/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 行)把统计口径与筛选合并写入收支列表的偏好键,并保留分页、排序等既有设置。 -
目的:下次进入统计面板自动还原时间范围、粒度与全部过滤条件。
收支列表筛选偏好 finance.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 相关偏好(收支列表)
| 键名 | 取值 | 说明 |
|---|---|---|
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()从后端获取。
新增偏好的约定
新增一项用户界面偏好时:
- 存储键以
finance.前缀命名,按页面/模块分段(如finance.transaction.xxx)。 - 优先用
watch自动保存,避免要求用户手动点保存。 - 读取时对取值做白名单校验,非法或损坏数据回退默认项。
- 写入包
try/catch,忽略存储异常。 - 需要跨设备一致时,改存后端并按当前用户隔离,而非
localStorage。