feat: 添加支持过期时间的存储缓存功能,优化语言加载逻辑

This commit is contained in:
2025-12-20 05:17:42 +07:00
parent 82fe8e8f6f
commit 11bcbafd6e
3 changed files with 272 additions and 0 deletions

10
auto-imports.d.ts vendored
View File

@@ -10,6 +10,7 @@ declare global {
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const clearExpiredCaches: typeof import('./src/composables/useStorageCache').clearExpiredCaches
const computed: typeof import('vue').computed
const computedAsync: typeof import('@vueuse/core').computedAsync
const computedEager: typeof import('@vueuse/core').computedEager
@@ -42,6 +43,7 @@ declare global {
const formatAmountWithUnit: typeof import('./src/utils/helper').formatAmountWithUnit
const formatBalance: typeof import('./src/utils/helper').formatBalance
const getActivePinia: typeof import('pinia').getActivePinia
const getCacheRemainingTime: typeof import('./src/composables/useStorageCache').getCacheRemainingTime
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
@@ -49,6 +51,7 @@ declare global {
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectLocal: typeof import('@vueuse/core').injectLocal
const isCacheExpired: typeof import('./src/composables/useStorageCache').isCacheExpired
const isDefined: typeof import('@vueuse/core').isDefined
const isFunction: typeof import('./src/utils/is').isFunction
const isPromise: typeof import('./src/utils/is').isPromise
@@ -104,6 +107,7 @@ declare global {
const refManualReset: typeof import('@vueuse/core').refManualReset
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const refreshCacheExpire: typeof import('./src/composables/useStorageCache').refreshCacheExpire
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const setActivePinia: typeof import('pinia').setActivePinia
@@ -272,6 +276,7 @@ declare global {
const useStepper: typeof import('@vueuse/core').useStepper
const useStorage: typeof import('@vueuse/core').useStorage
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
const useStorageCache: typeof import('./src/composables/useStorageCache').useStorageCache
const useStyleTag: typeof import('@vueuse/core').useStyleTag
const useSubscribeModal: typeof import('./src/composables/useSubscribeModal').useSubscribeModal
const useSupported: typeof import('@vueuse/core').useSupported
@@ -363,6 +368,7 @@ declare module 'vue' {
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly clearExpiredCaches: UnwrapRef<typeof import('./src/composables/useStorageCache')['clearExpiredCaches']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
@@ -394,6 +400,7 @@ declare module 'vue' {
readonly formatAmount: UnwrapRef<typeof import('./src/utils/helper')['formatAmount']>
readonly formatBalance: UnwrapRef<typeof import('./src/utils/helper')['formatBalance']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCacheRemainingTime: UnwrapRef<typeof import('./src/composables/useStorageCache')['getCacheRemainingTime']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
@@ -401,6 +408,7 @@ declare module 'vue' {
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isCacheExpired: UnwrapRef<typeof import('./src/composables/useStorageCache')['isCacheExpired']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isFunction: UnwrapRef<typeof import('./src/utils/is')['isFunction']>
readonly isPromise: UnwrapRef<typeof import('./src/utils/is')['isPromise']>
@@ -456,6 +464,7 @@ declare module 'vue' {
readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly refreshCacheExpire: UnwrapRef<typeof import('./src/composables/useStorageCache')['refreshCacheExpire']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
@@ -624,6 +633,7 @@ declare module 'vue' {
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStorageCache: UnwrapRef<typeof import('./src/composables/useStorageCache')['useStorageCache']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>

View File

@@ -43,7 +43,21 @@ export function useLanguage() {
}
function loadRemoteLanguage() {
const storageKey = `remote-lang-${language.value}`;
const remoteLangJson = useStorageCache<string>(storageKey, "", { ttl: 24 * 60 * 60 }); // 缓存 1 天
if (remoteLangJson.value) {
try {
const messages = JSON.parse(remoteLangJson.value);
mergeLocaleMessage(locale.value, messages);
return;
}
catch (e) {
console.error("Failed to parse remote language JSON:", e);
}
}
safeClient(client.api.error_messages({ lang: language.value }).get()).then((res) => {
clearExpiredCaches([storageKey]);
remoteLangJson.value = JSON.stringify(res.data.value);
mergeLocaleMessage(locale.value, res.data.value);
});
}

View File

@@ -0,0 +1,248 @@
import type { RemovableRef, StorageLike } from "@vueuse/core";
interface CacheData<T> {
value: T;
expireTime?: number; // 过期时间戳(毫秒)
}
interface UseStorageCacheOptions {
/**
* 缓存过期时间(秒)
* @default undefined 永不过期
*/
ttl?: number;
/**
* 存储介质
* @default localStorage
*/
storage?: StorageLike;
/**
* 是否在过期后自动删除
* @default true
*/
autoRemoveOnExpire?: boolean;
}
/**
* 使用支持过期时间的 Storage
* @param key 存储键
* @param initialValue 初始值
* @param options 配置项
* @returns 响应式存储引用
*
* @example
* ```ts
* // 创建一个 5 分钟后过期的缓存
* const token = useStorageCache('user-token', '', { ttl: 300 })
*
* // 设置值
* token.value = 'new-token'
*
* // 检查是否过期
* if (isExpired(token)) {
* console.log('Token 已过期')
* }
*
* // 刷新过期时间
* refreshExpire(token, 600) // 延长到 10 分钟
* ```
*/
export function useStorageCache<T>(
key: string,
initialValue: T,
options: UseStorageCacheOptions = {},
): RemovableRef<T> {
const {
ttl,
storage = localStorage,
autoRemoveOnExpire = true,
} = options;
// 尝试从 storage 读取缓存数据
const rawData = storage.getItem(key);
let cachedData: CacheData<T> | null = null;
if (rawData) {
try {
cachedData = JSON.parse(rawData) as CacheData<T>;
// 检查是否过期
if (cachedData.expireTime && Date.now() > cachedData.expireTime) {
// 已过期
if (autoRemoveOnExpire) {
storage.removeItem(key);
}
cachedData = null;
}
}
catch (error) {
console.error(`解析缓存数据失败 [${key}]:`, error);
cachedData = null;
}
}
// 计算初始值
const defaultValue = cachedData ? cachedData.value : initialValue;
// 使用 useStorage 创建响应式引用
const storageRef = useStorage<T>(
key,
defaultValue,
storage,
{
serializer: {
read: (raw: string) => {
try {
const data = JSON.parse(raw) as CacheData<T>;
// 再次检查过期时间(防止并发问题)
if (data.expireTime && Date.now() > data.expireTime) {
if (autoRemoveOnExpire) {
storage.removeItem(key);
}
return initialValue;
}
return data.value;
}
catch {
return initialValue;
}
},
write: (value: T) => {
const cacheData: CacheData<T> = {
value,
};
// 如果设置了 ttl计算过期时间
if (ttl && ttl > 0) {
cacheData.expireTime = Date.now() + ttl * 1000;
}
return JSON.stringify(cacheData);
},
},
},
);
return storageRef;
}
/**
* 检查缓存是否过期
* @param key 存储键或存储引用
* @param storage 存储介质
* @returns 是否过期
*/
export function isCacheExpired(
key: string | RemovableRef<any>,
storage: StorageLike = localStorage,
): boolean {
const cacheKey = typeof key === "string" ? key : key.value;
try {
const rawData = storage.getItem(cacheKey);
if (!rawData) {
return true;
}
const data = JSON.parse(rawData) as CacheData<any>;
if (!data.expireTime) {
return false; // 没有设置过期时间,永不过期
}
return Date.now() > data.expireTime;
}
catch {
return true;
}
}
/**
* 刷新缓存过期时间
* @param key 存储键
* @param ttl 新的过期时间(秒)
* @param storage 存储介质
*/
export function refreshCacheExpire(
key: string,
ttl: number,
storage: StorageLike = localStorage,
): void {
try {
const rawData = storage.getItem(key);
if (!rawData) {
return;
}
const data = JSON.parse(rawData) as CacheData<any>;
data.expireTime = Date.now() + ttl * 1000;
storage.setItem(key, JSON.stringify(data));
}
catch (error) {
console.error(`刷新缓存过期时间失败 [${key}]:`, error);
}
}
/**
* 获取缓存剩余有效时间(秒)
* @param key 存储键
* @param storage 存储介质
* @returns 剩余时间(秒),如果已过期返回 0如果永不过期返回 Infinity
*/
export function getCacheRemainingTime(
key: string,
storage: StorageLike = localStorage,
): number {
try {
const rawData = storage.getItem(key);
if (!rawData) {
return 0;
}
const data = JSON.parse(rawData) as CacheData<any>;
if (!data.expireTime) {
return Infinity; // 永不过期
}
const remaining = Math.max(0, data.expireTime - Date.now());
return Math.floor(remaining / 1000);
}
catch {
return 0;
}
}
/**
* 清除指定键的过期缓存
* @param keys 要检查的键列表
* @param storage 存储介质
* @returns 被删除的键数量
*/
export function clearExpiredCaches(
keys: string[],
storage: StorageLike = localStorage,
): number {
let removedCount = 0;
keys.forEach((key) => {
try {
const rawData = storage.getItem(key);
if (!rawData) {
return;
}
const data = JSON.parse(rawData) as CacheData<any>;
// 检查是否过期
if (data.expireTime && Date.now() > data.expireTime) {
storage.removeItem(key);
removedCount++;
}
}
catch {
// 忽略解析错误
}
});
return removedCount;
}