donar 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspell/custom-dictionary.txt +5 -0
- package/.github/actions/base-check/action.yaml +27 -0
- package/.github/actions/env-setup/action.yaml +74 -0
- package/.github/workflows/pr-check.yaml +33 -0
- package/.github/workflows/release.yaml +112 -0
- package/.gitignore +41 -0
- package/.node-version +1 -0
- package/.npmignore +7 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +44 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/commitlint.config.ts +3 -0
- package/cspell.json +27 -0
- package/dist/components.esm.js +2 -0
- package/dist/css/components.C24nnsjt.css +1 -0
- package/dist/hooks.esm.js +185 -0
- package/dist/index.esm.js +4 -0
- package/dist/js/components.nFDoAkCq.js +198 -0
- package/dist/js/utils.CVb1iSAU.js +330 -0
- package/dist/types/components.d.ts +1 -0
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/src/components/async-custom-show/index.d.ts +22 -0
- package/dist/types/src/components/carousel/hooks.d.ts +74 -0
- package/dist/types/src/components/carousel/index.d.ts +88 -0
- package/dist/types/src/components/custom-show/index.d.ts +21 -0
- package/dist/types/src/components/error-boundary/index.d.ts +31 -0
- package/dist/types/src/components/index.d.ts +8 -0
- package/dist/types/src/hooks/async-guard.d.ts +9 -0
- package/dist/types/src/hooks/index.d.ts +5 -0
- package/dist/types/src/hooks/observer.d.ts +70 -0
- package/dist/types/src/hooks/state.d.ts +44 -0
- package/dist/types/src/hooks/storage.d.ts +25 -0
- package/dist/types/src/hooks/timer.d.ts +16 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/test/emiter-test.d.ts +1 -0
- package/dist/types/src/test/index.d.ts +1 -0
- package/dist/types/src/test/retry-async-test.d.ts +1 -0
- package/dist/types/src/utils/class-singleton.d.ts +6 -0
- package/dist/types/src/utils/concurrency.d.ts +17 -0
- package/dist/types/src/utils/debounce.d.ts +8 -0
- package/dist/types/src/utils/deep-copy.d.ts +11 -0
- package/dist/types/src/utils/dev.d.ts +8 -0
- package/dist/types/src/utils/download.d.ts +15 -0
- package/dist/types/src/utils/event-emitter/helpers.d.ts +36 -0
- package/dist/types/src/utils/event-emitter/index.d.ts +65 -0
- package/dist/types/src/utils/fetch-xhr/helpers.d.ts +28 -0
- package/dist/types/src/utils/fetch-xhr/index.d.ts +25 -0
- package/dist/types/src/utils/hash.d.ts +8 -0
- package/dist/types/src/utils/index.d.ts +15 -0
- package/dist/types/src/utils/is-deep-plain-equal.d.ts +15 -0
- package/dist/types/src/utils/json-convert.d.ts +66 -0
- package/dist/types/src/utils/micro-queue-scheduler.d.ts +14 -0
- package/dist/types/src/utils/raf-interval.d.ts +16 -0
- package/dist/types/src/utils/record-typed-map.d.ts +27 -0
- package/dist/types/src/utils/retry-async.d.ts +9 -0
- package/dist/types/src/utils/thenable.d.ts +15 -0
- package/dist/types/utils.d.ts +1 -0
- package/dist/utils.esm.js +2 -0
- package/eslint.config.ts +48 -0
- package/lint-staged.config.ts +13 -0
- package/oxfmt.config.ts +14 -0
- package/package.json +90 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/sync-node-version/index.js +31 -0
- package/simple-git-hooks.js +4 -0
- package/src/components/async-custom-show/index.tsx +37 -0
- package/src/components/carousel/hooks.ts +312 -0
- package/src/components/carousel/index.module.scss +163 -0
- package/src/components/carousel/index.tsx +215 -0
- package/src/components/custom-show/index.tsx +31 -0
- package/src/components/error-boundary/index.tsx +48 -0
- package/src/components/index.ts +11 -0
- package/src/hooks/async-guard.ts +53 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/observer.ts +236 -0
- package/src/hooks/state.ts +140 -0
- package/src/hooks/storage.ts +103 -0
- package/src/hooks/timer.ts +42 -0
- package/src/index.ts +3 -0
- package/src/test/emiter-test.ts +19 -0
- package/src/test/index.ts +35 -0
- package/src/test/retry-async-test.ts +8 -0
- package/src/utils/class-singleton.ts +23 -0
- package/src/utils/concurrency.ts +49 -0
- package/src/utils/debounce.ts +20 -0
- package/src/utils/deep-copy.ts +132 -0
- package/src/utils/dev.ts +16 -0
- package/src/utils/download.ts +39 -0
- package/src/utils/event-emitter/helpers.ts +80 -0
- package/src/utils/event-emitter/index.ts +171 -0
- package/src/utils/fetch-xhr/helpers.ts +85 -0
- package/src/utils/fetch-xhr/index.ts +103 -0
- package/src/utils/hash.ts +25 -0
- package/src/utils/index.ts +18 -0
- package/src/utils/is-deep-plain-equal.ts +45 -0
- package/src/utils/json-convert.ts +257 -0
- package/src/utils/micro-queue-scheduler.ts +38 -0
- package/src/utils/raf-interval.ts +42 -0
- package/src/utils/record-typed-map.ts +38 -0
- package/src/utils/retry-async.ts +30 -0
- package/src/utils/thenable.ts +30 -0
- package/tsconfig.json +43 -0
- package/types/scss.d.ts +10 -0
- package/vite.config.ts +51 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FC,
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { useRAfInterval } from '@/hooks';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ShowArrowType,
|
|
13
|
+
type RerenderType,
|
|
14
|
+
type NonLoopEndFuncType,
|
|
15
|
+
usePlayControl,
|
|
16
|
+
useLoopChildren,
|
|
17
|
+
} from './hooks';
|
|
18
|
+
|
|
19
|
+
import styles from './index.module.scss';
|
|
20
|
+
|
|
21
|
+
type IndicatorType = 'line' | 'dot' | 'none';
|
|
22
|
+
export type CarouselProps = {
|
|
23
|
+
/** 单个轮播项的宽度 */
|
|
24
|
+
cardWidth?: number;
|
|
25
|
+
/** 单个轮播项的高度 */
|
|
26
|
+
cardHeight?: number;
|
|
27
|
+
/** 轮播容器的宽度 */
|
|
28
|
+
wrapperWidth?: number;
|
|
29
|
+
/** 轮播容器的高度 */
|
|
30
|
+
wrapperHeight?: number;
|
|
31
|
+
/** 轮播项之间的间隔 */
|
|
32
|
+
gapSize?: number;
|
|
33
|
+
/** 是否无缝轮播 */
|
|
34
|
+
loop?: boolean; // 是否循环
|
|
35
|
+
/** 切换时间(毫秒) */
|
|
36
|
+
duration?: number;
|
|
37
|
+
/** 是否自动播放 */
|
|
38
|
+
autoPlay?: boolean;
|
|
39
|
+
/** 箭头类型 */
|
|
40
|
+
showArrow?: ShowArrowType;
|
|
41
|
+
/** 指示器类型 */
|
|
42
|
+
indicatorType?: IndicatorType;
|
|
43
|
+
/** 主轮播类名,不包括指示器 */
|
|
44
|
+
className?: string;
|
|
45
|
+
/** 主轮播样式,不包括指示器 */
|
|
46
|
+
style?: CSSProperties;
|
|
47
|
+
/** 轮播项 */
|
|
48
|
+
children: ReactElement[];
|
|
49
|
+
/** 轮播项更新依赖方式 */
|
|
50
|
+
rerender?: RerenderType;
|
|
51
|
+
/** 非循环模式下,轮播结束回调 */
|
|
52
|
+
onNonLoopEnd?: NonLoopEndFuncType;
|
|
53
|
+
/** 非循环模式下,轮播结束时的偏移量。就是提前第几张就算结束。暂不支持延后。 */
|
|
54
|
+
offsetOnEnd?: number;
|
|
55
|
+
// 因外框可达于轮播项,一页可能展示多张,设置偏移时注意是否可能永远触发不到
|
|
56
|
+
/** 自定义渲染箭头 */
|
|
57
|
+
arrows?: [
|
|
58
|
+
/** 自定义渲染上一个箭头 */
|
|
59
|
+
FC<{ onClick: () => void; className?: string }>,
|
|
60
|
+
/** 自定义渲染下一个箭头 */
|
|
61
|
+
FC<{ onClick: () => void; className?: string }>,
|
|
62
|
+
];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type CarouselRef = {
|
|
66
|
+
/** 开始播放 */
|
|
67
|
+
run: () => void;
|
|
68
|
+
/** 停止播放 */
|
|
69
|
+
stop: () => void;
|
|
70
|
+
/** 切换到上一个或下一个 */
|
|
71
|
+
stepChange: (arg: 'prev' | 'next') => void;
|
|
72
|
+
/** 跳转到指定索引 */
|
|
73
|
+
jumpChange: (arg: number) => void;
|
|
74
|
+
/** 获取当前索引 */
|
|
75
|
+
getCurrentIndex: () => number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @author sonion
|
|
80
|
+
* @description 轮播组件,用于展示多个轮播项
|
|
81
|
+
* @param {CarouselProps} props - 组件的属性
|
|
82
|
+
* @param {number} [props.cardWidth] - 单个轮播项的宽度
|
|
83
|
+
* @param {number} [props.cardHeight] - 单个轮播项的高度
|
|
84
|
+
* @param {number} [props.wrapperWidth] - 轮播容器的宽度
|
|
85
|
+
* @param {number} [props.wrapperHeight] - 轮播容器的高度
|
|
86
|
+
* @param {number} [props.gapSize] - 轮播项之间的间隔
|
|
87
|
+
* @param {boolean} [props.loop] - 是否无缝循环
|
|
88
|
+
* @param {number} [props.duration=6000] - 切换时间(毫秒)
|
|
89
|
+
* @param {boolean} [props.autoPlay=true] - 是否自动播放
|
|
90
|
+
* @param {ShowArrowType} [props.showArrow='always'] - 箭头类型
|
|
91
|
+
* @param {IndicatorType} [props.indicatorType='line'] - 指示器类型
|
|
92
|
+
* @param {string} [props.className] - 主轮播类名,不包括指示器
|
|
93
|
+
* @param {CSSProperties} [props.style] - 主轮播样式,不包括指示器
|
|
94
|
+
* @param {ReactElement[]} children - 轮播项
|
|
95
|
+
* @param {RerenderType} [props.rerender='length'] - 轮播项更新依赖方式
|
|
96
|
+
* @param {(direction: 'prev' | 'next', current: number, total: number, offset: number) => void} [props.onNonLoopEnd] - 非循环模式下,轮播结束回调。
|
|
97
|
+
* @param {number} [props.offsetOnEnd=0] - 非循环模式下,轮播结束时的偏移量。就是提前第几张就算结束。暂不支持延后。
|
|
98
|
+
* @param {[FC<{ onClick: () => void; className?: string }>, FC<{ onClick: () => void; className?: string }>]} [props.arrows] - 自定义渲染箭头
|
|
99
|
+
*/
|
|
100
|
+
const Carousel = forwardRef<CarouselRef, CarouselProps>(
|
|
101
|
+
(
|
|
102
|
+
{
|
|
103
|
+
cardWidth,
|
|
104
|
+
cardHeight,
|
|
105
|
+
wrapperWidth,
|
|
106
|
+
wrapperHeight,
|
|
107
|
+
gapSize,
|
|
108
|
+
loop = true,
|
|
109
|
+
duration = 6000,
|
|
110
|
+
autoPlay = true,
|
|
111
|
+
showArrow = 'always' as ShowArrowType,
|
|
112
|
+
indicatorType = 'line',
|
|
113
|
+
className,
|
|
114
|
+
style,
|
|
115
|
+
children,
|
|
116
|
+
rerender = 'length' as RerenderType, // 默认按children更新依赖其数量变化,可以减少重渲染
|
|
117
|
+
onNonLoopEnd,
|
|
118
|
+
offsetOnEnd = 0,
|
|
119
|
+
arrows,
|
|
120
|
+
},
|
|
121
|
+
ref
|
|
122
|
+
) => {
|
|
123
|
+
const list = useLoopChildren(children, loop, rerender);
|
|
124
|
+
const [setBannerRef, stepChange, jumpChange, getCurrentIndex] =
|
|
125
|
+
usePlayControl({
|
|
126
|
+
length: children.length,
|
|
127
|
+
loop,
|
|
128
|
+
showArrow,
|
|
129
|
+
cardWidth,
|
|
130
|
+
onNonLoopEnd,
|
|
131
|
+
offsetOnEnd,
|
|
132
|
+
});
|
|
133
|
+
const [run, stop] = useRAfInterval(stepChange, duration);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (autoPlay) {
|
|
137
|
+
run();
|
|
138
|
+
}
|
|
139
|
+
return stop;
|
|
140
|
+
}, [run, stop, autoPlay]);
|
|
141
|
+
useImperativeHandle(
|
|
142
|
+
ref,
|
|
143
|
+
() => ({
|
|
144
|
+
run,
|
|
145
|
+
stop,
|
|
146
|
+
stepChange,
|
|
147
|
+
jumpChange,
|
|
148
|
+
getCurrentIndex,
|
|
149
|
+
}),
|
|
150
|
+
[run, stop, stepChange, jumpChange, getCurrentIndex]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<div
|
|
156
|
+
className={`${styles.carousel} ${className ?? ''}`}
|
|
157
|
+
style={
|
|
158
|
+
{
|
|
159
|
+
...style,
|
|
160
|
+
'--wrapperWidth': wrapperWidth,
|
|
161
|
+
'--wrapperHeight': wrapperHeight,
|
|
162
|
+
'--cardWidth': cardWidth,
|
|
163
|
+
'--cardHeight': cardHeight,
|
|
164
|
+
'--gapSize': gapSize,
|
|
165
|
+
} as unknown as CSSProperties
|
|
166
|
+
}
|
|
167
|
+
onMouseEnter={stop}
|
|
168
|
+
onMouseLeave={autoPlay ? run : void 0}
|
|
169
|
+
>
|
|
170
|
+
<div className={`${styles.arrow} ${showArrow}`}>
|
|
171
|
+
{arrows?.[0] ? (
|
|
172
|
+
arrows[0]({
|
|
173
|
+
onClick: () => stepChange('prev'),
|
|
174
|
+
className: styles.left,
|
|
175
|
+
})
|
|
176
|
+
) : (
|
|
177
|
+
<i className={styles.left} onClick={() => stepChange('prev')} />
|
|
178
|
+
)}
|
|
179
|
+
{arrows?.[1] ? (
|
|
180
|
+
arrows[1]({
|
|
181
|
+
onClick: () => stepChange(),
|
|
182
|
+
className: styles.right,
|
|
183
|
+
})
|
|
184
|
+
) : (
|
|
185
|
+
<i className={styles.right} onClick={() => stepChange()} />
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className={styles.wrapper}>
|
|
190
|
+
<div className={styles.inner} ref={setBannerRef}>
|
|
191
|
+
{list}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div className={`${styles.indicator} ${indicatorType}`}>
|
|
196
|
+
{children.map((_, i) => (
|
|
197
|
+
<i
|
|
198
|
+
key={i}
|
|
199
|
+
onClick={(e) => {
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
jumpChange(i);
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
</>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
Carousel.displayName = 'Carousel'; // 调试/错误 信息友好;屏幕阅读器可识别组件
|
|
212
|
+
|
|
213
|
+
type CarouselType = typeof Carousel;
|
|
214
|
+
export type { ShowArrowType, RerenderType, CarouselType };
|
|
215
|
+
export default Carousel;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FC, ReactNode, JSX, PropsWithChildren } from 'react';
|
|
2
|
+
|
|
3
|
+
export type CustomShowProps<T> = PropsWithChildren<{
|
|
4
|
+
/** 判断条件 */
|
|
5
|
+
when: T | null | undefined | false;
|
|
6
|
+
/** 不满足条件时的渲染内容 */
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export interface CustomShowType<T> extends FC<CustomShowProps<T>> {
|
|
11
|
+
(props: CustomShowProps<T>): JSX.Element;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @author sonion
|
|
16
|
+
* @description 根据条件渲染内容,替代用三目运算和短路规则条件渲染
|
|
17
|
+
* @param {CustomShowProps<T>} props 组件属性
|
|
18
|
+
* @param {T | null | undefined | false} props.when 条件
|
|
19
|
+
* @param {ReactNode} props.fallback 不满足条件时的渲染内容
|
|
20
|
+
* @param {(value: T | null | undefined | false) => ReactNode} props.children 满足条件时的渲染内容
|
|
21
|
+
* @returns {JSX.Element} - 组件节点
|
|
22
|
+
*/
|
|
23
|
+
function CustomShow<T>({
|
|
24
|
+
when,
|
|
25
|
+
fallback = void 0,
|
|
26
|
+
children,
|
|
27
|
+
}: CustomShowProps<T>): JSX.Element {
|
|
28
|
+
return <>{when ? children : fallback}</>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default CustomShow;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ErrorBoundaryProps {
|
|
4
|
+
/** 正常展示 */
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** 错误发生时显示的回退内容 */
|
|
7
|
+
fallback: ReactNode;
|
|
8
|
+
/** 错误信息的提示文本 */
|
|
9
|
+
msg?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface State {
|
|
13
|
+
hasError: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @author sonion
|
|
18
|
+
* @description 错误边界组件,用于捕获子组件的错误并显示自定义的回退内容
|
|
19
|
+
* @param {ErrorBoundaryProps} props - 组件的属性
|
|
20
|
+
* @param {ReactNode} props.fallback - 错误发生时显示的回退内容
|
|
21
|
+
* @param {string} [props.msg='组件加载失败'] - 错误信息的提示文本
|
|
22
|
+
* @param {ReactNode} props.children - 子组件,可能会触发错误
|
|
23
|
+
* @returns {import('react').JSX.Element} - 组件节点
|
|
24
|
+
*/
|
|
25
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, State> {
|
|
26
|
+
state: State = { hasError: false };
|
|
27
|
+
private msg: string = this.props.msg ?? '组件加载失败';
|
|
28
|
+
|
|
29
|
+
static getDerivedStateFromError(): State {
|
|
30
|
+
return { hasError: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
34
|
+
console.error(`${this.msg}:`, error, errorInfo);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render() {
|
|
38
|
+
if (this.state.hasError) {
|
|
39
|
+
return this.props.fallback;
|
|
40
|
+
}
|
|
41
|
+
return this.props.children;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ErrorBoundaryType = typeof ErrorBoundary;
|
|
46
|
+
|
|
47
|
+
export type { ErrorBoundaryType };
|
|
48
|
+
export default ErrorBoundary;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './carousel';
|
|
2
|
+
export { default as Carousel } from './carousel'; // 默认导出需重命名
|
|
3
|
+
|
|
4
|
+
export * from './custom-show';
|
|
5
|
+
export { default as CustomShow } from './custom-show';
|
|
6
|
+
|
|
7
|
+
export * from './async-custom-show';
|
|
8
|
+
export { default as AsyncCustomShow } from './async-custom-show';
|
|
9
|
+
|
|
10
|
+
export * from './error-boundary';
|
|
11
|
+
export { default as ErrorBoundary } from './error-boundary';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { useLatestCallback } from './state';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @author sonion
|
|
6
|
+
* @description 异步操作锁,防止重复调用。通过返回的 Promise 确定是否执行中,执行中不可重复触发。
|
|
7
|
+
* @param {(...args: T)=>Promise<R>} asyncAction - 异步方法,需返回Promise
|
|
8
|
+
* @param {(isPending: boolean) => void} [onChange] - 状态变化回调
|
|
9
|
+
* @param {string} [msg] - 警告信息
|
|
10
|
+
* @returns {[boolean, (...args: T) => (Promise<R> | Promise<void>)]} - 返回是否执行中,执行方法
|
|
11
|
+
*/
|
|
12
|
+
export const useAsyncActionLock = <T extends unknown[], R>(
|
|
13
|
+
asyncAction: (...args: T) => Promise<R>,
|
|
14
|
+
onChange?: (isPending: boolean) => void,
|
|
15
|
+
msg?: string
|
|
16
|
+
) => {
|
|
17
|
+
const [isPending, setIsPending] = useState(false); // 对外可能需要触发渲染
|
|
18
|
+
const syncPending = useRef(isPending); // 对内,同步更改,返回的函数不更改
|
|
19
|
+
const getLatestOnChange = useLatestCallback(onChange); // 稳定函数引用
|
|
20
|
+
const setPending = useCallback(
|
|
21
|
+
(val: boolean) => {
|
|
22
|
+
syncPending.current = val;
|
|
23
|
+
setIsPending(val);
|
|
24
|
+
getLatestOnChange()?.(val);
|
|
25
|
+
},
|
|
26
|
+
[syncPending, setIsPending, getLatestOnChange]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const getLatestAsyncAction = useLatestCallback(asyncAction); // 稳定函数引用
|
|
30
|
+
|
|
31
|
+
const handler = useCallback<(...args: T) => Promise<R> | Promise<void>>(
|
|
32
|
+
(...args: T) => {
|
|
33
|
+
if (syncPending.current) {
|
|
34
|
+
console.clog(msg || '正在提交中,请稍后再试');
|
|
35
|
+
return Promise.resolve(void 0);
|
|
36
|
+
}
|
|
37
|
+
setPending(true);
|
|
38
|
+
return new Promise<R>((resolve, reject) => {
|
|
39
|
+
try {
|
|
40
|
+
const latestAsyncAction = getLatestAsyncAction();
|
|
41
|
+
resolve(latestAsyncAction?.(...args));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
reject(err);
|
|
44
|
+
}
|
|
45
|
+
}).finally(() => {
|
|
46
|
+
setPending(false);
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
[getLatestAsyncAction, msg, setPending]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return [isPending, handler] as const;
|
|
53
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useCreateSafeRef, useLatestCallback } from './index';
|
|
4
|
+
|
|
5
|
+
type MutationObserverCallback = (
|
|
6
|
+
entry: MutationRecord[],
|
|
7
|
+
observer: MutationObserver
|
|
8
|
+
) => void;
|
|
9
|
+
/**
|
|
10
|
+
* @author sonion
|
|
11
|
+
* @description dom变化观察器hook
|
|
12
|
+
* @param {MutationObserverCallback} callback - 回调函数,参数为 MutationRecord[]
|
|
13
|
+
* @param {boolean} [once] - 是否只观察一次,默认false
|
|
14
|
+
*/
|
|
15
|
+
export const useMutationObserver = (
|
|
16
|
+
callback: MutationObserverCallback,
|
|
17
|
+
once = false
|
|
18
|
+
) => {
|
|
19
|
+
const observerRef = useRef<MutationObserver>();
|
|
20
|
+
const getLatestCallback = useLatestCallback(callback);
|
|
21
|
+
|
|
22
|
+
useEffect(
|
|
23
|
+
() => () => {
|
|
24
|
+
observerRef.current?.disconnect?.(); // 上一次观察器取消,observe没有重新运行的话,观察就丢失了
|
|
25
|
+
observerRef.current = void 0;
|
|
26
|
+
},
|
|
27
|
+
[]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// MutationObserver 重新生成对象,observe 不会更新,用 observe 做依赖可能造成
|
|
31
|
+
// MutationObserver 更新了,但没有重新观察。但理论上只有 StrictMode 模式下会重新生成对象
|
|
32
|
+
// 且observe不是作为依赖,而是直接绑定dom或依赖其它重渲染会改变的状态就不会有问题。
|
|
33
|
+
// 或用Set存下历史观察,重生成时恢复,但不能用WeakSet,非必要不建议
|
|
34
|
+
/** 开始观察 因重新生成观察器observe不会更新,故不可以observe是否更新做依赖依据 */
|
|
35
|
+
const observe = useCallback(
|
|
36
|
+
(el: Node | null, options?: MutationObserverInit) => {
|
|
37
|
+
if (!el) return;
|
|
38
|
+
observerRef.current ??= new MutationObserver(
|
|
39
|
+
(mutations: MutationRecord[], observer: MutationObserver) => {
|
|
40
|
+
getLatestCallback()?.(mutations, observer);
|
|
41
|
+
once && observer.disconnect();
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
observerRef.current?.observe?.(el, options);
|
|
45
|
+
},
|
|
46
|
+
[once, getLatestCallback]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const takeRecords = useCallback(
|
|
50
|
+
() => observerRef.current?.takeRecords?.(),
|
|
51
|
+
[]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const disconnect = useCallback(() => observerRef.current?.disconnect?.(), []);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
/** 开始观察 因重新生成观察器observe不会更新,故不可以observe是否更新做依赖依据 */
|
|
58
|
+
observe,
|
|
59
|
+
/** 获取所有未处理的观察记录 */
|
|
60
|
+
takeRecords,
|
|
61
|
+
/** 取消所有观察,用observe重新启用 */
|
|
62
|
+
disconnect,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type IntersectionObserverCallback = (
|
|
67
|
+
/** 交叉目标 */
|
|
68
|
+
target: Element,
|
|
69
|
+
/** 交叉目标的根元素 */
|
|
70
|
+
root: Element | undefined,
|
|
71
|
+
/** 交叉观察器实例 */
|
|
72
|
+
observer: IntersectionObserver
|
|
73
|
+
) => void;
|
|
74
|
+
type IntersectionObserverParams = {
|
|
75
|
+
callback: IntersectionObserverCallback;
|
|
76
|
+
rootMargin?: number;
|
|
77
|
+
threshold?: number;
|
|
78
|
+
once?: boolean;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* @author sonion
|
|
82
|
+
* @description 视口交叉观察器hook
|
|
83
|
+
* @param {IntersectionObserverParams} params - 观察器初始化参数
|
|
84
|
+
* @param {IntersectionObserverCallback} params.callback - 加载回调。
|
|
85
|
+
* @param {number} [params.rootMargin] - 提前多少px触发
|
|
86
|
+
* @param {number} [params.threshold] - 被观察目标与观察区交叉多少触发,0-1
|
|
87
|
+
* @param {boolean} [params.once] - 是否只触发一次
|
|
88
|
+
*/
|
|
89
|
+
export const useIntersectionObserver = ({
|
|
90
|
+
callback,
|
|
91
|
+
rootMargin = 100,
|
|
92
|
+
threshold = 0,
|
|
93
|
+
once = false,
|
|
94
|
+
}: IntersectionObserverParams) => {
|
|
95
|
+
const observerRef = useRef<IntersectionObserver>();
|
|
96
|
+
const [rootRef, setRootRef] = useCreateSafeRef<Element>();
|
|
97
|
+
const getLatestCallback = useLatestCallback(callback);
|
|
98
|
+
|
|
99
|
+
const options = useMemo(
|
|
100
|
+
() => ({
|
|
101
|
+
root: rootRef, // 要观察的区域,视口为null
|
|
102
|
+
rootMargin: `${rootMargin}px`, // 将观察区域向外扩张多少。扩张后就提前交叉了
|
|
103
|
+
threshold, // 被观察目标与观察区交叉多少触发,0-1
|
|
104
|
+
}),
|
|
105
|
+
[rootRef, rootMargin, threshold]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const prevOptions = useRef(options); // 纪录上一次的options,用于比较是否变化
|
|
109
|
+
|
|
110
|
+
useEffect(
|
|
111
|
+
() => () => {
|
|
112
|
+
observerRef.current?.disconnect?.();
|
|
113
|
+
observerRef.current = void 0;
|
|
114
|
+
},
|
|
115
|
+
[options]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const observe = useCallback(
|
|
119
|
+
(el: Element | null) => {
|
|
120
|
+
if (!el) return;
|
|
121
|
+
// options变化,取消观察,重新生成观察器
|
|
122
|
+
if (prevOptions.current !== options) {
|
|
123
|
+
observerRef.current?.disconnect?.();
|
|
124
|
+
observerRef.current = void 0;
|
|
125
|
+
prevOptions.current = options; // 纪录新值
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
observerRef.current ??= new IntersectionObserver((entries, observer) => {
|
|
129
|
+
entries.forEach((entry) => {
|
|
130
|
+
if (entry.isIntersecting) {
|
|
131
|
+
getLatestCallback()?.(entry.target, rootRef, observer);
|
|
132
|
+
once && observer.unobserve?.(entry.target); // 取消观察, 因为只需要触发一次。反复触发可不取消
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}, options);
|
|
136
|
+
observerRef.current?.unobserve?.(el); // 避免重复
|
|
137
|
+
observerRef.current?.observe?.(el);
|
|
138
|
+
},
|
|
139
|
+
// rootRef 已经是options 依赖了
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
+
[options, once, getLatestCallback]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const unobserve = useCallback(
|
|
145
|
+
(el: Element | null) => el && observerRef.current?.unobserve?.(el),
|
|
146
|
+
[]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const takeRecords = useCallback(
|
|
150
|
+
() => observerRef.current?.takeRecords?.(),
|
|
151
|
+
[]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const disconnect = useCallback(() => observerRef.current?.disconnect?.(), []);
|
|
155
|
+
return {
|
|
156
|
+
rootRef,
|
|
157
|
+
/** 设置观察区域 */
|
|
158
|
+
setRootRef,
|
|
159
|
+
/** 开始观察 */
|
|
160
|
+
observe,
|
|
161
|
+
/** 取消观察 */
|
|
162
|
+
unobserve,
|
|
163
|
+
/** 取消所有观察,用observe重新启用 */
|
|
164
|
+
disconnect,
|
|
165
|
+
/** 获取 未触发回调前,所有未处理的观察记录,触发后清空 */
|
|
166
|
+
takeRecords,
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
type ResizeObserverCallback = (
|
|
171
|
+
/** 变化记录 */
|
|
172
|
+
entry: ResizeObserverEntry,
|
|
173
|
+
/** 观察器实例 */
|
|
174
|
+
observer: ResizeObserver
|
|
175
|
+
) => void;
|
|
176
|
+
/**
|
|
177
|
+
* @author sonion
|
|
178
|
+
* @description 元素尺寸变化观察器hook
|
|
179
|
+
* @param {ResizeObserverCallback} callback - 回调函数,参数为ResizeObserverEntry
|
|
180
|
+
* @param {boolean} [once] - 是否只观察一次,默认false
|
|
181
|
+
*/
|
|
182
|
+
export const useResizeObserver = (
|
|
183
|
+
callback: ResizeObserverCallback,
|
|
184
|
+
once = false
|
|
185
|
+
) => {
|
|
186
|
+
const observerRef = useRef<ResizeObserver>();
|
|
187
|
+
const getLatestCallback = useLatestCallback(callback);
|
|
188
|
+
|
|
189
|
+
useEffect(
|
|
190
|
+
() => () => {
|
|
191
|
+
observerRef.current?.disconnect?.(); // 上一次观察器取消,observe没有重新运行的话,观察就丢失了
|
|
192
|
+
observerRef.current = void 0;
|
|
193
|
+
},
|
|
194
|
+
[]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// ResizeObserver 重新生成对象,observe 不会更新,用 observe 做依赖可能造成
|
|
198
|
+
// ResizeObserver 更新了,但没有重新观察。但理论上只有 StrictMode 模式下会重新生成对象
|
|
199
|
+
// 且observe不是作为依赖,而是直接绑定dom或依赖其它重渲染会改变的状态就不会有问题。
|
|
200
|
+
// 或用Set存下历史观察,重生成时恢复,但不能用WeakSet,非必要不建议
|
|
201
|
+
/** 开始观察 因重新生成观察器observe不会更新,故不可以observe是否更新做依赖依据 */
|
|
202
|
+
const observe = useCallback(
|
|
203
|
+
(el: Element | null, options?: ResizeObserverOptions) => {
|
|
204
|
+
if (!el) return;
|
|
205
|
+
observerRef.current ??= new ResizeObserver(
|
|
206
|
+
(entries: ResizeObserverEntry[], observer: ResizeObserver) => {
|
|
207
|
+
entries.forEach((entry) => {
|
|
208
|
+
getLatestCallback()?.(entry, observer);
|
|
209
|
+
});
|
|
210
|
+
once && observer.disconnect();
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
observerRef.current?.unobserve?.(el); // 避免重复
|
|
214
|
+
observerRef.current?.observe?.(el, options);
|
|
215
|
+
},
|
|
216
|
+
[once, getLatestCallback]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/** 取消观察 */
|
|
220
|
+
const unobserve = useCallback(
|
|
221
|
+
(el: Element | null) => el && observerRef.current?.unobserve?.(el),
|
|
222
|
+
[]
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
/** 取消所有观察,用observe重新启用 */
|
|
226
|
+
const disconnect = useCallback(() => observerRef.current?.disconnect?.(), []);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
/** 开始观察 因重新生成观察器observe不会更新,故不可以observe是否更新做依赖依据 */
|
|
230
|
+
observe,
|
|
231
|
+
/** 取消观察 */
|
|
232
|
+
unobserve,
|
|
233
|
+
/** 取消所有观察,用observe重新启用 */
|
|
234
|
+
disconnect,
|
|
235
|
+
};
|
|
236
|
+
};
|