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:
@@ -16,7 +16,10 @@ import { getTagList, createTag } from '../api/tag'
|
||||
import { getChannelLabel } from '../utils/channel'
|
||||
import { renderCategoryLabel } from '../utils/render'
|
||||
import { useKeywordHistory } from '../composables/useKeywordHistory'
|
||||
import { useStringPreference } from '../composables/usePreference'
|
||||
import { openSse } from '../utils/sse'
|
||||
import { STORAGE_KEYS } from '../utils/storageKeys'
|
||||
import { loadTransactionFilterPrefs, saveTransactionFilterPrefs } from '../utils/transactionFilterPrefs'
|
||||
import BillImportModal from './BillImportModal.vue'
|
||||
import TransactionDetailModal from '../components/TransactionDetailModal.vue'
|
||||
import TransactionRelationEditorModal from '../components/TransactionRelationEditorModal.vue'
|
||||
@@ -146,34 +149,18 @@ const aiEnabled = ref(false) // AI 建议是否启用
|
||||
const aiLoading = ref(false) // AI 建议生成状态
|
||||
const aiFormLoading = ref(false) // 编辑弹窗内 AI 推荐状态
|
||||
/** AI 建议范围偏好存储键 */
|
||||
const AI_SCOPE_KEY = 'finance.transaction.aiScope'
|
||||
const AI_SCOPE_KEY = STORAGE_KEYS.transactionAiScope
|
||||
|
||||
/** 读取上次的 AI 建议范围偏好,非法值回退 BOTH */
|
||||
function loadAiScope(): string {
|
||||
try {
|
||||
const saved = localStorage.getItem(AI_SCOPE_KEY)
|
||||
return saved && ['BOTH', 'CATEGORY', 'TAG'].includes(saved) ? saved : 'BOTH'
|
||||
} catch (e) {
|
||||
return 'BOTH'
|
||||
}
|
||||
}
|
||||
|
||||
const aiScope = ref(loadAiScope()) // 批量 AI 建议范围:BOTH/CATEGORY/TAG(记忆用户偏好)
|
||||
// 批量 AI 建议范围:BOTH/CATEGORY/TAG(记忆用户偏好,非法值回退 BOTH)
|
||||
const aiScope = useStringPreference(AI_SCOPE_KEY, 'BOTH', {
|
||||
validate: (value) => ['BOTH', 'CATEGORY', 'TAG'].includes(value),
|
||||
})
|
||||
// 批量 AI 建议范围选项
|
||||
const aiScopeOptions = computed(() => [
|
||||
{ label: t('ai.scope.both'), value: 'BOTH' },
|
||||
{ label: t('ai.scope.category'), value: 'CATEGORY' },
|
||||
{ label: t('ai.scope.tag'), value: 'TAG' },
|
||||
])
|
||||
|
||||
// 记住用户选择的 AI 建议范围
|
||||
watch(aiScope, (value) => {
|
||||
try {
|
||||
localStorage.setItem(AI_SCOPE_KEY, value)
|
||||
} catch (e) {
|
||||
// 忽略存储异常
|
||||
}
|
||||
})
|
||||
const aiNewLoading = ref(false) // 编辑弹窗内 AI 生成新分类状态
|
||||
const aiNewSaving = ref(false) // 新分类保存状态
|
||||
// AI 生成但尚未保存的新分类,需用户确认后再落库
|
||||
@@ -197,28 +184,16 @@ const showAiRecordModal = ref(false) // AI 记账弹窗显示状态
|
||||
const aiRecordText = ref('') // AI 记账输入
|
||||
const aiRecording = ref(false) // AI 记账解析状态
|
||||
/** AI 记账方式偏好存储键(参考邮箱页 Tab 记忆) */
|
||||
const AI_RECORD_TAB_KEY = 'finance.transaction.aiRecordTab'
|
||||
const AI_RECORD_TAB_KEY = STORAGE_KEYS.transactionAiRecordTab
|
||||
|
||||
/** 读取上次的 AI 记账方式偏好,非法值回退文字记账 */
|
||||
function loadAiRecordTab(): 'text' | 'image' {
|
||||
try {
|
||||
const saved = localStorage.getItem(AI_RECORD_TAB_KEY)
|
||||
return saved === 'image' ? 'image' : 'text'
|
||||
} catch (e) {
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
|
||||
const aiRecordTab = ref<'text' | 'image'>(loadAiRecordTab()) // AI 记账方式:文字/图片(记忆用户偏好)
|
||||
|
||||
// 记住用户选择的 AI 记账方式
|
||||
watch(aiRecordTab, (value) => {
|
||||
try {
|
||||
localStorage.setItem(AI_RECORD_TAB_KEY, value)
|
||||
} catch (e) {
|
||||
// 忽略存储异常
|
||||
}
|
||||
// AI 记账方式:文字/图片(记忆用户偏好,非法值回退文字记账)
|
||||
const aiRecordTab = useStringPreference(AI_RECORD_TAB_KEY, 'text', {
|
||||
validate: (value) => value === 'text' || value === 'image',
|
||||
})
|
||||
|
||||
/** AI 查询历史与浮窗位置尺寸存储键 */
|
||||
const AI_QUERY_HISTORY_KEY = STORAGE_KEYS.transactionAiQueryHistory
|
||||
const AI_QUERY_WINDOW_KEY = STORAGE_KEYS.transactionAiQueryWindow
|
||||
/** 图片记账单次最多上传的图片数量 */
|
||||
const MAX_AI_RECORD_IMAGES = 5
|
||||
const aiRecordFileList = ref<UploadFileInfo[]>([]) // 图片记账上传文件列表
|
||||
@@ -291,7 +266,7 @@ const filterDateRange = ref<[number, number] | null>(null) // 日期范围筛选
|
||||
const filterIncludeInStats = ref<string | null>(null) // 计入统计筛选:null-全部,'true'-计入,'false'-不计入
|
||||
const filterRemark = ref('') // 备注关键字搜索
|
||||
// 备注关键词历史存储键
|
||||
const REMARK_HISTORY_KEY = 'finance.transaction.remarkHistory'
|
||||
const REMARK_HISTORY_KEY = STORAGE_KEYS.transactionRemarkHistory
|
||||
// 备注关键词历史(最近输入,最新在前)
|
||||
const remarkHistory = useKeywordHistory({
|
||||
storageKey: REMARK_HISTORY_KEY,
|
||||
@@ -359,52 +334,37 @@ const includeInStatsOptions = computed(() => [
|
||||
{ label: t('transaction.includeFilter.no'), value: 'false' },
|
||||
])
|
||||
|
||||
/** 筛选条件偏好存储键 */
|
||||
const FILTER_PREFS_KEY = 'finance.transaction.filterPrefs'
|
||||
|
||||
/** 从 localStorage 读取上次的筛选条件偏好并还原 */
|
||||
function loadFilterPrefs() {
|
||||
try {
|
||||
const raw = localStorage.getItem(FILTER_PREFS_KEY)
|
||||
if (!raw) return
|
||||
const p = JSON.parse(raw)
|
||||
if (Array.isArray(p.filterType)) filterType.value = p.filterType
|
||||
if (Array.isArray(p.filterCategory)) filterCategory.value = p.filterCategory
|
||||
// 兼容旧版本以 filterPaymentMethod 存储的偏好
|
||||
const savedChannel = Array.isArray(p.filterChannel) ? p.filterChannel : p.filterPaymentMethod
|
||||
if (Array.isArray(savedChannel)) filterChannel.value = savedChannel
|
||||
if (Array.isArray(p.filterDateRange) && p.filterDateRange.length === 2) filterDateRange.value = p.filterDateRange
|
||||
if (p.filterIncludeInStats === 'true' || p.filterIncludeInStats === 'false') filterIncludeInStats.value = p.filterIncludeInStats
|
||||
if (typeof p.filterRemark === 'string') filterRemark.value = p.filterRemark
|
||||
if (typeof p.filterMinAmount === 'number') filterMinAmount.value = p.filterMinAmount
|
||||
if (typeof p.filterMaxAmount === 'number') filterMaxAmount.value = p.filterMaxAmount
|
||||
if (typeof p.pageSize === 'number' && pageSizes.includes(p.pageSize)) pageSize.value = p.pageSize
|
||||
if (typeof p.sortValue === 'string' && sortOptions.value.some((o) => o.value === p.sortValue)) sortValue.value = p.sortValue
|
||||
if (Array.isArray(p.filterTag)) filterTag.value = p.filterTag
|
||||
} catch (e) {
|
||||
// 忽略损坏的偏好数据
|
||||
}
|
||||
const p = loadTransactionFilterPrefs()
|
||||
if (p.filterType) filterType.value = p.filterType
|
||||
if (p.filterCategory) filterCategory.value = p.filterCategory
|
||||
if (p.filterChannel) filterChannel.value = p.filterChannel
|
||||
if (p.filterDateRange) filterDateRange.value = p.filterDateRange
|
||||
if (p.filterIncludeInStats) filterIncludeInStats.value = p.filterIncludeInStats
|
||||
if (typeof p.filterRemark === 'string') filterRemark.value = p.filterRemark
|
||||
if (typeof p.filterMinAmount === 'number') filterMinAmount.value = p.filterMinAmount
|
||||
if (typeof p.filterMaxAmount === 'number') filterMaxAmount.value = p.filterMaxAmount
|
||||
if (typeof p.pageSize === 'number' && pageSizes.includes(p.pageSize)) pageSize.value = p.pageSize
|
||||
if (typeof p.sortValue === 'string' && sortOptions.value.some((o) => o.value === p.sortValue)) sortValue.value = p.sortValue
|
||||
if (p.filterTag) filterTag.value = p.filterTag
|
||||
}
|
||||
|
||||
/** 将当前筛选条件写入 localStorage,下次进入自动还原 */
|
||||
function saveFilterPrefs() {
|
||||
try {
|
||||
localStorage.setItem(FILTER_PREFS_KEY, JSON.stringify({
|
||||
filterType: filterType.value,
|
||||
filterCategory: filterCategory.value,
|
||||
filterChannel: filterChannel.value,
|
||||
filterDateRange: filterDateRange.value,
|
||||
filterIncludeInStats: filterIncludeInStats.value,
|
||||
filterRemark: filterRemark.value,
|
||||
filterMinAmount: filterMinAmount.value,
|
||||
filterMaxAmount: filterMaxAmount.value,
|
||||
pageSize: pageSize.value,
|
||||
sortValue: sortValue.value,
|
||||
filterTag: filterTag.value,
|
||||
}))
|
||||
} catch (e) {
|
||||
// 忽略写入失败
|
||||
}
|
||||
saveTransactionFilterPrefs({
|
||||
filterType: filterType.value,
|
||||
filterCategory: filterCategory.value,
|
||||
filterChannel: filterChannel.value,
|
||||
filterDateRange: filterDateRange.value,
|
||||
filterIncludeInStats: filterIncludeInStats.value,
|
||||
filterRemark: filterRemark.value,
|
||||
filterMinAmount: filterMinAmount.value,
|
||||
filterMaxAmount: filterMaxAmount.value,
|
||||
pageSize: pageSize.value,
|
||||
sortValue: sortValue.value,
|
||||
filterTag: filterTag.value,
|
||||
})
|
||||
}
|
||||
|
||||
// 筛选条件变化时自动保存用户偏好
|
||||
@@ -2532,8 +2492,8 @@ onBeforeUnmount(() => {
|
||||
:placeholder="t('ai.query.placeholder')"
|
||||
:examples="aiQueryExamples"
|
||||
:loading="aiQuerying"
|
||||
history-key="finance.transaction.aiQueryHistory"
|
||||
storage-key="finance.transaction.aiQueryWindow"
|
||||
:history-key="AI_QUERY_HISTORY_KEY"
|
||||
:storage-key="AI_QUERY_WINDOW_KEY"
|
||||
@submit="handleAiQuery"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user