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