feat: 添加支持过期时间的存储缓存功能,优化语言加载逻辑
This commit is contained in:
10
auto-imports.d.ts
vendored
10
auto-imports.d.ts
vendored
@@ -10,6 +10,7 @@ declare global {
|
|||||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||||
const asyncComputed: typeof import('@vueuse/core').asyncComputed
|
const asyncComputed: typeof import('@vueuse/core').asyncComputed
|
||||||
const autoResetRef: typeof import('@vueuse/core').autoResetRef
|
const autoResetRef: typeof import('@vueuse/core').autoResetRef
|
||||||
|
const clearExpiredCaches: typeof import('./src/composables/useStorageCache').clearExpiredCaches
|
||||||
const computed: typeof import('vue').computed
|
const computed: typeof import('vue').computed
|
||||||
const computedAsync: typeof import('@vueuse/core').computedAsync
|
const computedAsync: typeof import('@vueuse/core').computedAsync
|
||||||
const computedEager: typeof import('@vueuse/core').computedEager
|
const computedEager: typeof import('@vueuse/core').computedEager
|
||||||
@@ -42,6 +43,7 @@ declare global {
|
|||||||
const formatAmountWithUnit: typeof import('./src/utils/helper').formatAmountWithUnit
|
const formatAmountWithUnit: typeof import('./src/utils/helper').formatAmountWithUnit
|
||||||
const formatBalance: typeof import('./src/utils/helper').formatBalance
|
const formatBalance: typeof import('./src/utils/helper').formatBalance
|
||||||
const getActivePinia: typeof import('pinia').getActivePinia
|
const getActivePinia: typeof import('pinia').getActivePinia
|
||||||
|
const getCacheRemainingTime: typeof import('./src/composables/useStorageCache').getCacheRemainingTime
|
||||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||||
@@ -49,6 +51,7 @@ declare global {
|
|||||||
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
|
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
|
||||||
const inject: typeof import('vue').inject
|
const inject: typeof import('vue').inject
|
||||||
const injectLocal: typeof import('@vueuse/core').injectLocal
|
const injectLocal: typeof import('@vueuse/core').injectLocal
|
||||||
|
const isCacheExpired: typeof import('./src/composables/useStorageCache').isCacheExpired
|
||||||
const isDefined: typeof import('@vueuse/core').isDefined
|
const isDefined: typeof import('@vueuse/core').isDefined
|
||||||
const isFunction: typeof import('./src/utils/is').isFunction
|
const isFunction: typeof import('./src/utils/is').isFunction
|
||||||
const isPromise: typeof import('./src/utils/is').isPromise
|
const isPromise: typeof import('./src/utils/is').isPromise
|
||||||
@@ -104,6 +107,7 @@ declare global {
|
|||||||
const refManualReset: typeof import('@vueuse/core').refManualReset
|
const refManualReset: typeof import('@vueuse/core').refManualReset
|
||||||
const refThrottled: typeof import('@vueuse/core').refThrottled
|
const refThrottled: typeof import('@vueuse/core').refThrottled
|
||||||
const refWithControl: typeof import('@vueuse/core').refWithControl
|
const refWithControl: typeof import('@vueuse/core').refWithControl
|
||||||
|
const refreshCacheExpire: typeof import('./src/composables/useStorageCache').refreshCacheExpire
|
||||||
const resolveComponent: typeof import('vue').resolveComponent
|
const resolveComponent: typeof import('vue').resolveComponent
|
||||||
const resolveRef: typeof import('@vueuse/core').resolveRef
|
const resolveRef: typeof import('@vueuse/core').resolveRef
|
||||||
const setActivePinia: typeof import('pinia').setActivePinia
|
const setActivePinia: typeof import('pinia').setActivePinia
|
||||||
@@ -272,6 +276,7 @@ declare global {
|
|||||||
const useStepper: typeof import('@vueuse/core').useStepper
|
const useStepper: typeof import('@vueuse/core').useStepper
|
||||||
const useStorage: typeof import('@vueuse/core').useStorage
|
const useStorage: typeof import('@vueuse/core').useStorage
|
||||||
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
|
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
|
||||||
|
const useStorageCache: typeof import('./src/composables/useStorageCache').useStorageCache
|
||||||
const useStyleTag: typeof import('@vueuse/core').useStyleTag
|
const useStyleTag: typeof import('@vueuse/core').useStyleTag
|
||||||
const useSubscribeModal: typeof import('./src/composables/useSubscribeModal').useSubscribeModal
|
const useSubscribeModal: typeof import('./src/composables/useSubscribeModal').useSubscribeModal
|
||||||
const useSupported: typeof import('@vueuse/core').useSupported
|
const useSupported: typeof import('@vueuse/core').useSupported
|
||||||
@@ -363,6 +368,7 @@ declare module 'vue' {
|
|||||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
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 computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
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 formatAmount: UnwrapRef<typeof import('./src/utils/helper')['formatAmount']>
|
||||||
readonly formatBalance: UnwrapRef<typeof import('./src/utils/helper')['formatBalance']>
|
readonly formatBalance: UnwrapRef<typeof import('./src/utils/helper')['formatBalance']>
|
||||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||||
|
readonly getCacheRemainingTime: UnwrapRef<typeof import('./src/composables/useStorageCache')['getCacheRemainingTime']>
|
||||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
|
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
|
||||||
@@ -401,6 +408,7 @@ declare module 'vue' {
|
|||||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
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 isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||||
readonly isFunction: UnwrapRef<typeof import('./src/utils/is')['isFunction']>
|
readonly isFunction: UnwrapRef<typeof import('./src/utils/is')['isFunction']>
|
||||||
readonly isPromise: UnwrapRef<typeof import('./src/utils/is')['isPromise']>
|
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 refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
|
||||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
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 resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||||
@@ -624,6 +633,7 @@ declare module 'vue' {
|
|||||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
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 useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||||
|
|||||||
@@ -43,7 +43,21 @@ export function useLanguage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadRemoteLanguage() {
|
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) => {
|
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);
|
mergeLocaleMessage(locale.value, res.data.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
248
src/composables/useStorageCache.ts
Normal file
248
src/composables/useStorageCache.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user