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:
@@ -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) {
|
||||
// 忽略迁移失败,保持旧键可用
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user