xxf_react 0.8.1 → 0.8.2
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 +17 -10
- package/dist/layout/image/XImage.d.ts.map +1 -1
- package/dist/layout/image/XImage.js +178 -183
- package/dist/layout/image/XImage.types.d.ts +87 -107
- package/dist/layout/image/XImage.types.d.ts.map +1 -1
- package/dist/layout/image/index.d.ts +1 -10
- package/dist/layout/image/index.d.ts.map +1 -1
- package/dist/layout/image/index.js +0 -1
- package/package.json +3 -4
- package/dist/layout/image/XImage.effects.d.ts +0 -16
- package/dist/layout/image/XImage.effects.d.ts.map +0 -1
- package/dist/layout/image/XImage.effects.js +0 -77
- package/dist/layout/image/XImageGallery.d.ts +0 -76
- package/dist/layout/image/XImageGallery.d.ts.map +0 -1
- package/dist/layout/image/XImageGallery.js +0 -94
|
@@ -3,31 +3,38 @@ import type { XImageProps } from "./XImage.types";
|
|
|
3
3
|
/**
|
|
4
4
|
* 生产级图片组件
|
|
5
5
|
*
|
|
6
|
-
* 基于
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
6
|
+
* 基于 next/image 实现,支持:
|
|
7
|
+
* - 双层渲染:placeholder 先加载,主图加载完成后 blur 过渡
|
|
8
|
+
* - 自动图片优化(Next.js 内置)
|
|
9
9
|
* - 错误处理 + 自动重试
|
|
10
10
|
* - fallback 备用图/组件
|
|
11
|
-
* - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
|
|
12
11
|
*
|
|
13
12
|
* @example
|
|
14
13
|
* ```tsx
|
|
15
14
|
* // 基础用法
|
|
16
15
|
* <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
|
|
17
16
|
*
|
|
18
|
-
* //
|
|
19
|
-
* <XImage
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* // 带 placeholder 模糊过渡(推荐使用小尺寸缩略图,如 w=20)
|
|
18
|
+
* <XImage
|
|
19
|
+
* src="/photo.jpg?w=800"
|
|
20
|
+
* placeholder="/photo.jpg?w=20"
|
|
21
|
+
* width={400}
|
|
22
|
+
* height={300}
|
|
23
|
+
* />
|
|
23
24
|
*
|
|
24
|
-
* //
|
|
25
|
+
* // 带错误处理和重试
|
|
25
26
|
* <XImage
|
|
26
27
|
* src="/photo.jpg"
|
|
27
28
|
* fallback="/fallback.jpg"
|
|
28
29
|
* retryCount={2}
|
|
29
30
|
* retryDelay={1000}
|
|
30
31
|
* />
|
|
32
|
+
*
|
|
33
|
+
* // 自定义占位组件
|
|
34
|
+
* <XImage
|
|
35
|
+
* src="/photo.jpg"
|
|
36
|
+
* placeholder={<Skeleton className="w-full h-full" />}
|
|
37
|
+
* />
|
|
31
38
|
* ```
|
|
32
39
|
*/
|
|
33
40
|
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,KAQN,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAmBhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,MAAM,kGA4TlB,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -1,99 +1,109 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useState, useEffect, useCallback, useRef, forwardRef, memo, useMemo,
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import React, { useState, useEffect, useCallback, useRef, forwardRef, memo, useMemo, } from "react";
|
|
3
|
+
import Image from "next/image";
|
|
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
|
+
// ============================================================================
|
|
6
16
|
/**
|
|
7
17
|
* 生产级图片组件
|
|
8
18
|
*
|
|
9
|
-
* 基于
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
19
|
+
* 基于 next/image 实现,支持:
|
|
20
|
+
* - 双层渲染:placeholder 先加载,主图加载完成后 blur 过渡
|
|
21
|
+
* - 自动图片优化(Next.js 内置)
|
|
12
22
|
* - 错误处理 + 自动重试
|
|
13
23
|
* - fallback 备用图/组件
|
|
14
|
-
* - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
|
|
15
24
|
*
|
|
16
25
|
* @example
|
|
17
26
|
* ```tsx
|
|
18
27
|
* // 基础用法
|
|
19
28
|
* <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
|
|
20
29
|
*
|
|
21
|
-
* //
|
|
22
|
-
* <XImage
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
30
|
+
* // 带 placeholder 模糊过渡(推荐使用小尺寸缩略图,如 w=20)
|
|
31
|
+
* <XImage
|
|
32
|
+
* src="/photo.jpg?w=800"
|
|
33
|
+
* placeholder="/photo.jpg?w=20"
|
|
34
|
+
* width={400}
|
|
35
|
+
* height={300}
|
|
36
|
+
* />
|
|
26
37
|
*
|
|
27
|
-
* //
|
|
38
|
+
* // 带错误处理和重试
|
|
28
39
|
* <XImage
|
|
29
40
|
* src="/photo.jpg"
|
|
30
41
|
* fallback="/fallback.jpg"
|
|
31
42
|
* retryCount={2}
|
|
32
43
|
* retryDelay={1000}
|
|
33
44
|
* />
|
|
45
|
+
*
|
|
46
|
+
* // 自定义占位组件
|
|
47
|
+
* <XImage
|
|
48
|
+
* src="/photo.jpg"
|
|
49
|
+
* placeholder={<Skeleton className="w-full h-full" />}
|
|
50
|
+
* />
|
|
34
51
|
* ```
|
|
35
52
|
*/
|
|
36
53
|
export const XImage = memo(forwardRef(function XImage({
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
//
|
|
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
|
-
// ============ 状态管理 ============
|
|
54
|
+
// -------- XImage 扩展属性 --------
|
|
55
|
+
placeholder, fallback, retryCount = 0, retryDelay = 1000, onStatusChange, wrapperProps, wrapperClassName,
|
|
56
|
+
// -------- next/image 常用属性(需要特殊处理) --------
|
|
57
|
+
src, alt = "", fill, style, className, onLoad, onError,
|
|
58
|
+
// -------- next/image 其余属性(透传) --------
|
|
59
|
+
...imageProps }, ref) {
|
|
60
|
+
// ========================================================================
|
|
61
|
+
// 解析 placeholder 类型
|
|
62
|
+
// ========================================================================
|
|
63
|
+
const placeholderSrc = typeof placeholder === "string" ? placeholder : undefined;
|
|
64
|
+
const placeholderElement = typeof placeholder !== "string" ? placeholder : undefined;
|
|
65
|
+
const hasPlaceholder = Boolean(placeholderSrc || placeholderElement);
|
|
66
|
+
// ========================================================================
|
|
67
|
+
// 状态管理
|
|
68
|
+
// ========================================================================
|
|
70
69
|
const [currentSrc, setCurrentSrc] = useState(src);
|
|
71
70
|
const [status, setStatus] = useState("idle");
|
|
72
71
|
const [hasError, setHasError] = useState(false);
|
|
73
72
|
const [retryAttempt, setRetryAttempt] = useState(0);
|
|
73
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
74
|
+
// ========================================================================
|
|
74
75
|
// Refs
|
|
76
|
+
// ========================================================================
|
|
75
77
|
const wrapperRef = useRef(null);
|
|
78
|
+
const mainImageRef = useRef(null);
|
|
76
79
|
const retryTimerRef = useRef(null);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
80
|
+
// 使用 ref 存储回调,避免 useCallback 依赖问题
|
|
81
|
+
const callbacksRef = useRef({ onStatusChange, onLoad, onError });
|
|
82
|
+
callbacksRef.current = { onStatusChange, onLoad, onError };
|
|
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
|
+
// ========================================================================
|
|
82
94
|
useEffect(() => {
|
|
83
|
-
var _a;
|
|
84
95
|
setCurrentSrc(src);
|
|
85
96
|
setHasError(false);
|
|
86
97
|
setRetryAttempt(0);
|
|
87
|
-
|
|
88
|
-
(
|
|
89
|
-
hasCalledOnVisible.current = false;
|
|
98
|
+
setIsLoaded(false);
|
|
99
|
+
updateStatus("loading");
|
|
90
100
|
// 清理重试定时器
|
|
91
101
|
if (retryTimerRef.current) {
|
|
92
102
|
clearTimeout(retryTimerRef.current);
|
|
93
103
|
retryTimerRef.current = null;
|
|
94
104
|
}
|
|
95
|
-
}, [src]);
|
|
96
|
-
//
|
|
105
|
+
}, [src, updateStatus]);
|
|
106
|
+
// 组件卸载时清理定时器
|
|
97
107
|
useEffect(() => {
|
|
98
108
|
return () => {
|
|
99
109
|
if (retryTimerRef.current) {
|
|
@@ -101,161 +111,146 @@ loading, decoding = "async", srcSet, sizes, crossOrigin, referrerPolicy, }, ref)
|
|
|
101
111
|
}
|
|
102
112
|
};
|
|
103
113
|
}, []);
|
|
104
|
-
//
|
|
114
|
+
// ========================================================================
|
|
115
|
+
// ref 转发:将内部 img 元素暴露给外部
|
|
116
|
+
// ========================================================================
|
|
105
117
|
useEffect(() => {
|
|
106
118
|
if (!ref || !wrapperRef.current)
|
|
107
119
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
// 获取主图 img 元素(双层渲染时取第二个,单层取第一个)
|
|
121
|
+
const imgs = wrapperRef.current.querySelectorAll("img");
|
|
122
|
+
const targetImg = imgs.length > 1 ? imgs[1] : imgs[0];
|
|
123
|
+
if (targetImg) {
|
|
110
124
|
if (typeof ref === "function") {
|
|
111
|
-
ref(
|
|
125
|
+
ref(targetImg);
|
|
112
126
|
}
|
|
113
127
|
else {
|
|
114
|
-
ref.current =
|
|
128
|
+
ref.current = targetImg;
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
}, [ref, isLoaded]); // isLoaded 变化时重新获取,确保 img 已渲染
|
|
132
|
+
// ========================================================================
|
|
133
|
+
// 事件处理
|
|
134
|
+
// ========================================================================
|
|
135
|
+
/**
|
|
136
|
+
* 主图加载成功
|
|
137
|
+
*/
|
|
138
|
+
const handleLoad = useCallback((e) => {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
setIsLoaded(true);
|
|
127
141
|
updateStatus("loaded");
|
|
128
|
-
onLoad === null ||
|
|
129
|
-
}, [updateStatus
|
|
130
|
-
|
|
142
|
+
(_b = (_a = callbacksRef.current).onLoad) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
143
|
+
}, [updateStatus]);
|
|
144
|
+
/**
|
|
145
|
+
* 主图加载失败 - 处理重试和 fallback 逻辑
|
|
146
|
+
*/
|
|
147
|
+
const handleError = useCallback((e) => {
|
|
131
148
|
setRetryAttempt((currentRetry) => {
|
|
132
|
-
//
|
|
149
|
+
// 还有重试次数,延迟后重试
|
|
133
150
|
if (currentRetry < retryCount) {
|
|
134
|
-
updateStatus("loading");
|
|
135
151
|
retryTimerRef.current = setTimeout(() => {
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
|
|
152
|
+
// 添加时间戳绕过缓存
|
|
153
|
+
const srcStr = typeof src === "string" ? src : "";
|
|
154
|
+
const separator = srcStr.includes("?") ? "&" : "?";
|
|
155
|
+
setCurrentSrc(`${srcStr}${separator}_retry=${Date.now()}`);
|
|
139
156
|
}, retryDelay);
|
|
140
157
|
return currentRetry + 1;
|
|
141
158
|
}
|
|
142
|
-
//
|
|
159
|
+
// 重试耗尽,尝试 fallback
|
|
143
160
|
setHasError((currentHasError) => {
|
|
161
|
+
var _a, _b;
|
|
144
162
|
if (fallback && !currentHasError) {
|
|
145
163
|
if (typeof fallback === "string") {
|
|
146
164
|
setCurrentSrc(fallback);
|
|
147
|
-
|
|
165
|
+
return true;
|
|
148
166
|
}
|
|
167
|
+
// fallback 是 ReactNode,标记错误状态
|
|
168
|
+
updateStatus("error");
|
|
149
169
|
return true;
|
|
150
170
|
}
|
|
151
|
-
//
|
|
171
|
+
// 无 fallback 或已经在 fallback 状态,触发错误回调
|
|
152
172
|
updateStatus("error");
|
|
153
|
-
onError === null ||
|
|
173
|
+
(_b = (_a = callbacksRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
154
174
|
return currentHasError;
|
|
155
175
|
});
|
|
156
176
|
return currentRetry;
|
|
157
177
|
});
|
|
158
|
-
}, [retryCount, retryDelay, src, fallback, updateStatus
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(通过类型扩展)
|
|
178
|
+
}, [retryCount, retryDelay, src, fallback, updateStatus]);
|
|
179
|
+
// ========================================================================
|
|
180
|
+
// 合并 wrapperProps 的 ref
|
|
181
|
+
// ========================================================================
|
|
182
|
+
const mergedWrapperRef = useCallback((node) => {
|
|
183
|
+
wrapperRef.current = node;
|
|
184
|
+
// 调用用户传入的 ref
|
|
216
185
|
const userRef = wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.ref;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
}
|
|
186
|
+
if (userRef) {
|
|
187
|
+
if (typeof userRef === "function") {
|
|
188
|
+
userRef(node);
|
|
229
189
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
190
|
+
else {
|
|
191
|
+
userRef.current = node;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, [wrapperProps]);
|
|
195
|
+
// ========================================================================
|
|
196
|
+
// 计算样式(使用 useMemo 避免重复创建对象)
|
|
197
|
+
// ========================================================================
|
|
198
|
+
/** 容器样式 */
|
|
199
|
+
const wrapperStyle = useMemo(() => ({
|
|
200
|
+
position: "relative",
|
|
201
|
+
display: "inline-block",
|
|
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
|
|
239
232
|
if (hasError && fallback && typeof fallback !== "string") {
|
|
240
|
-
return (React.createElement("div", { ref: ref, className: className, style: {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
233
|
+
return (React.createElement("div", { ref: ref, className: wrapperClassName || className, style: { ...wrapperStyle, ...style }, ...wrapperProps }, fallback));
|
|
234
|
+
}
|
|
235
|
+
// 有 placeholder(URL 或 ReactElement):双层渲染
|
|
236
|
+
if (hasPlaceholder) {
|
|
237
|
+
return (React.createElement("div", { ...wrapperProps, ref: mergedWrapperRef, className: wrapperClassName, style: wrapperStyle },
|
|
238
|
+
placeholderSrc ? (
|
|
239
|
+
// URL 占位图:使用 next/image,priority 立即加载
|
|
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 })));
|
|
247
251
|
}
|
|
248
|
-
//
|
|
249
|
-
return (React.createElement(
|
|
250
|
-
|
|
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 }));
|
|
252
|
+
// 无 placeholder:单层渲染
|
|
253
|
+
return (React.createElement("div", { ...wrapperProps, ref: mergedWrapperRef, className: wrapperClassName, style: wrapperStyle },
|
|
254
|
+
React.createElement(Image, { ...imageProps, ref: mainImageRef, src: currentSrc, alt: alt, fill: fill, className: className, style: style, onLoad: handleLoad, onError: handleError })));
|
|
260
255
|
}));
|
|
261
256
|
export default XImage;
|
|
@@ -1,130 +1,110 @@
|
|
|
1
|
-
import type {
|
|
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";
|
|
1
|
+
import type { ImageProps } from "next/image";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
13
3
|
/**
|
|
14
4
|
* 图片加载状态
|
|
5
|
+
*
|
|
6
|
+
* - `idle`: 初始状态(未开始加载)
|
|
7
|
+
* - `loading`: 加载中
|
|
8
|
+
* - `loaded`: 加载成功
|
|
9
|
+
* - `error`: 加载失败(重试耗尽且无 fallback)
|
|
15
10
|
*/
|
|
16
11
|
export type XImageStatus = "idle" | "loading" | "loaded" | "error";
|
|
17
|
-
/**
|
|
18
|
-
* 滚动位置类型 (用于 trackWindowScroll HOC)
|
|
19
|
-
*/
|
|
20
|
-
export interface ScrollPosition {
|
|
21
|
-
x: number;
|
|
22
|
-
y: number;
|
|
23
|
-
}
|
|
24
12
|
/**
|
|
25
13
|
* 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
|
+
* ```
|
|
26
39
|
*/
|
|
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;
|
|
40
|
+
export interface XImageProps extends Omit<ImageProps, "placeholder" | "ref"> {
|
|
52
41
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* -
|
|
42
|
+
* 占位内容(用于双层渲染 + blur 过渡)
|
|
43
|
+
*
|
|
44
|
+
* - `string`: 占位图片 URL(推荐使用小尺寸缩略图)
|
|
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
|
+
* ```
|
|
56
55
|
*/
|
|
57
56
|
placeholder?: string | ReactElement | null;
|
|
58
|
-
/** 是否使用 IntersectionObserver,默认 true */
|
|
59
|
-
useIntersectionObserver?: boolean;
|
|
60
|
-
/** 滚动/resize 事件的节流方法 */
|
|
61
|
-
delayMethod?: DelayMethod;
|
|
62
|
-
/** 节流延迟时间(毫秒) */
|
|
63
|
-
delayTime?: number;
|
|
64
57
|
/**
|
|
65
58
|
* 加载失败时显示的备用内容
|
|
66
|
-
*
|
|
67
|
-
* -
|
|
59
|
+
*
|
|
60
|
+
* - `string`: 备用图片 URL(会尝试加载)
|
|
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
|
+
* ```
|
|
68
71
|
*/
|
|
69
72
|
fallback?: string | ReactNode;
|
|
70
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* 加载失败后的重试次数
|
|
75
|
+
*
|
|
76
|
+
* @default 0
|
|
77
|
+
*/
|
|
71
78
|
retryCount?: number;
|
|
72
|
-
/** 重试间隔(毫秒),默认 1000 */
|
|
73
|
-
retryDelay?: number;
|
|
74
79
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
80
|
+
* 重试间隔(毫秒)
|
|
81
|
+
*
|
|
82
|
+
* @default 1000
|
|
77
83
|
*/
|
|
78
|
-
|
|
84
|
+
retryDelay?: number;
|
|
79
85
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
86
|
+
* 加载状态变化回调
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* <XImage
|
|
91
|
+
* src="/photo.jpg"
|
|
92
|
+
* onStatusChange={(status) => console.log('Status:', status)}
|
|
93
|
+
* />
|
|
94
|
+
* ```
|
|
82
95
|
*/
|
|
83
|
-
visibleByDefault?: boolean;
|
|
84
|
-
/** 图片加载完成回调 */
|
|
85
|
-
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
86
|
-
/** 图片加载失败回调 */
|
|
87
|
-
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
|
88
|
-
/** 图片进入视口时回调 */
|
|
89
|
-
onVisible?: () => void;
|
|
90
|
-
/** 加载状态变化回调 */
|
|
91
96
|
onStatusChange?: (status: XImageStatus) => void;
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
/**
|
|
98
|
+
* 传递给包裹 div 元素的属性
|
|
99
|
+
*
|
|
100
|
+
* 用于自定义容器的事件处理、数据属性等
|
|
101
|
+
*/
|
|
102
|
+
wrapperProps?: React.HTMLAttributes<HTMLDivElement>;
|
|
103
|
+
/**
|
|
104
|
+
* 包裹元素的类名
|
|
105
|
+
*
|
|
106
|
+
* 与 className 区分:className 应用于 img,wrapperClassName 应用于外层 div
|
|
107
|
+
*/
|
|
95
108
|
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;
|
|
129
109
|
}
|
|
130
110
|
//# 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":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,UAAU,EAAE,aAAa,GAAG,KAAK,CAAC;IACxE;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;IAE3C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAEhD;;;;OAIG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IAEpD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B"}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
1
|
export { XImage, default as XImageDefault } from "./XImage";
|
|
2
|
-
export {
|
|
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
|
-
*/
|
|
2
|
+
export type { XImageProps, XImageStatus } from "./XImage.types";
|
|
12
3
|
//# 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;AAG5D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xxf_react",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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"
|
|
73
|
+
"react": ">=18",
|
|
74
|
+
"next": ">=13"
|
|
74
75
|
},
|
|
75
76
|
"dependencies": {
|
|
76
77
|
"@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,7 +97,6 @@
|
|
|
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",
|
|
101
100
|
"next": "^16.1.6",
|
|
102
101
|
"typescript": "^5.9.3"
|
|
103
102
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,77 +0,0 @@
|
|
|
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,76 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,94 +0,0 @@
|
|
|
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;
|