xxf_react 0.7.6 → 0.8.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/dist/http/interceptor/DefaultCacheInterceptor.d.ts +20 -6
- package/dist/http/interceptor/DefaultCacheInterceptor.d.ts.map +1 -1
- package/dist/http/interceptor/DefaultCacheInterceptor.js +65 -22
- package/dist/layout/image/XImage.d.ts +31 -15
- 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 +248 -32
- package/dist/layout/image/XImage.types.d.ts +130 -0
- package/dist/layout/image/XImage.types.d.ts.map +1 -0
- package/dist/layout/image/XImage.types.js +2 -0
- 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 +12 -0
- package/dist/layout/image/index.d.ts.map +1 -0
- package/dist/layout/image/index.js +4 -0
- package/dist/layout/index.d.ts +1 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +1 -1
- package/package.json +3 -1
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
* 全局缓存拦截器
|
|
3
3
|
* 用于 xxf_react/http 的 ApiBuilder
|
|
4
4
|
*/
|
|
5
|
-
import { CacheInterceptor } from
|
|
5
|
+
import { CacheInterceptor } from './CacheInterceptor';
|
|
6
6
|
/**
|
|
7
7
|
* 默认缓存拦截器
|
|
8
|
+
*
|
|
8
9
|
* 只有满足以下条件才缓存:
|
|
9
10
|
* - HTTP 状态码 = 200
|
|
10
|
-
* - Content-Type 包含 application/json
|
|
11
|
-
* - data
|
|
12
|
-
* - ApiResponse.code
|
|
11
|
+
* - Content-Type 包含 application/json(不区分大小写)
|
|
12
|
+
* - data 是有效对象或数组
|
|
13
|
+
* - ApiResponse.code 存在且为成功值(0 或 200)
|
|
14
|
+
*
|
|
15
|
+
* 特殊处理:
|
|
16
|
+
* - code 字段支持 number 或 string 类型
|
|
17
|
+
* - 所有检查都有异常保护,异常时默认不缓存
|
|
13
18
|
*
|
|
14
19
|
* @example
|
|
15
20
|
* ```ts
|
|
@@ -22,17 +27,26 @@ import { CacheInterceptor } from "./CacheInterceptor";
|
|
|
22
27
|
export declare const defaultCacheInterceptor: CacheInterceptor;
|
|
23
28
|
/**
|
|
24
29
|
* 创建自定义缓存拦截器
|
|
25
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* 在默认检查基础上添加自定义逻辑。
|
|
32
|
+
* 默认检查通过后才会执行自定义检查。
|
|
26
33
|
*
|
|
27
34
|
* @param customCheck 自定义检查函数,在默认检查通过后执行
|
|
35
|
+
* @returns 组合后的缓存拦截器
|
|
28
36
|
*
|
|
29
37
|
* @example
|
|
30
38
|
* ```ts
|
|
39
|
+
* // 额外检查:只缓存有 data 字段的响应
|
|
31
40
|
* const myInterceptor = createCacheInterceptor((ctx) => {
|
|
32
|
-
* // 额外检查:只缓存有 data 字段的响应
|
|
33
41
|
* const res = ctx.data as { data?: unknown }
|
|
34
42
|
* return res.data !== undefined
|
|
35
43
|
* })
|
|
44
|
+
*
|
|
45
|
+
* // 额外检查:只缓存列表数据
|
|
46
|
+
* const listInterceptor = createCacheInterceptor((ctx) => {
|
|
47
|
+
* const res = ctx.data as { data?: unknown[] }
|
|
48
|
+
* return Array.isArray(res.data) && res.data.length > 0
|
|
49
|
+
* })
|
|
36
50
|
* ```
|
|
37
51
|
*/
|
|
38
52
|
export declare function createCacheInterceptor(customCheck?: (ctx: Parameters<CacheInterceptor>[0]) => boolean): CacheInterceptor;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DefaultCacheInterceptor.d.ts","sourceRoot":"","sources":["../../../src/http/interceptor/DefaultCacheInterceptor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"DefaultCacheInterceptor.d.ts","sourceRoot":"","sources":["../../../src/http/interceptor/DefaultCacheInterceptor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGrD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAuCrC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CAClC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,GAChE,gBAAgB,CAiBlB"}
|
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
* 全局缓存拦截器
|
|
3
3
|
* 用于 xxf_react/http 的 ApiBuilder
|
|
4
4
|
*/
|
|
5
|
+
import { isApiSuccess } from '../../models';
|
|
5
6
|
/**
|
|
6
7
|
* 默认缓存拦截器
|
|
8
|
+
*
|
|
7
9
|
* 只有满足以下条件才缓存:
|
|
8
10
|
* - HTTP 状态码 = 200
|
|
9
|
-
* - Content-Type 包含 application/json
|
|
10
|
-
* - data
|
|
11
|
-
* - ApiResponse.code
|
|
11
|
+
* - Content-Type 包含 application/json(不区分大小写)
|
|
12
|
+
* - data 是有效对象或数组
|
|
13
|
+
* - ApiResponse.code 存在且为成功值(0 或 200)
|
|
14
|
+
*
|
|
15
|
+
* 特殊处理:
|
|
16
|
+
* - code 字段支持 number 或 string 类型
|
|
17
|
+
* - 所有检查都有异常保护,异常时默认不缓存
|
|
12
18
|
*
|
|
13
19
|
* @example
|
|
14
20
|
* ```ts
|
|
@@ -19,45 +25,82 @@
|
|
|
19
25
|
* ```
|
|
20
26
|
*/
|
|
21
27
|
export const defaultCacheInterceptor = (ctx) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
var _a, _b;
|
|
29
|
+
try {
|
|
30
|
+
// 1. 检查 HTTP 状态码
|
|
31
|
+
if (ctx.status !== 200) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// 2. 检查 Content-Type 是否为 JSON(不区分大小写)
|
|
35
|
+
const contentType = (_b = (_a = ctx.headers) === null || _a === void 0 ? void 0 : _a.get('Content-Type')) !== null && _b !== void 0 ? _b : '';
|
|
36
|
+
if (!contentType.toLowerCase().includes('application/json')) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// 3. 检查 data 是否为有效对象或数组
|
|
40
|
+
if (ctx.data === null || ctx.data === undefined) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (typeof ctx.data !== 'object') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// 4. 检查业务码(支持 number 或 string 类型)
|
|
47
|
+
const res = ctx.data;
|
|
48
|
+
if (res.code === undefined || res.code === null) {
|
|
49
|
+
// 没有 code 字段,不符合 ApiResponse 规范,不缓存
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// 将 code 转换为 number 进行判断
|
|
53
|
+
const codeNum = typeof res.code === 'string' ? parseInt(res.code, 10) : res.code;
|
|
54
|
+
if (isNaN(codeNum)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return isApiSuccess(codeNum);
|
|
30
58
|
}
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
catch {
|
|
60
|
+
// 任何异常都不缓存,保守策略
|
|
33
61
|
return false;
|
|
34
62
|
}
|
|
35
|
-
// 检查业务码:code 必须是 0 或 200
|
|
36
|
-
const res = ctx.data;
|
|
37
|
-
return !(res.code !== 0 && res.code !== 200);
|
|
38
63
|
};
|
|
39
64
|
/**
|
|
40
65
|
* 创建自定义缓存拦截器
|
|
41
|
-
*
|
|
66
|
+
*
|
|
67
|
+
* 在默认检查基础上添加自定义逻辑。
|
|
68
|
+
* 默认检查通过后才会执行自定义检查。
|
|
42
69
|
*
|
|
43
70
|
* @param customCheck 自定义检查函数,在默认检查通过后执行
|
|
71
|
+
* @returns 组合后的缓存拦截器
|
|
44
72
|
*
|
|
45
73
|
* @example
|
|
46
74
|
* ```ts
|
|
75
|
+
* // 额外检查:只缓存有 data 字段的响应
|
|
47
76
|
* const myInterceptor = createCacheInterceptor((ctx) => {
|
|
48
|
-
* // 额外检查:只缓存有 data 字段的响应
|
|
49
77
|
* const res = ctx.data as { data?: unknown }
|
|
50
78
|
* return res.data !== undefined
|
|
51
79
|
* })
|
|
80
|
+
*
|
|
81
|
+
* // 额外检查:只缓存列表数据
|
|
82
|
+
* const listInterceptor = createCacheInterceptor((ctx) => {
|
|
83
|
+
* const res = ctx.data as { data?: unknown[] }
|
|
84
|
+
* return Array.isArray(res.data) && res.data.length > 0
|
|
85
|
+
* })
|
|
52
86
|
* ```
|
|
53
87
|
*/
|
|
54
88
|
export function createCacheInterceptor(customCheck) {
|
|
55
89
|
return (ctx) => {
|
|
56
|
-
|
|
57
|
-
|
|
90
|
+
try {
|
|
91
|
+
// 先执行默认检查
|
|
92
|
+
if (!defaultCacheInterceptor(ctx)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// 执行自定义检查(如果有)
|
|
96
|
+
if (customCheck) {
|
|
97
|
+
return customCheck(ctx);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// 自定义检查异常时不缓存
|
|
58
103
|
return false;
|
|
59
104
|
}
|
|
60
|
-
// 执行自定义检查
|
|
61
|
-
return !(customCheck && !customCheck(ctx));
|
|
62
105
|
};
|
|
63
106
|
}
|
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
export interface XImageProps extends ImageProps {
|
|
4
|
-
/** 图片加载失败时显示的备用图片 URL 或自定义组件
|
|
5
|
-
* 可选:urlPlaceholder函数来生成失败的占位图
|
|
6
|
-
*/
|
|
7
|
-
fallback?: string | ReactNode;
|
|
8
|
-
}
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { XImageProps } from "./XImage.types";
|
|
9
3
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
4
|
+
* 生产级图片组件
|
|
5
|
+
*
|
|
6
|
+
* 基于 react-lazy-load-image-component 实现,支持:
|
|
7
|
+
* - 懒加载 + IntersectionObserver
|
|
8
|
+
* - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
|
|
9
|
+
* - 错误处理 + 自动重试
|
|
10
|
+
* - fallback 备用图/组件
|
|
11
|
+
* - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // 基础用法
|
|
16
|
+
* <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
|
|
17
|
+
*
|
|
18
|
+
* // 带模糊效果(提供 placeholder URL 自动启用 blur)
|
|
19
|
+
* <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
|
|
20
|
+
*
|
|
21
|
+
* // 自定义占位组件
|
|
22
|
+
* <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
|
|
23
|
+
*
|
|
24
|
+
* // 带错误处理
|
|
25
|
+
* <XImage
|
|
26
|
+
* src="/photo.jpg"
|
|
27
|
+
* fallback="/fallback.jpg"
|
|
28
|
+
* retryCount={2}
|
|
29
|
+
* retryDelay={1000}
|
|
30
|
+
* />
|
|
31
|
+
* ```
|
|
16
32
|
*/
|
|
17
|
-
export declare
|
|
33
|
+
export declare const XImage: React.NamedExoticComponent<XImageProps & React.RefAttributes<HTMLDivElement | HTMLImageElement>>;
|
|
18
34
|
export default XImage;
|
|
19
35
|
//# sourceMappingURL=XImage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"XImage.d.ts","sourceRoot":"","sources":["../../../src/layout/image/XImage.tsx"],"names":[],"mappings":"AAEA,
|
|
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,45 +1,261 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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";
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 生产级图片组件
|
|
8
|
+
*
|
|
9
|
+
* 基于 react-lazy-load-image-component 实现,支持:
|
|
10
|
+
* - 懒加载 + IntersectionObserver
|
|
11
|
+
* - 多种加载效果 (blur/opacity/black-and-white) - CSS 已内置,无需导入
|
|
12
|
+
* - 错误处理 + 自动重试
|
|
13
|
+
* - fallback 备用图/组件
|
|
14
|
+
* - 性能优化 (配合 XImageGallery 使用 trackWindowScroll)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // 基础用法
|
|
19
|
+
* <XImage src="/photo.jpg" alt="Photo" width={400} height={300} />
|
|
20
|
+
*
|
|
21
|
+
* // 带模糊效果(提供 placeholder URL 自动启用 blur)
|
|
22
|
+
* <XImage src="/photo.jpg" placeholder="/photo-tiny.jpg" />
|
|
23
|
+
*
|
|
24
|
+
* // 自定义占位组件
|
|
25
|
+
* <XImage src="/photo.jpg" placeholder={<Skeleton />} effect="opacity" />
|
|
26
|
+
*
|
|
27
|
+
* // 带错误处理
|
|
28
|
+
* <XImage
|
|
29
|
+
* src="/photo.jpg"
|
|
30
|
+
* fallback="/fallback.jpg"
|
|
31
|
+
* retryCount={2}
|
|
32
|
+
* retryDelay={1000}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
11
35
|
*/
|
|
12
|
-
export function XImage({
|
|
13
|
-
|
|
36
|
+
export const XImage = memo(forwardRef(function XImage({
|
|
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
|
+
// ============ 状态管理 ============
|
|
70
|
+
const [currentSrc, setCurrentSrc] = useState(src);
|
|
71
|
+
const [status, setStatus] = useState("idle");
|
|
14
72
|
const [hasError, setHasError] = useState(false);
|
|
15
|
-
|
|
73
|
+
const [retryAttempt, setRetryAttempt] = useState(0);
|
|
74
|
+
// Refs
|
|
75
|
+
const wrapperRef = useRef(null);
|
|
76
|
+
const retryTimerRef = useRef(null);
|
|
77
|
+
const hasCalledOnVisible = useRef(false);
|
|
78
|
+
// 使用 ref 存储回调,避免 effect 依赖问题
|
|
79
|
+
const onStatusChangeRef = useRef(onStatusChange);
|
|
80
|
+
onStatusChangeRef.current = onStatusChange;
|
|
81
|
+
// ============ src 变化重置状态 ============
|
|
16
82
|
useEffect(() => {
|
|
17
|
-
|
|
83
|
+
var _a;
|
|
84
|
+
setCurrentSrc(src);
|
|
18
85
|
setHasError(false);
|
|
86
|
+
setRetryAttempt(0);
|
|
87
|
+
setStatus("idle");
|
|
88
|
+
(_a = onStatusChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStatusChangeRef, "idle");
|
|
89
|
+
hasCalledOnVisible.current = false;
|
|
90
|
+
// 清理重试定时器
|
|
91
|
+
if (retryTimerRef.current) {
|
|
92
|
+
clearTimeout(retryTimerRef.current);
|
|
93
|
+
retryTimerRef.current = null;
|
|
94
|
+
}
|
|
19
95
|
}, [src]);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
96
|
+
// 清理定时器
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
return () => {
|
|
99
|
+
if (retryTimerRef.current) {
|
|
100
|
+
clearTimeout(retryTimerRef.current);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}, []);
|
|
104
|
+
// ============ ref 转发:从 wrapper 中获取 img 元素 ============
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!ref || !wrapperRef.current)
|
|
107
|
+
return;
|
|
108
|
+
const imgElement = wrapperRef.current.querySelector("img");
|
|
109
|
+
if (imgElement) {
|
|
110
|
+
if (typeof ref === "function") {
|
|
111
|
+
ref(imgElement);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
ref.current = imgElement;
|
|
27
115
|
}
|
|
28
116
|
}
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
//
|
|
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) => {
|
|
127
|
+
updateStatus("loaded");
|
|
128
|
+
onLoad === null || onLoad === void 0 ? void 0 : onLoad(event);
|
|
129
|
+
}, [updateStatus, onLoad]);
|
|
130
|
+
const handleError = useCallback((event) => {
|
|
131
|
+
setRetryAttempt((currentRetry) => {
|
|
132
|
+
// 还有重试次数
|
|
133
|
+
if (currentRetry < retryCount) {
|
|
134
|
+
updateStatus("loading");
|
|
135
|
+
retryTimerRef.current = setTimeout(() => {
|
|
136
|
+
// 通过修改 src 触发重新加载(添加时间戳绕过缓存)
|
|
137
|
+
const separator = src.includes("?") ? "&" : "?";
|
|
138
|
+
setCurrentSrc(`${src}${separator}_retry=${Date.now()}`);
|
|
139
|
+
}, retryDelay);
|
|
140
|
+
return currentRetry + 1;
|
|
141
|
+
}
|
|
142
|
+
// 重试耗尽,检查 fallback
|
|
143
|
+
setHasError((currentHasError) => {
|
|
144
|
+
if (fallback && !currentHasError) {
|
|
145
|
+
if (typeof fallback === "string") {
|
|
146
|
+
setCurrentSrc(fallback);
|
|
147
|
+
updateStatus("loading");
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
// 最终失败
|
|
152
|
+
updateStatus("error");
|
|
153
|
+
onError === null || onError === void 0 ? void 0 : onError(event);
|
|
154
|
+
return currentHasError;
|
|
155
|
+
});
|
|
156
|
+
return currentRetry;
|
|
157
|
+
});
|
|
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(通过类型扩展)
|
|
216
|
+
const userRef = wrapperProps === null || wrapperProps === void 0 ? void 0 : wrapperProps.ref;
|
|
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
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
...wrapperProps,
|
|
233
|
+
ref: combinedRef,
|
|
234
|
+
style: wrapperStyle,
|
|
235
|
+
className: wrapperClassName,
|
|
236
|
+
};
|
|
237
|
+
}, [wrapperProps, wrapperStyle, wrapperClassName]);
|
|
238
|
+
// ============ 渲染 fallback ReactNode ============
|
|
32
239
|
if (hasError && fallback && typeof fallback !== "string") {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
inset: props.fill ? 0 : undefined,
|
|
40
|
-
...props.style,
|
|
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,
|
|
41
246
|
} }, fallback));
|
|
42
247
|
}
|
|
43
|
-
|
|
44
|
-
|
|
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 }));
|
|
260
|
+
}));
|
|
45
261
|
export default XImage;
|
|
@@ -0,0 +1,130 @@
|
|
|
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";
|
|
13
|
+
/**
|
|
14
|
+
* 图片加载状态
|
|
15
|
+
*/
|
|
16
|
+
export type XImageStatus = "idle" | "loading" | "loaded" | "error";
|
|
17
|
+
/**
|
|
18
|
+
* 滚动位置类型 (用于 trackWindowScroll HOC)
|
|
19
|
+
*/
|
|
20
|
+
export interface ScrollPosition {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* XImage 组件 Props
|
|
26
|
+
*/
|
|
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;
|
|
52
|
+
/**
|
|
53
|
+
* 占位内容
|
|
54
|
+
* - string: 占位图片 URL(自动启用 blur 效果)
|
|
55
|
+
* - ReactElement: 自定义占位组件(如 Skeleton,需手动指定 effect)
|
|
56
|
+
*/
|
|
57
|
+
placeholder?: string | ReactElement | null;
|
|
58
|
+
/** 是否使用 IntersectionObserver,默认 true */
|
|
59
|
+
useIntersectionObserver?: boolean;
|
|
60
|
+
/** 滚动/resize 事件的节流方法 */
|
|
61
|
+
delayMethod?: DelayMethod;
|
|
62
|
+
/** 节流延迟时间(毫秒) */
|
|
63
|
+
delayTime?: number;
|
|
64
|
+
/**
|
|
65
|
+
* 加载失败时显示的备用内容
|
|
66
|
+
* - string: 备用图片 URL
|
|
67
|
+
* - ReactNode: 自定义错误展示组件
|
|
68
|
+
*/
|
|
69
|
+
fallback?: string | ReactNode;
|
|
70
|
+
/** 加载失败后的重试次数,默认 0 */
|
|
71
|
+
retryCount?: number;
|
|
72
|
+
/** 重试间隔(毫秒),默认 1000 */
|
|
73
|
+
retryDelay?: number;
|
|
74
|
+
/**
|
|
75
|
+
* 滚动位置(来自 trackWindowScroll HOC)
|
|
76
|
+
* 当父组件使用 withScrollTracking 包裹时自动传入
|
|
77
|
+
*/
|
|
78
|
+
scrollPosition?: ScrollPosition;
|
|
79
|
+
/**
|
|
80
|
+
* 设为 true 则图片立即显示(不触发懒加载)
|
|
81
|
+
* 适用于已在浏览器缓存中的图片
|
|
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
|
+
/** 包裹元素的类名 */
|
|
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;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=XImage.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { XImage, default as XImageDefault } from "./XImage";
|
|
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
|
+
*/
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;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/dist/layout/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export * from './resize/core/SizedLayoutContext';
|
|
|
3
3
|
export * from './resize/core/ResizeObserverHook';
|
|
4
4
|
export * from './resize/impl/SizedContainer';
|
|
5
5
|
export * from './resize/impl/SizedLayout';
|
|
6
|
-
export * from './image
|
|
6
|
+
export * from './image';
|
|
7
7
|
export * from './visibility/ElementVisibilityHooks';
|
|
8
8
|
export * from './virtualized/VirtualizedConfig';
|
|
9
9
|
export * from './hover/XHover';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA;AAChD,cAAc,kCAAkC,CAAA;AAChD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA;AACzC,cAAc,SAAS,CAAA;AACvB,cAAc,qCAAqC,CAAA;AACnD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA"}
|
package/dist/layout/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export * from './resize/core/SizedLayoutContext';
|
|
|
3
3
|
export * from './resize/core/ResizeObserverHook';
|
|
4
4
|
export * from './resize/impl/SizedContainer';
|
|
5
5
|
export * from './resize/impl/SizedLayout';
|
|
6
|
-
export * from './image
|
|
6
|
+
export * from './image';
|
|
7
7
|
export * from './visibility/ElementVisibilityHooks';
|
|
8
8
|
export * from './virtualized/VirtualizedConfig';
|
|
9
9
|
export * from './hover/XHover';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xxf_react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@microsoft/fetch-event-source": "^2.0.1",
|
|
77
|
+
"react-lazy-load-image-component": "^1.6.2",
|
|
77
78
|
"@use-gesture/react": "^10.3.1",
|
|
78
79
|
"bowser": "^2.14.1",
|
|
79
80
|
"broadcast-channel": "^7.3.0",
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"devDependencies": {
|
|
97
98
|
"@types/node": "^25.3.0",
|
|
98
99
|
"@types/react": "^19.2.14",
|
|
100
|
+
"@types/react-lazy-load-image-component": "^1.6.4",
|
|
99
101
|
"next": "^16.1.6",
|
|
100
102
|
"typescript": "^5.9.3"
|
|
101
103
|
}
|