refactor: 前端本地偏好统一为公共存储层

- 新增 frontend/src/utils/storageKeys.ts 集中维护存储键常量并登记旧键迁移
- 新增 frontend/src/utils/storage.ts 统一 localStorage/sessionStorage 读写与异常容错
- 新增 frontend/src/composables/usePreference.ts 提供 useStringPreference 响应式字符串偏好
- 新增 frontend/src/utils/transactionFilterPrefs.ts 抽出收支筛选偏好字段定义与共享读写
- 存储键统一以 finance. 前缀命名,locale、emailBill.tab、logs.tab 迁移到新键并自动迁移旧值
- 各页面与组件改用公共层,移除重复的 try/catch 与跨组件重复的序列化逻辑
- 认证凭据 token 属登录状态,保持原键名不纳入 finance. 前缀
This commit is contained in:
2026-09-23 09:41:55 +08:00
parent 66d6ab86b5
commit eb358d0ba1
16 changed files with 396 additions and 242 deletions
+83
View File
@@ -0,0 +1,83 @@
// 本地存储工具:统一 localStorage / sessionStorage 的读写与容错,避免各处重复 try/catch
/** 读取字符串,键不存在或读取异常时回退 fallback */
export function readString(key: string, fallback = ''): string {
try {
return localStorage.getItem(key) ?? fallback
} catch (e) {
return fallback
}
}
/** 读取 JSON,解析失败或读取异常时回退 fallback */
export function readJson<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(key)
return raw ? (JSON.parse(raw) as T) : fallback
} catch (e) {
return fallback
}
}
/** 写入字符串,忽略存储异常(如隐私模式) */
export function writeString(key: string, value: string): void {
try {
localStorage.setItem(key, value)
} catch (e) {
// 忽略写入失败
}
}
/** 写入 JSON,忽略存储异常 */
export function writeJson(key: string, value: unknown): void {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch (e) {
// 忽略写入失败
}
}
/** 删除键,忽略存储异常 */
export function removeKey(key: string): void {
try {
localStorage.removeItem(key)
} catch (e) {
// 忽略删除失败
}
}
/** 读取 sessionStorage 字符串,读取异常时回退 fallback */
export function readSessionString(key: string, fallback = ''): string {
try {
return sessionStorage.getItem(key) ?? fallback
} catch (e) {
return fallback
}
}
/** 写入 sessionStorage 字符串,忽略存储异常 */
export function writeSessionString(key: string, value: string): void {
try {
sessionStorage.setItem(key, value)
} catch (e) {
// 忽略写入失败
}
}
/**
* 迁移历史存储键:新键不存在且旧键存在时,将旧值搬运到新键并删除旧键。
*
* :param oldKey: 历史键
* :param newKey: 当前键
*/
export function migrateKey(oldKey: string, newKey: string): void {
try {
if (localStorage.getItem(newKey) !== null) return
const legacy = localStorage.getItem(oldKey)
if (legacy === null) return
localStorage.setItem(newKey, legacy)
localStorage.removeItem(oldKey)
} catch (e) {
// 忽略迁移失败,保持旧键可用
}
}
+47
View File
@@ -0,0 +1,47 @@
// 本地存储键常量:界面/用户偏好统一以 finance. 前缀命名,作为唯一来源供路由与各页面复用
/** 当前使用的本地存储键 */
export const STORAGE_KEYS = {
// 语言偏好
locale: 'finance.locale',
// 邮箱账单页标签页
emailBillTab: 'finance.emailBill.tab',
// 系统日志页标签页
logsTab: 'finance.logs.tab',
// 账单导入的账单类型
billImportBillType: 'finance.billImport.billType',
// 统计面板偏好
dashboardPrefs: 'finance.dashboard.prefs',
// 统计面板 AI 查询历史
dashboardAiSearchHistory: 'finance.dashboard.aiSearchHistory',
// 统计面板 AI 查询浮窗位置尺寸
dashboardAiQueryWindow: 'finance.dashboard.aiQueryWindow',
// 收支记录 AI 建议范围
transactionAiScope: 'finance.transaction.aiScope',
// 收支记录 AI 记账方式
transactionAiRecordTab: 'finance.transaction.aiRecordTab',
// 收支记录备注关键词历史
transactionRemarkHistory: 'finance.transaction.remarkHistory',
// 收支记录筛选偏好(收支记录页与统计面板共用)
transactionFilterPrefs: 'finance.transaction.filterPrefs',
// 账单关系类型
transactionRelationType: 'finance.transaction.relationType',
// 收支记录 AI 查询历史
transactionAiQueryHistory: 'finance.transaction.aiQueryHistory',
// 收支记录 AI 查询浮窗位置尺寸
transactionAiQueryWindow: 'finance.transaction.aiQueryWindow',
// 内容区滚动位置(sessionStorage,按路由拼接后缀)
scrollPrefix: 'finance.scroll.',
} as const
/** 历史遗留的存储键(未带 finance. 前缀),首次读取时迁移到新键 */
export const LEGACY_STORAGE_KEYS = {
locale: 'locale',
emailBillTab: 'emailBill.tab',
logsTab: 'logs.tab',
} as const
/** 生成指定路由的滚动位置存储键 */
export function scrollKey(path: string): string {
return `${STORAGE_KEYS.scrollPrefix}${path}`
}
@@ -0,0 +1,63 @@
// 收支记录筛选偏好:收支记录页与统计面板共用的读写与合并逻辑,字段定义与校验集中于此
import { readJson, writeJson } from './storage'
import { STORAGE_KEYS } from './storageKeys'
/** 收支记录筛选偏好字段 */
export interface TransactionFilterPrefs {
/** 类型筛选(多选) */
filterType: string[]
/** 分类筛选(多选) */
filterCategory: number[]
/** 交易渠道筛选(多选) */
filterChannel: string[]
/** 日期范围筛选(起止时间戳) */
filterDateRange: [number, number] | null
/** 计入统计筛选:null-全部,'true'-计入,'false'-不计入 */
filterIncludeInStats: string | null
/** 备注关键字搜索 */
filterRemark: string
/** 最小金额筛选 */
filterMinAmount: number | null
/** 最大金额筛选 */
filterMaxAmount: number | null
/** 每页条数 */
pageSize: number
/** 排序字段 */
sortValue: string
/** 标签筛选(多选) */
filterTag: number[]
}
/** 读取筛选偏好,仅返回类型合法的字段(兼容旧字段 filterPaymentMethod) */
export function loadTransactionFilterPrefs(): Partial<TransactionFilterPrefs> {
const raw = readJson<Record<string, any>>(STORAGE_KEYS.transactionFilterPrefs, {})
const result: Partial<TransactionFilterPrefs> = {}
if (Array.isArray(raw.filterType)) result.filterType = raw.filterType
if (Array.isArray(raw.filterCategory)) result.filterCategory = raw.filterCategory
// 兼容旧版本以 filterPaymentMethod 存储的渠道筛选
const channel = Array.isArray(raw.filterChannel) ? raw.filterChannel : raw.filterPaymentMethod
if (Array.isArray(channel)) result.filterChannel = channel
if (Array.isArray(raw.filterDateRange) && raw.filterDateRange.length === 2) {
result.filterDateRange = raw.filterDateRange as [number, number]
}
if (raw.filterIncludeInStats === 'true' || raw.filterIncludeInStats === 'false') {
result.filterIncludeInStats = raw.filterIncludeInStats
}
if (typeof raw.filterRemark === 'string') result.filterRemark = raw.filterRemark
if (typeof raw.filterMinAmount === 'number') result.filterMinAmount = raw.filterMinAmount
if (typeof raw.filterMaxAmount === 'number') result.filterMaxAmount = raw.filterMaxAmount
if (typeof raw.pageSize === 'number') result.pageSize = raw.pageSize
if (typeof raw.sortValue === 'string') result.sortValue = raw.sortValue
if (Array.isArray(raw.filterTag)) result.filterTag = raw.filterTag
return result
}
/**
* 合并写入筛选偏好:保留未传入的既有字段(如跳转时无需改动的分页、排序)。
*
* :param patch: 需要更新的字段
*/
export function saveTransactionFilterPrefs(patch: Partial<TransactionFilterPrefs>): void {
const merged = { ...loadTransactionFilterPrefs(), ...patch }
writeJson(STORAGE_KEYS.transactionFilterPrefs, merged)
}