clxx 2.1.7 → 3.0.0

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.
Files changed (75) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +147 -22
  3. package/build/Alert/Wrapper.js +12 -14
  4. package/build/Alert/style.js +44 -25
  5. package/build/AutoGrid/index.js +21 -15
  6. package/build/CarouselNotice/index.d.ts +19 -11
  7. package/build/CarouselNotice/index.js +80 -74
  8. package/build/CarouselNotice/style.js +14 -4
  9. package/build/CitySelect/index.js +81 -71
  10. package/build/CitySelect/style.js +22 -56
  11. package/build/Clickable/index.js +7 -0
  12. package/build/Container/index.d.ts +12 -4
  13. package/build/Container/index.js +94 -89
  14. package/build/Countdowner/index.js +4 -2
  15. package/build/DatePicker/Column.d.ts +9 -0
  16. package/build/DatePicker/Column.js +330 -0
  17. package/build/DatePicker/index.d.ts +32 -0
  18. package/build/DatePicker/index.js +230 -0
  19. package/build/DatePicker/style.d.ts +6 -0
  20. package/build/DatePicker/style.js +130 -0
  21. package/build/Dialog/Wrapper.d.ts +0 -1
  22. package/build/Dialog/Wrapper.js +22 -12
  23. package/build/Dialog/index.d.ts +7 -1
  24. package/build/Dialog/index.js +57 -32
  25. package/build/Dialog/style.js +6 -2
  26. package/build/Effect/useInterval.js +6 -3
  27. package/build/Fixed/index.js +13 -22
  28. package/build/Flex/FlexItem.d.ts +11 -0
  29. package/build/Flex/FlexItem.js +26 -0
  30. package/build/Flex/index.d.ts +2 -10
  31. package/build/Flex/index.js +12 -22
  32. package/build/Indicator/index.d.ts +9 -6
  33. package/build/Indicator/index.js +34 -37
  34. package/build/Indicator/style.d.ts +4 -3
  35. package/build/Indicator/style.js +8 -13
  36. package/build/Loading/Wrapper.js +2 -1
  37. package/build/Loading/style.js +9 -12
  38. package/build/Overlay/index.js +6 -1
  39. package/build/RegionPicker/data.d.ts +6 -0
  40. package/build/RegionPicker/data.js +14486 -0
  41. package/build/RegionPicker/index.d.ts +33 -0
  42. package/build/RegionPicker/index.js +205 -0
  43. package/build/RegionPicker/style.d.ts +4 -0
  44. package/build/RegionPicker/style.js +187 -0
  45. package/build/SafeArea/index.js +14 -17
  46. package/build/ScrollView/index.d.ts +23 -11
  47. package/build/ScrollView/index.js +132 -118
  48. package/build/ScrollView/style.d.ts +1 -1
  49. package/build/ScrollView/style.js +33 -22
  50. package/build/Toast/Toast.d.ts +0 -1
  51. package/build/Toast/Toast.js +6 -4
  52. package/build/Toast/style.d.ts +3 -10
  53. package/build/Toast/style.js +41 -45
  54. package/build/index.d.ts +3 -0
  55. package/build/index.js +7 -1
  56. package/build/utils/color.d.ts +5 -0
  57. package/build/utils/color.js +18 -0
  58. package/build/utils/dom.js +4 -3
  59. package/build/utils/theme.d.ts +2 -0
  60. package/build/utils/theme.js +7 -0
  61. package/package.json +1 -1
  62. package/test/src/date-picker/index.jsx +119 -0
  63. package/test/src/index/index.jsx +2 -0
  64. package/test/src/index.jsx +1 -0
  65. package/test/src/loading/index.jsx +2 -2
  66. package/test/src/region-picker/index.jsx +120 -0
  67. package/test/src/scrollview/BasicSection.jsx +56 -0
  68. package/test/src/scrollview/CustomLoadingSection.jsx +53 -0
  69. package/test/src/scrollview/HeightModeSection.jsx +42 -0
  70. package/test/src/scrollview/ImperativeSection.jsx +56 -0
  71. package/test/src/scrollview/NotScrollableSection.jsx +32 -0
  72. package/test/src/scrollview/PerfSection.jsx +34 -0
  73. package/test/src/scrollview/index.css +92 -8
  74. package/test/src/scrollview/index.jsx +13 -45
  75. package/test/src/toast/index.jsx +1 -0
