xxf_react 0.8.2 → 0.8.3
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/dist/layout/image/XImage.d.ts +10 -17
- package/dist/layout/image/XImage.d.ts.map +1 -1
- package/dist/layout/image/XImage.effects.d.ts +16 -0
- package/dist/layout/image/XImage.effects.d.ts.map +1 -0
- package/dist/layout/image/XImage.effects.js +77 -0
- package/dist/layout/image/XImage.js +183 -178
- package/dist/layout/image/XImage.types.d.ts +107 -87
- package/dist/layout/image/XImage.types.d.ts.map +1 -1
- package/dist/layout/image/XImageGallery.d.ts +76 -0
- package/dist/layout/image/XImageGallery.d.ts.map +1 -0
- package/dist/layout/image/XImageGallery.js +94 -0
- package/dist/layout/image/index.d.ts +10 -1
- package/dist/layout/image/index.d.ts.map +1 -1
- package/dist/layout/image/index.js +1 -0
- package/package.json +4 -3
|
@@ -3,38 +3,31 @@ import type { XImageProps } from "./XImage.types";
|
|
|
3
3
|
/**
|
|
4
4
|
* 生产级图片组件
|
|
5
5
|
*
|
|
6
|
-
* 基于
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
6
|
+
* 基于 react-lazy-load-image-component 实现,支持:
|
|
7
|
+
* - 懒加载 + IntersectionObserver
|
|
8
|
+
* - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
|
|
9
9
|
* - 错误处理 + 自动重试
|
|
10
10
|
* - fallback 备用图/组件
|
|
11
|
+
* - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
|
|
11
12
|
*
|
|
12
13
|
* @example
|
|
13
14
|
* ```tsx
|
|
14
15
|
* // 基础用法
|
|
15
16
|
* <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
|
|
16
17
|
*
|
|
17
|
-
* //
|
|
18
|
-
* <XImage
|
|
19
|
-
* src="/photo.jpg?w=800"
|
|
20
|
-
* placeholder="/photo.jpg?w=20"
|
|
21
|
-
* width={400}
|
|
22
|
-
* height={300}
|
|
23
|
-
* />
|
|
18
|
+
* // 带模糊效果(提供 placeholder URL 自动启用 blur)
|
|
19
|
+
* <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
|
|
24
20
|
*
|
|
25
|
-
* //
|
|
21
|
+
* // 自定义占位组件
|
|
22
|
+
* <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
|
|
23
|
+
*
|
|
24
|
+
* // 带错误处理
|
|
26
25
|
* <XImage
|
|
27
26
|
* src="/photo.jpg"
|
|
28
27
|
* fallback="/fallback.jpg"
|
|
29
28
|
* retryCount={2}
|
|
30
29
|
* retryDelay={1000}
|
|
31
30
|
* />
|
|
32
|
-
*
|
|
33
|
-
* // 自定义占位组件
|
|
34
|
-
* <XImage
|
|
35
|
-
* src="/photo.jpg"
|
|
36
|
-
* placeholder={<Skeleton className="w-full h-full" />}
|
|
37
|
-
* />
|
|
38
31
|
* ```
|
|
39
32
|
*/
|
|
40
33
|
export declare const XImage: React.NamedExoticComponent<XImageProps & React.RefAttributes<HTMLDivElement | HTMLImageElement>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"XImage.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"XImage.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KASN,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAIhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,MAAM,kGAoUlB,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XImage 效果样式管理
|
|
3
|
+
* 自动注入 CSS 到 document.head,用户无需手动导入
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 注入效果 CSS 到 document.head
|
|
7
|
+
* - 仅在浏览器环境执行
|
|
8
|
+
* - 只执行一次(多个 XImage 实例共享)
|
|
9
|
+
* - SSR 安全
|
|
10
|
+
*/
|
|
11
|
+
export declare function injectEffectStyles(): void;
|
|
12
|
+
/**
|
|
13
|
+
* 移除已注入的效果 CSS(用于测试或清理)
|
|
14
|
+
*/
|
|
15
|
+
export declare function removeEffectStyles(): void;
|
|
16
|
+
//# sourceMappingURL=XImage.effects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XImage.effects.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.effects.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8CH;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAczC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAQzC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XImage 效果样式管理
|
|
3
|
+
* 自动注入 CSS 到 document.head,用户无需手动导入
|
|
4
|
+
*/
|
|
5
|
+
// ============ 效果 CSS 定义 ============
|
|
6
|
+
const EFFECT_STYLES = `
|
|
7
|
+
.lazy-load-image-background.blur {
|
|
8
|
+
filter: blur(15px);
|
|
9
|
+
}
|
|
10
|
+
.lazy-load-image-background.blur.lazy-load-image-loaded {
|
|
11
|
+
filter: blur(0);
|
|
12
|
+
transition: filter .3s;
|
|
13
|
+
}
|
|
14
|
+
.lazy-load-image-background.blur > img {
|
|
15
|
+
opacity: 0;
|
|
16
|
+
}
|
|
17
|
+
.lazy-load-image-background.blur.lazy-load-image-loaded > img {
|
|
18
|
+
opacity: 1;
|
|
19
|
+
transition: opacity .3s;
|
|
20
|
+
}
|
|
21
|
+
.lazy-load-image-background.opacity {
|
|
22
|
+
opacity: 0;
|
|
23
|
+
}
|
|
24
|
+
.lazy-load-image-background.opacity.lazy-load-image-loaded {
|
|
25
|
+
opacity: 1;
|
|
26
|
+
transition: opacity .3s;
|
|
27
|
+
}
|
|
28
|
+
.lazy-load-image-background.black-and-white {
|
|
29
|
+
filter: grayscale(1);
|
|
30
|
+
}
|
|
31
|
+
.lazy-load-image-background.black-and-white.lazy-load-image-loaded {
|
|
32
|
+
filter: grayscale(0);
|
|
33
|
+
transition: filter .3s;
|
|
34
|
+
}
|
|
35
|
+
.lazy-load-image-background.black-and-white > img {
|
|
36
|
+
opacity: 0;
|
|
37
|
+
}
|
|
38
|
+
.lazy-load-image-background.black-and-white.lazy-load-image-loaded > img {
|
|
39
|
+
opacity: 1;
|
|
40
|
+
transition: opacity .3s;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
const STYLE_ID = "ximage-effect-styles";
|
|
44
|
+
// CSS 注入标记,确保只注入一次
|
|
45
|
+
let stylesInjected = false;
|
|
46
|
+
/**
|
|
47
|
+
* 注入效果 CSS 到 document.head
|
|
48
|
+
* - 仅在浏览器环境执行
|
|
49
|
+
* - 只执行一次(多个 XImage 实例共享)
|
|
50
|
+
* - SSR 安全
|
|
51
|
+
*/
|
|
52
|
+
export function injectEffectStyles() {
|
|
53
|
+
if (stylesInjected || typeof document === "undefined")
|
|
54
|
+
return;
|
|
55
|
+
// 双重检查:防止多实例并发注入
|
|
56
|
+
if (document.getElementById(STYLE_ID)) {
|
|
57
|
+
stylesInjected = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const styleElement = document.createElement("style");
|
|
61
|
+
styleElement.id = STYLE_ID;
|
|
62
|
+
styleElement.textContent = EFFECT_STYLES;
|
|
63
|
+
document.head.appendChild(styleElement);
|
|
64
|
+
stylesInjected = true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 移除已注入的效果 CSS(用于测试或清理)
|
|
68
|
+
*/
|
|
69
|
+
export function removeEffectStyles() {
|
|
70
|
+
if (typeof document === "undefined")
|
|
71
|
+
return;
|
|
72
|
+
const styleElement = document.getElementById(STYLE_ID);
|
|
73
|
+
if (styleElement) {
|
|
74
|
+
styleElement.remove();
|
|
75
|
+
stylesInjected = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,109 +1,99 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useState, useEffect, useCallback, useRef, forwardRef, memo, useMemo, } from "react";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// ============================================================================
|
|
7
|
-
/** 过渡动画时长 */
|
|
8
|
-
const TRANSITION_DURATION = "0.3s";
|
|
9
|
-
/** 占位图模糊程度 */
|
|
10
|
-
const PLACEHOLDER_BLUR = "20px";
|
|
11
|
-
/** 占位图放大比例(防止模糊边缘露出) */
|
|
12
|
-
const PLACEHOLDER_SCALE = 1.1;
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// 组件实现
|
|
15
|
-
// ============================================================================
|
|
2
|
+
import React, { useState, useEffect, useCallback, useRef, forwardRef, memo, useMemo, useContext, } from "react";
|
|
3
|
+
import { LazyLoadImage } from "react-lazy-load-image-component";
|
|
4
|
+
import { ScrollPositionContext } from "./XImageGallery";
|
|
5
|
+
import { injectEffectStyles } from "./XImage.effects";
|
|
16
6
|
/**
|
|
17
7
|
* 生产级图片组件
|
|
18
8
|
*
|
|
19
|
-
* 基于
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
9
|
+
* 基于 react-lazy-load-image-component 实现,支持:
|
|
10
|
+
* - 懒加载 + IntersectionObserver
|
|
11
|
+
* - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
|
|
22
12
|
* - 错误处理 + 自动重试
|
|
23
13
|
* - fallback 备用图/组件
|
|
14
|
+
* - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
|
|
24
15
|
*
|
|
25
16
|
* @example
|
|
26
17
|
* ```tsx
|
|
27
18
|
* // 基础用法
|
|
28
19
|
* <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
|
|
29
20
|
*
|
|
30
|
-
* //
|
|
31
|
-
* <XImage
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* height={300}
|
|
36
|
-
* />
|
|
21
|
+
* // 带模糊效果(提供 placeholder URL 自动启用 blur)
|
|
22
|
+
* <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
|
|
23
|
+
*
|
|
24
|
+
* // 自定义占位组件
|
|
25
|
+
* <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
|
|
37
26
|
*
|
|
38
|
-
* //
|
|
27
|
+
* // 带错误处理
|
|
39
28
|
* <XImage
|
|
40
29
|
* src="/photo.jpg"
|
|
41
30
|
* fallback="/fallback.jpg"
|
|
42
31
|
* retryCount={2}
|
|
43
32
|
* retryDelay={1000}
|
|
44
33
|
* />
|
|
45
|
-
*
|
|
46
|
-
* // 自定义占位组件
|
|
47
|
-
* <XImage
|
|
48
|
-
* src="/photo.jpg"
|
|
49
|
-
* placeholder={<Skeleton className="w-full h-full" />}
|
|
50
|
-
* />
|
|
51
34
|
* ```
|
|
52
35
|
*/
|
|
53
36
|
export const XImage = memo(forwardRef(function XImage({
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
37
|
+
// 基础属性
|
|
38
|
+
src, alt = "", width, height, className, style, objectFit, objectPosition, fill,
|
|
39
|
+
// 懒加载配置
|
|
40
|
+
lazy = true, threshold = 100, effect, placeholder, useIntersectionObserver = true, delayMethod, delayTime,
|
|
41
|
+
// 错误处理
|
|
42
|
+
fallback, retryCount = 0, retryDelay = 1000,
|
|
43
|
+
// 性能优化
|
|
44
|
+
scrollPosition, visibleByDefault = false,
|
|
45
|
+
// 事件回调
|
|
46
|
+
onLoad, onError, onVisible, onStatusChange,
|
|
47
|
+
// 包裹元素属性
|
|
48
|
+
wrapperProps, wrapperClassName,
|
|
49
|
+
// 原生属性
|
|
50
|
+
loading, decoding = "async", srcSet, sizes, crossOrigin, referrerPolicy, }, ref) {
|
|
51
|
+
// ============ 解析 placeholder 类型 ============
|
|
52
|
+
const isPlaceholderSrc = typeof placeholder === "string";
|
|
53
|
+
const placeholderSrc = isPlaceholderSrc ? placeholder : undefined;
|
|
54
|
+
const placeholderElement = !isPlaceholderSrc ? placeholder : undefined;
|
|
55
|
+
// ============ 智能默认 effect ============
|
|
56
|
+
// 只有当 placeholder 是字符串(URL)时才自动启用 blur
|
|
57
|
+
// ReactElement 类型不自动启用(因为没有图片可以模糊)
|
|
58
|
+
const effectiveEffect = effect !== null && effect !== void 0 ? effect : (isPlaceholderSrc ? "blur" : undefined);
|
|
59
|
+
// ============ 自动注入效果 CSS ============
|
|
60
|
+
// 使用 effect 属性时自动注入,确保 SSR 兼容
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (effectiveEffect) {
|
|
63
|
+
injectEffectStyles();
|
|
64
|
+
}
|
|
65
|
+
}, [effectiveEffect]);
|
|
66
|
+
// ============ Context: 自动获取 Gallery 的 scrollPosition ============
|
|
67
|
+
const contextScrollPosition = useContext(ScrollPositionContext);
|
|
68
|
+
const effectiveScrollPosition = scrollPosition !== null && scrollPosition !== void 0 ? scrollPosition : contextScrollPosition;
|
|
69
|
+
// ============ 状态管理 ============
|
|
69
70
|
const [currentSrc, setCurrentSrc] = useState(src);
|
|
70
71
|
const [status, setStatus] = useState("idle");
|
|
71
72
|
const [hasError, setHasError] = useState(false);
|
|
72
73
|
const [retryAttempt, setRetryAttempt] = useState(0);
|
|
73
|
-
const [isLoaded, setIsLoaded] = useState(false);
|
|
74
|
-
// ========================================================================
|
|
75
74
|
// Refs
|
|
76
|
-
// ========================================================================
|
|
77
75
|
const wrapperRef = useRef(null);
|
|
78
|
-
const mainImageRef = useRef(null);
|
|
79
76
|
const retryTimerRef = useRef(null);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
// ========================================================================
|
|
86
|
-
const updateStatus = useCallback((newStatus) => {
|
|
87
|
-
var _a, _b;
|
|
88
|
-
setStatus(newStatus);
|
|
89
|
-
(_b = (_a = callbacksRef.current).onStatusChange) === null || _b === void 0 ? void 0 : _b.call(_a, newStatus);
|
|
90
|
-
}, []);
|
|
91
|
-
// ========================================================================
|
|
92
|
-
// src 变化时重置状态
|
|
93
|
-
// ========================================================================
|
|
77
|
+
const hasCalledOnVisible = useRef(false);
|
|
78
|
+
// 使用 ref 存储回调,避免 effect 依赖问题
|
|
79
|
+
const onStatusChangeRef = useRef(onStatusChange);
|
|
80
|
+
onStatusChangeRef.current = onStatusChange;
|
|
81
|
+
// ============ src 变化重置状态 ============
|
|
94
82
|
useEffect(() => {
|
|
83
|
+
var _a;
|
|
95
84
|
setCurrentSrc(src);
|
|
96
85
|
setHasError(false);
|
|
97
86
|
setRetryAttempt(0);
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
setStatus("idle");
|
|
88
|
+
(_a = onStatusChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStatusChangeRef, "idle");
|
|
89
|
+
hasCalledOnVisible.current = false;
|
|
100
90
|
// 清理重试定时器
|
|
101
91
|
if (retryTimerRef.current) {
|
|
102
92
|
clearTimeout(retryTimerRef.current);
|
|
103
93
|
retryTimerRef.current = null;
|
|
104
94
|
}
|
|
105
|
-
}, [src
|
|
106
|
-
//
|
|
95
|
+
}, [src]);
|
|
96
|
+
// 清理定时器
|
|
107
97
|
useEffect(() => {
|
|
108
98
|
return () => {
|
|
109
99
|
if (retryTimerRef.current) {
|
|
@@ -111,146 +101,161 @@ src, alt = "", fill, style, className, onLoad, onError,
|
|
|
111
101
|
}
|
|
112
102
|
};
|
|
113
103
|
}, []);
|
|
114
|
-
//
|
|
115
|
-
// ref 转发:将内部 img 元素暴露给外部
|
|
116
|
-
// ========================================================================
|
|
104
|
+
// ============ ref 转发:从 wrapper 中获取 img 元素 ============
|
|
117
105
|
useEffect(() => {
|
|
118
106
|
if (!ref || !wrapperRef.current)
|
|
119
107
|
return;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const targetImg = imgs.length > 1 ? imgs[1] : imgs[0];
|
|
123
|
-
if (targetImg) {
|
|
108
|
+
const imgElement = wrapperRef.current.querySelector("img");
|
|
109
|
+
if (imgElement) {
|
|
124
110
|
if (typeof ref === "function") {
|
|
125
|
-
ref(
|
|
111
|
+
ref(imgElement);
|
|
126
112
|
}
|
|
127
113
|
else {
|
|
128
|
-
ref.current =
|
|
114
|
+
ref.current = imgElement;
|
|
129
115
|
}
|
|
130
116
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
117
|
+
// 依赖 status 确保图片加载后能获取到正确的 img 元素
|
|
118
|
+
}, [ref, status]);
|
|
119
|
+
// ============ 状态更新辅助 ============
|
|
120
|
+
const updateStatus = useCallback((newStatus) => {
|
|
121
|
+
var _a;
|
|
122
|
+
setStatus(newStatus);
|
|
123
|
+
(_a = onStatusChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStatusChangeRef, newStatus);
|
|
124
|
+
}, []);
|
|
125
|
+
// ============ 事件处理 ============
|
|
126
|
+
const handleLoad = useCallback((event) => {
|
|
141
127
|
updateStatus("loaded");
|
|
142
|
-
|
|
143
|
-
}, [updateStatus]);
|
|
144
|
-
|
|
145
|
-
* 主图加载失败 - 处理重试和 fallback 逻辑
|
|
146
|
-
*/
|
|
147
|
-
const handleError = useCallback((e) => {
|
|
128
|
+
onLoad === null || onLoad === void 0 ? void 0 : onLoad(event);
|
|
129
|
+
}, [updateStatus, onLoad]);
|
|
130
|
+
const handleError = useCallback((event) => {
|
|
148
131
|
setRetryAttempt((currentRetry) => {
|
|
149
|
-
//
|
|
132
|
+
// 还有重试次数
|
|
150
133
|
if (currentRetry < retryCount) {
|
|
134
|
+
updateStatus("loading");
|
|
151
135
|
retryTimerRef.current = setTimeout(() => {
|
|
152
|
-
//
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
setCurrentSrc(`${srcStr}${separator}_retry=${Date.now()}`);
|
|
136
|
+
// 通过修改 src 触发重新加载(添加时间戳绕过缓存)
|
|
137
|
+
const separator = src.includes("?") ? "&" : "?";
|
|
138
|
+
setCurrentSrc(`${src}${separator}_retry=${Date.now()}`);
|
|
156
139
|
}, retryDelay);
|
|
157
140
|
return currentRetry + 1;
|
|
158
141
|
}
|
|
159
|
-
//
|
|
142
|
+
// 重试耗尽,检查 fallback
|
|
160
143
|
setHasError((currentHasError) => {
|
|
161
|
-
var _a, _b;
|
|
162
144
|
if (fallback && !currentHasError) {
|
|
163
145
|
if (typeof fallback === "string") {
|
|
164
146
|
setCurrentSrc(fallback);
|
|
165
|
-
|
|
147
|
+
updateStatus("loading");
|
|
166
148
|
}
|
|
167
|
-
// fallback 是 ReactNode,标记错误状态
|
|
168
|
-
updateStatus("error");
|
|
169
149
|
return true;
|
|
170
150
|
}
|
|
171
|
-
//
|
|
151
|
+
// 最终失败
|
|
172
152
|
updateStatus("error");
|
|
173
|
-
|
|
153
|
+
onError === null || onError === void 0 ? void 0 : onError(event);
|
|
174
154
|
return currentHasError;
|
|
175
155
|
});
|
|
176
156
|
return currentRetry;
|
|
177
157
|
});
|
|
178
|
-
}, [retryCount, retryDelay, src, fallback, updateStatus]);
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
158
|
+
}, [retryCount, retryDelay, src, fallback, updateStatus, onError]);
|
|
159
|
+
// beforeLoad: 进入视口,开始加载前调用
|
|
160
|
+
const handleBeforeLoad = useCallback(() => {
|
|
161
|
+
if (!hasCalledOnVisible.current) {
|
|
162
|
+
hasCalledOnVisible.current = true;
|
|
163
|
+
updateStatus("loading");
|
|
164
|
+
onVisible === null || onVisible === void 0 ? void 0 : onVisible();
|
|
165
|
+
}
|
|
166
|
+
}, [updateStatus, onVisible]);
|
|
167
|
+
// ============ 计算样式 ============
|
|
168
|
+
const computedStyle = useMemo(() => {
|
|
169
|
+
const baseStyle = { ...style };
|
|
170
|
+
if (objectFit) {
|
|
171
|
+
baseStyle.objectFit = objectFit;
|
|
172
|
+
}
|
|
173
|
+
if (objectPosition) {
|
|
174
|
+
baseStyle.objectPosition = objectPosition;
|
|
175
|
+
}
|
|
176
|
+
// fill 模式:图片绝对定位填充
|
|
177
|
+
if (fill) {
|
|
178
|
+
baseStyle.position = "absolute";
|
|
179
|
+
baseStyle.width = "100%";
|
|
180
|
+
baseStyle.height = "100%";
|
|
181
|
+
baseStyle.inset = 0;
|
|
182
|
+
baseStyle.objectFit = objectFit || "cover";
|
|
183
|
+
}
|
|
184
|
+
return baseStyle;
|
|
185
|
+
}, [style, objectFit, objectPosition, fill]);
|
|
186
|
+
const wrapperStyle = useMemo(() => {
|
|
187
|
+
const base = { ...wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style };
|
|
188
|
+
// fill 模式下包裹元素需要绝对定位并填满父容器
|
|
189
|
+
if (fill) {
|
|
190
|
+
base.display = "block";
|
|
191
|
+
base.position = "absolute";
|
|
192
|
+
base.inset = 0;
|
|
193
|
+
base.width = "100%";
|
|
194
|
+
base.height = "100%";
|
|
195
|
+
}
|
|
196
|
+
return base;
|
|
197
|
+
}, [fill, wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style]);
|
|
198
|
+
// ============ 解析尺寸(简单计算,不需要 useMemo) ============
|
|
199
|
+
const parsedWidth = fill
|
|
200
|
+
? undefined
|
|
201
|
+
: typeof width === "number"
|
|
202
|
+
? width
|
|
203
|
+
: typeof width === "string"
|
|
204
|
+
? parseInt(width, 10) || undefined
|
|
205
|
+
: undefined;
|
|
206
|
+
const parsedHeight = fill
|
|
207
|
+
? undefined
|
|
208
|
+
: typeof height === "number"
|
|
209
|
+
? height
|
|
210
|
+
: typeof height === "string"
|
|
211
|
+
? parseInt(height, 10) || undefined
|
|
212
|
+
: undefined;
|
|
213
|
+
// ============ 合并 wrapperProps(处理 ref 合并) ============
|
|
214
|
+
const mergedWrapperProps = useMemo(() => {
|
|
215
|
+
// wrapperProps 可能包含 ref(通过类型扩展)
|
|
185
216
|
const userRef = wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.ref;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
217
|
+
// 合并 ref
|
|
218
|
+
const combinedRef = (node) => {
|
|
219
|
+
// 设置内部 ref
|
|
220
|
+
wrapperRef.current = node;
|
|
221
|
+
// 调用用户的 ref
|
|
222
|
+
if (userRef) {
|
|
223
|
+
if (typeof userRef === "function") {
|
|
224
|
+
userRef(node);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
userRef.current = node;
|
|
228
|
+
}
|
|
192
229
|
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
overflow: "hidden",
|
|
203
|
-
...wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style,
|
|
204
|
-
}), [wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.style]);
|
|
205
|
-
/** 占位图样式(带 blur 效果) */
|
|
206
|
-
const placeholderStyle = useMemo(() => ({
|
|
207
|
-
...style,
|
|
208
|
-
position: "absolute",
|
|
209
|
-
inset: 0,
|
|
210
|
-
width: "100%",
|
|
211
|
-
height: "100%",
|
|
212
|
-
transition: `opacity ${TRANSITION_DURATION} ease-in-out, filter ${TRANSITION_DURATION} ease-in-out`,
|
|
213
|
-
opacity: isLoaded ? 0 : 1,
|
|
214
|
-
filter: isLoaded ? "blur(0px)" : `blur(${PLACEHOLDER_BLUR})`,
|
|
215
|
-
transform: `scale(${PLACEHOLDER_SCALE})`,
|
|
216
|
-
pointerEvents: "none", // 防止占位图拦截事件
|
|
217
|
-
}), [style, isLoaded]);
|
|
218
|
-
/** 主图样式(加载完成后显示) */
|
|
219
|
-
const mainImageStyle = useMemo(() => ({
|
|
220
|
-
...style,
|
|
221
|
-
position: "absolute",
|
|
222
|
-
inset: 0,
|
|
223
|
-
width: "100%",
|
|
224
|
-
height: "100%",
|
|
225
|
-
transition: `opacity ${TRANSITION_DURATION} ease-in-out`,
|
|
226
|
-
opacity: isLoaded ? 1 : 0,
|
|
227
|
-
}), [style, isLoaded]);
|
|
228
|
-
// ========================================================================
|
|
229
|
-
// 渲染
|
|
230
|
-
// ========================================================================
|
|
231
|
-
// 错误状态 + ReactNode fallback
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
...wrapperProps,
|
|
233
|
+
ref: combinedRef,
|
|
234
|
+
style: wrapperStyle,
|
|
235
|
+
className: wrapperClassName,
|
|
236
|
+
};
|
|
237
|
+
}, [wrapperProps, wrapperStyle, wrapperClassName]);
|
|
238
|
+
// ============ 渲染 fallback ReactNode ============
|
|
232
239
|
if (hasError && fallback && typeof fallback !== "string") {
|
|
233
|
-
return (React.createElement("div", { ref: ref, className:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
React.createElement(Image, { src: placeholderSrc, alt: "" // 占位图不需要 alt(屏幕阅读器跳过)
|
|
241
|
-
, fill: true, priority: true, unoptimized: true, className: className, style: placeholderStyle, sizes: imageProps.sizes })) : (
|
|
242
|
-
// ReactElement 占位组件(如 Skeleton)
|
|
243
|
-
React.createElement("div", { style: {
|
|
244
|
-
position: "absolute",
|
|
245
|
-
inset: 0,
|
|
246
|
-
transition: `opacity ${TRANSITION_DURATION} ease-in-out`,
|
|
247
|
-
opacity: isLoaded ? 0 : 1,
|
|
248
|
-
pointerEvents: "none",
|
|
249
|
-
} }, placeholderElement)),
|
|
250
|
-
React.createElement(Image, { ...imageProps, ref: mainImageRef, src: currentSrc, alt: alt, fill: true, className: className, style: mainImageStyle, onLoad: handleLoad, onError: handleError })));
|
|
240
|
+
return (React.createElement("div", { ref: ref, className: className, style: {
|
|
241
|
+
width: parsedWidth,
|
|
242
|
+
height: parsedHeight,
|
|
243
|
+
position: fill ? "absolute" : undefined,
|
|
244
|
+
inset: fill ? 0 : undefined,
|
|
245
|
+
...style,
|
|
246
|
+
} }, fallback));
|
|
251
247
|
}
|
|
252
|
-
//
|
|
253
|
-
return (React.createElement(
|
|
254
|
-
|
|
248
|
+
// ============ 统一使用 LazyLoadImage ============
|
|
249
|
+
return (React.createElement(LazyLoadImage, { src: currentSrc, alt: alt, width: parsedWidth, height: parsedHeight, className: className, style: computedStyle,
|
|
250
|
+
// 懒加载配置 - lazy=false 时禁用懒加载特性
|
|
251
|
+
threshold: lazy ? threshold : 0, effect: lazy ? effectiveEffect : undefined, placeholderSrc: lazy ? placeholderSrc : undefined, placeholder: lazy ? placeholderElement : undefined, useIntersectionObserver: lazy ? useIntersectionObserver : false, visibleByDefault: !lazy || visibleByDefault, delayMethod: delayMethod, delayTime: delayTime,
|
|
252
|
+
// 性能优化
|
|
253
|
+
scrollPosition: lazy ? effectiveScrollPosition : undefined,
|
|
254
|
+
// 事件
|
|
255
|
+
beforeLoad: lazy ? handleBeforeLoad : undefined, onLoad: handleLoad, onError: handleError,
|
|
256
|
+
// 包裹元素
|
|
257
|
+
wrapperProps: mergedWrapperProps,
|
|
258
|
+
// 原生属性
|
|
259
|
+
loading: lazy ? loading : "eager", decoding: decoding, srcSet: srcSet, sizes: sizes, crossOrigin: crossOrigin, referrerPolicy: referrerPolicy }));
|
|
255
260
|
}));
|
|
256
261
|
export default XImage;
|
|
@@ -1,110 +1,130 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { CSSProperties, ReactElement, ReactNode, SyntheticEvent } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* 懒加载效果类型
|
|
4
|
+
* - blur: 模糊渐变效果
|
|
5
|
+
* - opacity: 透明度渐变效果
|
|
6
|
+
* - black-and-white: 黑白到彩色渐变效果
|
|
7
|
+
*/
|
|
8
|
+
export type XImageEffect = "blur" | "opacity" | "black-and-white";
|
|
9
|
+
/**
|
|
10
|
+
* 节流/防抖方法类型
|
|
11
|
+
*/
|
|
12
|
+
export type DelayMethod = "throttle" | "debounce";
|
|
3
13
|
/**
|
|
4
14
|
* 图片加载状态
|
|
5
|
-
*
|
|
6
|
-
* - `idle`: 初始状态(未开始加载)
|
|
7
|
-
* - `loading`: 加载中
|
|
8
|
-
* - `loaded`: 加载成功
|
|
9
|
-
* - `error`: 加载失败(重试耗尽且无 fallback)
|
|
10
15
|
*/
|
|
11
16
|
export type XImageStatus = "idle" | "loading" | "loaded" | "error";
|
|
17
|
+
/**
|
|
18
|
+
* 滚动位置类型 (用于 trackWindowScroll HOC)
|
|
19
|
+
*/
|
|
20
|
+
export interface ScrollPosition {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
12
24
|
/**
|
|
13
25
|
* XImage 组件 Props
|
|
14
|
-
*
|
|
15
|
-
* 继承 next/image 的所有属性,扩展双层渲染和错误处理能力。
|
|
16
|
-
*
|
|
17
|
-
* ## 性能建议
|
|
18
|
-
*
|
|
19
|
-
* 1. **placeholder 使用小尺寸缩略图**:推荐 20-40px 宽度,体积 < 1KB
|
|
20
|
-
* ```tsx
|
|
21
|
-
* <XImage
|
|
22
|
-
* src="/photo.jpg?w=800"
|
|
23
|
-
* placeholder="/photo.jpg?w=20"
|
|
24
|
-
* />
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* 2. **首屏图片使用 priority**:跳过懒加载
|
|
28
|
-
* ```tsx
|
|
29
|
-
* <XImage src="/hero.jpg" priority />
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* 3. **设置正确的 sizes**:避免加载过大图片
|
|
33
|
-
* ```tsx
|
|
34
|
-
* <XImage
|
|
35
|
-
* src="/photo.jpg"
|
|
36
|
-
* sizes="(max-width: 768px) 100vw, 50vw"
|
|
37
|
-
* />
|
|
38
|
-
* ```
|
|
39
26
|
*/
|
|
40
|
-
export interface XImageProps
|
|
27
|
+
export interface XImageProps {
|
|
28
|
+
/** 图片源地址 */
|
|
29
|
+
src: string;
|
|
30
|
+
/** 图片描述文本 */
|
|
31
|
+
alt?: string;
|
|
32
|
+
/** 图片宽度 */
|
|
33
|
+
width?: number | string;
|
|
34
|
+
/** 图片高度 */
|
|
35
|
+
height?: number | string;
|
|
36
|
+
/** CSS 类名 */
|
|
37
|
+
className?: string;
|
|
38
|
+
/** 内联样式 */
|
|
39
|
+
style?: CSSProperties;
|
|
40
|
+
/** 图片填充方式 */
|
|
41
|
+
objectFit?: CSSProperties["objectFit"];
|
|
42
|
+
/** 图片对齐方式 */
|
|
43
|
+
objectPosition?: CSSProperties["objectPosition"];
|
|
44
|
+
/** 是否使用 fill 模式(占满父容器) */
|
|
45
|
+
fill?: boolean;
|
|
46
|
+
/** 是否启用懒加载,默认 true */
|
|
47
|
+
lazy?: boolean;
|
|
48
|
+
/** 进入视口前多少像素开始加载,默认 100 */
|
|
49
|
+
threshold?: number;
|
|
50
|
+
/** 加载效果类型(如果提供了 placeholder URL 但未指定 effect,默认为 'blur') */
|
|
51
|
+
effect?: XImageEffect;
|
|
41
52
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* -
|
|
45
|
-
* - `ReactElement`: 自定义占位组件(如 Skeleton)
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```tsx
|
|
49
|
-
* // URL 占位图(推荐)
|
|
50
|
-
* <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
|
|
51
|
-
*
|
|
52
|
-
* // 自定义 Skeleton
|
|
53
|
-
* <XImage src="/photo.jpg" placeholder={<Skeleton />} />
|
|
54
|
-
* ```
|
|
53
|
+
* 占位内容
|
|
54
|
+
* - string: 占位图片 URL(自动启用 blur 效果)
|
|
55
|
+
* - ReactElement: 自定义占位组件(如 Skeleton,需手动指定 effect)
|
|
55
56
|
*/
|
|
56
57
|
placeholder?: string | ReactElement | null;
|
|
58
|
+
/** 是否使用 IntersectionObserver,默认 true */
|
|
59
|
+
useIntersectionObserver?: boolean;
|
|
60
|
+
/** 滚动/resize 事件的节流方法 */
|
|
61
|
+
delayMethod?: DelayMethod;
|
|
62
|
+
/** 节流延迟时间(毫秒) */
|
|
63
|
+
delayTime?: number;
|
|
57
64
|
/**
|
|
58
65
|
* 加载失败时显示的备用内容
|
|
59
|
-
*
|
|
60
|
-
* -
|
|
61
|
-
* - `ReactNode`: 自定义错误展示组件
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```tsx
|
|
65
|
-
* // 备用图片
|
|
66
|
-
* <XImage src="/photo.jpg" fallback="/fallback.jpg" />
|
|
67
|
-
*
|
|
68
|
-
* // 自定义错误组件
|
|
69
|
-
* <XImage src="/photo.jpg" fallback={<ErrorPlaceholder />} />
|
|
70
|
-
* ```
|
|
66
|
+
* - string: 备用图片 URL
|
|
67
|
+
* - ReactNode: 自定义错误展示组件
|
|
71
68
|
*/
|
|
72
69
|
fallback?: string | ReactNode;
|
|
73
|
-
/**
|
|
74
|
-
* 加载失败后的重试次数
|
|
75
|
-
*
|
|
76
|
-
* @default 0
|
|
77
|
-
*/
|
|
70
|
+
/** 加载失败后的重试次数,默认 0 */
|
|
78
71
|
retryCount?: number;
|
|
79
|
-
/**
|
|
80
|
-
* 重试间隔(毫秒)
|
|
81
|
-
*
|
|
82
|
-
* @default 1000
|
|
83
|
-
*/
|
|
72
|
+
/** 重试间隔(毫秒),默认 1000 */
|
|
84
73
|
retryDelay?: number;
|
|
85
74
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* ```tsx
|
|
90
|
-
* <XImage
|
|
91
|
-
* src="/photo.jpg"
|
|
92
|
-
* onStatusChange={(status) => console.log('Status:', status)}
|
|
93
|
-
* />
|
|
94
|
-
* ```
|
|
75
|
+
* 滚动位置(来自 trackWindowScroll HOC)
|
|
76
|
+
* 当父组件使用 withScrollTracking 包裹时自动传入
|
|
95
77
|
*/
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 传递给包裹 div 元素的属性
|
|
99
|
-
*
|
|
100
|
-
* 用于自定义容器的事件处理、数据属性等
|
|
101
|
-
*/
|
|
102
|
-
wrapperProps?: React.HTMLAttributes<HTMLDivElement>;
|
|
78
|
+
scrollPosition?: ScrollPosition;
|
|
103
79
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* 与 className 区分:className 应用于 img,wrapperClassName 应用于外层 div
|
|
80
|
+
* 设为 true 则图片立即显示(不触发懒加载)
|
|
81
|
+
* 适用于已在浏览器缓存中的图片
|
|
107
82
|
*/
|
|
83
|
+
visibleByDefault?: boolean;
|
|
84
|
+
/** 图片加载完成回调 */
|
|
85
|
+
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
86
|
+
/** 图片加载失败回调 */
|
|
87
|
+
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
88
|
+
/** 图片进入视口时回调 */
|
|
89
|
+
onVisible?: () => void;
|
|
90
|
+
/** 加载状态变化回调 */
|
|
91
|
+
onStatusChange?: (status: XImageStatus) => void;
|
|
92
|
+
/** 传递给包裹 span 元素的属性 */
|
|
93
|
+
wrapperProps?: React.HTMLAttributes<HTMLSpanElement>;
|
|
94
|
+
/** 包裹元素的类名 */
|
|
108
95
|
wrapperClassName?: string;
|
|
96
|
+
/** 图片加载优先级 */
|
|
97
|
+
loading?: "lazy" | "eager";
|
|
98
|
+
/** 解码方式 */
|
|
99
|
+
decoding?: "async" | "auto" | "sync";
|
|
100
|
+
/** 响应式图片源集 */
|
|
101
|
+
srcSet?: string;
|
|
102
|
+
/** 响应式尺寸 */
|
|
103
|
+
sizes?: string;
|
|
104
|
+
/** 跨域设置 */
|
|
105
|
+
crossOrigin?: "anonymous" | "use-credentials" | "";
|
|
106
|
+
/** referrer 策略 */
|
|
107
|
+
referrerPolicy?: React.HTMLAttributeReferrerPolicy;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* XImageGallery 组件 Props
|
|
111
|
+
* 用于包裹多个 XImage 实现统一的滚动监听优化
|
|
112
|
+
*/
|
|
113
|
+
export interface XImageGalleryProps {
|
|
114
|
+
children: ReactNode;
|
|
115
|
+
/** 自定义容器类名 */
|
|
116
|
+
className?: string;
|
|
117
|
+
/** 自定义容器样式 */
|
|
118
|
+
style?: CSSProperties;
|
|
119
|
+
/** 节流方法: throttle 或 debounce */
|
|
120
|
+
delayMethod?: DelayMethod;
|
|
121
|
+
/** 节流/防抖延迟时间(毫秒) */
|
|
122
|
+
delayTime?: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* withScrollTracking HOC 注入的 Props
|
|
126
|
+
*/
|
|
127
|
+
export interface WithScrollTrackingProps {
|
|
128
|
+
scrollPosition: ScrollPosition;
|
|
109
129
|
}
|
|
110
130
|
//# sourceMappingURL=XImage.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"XImage.types.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"XImage.types.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { ComponentType } from "react";
|
|
2
|
+
import type { XImageGalleryProps, ScrollPosition, WithScrollTrackingProps } from "./XImage.types";
|
|
3
|
+
/**
|
|
4
|
+
* ScrollPosition Context
|
|
5
|
+
* 用于在组件树中共享滚动位置,支持深层嵌套的 XImage 自动获取
|
|
6
|
+
*/
|
|
7
|
+
export declare const ScrollPositionContext: React.Context<ScrollPosition | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* Hook: 获取当前的滚动位置
|
|
10
|
+
* 在 XImageGallery 内部的任意深度使用
|
|
11
|
+
*/
|
|
12
|
+
export declare function useScrollPosition(): ScrollPosition | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* XImageGallery - 图片画廊/列表性能优化组件
|
|
15
|
+
*
|
|
16
|
+
* 当页面中有大量图片需要懒加载时,使用此组件包裹可以:
|
|
17
|
+
* - 统一管理滚动监听,避免每个图片单独监听 scroll/resize 事件
|
|
18
|
+
* - 通过 trackWindowScroll HOC 实现性能优化
|
|
19
|
+
* - 通过 Context 自动向任意深度的子 XImage 组件传递 scrollPosition
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* // 基础用法 - 包裹多个 XImage(支持任意嵌套深度)
|
|
24
|
+
* <XImageGallery>
|
|
25
|
+
* <div className="grid">
|
|
26
|
+
* {images.map(img => (
|
|
27
|
+
* <div key={img.id} className="card">
|
|
28
|
+
* <XImage src={img.url} effect="blur" />
|
|
29
|
+
* </div>
|
|
30
|
+
* ))}
|
|
31
|
+
* </div>
|
|
32
|
+
* </XImageGallery>
|
|
33
|
+
*
|
|
34
|
+
* // 自定义节流配置
|
|
35
|
+
* <XImageGallery delayMethod="debounce" delayTime={200}>
|
|
36
|
+
* {images.map(img => (
|
|
37
|
+
* <XImage key={img.id} src={img.url} />
|
|
38
|
+
* ))}
|
|
39
|
+
* </XImageGallery>
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @note 注意事项:
|
|
43
|
+
* - 不要在 XImageGallery 内部嵌套另一个 XImageGallery
|
|
44
|
+
* - 内部的 XImage 会自动通过 Context 获取 scrollPosition,无需手动传递
|
|
45
|
+
*/
|
|
46
|
+
export declare const XImageGallery: React.NamedExoticComponent<XImageGalleryProps & React.RefAttributes<HTMLDivElement>>;
|
|
47
|
+
/**
|
|
48
|
+
* HOC: 为任意组件添加滚动位置追踪能力
|
|
49
|
+
*
|
|
50
|
+
* 适用于需要自定义容器但又想使用 trackWindowScroll 优化的场景
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* // 自定义图片网格组件
|
|
55
|
+
* const ImageGrid = ({ images, scrollPosition }) => (
|
|
56
|
+
* <div className="grid grid-cols-3">
|
|
57
|
+
* {images.map(img => (
|
|
58
|
+
* <XImage
|
|
59
|
+
* key={img.id}
|
|
60
|
+
* src={img.url}
|
|
61
|
+
* scrollPosition={scrollPosition}
|
|
62
|
+
* />
|
|
63
|
+
* ))}
|
|
64
|
+
* </div>
|
|
65
|
+
* );
|
|
66
|
+
*
|
|
67
|
+
* // 使用 HOC 包裹
|
|
68
|
+
* const OptimizedImageGrid = withScrollTracking(ImageGrid);
|
|
69
|
+
*
|
|
70
|
+
* // 使用
|
|
71
|
+
* <OptimizedImageGrid images={images} />
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function withScrollTracking<P extends object>(WrappedComponent: ComponentType<P & WithScrollTrackingProps>): ComponentType<Omit<P, "scrollPosition">>;
|
|
75
|
+
export default XImageGallery;
|
|
76
|
+
//# sourceMappingURL=XImageGallery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XImageGallery.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImageGallery.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAQ,aAAa,EAAyC,MAAM,OAAO,CAAC;AAE1F,OAAO,KAAK,EACR,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EAC1B,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,2CAAuD,CAAC;AAE1F;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D;AAgCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,aAAa,sFAiBzB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAC/C,gBAAgB,EAAE,aAAa,CAAC,CAAC,GAAG,uBAAuB,CAAC,GAC7D,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAW1C;AAED,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { memo, forwardRef, createContext, useContext } from "react";
|
|
3
|
+
import { trackWindowScroll } from "react-lazy-load-image-component";
|
|
4
|
+
/**
|
|
5
|
+
* ScrollPosition Context
|
|
6
|
+
* 用于在组件树中共享滚动位置,支持深层嵌套的 XImage 自动获取
|
|
7
|
+
*/
|
|
8
|
+
export const ScrollPositionContext = createContext(undefined);
|
|
9
|
+
/**
|
|
10
|
+
* Hook: 获取当前的滚动位置
|
|
11
|
+
* 在 XImageGallery 内部的任意深度使用
|
|
12
|
+
*/
|
|
13
|
+
export function useScrollPosition() {
|
|
14
|
+
return useContext(ScrollPositionContext);
|
|
15
|
+
}
|
|
16
|
+
const InternalGallery = memo(forwardRef(function InternalGallery({ children, className, style, scrollPosition }, ref) {
|
|
17
|
+
return (React.createElement(ScrollPositionContext.Provider, { value: scrollPosition },
|
|
18
|
+
React.createElement("div", { ref: ref, className: className, style: style }, children)));
|
|
19
|
+
}));
|
|
20
|
+
/**
|
|
21
|
+
* 使用 trackWindowScroll HOC 包裹的 Gallery
|
|
22
|
+
*/
|
|
23
|
+
const TrackedGallery = trackWindowScroll(InternalGallery);
|
|
24
|
+
/**
|
|
25
|
+
* XImageGallery - 图片画廊/列表性能优化组件
|
|
26
|
+
*
|
|
27
|
+
* 当页面中有大量图片需要懒加载时,使用此组件包裹可以:
|
|
28
|
+
* - 统一管理滚动监听,避免每个图片单独监听 scroll/resize 事件
|
|
29
|
+
* - 通过 trackWindowScroll HOC 实现性能优化
|
|
30
|
+
* - 通过 Context 自动向任意深度的子 XImage 组件传递 scrollPosition
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* // 基础用法 - 包裹多个 XImage(支持任意嵌套深度)
|
|
35
|
+
* <XImageGallery>
|
|
36
|
+
* <div className="grid">
|
|
37
|
+
* {images.map(img => (
|
|
38
|
+
* <div key={img.id} className="card">
|
|
39
|
+
* <XImage src={img.url} effect="blur" />
|
|
40
|
+
* </div>
|
|
41
|
+
* ))}
|
|
42
|
+
* </div>
|
|
43
|
+
* </XImageGallery>
|
|
44
|
+
*
|
|
45
|
+
* // 自定义节流配置
|
|
46
|
+
* <XImageGallery delayMethod="debounce" delayTime={200}>
|
|
47
|
+
* {images.map(img => (
|
|
48
|
+
* <XImage key={img.id} src={img.url} />
|
|
49
|
+
* ))}
|
|
50
|
+
* </XImageGallery>
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @note 注意事项:
|
|
54
|
+
* - 不要在 XImageGallery 内部嵌套另一个 XImageGallery
|
|
55
|
+
* - 内部的 XImage 会自动通过 Context 获取 scrollPosition,无需手动传递
|
|
56
|
+
*/
|
|
57
|
+
export const XImageGallery = memo(forwardRef(function XImageGallery({ children, className, style, delayMethod, delayTime }, ref) {
|
|
58
|
+
return (React.createElement(TrackedGallery, { ref: ref, className: className, style: style, delayMethod: delayMethod, delayTime: delayTime }, children));
|
|
59
|
+
}));
|
|
60
|
+
/**
|
|
61
|
+
* HOC: 为任意组件添加滚动位置追踪能力
|
|
62
|
+
*
|
|
63
|
+
* 适用于需要自定义容器但又想使用 trackWindowScroll 优化的场景
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* // 自定义图片网格组件
|
|
68
|
+
* const ImageGrid = ({ images, scrollPosition }) => (
|
|
69
|
+
* <div className="grid grid-cols-3">
|
|
70
|
+
* {images.map(img => (
|
|
71
|
+
* <XImage
|
|
72
|
+
* key={img.id}
|
|
73
|
+
* src={img.url}
|
|
74
|
+
* scrollPosition={scrollPosition}
|
|
75
|
+
* />
|
|
76
|
+
* ))}
|
|
77
|
+
* </div>
|
|
78
|
+
* );
|
|
79
|
+
*
|
|
80
|
+
* // 使用 HOC 包裹
|
|
81
|
+
* const OptimizedImageGrid = withScrollTracking(ImageGrid);
|
|
82
|
+
*
|
|
83
|
+
* // 使用
|
|
84
|
+
* <OptimizedImageGrid images={images} />
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function withScrollTracking(WrappedComponent) {
|
|
88
|
+
const WithScrollTracking = trackWindowScroll(WrappedComponent);
|
|
89
|
+
// 设置 displayName 便于调试
|
|
90
|
+
const wrappedName = WrappedComponent.displayName || WrappedComponent.name || "Component";
|
|
91
|
+
WithScrollTracking.displayName = `withScrollTracking(${wrappedName})`;
|
|
92
|
+
return WithScrollTracking;
|
|
93
|
+
}
|
|
94
|
+
export default XImageGallery;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
export { XImage, default as XImageDefault } from "./XImage";
|
|
2
|
-
export
|
|
2
|
+
export { XImageGallery, withScrollTracking, useScrollPosition, ScrollPositionContext, } from "./XImageGallery";
|
|
3
|
+
export type { XImageProps, XImageEffect, XImageStatus, XImageGalleryProps, ScrollPosition, WithScrollTrackingProps, DelayMethod, } from "./XImage.types";
|
|
4
|
+
/**
|
|
5
|
+
* XImage 效果 CSS 已内置,无需手动导入!
|
|
6
|
+
*
|
|
7
|
+
* 直接使用 effect 属性即可:
|
|
8
|
+
* <XImage src="..." effect="blur" />
|
|
9
|
+
* <XImage src="..." effect="opacity" />
|
|
10
|
+
* <XImage src="..." effect="black-and-white" />
|
|
11
|
+
*/
|
|
3
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/layout/image/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/layout/image/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EACH,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,GACxB,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EACR,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,WAAW,GACd,MAAM,gBAAgB,CAAC;AAGxB;;;;;;;GAOG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xxf_react",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"build": "tsc"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"react": ">=18"
|
|
74
|
-
"next": ">=13"
|
|
73
|
+
"react": ">=18"
|
|
75
74
|
},
|
|
76
75
|
"dependencies": {
|
|
77
76
|
"@microsoft/fetch-event-source": "^2.0.1",
|
|
77
|
+
"react-lazy-load-image-component": "^1.6.2",
|
|
78
78
|
"@use-gesture/react": "^10.3.1",
|
|
79
79
|
"bowser": "^2.14.1",
|
|
80
80
|
"broadcast-channel": "^7.3.0",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@types/node": "^25.3.0",
|
|
99
99
|
"@types/react": "^19.2.14",
|
|
100
|
+
"@types/react-lazy-load-image-component": "^1.6.4",
|
|
100
101
|
"next": "^16.1.6",
|
|
101
102
|
"typescript": "^5.9.3"
|
|
102
103
|
}
|