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.
- package/AGENTS.md +2 -1
- package/README.md +147 -22
- package/build/Alert/Wrapper.js +12 -14
- package/build/Alert/style.js +44 -25
- package/build/AutoGrid/index.js +21 -15
- package/build/CarouselNotice/index.d.ts +19 -11
- package/build/CarouselNotice/index.js +80 -74
- package/build/CarouselNotice/style.js +14 -4
- package/build/CitySelect/index.js +81 -71
- package/build/CitySelect/style.js +22 -56
- package/build/Clickable/index.js +7 -0
- package/build/Container/index.d.ts +12 -4
- package/build/Container/index.js +94 -89
- package/build/Countdowner/index.js +4 -2
- package/build/DatePicker/Column.d.ts +9 -0
- package/build/DatePicker/Column.js +330 -0
- package/build/DatePicker/index.d.ts +32 -0
- package/build/DatePicker/index.js +230 -0
- package/build/DatePicker/style.d.ts +6 -0
- package/build/DatePicker/style.js +130 -0
- package/build/Dialog/Wrapper.d.ts +0 -1
- package/build/Dialog/Wrapper.js +22 -12
- package/build/Dialog/index.d.ts +7 -1
- package/build/Dialog/index.js +57 -32
- package/build/Dialog/style.js +6 -2
- package/build/Effect/useInterval.js +6 -3
- package/build/Fixed/index.js +13 -22
- package/build/Flex/FlexItem.d.ts +11 -0
- package/build/Flex/FlexItem.js +26 -0
- package/build/Flex/index.d.ts +2 -10
- package/build/Flex/index.js +12 -22
- package/build/Indicator/index.d.ts +9 -6
- package/build/Indicator/index.js +34 -37
- package/build/Indicator/style.d.ts +4 -3
- package/build/Indicator/style.js +8 -13
- package/build/Loading/Wrapper.js +2 -1
- package/build/Loading/style.js +9 -12
- package/build/Overlay/index.js +6 -1
- package/build/RegionPicker/data.d.ts +6 -0
- package/build/RegionPicker/data.js +14486 -0
- package/build/RegionPicker/index.d.ts +33 -0
- package/build/RegionPicker/index.js +205 -0
- package/build/RegionPicker/style.d.ts +4 -0
- package/build/RegionPicker/style.js +187 -0
- package/build/SafeArea/index.js +14 -17
- package/build/ScrollView/index.d.ts +23 -11
- package/build/ScrollView/index.js +132 -118
- package/build/ScrollView/style.d.ts +1 -1
- package/build/ScrollView/style.js +33 -22
- package/build/Toast/Toast.d.ts +0 -1
- package/build/Toast/Toast.js +6 -4
- package/build/Toast/style.d.ts +3 -10
- package/build/Toast/style.js +41 -45
- package/build/index.d.ts +3 -0
- package/build/index.js +7 -1
- package/build/utils/color.d.ts +5 -0
- package/build/utils/color.js +18 -0
- package/build/utils/dom.js +4 -3
- package/build/utils/theme.d.ts +2 -0
- package/build/utils/theme.js +7 -0
- package/package.json +1 -1
- package/test/src/date-picker/index.jsx +119 -0
- package/test/src/index/index.jsx +2 -0
- package/test/src/index.jsx +1 -0
- package/test/src/loading/index.jsx +2 -2
- package/test/src/region-picker/index.jsx +120 -0
- package/test/src/scrollview/BasicSection.jsx +56 -0
- package/test/src/scrollview/CustomLoadingSection.jsx +53 -0
- package/test/src/scrollview/HeightModeSection.jsx +42 -0
- package/test/src/scrollview/ImperativeSection.jsx +56 -0
- package/test/src/scrollview/NotScrollableSection.jsx +32 -0
- package/test/src/scrollview/PerfSection.jsx +34 -0
- package/test/src/scrollview/index.css +92 -8
- package/test/src/scrollview/index.jsx +13 -45
- package/test/src/toast/index.jsx +1 -0
package/build/Container/index.js
CHANGED
|
@@ -1,114 +1,119 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.Container = Container;
|
|
37
4
|
const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
|
|
38
5
|
const react_1 = require("@emotion/react");
|
|
39
|
-
const react_2 =
|
|
6
|
+
const react_2 = require("react");
|
|
40
7
|
const useWindowResize_1 = require("../Effect/useWindowResize");
|
|
41
8
|
const useViewport_1 = require("../Effect/useViewport");
|
|
9
|
+
const isBrowser = typeof window !== "undefined";
|
|
10
|
+
// SSR 时 useLayoutEffect 会 warn,统一降级
|
|
11
|
+
const useIsomorphicLayoutEffect = isBrowser ? react_2.useLayoutEffect : react_2.useEffect;
|
|
12
|
+
// 取当前视口宽度,可选 maxWidth 兑底(0 / Infinity 表示不限制)
|
|
13
|
+
function getViewportWidth(maxWidth) {
|
|
14
|
+
if (!isBrowser)
|
|
15
|
+
return 0;
|
|
16
|
+
const w = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
17
|
+
if (!maxWidth || !isFinite(maxWidth))
|
|
18
|
+
return w;
|
|
19
|
+
return Math.min(w, maxWidth);
|
|
20
|
+
}
|
|
42
21
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
22
|
+
* 自适应容器:所有使用本库的工程都需在根节点放置该组件,
|
|
23
|
+
* 否则各组件中的 rem 单位将无法自动跟随设备宽度缩放。
|
|
24
|
+
*
|
|
25
|
+
* 实现要点:
|
|
26
|
+
* - <Global> 通过 useInsertionEffect 早于 useLayoutEffect 注入样式,
|
|
27
|
+
* 所以首次 layout 阶段所有 rem 已使用正确 fontSize,无需阻塞 children
|
|
28
|
+
* - 浏览器字体缩放(用户系统调大字号)首挂载同步检测一次,scaleFactor 修正
|
|
29
|
+
* - resize 走 rAF 节流,桌面拖拽 / 模拟器切设备不会反复 setState
|
|
30
|
+
* - SSR 安全:所有 window 访问加 isBrowser 守卫
|
|
45
31
|
*/
|
|
46
32
|
function Container(props) {
|
|
47
|
-
const { designWidth = 750, globalStyle, children } = props;
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
}, [designWidth]);
|
|
52
|
-
// 理论基准字体大小(跟随窗口尺寸变化)
|
|
53
|
-
const [rawFontSize, setRawFontSize] = (0, react_2.useState)(() => calculateFontSize(window.innerWidth));
|
|
54
|
-
// 浏览器字体缩放因子(>1 表示用户放大了系统字体,<1 表示缩小)
|
|
55
|
-
// 独立存储,使得 resize 后缩放修正依然生效
|
|
33
|
+
const { designWidth = 750, maxWidth = 750, globalStyle, children } = props;
|
|
34
|
+
// 当前视口宽度
|
|
35
|
+
const [viewportWidth, setViewportWidth] = (0, react_2.useState)(() => getViewportWidth(maxWidth));
|
|
36
|
+
// 浏览器字体缩放因子(>1 用户放大系统字体),首挂载探测一次
|
|
56
37
|
const [scaleFactor, setScaleFactor] = (0, react_2.useState)(1);
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// 因此 useLayoutEffect 内 getComputedStyle 可正确读取已注入的字体大小
|
|
66
|
-
// 检测和修正均在浏览器绘制前同步完成,避免闪烁
|
|
67
|
-
(0, react_2.useLayoutEffect)(() => {
|
|
68
|
-
// 缩放因子在页面生命周期内不变,只需检测一次
|
|
69
|
-
if (isInitialized)
|
|
38
|
+
// 理论 fontSize(未修正)
|
|
39
|
+
const rawFontSize = (0, react_2.useMemo)(() => (viewportWidth * 100) / designWidth, [viewportWidth, designWidth]);
|
|
40
|
+
// 最终 fontSize:缩放修正 + 1 位小数,避免浮点抖动
|
|
41
|
+
const fontSize = (0, react_2.useMemo)(() => Math.round((scaleFactor === 1 ? rawFontSize : rawFontSize / scaleFactor) * 10) / 10, [rawFontSize, scaleFactor]);
|
|
42
|
+
// 浏览器字体缩放检测:仅首挂载执行一次,避免 setState 触发循环
|
|
43
|
+
const detectedRef = (0, react_2.useRef)(false);
|
|
44
|
+
useIsomorphicLayoutEffect(() => {
|
|
45
|
+
if (detectedRef.current || !isBrowser)
|
|
70
46
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// 容差 1px
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
setScaleFactor(computedSize / rawFontSize);
|
|
47
|
+
detectedRef.current = true;
|
|
48
|
+
const computed = parseFloat(window.getComputedStyle(document.documentElement).fontSize);
|
|
49
|
+
// 容差 1px 屏蔽浮点误差
|
|
50
|
+
if (computed > 0 && Math.abs(computed - rawFontSize) > 1) {
|
|
51
|
+
setScaleFactor(computed / rawFontSize);
|
|
77
52
|
}
|
|
78
|
-
|
|
79
|
-
}, [
|
|
80
|
-
//
|
|
81
|
-
|
|
53
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
|
+
}, []);
|
|
55
|
+
// maxWidth 变更时重算 viewportWidth
|
|
56
|
+
(0, react_2.useEffect)(() => {
|
|
57
|
+
setViewportWidth((prev) => {
|
|
58
|
+
const next = getViewportWidth(maxWidth);
|
|
59
|
+
return prev === next ? prev : next;
|
|
60
|
+
});
|
|
61
|
+
}, [maxWidth]);
|
|
62
|
+
// resize:rAF 节流;桌面拖动窗口、DevTools 切换设备模拟时也只每帧一次
|
|
63
|
+
const rafRef = (0, react_2.useRef)(null);
|
|
82
64
|
(0, useWindowResize_1.useWindowResize)(() => {
|
|
83
|
-
|
|
65
|
+
if (!isBrowser || rafRef.current !== null)
|
|
66
|
+
return;
|
|
67
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
68
|
+
rafRef.current = null;
|
|
69
|
+
const next = getViewportWidth(maxWidth);
|
|
70
|
+
setViewportWidth((prev) => (prev === next ? prev : next));
|
|
71
|
+
});
|
|
84
72
|
});
|
|
85
|
-
//
|
|
73
|
+
// 组件卸载清理 pending rAF
|
|
74
|
+
(0, react_2.useEffect)(() => {
|
|
75
|
+
return () => {
|
|
76
|
+
if (rafRef.current !== null) {
|
|
77
|
+
cancelAnimationFrame(rafRef.current);
|
|
78
|
+
rafRef.current = null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}, []);
|
|
82
|
+
// viewport meta
|
|
86
83
|
(0, useViewport_1.useViewport)();
|
|
87
84
|
// 激活 iOS 上的 :active 伪类
|
|
88
85
|
(0, react_2.useEffect)(() => {
|
|
86
|
+
if (!isBrowser)
|
|
87
|
+
return;
|
|
89
88
|
const noop = () => { };
|
|
90
89
|
document.body.addEventListener("touchstart", noop, { passive: true });
|
|
91
90
|
return () => {
|
|
92
91
|
document.body.removeEventListener("touchstart", noop);
|
|
93
92
|
};
|
|
94
93
|
}, []);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
94
|
+
// 全局样式:fontSize 写入 html,rem 自动跟随
|
|
95
|
+
// body 以 maxWidth 居中,保证 PC 端上页面不超过设计宽度,两侧留白
|
|
96
|
+
// CSS 变量 --clxx-max-width 供 Overlay/Fixed 等使用 fixed 定位的组件读取
|
|
97
|
+
// 从而把弹窗 / 遮罩限制在视口内(fixed 默认参考浏览器窗口,无法继承 body 宽度)
|
|
98
|
+
const hasMaxWidth = !!maxWidth && isFinite(maxWidth);
|
|
99
|
+
const globalStyles = (0, react_2.useMemo)(() => [
|
|
100
|
+
{
|
|
101
|
+
":root": {
|
|
102
|
+
"--clxx-max-width": hasMaxWidth ? `${maxWidth}px` : "100%",
|
|
103
|
+
},
|
|
104
|
+
"*": {
|
|
105
|
+
boxSizing: "border-box",
|
|
106
|
+
},
|
|
107
|
+
html: {
|
|
108
|
+
WebkitTapHighlightColor: "transparent",
|
|
109
|
+
WebkitOverflowScrolling: "touch",
|
|
110
|
+
WebkitTextSizeAdjust: "100%",
|
|
111
|
+
fontSize: `${fontSize}px`,
|
|
112
|
+
touchAction: "manipulation",
|
|
113
|
+
},
|
|
114
|
+
body: Object.assign({ fontSize: "16px", margin: "0 auto" }, (hasMaxWidth ? { maxWidth: `${maxWidth}px` } : null)),
|
|
115
|
+
},
|
|
116
|
+
globalStyle,
|
|
117
|
+
], [fontSize, maxWidth, hasMaxWidth, globalStyle]);
|
|
118
|
+
return ((0, jsx_runtime_1.jsxs)(react_2.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_1.Global, { styles: globalStyles }), children] }));
|
|
114
119
|
}
|
|
@@ -50,12 +50,14 @@ const react_1 = __importStar(require("react"));
|
|
|
50
50
|
const Countdown_1 = require("../utils/Countdown");
|
|
51
51
|
const Row_1 = require("../Flex/Row");
|
|
52
52
|
function Countdowner(props) {
|
|
53
|
-
|
|
53
|
+
const { remain = 0, separator = ':', format = 'his', onUpdate, onEnd, separatorStyle, containerStyle, numberStyle, renderNumber, renderSeparator } = props, extra = __rest(props, ["remain", "separator", "format", "onUpdate", "onEnd", "separatorStyle", "containerStyle", "numberStyle", "renderNumber", "renderSeparator"]);
|
|
54
54
|
const [value, setValue] = (0, react_1.useState)(null);
|
|
55
55
|
// 使用 ref 保存最新的回调,避免频繁重建倒计时实例
|
|
56
56
|
const callbacksRef = react_1.default.useRef({ onUpdate, onEnd });
|
|
57
57
|
callbacksRef.current = { onUpdate, onEnd };
|
|
58
|
-
|
|
58
|
+
// content 每秒因 value 变化必然重建;这里没有 emotion 序列化负担:
|
|
59
|
+
// numberStyle / separatorStyle 仅做引用透传,emotion 对相同引用有 cache
|
|
60
|
+
const content = [];
|
|
59
61
|
if (value && typeof value === 'object') {
|
|
60
62
|
for (let i = 0; i < format.length; i++) {
|
|
61
63
|
// 渲染数字进组件
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DatePickerStyle } from "./style";
|
|
2
|
+
export interface ColumnProps {
|
|
3
|
+
items: number[];
|
|
4
|
+
value: number;
|
|
5
|
+
onChange: (v: number) => void;
|
|
6
|
+
format?: (n: number) => string;
|
|
7
|
+
style: DatePickerStyle;
|
|
8
|
+
}
|
|
9
|
+
export declare function Column(props: ColumnProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Column = Column;
|
|
4
|
+
const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
// === 物理参数(统一阻尼-弹簧模型)===
|
|
7
|
+
// 释放后整段运动是「单一连续 ODE」:摩擦 + 弹簧(朝预测落点),由 rAF 步进。
|
|
8
|
+
// 既无双阶段切换,也不存在「等惯性停 → 启动吸附」的间隙。
|
|
9
|
+
// 单位:位置 px、时间 ms、速度 px/ms、加速度 px/ms²
|
|
10
|
+
// 摩擦系数:用于「预测最终落点」。指数衰减下 v(t)=v0·e^(-kt),落点 = offset0 + v0/k
|
|
11
|
+
const FRICTION_K = 0.005;
|
|
12
|
+
// 弹簧刚度:朝目标项的回正力度。ω = sqrt(STIFF) ≈ 0.0134 rad/ms ≈ 470ms 周期
|
|
13
|
+
const STIFF = 0.00018;
|
|
14
|
+
// 阻尼比 1.0 = 临界阻尼(无超调,最快稳态)
|
|
15
|
+
const DAMP_RATIO = 1.0;
|
|
16
|
+
const DAMPING = 2 * Math.sqrt(STIFF) * DAMP_RATIO;
|
|
17
|
+
// 边界橡皮筋:手指拖到边界外,offset 实际变化按此比例(iOS 风格)
|
|
18
|
+
const RUBBER = 0.45;
|
|
19
|
+
// 越界回弹:边界吸引到内边的强弹簧
|
|
20
|
+
const EDGE_STIFF = 0.0006;
|
|
21
|
+
const EDGE_DAMP = 2 * Math.sqrt(EDGE_STIFF);
|
|
22
|
+
// 终止阈值
|
|
23
|
+
const EPS_V = 0.02; // px/ms
|
|
24
|
+
const EPS_X = 0.5; // px
|
|
25
|
+
function Column(props) {
|
|
26
|
+
const { items, value, onChange, format, style } = props;
|
|
27
|
+
const containerRef = (0, react_1.useRef)(null);
|
|
28
|
+
const innerRef = (0, react_1.useRef)(null);
|
|
29
|
+
const [itemHeight, setItemHeight] = (0, react_1.useState)(0);
|
|
30
|
+
const [activeIndex, setActiveIndex] = (0, react_1.useState)(() => Math.max(0, items.indexOf(value)));
|
|
31
|
+
// refs 让 rAF 闭包始终读到最新值
|
|
32
|
+
const offsetRef = (0, react_1.useRef)(0);
|
|
33
|
+
const velocityRef = (0, react_1.useRef)(0);
|
|
34
|
+
const targetRef = (0, react_1.useRef)(0);
|
|
35
|
+
const animRef = (0, react_1.useRef)(0);
|
|
36
|
+
const lastFrameRef = (0, react_1.useRef)(0);
|
|
37
|
+
// 缓存上一次 activeIndex,避免每帧 setState 调度开销
|
|
38
|
+
const lastActiveRef = (0, react_1.useRef)(-1);
|
|
39
|
+
const valueRef = (0, react_1.useRef)(value);
|
|
40
|
+
valueRef.current = value;
|
|
41
|
+
const itemsRef = (0, react_1.useRef)(items);
|
|
42
|
+
itemsRef.current = items;
|
|
43
|
+
const onChangeRef = (0, react_1.useRef)(onChange);
|
|
44
|
+
onChangeRef.current = onChange;
|
|
45
|
+
const itemHeightRef = (0, react_1.useRef)(itemHeight);
|
|
46
|
+
itemHeightRef.current = itemHeight;
|
|
47
|
+
// === 测量 itemHeight(随 rem 变化)===
|
|
48
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
49
|
+
const el = innerRef.current;
|
|
50
|
+
if (!el)
|
|
51
|
+
return;
|
|
52
|
+
const measure = () => {
|
|
53
|
+
const first = el.querySelector("[data-pick-item]");
|
|
54
|
+
if (first) {
|
|
55
|
+
const h = first.getBoundingClientRect().height;
|
|
56
|
+
if (h > 0)
|
|
57
|
+
setItemHeight(h);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
measure();
|
|
61
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
62
|
+
const ro = new ResizeObserver(measure);
|
|
63
|
+
ro.observe(el);
|
|
64
|
+
return () => ro.disconnect();
|
|
65
|
+
}
|
|
66
|
+
}, []);
|
|
67
|
+
// === 渲染辅助 ===
|
|
68
|
+
const applyTransform = (offset) => {
|
|
69
|
+
const inner = innerRef.current;
|
|
70
|
+
if (inner)
|
|
71
|
+
inner.style.transform = `translate3d(0,${-offset}px,0)`;
|
|
72
|
+
};
|
|
73
|
+
const updateActive = (offset) => {
|
|
74
|
+
const ih = itemHeightRef.current;
|
|
75
|
+
if (ih === 0)
|
|
76
|
+
return;
|
|
77
|
+
const list = itemsRef.current;
|
|
78
|
+
const idx = Math.max(0, Math.min(list.length - 1, Math.round(offset / ih)));
|
|
79
|
+
if (lastActiveRef.current === idx)
|
|
80
|
+
return;
|
|
81
|
+
lastActiveRef.current = idx;
|
|
82
|
+
setActiveIndex(idx);
|
|
83
|
+
};
|
|
84
|
+
const setOffsetImmediate = (offset) => {
|
|
85
|
+
offsetRef.current = offset;
|
|
86
|
+
applyTransform(offset);
|
|
87
|
+
updateActive(offset);
|
|
88
|
+
};
|
|
89
|
+
const stopAnimation = () => {
|
|
90
|
+
if (animRef.current) {
|
|
91
|
+
cancelAnimationFrame(animRef.current);
|
|
92
|
+
animRef.current = 0;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
// === 物理动画循环 ===
|
|
96
|
+
const startAnimation = () => {
|
|
97
|
+
stopAnimation();
|
|
98
|
+
lastFrameRef.current = performance.now();
|
|
99
|
+
const step = (now) => {
|
|
100
|
+
// dt 上限 32ms:防止页面切回时大跳,导致一帧位移过大
|
|
101
|
+
const dt = Math.min(32, now - lastFrameRef.current);
|
|
102
|
+
lastFrameRef.current = now;
|
|
103
|
+
const ih = itemHeightRef.current;
|
|
104
|
+
const list = itemsRef.current;
|
|
105
|
+
if (ih === 0 || list.length === 0) {
|
|
106
|
+
animRef.current = 0;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const maxOffset = (list.length - 1) * ih;
|
|
110
|
+
let offset = offsetRef.current;
|
|
111
|
+
let velocity = velocityRef.current;
|
|
112
|
+
const target = targetRef.current;
|
|
113
|
+
// 越界判断:使用边界弹簧把 offset 拉回 [0, maxOffset]
|
|
114
|
+
let outOfBound = 0;
|
|
115
|
+
if (offset < 0)
|
|
116
|
+
outOfBound = offset;
|
|
117
|
+
else if (offset > maxOffset)
|
|
118
|
+
outOfBound = offset - maxOffset;
|
|
119
|
+
let acc;
|
|
120
|
+
if (outOfBound !== 0) {
|
|
121
|
+
// 越界期间用更强的边界弹簧;忽略原 target,先回到合法范围
|
|
122
|
+
acc = -EDGE_STIFF * outOfBound - EDGE_DAMP * velocity;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
acc = -STIFF * (offset - target) - DAMPING * velocity;
|
|
126
|
+
}
|
|
127
|
+
velocity += acc * dt;
|
|
128
|
+
offset += velocity * dt;
|
|
129
|
+
offsetRef.current = offset;
|
|
130
|
+
velocityRef.current = velocity;
|
|
131
|
+
applyTransform(offset);
|
|
132
|
+
updateActive(offset);
|
|
133
|
+
const settled = Math.abs(velocity) < EPS_V &&
|
|
134
|
+
Math.abs(offset - target) < EPS_X &&
|
|
135
|
+
outOfBound === 0;
|
|
136
|
+
if (settled) {
|
|
137
|
+
// 精确对齐到 target(target 已是 itemH 整数倍)
|
|
138
|
+
offsetRef.current = target;
|
|
139
|
+
velocityRef.current = 0;
|
|
140
|
+
applyTransform(target);
|
|
141
|
+
updateActive(target);
|
|
142
|
+
const idx = Math.round(target / ih);
|
|
143
|
+
const v = list[idx];
|
|
144
|
+
if (v !== valueRef.current)
|
|
145
|
+
onChangeRef.current(v);
|
|
146
|
+
animRef.current = 0;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
animRef.current = requestAnimationFrame(step);
|
|
150
|
+
};
|
|
151
|
+
animRef.current = requestAnimationFrame(step);
|
|
152
|
+
};
|
|
153
|
+
// 释放:用速度预测落点 → 吸附到最近 item → 启动物理动画
|
|
154
|
+
const releaseWithVelocity = (v) => {
|
|
155
|
+
const ih = itemHeightRef.current;
|
|
156
|
+
const list = itemsRef.current;
|
|
157
|
+
if (ih === 0 || list.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
const maxOffset = (list.length - 1) * ih;
|
|
160
|
+
// 摩擦预测:落点 = 当前 + v / k(指数衰减积分)
|
|
161
|
+
const projected = offsetRef.current + v / FRICTION_K;
|
|
162
|
+
const clamped = Math.max(0, Math.min(maxOffset, projected));
|
|
163
|
+
const idx = Math.max(0, Math.min(list.length - 1, Math.round(clamped / ih)));
|
|
164
|
+
targetRef.current = idx * ih;
|
|
165
|
+
velocityRef.current = v;
|
|
166
|
+
startAnimation();
|
|
167
|
+
};
|
|
168
|
+
// === 手势处理 ===
|
|
169
|
+
(0, react_1.useEffect)(() => {
|
|
170
|
+
const el = containerRef.current;
|
|
171
|
+
if (!el || itemHeight === 0)
|
|
172
|
+
return;
|
|
173
|
+
let dragging = false;
|
|
174
|
+
let lastY = 0;
|
|
175
|
+
let lastTime = 0;
|
|
176
|
+
// 手势起点:用「起点 offset + 累计位移」重算每帧位置,
|
|
177
|
+
// 避免越界橡皮筋反馈振荡(上一帧衰减后的 offset 不能再作为下一帧起点)
|
|
178
|
+
let startOffset = 0;
|
|
179
|
+
let totalDelta = 0;
|
|
180
|
+
// 速度采样:保留最近若干帧用于释放速度估算
|
|
181
|
+
let samples = [];
|
|
182
|
+
const onDown = (clientY) => {
|
|
183
|
+
stopAnimation();
|
|
184
|
+
dragging = true;
|
|
185
|
+
lastY = clientY;
|
|
186
|
+
lastTime = performance.now();
|
|
187
|
+
startOffset = offsetRef.current;
|
|
188
|
+
totalDelta = 0;
|
|
189
|
+
samples = [];
|
|
190
|
+
velocityRef.current = 0;
|
|
191
|
+
};
|
|
192
|
+
const onMove = (clientY) => {
|
|
193
|
+
if (!dragging)
|
|
194
|
+
return;
|
|
195
|
+
const ih = itemHeightRef.current;
|
|
196
|
+
const list = itemsRef.current;
|
|
197
|
+
if (ih === 0)
|
|
198
|
+
return;
|
|
199
|
+
const maxOffset = (list.length - 1) * ih;
|
|
200
|
+
const now = performance.now();
|
|
201
|
+
// 屏幕向下拖(dy>0)== 列表向下平移 == offset 减少
|
|
202
|
+
const dy = clientY - lastY;
|
|
203
|
+
const delta = -dy;
|
|
204
|
+
totalDelta += delta;
|
|
205
|
+
// 「原始位置」不受衰减影响:始终 = 起点 + 累计位移
|
|
206
|
+
const rawPos = startOffset + totalDelta;
|
|
207
|
+
// 只对越界部分衰减,避免衰减后的值反馈到下一帧产生振荡
|
|
208
|
+
let next;
|
|
209
|
+
if (rawPos < 0) {
|
|
210
|
+
next = rawPos * RUBBER;
|
|
211
|
+
}
|
|
212
|
+
else if (rawPos > maxOffset) {
|
|
213
|
+
next = maxOffset + (rawPos - maxOffset) * RUBBER;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
next = rawPos;
|
|
217
|
+
}
|
|
218
|
+
offsetRef.current = next;
|
|
219
|
+
applyTransform(next);
|
|
220
|
+
updateActive(next);
|
|
221
|
+
const dt = Math.max(1, now - lastTime);
|
|
222
|
+
samples.push({ dy: delta, dt, t: now });
|
|
223
|
+
// 只保留最近 100ms 的样本
|
|
224
|
+
const cutoff = now - 100;
|
|
225
|
+
while (samples.length > 0 && samples[0].t < cutoff)
|
|
226
|
+
samples.shift();
|
|
227
|
+
lastY = clientY;
|
|
228
|
+
lastTime = now;
|
|
229
|
+
};
|
|
230
|
+
const onUp = () => {
|
|
231
|
+
if (!dragging)
|
|
232
|
+
return;
|
|
233
|
+
dragging = false;
|
|
234
|
+
// 释放速度:最近 100ms 样本的总位移 / 总时间(px/ms)
|
|
235
|
+
// 等价于近 100ms 的平均速度,比单帧速度更稳,能滤掉手指最后一瞬的抖动
|
|
236
|
+
let totalDy = 0;
|
|
237
|
+
let totalDt = 0;
|
|
238
|
+
for (const s of samples) {
|
|
239
|
+
totalDy += s.dy;
|
|
240
|
+
totalDt += s.dt;
|
|
241
|
+
}
|
|
242
|
+
const v = totalDt > 0 ? totalDy / totalDt : 0;
|
|
243
|
+
releaseWithVelocity(v);
|
|
244
|
+
};
|
|
245
|
+
// touch
|
|
246
|
+
const onTouchStart = (e) => {
|
|
247
|
+
if (e.touches.length !== 1)
|
|
248
|
+
return;
|
|
249
|
+
onDown(e.touches[0].clientY);
|
|
250
|
+
};
|
|
251
|
+
const onTouchMove = (e) => {
|
|
252
|
+
if (e.touches.length !== 1)
|
|
253
|
+
return;
|
|
254
|
+
// 阻止页面滚动(touch-action: none 已防大部分,再保险)
|
|
255
|
+
if (e.cancelable)
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
onMove(e.touches[0].clientY);
|
|
258
|
+
};
|
|
259
|
+
const onTouchEnd = () => onUp();
|
|
260
|
+
// mouse(document 级监听 move/up,避免拖出元素丢事件)
|
|
261
|
+
const onMouseDown = (e) => {
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
onDown(e.clientY);
|
|
264
|
+
};
|
|
265
|
+
const onMouseMove = (e) => onMove(e.clientY);
|
|
266
|
+
const onMouseUp = () => onUp();
|
|
267
|
+
// wheel:直接累加到 offset,停止后启动 0 速度回弹(=纯弹簧吸附最近项)
|
|
268
|
+
let wheelTimer = 0;
|
|
269
|
+
const onWheel = (e) => {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
stopAnimation();
|
|
272
|
+
const ih = itemHeightRef.current;
|
|
273
|
+
const list = itemsRef.current;
|
|
274
|
+
if (ih === 0)
|
|
275
|
+
return;
|
|
276
|
+
const maxOffset = (list.length - 1) * ih;
|
|
277
|
+
let next = offsetRef.current + e.deltaY;
|
|
278
|
+
// 滚轮不做橡皮筋(多余),直接夹到合法范围
|
|
279
|
+
next = Math.max(0, Math.min(maxOffset, next));
|
|
280
|
+
offsetRef.current = next;
|
|
281
|
+
velocityRef.current = 0;
|
|
282
|
+
applyTransform(next);
|
|
283
|
+
updateActive(next);
|
|
284
|
+
if (wheelTimer)
|
|
285
|
+
clearTimeout(wheelTimer);
|
|
286
|
+
wheelTimer = setTimeout(() => {
|
|
287
|
+
wheelTimer = 0;
|
|
288
|
+
releaseWithVelocity(0);
|
|
289
|
+
}, 80);
|
|
290
|
+
};
|
|
291
|
+
el.addEventListener("touchstart", onTouchStart, { passive: false });
|
|
292
|
+
el.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
293
|
+
el.addEventListener("touchend", onTouchEnd);
|
|
294
|
+
el.addEventListener("touchcancel", onTouchEnd);
|
|
295
|
+
el.addEventListener("mousedown", onMouseDown);
|
|
296
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
297
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
298
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
299
|
+
return () => {
|
|
300
|
+
el.removeEventListener("touchstart", onTouchStart);
|
|
301
|
+
el.removeEventListener("touchmove", onTouchMove);
|
|
302
|
+
el.removeEventListener("touchend", onTouchEnd);
|
|
303
|
+
el.removeEventListener("touchcancel", onTouchEnd);
|
|
304
|
+
el.removeEventListener("mousedown", onMouseDown);
|
|
305
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
306
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
307
|
+
el.removeEventListener("wheel", onWheel);
|
|
308
|
+
if (wheelTimer)
|
|
309
|
+
clearTimeout(wheelTimer);
|
|
310
|
+
stopAnimation();
|
|
311
|
+
};
|
|
312
|
+
}, [itemHeight]);
|
|
313
|
+
// === 同步外部 value(含首次 itemHeight 测量) ===
|
|
314
|
+
// 用 useLayoutEffect 避免首帧闪烁(在浏览器绘制前同步 transform)
|
|
315
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
316
|
+
const ih = itemHeightRef.current;
|
|
317
|
+
if (ih === 0)
|
|
318
|
+
return;
|
|
319
|
+
const idx = items.indexOf(value);
|
|
320
|
+
if (idx < 0)
|
|
321
|
+
return;
|
|
322
|
+
const target = idx * ih;
|
|
323
|
+
if (Math.abs(offsetRef.current - target) > 0.5) {
|
|
324
|
+
stopAnimation();
|
|
325
|
+
velocityRef.current = 0;
|
|
326
|
+
setOffsetImmediate(target);
|
|
327
|
+
}
|
|
328
|
+
}, [value, items, itemHeight]);
|
|
329
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, css: style.column, children: (0, jsx_runtime_1.jsxs)("div", { ref: innerRef, css: style.columnInner, children: [(0, jsx_runtime_1.jsx)("div", { css: style.spacer }), items.map((v, i) => ((0, jsx_runtime_1.jsx)("div", { "data-pick-item": true, css: [style.item, i === activeIndex && style.itemActive], children: format ? format(v) : v }, v))), (0, jsx_runtime_1.jsx)("div", { css: style.spacer })] }) }));
|
|
330
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { Dayjs } from "dayjs";
|
|
3
|
+
export type DatePickerPrecision = "day" | "hour" | "minute" | "second";
|
|
4
|
+
export type DateInput = Date | string | number | Dayjs;
|
|
5
|
+
export interface DatePickerUnits {
|
|
6
|
+
year?: string;
|
|
7
|
+
month?: string;
|
|
8
|
+
day?: string;
|
|
9
|
+
hour?: string;
|
|
10
|
+
minute?: string;
|
|
11
|
+
second?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_UNITS: Required<DatePickerUnits>;
|
|
14
|
+
export interface DatePickerProps {
|
|
15
|
+
value?: DateInput;
|
|
16
|
+
precision?: DatePickerPrecision;
|
|
17
|
+
title?: ReactNode;
|
|
18
|
+
cancelText?: ReactNode;
|
|
19
|
+
confirmText?: ReactNode;
|
|
20
|
+
maskClosable?: boolean;
|
|
21
|
+
primary?: string;
|
|
22
|
+
rounded?: boolean;
|
|
23
|
+
showUnit?: boolean;
|
|
24
|
+
units?: DatePickerUnits;
|
|
25
|
+
minDate?: DateInput;
|
|
26
|
+
maxDate?: DateInput;
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
onCancel?: () => void;
|
|
29
|
+
onConfirm?: (date: Dayjs) => void;
|
|
30
|
+
}
|
|
31
|
+
export declare function DatePicker(props: DatePickerProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
32
|
+
export declare function showDatePicker(options?: Pick<DatePickerProps, "value" | "precision" | "title" | "cancelText" | "confirmText" | "maskClosable" | "primary" | "rounded" | "showUnit" | "units" | "minDate" | "maxDate" | "onCancel" | "onConfirm">): void;
|