clxx 2.1.8 → 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 +4 -3
- package/build/Alert/style.js +11 -7
- 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 +30 -55
- 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 -7
- package/build/Toast/style.js +33 -41
- 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
|
@@ -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;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.DEFAULT_UNITS = void 0;
|
|
18
|
+
exports.DatePicker = DatePicker;
|
|
19
|
+
exports.showDatePicker = showDatePicker;
|
|
20
|
+
const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
|
|
21
|
+
const react_1 = require("react");
|
|
22
|
+
const dayjs_1 = __importDefault(require("dayjs"));
|
|
23
|
+
const Dialog_1 = require("../Dialog");
|
|
24
|
+
const Clickable_1 = require("../Clickable");
|
|
25
|
+
const style_1 = require("./style");
|
|
26
|
+
const Column_1 = require("./Column");
|
|
27
|
+
exports.DEFAULT_UNITS = {
|
|
28
|
+
year: "年",
|
|
29
|
+
month: "月",
|
|
30
|
+
day: "日",
|
|
31
|
+
hour: "时",
|
|
32
|
+
minute: "分",
|
|
33
|
+
second: "秒",
|
|
34
|
+
};
|
|
35
|
+
// ----------------- 主组件 -----------------
|
|
36
|
+
function toDayjs(v) {
|
|
37
|
+
if (v === undefined)
|
|
38
|
+
return (0, dayjs_1.default)();
|
|
39
|
+
// dayjs 可以接收 Date/string/number/Dayjs
|
|
40
|
+
const d = (0, dayjs_1.default)(v);
|
|
41
|
+
return d.isValid() ? d : (0, dayjs_1.default)();
|
|
42
|
+
}
|
|
43
|
+
function range(start, end) {
|
|
44
|
+
const arr = [];
|
|
45
|
+
for (let i = start; i <= end; i++)
|
|
46
|
+
arr.push(i);
|
|
47
|
+
return arr;
|
|
48
|
+
}
|
|
49
|
+
const pad2 = (n) => (n < 10 ? "0" + n : "" + n);
|
|
50
|
+
function DatePicker(props) {
|
|
51
|
+
const { value, precision = "day", title = "请选择", cancelText = "取消", confirmText = "确定", primary = style_1.DEFAULT_PRIMARY, rounded = true, showUnit = true, units, minDate, maxDate, onClose, onCancel, onConfirm, } = props;
|
|
52
|
+
const style = (0, react_1.useMemo)(() => (0, style_1.createStyle)(primary, rounded), [primary, rounded]);
|
|
53
|
+
// 合并单位配置:用户部分覆盖 + 默认中文
|
|
54
|
+
const u = (0, react_1.useMemo)(() => (Object.assign(Object.assign({}, exports.DEFAULT_UNITS), units)), [units]);
|
|
55
|
+
// showUnit=false 时不追加单位后缀
|
|
56
|
+
const suffix = (key) => (showUnit ? u[key] : "");
|
|
57
|
+
const minD = (0, react_1.useMemo)(() => (minDate !== undefined ? toDayjs(minDate) : (0, dayjs_1.default)("1900-01-01")), [minDate]);
|
|
58
|
+
const maxD = (0, react_1.useMemo)(() => maxDate !== undefined ? toDayjs(maxDate) : (0, dayjs_1.default)("2100-12-31 23:59:59"), [maxDate]);
|
|
59
|
+
// 初始值限制在 [minD, maxD] 范围内
|
|
60
|
+
const initial = (0, react_1.useMemo)(() => {
|
|
61
|
+
let d = toDayjs(value);
|
|
62
|
+
if (d.isBefore(minD))
|
|
63
|
+
d = minD;
|
|
64
|
+
if (d.isAfter(maxD))
|
|
65
|
+
d = maxD;
|
|
66
|
+
return d;
|
|
67
|
+
}, [value, minD, maxD]);
|
|
68
|
+
const [year, setYear] = (0, react_1.useState)(initial.year());
|
|
69
|
+
const [month, setMonth] = (0, react_1.useState)(initial.month() + 1); // 1-12
|
|
70
|
+
const [day, setDay] = (0, react_1.useState)(initial.date());
|
|
71
|
+
const [hour, setHour] = (0, react_1.useState)(initial.hour());
|
|
72
|
+
const [minute, setMinute] = (0, react_1.useState)(initial.minute());
|
|
73
|
+
const [second, setSecond] = (0, react_1.useState)(initial.second());
|
|
74
|
+
const showHour = precision !== "day";
|
|
75
|
+
const showMinute = precision === "minute" || precision === "second";
|
|
76
|
+
const showSecond = precision === "second";
|
|
77
|
+
// 各列范围计算(考虑 min/max)
|
|
78
|
+
const years = (0, react_1.useMemo)(() => range(minD.year(), maxD.year()), [minD, maxD]);
|
|
79
|
+
const months = (0, react_1.useMemo)(() => {
|
|
80
|
+
let s = 1;
|
|
81
|
+
let e = 12;
|
|
82
|
+
if (year === minD.year())
|
|
83
|
+
s = minD.month() + 1;
|
|
84
|
+
if (year === maxD.year())
|
|
85
|
+
e = maxD.month() + 1;
|
|
86
|
+
if (s > e) {
|
|
87
|
+
// 不应发生;兜底返回单月
|
|
88
|
+
return [s];
|
|
89
|
+
}
|
|
90
|
+
return range(s, e);
|
|
91
|
+
}, [year, minD, maxD]);
|
|
92
|
+
const days = (0, react_1.useMemo)(() => {
|
|
93
|
+
const daysInMonth = (0, dayjs_1.default)(`${year}-${pad2(month)}-01`).daysInMonth();
|
|
94
|
+
let s = 1;
|
|
95
|
+
let e = daysInMonth;
|
|
96
|
+
if (year === minD.year() && month === minD.month() + 1)
|
|
97
|
+
s = minD.date();
|
|
98
|
+
if (year === maxD.year() && month === maxD.month() + 1)
|
|
99
|
+
e = maxD.date();
|
|
100
|
+
if (s > e)
|
|
101
|
+
return [s];
|
|
102
|
+
return range(s, e);
|
|
103
|
+
}, [year, month, minD, maxD]);
|
|
104
|
+
const hours = (0, react_1.useMemo)(() => {
|
|
105
|
+
let s = 0;
|
|
106
|
+
let e = 23;
|
|
107
|
+
if (year === minD.year() &&
|
|
108
|
+
month === minD.month() + 1 &&
|
|
109
|
+
day === minD.date())
|
|
110
|
+
s = minD.hour();
|
|
111
|
+
if (year === maxD.year() &&
|
|
112
|
+
month === maxD.month() + 1 &&
|
|
113
|
+
day === maxD.date())
|
|
114
|
+
e = maxD.hour();
|
|
115
|
+
if (s > e)
|
|
116
|
+
return [s];
|
|
117
|
+
return range(s, e);
|
|
118
|
+
}, [year, month, day, minD, maxD]);
|
|
119
|
+
const minutes = (0, react_1.useMemo)(() => {
|
|
120
|
+
let s = 0;
|
|
121
|
+
let e = 59;
|
|
122
|
+
if (year === minD.year() &&
|
|
123
|
+
month === minD.month() + 1 &&
|
|
124
|
+
day === minD.date() &&
|
|
125
|
+
hour === minD.hour())
|
|
126
|
+
s = minD.minute();
|
|
127
|
+
if (year === maxD.year() &&
|
|
128
|
+
month === maxD.month() + 1 &&
|
|
129
|
+
day === maxD.date() &&
|
|
130
|
+
hour === maxD.hour())
|
|
131
|
+
e = maxD.minute();
|
|
132
|
+
if (s > e)
|
|
133
|
+
return [s];
|
|
134
|
+
return range(s, e);
|
|
135
|
+
}, [year, month, day, hour, minD, maxD]);
|
|
136
|
+
const seconds = (0, react_1.useMemo)(() => {
|
|
137
|
+
let s = 0;
|
|
138
|
+
let e = 59;
|
|
139
|
+
if (year === minD.year() &&
|
|
140
|
+
month === minD.month() + 1 &&
|
|
141
|
+
day === minD.date() &&
|
|
142
|
+
hour === minD.hour() &&
|
|
143
|
+
minute === minD.minute())
|
|
144
|
+
s = minD.second();
|
|
145
|
+
if (year === maxD.year() &&
|
|
146
|
+
month === maxD.month() + 1 &&
|
|
147
|
+
day === maxD.date() &&
|
|
148
|
+
hour === maxD.hour() &&
|
|
149
|
+
minute === maxD.minute())
|
|
150
|
+
e = maxD.second();
|
|
151
|
+
if (s > e)
|
|
152
|
+
return [s];
|
|
153
|
+
return range(s, e);
|
|
154
|
+
}, [year, month, day, hour, minute, minD, maxD]);
|
|
155
|
+
// 联动夹取:超出范围 → 夹到最近边界(小于 min 取首;大于 max 取尾)
|
|
156
|
+
const clampToList = (v, list) => {
|
|
157
|
+
if (list.includes(v))
|
|
158
|
+
return v;
|
|
159
|
+
if (v < list[0])
|
|
160
|
+
return list[0];
|
|
161
|
+
return list[list.length - 1];
|
|
162
|
+
};
|
|
163
|
+
(0, react_1.useEffect)(() => {
|
|
164
|
+
const next = clampToList(month, months);
|
|
165
|
+
if (next !== month)
|
|
166
|
+
setMonth(next);
|
|
167
|
+
}, [months, month]);
|
|
168
|
+
(0, react_1.useEffect)(() => {
|
|
169
|
+
const next = clampToList(day, days);
|
|
170
|
+
if (next !== day)
|
|
171
|
+
setDay(next);
|
|
172
|
+
}, [days, day]);
|
|
173
|
+
(0, react_1.useEffect)(() => {
|
|
174
|
+
if (!showHour)
|
|
175
|
+
return;
|
|
176
|
+
const next = clampToList(hour, hours);
|
|
177
|
+
if (next !== hour)
|
|
178
|
+
setHour(next);
|
|
179
|
+
}, [hours, hour, showHour]);
|
|
180
|
+
(0, react_1.useEffect)(() => {
|
|
181
|
+
if (!showMinute)
|
|
182
|
+
return;
|
|
183
|
+
const next = clampToList(minute, minutes);
|
|
184
|
+
if (next !== minute)
|
|
185
|
+
setMinute(next);
|
|
186
|
+
}, [minutes, minute, showMinute]);
|
|
187
|
+
(0, react_1.useEffect)(() => {
|
|
188
|
+
if (!showSecond)
|
|
189
|
+
return;
|
|
190
|
+
const next = clampToList(second, seconds);
|
|
191
|
+
if (next !== second)
|
|
192
|
+
setSecond(next);
|
|
193
|
+
}, [seconds, second, showSecond]);
|
|
194
|
+
// 关闭:动画与卸载交由 Dialog 处理
|
|
195
|
+
const handleCancel = () => {
|
|
196
|
+
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
197
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
198
|
+
};
|
|
199
|
+
const handleConfirm = () => {
|
|
200
|
+
const hh = showHour ? hour : 0;
|
|
201
|
+
const mm = showMinute ? minute : 0;
|
|
202
|
+
const ss = showSecond ? second : 0;
|
|
203
|
+
// 用 ISO 字符串构造,避免链式 set 时的月份溢出问题
|
|
204
|
+
const d = (0, dayjs_1.default)(`${year}-${pad2(month)}-${pad2(day)}T${pad2(hh)}:${pad2(mm)}:${pad2(ss)}`);
|
|
205
|
+
onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm(d);
|
|
206
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
207
|
+
};
|
|
208
|
+
return ((0, jsx_runtime_1.jsxs)("div", { css: style.sheet, children: [(0, jsx_runtime_1.jsxs)("div", { css: style.header, children: [(0, jsx_runtime_1.jsx)(Clickable_1.Clickable, { css: [style.btn, style.btnCancel], onClick: handleCancel, children: cancelText }), (0, jsx_runtime_1.jsx)("div", { css: style.title, children: title }), (0, jsx_runtime_1.jsx)(Clickable_1.Clickable, { css: [style.btn, style.btnConfirm], onClick: handleConfirm, children: confirmText })] }), (0, jsx_runtime_1.jsxs)("div", { css: style.body, children: [(0, jsx_runtime_1.jsx)("div", { css: style.indicator }), (0, jsx_runtime_1.jsx)(Column_1.Column, { items: years, value: year, onChange: setYear, format: (n) => `${n}${suffix("year")}`, style: style }), (0, jsx_runtime_1.jsx)(Column_1.Column, { items: months, value: month, onChange: setMonth, format: (n) => `${pad2(n)}${suffix("month")}`, style: style }), (0, jsx_runtime_1.jsx)(Column_1.Column, { items: days, value: day, onChange: setDay, format: (n) => `${pad2(n)}${suffix("day")}`, style: style }), showHour && ((0, jsx_runtime_1.jsx)(Column_1.Column, { items: hours, value: hour, onChange: setHour, format: (n) => `${pad2(n)}${suffix("hour")}`, style: style })), showMinute && ((0, jsx_runtime_1.jsx)(Column_1.Column, { items: minutes, value: minute, onChange: setMinute, format: (n) => `${pad2(n)}${suffix("minute")}`, style: style })), showSecond && ((0, jsx_runtime_1.jsx)(Column_1.Column, { items: seconds, value: second, onChange: setSecond, format: (n) => `${pad2(n)}${suffix("second")}`, style: style }))] })] }));
|
|
209
|
+
}
|
|
210
|
+
function showDatePicker(options = {}) {
|
|
211
|
+
const { maskClosable = true, onCancel } = options, rest = __rest(options, ["maskClosable", "onCancel"]);
|
|
212
|
+
// 防止动画结束前重复触发 close(双击确认/取消、close 后再点遮罩等场景)
|
|
213
|
+
let closing = false;
|
|
214
|
+
let close;
|
|
215
|
+
const requestClose = () => {
|
|
216
|
+
if (closing)
|
|
217
|
+
return;
|
|
218
|
+
closing = true;
|
|
219
|
+
close === null || close === void 0 ? void 0 : close();
|
|
220
|
+
};
|
|
221
|
+
close = (0, Dialog_1.showDialog)({
|
|
222
|
+
type: "pullUp",
|
|
223
|
+
blankClosable: maskClosable,
|
|
224
|
+
// 点击空白时同步补发 onCancel 通知(关闭动画并行进行)
|
|
225
|
+
onBlankClick: () => {
|
|
226
|
+
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
227
|
+
},
|
|
228
|
+
content: ((0, jsx_runtime_1.jsx)(DatePicker, Object.assign({}, rest, { onCancel: onCancel, onClose: requestClose }))),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Interpolation, Theme } from "@emotion/react";
|
|
2
|
+
export declare const ITEM_HEIGHT_REM = 0.8;
|
|
3
|
+
export declare const VISIBLE_ROWS = 5;
|
|
4
|
+
export type DatePickerStyle = Record<string, Interpolation<Theme>>;
|
|
5
|
+
export declare function createStyle(primary: string, rounded?: boolean): DatePickerStyle;
|
|
6
|
+
export declare const DEFAULT_PRIMARY = "#2f7dff";
|