This commit is contained in:
2026-05-02 09:59:06 +08:00
parent ea3e60c17c
commit 1af4075e9b
249 changed files with 11209 additions and 14884 deletions

View File

@@ -1,4 +1,4 @@
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
const isLogin = () => !!SafeStorage.get(AppStorageKey.TOKEN)

View File

@@ -1,158 +1,158 @@
/**
* 类型安全的 localStorage 封装,具备严格的 key 管理
*/
// 1. 定义应用程序使用的存储键名(防止使用任意字符串作为 key
export enum AppStorageKey {
PASSPORT_DATA = 'passportData',
PASSPORT_TOKEN = 'passportToken',
MENU_DATA = 'menuData', // 全部的菜单数据
SLIDER_MENU = 'sliderMenu', // 侧边栏菜单数据
USER_INFO = 'userInfo',
TOKEN = 'token',
MODE = 'mode', // 日间夜间模式
}
// 2. 存储值类型定义(用于内部处理)
type StorageValue<T> = {
__data: T; // 实际存储的数据
__expiry?: number; // 可选的过期时间戳
};
// 3. 主存储类
class SafeStorage {
/**
* 存储数据(自动处理 JSON 序列化)
* @param key 预定义的存储键
* @param value 要存储的值(支持所有 JSON 安全类型)
* @param ttl 可选的时间有效期(毫秒)
*/
static set<T>(key: AppStorageKey, value: T, ttl?: number): void {
if (typeof window === "undefined") return;
try {
// 创建存储对象(包含数据值和过期时间)
const storageValue: StorageValue<T> = {
__data: value,
};
// 设置过期时间(如果提供)
if (ttl !== undefined && ttl > 0) {
storageValue.__expiry = Date.now() + ttl;
}
// 自动序列化并存储
localStorage.setItem(key, JSON.stringify(storageValue));
} catch (error) {
console.error(`[存储] 保存失败 (key: ${key})`, error);
this.handleStorageError(key, error);
PASSPORT_DATA = 'passportData',
PASSPORT_TOKEN = 'passportToken',
MENU_DATA = 'menuData', // 全部的菜单数据
SLIDER_MENU = 'sliderMenu', // 侧边栏菜单数据
USER_INFO = 'userInfo',
TOKEN = 'token',
MODE = 'mode', // 日间夜间模式
}
// 2. 存储值类型定义(用于内部处理)
type StorageValue<T> = {
__data: T // 实际存储的数据
__expiry?: number // 可选的过期时间戳
}
// 3. 主存储类
class SafeStorage {
/**
* 存储数据(自动处理 JSON 序列化)
* @param key 预定义的存储键
* @param value 要存储的值(支持所有 JSON 安全类型)
* @param ttl 可选的时间有效期(毫秒)
*/
static set<T>(key: AppStorageKey, value: T, ttl?: number): void {
if (typeof window === 'undefined') return
try {
// 创建存储对象(包含数据值和过期时间)
const storageValue: StorageValue<T> = {
__data: value,
}
}
/**
* 获取数据(自动反序列化并检查过期)
* @param key 预定义的存储键
* @returns 存储的值或 null如果不存在或过期
*/
static get<T>(key: AppStorageKey): T | null {
if (typeof window === "undefined") return null;
try {
// 获取原始数据
const rawData = localStorage.getItem(key);
if (!rawData) return null;
// 解析为内部存储结构
const storageValue = JSON.parse(rawData) as StorageValue<T>;
// 检查是否过期
if (this.isExpired(storageValue)) {
this.remove(key);
return null;
}
// 返回实际数据
return storageValue.__data;
} catch (error) {
console.error(`[存储] 解析失败 (key: ${key})`, error);
this.remove(key); // 移除无效数据
return null;
// 设置过期时间(如果提供)
if (ttl !== undefined && ttl > 0) {
storageValue.__expiry = Date.now() + ttl
}
}
/**
* 删除指定存储项
* @param key 要删除的存储键
*/
static remove(key: AppStorageKey): void {
if (typeof window === "undefined") return;
localStorage.removeItem(key);
}
/**
* 清除应用相关的所有存储
*/
static clearAppStorage(): void {
if (typeof window === "undefined") return;
// 遍历所有预定义的 key 进行删除
Object.values(AppStorageKey).forEach(key => {
localStorage.removeItem(key);
});
}
// ----------------------------- 私有辅助方法 -----------------------------
/**
* 检查存储值是否过期
*/
private static isExpired<T>(value: StorageValue<T>): boolean {
return value.__expiry !== undefined && Date.now() > value.__expiry;
}
/**
* 处理存储错误(配额不足等)
*/
private static handleStorageError(key: AppStorageKey, error: unknown): void {
// 处理存储空间不足错误
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
console.warn('存储空间不足,尝试清理过期数据');
this.clearExpiredItems();
// 尝试重新存储(最多尝试一次)
try {
const raw = localStorage.getItem(key);
if (raw) {
const value = JSON.parse(raw) as StorageValue<unknown>;
if (!this.isExpired(value)) {
localStorage.setItem(key, raw);
return;
}
}
} catch (retryError) {
console.error('重试存储失败', retryError);
}
}
}
/**
* 清理所有过期的存储项
*/
private static clearExpiredItems(): void {
Object.values(AppStorageKey).forEach(key => {
try {
const raw = localStorage.getItem(key);
if (raw) {
const value = JSON.parse(raw) as StorageValue<unknown>;
if (this.isExpired(value)) {
localStorage.removeItem(key);
}
}
} catch (e) {
// 忽略无效数据
}
});
// 自动序列化并存储
localStorage.setItem(key, JSON.stringify(storageValue))
} catch (error) {
console.error(`[存储] 保存失败 (key: ${key})`, error)
this.handleStorageError(key, error)
}
}
export default SafeStorage;
/**
* 获取数据(自动反序列化并检查过期)
* @param key 预定义的存储键
* @returns 存储的值或 null如果不存在或过期
*/
static get<T>(key: AppStorageKey): T | null {
if (typeof window === 'undefined') return null
try {
// 获取原始数据
const rawData = localStorage.getItem(key)
if (!rawData) return null
// 解析为内部存储结构
const storageValue = JSON.parse(rawData) as StorageValue<T>
// 检查是否过期
if (this.isExpired(storageValue)) {
this.remove(key)
return null
}
// 返回实际数据
return storageValue.__data
} catch (error) {
console.error(`[存储] 解析失败 (key: ${key})`, error)
this.remove(key) // 移除无效数据
return null
}
}
/**
* 删除指定存储项
* @param key 要删除的存储键
*/
static remove(key: AppStorageKey): void {
if (typeof window === 'undefined') return
localStorage.removeItem(key)
}
/**
* 清除应用相关的所有存储
*/
static clearAppStorage(): void {
if (typeof window === 'undefined') return
// 遍历所有预定义的 key 进行删除
Object.values(AppStorageKey).forEach((key) => {
localStorage.removeItem(key)
})
}
// ----------------------------- 私有辅助方法 -----------------------------
/**
* 检查存储值是否过期
*/
private static isExpired<T>(value: StorageValue<T>): boolean {
return value.__expiry !== undefined && Date.now() > value.__expiry
}
/**
* 处理存储错误(配额不足等)
*/
private static handleStorageError(key: AppStorageKey, error: unknown): void {
// 处理存储空间不足错误
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
console.warn('存储空间不足,尝试清理过期数据')
this.clearExpiredItems()
// 尝试重新存储(最多尝试一次)
try {
const raw = localStorage.getItem(key)
if (raw) {
const value = JSON.parse(raw) as StorageValue<unknown>
if (!this.isExpired(value)) {
localStorage.setItem(key, raw)
return
}
}
} catch (retryError) {
console.error('重试存储失败', retryError)
}
}
}
/**
* 清理所有过期的存储项
*/
private static clearExpiredItems(): void {
Object.values(AppStorageKey).forEach((key) => {
try {
const raw = localStorage.getItem(key)
if (raw) {
const value = JSON.parse(raw) as StorageValue<unknown>
if (this.isExpired(value)) {
localStorage.removeItem(key)
}
}
} catch (e) {
// 忽略无效数据
}
})
}
}
export default SafeStorage

View File

@@ -42,16 +42,8 @@ export interface BuildTreeResult<T extends TreeNodeBase> {
* @param options 构建选项
* @returns 包含根节点列表和节点映射的对象
*/
export function buildTree<T extends TreeNodeBase>(
items: T[],
options: BuildTreeOptions<T> = {}
): BuildTreeResult<T> {
const {
idKey = 'id' as keyof T,
parentKey = 'parent_id' as keyof T,
childrenKey = 'children',
orderKey,
} = options
export function buildTree<T extends TreeNodeBase>(items: T[], options: BuildTreeOptions<T> = {}): BuildTreeResult<T> {
const { idKey = 'id' as keyof T, parentKey = 'parent_id' as keyof T, childrenKey = 'children', orderKey } = options
const itemMap = new Map<number | string, T>()
const rootItems: T[] = []
@@ -61,10 +53,13 @@ export function buildTree<T extends TreeNodeBase>(
const id = item[idKey]
if (id !== undefined && id !== null) {
// 创建带有空 children 数组的节点副本
itemMap.set(id as number | string, {
...item,
[childrenKey]: []
} as T)
itemMap.set(
id as number | string,
{
...item,
[childrenKey]: [],
} as T
)
}
})
@@ -164,7 +159,7 @@ export function getAncestors<T extends TreeNodeBase>(
): T[] {
const ancestors: T[] = []
let current = itemMap.get(nodeId)
while (current) {
const parentId = current[parentKey]
if (parentId && itemMap.has(parentId as number | string)) {
@@ -175,7 +170,7 @@ export function getAncestors<T extends TreeNodeBase>(
break
}
}
return ancestors
}
@@ -185,20 +180,17 @@ export function getAncestors<T extends TreeNodeBase>(
* @param childrenKey 子节点字段名
* @returns 所有子孙节点(扁平数组)
*/
export function getDescendants<T extends TreeNodeBase>(
node: T,
childrenKey: string = 'children'
): T[] {
export function getDescendants<T extends TreeNodeBase>(node: T, childrenKey: string = 'children'): T[] {
const descendants: T[] = []
const children = node[childrenKey] as T[]
if (children && children.length > 0) {
for (const child of children) {
descendants.push(child)
descendants.push(...getDescendants(child, childrenKey))
}
}
return descendants
}
@@ -208,16 +200,17 @@ export function getDescendants<T extends TreeNodeBase>(
* @param childrenKey 子节点字段名
* @returns 扁平化后的数组(移除 children 属性)
*/
export function flattenTree<T extends TreeNodeBase>(
nodes: T[],
childrenKey: string = 'children'
): Omit<T, 'children'>[] {
export function flattenTree<T extends TreeNodeBase>(nodes: T[], childrenKey: string = 'children'): Omit<T, 'children'>[] {
const result: Omit<T, 'children'>[] = []
traverseTree(nodes, (node) => {
const { [childrenKey]: _, ...rest } = node
result.push(rest as Omit<T, 'children'>)
}, childrenKey)
traverseTree(
nodes,
(node) => {
const { [childrenKey]: _, ...rest } = node
result.push(rest as Omit<T, 'children'>)
},
childrenKey
)
return result
}