@@ -23,12 +23,13 @@ function createPortalDOM(point) {
23
23
  root.render(component);
24
24
  },
25
25
  unmount() {
26
- // 先从 DOM 移除容器,再卸载 React
27
- // 避免 React 18+ 在已卸载的根上发出警告
26
+ // 先卸载 React 根,再从 DOM 移除容器
27
+ // React 18+ 推荐先 unmount 让 React 完成清理(包括 effect 的 cleanup),
28
+ // 然后再移除真实 DOM 节点;倒过来在严格模式下可能产生警告
29
+ root.unmount();
28
30
  if (container.parentNode) {
29
31
  container.parentNode.removeChild(container);
30
32
  }
31
- root.unmount();
32
33
  },
33
34
  };
34
35
  }
@@ -0,0 +1,2 @@
1
+ export declare const fontStack = "-apple-system, BlinkMacSystemFont, \"Helvetica Neue\", \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif";
2
+ export declare const numberFontStack = "\"SF Pro Text\", -apple-system, BlinkMacSystemFont, \"Helvetica Neue\", \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif";
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.numberFontStack = exports.fontStack = void 0;
4
+ // 设计通用字体栈(与 iOS / 桌面/ 中文常见系统字体兼容)
5
+ exports.fontStack = '-apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';
6
+ // 数字场景优先使用 SF Pro Text(iOS),其它退化到通用字体栈
7
+ exports.numberFontStack = `"SF Pro Text", ${exports.fontStack}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clxx",
3
- "version": "2.1.7",
3
+ "version": "3.0.0",
4
4
  "description": "Basic JS library for mobile devices",
5
5
  "main": "./build/index.js",
6
6
  "module": "./build/index.js",
@@ -0,0 +1,119 @@
1
+ import { showDatePicker } from "@";
2
+ import { useState } from "react";
3
+
4
+ export default function DatePickerDemo() {
5
+ const [result, setResult] = useState("");
6
+
7
+ const open = (precision) => {
8
+ showDatePicker({
9
+ precision,
10
+ title: `选择日期(${precision})`,
11
+ onConfirm: (d) => {
12
+ const fmt =
13
+ precision === "day"
14
+ ? "YYYY-MM-DD"
15
+ : precision === "hour"
16
+ ? "YYYY-MM-DD HH时"
17
+ : precision === "minute"
18
+ ? "YYYY-MM-DD HH:mm"
19
+ : "YYYY-MM-DD HH:mm:ss";
20
+ setResult(d.format(fmt));
21
+ },
22
+ onCancel: () => {
23
+ console.log("取消选择");
24
+ },
25
+ });
26
+ };
27
+
28
+ return (
29
+ <div style={{ padding: "0.3rem" }}>
30
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.2rem" }}>
31
+ <button onClick={() => open("day")}>选择年/月/日</button>
32
+ <button onClick={() => open("hour")}>选择年/月/日 + 时</button>
33
+ <button onClick={() => open("minute")}>选择年/月/日 + 时分</button>
34
+ <button onClick={() => open("second")}>选择年/月/日 + 时分秒</button>
35
+ <button
36
+ onClick={() => {
37
+ showDatePicker({
38
+ precision: "minute",
39
+ value: "2024-06-15 10:30",
40
+ primary: "#16a34a",
41
+ minDate: "2024-01-01",
42
+ maxDate: "2026-12-31 23:59",
43
+ title: "自定义范围 + 绿色主题",
44
+ onConfirm: (d) => {
45
+ setResult(d.format("YYYY-MM-DD HH:mm"));
46
+ },
47
+ });
48
+ }}
49
+ >
50
+ 自定义初值 / 范围 / 主题
51
+ </button>
52
+ <button
53
+ onClick={() => {
54
+ showDatePicker({
55
+ precision: "minute",
56
+ showUnit: false,
57
+ title: "不显示单位",
58
+ onConfirm: (d) => setResult(d.format("YYYY-MM-DD HH:mm")),
59
+ });
60
+ }}
61
+ >
62
+ 不显示单位
63
+ </button>
64
+ <button
65
+ onClick={() => {
66
+ showDatePicker({
67
+ precision: "second",
68
+ units: {
69
+ year: "Y",
70
+ month: "M",
71
+ day: "D",
72
+ hour: "h",
73
+ minute: "m",
74
+ second: "s",
75
+ },
76
+ title: "英文单位",
77
+ onConfirm: (d) => setResult(d.format("YYYY-MM-DD HH:mm:ss")),
78
+ });
79
+ }}
80
+ >
81
+ 自定义单位(英文)
82
+ </button>
83
+ <button
84
+ onClick={() => {
85
+ showDatePicker({
86
+ precision: "day",
87
+ rounded: false,
88
+ title: "无圆角(rounded=false)",
89
+ onConfirm: (d) => setResult(d.format("YYYY-MM-DD")),
90
+ });
91
+ }}
92
+ >
93
+ 无圆角
94
+ </button>
95
+ <button
96
+ onClick={() => {
97
+ showDatePicker({
98
+ precision: "day",
99
+ title: (
100
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "0.08rem" }}>
101
+ <span style={{ color: "#ef4444" }}>★</span>
102
+ <span>ReactNode 标题</span>
103
+ </span>
104
+ ),
105
+ cancelText: <span style={{ color: "#ef4444" }}>✕ 关</span>,
106
+ confirmText: <strong>OK ✓</strong>,
107
+ onConfirm: (d) => setResult(d.format("YYYY-MM-DD")),
108
+ });
109
+ }}
110
+ >
111
+ ReactNode 标题/按钮文案
112
+ </button>
113
+ </div>
114
+ <div style={{ marginTop: "0.4rem", fontSize: "0.3rem" }}>
115
+ 选择结果:<span style={{ color: "#2f7dff" }}>{result || "(未选择)"}</span>
116
+ </div>
117
+ </div>
118
+ );
119
+ }
@@ -17,6 +17,8 @@ const pageConfig = [
17
17
  // { path: 'privacy', title: 'Privacy去标识化', enable: true },
18
18
  { path: "autogrid", title: "AutoGrid生成自动对齐的表格", enable: true },
19
19
  { path: "city-select", title: "CitySelect城市选择器", enable: true },
20
+ { path: "date-picker", title: "DatePicker日期时间选择器", enable: true },
21
+ { path: "region-picker", title: "RegionPicker省市区选择器", enable: true },
20
22
  ];
21
23
 
22
24
  export default function Index() {
@@ -6,6 +6,7 @@ import Home from './index/index';
6
6
  createApp({
7
7
  target: "#root",
8
8
  // maxDocWidth: 10000,
9
+ maxWidth: 750,
9
10
  async render(pathname) {
10
11
  const module = await import(`./${pathname}/index.jsx`);
11
12
  const Page = module.default;
@@ -8,7 +8,7 @@ export default function Index () {
8
8
  <button
9
9
  onClick={async () => {
10
10
  const close = await showLoading();
11
- window.setTimeout(close, 5000);
11
+ window.setTimeout(close, 500000);
12
12
  }}
13
13
  >
14
14
  显示Loading
@@ -17,7 +17,7 @@ export default function Index () {
17
17
  <button
18
18
  onClick={async () => {
19
19
  const close = await showLoading("数据加载中...");
20
- window.setTimeout(close, 5000);
20
+ window.setTimeout(close, 500000);
21
21
  }}
22
22
  >
23
23
  显示Loading
@@ -0,0 +1,120 @@
1
+ import { showRegionPicker } from "@";
2
+ import { useState } from "react";
3
+
4
+ export default function RegionPickerDemo() {
5
+ const [result, setResult] = useState("");
6
+ const [initial, setInitial] = useState(null);
7
+
8
+ const openDefault = () => {
9
+ showRegionPicker({
10
+ value: initial || undefined,
11
+ onConfirm: (sel) => {
12
+ const text = `${sel.province.label} / ${sel.city.label} / ${sel.district.label}`;
13
+ setResult(text);
14
+ setInitial([sel.province.value, sel.city.value, sel.district.value]);
15
+ },
16
+ onCancel: () => {
17
+ console.log("取消选择");
18
+ },
19
+ });
20
+ };
21
+
22
+ const openWithInitial = () => {
23
+ showRegionPicker({
24
+ // 广东 / 广州 / 天河区
25
+ value: ["440000", "440100", "440106"],
26
+ title: "带初始值",
27
+ onConfirm: (sel) => {
28
+ setResult(
29
+ `${sel.province.label} / ${sel.city.label} / ${sel.district.label}`,
30
+ );
31
+ },
32
+ });
33
+ };
34
+
35
+ const openCustomTheme = () => {
36
+ showRegionPicker({
37
+ primary: "#e53935",
38
+ title: "京东红主题",
39
+ onConfirm: (sel) => {
40
+ setResult(
41
+ `${sel.province.label} / ${sel.city.label} / ${sel.district.label}`,
42
+ );
43
+ },
44
+ });
45
+ };
46
+
47
+ const openNoRounded = () => {
48
+ showRegionPicker({
49
+ rounded: false,
50
+ title: "无圆角",
51
+ onConfirm: (sel) => {
52
+ setResult(
53
+ `${sel.province.label} / ${sel.city.label} / ${sel.district.label}`,
54
+ );
55
+ },
56
+ });
57
+ };
58
+
59
+ const openCustomLabels = () => {
60
+ showRegionPicker({
61
+ labels: { province: "选省份", city: "选城市", district: "选区县" },
62
+ cancelText: "放弃",
63
+ confirmText: "选好了",
64
+ title: "自定义文案",
65
+ onConfirm: (sel) => {
66
+ setResult(
67
+ `${sel.province.label} / ${sel.city.label} / ${sel.district.label}`,
68
+ );
69
+ },
70
+ });
71
+ };
72
+
73
+ const openNoMaskClose = () => {
74
+ showRegionPicker({
75
+ maskClosable: false,
76
+ title: "点击遮罩不关闭",
77
+ onConfirm: (sel) => {
78
+ setResult(
79
+ `${sel.province.label} / ${sel.city.label} / ${sel.district.label}`,
80
+ );
81
+ },
82
+ });
83
+ };
84
+
85
+ const openReactNodeTitle = () => {
86
+ showRegionPicker({
87
+ title: (
88
+ <span style={{ display: "inline-flex", alignItems: "center", gap: "0.08rem" }}>
89
+ <span style={{ color: "#f59e0b" }}>★</span>
90
+ <span>ReactNode 标题</span>
91
+ </span>
92
+ ),
93
+ cancelText: <span style={{ color: "#ef4444" }}>✕ 关</span>,
94
+ confirmText: <strong>OK ✓</strong>,
95
+ onConfirm: (sel) => {
96
+ setResult(
97
+ `${sel.province.label} / ${sel.city.label} / ${sel.district.label}`,
98
+ );
99
+ },
100
+ });
101
+ };
102
+
103
+ return (
104
+ <div style={{ padding: "0.3rem" }}>
105
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.2rem" }}>
106
+ <button onClick={openDefault}>默认(保留上次选择)</button>
107
+ <button onClick={openWithInitial}>带初始值(广东/广州/天河)</button>
108
+ <button onClick={openCustomTheme}>自定义主色(京东红)</button>
109
+ <button onClick={openNoRounded}>无圆角</button>
110
+ <button onClick={openCustomLabels}>自定义 tab 占位 / 按钮文案</button>
111
+ <button onClick={openNoMaskClose}>点击遮罩不关闭</button>
112
+ <button onClick={openReactNodeTitle}>ReactNode 标题/按钮</button>
113
+ </div>
114
+ <div style={{ marginTop: "0.4rem", fontSize: "0.3rem" }}>
115
+ 选择结果:
116
+ <span style={{ color: "#2f7dff" }}>{result || "(未选择)"}</span>
117
+ </div>
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,56 @@
1
+ import { useRef, useState } from "react";
2
+ import { ScrollView } from "@";
3
+
4
+ export default function BasicSection() {
5
+ const [list, setList] = useState(() =>
6
+ Array.from({ length: 8 }, (_, i) => i + 1),
7
+ );
8
+ const [hasMore, setHasMore] = useState(true);
9
+ const loadingRef = useRef(false);
10
+ const [stat, setStat] = useState({ scrollTop: 0, dir: "-" });
11
+
12
+ const loadMore = () => {
13
+ if (loadingRef.current || !hasMore) return;
14
+ loadingRef.current = true;
15
+ window.setTimeout(() => {
16
+ setList((prev) => {
17
+ const next = [...prev];
18
+ const start = prev.length;
19
+ for (let i = 1; i <= 8; i++) next.push(start + i);
20
+ if (next.length >= 40) setHasMore(false);
21
+ return next;
22
+ });
23
+ loadingRef.current = false;
24
+ }, 700);
25
+ };
26
+
27
+ return (
28
+ <div className="section">
29
+ <div className="section-title">基础:触底加载更多 + 触顶事件</div>
30
+ <div className="stat">
31
+ scrollTop: {stat.scrollTop.toFixed(0)}px / 方向: {stat.dir} / 共{" "}
32
+ {list.length} 项
33
+ {!hasMore && <span className="badge">已全部加载</span>}
34
+ </div>
35
+ <div className="section-body" style={{ height: "4.4rem" }}>
36
+ <ScrollView
37
+ showLoading={hasMore}
38
+ onScroll={(e) =>
39
+ setStat({ scrollTop: e.scrollTop, dir: e.direction })
40
+ }
41
+ onReachTop={() => console.log("[basic] reach top")}
42
+ onReachBottom={() => {
43
+ console.log("[basic] reach bottom => loadMore");
44
+ loadMore();
45
+ }}
46
+ >
47
+ {list.map((n) => (
48
+ <div className="item" key={n}>
49
+ 第 {n} 项 — 内容占位行
50
+ </div>
51
+ ))}
52
+ </ScrollView>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,53 @@
1
+ import { useState } from "react";
2
+ import { ScrollView } from "@";
3
+
4
+ export default function CustomLoadingSection() {
5
+ const [list, setList] = useState(() =>
6
+ Array.from({ length: 5 }, (_, i) => i + 1),
7
+ );
8
+ const [loading, setLoading] = useState(false);
9
+
10
+ const onReachBottom = () => {
11
+ if (loading) return;
12
+ setLoading(true);
13
+ window.setTimeout(() => {
14
+ setList((prev) => [
15
+ ...prev,
16
+ ...Array.from({ length: 5 }, (_, i) => prev.length + i + 1),
17
+ ]);
18
+ setLoading(false);
19
+ }, 600);
20
+ };
21
+
22
+ return (
23
+ <div className="section">
24
+ <div className="section-title">
25
+ 自定义 loading 内容 + 触底阈值 100px
26
+ </div>
27
+ <div className="section-body" style={{ height: "4rem" }}>
28
+ <ScrollView
29
+ reachBottomThreshold={100}
30
+ onReachBottom={onReachBottom}
31
+ loadingContent={
32
+ <div
33
+ style={{
34
+ padding: "0.2rem",
35
+ textAlign: "center",
36
+ fontSize: "0.24rem",
37
+ color: loading ? "#2f7dff" : "#8e8e93",
38
+ }}
39
+ >
40
+ {loading ? "正在加载…" : "上拉加载更多"}
41
+ </div>
42
+ }
43
+ >
44
+ {list.map((n) => (
45
+ <div className="item" key={n}>
46
+ 卡片 {n}
47
+ </div>
48
+ ))}
49
+ </ScrollView>
50
+ </div>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,42 @@
1
+ import { ScrollView } from "@";
2
+
3
+ export default function HeightModeSection() {
4
+ return (
5
+ <div className="section">
6
+ <div className="section-title">两种高度方式:prop height vs CSS</div>
7
+ <div className="section-body">
8
+ <div style={{ display: "flex", gap: "0.16rem", padding: "0.16rem" }}>
9
+ <div style={{ flex: 1, border: "1px solid #eee" }}>
10
+ <div className="stat">prop height="2.4rem"</div>
11
+ <ScrollView height="2.4rem">
12
+ {Array.from({ length: 12 }, (_, i) => (
13
+ <div className="item" key={i}>
14
+ A{i + 1}
15
+ </div>
16
+ ))}
17
+ </ScrollView>
18
+ </div>
19
+ <div
20
+ style={{
21
+ flex: 1,
22
+ border: "1px solid #eee",
23
+ display: "flex",
24
+ flexDirection: "column",
25
+ }}
26
+ >
27
+ <div className="stat">外层 flex 高 2.4rem</div>
28
+ <div style={{ height: "2.4rem" }}>
29
+ <ScrollView>
30
+ {Array.from({ length: 12 }, (_, i) => (
31
+ <div className="item" key={i}>
32
+ B{i + 1}
33
+ </div>
34
+ ))}
35
+ </ScrollView>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,56 @@
1
+ import { useRef } from "react";
2
+ import { ScrollView } from "@";
3
+
4
+ export default function ImperativeSection() {
5
+ const ref = useRef(null);
6
+ const list = Array.from({ length: 30 }, (_, i) => i + 1);
7
+
8
+ return (
9
+ <div className="section">
10
+ <div className="section-title">命令式 API(forwardRef + handle)</div>
11
+ <div className="section-body" style={{ height: "4rem" }}>
12
+ <ScrollView ref={ref}>
13
+ {list.map((n) => (
14
+ <div
15
+ className={"item" + (n === 18 ? " target" : "")}
16
+ key={n}
17
+ data-index={n}
18
+ >
19
+ Item #{n}
20
+ {n === 18 && " ← 目标项"}
21
+ </div>
22
+ ))}
23
+ </ScrollView>
24
+ </div>
25
+ <div className="toolbar">
26
+ <button onClick={() => ref.current?.scrollToTop("smooth")}>
27
+ ↑ 顶部
28
+ </button>
29
+ <button onClick={() => ref.current?.scrollToBottom("smooth")}>
30
+ ↓ 底部
31
+ </button>
32
+ <button onClick={() => ref.current?.scrollTo({ top: 200 })}>
33
+ 跳到 200px
34
+ </button>
35
+ <button
36
+ onClick={() =>
37
+ ref.current?.scrollToElement('[data-index="18"]', {
38
+ behavior: "smooth",
39
+ offset: -20,
40
+ })
41
+ }
42
+ >
43
+ 滚到目标项
44
+ </button>
45
+ <button
46
+ onClick={() => {
47
+ const el = ref.current?.getElement();
48
+ console.log("getElement →", el);
49
+ }}
50
+ >
51
+ 打印 DOM
52
+ </button>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,32 @@
1
+ import { useState } from "react";
2
+ import { ScrollView } from "@";
3
+
4
+ export default function NotScrollableSection() {
5
+ const [items, setItems] = useState([1, 2]);
6
+ return (
7
+ <div className="section">
8
+ <div className="section-title">
9
+ 内容不足时自动隐藏 loading(点击按钮加内容观察 loading 出现)
10
+ </div>
11
+ <div className="section-body" style={{ height: "3rem" }}>
12
+ <ScrollView showLoading>
13
+ {items.map((n) => (
14
+ <div className="item" key={n}>
15
+ Row {n}
16
+ </div>
17
+ ))}
18
+ </ScrollView>
19
+ </div>
20
+ <div className="toolbar">
21
+ <button
22
+ onClick={() =>
23
+ setItems((prev) => [...prev, prev.length + 1, prev.length + 2])
24
+ }
25
+ >
26
+ + 加 2 行
27
+ </button>
28
+ <button onClick={() => setItems([1, 2])}>重置</button>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,34 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { ScrollView } from "@";
3
+
4
+ export default function PerfSection() {
5
+ const ref = useRef(null);
6
+ const [count, setCount] = useState(0);
7
+ const list = Array.from({ length: 1000 }, (_, i) => i + 1);
8
+
9
+ useEffect(() => {
10
+ console.log("PerfSection mounted, 1000 items");
11
+ }, []);
12
+
13
+ return (
14
+ <div className="section">
15
+ <div className="section-title">大列表性能(1000 项 + RAF 节流)</div>
16
+ <div className="stat">onScroll 触发次数: {count}</div>
17
+ <div className="section-body" style={{ height: "4rem" }}>
18
+ <ScrollView ref={ref} onScroll={() => setCount((c) => c + 1)}>
19
+ {list.map((n) => (
20
+ <div className="item" key={n}>
21
+ 性能行 {n}
22
+ </div>
23
+ ))}
24
+ </ScrollView>
25
+ </div>
26
+ <div className="toolbar">
27
+ <button onClick={() => ref.current?.scrollToTop("smooth")}>
28
+ 回顶部
29
+ </button>
30
+ <button onClick={() => setCount(0)}>重置计数</button>
31
+ </div>
32
+ </div>
33
+ );
34
+ }