This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
import { buildCachedRegFn, buildRegFn } from '../buildReg';
describe('buildRegFn should be ok', () => {
test('normal', () => {
const [get, set] = buildRegFn('test');
const fn = jest.fn();
set(fn);
get();
get(1);
get(2);
const fn2 = jest.fn();
set(fn2);
get(3);
expect(fn.mock.calls.length).toBe(3);
expect(fn.mock.calls[0]).toEqual([]);
expect(fn.mock.calls[1]).toEqual([1]);
expect(fn.mock.calls[2]).toEqual([2]);
expect(fn2.mock.calls[0]).toEqual([3]);
});
test('with default', () => {
const fn = jest.fn();
const [get, set] = buildRegFn('test', fn);
get();
get(1);
get(2);
const fn2 = jest.fn();
set(fn2);
get(3);
expect(fn.mock.calls.length).toBe(3);
expect(fn.mock.calls[0]).toEqual([]);
expect(fn.mock.calls[1]).toEqual([1]);
expect(fn.mock.calls[2]).toEqual([2]);
expect(fn2.mock.calls[0]).toEqual([3]);
});
});
describe('buildCachedRegFn should be ok', () => {
test('normal', () => {
const [get, set] = buildCachedRegFn('test');
const fn = jest.fn();
set(fn);
get(1);
const fn2 = jest.fn();
set(fn2);
get(2);
expect(fn.mock.calls[0]).toEqual([1]);
expect(fn2.mock.calls[0]).toEqual([2]);
});
test('should be cache value', () => {
const [get, set] = buildCachedRegFn('test');
const fn = jest.fn((v) => v);
set(fn);
const res1 = get(1);
const res2 = get(1);
expect(fn.mock.calls.length).toBe(1);
expect(fn.mock.calls[0]).toEqual([1]);
expect(res1).toBe(res2);
});
test('should be refresh if re-set', () => {
const [get, set] = buildCachedRegFn('test');
const fn = jest.fn((v) => v);
set(fn);
get(1);
get(1);
const fn2 = jest.fn((v) => v);
set(fn2);
get(1);
get(1);
expect(fn.mock.calls.length).toBe(1);
expect(fn.mock.calls[0]).toEqual([1]);
expect(fn2.mock.calls.length).toBe(1);
expect(fn2.mock.calls[0]).toEqual([1]);
});
test('should call forever if return null', () => {
const [get, set] = buildCachedRegFn('test');
const fn = jest.fn(() => null);
set(fn);
get();
get();
get();
expect(fn.mock.calls.length).toBe(3);
});
test('should call forever if return undefined', () => {
const [get, set] = buildCachedRegFn('test');
const fn = jest.fn(() => undefined);
set(fn);
get();
get();
get();
expect(fn.mock.calls.length).toBe(3);
});
test('should cache promise fn', () => {
const [get, set] = buildCachedRegFn('test');
const fn = jest.fn(async () => undefined);
set(fn);
get();
get();
get();
expect(fn.mock.calls.length).toBe(1);
get(1);
expect(fn.mock.calls.length).toBe(2);
});
});

View File

@@ -0,0 +1,173 @@
import _isFunction from 'lodash/isFunction';
import _isEqual from 'lodash/isEqual';
/**
* 构建一对get set 方法
* 用于在不同平台拥有统一方式调用体验
*/
export function buildRegFn<F extends (...args: any[]) => any>(
name: string,
defaultFunc?: F
) {
let func: F | undefined;
const get = (...args: Parameters<F>): ReturnType<F> => {
if (!func) {
if (_isFunction(defaultFunc)) {
return defaultFunc(...args);
}
throw new Error(`${name} not regist`);
}
return func(...args);
};
const set = (fn: F): void => {
func = fn;
};
const reset = (): void => {
func = defaultFunc;
};
return [get, set, reset] as const;
}
/**
* 构建带事件监听的get set 方法
*/
export function buildRegFnWithEvent<F extends (...args: any[]) => any>(
name: string,
defaultFunc?: F
) {
const [get, _set] = buildRegFn(name, defaultFunc);
const listenerList: ((v: F) => void)[] = [];
const onSet = (cb: (v: F) => void): void => {
listenerList.push(cb);
};
const set = (fn: F): void => {
_set(fn);
listenerList.forEach((listener) => listener(fn));
};
return [get, set, onSet] as const;
}
/**
* 缓存版本的buildRegFn
*/
export function buildCachedRegFn<F extends (...args: any) => any>(
name: string,
defaultFunc?: F
) {
const [get, set] = buildRegFn(name, defaultFunc);
let _result: any = null; // 缓存的返回值
let _lastArgs: any;
function isSame(args: any[]) {
// 当有缓存的返回值且两次参数一致
return _result !== null && _isEqual(args, _lastArgs);
}
// 根据是否为 promise 做区分
const cachedGet: any = (...args: any) => {
if (isSame(args)) {
return _result;
} else {
const result = get(...args);
_result = result ?? null;
_lastArgs = args;
return result;
}
};
const refreshCache = () => {
_result = null;
};
const cachedSet = (fn: F) => {
set(fn);
refreshCache();
};
return [cachedGet, cachedSet, refreshCache];
}
/**
* 缓存版本的buildRegFn
* Promise 版本
*/
export function buildCachedRegFnAsync<F extends (...args: any) => any>(
name: string,
defaultFunc?: F
) {
const [get, set] = buildRegFn(name, defaultFunc);
let _result: any = null; // 缓存的返回值
let _lastArgs: any;
function isSame(args: any[]) {
// 当有缓存的返回值且两次参数一致
return _result !== null && _isEqual(args, _lastArgs);
}
// 根据是否为 promise 做区分
const cachedGet: any = async (...args: any) => {
if (isSame(args)) {
return _result;
} else {
const result = await get(...args);
_result = result ?? null;
_lastArgs = args;
return result;
}
};
const refreshCache = () => {
_result = null;
};
const cachedSet = (fn: F) => {
set(fn);
refreshCache();
};
return [cachedGet, cachedSet, refreshCache];
}
/**
* 构建一组注册列表的方式
* 用于从其他地方统一获取数据
*/
export function buildRegList<T>(): [T[], (item: T) => void] {
const list: T[] = [];
const reg = (item: T) => {
list.push(item);
};
return [list, reg];
}
/**
* 构建一组注册Mapping的方式
* 用于从其他地方统一获取数据
*/
export function buildRegMap<T>(): [
Record<string, T>,
(name: string, item: T) => void
] {
const mapping: Record<string, T> = {};
const reg = (name: string, item: T) => {
if (mapping[name]) {
console.warn('[buildRegMap] 重复注册:', name);
}
mapping[name] = item;
};
return [mapping, reg];
}

