24 changed files with 595 additions and 1695 deletions
@ -0,0 +1,130 @@ |
|||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
||||
|
|
||||
|
import { StorageManager } from './storage-manager'; |
||||
|
|
||||
|
describe('storageManager', () => { |
||||
|
let storageManager: StorageManager<{ age: number; name: string }>; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
vi.useFakeTimers(); |
||||
|
localStorage.clear(); |
||||
|
storageManager = new StorageManager<{ age: number; name: string }>({ |
||||
|
prefix: 'test_', |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should set and get an item', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }); |
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' }); |
||||
|
}); |
||||
|
|
||||
|
it('should return default value if item does not exist', () => { |
||||
|
const user = storageManager.getItem('nonexistent', { |
||||
|
age: 0, |
||||
|
name: 'Default User', |
||||
|
}); |
||||
|
expect(user).toEqual({ age: 0, name: 'Default User' }); |
||||
|
}); |
||||
|
|
||||
|
it('should remove an item', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }); |
||||
|
storageManager.removeItem('user'); |
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should clear all items with the prefix', () => { |
||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' }); |
||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }); |
||||
|
storageManager.clear(); |
||||
|
expect(storageManager.getItem('user1')).toBeNull(); |
||||
|
expect(storageManager.getItem('user2')).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should clear expired items', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
|
vi.advanceTimersByTime(1001); // 快进时间
|
||||
|
storageManager.clearExpiredItems(); |
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should not clear non-expired items', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||
|
vi.advanceTimersByTime(5000); // 快进时间
|
||||
|
storageManager.clearExpiredItems(); |
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' }); |
||||
|
}); |
||||
|
|
||||
|
it('should handle JSON parse errors gracefully', () => { |
||||
|
localStorage.setItem('test_user', '{ invalid JSON }'); |
||||
|
const user = storageManager.getItem('user', { |
||||
|
age: 0, |
||||
|
name: 'Default User', |
||||
|
}); |
||||
|
expect(user).toEqual({ age: 0, name: 'Default User' }); |
||||
|
}); |
||||
|
it('should return null for non-existent items without default value', () => { |
||||
|
const user = storageManager.getItem('nonexistent'); |
||||
|
expect(user).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should overwrite existing items', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }); |
||||
|
storageManager.setItem('user', { age: 25, name: 'Jane Doe' }); |
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toEqual({ age: 25, name: 'Jane Doe' }); |
||||
|
}); |
||||
|
|
||||
|
it('should handle items without expiry correctly', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }); |
||||
|
vi.advanceTimersByTime(5000); |
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' }); |
||||
|
}); |
||||
|
|
||||
|
it('should remove expired items when accessed', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
|
vi.advanceTimersByTime(1001); // 快进时间
|
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should not remove non-expired items when accessed', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||
|
vi.advanceTimersByTime(5000); // 快进时间
|
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' }); |
||||
|
}); |
||||
|
|
||||
|
it('should handle multiple items with different expiry times', () => { |
||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
|
||||
|
vi.advanceTimersByTime(1500); // 快进时间
|
||||
|
storageManager.clearExpiredItems(); |
||||
|
const user1 = storageManager.getItem('user1'); |
||||
|
const user2 = storageManager.getItem('user2'); |
||||
|
expect(user1).toBeNull(); |
||||
|
expect(user2).toEqual({ age: 25, name: 'Jane Doe' }); |
||||
|
}); |
||||
|
|
||||
|
it('should handle items with no expiry', () => { |
||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }); |
||||
|
vi.advanceTimersByTime(10_000); // 快进时间
|
||||
|
storageManager.clearExpiredItems(); |
||||
|
const user = storageManager.getItem('user'); |
||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' }); |
||||
|
}); |
||||
|
|
||||
|
it('should clear all items correctly', () => { |
||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' }); |
||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }); |
||||
|
storageManager.clear(); |
||||
|
const user1 = storageManager.getItem('user1'); |
||||
|
const user2 = storageManager.getItem('user2'); |
||||
|
expect(user1).toBeNull(); |
||||
|
expect(user2).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,118 @@ |
|||||
|
type StorageType = 'localStorage' | 'sessionStorage'; |
||||
|
|
||||
|
interface StorageManagerOptions { |
||||
|
prefix?: string; |
||||
|
storageType?: StorageType; |
||||
|
} |
||||
|
|
||||
|
interface StorageItem<T> { |
||||
|
expiry?: number; |
||||
|
value: T; |
||||
|
} |
||||
|
|
||||
|
class StorageManager { |
||||
|
private prefix: string; |
||||
|
private storage: Storage; |
||||
|
|
||||
|
constructor({ |
||||
|
prefix = '', |
||||
|
storageType = 'localStorage', |
||||
|
}: StorageManagerOptions = {}) { |
||||
|
this.prefix = prefix; |
||||
|
this.storage = |
||||
|
storageType === 'localStorage' |
||||
|
? window.localStorage |
||||
|
: window.sessionStorage; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取完整的存储键 |
||||
|
* @param key 原始键 |
||||
|
* @returns 带前缀的完整键 |
||||
|
*/ |
||||
|
private getFullKey(key: string): string { |
||||
|
return `${this.prefix}-${key}`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除所有带前缀的存储项 |
||||
|
*/ |
||||
|
clear(): void { |
||||
|
const keysToRemove: string[] = []; |
||||
|
for (let i = 0; i < this.storage.length; i++) { |
||||
|
const key = this.storage.key(i); |
||||
|
if (key && key.startsWith(this.prefix)) { |
||||
|
keysToRemove.push(key); |
||||
|
} |
||||
|
} |
||||
|
keysToRemove.forEach((key) => this.storage.removeItem(key)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除所有过期的存储项 |
||||
|
*/ |
||||
|
clearExpiredItems(): void { |
||||
|
for (let i = 0; i < this.storage.length; i++) { |
||||
|
const key = this.storage.key(i); |
||||
|
if (key && key.startsWith(this.prefix)) { |
||||
|
const shortKey = key.replace(this.prefix, ''); |
||||
|
this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取存储项 |
||||
|
* @param key 键 |
||||
|
* @param defaultValue 当项不存在或已过期时返回的默认值 |
||||
|
* @returns 值,如果项已过期或解析错误则返回默认值 |
||||
|
*/ |
||||
|
getItem<T>(key: string, defaultValue: T | null = null): T | null { |
||||
|
const fullKey = this.getFullKey(key); |
||||
|
const itemStr = this.storage.getItem(fullKey); |
||||
|
if (!itemStr) { |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const item: StorageItem<T> = JSON.parse(itemStr); |
||||
|
if (item.expiry && Date.now() > item.expiry) { |
||||
|
this.storage.removeItem(fullKey); |
||||
|
return defaultValue; |
||||
|
} |
||||
|
return item.value; |
||||
|
} catch (error) { |
||||
|
console.error(`Error parsing item with key "${fullKey}":`, error); |
||||
|
this.storage.removeItem(fullKey); // 如果解析失败,删除该项
|
||||
|
return defaultValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 移除存储项 |
||||
|
* @param key 键 |
||||
|
*/ |
||||
|
removeItem(key: string): void { |
||||
|
const fullKey = this.getFullKey(key); |
||||
|
this.storage.removeItem(fullKey); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置存储项 |
||||
|
* @param key 键 |
||||
|
* @param value 值 |
||||
|
* @param ttl 存活时间(毫秒) |
||||
|
*/ |
||||
|
setItem<T>(key: string, value: T, ttl?: number): void { |
||||
|
const fullKey = this.getFullKey(key); |
||||
|
const expiry = ttl ? Date.now() + ttl : undefined; |
||||
|
const item: StorageItem<T> = { expiry, value }; |
||||
|
try { |
||||
|
this.storage.setItem(fullKey, JSON.stringify(item)); |
||||
|
} catch (error) { |
||||
|
console.error(`Error setting item with key "${fullKey}":`, error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { StorageManager }; |
||||
@ -0,0 +1,17 @@ |
|||||
|
type StorageType = 'localStorage' | 'sessionStorage'; |
||||
|
|
||||
|
interface StorageValue<T> { |
||||
|
data: T; |
||||
|
expiry: null | number; |
||||
|
} |
||||
|
|
||||
|
interface IStorageCache { |
||||
|
clear(): void; |
||||
|
getItem<T>(key: string): T | null; |
||||
|
key(index: number): null | string; |
||||
|
length(): number; |
||||
|
removeItem(key: string): void; |
||||
|
setItem<T>(key: string, value: T, expiryInMinutes?: number): void; |
||||
|
} |
||||
|
|
||||
|
export type { IStorageCache, StorageType, StorageValue }; |
||||
@ -0,0 +1,56 @@ |
|||||
|
import { computed, ref } from 'vue'; |
||||
|
import { useRouter } from 'vue-router'; |
||||
|
|
||||
|
import { preferences } from '@vben-core/preferences'; |
||||
|
|
||||
|
function useContentSpinner() { |
||||
|
const spinning = ref(false); |
||||
|
const isStartTransition = ref(false); |
||||
|
const startTime = ref(0); |
||||
|
const router = useRouter(); |
||||
|
const minShowTime = 500; |
||||
|
const enableLoading = computed(() => preferences.transition.loading); |
||||
|
|
||||
|
const onEnd = () => { |
||||
|
if (!enableLoading.value) { |
||||
|
return; |
||||
|
} |
||||
|
const processTime = performance.now() - startTime.value; |
||||
|
if (processTime < minShowTime) { |
||||
|
setTimeout(() => { |
||||
|
spinning.value = false; |
||||
|
}, minShowTime - processTime); |
||||
|
} else { |
||||
|
spinning.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
router.beforeEach((to) => { |
||||
|
if (to.meta.loaded || !enableLoading.value) { |
||||
|
return true; |
||||
|
} |
||||
|
isStartTransition.value = false; |
||||
|
startTime.value = performance.now(); |
||||
|
spinning.value = true; |
||||
|
return true; |
||||
|
}); |
||||
|
|
||||
|
router.afterEach((to) => { |
||||
|
if (to.meta.loaded || !enableLoading.value) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 未进入过渡动画
|
||||
|
if (!isStartTransition.value) { |
||||
|
// 关闭加载动画
|
||||
|
onEnd(); |
||||
|
} |
||||
|
|
||||
|
isStartTransition.value = false; |
||||
|
return true; |
||||
|
}); |
||||
|
|
||||
|
return { onTransitionEnd: onEnd, spinning }; |
||||
|
} |
||||
|
|
||||
|
export { useContentSpinner }; |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue