32 changed files with 585 additions and 550 deletions
@ -0,0 +1,3 @@ |
|||||
|
declare module '@tailwindcss/nesting' { |
||||
|
export default any; |
||||
|
} |
||||
@ -1,22 +0,0 @@ |
|||||
import type { Config } from 'tailwindcss'; |
|
||||
|
|
||||
import plugin from 'tailwindcss/plugin'; |
|
||||
|
|
||||
const flexCenterStyles = { |
|
||||
'align-items': 'center', |
|
||||
display: 'flex', |
|
||||
'justify-content': 'center', |
|
||||
}; |
|
||||
|
|
||||
const plugins = [ |
|
||||
plugin(({ addUtilities }) => { |
|
||||
addUtilities({ |
|
||||
'.flex-center': flexCenterStyles, |
|
||||
'.flex-col-center': { |
|
||||
...flexCenterStyles, |
|
||||
}, |
|
||||
}); |
|
||||
}), |
|
||||
] as unknown as Config['plugins'][]; |
|
||||
|
|
||||
export { plugins }; |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
import { defineBuildConfig } from 'unbuild'; |
||||
|
|
||||
|
export default defineBuildConfig({ |
||||
|
clean: true, |
||||
|
declaration: true, |
||||
|
entries: ['src/index'], |
||||
|
}); |
||||
@ -0,0 +1,43 @@ |
|||||
|
{ |
||||
|
"name": "@vben-core/cache", |
||||
|
"version": "1.0.0", |
||||
|
"type": "module", |
||||
|
"license": "MIT", |
||||
|
"homepage": "https://github.com/vbenjs/vue-vben-admin", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git", |
||||
|
"directory": "packages/@vben-core/shared/toolkit" |
||||
|
}, |
||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues", |
||||
|
"scripts": { |
||||
|
"build": "pnpm unbuild", |
||||
|
"stub": "pnpm unbuild --stub" |
||||
|
}, |
||||
|
"files": [ |
||||
|
"dist" |
||||
|
], |
||||
|
"sideEffects": false, |
||||
|
"main": "./dist/index.mjs", |
||||
|
"module": "./dist/index.mjs", |
||||
|
"imports": { |
||||
|
"#*": "./src/*" |
||||
|
}, |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./src/index.ts", |
||||
|
"development": "./src/index.ts", |
||||
|
"default": "./dist/index.mjs" |
||||
|
} |
||||
|
}, |
||||
|
"publishConfig": { |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./dist/index.d.ts", |
||||
|
"default": "./dist/index.mjs" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": {}, |
||||
|
"devDependencies": {} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from './storage-cache'; |
||||
@ -0,0 +1,104 @@ |
|||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
||||
|
|
||||
|
import { StorageCache } from './storage-cache'; |
||||
|
|
||||
|
describe('storageCache', () => { |
||||
|
let localStorageCache: StorageCache; |
||||
|
let sessionStorageCache: StorageCache; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
localStorageCache = new StorageCache('prefix_', 'localStorage'); |
||||
|
sessionStorageCache = new StorageCache('prefix_', 'sessionStorage'); |
||||
|
localStorage.clear(); |
||||
|
sessionStorage.clear(); |
||||
|
vi.useFakeTimers(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
vi.useRealTimers(); |
||||
|
}); |
||||
|
|
||||
|
it('should set and get an item with prefix in localStorage', () => { |
||||
|
localStorageCache.setItem('testKey', 'testValue'); |
||||
|
const value = localStorageCache.getItem<string>('testKey'); |
||||
|
expect(value).toBe('testValue'); |
||||
|
expect(localStorage.getItem('prefix_testKey')).not.toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should set and get an item with prefix in sessionStorage', () => { |
||||
|
sessionStorageCache.setItem('testKey', 'testValue'); |
||||
|
const value = sessionStorageCache.getItem<string>('testKey'); |
||||
|
expect(value).toBe('testValue'); |
||||
|
expect(sessionStorage.getItem('prefix_testKey')).not.toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should return null for expired item in localStorage', () => { |
||||
|
localStorageCache.setItem('testKey', 'testValue', 1 / 60); // 1 second expiry
|
||||
|
vi.advanceTimersByTime(2000); // Fast-forward 2 seconds
|
||||
|
const value = localStorageCache.getItem<string>('testKey'); |
||||
|
expect(value).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should return null for expired item in sessionStorage', () => { |
||||
|
sessionStorageCache.setItem('testKey', 'testValue', 1 / 60); // 1 second expiry
|
||||
|
vi.advanceTimersByTime(2000); // Fast-forward 2 seconds
|
||||
|
const value = sessionStorageCache.getItem<string>('testKey'); |
||||
|
expect(value).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should remove an item with prefix in localStorage', () => { |
||||
|
localStorageCache.setItem('testKey', 'testValue'); |
||||
|
localStorageCache.removeItem('testKey'); |
||||
|
const value = localStorageCache.getItem<string>('testKey'); |
||||
|
expect(value).toBeNull(); |
||||
|
expect(localStorage.getItem('prefix_testKey')).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should remove an item with prefix in sessionStorage', () => { |
||||
|
sessionStorageCache.setItem('testKey', 'testValue'); |
||||
|
sessionStorageCache.removeItem('testKey'); |
||||
|
const value = sessionStorageCache.getItem<string>('testKey'); |
||||
|
expect(value).toBeNull(); |
||||
|
expect(sessionStorage.getItem('prefix_testKey')).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it('should clear all items in localStorage', () => { |
||||
|
localStorageCache.setItem('testKey1', 'testValue1'); |
||||
|
localStorageCache.setItem('testKey2', 'testValue2'); |
||||
|
localStorageCache.clear(); |
||||
|
expect(localStorageCache.length()).toBe(0); |
||||
|
}); |
||||
|
|
||||
|
it('should clear all items in sessionStorage', () => { |
||||
|
sessionStorageCache.setItem('testKey1', 'testValue1'); |
||||
|
sessionStorageCache.setItem('testKey2', 'testValue2'); |
||||
|
sessionStorageCache.clear(); |
||||
|
expect(sessionStorageCache.length()).toBe(0); |
||||
|
}); |
||||
|
|
||||
|
it('should return correct length in localStorage', () => { |
||||
|
localStorageCache.setItem('testKey1', 'testValue1'); |
||||
|
localStorageCache.setItem('testKey2', 'testValue2'); |
||||
|
expect(localStorageCache.length()).toBe(2); |
||||
|
}); |
||||
|
|
||||
|
it('should return correct length in sessionStorage', () => { |
||||
|
sessionStorageCache.setItem('testKey1', 'testValue1'); |
||||
|
sessionStorageCache.setItem('testKey2', 'testValue2'); |
||||
|
expect(sessionStorageCache.length()).toBe(2); |
||||
|
}); |
||||
|
|
||||
|
it('should return correct key by index in localStorage', () => { |
||||
|
localStorageCache.setItem('testKey1', 'testValue1'); |
||||
|
localStorageCache.setItem('testKey2', 'testValue2'); |
||||
|
expect(localStorageCache.key(0)).toBe('prefix_testKey1'); |
||||
|
expect(localStorageCache.key(1)).toBe('prefix_testKey2'); |
||||
|
}); |
||||
|
|
||||
|
it('should return correct key by index in sessionStorage', () => { |
||||
|
sessionStorageCache.setItem('testKey1', 'testValue1'); |
||||
|
sessionStorageCache.setItem('testKey2', 'testValue2'); |
||||
|
expect(sessionStorageCache.key(0)).toBe('prefix_testKey1'); |
||||
|
expect(sessionStorageCache.key(1)).toBe('prefix_testKey2'); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,145 @@ |
|||||
|
import type { IStorageCache, StorageType, StorageValue } from './types'; |
||||
|
|
||||
|
class StorageCache implements IStorageCache { |
||||
|
protected prefix: string; |
||||
|
protected storage: Storage; |
||||
|
|
||||
|
constructor(prefix: string = '', storageType: StorageType = 'localStorage') { |
||||
|
this.prefix = prefix; |
||||
|
this.storage = |
||||
|
storageType === 'localStorage' ? localStorage : sessionStorage; |
||||
|
} |
||||
|
|
||||
|
// 获取带前缀的键名
|
||||
|
private getFullKey(key: string): string { |
||||
|
return this.prefix + key; |
||||
|
} |
||||
|
|
||||
|
// 获取项之后的钩子方法
|
||||
|
protected afterGetItem<T>(_key: string, _value: T | null): void {} |
||||
|
|
||||
|
// 设置项之后的钩子方法
|
||||
|
protected afterSetItem<T>( |
||||
|
_key: string, |
||||
|
_value: T, |
||||
|
_expiryInMinutes?: number, |
||||
|
): void {} |
||||
|
|
||||
|
// 获取项之前的钩子方法
|
||||
|
protected beforeGetItem(_key: string): void {} |
||||
|
|
||||
|
// 设置项之前的钩子方法
|
||||
|
protected beforeSetItem<T>( |
||||
|
_key: string, |
||||
|
_value: T, |
||||
|
_expiryInMinutes?: number, |
||||
|
): void {} |
||||
|
|
||||
|
/** |
||||
|
* 清空存储 |
||||
|
*/ |
||||
|
clear(): void { |
||||
|
try { |
||||
|
this.storage.clear(); |
||||
|
} catch (error) { |
||||
|
console.error('Error clearing storage', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取存储项 |
||||
|
* @param key 存储键 |
||||
|
* @returns 存储值或 null |
||||
|
*/ |
||||
|
getItem<T>(key: string): T | null { |
||||
|
const fullKey = this.getFullKey(key); |
||||
|
this.beforeGetItem(fullKey); |
||||
|
|
||||
|
let value: T | null = null; |
||||
|
try { |
||||
|
const item = this.storage.getItem(fullKey); |
||||
|
if (item) { |
||||
|
const storageValue: StorageValue<T> = JSON.parse(item); |
||||
|
if (storageValue.expiry && storageValue.expiry < Date.now()) { |
||||
|
this.storage.removeItem(fullKey); |
||||
|
} else { |
||||
|
value = storageValue.data; |
||||
|
} |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('Error getting item from storage', error); |
||||
|
} |
||||
|
|
||||
|
this.afterGetItem(fullKey, value); |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取存储中的键 |
||||
|
* @param index 键的索引 |
||||
|
* @returns 存储键或 null |
||||
|
*/ |
||||
|
key(index: number): null | string { |
||||
|
try { |
||||
|
return this.storage.key(index); |
||||
|
} catch (error) { |
||||
|
console.error('Error getting key from storage', error); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取存储项的数量 |
||||
|
* @returns 存储项的数量 |
||||
|
*/ |
||||
|
length(): number { |
||||
|
try { |
||||
|
return this.storage.length; |
||||
|
} catch (error) { |
||||
|
console.error('Error getting storage length', error); |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除存储项 |
||||
|
* @param key 存储键 |
||||
|
*/ |
||||
|
removeItem(key: string): void { |
||||
|
const fullKey = this.getFullKey(key); |
||||
|
try { |
||||
|
this.storage.removeItem(fullKey); |
||||
|
} catch (error) { |
||||
|
console.error('Error removing item from storage', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置存储项 |
||||
|
* @param key 存储键 |
||||
|
* @param value 存储值 |
||||
|
* @param expiryInMinutes 过期时间(分钟) |
||||
|
*/ |
||||
|
setItem<T>(key: string, value: T, expiryInMinutes?: number): void { |
||||
|
const fullKey = this.getFullKey(key); |
||||
|
this.beforeSetItem(fullKey, value, expiryInMinutes); |
||||
|
|
||||
|
const now = Date.now(); |
||||
|
const expiry = expiryInMinutes ? now + expiryInMinutes * 60_000 : null; |
||||
|
|
||||
|
const storageValue: StorageValue<T> = { |
||||
|
data: value, |
||||
|
expiry, |
||||
|
}; |
||||
|
|
||||
|
try { |
||||
|
this.storage.setItem(fullKey, JSON.stringify(storageValue)); |
||||
|
} catch (error) { |
||||
|
console.error('Error setting item in storage', error); |
||||
|
} |
||||
|
|
||||
|
this.afterSetItem(fullKey, value, expiryInMinutes); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { StorageCache }; |
||||
@ -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,5 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/tsconfig", |
||||
|
"extends": "@vben/tsconfig/library.json", |
||||
|
"include": ["src"] |
||||
|
} |
||||
@ -1,6 +1,4 @@ |
|||||
:root.dark { |
:root.dark { |
||||
/* authentication */ |
/* authentication */ |
||||
--color-authentication: hsl(240deg 11% 2%); |
--color-authentication: hsl(240deg 11% 2%); |
||||
|
|
||||
color-scheme: dark; |
|
||||
} |
} |
||||
|
|||||
File diff suppressed because it is too large
Loading…
Reference in new issue