@rxdrag/website-lib-core 0.0.12 → 0.0.14
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 +5 -5
- package/src/component-logic/modal.ts +11 -2
- package/src/controller/OpenableController.ts +5 -0
- package/src/controller/PageLoader.ts +1 -1
- package/src/entify/Entify.ts +4 -0
- package/src/entify/IEntify.ts +1 -0
- package/src/entify/lib/queryOneProductBySlug.ts +8 -3
- package/src/entify/lib/queryPostCategories.ts +7 -0
- package/src/entify/lib/queryProductCategories.ts +7 -0
- package/src/entify/lib/queryWebSiteSettings.ts +2 -3
- package/src/entify/view-model/models.ts +0 -2
- package/src/react/components/ContactForm/index.tsx +3 -26
- package/src/react/components/Medias/index.tsx +270 -273
- package/src/react/components/ProductCard/ProductCta/index.tsx +1 -1
- package/src/react/components/RichTextOutline/index.tsx +4 -5
- package/src/react/components/RichTextOutline/useAcitviedHeading.ts +81 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rxdrag/website-lib-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.ts"
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"@types/react-dom": "^18.2.7",
|
|
25
25
|
"eslint": "^7.32.0",
|
|
26
26
|
"typescript": "^5",
|
|
27
|
-
"@rxdrag/entify-hooks": "0.2.
|
|
27
|
+
"@rxdrag/entify-hooks": "0.2.43",
|
|
28
|
+
"@rxdrag/slate-preview": "1.2.56",
|
|
28
29
|
"@rxdrag/eslint-config-custom": "0.2.12",
|
|
29
|
-
"@rxdrag/tsconfig": "0.2.0"
|
|
30
|
-
"@rxdrag/slate-preview": "1.2.55"
|
|
30
|
+
"@rxdrag/tsconfig": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"clsx": "^2.1.0",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"lodash-es": "^4.17.21",
|
|
36
36
|
"react": "^18.2.0",
|
|
37
37
|
"react-dom": "^18.2.0",
|
|
38
|
-
"@rxdrag/rxcms-models": "0.3.
|
|
38
|
+
"@rxdrag/rxcms-models": "0.3.49"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"astro": "^4.0.0 || ^5.0.0"
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DATA_OPENABLE,
|
|
3
|
+
DATA_OPENABLE_ROLE,
|
|
4
|
+
DATA_POPUP_CTA,
|
|
5
|
+
OpenAble,
|
|
6
|
+
} from "../controller";
|
|
2
7
|
import { HTMLElementsWithChildren, IMotionProps } from "./motion";
|
|
3
8
|
|
|
4
9
|
export type ModalTriggerProps = IMotionProps & {
|
|
@@ -28,9 +33,13 @@ export function getModalPanelDataAttrs(openableKey?: string) {
|
|
|
28
33
|
};
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
export function getModalTriggerDataAttrs(
|
|
36
|
+
export function getModalTriggerDataAttrs(
|
|
37
|
+
openableKey: string | undefined,
|
|
38
|
+
callToAction?: string
|
|
39
|
+
) {
|
|
32
40
|
return {
|
|
33
41
|
[DATA_OPENABLE]: openableKey,
|
|
34
42
|
[DATA_OPENABLE_ROLE]: OpenAble.ModalTrigger,
|
|
43
|
+
[DATA_POPUP_CTA]: callToAction,
|
|
35
44
|
};
|
|
36
45
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
OpenAble,
|
|
10
10
|
EVENT_SELECT,
|
|
11
11
|
EVENT_UNSELECT,
|
|
12
|
+
DATA_POPUP_CTA,
|
|
12
13
|
} from "./consts";
|
|
13
14
|
import { applyInitialState } from "./applyInitialState";
|
|
14
15
|
|
|
@@ -130,6 +131,7 @@ export class OpenableController {
|
|
|
130
131
|
constructor(protected doc: Document | undefined) {}
|
|
131
132
|
protected unmountHandlers: (() => void)[] = [];
|
|
132
133
|
protected eventBus = new EventBus();
|
|
134
|
+
public lastCta?: string | null;
|
|
133
135
|
|
|
134
136
|
mount() {
|
|
135
137
|
this.unmount();
|
|
@@ -152,6 +154,9 @@ export class OpenableController {
|
|
|
152
154
|
// 查找 Modal 容器
|
|
153
155
|
const modalContainer = this.getOpenableContainer(openableKey);
|
|
154
156
|
if (modalContainer) {
|
|
157
|
+
if (target.getAttribute(DATA_POPUP_CTA)) {
|
|
158
|
+
this.lastCta = target.getAttribute(DATA_POPUP_CTA);
|
|
159
|
+
}
|
|
155
160
|
modalContainer.classList.add("open");
|
|
156
161
|
this.eventBus.emit(EVENT_OPEN, { key: openableKey, target });
|
|
157
162
|
} else {
|
|
@@ -136,7 +136,7 @@ export class PageLoader implements IPageLoader {
|
|
|
136
136
|
// 移除所有事件监听器
|
|
137
137
|
const noop = () => {};
|
|
138
138
|
this.doc.removeEventListener("astro:after-swap", noop);
|
|
139
|
-
this.doc.removeEventListener("
|
|
139
|
+
this.doc.removeEventListener("astro:page-load", noop);
|
|
140
140
|
|
|
141
141
|
// 清空回调集合
|
|
142
142
|
this.callbacks.clear();
|
package/src/entify/Entify.ts
CHANGED
package/src/entify/IEntify.ts
CHANGED
|
@@ -59,9 +59,14 @@ export async function queryOneProductBySlug(
|
|
|
59
59
|
envVariables
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const relatedProducts = result?.items?.map((pro) =>
|
|
63
|
+
productToViewModel(pro)
|
|
64
|
+
) as TProduct[] | undefined;
|
|
65
|
+
|
|
66
|
+
//重新排序后赋值
|
|
67
|
+
tProduct.related = product?.relatedSlugs.map((slug) =>
|
|
68
|
+
relatedProducts?.find((p) => p.slug === slug)
|
|
69
|
+
) as TProduct[] | undefined;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
return tProduct;
|
|
@@ -30,6 +30,13 @@ export async function queryPostCategories(envVariables: EnvVariables) {
|
|
|
30
30
|
PostCategoryFields.description,
|
|
31
31
|
],
|
|
32
32
|
{
|
|
33
|
+
where: {
|
|
34
|
+
lang: {
|
|
35
|
+
abbr: {
|
|
36
|
+
_eq: envVariables.language,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
33
40
|
orderBy: [{ [PostCategoryFields.seqValue]: "asc" }],
|
|
34
41
|
}
|
|
35
42
|
).posts(
|
|
@@ -15,6 +15,13 @@ export async function queryProductCategories(envVariables: EnvVariables) {
|
|
|
15
15
|
ProductCategoryFields.description,
|
|
16
16
|
],
|
|
17
17
|
{
|
|
18
|
+
where: {
|
|
19
|
+
lang: {
|
|
20
|
+
abbr: {
|
|
21
|
+
_eq: envVariables.language,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
18
25
|
orderBy: [
|
|
19
26
|
{ [ProductCategoryFields.seqValue]: "asc" }
|
|
20
27
|
]
|
|
@@ -17,11 +17,10 @@ export async function queryWebSiteSettings(envVariables: EnvVariables) {
|
|
|
17
17
|
WebsiteSettingsDistinctExp
|
|
18
18
|
>(
|
|
19
19
|
new WebsiteSettingsQueryOptions([
|
|
20
|
-
WebsiteSettingsFields.footerCode,
|
|
21
|
-
WebsiteSettingsFields.headerCode,
|
|
22
|
-
WebsiteSettingsFields.gaTrackingId,
|
|
23
20
|
WebsiteSettingsFields.noticeEmail,
|
|
24
21
|
WebsiteSettingsFields.smtpConfig,
|
|
22
|
+
WebsiteSettingsFields.replyToEmail,
|
|
23
|
+
WebsiteSettingsFields.useCustomizedSmtp,
|
|
25
24
|
]),
|
|
26
25
|
envVariables
|
|
27
26
|
);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { forwardRef,
|
|
1
|
+
import { forwardRef, useState } from "react";
|
|
2
2
|
import { Input } from "./Input";
|
|
3
3
|
import { Submit } from "./Submit";
|
|
4
4
|
import { Textarea } from "./Textarea";
|
|
5
|
-
import { DATA_POPUP_CTA } from "../../../controller/consts";
|
|
6
5
|
import clsx from "clsx";
|
|
7
|
-
import {
|
|
6
|
+
import { modal } from "../../../controller";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* 简单的加密函数,用于生成防机器人的加密字段
|
|
@@ -114,7 +113,6 @@ export const ContactForm = forwardRef<HTMLDivElement, ContactFormProps>(
|
|
|
114
113
|
email: "",
|
|
115
114
|
company: "",
|
|
116
115
|
message: "",
|
|
117
|
-
fromCta: "",
|
|
118
116
|
phone: "", // 初始化蜜罐字段
|
|
119
117
|
});
|
|
120
118
|
// 错误状态
|
|
@@ -124,26 +122,6 @@ export const ContactForm = forwardRef<HTMLDivElement, ContactFormProps>(
|
|
|
124
122
|
success?: boolean;
|
|
125
123
|
message?: string;
|
|
126
124
|
}>({});
|
|
127
|
-
//最近被点击过的cta
|
|
128
|
-
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
const unsub = popover.onOpenAll((event) => {
|
|
131
|
-
setFormData((prev) => ({
|
|
132
|
-
...prev,
|
|
133
|
-
fromCta: event.target?.getAttribute(DATA_POPUP_CTA) || undefined,
|
|
134
|
-
}));
|
|
135
|
-
});
|
|
136
|
-
const unsub2 = popover.onCloseAll(() => {
|
|
137
|
-
setFormData((prev) => ({
|
|
138
|
-
...prev,
|
|
139
|
-
fromCta: "",
|
|
140
|
-
}));
|
|
141
|
-
});
|
|
142
|
-
return () => {
|
|
143
|
-
unsub();
|
|
144
|
-
unsub2();
|
|
145
|
-
};
|
|
146
|
-
}, []);
|
|
147
125
|
|
|
148
126
|
// 处理输入变化
|
|
149
127
|
const handleChange = (
|
|
@@ -205,11 +183,11 @@ export const ContactForm = forwardRef<HTMLDivElement, ContactFormProps>(
|
|
|
205
183
|
try {
|
|
206
184
|
setSubmitting(true);
|
|
207
185
|
setSubmitStatus({}); // 重置提交状态
|
|
208
|
-
|
|
209
186
|
const response = await fetch(actionUrl, {
|
|
210
187
|
method: "POST",
|
|
211
188
|
body: JSON.stringify({
|
|
212
189
|
...formData,
|
|
190
|
+
fromCta: modal.lastCta,
|
|
213
191
|
encryptedField: encrypt(formData.phone, formSalt),
|
|
214
192
|
}),
|
|
215
193
|
headers: {
|
|
@@ -236,7 +214,6 @@ export const ContactForm = forwardRef<HTMLDivElement, ContactFormProps>(
|
|
|
236
214
|
email: "",
|
|
237
215
|
company: "",
|
|
238
216
|
message: "",
|
|
239
|
-
fromCta: "产品页面",
|
|
240
217
|
phone: "",
|
|
241
218
|
});
|
|
242
219
|
window.location.href = "/thanks";
|
|
@@ -8,146 +8,227 @@ export type MediasProps = {
|
|
|
8
8
|
children?: React.ReactNode;
|
|
9
9
|
// Aspect ratio, format is `aspect-[width/height]`
|
|
10
10
|
aspect?: string;
|
|
11
|
+
thumbnailAspect?: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export const Medias = forwardRef<HTMLDivElement, MediasProps>(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
export const Medias = forwardRef<HTMLDivElement, MediasProps>((props, ref) => {
|
|
15
|
+
const {
|
|
16
|
+
value,
|
|
17
|
+
className,
|
|
18
|
+
children,
|
|
19
|
+
aspect = "aspect-[1/1]",
|
|
20
|
+
thumbnailAspect = "aspect-[5/4]",
|
|
21
|
+
...rest
|
|
22
|
+
} = props;
|
|
23
|
+
const [selectedId, setSelectedId] = useState<string | undefined | null>(
|
|
24
|
+
value?.externalVideoUrl ? "video" : value?.medias?.[0]?.id || ""
|
|
25
|
+
);
|
|
26
|
+
const [videoUrl, setVideoUrl] = useState<string>("");
|
|
27
|
+
const [thumbnailUrl, setThumbnailUrl] = useState<string>("");
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setSelectedId(
|
|
23
31
|
value?.externalVideoUrl ? "video" : value?.medias?.[0]?.id || ""
|
|
24
32
|
);
|
|
25
|
-
|
|
26
|
-
const [thumbnailUrl, setThumbnailUrl] = useState<string>("");
|
|
33
|
+
}, [value]);
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (value?.externalVideoUrl) {
|
|
37
|
+
const baseUrl = value.externalVideoUrl.replace(
|
|
38
|
+
"https://youtu.be/",
|
|
39
|
+
"https://www.youtube.com/embed/"
|
|
31
40
|
);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
41
|
-
setVideoUrl(
|
|
42
|
-
`${baseUrl}${separator}autoplay=1&muted=1&modestbranding=1&rel=0&controls=1&playsinline=1&enablejsapi=1&origin=${encodeURIComponent(
|
|
43
|
-
window.location.origin
|
|
44
|
-
)}`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}, [value?.externalVideoUrl]);
|
|
41
|
+
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
42
|
+
setVideoUrl(
|
|
43
|
+
`${baseUrl}${separator}autoplay=1&muted=1&modestbranding=1&rel=0&controls=1&playsinline=1&enablejsapi=1&origin=${encodeURIComponent(
|
|
44
|
+
window.location.origin
|
|
45
|
+
)}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}, [value?.externalVideoUrl]);
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (value?.externalVideoUrl) {
|
|
52
|
+
const videoId = value.externalVideoUrl.includes("youtu.be/")
|
|
53
|
+
? value.externalVideoUrl.split("youtu.be/")[1].split("?")[0]
|
|
54
|
+
: value.externalVideoUrl.split("v=")[1]?.split("&")[0];
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`https://img.youtube.com/vi/${videoId}/hqdefault.jpg`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
56
|
+
if (videoId) {
|
|
57
|
+
setThumbnailUrl(`https://img.youtube.com/vi/${videoId}/hqdefault.jpg`);
|
|
60
58
|
}
|
|
61
|
-
}
|
|
59
|
+
}
|
|
60
|
+
}, [value?.externalVideoUrl]);
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
: value?.medias?.findIndex((media) => media.id === selectedId) || 0;
|
|
62
|
+
const selectedIndex = value?.externalVideoUrl
|
|
63
|
+
? selectedId === "video"
|
|
64
|
+
? 0
|
|
65
|
+
: (value?.medias?.findIndex((media) => media.id === selectedId) || 0) + 1
|
|
66
|
+
: value?.medias?.findIndex((media) => media.id === selectedId) || 0;
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
const totalItems =
|
|
69
|
+
(value?.externalVideoUrl ? 1 : 0) + (value?.medias?.length || 0);
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
// Calculate visible thumbnails (show 6 items)
|
|
72
|
+
const visibleCount = 6;
|
|
73
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
74
|
+
const startIndex = Math.max(
|
|
75
|
+
0,
|
|
76
|
+
Math.min(selectedIndex - halfVisible, totalItems - visibleCount)
|
|
77
|
+
);
|
|
78
|
+
const endIndex = Math.min(startIndex + visibleCount, totalItems);
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} else {
|
|
88
|
-
const prevIndex = selectedIndex - 2;
|
|
89
|
-
setSelectedId(value.medias?.[prevIndex]?.id || "");
|
|
90
|
-
}
|
|
80
|
+
const handlePrevious = useCallback(() => {
|
|
81
|
+
if (selectedIndex > 0) {
|
|
82
|
+
if (value?.externalVideoUrl) {
|
|
83
|
+
if (selectedIndex === 1) {
|
|
84
|
+
setSelectedId("video");
|
|
91
85
|
} else {
|
|
92
|
-
const prevIndex = selectedIndex -
|
|
93
|
-
setSelectedId(value
|
|
86
|
+
const prevIndex = selectedIndex - 2;
|
|
87
|
+
setSelectedId(value.medias?.[prevIndex]?.id || "");
|
|
94
88
|
}
|
|
89
|
+
} else {
|
|
90
|
+
const prevIndex = selectedIndex - 1;
|
|
91
|
+
setSelectedId(value?.medias?.[prevIndex]?.id || "");
|
|
95
92
|
}
|
|
96
|
-
}
|
|
93
|
+
}
|
|
94
|
+
}, [selectedIndex, value]);
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} else {
|
|
104
|
-
const currentMediaIndex =
|
|
105
|
-
value.medias?.findIndex((media) => media.id === selectedId) || 0;
|
|
106
|
-
const nextIndex = currentMediaIndex + 1;
|
|
107
|
-
setSelectedId(value.medias?.[nextIndex]?.id || "");
|
|
108
|
-
}
|
|
96
|
+
const handleNext = useCallback(() => {
|
|
97
|
+
if (selectedIndex < totalItems - 1) {
|
|
98
|
+
if (value?.externalVideoUrl) {
|
|
99
|
+
if (selectedId === "video") {
|
|
100
|
+
setSelectedId(value.medias?.[0]?.id || "");
|
|
109
101
|
} else {
|
|
110
102
|
const currentMediaIndex =
|
|
111
|
-
value
|
|
103
|
+
value.medias?.findIndex((media) => media.id === selectedId) || 0;
|
|
112
104
|
const nextIndex = currentMediaIndex + 1;
|
|
113
|
-
setSelectedId(value
|
|
105
|
+
setSelectedId(value.medias?.[nextIndex]?.id || "");
|
|
114
106
|
}
|
|
107
|
+
} else {
|
|
108
|
+
const currentMediaIndex =
|
|
109
|
+
value?.medias?.findIndex((media) => media.id === selectedId) || 0;
|
|
110
|
+
const nextIndex = currentMediaIndex + 1;
|
|
111
|
+
setSelectedId(value?.medias?.[nextIndex]?.id || "");
|
|
115
112
|
}
|
|
116
|
-
}
|
|
113
|
+
}
|
|
114
|
+
}, [selectedIndex, totalItems, value, selectedId]);
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
const handleKeyDown = useCallback(
|
|
117
|
+
(e: KeyboardEvent) => {
|
|
118
|
+
if (e.key === "ArrowLeft") {
|
|
119
|
+
handlePrevious();
|
|
120
|
+
} else if (e.key === "ArrowRight") {
|
|
121
|
+
handleNext();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[handleNext, handlePrevious]
|
|
125
|
+
);
|
|
128
126
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
129
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
130
|
+
}, [handleKeyDown]);
|
|
133
131
|
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
const canPrevious = selectedIndex > 0;
|
|
133
|
+
const canNext = selectedIndex < totalItems - 1;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div ref={ref} className={clsx("flex flex-col", className)} {...rest}>
|
|
137
|
+
{children}
|
|
138
|
+
{/* Main display area */}
|
|
139
|
+
<div className={clsx("relative group")}>
|
|
140
|
+
{canPrevious && (
|
|
141
|
+
<button
|
|
142
|
+
onClick={handlePrevious}
|
|
143
|
+
className="absolute left-4 top-1/2 transform -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
|
144
|
+
aria-label="Previous slide"
|
|
145
|
+
>
|
|
146
|
+
<svg
|
|
147
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
148
|
+
className="h-6 w-6"
|
|
149
|
+
fill="none"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
stroke="currentColor"
|
|
152
|
+
>
|
|
153
|
+
<path
|
|
154
|
+
strokeLinecap="round"
|
|
155
|
+
strokeLinejoin="round"
|
|
156
|
+
strokeWidth={2}
|
|
157
|
+
d="M15 19l-7-7 7-7"
|
|
158
|
+
/>
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
{canNext && (
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleNext}
|
|
165
|
+
className="absolute right-4 top-1/2 transform -translate-y-1/2 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
|
|
166
|
+
aria-label="Next slide"
|
|
167
|
+
>
|
|
168
|
+
<svg
|
|
169
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
170
|
+
className="h-6 w-6"
|
|
171
|
+
fill="none"
|
|
172
|
+
viewBox="0 0 24 24"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
>
|
|
175
|
+
<path
|
|
176
|
+
strokeLinecap="round"
|
|
177
|
+
strokeLinejoin="round"
|
|
178
|
+
strokeWidth={2}
|
|
179
|
+
d="M9 5l7 7-7 7"
|
|
180
|
+
/>
|
|
181
|
+
</svg>
|
|
182
|
+
</button>
|
|
183
|
+
)}
|
|
184
|
+
{value?.externalVideoUrl && selectedId === "video" && (
|
|
185
|
+
<div className={clsx("w-full rounded-md overflow-hidden", aspect)}>
|
|
186
|
+
<iframe
|
|
187
|
+
src={videoUrl}
|
|
188
|
+
className="w-full h-full"
|
|
189
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; autoplay"
|
|
190
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
191
|
+
allowFullScreen
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
{value?.medias?.map((media) => (
|
|
196
|
+
<div
|
|
197
|
+
key={media.id}
|
|
198
|
+
className={clsx(
|
|
199
|
+
"transition-opacity duration-300 overflow-hidden rounded-md",
|
|
200
|
+
media.id === selectedId ? "opacity-100" : "opacity-0 hidden",
|
|
201
|
+
aspect
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
<img
|
|
205
|
+
src={media?.resize || media?.url}
|
|
206
|
+
alt={media?.alt}
|
|
207
|
+
className="w-full h-full object-cover object-center"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
136
212
|
|
|
137
|
-
|
|
138
|
-
<div
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<div className={clsx("relative group")}>
|
|
142
|
-
{canPrevious && (
|
|
213
|
+
{/* Thumbnail navigation */}
|
|
214
|
+
<div className="relative mt-4">
|
|
215
|
+
<div className="flex items-stretch gap-2">
|
|
216
|
+
{totalItems > 6 && (
|
|
143
217
|
<button
|
|
144
218
|
onClick={handlePrevious}
|
|
145
|
-
|
|
146
|
-
|
|
219
|
+
disabled={!canPrevious}
|
|
220
|
+
className={clsx(
|
|
221
|
+
"flex items-center justify-center w-6",
|
|
222
|
+
"transition-colors duration-200 rounded-l-md",
|
|
223
|
+
!canPrevious
|
|
224
|
+
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
225
|
+
: "bg-gray-200 hover:bg-gray-300 text-gray-700"
|
|
226
|
+
)}
|
|
227
|
+
aria-label="Previous"
|
|
147
228
|
>
|
|
148
229
|
<svg
|
|
149
230
|
xmlns="http://www.w3.org/2000/svg"
|
|
150
|
-
className="h-
|
|
231
|
+
className="h-4 w-4 md:h-5 md:w-5"
|
|
151
232
|
fill="none"
|
|
152
233
|
viewBox="0 0 24 24"
|
|
153
234
|
stroke="currentColor"
|
|
@@ -161,15 +242,88 @@ export const Medias = forwardRef<HTMLDivElement, MediasProps>(
|
|
|
161
242
|
</svg>
|
|
162
243
|
</button>
|
|
163
244
|
)}
|
|
164
|
-
|
|
245
|
+
<div className="flex-1">
|
|
246
|
+
<div className="grid grid-cols-6 gap-2">
|
|
247
|
+
{value?.externalVideoUrl && startIndex === 0 && (
|
|
248
|
+
<div
|
|
249
|
+
className={clsx(
|
|
250
|
+
"relative cursor-pointer overflow-hidden rounded-sm border-2",
|
|
251
|
+
thumbnailAspect,
|
|
252
|
+
selectedId === "video"
|
|
253
|
+
? "border-primary-500"
|
|
254
|
+
: "border-transparent hover:border-primary-300"
|
|
255
|
+
)}
|
|
256
|
+
onClick={() => setSelectedId("video")}
|
|
257
|
+
>
|
|
258
|
+
<img
|
|
259
|
+
src={thumbnailUrl}
|
|
260
|
+
alt="Video thumbnail"
|
|
261
|
+
className="w-full h-full object-cover"
|
|
262
|
+
/>
|
|
263
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
264
|
+
<svg
|
|
265
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
266
|
+
className="h-8 w-8 text-white"
|
|
267
|
+
viewBox="0 0 24 24"
|
|
268
|
+
>
|
|
269
|
+
<path
|
|
270
|
+
fill="currentColor"
|
|
271
|
+
fill-rule="evenodd"
|
|
272
|
+
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10"
|
|
273
|
+
clipRule="evenodd"
|
|
274
|
+
opacity="0.5"
|
|
275
|
+
/>
|
|
276
|
+
<path
|
|
277
|
+
fill="currentColor"
|
|
278
|
+
d="m15.414 13.059l-4.72 2.787C9.934 16.294 9 15.71 9 14.786V9.214c0-.924.934-1.507 1.694-1.059l4.72 2.787c.781.462.781 1.656 0 2.118"
|
|
279
|
+
/>
|
|
280
|
+
</svg>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
{value?.medias?.map((media, index) => {
|
|
285
|
+
const adjustedIndex = value?.externalVideoUrl
|
|
286
|
+
? index + 1
|
|
287
|
+
: index;
|
|
288
|
+
return adjustedIndex >= startIndex &&
|
|
289
|
+
adjustedIndex < endIndex ? (
|
|
290
|
+
<div
|
|
291
|
+
key={media.id}
|
|
292
|
+
className={clsx(
|
|
293
|
+
"relative cursor-pointer overflow-hidden rounded-sm border-2",
|
|
294
|
+
thumbnailAspect,
|
|
295
|
+
selectedId === media.id
|
|
296
|
+
? "border-primary-500"
|
|
297
|
+
: "border-transparent hover:border-primary-300"
|
|
298
|
+
)}
|
|
299
|
+
onClick={() => setSelectedId(media.id)}
|
|
300
|
+
>
|
|
301
|
+
<img
|
|
302
|
+
src={media?.resize || media?.url}
|
|
303
|
+
alt={media?.alt}
|
|
304
|
+
className="w-full h-full object-cover"
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
) : null;
|
|
308
|
+
})}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
{totalItems > 6 && (
|
|
165
312
|
<button
|
|
166
313
|
onClick={handleNext}
|
|
167
|
-
|
|
168
|
-
|
|
314
|
+
disabled={!canNext}
|
|
315
|
+
className={clsx(
|
|
316
|
+
"flex items-center justify-center w-6",
|
|
317
|
+
"transition-colors duration-200 rounded-r-md",
|
|
318
|
+
!canNext
|
|
319
|
+
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
320
|
+
: "bg-gray-200 hover:bg-gray-300 text-gray-700"
|
|
321
|
+
)}
|
|
322
|
+
aria-label="Next"
|
|
169
323
|
>
|
|
170
324
|
<svg
|
|
171
325
|
xmlns="http://www.w3.org/2000/svg"
|
|
172
|
-
className="h-
|
|
326
|
+
className="h-4 w-4 md:h-5 md:w-5"
|
|
173
327
|
fill="none"
|
|
174
328
|
viewBox="0 0 24 24"
|
|
175
329
|
stroke="currentColor"
|
|
@@ -183,165 +337,8 @@ export const Medias = forwardRef<HTMLDivElement, MediasProps>(
|
|
|
183
337
|
</svg>
|
|
184
338
|
</button>
|
|
185
339
|
)}
|
|
186
|
-
{value?.externalVideoUrl && selectedId === "video" && (
|
|
187
|
-
<div className={clsx("w-full rounded-md overflow-hidden", aspect)}>
|
|
188
|
-
<iframe
|
|
189
|
-
src={videoUrl}
|
|
190
|
-
className="w-full h-full"
|
|
191
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; autoplay"
|
|
192
|
-
referrerPolicy="strict-origin-when-cross-origin"
|
|
193
|
-
allowFullScreen
|
|
194
|
-
/>
|
|
195
|
-
</div>
|
|
196
|
-
)}
|
|
197
|
-
{value?.medias?.map((media) => (
|
|
198
|
-
<div
|
|
199
|
-
key={media.id}
|
|
200
|
-
className={clsx(
|
|
201
|
-
"transition-opacity duration-300 overflow-hidden rounded-md",
|
|
202
|
-
media.id === selectedId ? "opacity-100" : "opacity-0 hidden",
|
|
203
|
-
aspect
|
|
204
|
-
)}
|
|
205
|
-
>
|
|
206
|
-
<img
|
|
207
|
-
src={media?.resize || media?.url}
|
|
208
|
-
alt={media?.alt}
|
|
209
|
-
className="w-full h-full object-cover object-center"
|
|
210
|
-
/>
|
|
211
|
-
</div>
|
|
212
|
-
))}
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{/* Thumbnail navigation */}
|
|
216
|
-
<div className="relative mt-4">
|
|
217
|
-
<div className="flex items-stretch gap-2">
|
|
218
|
-
{totalItems > 6 && (
|
|
219
|
-
<button
|
|
220
|
-
onClick={handlePrevious}
|
|
221
|
-
disabled={!canPrevious}
|
|
222
|
-
className={clsx(
|
|
223
|
-
"flex items-center justify-center w-6",
|
|
224
|
-
"transition-colors duration-200 rounded-l-md",
|
|
225
|
-
!canPrevious
|
|
226
|
-
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
227
|
-
: "bg-gray-200 hover:bg-gray-300 text-gray-700"
|
|
228
|
-
)}
|
|
229
|
-
aria-label="Previous"
|
|
230
|
-
>
|
|
231
|
-
<svg
|
|
232
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
233
|
-
className="h-4 w-4 md:h-5 md:w-5"
|
|
234
|
-
fill="none"
|
|
235
|
-
viewBox="0 0 24 24"
|
|
236
|
-
stroke="currentColor"
|
|
237
|
-
>
|
|
238
|
-
<path
|
|
239
|
-
strokeLinecap="round"
|
|
240
|
-
strokeLinejoin="round"
|
|
241
|
-
strokeWidth={2}
|
|
242
|
-
d="M15 19l-7-7 7-7"
|
|
243
|
-
/>
|
|
244
|
-
</svg>
|
|
245
|
-
</button>
|
|
246
|
-
)}
|
|
247
|
-
<div className="flex-1">
|
|
248
|
-
<div className="grid grid-cols-6 gap-2">
|
|
249
|
-
{value?.externalVideoUrl && startIndex === 0 && (
|
|
250
|
-
<div
|
|
251
|
-
className={clsx(
|
|
252
|
-
"relative cursor-pointer overflow-hidden rounded-sm border-2",
|
|
253
|
-
aspect,
|
|
254
|
-
selectedId === "video"
|
|
255
|
-
? "border-primary-500"
|
|
256
|
-
: "border-transparent hover:border-primary-300"
|
|
257
|
-
)}
|
|
258
|
-
onClick={() => setSelectedId("video")}
|
|
259
|
-
>
|
|
260
|
-
<img
|
|
261
|
-
src={thumbnailUrl}
|
|
262
|
-
alt="Video thumbnail"
|
|
263
|
-
className="w-full h-full object-cover"
|
|
264
|
-
/>
|
|
265
|
-
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
266
|
-
<svg
|
|
267
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
268
|
-
className="h-8 w-8 text-white"
|
|
269
|
-
viewBox="0 0 24 24"
|
|
270
|
-
>
|
|
271
|
-
<path
|
|
272
|
-
fill="currentColor"
|
|
273
|
-
fill-rule="evenodd"
|
|
274
|
-
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10"
|
|
275
|
-
clipRule="evenodd"
|
|
276
|
-
opacity="0.5"
|
|
277
|
-
/>
|
|
278
|
-
<path
|
|
279
|
-
fill="currentColor"
|
|
280
|
-
d="m15.414 13.059l-4.72 2.787C9.934 16.294 9 15.71 9 14.786V9.214c0-.924.934-1.507 1.694-1.059l4.72 2.787c.781.462.781 1.656 0 2.118"
|
|
281
|
-
/>
|
|
282
|
-
</svg>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
)}
|
|
286
|
-
{value?.medias?.map((media, index) => {
|
|
287
|
-
const adjustedIndex = value?.externalVideoUrl
|
|
288
|
-
? index + 1
|
|
289
|
-
: index;
|
|
290
|
-
return adjustedIndex >= startIndex &&
|
|
291
|
-
adjustedIndex < endIndex ? (
|
|
292
|
-
<div
|
|
293
|
-
key={media.id}
|
|
294
|
-
className={clsx(
|
|
295
|
-
"relative cursor-pointer overflow-hidden rounded-sm border-2",
|
|
296
|
-
aspect,
|
|
297
|
-
selectedId === media.id
|
|
298
|
-
? "border-primary-500"
|
|
299
|
-
: "border-transparent hover:border-primary-300"
|
|
300
|
-
)}
|
|
301
|
-
onClick={() => setSelectedId(media.id)}
|
|
302
|
-
>
|
|
303
|
-
<img
|
|
304
|
-
src={media?.resize || media?.url}
|
|
305
|
-
alt={media?.alt}
|
|
306
|
-
className="w-full h-full object-cover"
|
|
307
|
-
/>
|
|
308
|
-
</div>
|
|
309
|
-
) : null;
|
|
310
|
-
})}
|
|
311
|
-
</div>
|
|
312
|
-
</div>
|
|
313
|
-
{totalItems > 6 && (
|
|
314
|
-
<button
|
|
315
|
-
onClick={handleNext}
|
|
316
|
-
disabled={!canNext}
|
|
317
|
-
className={clsx(
|
|
318
|
-
"flex items-center justify-center w-6",
|
|
319
|
-
"transition-colors duration-200 rounded-r-md",
|
|
320
|
-
!canNext
|
|
321
|
-
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
322
|
-
: "bg-gray-200 hover:bg-gray-300 text-gray-700"
|
|
323
|
-
)}
|
|
324
|
-
aria-label="Next"
|
|
325
|
-
>
|
|
326
|
-
<svg
|
|
327
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
328
|
-
className="h-4 w-4 md:h-5 md:w-5"
|
|
329
|
-
fill="none"
|
|
330
|
-
viewBox="0 0 24 24"
|
|
331
|
-
stroke="currentColor"
|
|
332
|
-
>
|
|
333
|
-
<path
|
|
334
|
-
strokeLinecap="round"
|
|
335
|
-
strokeLinejoin="round"
|
|
336
|
-
strokeWidth={2}
|
|
337
|
-
d="M9 5l7 7-7 7"
|
|
338
|
-
/>
|
|
339
|
-
</svg>
|
|
340
|
-
</button>
|
|
341
|
-
)}
|
|
342
|
-
</div>
|
|
343
340
|
</div>
|
|
344
341
|
</div>
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
);
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractOutline, mdxToSlate } from "@rxdrag/slate-preview";
|
|
2
|
-
import { forwardRef,
|
|
2
|
+
import { forwardRef, useEffect } from "react";
|
|
3
3
|
import { useAcitviedHeading } from "./useAcitviedHeading";
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
|
|
@@ -16,8 +16,8 @@ export const RichTextOutline = forwardRef<
|
|
|
16
16
|
HTMLUListElement,
|
|
17
17
|
RichTextOutlineProps
|
|
18
18
|
>((props, ref) => {
|
|
19
|
-
const { className, itemClassName, value, yOffset =
|
|
20
|
-
const activiedId = useAcitviedHeading();
|
|
19
|
+
const { className, itemClassName, value, yOffset = 200, ...rest } = props;
|
|
20
|
+
const activiedId = useAcitviedHeading(yOffset);
|
|
21
21
|
const nodes = mdxToSlate(value ?? "");
|
|
22
22
|
const outline = extractOutline(nodes ?? []);
|
|
23
23
|
|
|
@@ -38,7 +38,7 @@ export const RichTextOutline = forwardRef<
|
|
|
38
38
|
return () => {
|
|
39
39
|
window.removeEventListener("hashchange", handleHashChange);
|
|
40
40
|
};
|
|
41
|
-
}, []);
|
|
41
|
+
}, [yOffset]);
|
|
42
42
|
|
|
43
43
|
return outline?.length ? (
|
|
44
44
|
<ul ref={ref} className={className} {...rest}>
|
|
@@ -57,7 +57,6 @@ export const RichTextOutline = forwardRef<
|
|
|
57
57
|
e.preventDefault();
|
|
58
58
|
const element = document.getElementById(item?.key);
|
|
59
59
|
if (element) {
|
|
60
|
-
const yOffset = -100; // 偏移量
|
|
61
60
|
const y =
|
|
62
61
|
element.getBoundingClientRect().top +
|
|
63
62
|
window.scrollY +
|
|
@@ -1,54 +1,81 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
|
|
2
|
+
import { throttle } from "lodash-es";
|
|
3
|
+
|
|
4
|
+
export function useAcitviedHeading(yOffset = 200) {
|
|
5
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
6
|
+
const anchorElementsRef = useRef<HTMLAnchorElement[]>([]);
|
|
7
|
+
const lastScrollTopRef = useRef(0);
|
|
8
|
+
const activeIdRef = useRef<string | null>(null);
|
|
9
|
+
|
|
10
|
+
// 同步 activeId 到 ref
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
activeIdRef.current = activeId;
|
|
13
|
+
}, [activeId]);
|
|
14
|
+
|
|
15
|
+
// 初始化时获取所有锚点元素
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
anchorElementsRef.current = Array.from(
|
|
18
|
+
document.querySelectorAll('a[href^="#"]')
|
|
19
|
+
);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const handleScroll = useCallback(() => {
|
|
23
|
+
const scrollTop = window.scrollY;
|
|
24
|
+
lastScrollTopRef.current = scrollTop;
|
|
25
|
+
|
|
26
|
+
let closestId = null;
|
|
27
|
+
let closestDistance = Infinity;
|
|
28
|
+
|
|
29
|
+
anchorElementsRef.current.forEach((element) => {
|
|
30
|
+
const id = element.getAttribute("href")?.slice(1) || "";
|
|
31
|
+
const targetElement = document.getElementById(id);
|
|
32
|
+
|
|
33
|
+
if (targetElement) {
|
|
34
|
+
const { top } = targetElement.getBoundingClientRect();
|
|
35
|
+
const distance = Math.abs(top);
|
|
36
|
+
if (top <= yOffset && distance < closestDistance) {
|
|
37
|
+
closestId = id;
|
|
38
|
+
closestDistance = distance;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (activeIdRef.current !== closestId) {
|
|
44
|
+
setActiveId(closestId);
|
|
45
|
+
}
|
|
46
|
+
}, [yOffset]); // 移除 activeId 依赖,因为我们只需要比较当前值
|
|
47
|
+
|
|
48
|
+
// 使用节流函数包装handleScroll
|
|
49
|
+
const throttledHandleScroll = useMemo(
|
|
50
|
+
() => throttle(handleScroll, 100, { leading: true, trailing: true }),
|
|
51
|
+
[handleScroll]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const handleHashChange = () => {
|
|
56
|
+
const hash = window.location.hash.substring(1);
|
|
57
|
+
setActiveId(hash);
|
|
58
|
+
|
|
59
|
+
const element = document.getElementById(hash);
|
|
60
|
+
if (element) {
|
|
61
|
+
const y =
|
|
62
|
+
element.getBoundingClientRect().top + window.scrollY - yOffset;
|
|
63
|
+
window.scrollTo({ top: y, behavior: "smooth" });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
window.addEventListener("hashchange", handleHashChange);
|
|
68
|
+
window.addEventListener("scroll", throttledHandleScroll);
|
|
69
|
+
|
|
70
|
+
// 初始化时检查当前哈希值
|
|
71
|
+
handleHashChange();
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
window.removeEventListener("hashchange", handleHashChange);
|
|
75
|
+
window.removeEventListener("scroll", throttledHandleScroll);
|
|
76
|
+
throttledHandleScroll.cancel(); // 清理节流函数
|
|
77
|
+
};
|
|
78
|
+
}, [handleScroll, throttledHandleScroll, yOffset]);
|
|
79
|
+
|
|
80
|
+
return activeId;
|
|
81
|
+
}
|