@rxdrag/website-lib-core 0.0.103 → 0.0.104
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/package.json +4 -4
- package/src/component-logic/index.ts +1 -8
- package/src/global.d.ts +7 -0
- package/src/index.ts +1 -3
- package/src/react/components/BackgroundHlsVideoPlayer.tsx +30 -1
- package/src/react/components/ContactForm/ContactForm.tsx +3 -2
- package/src/react/components/ContactForm/types.ts +1 -0
- package/src/react/components/Scroller.tsx +32 -4
- package/src/react/components/Share/socials.tsx +1 -2
- package/src/react/index.ts +1 -2
- package/src/component-logic/collapse.ts +0 -61
- package/src/component-logic/gsap.d.ts +0 -4
- package/src/component-logic/modal.ts +0 -45
- package/src/component-logic/motion.ts +0 -272
- package/src/component-logic/number.ts +0 -45
- package/src/component-logic/popover.ts +0 -51
- package/src/component-logic/tabs.ts +0 -10
- package/src/controller/AnimateController.ts +0 -138
- package/src/controller/AosController.ts +0 -240
- package/src/controller/CollapseController.ts +0 -130
- package/src/controller/FlipController.ts +0 -339
- package/src/controller/ModalController.ts +0 -127
- package/src/controller/NumberController.ts +0 -161
- package/src/controller/OpenableController.ts +0 -367
- package/src/controller/PageLoader.ts +0 -154
- package/src/controller/PopoverController.ts +0 -116
- package/src/controller/TabsController.ts +0 -271
- package/src/controller/applyAnimation.ts +0 -86
- package/src/controller/applyInitialState.ts +0 -79
- package/src/controller/consts.ts +0 -33
- package/src/controller/index.ts +0 -10
- package/src/controller/utils.ts +0 -48
- package/src/motion/consts.ts +0 -428
- package/src/motion/convertToGsapVars.ts +0 -102
- package/src/motion/index.ts +0 -6
- package/src/motion/normalizeAnimation.ts +0 -28
- package/src/motion/normalizeAosAnimation.ts +0 -22
- package/src/motion/normalizePopupAnimation.ts +0 -24
- package/src/motion/types.ts +0 -133
- package/src/react/hooks/index.ts +0 -1
- package/src/react/hooks/useScroll.ts +0 -30
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import { DATA_MOTION_FLIP } from "../component-logic/tabs";
|
|
2
|
-
|
|
3
|
-
// 全局 Flip 控制器
|
|
4
|
-
export class FlipController {
|
|
5
|
-
// 单例实例
|
|
6
|
-
private static instances: Record<string, FlipController> = {};
|
|
7
|
-
private constructor(private doc?: Document) {}
|
|
8
|
-
|
|
9
|
-
// 记录元素的上一个位置(相对于文档的绝对位置)
|
|
10
|
-
private previousPositions = new Map<
|
|
11
|
-
string,
|
|
12
|
-
{
|
|
13
|
-
left: number;
|
|
14
|
-
top: number;
|
|
15
|
-
width: number;
|
|
16
|
-
height: number;
|
|
17
|
-
wasVisible?: boolean; // 标记元素是否曾经可见
|
|
18
|
-
}
|
|
19
|
-
>();
|
|
20
|
-
// 标记正在处理的元素,防止递归循环
|
|
21
|
-
private processingElements = new Set<HTMLElement>();
|
|
22
|
-
// 观察器
|
|
23
|
-
private resizeObserver: ResizeObserver | null = null;
|
|
24
|
-
|
|
25
|
-
// 获取单例实例
|
|
26
|
-
static getInstance(key: string = "runtime", doc?: Document): FlipController {
|
|
27
|
-
if (!FlipController.instances[key]) {
|
|
28
|
-
FlipController.instances[key] = new FlipController(doc);
|
|
29
|
-
} else if (doc) {
|
|
30
|
-
FlipController.instances[key].setDoc(doc);
|
|
31
|
-
}
|
|
32
|
-
return FlipController.instances[key];
|
|
33
|
-
}
|
|
34
|
-
// 初始化控制器
|
|
35
|
-
mount() {
|
|
36
|
-
this.unmount();
|
|
37
|
-
// 设置观察器
|
|
38
|
-
this.setupObservers();
|
|
39
|
-
// 初始化元素
|
|
40
|
-
this.initElements();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
unmount() {
|
|
44
|
-
// 移除观察器
|
|
45
|
-
this.resizeObserver?.disconnect();
|
|
46
|
-
this.resizeObserver = null;
|
|
47
|
-
// 移除事件监听器
|
|
48
|
-
this.doc?.defaultView?.removeEventListener("scroll", this.handleScroll);
|
|
49
|
-
// 清理数据结构
|
|
50
|
-
this.previousPositions.clear();
|
|
51
|
-
this.processingElements.clear();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 销毁指定实例
|
|
55
|
-
static destroyInstance(key: string = "runtime") {
|
|
56
|
-
if (FlipController.instances[key]) {
|
|
57
|
-
FlipController.instances[key].unmount();
|
|
58
|
-
delete FlipController.instances[key];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 销毁所有实例
|
|
63
|
-
static destroyAllInstances() {
|
|
64
|
-
Object.keys(FlipController.instances).forEach((key) => {
|
|
65
|
-
FlipController.instances[key].unmount();
|
|
66
|
-
});
|
|
67
|
-
FlipController.instances = {};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private setDoc(doc?: Document) {
|
|
71
|
-
this.doc = doc;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 滚动事件处理函数
|
|
75
|
-
private handleScroll = () => {
|
|
76
|
-
// 页面滚动时,重新计算所有需要动画的元素
|
|
77
|
-
this.doc
|
|
78
|
-
?.querySelectorAll<HTMLElement>("[data-flip]")
|
|
79
|
-
.forEach((element) => {
|
|
80
|
-
if (this.previousPositions.has(element.dataset.flip || "")) {
|
|
81
|
-
this.performAnimation(element);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// 设置观察器
|
|
87
|
-
private setupObservers() {
|
|
88
|
-
// 设置 ResizeObserver 监控元素尺寸变化
|
|
89
|
-
this.resizeObserver = new ResizeObserver((entries) => {
|
|
90
|
-
entries.forEach((entry) => {
|
|
91
|
-
if (
|
|
92
|
-
this.doc?.defaultView?.HTMLElement &&
|
|
93
|
-
entry.target instanceof this.doc?.defaultView?.HTMLElement
|
|
94
|
-
) {
|
|
95
|
-
this.performAnimation(entry.target);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// 监听滚动事件 - 滚动不会影响相对于文档的绝对位置,所以不需要特殊处理
|
|
101
|
-
this.doc?.defaultView?.addEventListener("scroll", this.handleScroll, {
|
|
102
|
-
passive: true,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 初始化元素,记录初始位置
|
|
107
|
-
private initElements() {
|
|
108
|
-
// 查找所有带有 data-flip 属性的元素
|
|
109
|
-
const flipElements = this.doc?.querySelectorAll<HTMLElement>("[data-flip]");
|
|
110
|
-
flipElements?.forEach((element) => {
|
|
111
|
-
// 添加到 ResizeObserver 监控
|
|
112
|
-
if (this.resizeObserver) {
|
|
113
|
-
this.resizeObserver.observe(element);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// 添加 IntersectionObserver 来监测元素进入视窗
|
|
118
|
-
if (typeof IntersectionObserver !== "undefined" && this.doc) {
|
|
119
|
-
const intersectionObserver = new IntersectionObserver(
|
|
120
|
-
(entries) => {
|
|
121
|
-
entries.forEach((entry) => {
|
|
122
|
-
if (entry.isIntersecting && entry.target instanceof HTMLElement) {
|
|
123
|
-
const flipKey = entry.target.dataset.flip;
|
|
124
|
-
if (flipKey) {
|
|
125
|
-
const position = this.previousPositions.get(flipKey);
|
|
126
|
-
if (position && position.wasVisible === false) {
|
|
127
|
-
// 元素首次进入视窗,更新位置记录
|
|
128
|
-
this.recordPosition(entry.target);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
{ threshold: 0.1 }
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
// 观察所有 flip 元素
|
|
138
|
-
flipElements?.forEach((element) => {
|
|
139
|
-
intersectionObserver.observe(element);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 延迟记录初始位置,确保元素已经渲染完成
|
|
144
|
-
setTimeout(() => {
|
|
145
|
-
// 记录每个元素的初始位置
|
|
146
|
-
flipElements?.forEach((element) => {
|
|
147
|
-
this.recordPosition(element);
|
|
148
|
-
});
|
|
149
|
-
}, 100); // 给予足够的时间让元素渲染完成
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// 记录元素位置
|
|
153
|
-
private recordPosition(element: HTMLElement) {
|
|
154
|
-
const flipKey = element.dataset.flip;
|
|
155
|
-
if (!flipKey) return;
|
|
156
|
-
|
|
157
|
-
// 检查元素是否可见
|
|
158
|
-
const isCurrentlyVisible = this.isVisible(element);
|
|
159
|
-
// 获取之前的位置记录
|
|
160
|
-
const previousPosition = this.previousPositions.get(flipKey);
|
|
161
|
-
|
|
162
|
-
// 如果元素不可见且没有之前的记录,我们仍然记录它的位置
|
|
163
|
-
// 但标记它为不可见,这样当它首次出现时我们可以决定是否应用动画
|
|
164
|
-
if (!isCurrentlyVisible && !previousPosition) {
|
|
165
|
-
// 继续执行,记录位置
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 获取元素相对于视口的位置
|
|
169
|
-
const rect = element.getBoundingClientRect();
|
|
170
|
-
|
|
171
|
-
// 计算元素相对于文档的绝对位置
|
|
172
|
-
const absoluteLeft = rect.left + (this.doc?.defaultView?.scrollX || 0);
|
|
173
|
-
const absoluteTop = rect.top + (this.doc?.defaultView?.scrollY || 0);
|
|
174
|
-
|
|
175
|
-
// 记录元素相对于文档的绝对位置
|
|
176
|
-
this.previousPositions.set(flipKey, {
|
|
177
|
-
left: absoluteLeft,
|
|
178
|
-
top: absoluteTop,
|
|
179
|
-
width: rect.width,
|
|
180
|
-
height: rect.height,
|
|
181
|
-
wasVisible: isCurrentlyVisible, // 记录元素是否可见
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 检查元素是否可见
|
|
186
|
-
private isVisible(element: HTMLElement): boolean {
|
|
187
|
-
const rect = element.getBoundingClientRect();
|
|
188
|
-
return (
|
|
189
|
-
rect.width > 0 &&
|
|
190
|
-
rect.height > 0 &&
|
|
191
|
-
rect.top < (this.doc?.defaultView?.innerHeight || 0) &&
|
|
192
|
-
rect.bottom > 0 &&
|
|
193
|
-
rect.left < (this.doc?.defaultView?.innerWidth || 0) &&
|
|
194
|
-
rect.right > 0
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 执行 FLIP 动画
|
|
199
|
-
performAnimation(element: HTMLElement) {
|
|
200
|
-
// 防止重复处理同一元素
|
|
201
|
-
if (this.processingElements.has(element)) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 获取元素的 flip key
|
|
206
|
-
const flipKey = element.dataset.flip;
|
|
207
|
-
if (!flipKey) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// 检查元素是否可见
|
|
212
|
-
if (!this.isVisible(element)) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 记录当前正在处理的元素
|
|
217
|
-
this.processingElements.add(element);
|
|
218
|
-
|
|
219
|
-
// 获取元素的当前位置(相对于视口)
|
|
220
|
-
const currentRect = element.getBoundingClientRect();
|
|
221
|
-
|
|
222
|
-
// 检查当前位置是否有效
|
|
223
|
-
if (currentRect.width === 0 || currentRect.height === 0) {
|
|
224
|
-
this.processingElements.delete(element);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// 计算当前元素相对于文档的绝对位置
|
|
229
|
-
const currentAbsoluteLeft =
|
|
230
|
-
currentRect.left + (this.doc?.defaultView?.scrollX || 0);
|
|
231
|
-
const currentAbsoluteTop =
|
|
232
|
-
currentRect.top + (this.doc?.defaultView?.scrollY || 0);
|
|
233
|
-
|
|
234
|
-
// 获取元素的上一个位置
|
|
235
|
-
const previousPosition = this.previousPositions.get(flipKey);
|
|
236
|
-
|
|
237
|
-
// 如果没有上一个位置,记录当前位置并返回
|
|
238
|
-
if (!previousPosition) {
|
|
239
|
-
this.recordPosition(element);
|
|
240
|
-
this.processingElements.delete(element);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 如果元素之前未曾可见(首次进入视窗),我们可能想要跳过动画
|
|
245
|
-
// 或者应用特殊的入场动画
|
|
246
|
-
if (this.isVisible(element) && previousPosition.wasVisible === false) {
|
|
247
|
-
// 更新位置记录,标记为可见
|
|
248
|
-
this.recordPosition(element);
|
|
249
|
-
this.processingElements.delete(element);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 检查上一个位置是否有效
|
|
254
|
-
if (previousPosition.width === 0 || previousPosition.height === 0) {
|
|
255
|
-
this.recordPosition(element);
|
|
256
|
-
this.processingElements.delete(element);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 计算位置差异(基于绝对位置,不需要考虑滚动)
|
|
261
|
-
const deltaX = previousPosition.left - currentAbsoluteLeft;
|
|
262
|
-
const deltaY = previousPosition.top - currentAbsoluteTop;
|
|
263
|
-
const scaleX = previousPosition.width / currentRect.width;
|
|
264
|
-
const scaleY = previousPosition.height / currentRect.height;
|
|
265
|
-
|
|
266
|
-
// 如果位置没有变化,跳过动画
|
|
267
|
-
if (
|
|
268
|
-
Math.abs(deltaX) < 1 &&
|
|
269
|
-
Math.abs(deltaY) < 1 &&
|
|
270
|
-
Math.abs(scaleX - 1) < 0.01 &&
|
|
271
|
-
Math.abs(scaleY - 1) < 0.01
|
|
272
|
-
) {
|
|
273
|
-
this.recordPosition(element);
|
|
274
|
-
this.processingElements.delete(element);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
// 使用原生 Web Animation API 执行动画
|
|
280
|
-
|
|
281
|
-
// 先将元素设置为初始位置
|
|
282
|
-
element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`;
|
|
283
|
-
element.style.transformOrigin = "0 0";
|
|
284
|
-
|
|
285
|
-
// 使用 requestAnimationFrame 确保初始状态已应用
|
|
286
|
-
requestAnimationFrame(() => {
|
|
287
|
-
const container = element.closest(`[${DATA_MOTION_FLIP}]`);
|
|
288
|
-
const transionJSON = (container as HTMLElement | null)?.getAttribute(
|
|
289
|
-
DATA_MOTION_FLIP
|
|
290
|
-
);
|
|
291
|
-
const transion = transionJSON
|
|
292
|
-
? JSON.parse(transionJSON)
|
|
293
|
-
: {
|
|
294
|
-
duration: 300,
|
|
295
|
-
};
|
|
296
|
-
// 创建动画
|
|
297
|
-
const animation = element.animate(
|
|
298
|
-
[
|
|
299
|
-
{
|
|
300
|
-
transform: `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})`,
|
|
301
|
-
},
|
|
302
|
-
{ transform: "translate(0, 0) scale(1, 1)" },
|
|
303
|
-
],
|
|
304
|
-
transion
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
// 动画完成后的处理
|
|
308
|
-
animation.onfinish = () => {
|
|
309
|
-
// 清除样式并更新位置
|
|
310
|
-
element.style.transform = "";
|
|
311
|
-
element.style.transformOrigin = "";
|
|
312
|
-
this.recordPosition(element);
|
|
313
|
-
this.processingElements.delete(element);
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
// 设置一个备用的超时处理,以防动画事件不触发
|
|
317
|
-
setTimeout(() => {
|
|
318
|
-
if (this.processingElements.has(element)) {
|
|
319
|
-
element.style.transform = "";
|
|
320
|
-
element.style.transformOrigin = "";
|
|
321
|
-
this.recordPosition(element);
|
|
322
|
-
this.processingElements.delete(element);
|
|
323
|
-
}
|
|
324
|
-
}, 350); // 略长于动画时间
|
|
325
|
-
});
|
|
326
|
-
} catch (error) {
|
|
327
|
-
// 出错时也要清理
|
|
328
|
-
element.style.transform = "";
|
|
329
|
-
element.style.transformOrigin = "";
|
|
330
|
-
this.recordPosition(element);
|
|
331
|
-
this.processingElements.delete(element);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export const flip = FlipController.getInstance(
|
|
337
|
-
"runtime",
|
|
338
|
-
typeof document !== "undefined" ? document : undefined
|
|
339
|
-
);
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { DATA_OPENABLE, DATA_OPENABLE_ROLE, OpenAble } from "./consts";
|
|
2
|
-
import { OpenableController } from "./OpenableController";
|
|
3
|
-
|
|
4
|
-
export class ModalController extends OpenableController {
|
|
5
|
-
private static instances: Record<string, ModalController> = {};
|
|
6
|
-
|
|
7
|
-
private constructor(doc?: Document) {
|
|
8
|
-
super(doc);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
public static getInstance(
|
|
12
|
-
key: string = "runtime",
|
|
13
|
-
doc?: Document
|
|
14
|
-
): ModalController {
|
|
15
|
-
if (!ModalController.instances[key]) {
|
|
16
|
-
ModalController.instances[key] = new ModalController(doc);
|
|
17
|
-
} else if (doc) {
|
|
18
|
-
ModalController.instances[key].setDoc(doc);
|
|
19
|
-
}
|
|
20
|
-
return ModalController.instances[key];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 销毁指定 key 的实例
|
|
25
|
-
* @param key 实例的唯一标识
|
|
26
|
-
*/
|
|
27
|
-
public static destroyInstance(key: string = "runtime"): void {
|
|
28
|
-
if (ModalController.instances[key]) {
|
|
29
|
-
ModalController.instances[key].destroy();
|
|
30
|
-
delete ModalController.instances[key];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private setDoc(doc?: Document) {
|
|
35
|
-
this.doc = doc;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
mount() {
|
|
39
|
-
super.mount();
|
|
40
|
-
const popups = this.doc?.querySelectorAll(`[${DATA_OPENABLE}]`);
|
|
41
|
-
popups?.forEach((popup) => {
|
|
42
|
-
const openableKey = popup.getAttribute(DATA_OPENABLE);
|
|
43
|
-
//处理鼠标交互事件
|
|
44
|
-
if (openableKey && popup) {
|
|
45
|
-
if (popup.getAttribute(DATA_OPENABLE_ROLE) === OpenAble.ModalTrigger) {
|
|
46
|
-
const unsub = this.initModalTrigger(openableKey, popup as HTMLElement);
|
|
47
|
-
this.unmountHandlers.push(unsub);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
//获取所有 ModalCloser 实例,并处理点击事件
|
|
51
|
-
//TODO:未处理嵌套跟Modal外部关闭的情况
|
|
52
|
-
const closers = popup?.querySelectorAll(
|
|
53
|
-
`[${DATA_OPENABLE_ROLE}="${OpenAble.ModalCloser}"]`
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
closers?.forEach((closer) => {
|
|
57
|
-
// 使用initModalCloser函数处理关闭按钮事件
|
|
58
|
-
if (openableKey) {
|
|
59
|
-
const unsub = this.initModalCloser(openableKey, closer as HTMLElement);
|
|
60
|
-
this.unmountHandlers.push(unsub);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* 初始化Modal的点击交互事件
|
|
67
|
-
* @param openableKey - 弹出层的唯一标识
|
|
68
|
-
* @param trigger - 触发器元素
|
|
69
|
-
* @returns 清理函数,用于移除事件监听器
|
|
70
|
-
*/
|
|
71
|
-
public initModalTrigger(openableKey: string, trigger: HTMLElement): () => void {
|
|
72
|
-
if (!this.doc) return () => {};
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// 创建新的事件处理函数
|
|
76
|
-
const handleTriggerClick = () => {
|
|
77
|
-
// 查找 Modal 容器
|
|
78
|
-
this.open(openableKey, trigger);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// 添加新的事件监听器
|
|
82
|
-
trigger.addEventListener("click", handleTriggerClick);
|
|
83
|
-
|
|
84
|
-
return () => {
|
|
85
|
-
trigger.removeEventListener("click", handleTriggerClick);
|
|
86
|
-
};
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error(`初始化Modal事件失败: ${openableKey}`, error);
|
|
89
|
-
return () => {};
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 初始化Modal关闭按钮的点击交互事件
|
|
95
|
-
* @param openableKey - 弹出层的唯一标识
|
|
96
|
-
* @param closer - 关闭按钮元素
|
|
97
|
-
* @returns 清理函数,用于移除事件监听器
|
|
98
|
-
*/
|
|
99
|
-
public initModalCloser(openableKey: string, closer: HTMLElement): () => void {
|
|
100
|
-
if (!this.doc) return () => {};
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
// 创建新的事件处理函数
|
|
104
|
-
const handleCloserClick = () => {
|
|
105
|
-
this.close(openableKey);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// 添加新的事件监听器
|
|
109
|
-
closer.addEventListener("click", handleCloserClick);
|
|
110
|
-
|
|
111
|
-
return () => {
|
|
112
|
-
closer.removeEventListener("click", handleCloserClick);
|
|
113
|
-
};
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error(`初始化Modal关闭按钮事件失败: ${openableKey}`, error);
|
|
116
|
-
return () => {};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
unmount(): void {
|
|
120
|
-
super.unmount();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export const modal = ModalController.getInstance(
|
|
125
|
-
"runtime",
|
|
126
|
-
typeof document !== "undefined" ? document : undefined
|
|
127
|
-
);
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { DATA_MOTION_NUMBER, DATA_MOTION_NUMBER_ONCE } from "../motion/consts";
|
|
2
|
-
import { gsap } from "gsap/dist/gsap";
|
|
3
|
-
|
|
4
|
-
export class NumberController {
|
|
5
|
-
private static instances: Record<string, NumberController> = {};
|
|
6
|
-
private doc?: Document;
|
|
7
|
-
private observers = new Map();
|
|
8
|
-
|
|
9
|
-
private constructor(doc?: Document) {
|
|
10
|
-
this.doc = doc;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 获取 NumberController 实例
|
|
15
|
-
* @param key 实例的唯一标识符
|
|
16
|
-
* @param doc 文档对象
|
|
17
|
-
* @returns NumberController 实例
|
|
18
|
-
*/
|
|
19
|
-
public static getInstance(
|
|
20
|
-
key: string = "runtime",
|
|
21
|
-
doc?: Document
|
|
22
|
-
): NumberController {
|
|
23
|
-
if (!NumberController.instances[key]) {
|
|
24
|
-
NumberController.instances[key] = new NumberController(doc);
|
|
25
|
-
} else if (doc) {
|
|
26
|
-
NumberController.instances[key].setDoc(doc);
|
|
27
|
-
}
|
|
28
|
-
return NumberController.instances[key];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 销毁指定 key 的实例
|
|
33
|
-
* @param key 实例的唯一标识符
|
|
34
|
-
*/
|
|
35
|
-
public static destroyInstance(key: string = "runtime"): void {
|
|
36
|
-
if (NumberController.instances[key]) {
|
|
37
|
-
delete NumberController.instances[key];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 设置文档对象
|
|
43
|
-
* @param doc 文档对象
|
|
44
|
-
*/
|
|
45
|
-
public setDoc(doc: Document): void {
|
|
46
|
-
this.doc = doc;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
mount() {
|
|
50
|
-
// 清理之前的观察器
|
|
51
|
-
this.unmount();
|
|
52
|
-
|
|
53
|
-
// 检查文档对象是否存在
|
|
54
|
-
if (!this.doc) {
|
|
55
|
-
console.error("文档对象未设置,无法初始化数字动画");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 使用正确的属性选择器
|
|
60
|
-
const elements = this.doc.querySelectorAll(`[${DATA_MOTION_NUMBER}]`);
|
|
61
|
-
|
|
62
|
-
elements?.forEach((element) => {
|
|
63
|
-
if (
|
|
64
|
-
this.doc?.defaultView?.HTMLElement &&
|
|
65
|
-
element instanceof this.doc?.defaultView?.HTMLElement
|
|
66
|
-
) {
|
|
67
|
-
const dataAttr = element.getAttribute(DATA_MOTION_NUMBER);
|
|
68
|
-
if (dataAttr) {
|
|
69
|
-
try {
|
|
70
|
-
const data = JSON.parse(dataAttr);
|
|
71
|
-
// 确保 start 和 end 是数字类型
|
|
72
|
-
const startValue = Number(data.start);
|
|
73
|
-
const endValue = Number(data.end);
|
|
74
|
-
const transition = data.transition;
|
|
75
|
-
const once =
|
|
76
|
-
element.getAttribute(DATA_MOTION_NUMBER_ONCE) === "true";
|
|
77
|
-
|
|
78
|
-
const numberElement = element.querySelector(
|
|
79
|
-
".motion-safe\\:animate-number"
|
|
80
|
-
) as HTMLElement;
|
|
81
|
-
const signElement = element.querySelector(
|
|
82
|
-
".motion-safe\\:animate-sign"
|
|
83
|
-
) as HTMLElement;
|
|
84
|
-
|
|
85
|
-
if (!numberElement) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 重置初始状态
|
|
90
|
-
numberElement.innerHTML = startValue.toString();
|
|
91
|
-
if (signElement) {
|
|
92
|
-
signElement.style.display = "none";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 使用 IntersectionObserver 监听元素进入视口
|
|
96
|
-
const observer = new IntersectionObserver(
|
|
97
|
-
(entries) => {
|
|
98
|
-
if (entries[0].isIntersecting) {
|
|
99
|
-
const aleadyRun =
|
|
100
|
-
numberElement.dataset.animationNumberRun === "true";
|
|
101
|
-
if (aleadyRun && once) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
numberElement.dataset.animationNumberRun = "true";
|
|
106
|
-
// 使用 GSAP 动画替代 motion.animate
|
|
107
|
-
const obj = { value: startValue };
|
|
108
|
-
gsap.to(obj, {
|
|
109
|
-
value: endValue,
|
|
110
|
-
duration: transition?.duration || 1,
|
|
111
|
-
ease: transition?.ease || "power2.out",
|
|
112
|
-
onUpdate: () => {
|
|
113
|
-
numberElement.innerHTML = Math.round(
|
|
114
|
-
obj.value
|
|
115
|
-
)?.toString();
|
|
116
|
-
},
|
|
117
|
-
onComplete: () => {
|
|
118
|
-
if (signElement) {
|
|
119
|
-
signElement.style.display = "inline";
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (once) {
|
|
125
|
-
observer.disconnect();
|
|
126
|
-
this.observers.delete(element);
|
|
127
|
-
}
|
|
128
|
-
} else if (!once) {
|
|
129
|
-
numberElement.innerHTML = startValue.toString();
|
|
130
|
-
if (signElement) {
|
|
131
|
-
signElement.style.display = "none";
|
|
132
|
-
console.log("隐藏符号元素");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
{ threshold: 0.1, rootMargin: "0px 0px -10% 0px" }
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
observer.observe(element);
|
|
140
|
-
this.observers.set(element, observer);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
console.error("处理数字动画时出错:", error);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
public unmount() {
|
|
150
|
-
this.observers.forEach((observer) => {
|
|
151
|
-
observer.disconnect();
|
|
152
|
-
});
|
|
153
|
-
this.observers.clear();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 导出默认实例
|
|
158
|
-
export const numberController = NumberController.getInstance(
|
|
159
|
-
"runtime",
|
|
160
|
-
typeof document !== "undefined" ? document : undefined
|
|
161
|
-
);
|