View File

@@ -0,0 +1,9 @@
import { buildRegFn, buildCachedRegFnAsync } from './buildReg';
export const [getErrorHook, setErrorHook] = buildRegFn<(err: any) => boolean>(
'requestErrorHook',
() => true
);
export const [tokenGetter, setTokenGetter, refreshTokenGetter] =
buildCachedRegFnAsync<() => Promise<string>>('requestTokenGetter');

View File

@@ -0,0 +1,8 @@
import { buildRegFnWithEvent } from './buildReg';
/**
* 获取服务器地址相关逻辑
* @default window.location.origin (前后端一体)
*/
export const [getServiceUrl, setServiceUrl, onServiceUrlChange] =
buildRegFnWithEvent<() => string>('serverUrl', () => window.location.origin);

View File

@@ -0,0 +1,8 @@
import { buildRegList } from './buildReg';
interface PluginSocketEventListener {
eventName: string;
eventFn: (...args: any[]) => void;
}
export const [socketEventListeners, regSocketEventListener] =
buildRegList<PluginSocketEventListener>();

View File

@@ -0,0 +1,66 @@
import { buildRegFn } from './buildReg';
import { useCallback, useLayoutEffect, useState } from 'react';
export interface StorageObject {
/**
* NOTICE: 与save不同 set存储 1 天
*/
set: (key: string, data: any) => Promise<void>;
get: (key: string, defaultVal?: any) => Promise<any>;
remove: (key: string) => Promise<void>;
save: (key: string, data: any) => Promise<void>;
}
/**
* 持久化存储相关逻辑
*/
export const [getStorage, setStorage] =
buildRegFn<() => StorageObject>('storage');
/**
* 持久化hook的缓存, 减少因一步获取数据导致的gap
*/
const useStorageCache = new Map<string, any>();
/**
* 管理持久化存储数据
* @param key 存储键
* @param defaultValue 默认值
*/
export function useStorage<T = any>(
key: string,
defaultValue?: T
): [T | undefined, { set: (v: T) => void; save: (v: T) => void }] {
const [value, setValue] = useState<T | undefined>(
useStorageCache.get(key) ?? defaultValue
);
useLayoutEffect(() => {
getStorage()
.get(key)
.then((data: T) => {
setValue(data ?? defaultValue);
useStorageCache.set(key, data ?? defaultValue);
});
}, [key]);
const set = useCallback(
(newVal: T) => {
setValue(newVal);
getStorage().set(key, newVal);
useStorageCache.set(key, newVal);
},
[key]
);
const save = useCallback(
(newVal: T) => {
setValue(newVal);
getStorage().save(key, newVal);
useStorageCache.set(key, newVal);
},
[key]
);
return [value, { set, save }];
}

View File

@@ -0,0 +1,57 @@
import { t } from '../i18n';
import { buildRegFn } from './buildReg';
/**
* 通用UI api设置
*/
type ToastsType = 'info' | 'success' | 'error' | 'warning';
export const [showToasts, setToasts] =
buildRegFn<(message: string, type?: ToastsType) => void>('toasts');
/**
* 一个封装方法, 用于直接抛出错误
* @param error 错误信息
*/
export function showErrorToasts(error: unknown) {
let msg = '';
if (error instanceof Error) {
msg = error.message;
} else {
msg = String(error);
}
showToasts(msg, 'error');
}
/**
* 展示成功消息
*/
export function showSuccessToasts(msg = t('操作成功')) {
showToasts(msg, 'success');
}
interface AlertOptions {
message: React.ReactNode;
onConfirm?: () => void | Promise<void>;
}
export const [showAlert, setAlert] =
buildRegFn<(options: AlertOptions) => void>('alert');
/**
* 全局Loading提示
* 返回移除函数
*/
export const [showGlobalLoading, setGlobalLoading] = buildRegFn<
(text: string) => () => void
>('global-loading', () => {
return () => {};
});
/**
* 提示通知,返回关闭函数
*/
export const [showNotification, setNotification] =
buildRegFn<(message: React.ReactNode, duration?: number) => () => void>(
'notification'
);