feat
This commit is contained in:
@@ -1,19 +1,15 @@
|
||||
const TOKEN_KEY = 'token'
|
||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
|
||||
|
||||
const isLogin = () => {
|
||||
return !!localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
const isLogin = () => !!SafeStorage.get(AppStorageKey.TOKEN)
|
||||
|
||||
const getToken = () => {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
const getToken = () => SafeStorage.get(AppStorageKey.TOKEN)
|
||||
|
||||
const setToken = (token: string) => {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
SafeStorage.set(AppStorageKey.TOKEN, token)
|
||||
}
|
||||
|
||||
const clearToken = () => {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
SafeStorage.remove(AppStorageKey.TOKEN)
|
||||
}
|
||||
|
||||
export { clearToken, getToken, isLogin, setToken }
|
||||
|
||||
158
src/utils/safeStorage.ts
Normal file
158
src/utils/safeStorage.ts
Normal file
@@ -0,0 +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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据(自动反序列化并检查过期)
|
||||
* @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;
|
||||
223
src/utils/tree.ts
Normal file
223
src/utils/tree.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 树结构相关工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 树节点基础接口
|
||||
*/
|
||||
export interface TreeNodeBase {
|
||||
id?: number | string
|
||||
parent_id?: number | string | null
|
||||
children?: TreeNodeBase[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树结构的选项
|
||||
*/
|
||||
export interface BuildTreeOptions<T extends TreeNodeBase> {
|
||||
/** ID 字段名,默认 'id' */
|
||||
idKey?: keyof T
|
||||
/** 父ID 字段名,默认 'parent_id' */
|
||||
parentKey?: keyof T
|
||||
/** 子节点字段名,默认 'children' */
|
||||
childrenKey?: string
|
||||
/** 排序字段名,可选 */
|
||||
orderKey?: keyof T
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树结构的结果
|
||||
*/
|
||||
export interface BuildTreeResult<T extends TreeNodeBase> {
|
||||
/** 根节点列表 */
|
||||
rootItems: T[]
|
||||
/** 节点映射表 (id -> node) */
|
||||
itemMap: Map<number | string, T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 将扁平数组构建为树状结构
|
||||
* @param items 扁平数组
|
||||
* @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
|
||||
|
||||
const itemMap = new Map<number | string, T>()
|
||||
const rootItems: T[] = []
|
||||
|
||||
// 创建节点映射
|
||||
items.forEach((item) => {
|
||||
const id = item[idKey]
|
||||
if (id !== undefined && id !== null) {
|
||||
// 创建带有空 children 数组的节点副本
|
||||
itemMap.set(id as number | string, {
|
||||
...item,
|
||||
[childrenKey]: []
|
||||
} as T)
|
||||
}
|
||||
})
|
||||
|
||||
// 构建树结构
|
||||
itemMap.forEach((item) => {
|
||||
const parentId = item[parentKey]
|
||||
if (parentId && itemMap.has(parentId as number | string)) {
|
||||
const parent = itemMap.get(parentId as number | string)!
|
||||
const parentChildren = parent[childrenKey] as T[]
|
||||
parentChildren.push(item)
|
||||
} else {
|
||||
rootItems.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
// 排序函数
|
||||
if (orderKey) {
|
||||
const sortByOrder = (a: T, b: T) => {
|
||||
const orderA = (a[orderKey] as number) || 0
|
||||
const orderB = (b[orderKey] as number) || 0
|
||||
return orderA - orderB
|
||||
}
|
||||
|
||||
rootItems.sort(sortByOrder)
|
||||
itemMap.forEach((item) => {
|
||||
const children = item[childrenKey] as T[]
|
||||
if (children && children.length > 0) {
|
||||
children.sort(sortByOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { rootItems, itemMap }
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历树节点
|
||||
* @param nodes 节点列表
|
||||
* @param callback 回调函数
|
||||
* @param childrenKey 子节点字段名
|
||||
*/
|
||||
export function traverseTree<T extends TreeNodeBase>(
|
||||
nodes: T[],
|
||||
callback: (node: T, depth: number, parent: T | null) => void | boolean,
|
||||
childrenKey: string = 'children',
|
||||
depth: number = 0,
|
||||
parent: T | null = null
|
||||
): void {
|
||||
for (const node of nodes) {
|
||||
const result = callback(node, depth, parent)
|
||||
// 如果回调返回 false,停止遍历
|
||||
if (result === false) return
|
||||
|
||||
const children = node[childrenKey] as T[]
|
||||
if (children && children.length > 0) {
|
||||
traverseTree(children, callback, childrenKey, depth + 1, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在树中查找节点
|
||||
* @param nodes 节点列表
|
||||
* @param predicate 判断条件
|
||||
* @param childrenKey 子节点字段名
|
||||
* @returns 找到的节点或 null
|
||||
*/
|
||||
export function findInTree<T extends TreeNodeBase>(
|
||||
nodes: T[],
|
||||
predicate: (node: T) => boolean,
|
||||
childrenKey: string = 'children'
|
||||
): T | null {
|
||||
for (const node of nodes) {
|
||||
if (predicate(node)) {
|
||||
return node
|
||||
}
|
||||
const children = node[childrenKey] as T[]
|
||||
if (children && children.length > 0) {
|
||||
const found = findInTree(children, predicate, childrenKey)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的所有父节点路径
|
||||
* @param nodeId 目标节点ID
|
||||
* @param itemMap 节点映射表
|
||||
* @param parentKey 父ID字段名
|
||||
* @returns 从根到目标节点的路径(不包含目标节点本身)
|
||||
*/
|
||||
export function getAncestors<T extends TreeNodeBase>(
|
||||
nodeId: number | string,
|
||||
itemMap: Map<number | string, T>,
|
||||
parentKey: keyof T = 'parent_id' as keyof T
|
||||
): T[] {
|
||||
const ancestors: T[] = []
|
||||
let current = itemMap.get(nodeId)
|
||||
|
||||
while (current) {
|
||||
const parentId = current[parentKey]
|
||||
if (parentId && itemMap.has(parentId as number | string)) {
|
||||
const parent = itemMap.get(parentId as number | string)!
|
||||
ancestors.unshift(parent)
|
||||
current = parent
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的所有子孙节点
|
||||
* @param node 目标节点
|
||||
* @param childrenKey 子节点字段名
|
||||
* @returns 所有子孙节点(扁平数组)
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 将树结构扁平化为数组
|
||||
* @param nodes 节点列表
|
||||
* @param childrenKey 子节点字段名
|
||||
* @returns 扁平化后的数组(移除 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)
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user