@coffic/cosy-ui 0.3.39 → 0.3.45

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.
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  /**
3
3
  * @component TableOfContents
4
- *
4
+ *
5
5
  * @description
6
6
  * TableOfContents 组件是一个目录导航组件,用于显示页面内容的标题结构。
7
7
  * 它会自动检测页面中的标题元素,生成目录列表,并在用户滚动页面时高亮当前可见的标题。
8
8
  * 当页面只有一个标题或没有足够的标题结构时,组件会自动隐藏。
9
- *
9
+ *
10
10
  * @design
11
11
  * 设计理念:
12
12
  * 1. 导航辅助 - 帮助用户快速了解页面结构和导航到特定内容
@@ -15,37 +15,37 @@
15
15
  * 4. 响应式设计 - 在小屏幕设备上自动隐藏,优化空间利用
16
16
  * 5. 智能显示 - 当页面结构不需要目录时自动隐藏
17
17
  * 6. 多语言支持 - 支持多种语言显示,自动检测当前语言环境
18
- *
18
+ *
19
19
  * @usage
20
20
  * 基本用法:
21
21
  * ```astro
22
22
  * <TableOfContents />
23
23
  * ```
24
- *
24
+ *
25
25
  * 自定义标题和选择器:
26
26
  * ```astro
27
- * <TableOfContents
28
- * title="章节导航"
29
- * selector="h2, h3, h4"
30
- * maxDepth={4}
27
+ * <TableOfContents
28
+ * title="章节导航"
29
+ * selector="h2, h3, h4"
30
+ * maxDepth={4}
31
31
  * />
32
32
  * ```
33
- *
33
+ *
34
34
  * 非固定位置:
35
35
  * ```astro
36
36
  * <TableOfContents fixed={false} />
37
37
  * ```
38
- *
38
+ *
39
39
  * 指定内容容器:
40
40
  * ```astro
41
41
  * <TableOfContents containerSelector=".article-content" />
42
42
  * ```
43
- *
43
+ *
44
44
  * 指定语言:
45
45
  * ```astro
46
46
  * <TableOfContents lang="zh-cn" />
47
47
  * ```
48
- *
48
+ *
49
49
  * 启用日志:
50
50
  * ```astro
51
51
  * <TableOfContents enableLogging={true} />
@@ -58,56 +58,56 @@ import { getCurrentLanguage } from '../../utils/language';
58
58
  import { createTextGetter } from '../../utils/i18n';
59
59
 
60
60
  interface Props {
61
- /**
62
- * 是否固定在右侧
63
- * @default true
64
- */
65
- fixed?: boolean;
66
- /**
67
- * 自定义类名
68
- */
69
- class?: string;
70
- /**
71
- * 标题选择器
72
- * @default "h2, h3"
73
- */
74
- selector?: string;
75
- /**
76
- * 最大深度
77
- * @default 3
78
- */
79
- maxDepth?: number;
80
- /**
81
- * 标题文本
82
- * @default 根据语言自动选择
83
- */
84
- title?: string;
85
- /**
86
- * 内容容器选择器,用于限制标题搜索范围
87
- * @default "main"
88
- */
89
- containerSelector?: string;
90
- /**
91
- * 显示目录所需的最少标题数量
92
- * @default 2
93
- */
94
- minHeadings?: number;
95
- /**
96
- * 语言
97
- * @default 自动检测
98
- */
99
- lang?: string;
61
+ /**
62
+ * 是否固定在右侧
63
+ * @default true
64
+ */
65
+ fixed?: boolean;
66
+ /**
67
+ * 自定义类名
68
+ */
69
+ class?: string;
70
+ /**
71
+ * 标题选择器
72
+ * @default "h2, h3"
73
+ */
74
+ selector?: string;
75
+ /**
76
+ * 最大深度
77
+ * @default 3
78
+ */
79
+ maxDepth?: number;
80
+ /**
81
+ * 标题文本
82
+ * @default 根据语言自动选择
83
+ */
84
+ title?: string;
85
+ /**
86
+ * 内容容器选择器,用于限制标题搜索范围
87
+ * @default "main"
88
+ */
89
+ containerSelector?: string;
90
+ /**
91
+ * 显示目录所需的最少标题数量
92
+ * @default 2
93
+ */
94
+ minHeadings?: number;
95
+ /**
96
+ * 语言
97
+ * @default 自动检测
98
+ */
99
+ lang?: string;
100
100
  }
101
101
 
102
102
  const {
103
- fixed = true,
104
- class: className = '',
105
- selector = 'h2, h3',
106
- maxDepth = 3,
107
- containerSelector = 'main',
108
- minHeadings = 2,
109
- lang: userLang,
110
- title,
103
+ fixed = true,
104
+ class: className = '',
105
+ selector = 'h2, h3',
106
+ maxDepth = 3,
107
+ containerSelector = 'main',
108
+ minHeadings = 2,
109
+ lang: userLang,
110
+ title,
111
111
  } = Astro.props;
112
112
 
113
113
  // 获取当前语言
@@ -121,268 +121,281 @@ const titleText = title || t('title');
121
121
  const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
122
122
  ---
123
123
 
124
- <aside class:list={[
125
- 'cosy:hidden cosy:xl:block cosy:w-64 cosy:shrink-0',
126
- className
127
- ]}>
128
- <div class="cosy:top-16 cosy:sticky">
129
- <div class={`toc-container ${fixed ? 'cosy:w-64' : 'cosy:w-full cosy:max-w-xs'}`} id={`${tocId}-container`} style="display: none;">
130
- <div class="cosy:bg-base-100 cosy:shadow-xl cosy:card">
131
- <div class="cosy:p-4 cosy:card-body">
132
- <div class="cosy:mb-2 cosy:font-bold cosy:text-lg cosy:card-title">{titleText}</div>
133
- <ul class="cosy:menu cosy:menu-sm" id={tocId}>
134
- <!-- 目录项将通过 JavaScript 动态生成 -->
135
- <li class="cosy:text-base-content/60">{t('loading')}</li>
136
- </ul>
137
- </div>
138
- </div>
139
- </div>
140
- </div>
124
+ <aside class:list={['cosy:hidden cosy:xl:block cosy:w-64 cosy:shrink-0', className]}>
125
+ <div class="cosy:top-18 cosy:sticky">
126
+ <div
127
+ class={`toc-container toc-scroll-container ${fixed ? 'cosy:w-64' : 'cosy:w-full cosy:max-w-xs'}`}
128
+ id={`${tocId}-container`}
129
+ style="display: none;">
130
+ <div class="cosy:bg-base-100 cosy:shadow-inner cosy:card">
131
+ <div class="cosy:p-4 cosy:card-body">
132
+ <div class="cosy:mb-2 cosy:font-bold cosy:text-lg cosy:card-title">{titleText}</div>
133
+ <ul class="cosy:menu cosy:menu-sm" id={tocId}>
134
+ <!-- 目录项将通过 JavaScript 动态生成 -->
135
+ <li class="cosy:text-base-content/60">{t('loading')}</li>
136
+ </ul>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
141
  </aside>
142
142
 
143
143
  <script define:vars={{ selector, maxDepth, tocId, containerSelector, minHeadings, langInfo }}>
144
- // 在页面加载完成后生成目录
145
- function generateTOC() {
146
- // 使用指定的容器选择器查找内容容器
147
- const container = document.querySelector(containerSelector);
148
- if (!container) return;
149
-
150
- // 排除 CodeExample 组件中的标题
151
- const headings = Array.from(container.querySelectorAll(selector)).filter(heading => {
152
- // 检查标题是否在 CodeExample 组件内
153
- const isInCodeExample = heading.closest('.code-example') !== null;
154
- return !isInCodeExample;
155
- });
156
-
157
- const tocList = document.getElementById(tocId);
158
- const tocContainer = document.getElementById(`${tocId}-container`);
159
-
160
- if (!tocList || !tocContainer) return;
161
-
162
- // 如果标题数量少于最小要求,保持隐藏状态并退出
163
- if (headings.length < minHeadings) {
164
- tocContainer.style.display = 'none';
165
- return;
166
- }
167
-
168
- // 显示目录容器
169
- tocContainer.style.display = 'block';
170
-
171
- // 清空占位符
172
- tocList.innerHTML = '';
173
-
174
- // 用于存储已使用的 ID,确保唯一性
175
- const usedIds = new Set();
176
-
177
- // 为每个标题创建目录项
178
- headings.forEach((heading) => {
179
- // 如果标题没有 ID 或 ID 为空,则生成一个
180
- if (!heading.id || heading.id.trim() === '') {
181
- // 从标题文本生成 ID
182
- let newId = generateIdFromText(heading.textContent || '');
183
-
184
- // 确保 ID 唯一
185
- let counter = 0;
186
- let uniqueId = newId;
187
- while (usedIds.has(uniqueId) || document.getElementById(uniqueId)) {
188
- counter++;
189
- uniqueId = `${newId}-${counter}`;
190
- }
191
-
192
- // 设置标题的 ID
193
- heading.id = uniqueId;
194
- }
195
-
196
- // 记录已使用的 ID
197
- usedIds.add(heading.id);
198
-
199
- const level = parseInt(heading.tagName.substring(1));
200
- if (level > maxDepth) return;
201
-
202
- const listItem = document.createElement('li');
203
- listItem.className = `toc-level-${level}`;
204
- listItem.dataset.headingId = heading.id;
205
-
206
- const link = document.createElement('a');
207
- link.href = `#${heading.id}`;
208
- link.textContent = heading.textContent;
209
- link.className = 'cosy:text-base-content/80 cosy:hover:text-primary cosy:transition-colors';
210
-
211
- // 添加缩进
212
- if (level === 3) {
213
- link.style.paddingLeft = '1.5rem';
214
- }
215
-
216
- listItem.appendChild(link);
217
- tocList.appendChild(listItem);
218
-
219
- // 添加点击事件,平滑滚动到目标位置
220
- link.addEventListener('click', (e) => {
221
- e.preventDefault();
222
- const targetId = link.getAttribute('href').substring(1);
223
- const targetElement = document.getElementById(targetId);
224
-
225
- if (targetElement) {
226
- // 滚动到目标位置,并添加一些偏移以避免被固定导航遮挡
227
- const offset = 80; // 可根据实际情况调整
228
- const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset;
229
-
230
- window.scrollTo({
231
- top: targetPosition,
232
- behavior: 'smooth'
233
- });
234
-
235
- // 更新 URL,但不跳转
236
- history.pushState(null, null, `#${targetId}`);
237
-
238
- // 添加高亮效果
239
- setTimeout(() => {
240
- highlightActiveHeading();
241
- }, 500);
242
- }
243
- });
244
- });
245
-
246
- // 如果没有找到标题或标题数量不足,隐藏目录
247
- if (tocList.children.length < minHeadings) {
248
- tocContainer.style.display = 'none';
249
- return;
250
- }
251
-
252
- // 添加活动标题高亮
253
- highlightActiveHeading();
254
-
255
- // 使用节流函数优化滚动事件
256
- window.addEventListener('scroll', throttle(highlightActiveHeading, 100));
257
- }
258
-
259
- // 从文本生成有效的 ID
260
- function generateIdFromText(text) {
261
- return text
262
- .trim()
263
- .toLowerCase()
264
- .replace(/[\s\n]+/g, '-') // 将空格和换行替换为连字符
265
- .replace(/[^\w\u4e00-\u9fa5-]/g, '') // 只保留字母、数字、中文和连字符
266
- .replace(/^-+|-+$/g, '') // 移除开头和结尾的连字符
267
- .replace(/-{2,}/g, '-') // 将多个连字符替换为单个
268
- || 'heading'; // 如果结果为空,则使用默认值
269
- }
270
-
271
- // 节流函数,限制函数调用频率
272
- function throttle(func, delay) {
273
- let lastCall = 0;
274
- return function(...args) {
275
- const now = new Date().getTime();
276
- if (now - lastCall < delay) {
277
- return;
278
- }
279
- lastCall = now;
280
- return func(...args);
281
- };
282
- }
283
-
284
- // 高亮当前可见的标题
285
- function highlightActiveHeading() {
286
- const tocContainer = document.getElementById(`${tocId}-container`);
287
- if (!tocContainer || tocContainer.style.display === 'none') return;
288
-
289
- const container = document.querySelector(containerSelector);
290
- if (!container) return;
291
-
292
- const headings = Array.from(container.querySelectorAll(selector));
293
- if (headings.length < minHeadings) return;
294
-
295
- // 获取视口高度和滚动位置
296
- const viewportHeight = window.innerHeight;
297
- const scrollTop = window.scrollY;
298
-
299
- // 计算视口中心位置
300
- const viewportMiddle = scrollTop + viewportHeight / 3;
301
-
302
- let activeHeadingId = null;
303
- let closestHeading = null;
304
- let closestDistance = Infinity;
305
-
306
- // 找到最接近视口中心的标题
307
- for (const heading of headings) {
308
- const headingTop = heading.getBoundingClientRect().top + scrollTop;
309
- const distance = Math.abs(headingTop - viewportMiddle);
310
-
311
- if (distance < closestDistance) {
312
- closestDistance = distance;
313
- closestHeading = heading;
314
- }
315
- }
316
-
317
- // 如果找到了最接近的标题,使用它的ID
318
- if (closestHeading) {
319
- activeHeadingId = closestHeading.id;
320
- } else if (headings.length > 0) {
321
- // 如果没有找到,使用第一个标题
322
- activeHeadingId = headings[0].id;
323
- }
324
-
325
- // 更新活动链接样式
326
- const tocLinks = document.querySelectorAll(`#${tocId} a`);
327
- let hasActiveLink = false;
328
-
329
- tocLinks.forEach(link => {
330
- const href = link.getAttribute('href').substring(1);
331
-
332
- // 清除所有高亮
333
- link.classList.remove('cosy:text-primary', 'cosy:font-medium');
334
-
335
- // 添加当前活动标题的高亮
336
- if (href === activeHeadingId) {
337
- link.classList.add('cosy:text-primary', 'cosy:font-medium');
338
- hasActiveLink = true;
339
-
340
- // 添加一个动画效果,使高亮更明显
341
- link.animate([
342
- { transform: 'translateX(-5px)' },
343
- { transform: 'translateX(0)' }
344
- ], {
345
- duration: 300,
346
- easing: 'ease-out'
347
- });
348
- }
349
- });
350
-
351
- // 如果没有找到活动链接,尝试高亮第一个标题
352
- if (!hasActiveLink && tocLinks.length > 0) {
353
- tocLinks[0].classList.add('cosy:text-primary', 'cosy:font-medium');
354
- }
355
-
356
- // 确保活动链接在视图中可见
357
- const activeLink = document.querySelector(`#${tocId} a.cosy:text-primary`);
358
- if (activeLink && tocList) {
359
- const tocScrollContainer = document.querySelector('.cosy:fixed');
360
- if (tocScrollContainer) {
361
- const linkTop = activeLink.offsetTop;
362
- const containerScrollTop = tocScrollContainer.scrollTop;
363
- const containerHeight = tocScrollContainer.clientHeight;
364
-
365
- // 如果链接不在可视区域内,滚动到可见位置
366
- if (linkTop < containerScrollTop || linkTop > containerScrollTop + containerHeight) {
367
- tocScrollContainer.scrollTop = linkTop - containerHeight / 2;
368
- }
369
- }
370
- }
371
- }
372
-
373
- // 页面加载时初始化
374
- document.addEventListener('astro:page-load', generateTOC);
375
- // 初始加载时也初始化
376
- generateTOC();
377
-
378
- // 添加resize事件监听,在窗口大小变化时重新计算高亮
379
- window.addEventListener('resize', throttle(() => {
380
- generateTOC(); // 重新生成TOC,以防窗口大小变化导致标题可见性变化
381
- highlightActiveHeading();
382
- }, 100));
383
-
384
- // 立即触发一次高亮计算
385
- setTimeout(generateTOC, 100);
386
- setTimeout(highlightActiveHeading, 500);
387
- </script>
144
+ // 在页面加载完成后生成目录
145
+ function generateTOC() {
146
+ // 使用指定的容器选择器查找内容容器
147
+ const container = document.querySelector(containerSelector);
148
+ if (!container) return;
149
+
150
+ // 排除 CodeExample 组件中的标题
151
+ const headings = Array.from(container.querySelectorAll(selector)).filter((heading) => {
152
+ // 检查标题是否在 CodeExample 组件内
153
+ const isInCodeExample = heading.closest('.code-example') !== null;
154
+ return !isInCodeExample;
155
+ });
156
+
157
+ const tocList = document.getElementById(tocId);
158
+ const tocContainer = document.getElementById(`${tocId}-container`);
159
+
160
+ if (!tocList || !tocContainer) return;
161
+
162
+ // 如果标题数量少于最小要求,保持隐藏状态并退出
163
+ if (headings.length < minHeadings) {
164
+ tocContainer.style.display = 'none';
165
+ return;
166
+ }
167
+
168
+ // 显示目录容器
169
+ tocContainer.style.display = 'block';
170
+
171
+ // 清空占位符
172
+ tocList.innerHTML = '';
173
+
174
+ // 用于存储已使用的 ID,确保唯一性
175
+ const usedIds = new Set();
176
+
177
+ // 为每个标题创建目录项
178
+ headings.forEach((heading) => {
179
+ // 如果标题没有 ID 或 ID 为空,则生成一个
180
+ if (!heading.id || heading.id.trim() === '') {
181
+ // 从标题文本生成 ID
182
+ let newId = generateIdFromText(heading.textContent || '');
183
+
184
+ // 确保 ID 唯一
185
+ let counter = 0;
186
+ let uniqueId = newId;
187
+ while (usedIds.has(uniqueId) || document.getElementById(uniqueId)) {
188
+ counter++;
189
+ uniqueId = `${newId}-${counter}`;
190
+ }
191
+
192
+ // 设置标题的 ID
193
+ heading.id = uniqueId;
194
+ }
195
+
196
+ // 记录已使用的 ID
197
+ usedIds.add(heading.id);
198
+
199
+ const level = parseInt(heading.tagName.substring(1));
200
+ if (level > maxDepth) return;
201
+
202
+ const listItem = document.createElement('li');
203
+ listItem.className = `toc-level-${level}`;
204
+ listItem.dataset.headingId = heading.id;
205
+
206
+ const link = document.createElement('a');
207
+ link.href = `#${heading.id}`;
208
+ link.textContent = heading.textContent;
209
+ link.className = 'cosy:text-base-content/80 cosy:hover:text-primary cosy:transition-colors';
210
+
211
+ // 添加缩进
212
+ if (level === 3) {
213
+ link.style.paddingLeft = '1.5rem';
214
+ }
215
+
216
+ listItem.appendChild(link);
217
+ tocList.appendChild(listItem);
218
+
219
+ // 添加点击事件,平滑滚动到目标位置
220
+ link.addEventListener('click', (e) => {
221
+ e.preventDefault();
222
+ const targetId = link.getAttribute('href').substring(1);
223
+ const targetElement = document.getElementById(targetId);
224
+
225
+ if (targetElement) {
226
+ // 滚动到目标位置,并添加一些偏移以避免被固定导航遮挡
227
+ const offset = 80; // 可根据实际情况调整
228
+ const targetPosition =
229
+ targetElement.getBoundingClientRect().top + window.scrollY - offset;
230
+
231
+ window.scrollTo({
232
+ top: targetPosition,
233
+ behavior: 'smooth',
234
+ });
235
+
236
+ // 更新 URL,但不跳转
237
+ history.pushState(null, null, `#${targetId}`);
238
+
239
+ // 添加高亮效果
240
+ setTimeout(() => {
241
+ highlightActiveHeading();
242
+ }, 500);
243
+ }
244
+ });
245
+ });
246
+
247
+ // 如果没有找到标题或标题数量不足,隐藏目录
248
+ if (tocList.children.length < minHeadings) {
249
+ tocContainer.style.display = 'none';
250
+ return;
251
+ }
252
+
253
+ // 添加活动标题高亮
254
+ highlightActiveHeading();
255
+
256
+ // 使用节流函数优化滚动事件
257
+ window.addEventListener('scroll', throttle(highlightActiveHeading, 200));
258
+ }
388
259
 
260
+ // 从文本生成有效的 ID
261
+ function generateIdFromText(text) {
262
+ return (
263
+ text
264
+ .trim()
265
+ .toLowerCase()
266
+ .replace(/[\s\n]+/g, '-') // 将空格和换行替换为连字符
267
+ .replace(/[^\w\u4e00-\u9fa5-]/g, '') // 只保留字母、数字、中文和连字符
268
+ .replace(/^-+|-+$/g, '') // 移除开头和结尾的连字符
269
+ .replace(/-{2,}/g, '-') || // 将多个连字符替换为单个
270
+ 'heading'
271
+ ); // 如果结果为空,则使用默认值
272
+ }
273
+
274
+ // 高亮当前可见的标题
275
+ function highlightActiveHeading() {
276
+ const tocContainer = document.getElementById(`${tocId}-container`);
277
+ if (!tocContainer || tocContainer.style.display === 'none') return;
278
+
279
+ const container = document.querySelector(containerSelector);
280
+ if (!container) return;
281
+
282
+ const headings = Array.from(container.querySelectorAll(selector));
283
+ if (headings.length < minHeadings) return;
284
+
285
+ // 获取视口高度和滚动位置
286
+ const viewportHeight = window.innerHeight;
287
+ const scrollTop = window.scrollY;
288
+
289
+ // 计算视口中心位置
290
+ const viewportMiddle = scrollTop + viewportHeight / 3;
291
+
292
+ let activeHeadingId = null;
293
+ let closestHeading = null;
294
+ let closestDistance = Infinity;
295
+
296
+ // 找到最接近视口中心的标题
297
+ for (const heading of headings) {
298
+ const headingTop = heading.getBoundingClientRect().top + scrollTop;
299
+ const distance = Math.abs(headingTop - viewportMiddle);
300
+
301
+ if (distance < closestDistance) {
302
+ closestDistance = distance;
303
+ closestHeading = heading;
304
+ }
305
+ }
306
+
307
+ // 如果找到了最接近的标题,使用它的ID
308
+ if (closestHeading) {
309
+ activeHeadingId = closestHeading.id;
310
+ } else if (headings.length > 0) {
311
+ // 如果没有找到,使用第一个标题
312
+ activeHeadingId = headings[0].id;
313
+ }
314
+
315
+ // 更新活动链接样式
316
+ const tocLinks = document.querySelectorAll(`#${tocId} a`);
317
+ let activeLink = document.querySelector(`#${tocId} a.active-toc-link`);
318
+ let currentActiveId = activeLink ? activeLink.getAttribute('href').substring(1) : null;
319
+
320
+ // 如果当前活动标题没有变化,避免不必要的样式更新
321
+ if (currentActiveId === activeHeadingId) {
322
+ return;
323
+ }
324
+
325
+ let hasActiveLink = false;
326
+
327
+ tocLinks.forEach((link) => {
328
+ const href = link.getAttribute('href').substring(1);
329
+
330
+ // 清除所有高亮
331
+ link.classList.remove('cosy:text-primary', 'cosy:font-medium', 'active-toc-link');
332
+
333
+ // 添加当前活动标题的高亮
334
+ if (href === activeHeadingId) {
335
+ link.classList.add('cosy:text-primary', 'cosy:font-medium', 'active-toc-link');
336
+ hasActiveLink = true;
337
+
338
+ // 只在首次高亮时添加一个轻微的过渡效果,而不是每次滚动都动画
339
+ if (href !== currentActiveId) {
340
+ link.style.transition = 'color 0.3s ease';
341
+ }
342
+ }
343
+ });
344
+
345
+ // 如果没有找到活动链接,尝试高亮第一个标题
346
+ if (!hasActiveLink && tocLinks.length > 0) {
347
+ tocLinks[0].classList.add('cosy:text-primary', 'cosy:font-medium', 'active-toc-link');
348
+ }
349
+
350
+ // 确保活动链接在视图中可见
351
+ const newActiveLink = document.querySelector(`#${tocId} a.active-toc-link`);
352
+ const tocList = document.getElementById(tocId);
353
+ if (newActiveLink && tocList) {
354
+ const tocScrollContainer = document.querySelector('.toc-scroll-container');
355
+ if (tocScrollContainer) {
356
+ const linkTop = newActiveLink.offsetTop;
357
+ const containerScrollTop = tocScrollContainer.scrollTop;
358
+ const containerHeight = tocScrollContainer.clientHeight;
359
+
360
+ // 如果链接不在可视区域内,平滑滚动到可见位置
361
+ if (linkTop < containerScrollTop || linkTop > containerScrollTop + containerHeight) {
362
+ tocScrollContainer.scrollTo({
363
+ top: linkTop - containerHeight / 2,
364
+ behavior: 'smooth',
365
+ });
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ // 节流函数,限制函数调用频率
372
+ function throttle(func, delay) {
373
+ let lastCall = 0;
374
+ return function (...args) {
375
+ const now = new Date().getTime();
376
+ if (now - lastCall < delay) {
377
+ return;
378
+ }
379
+ lastCall = now;
380
+ return func(...args);
381
+ };
382
+ }
383
+
384
+ // 页面加载时初始化
385
+ document.addEventListener('astro:page-load', generateTOC);
386
+ // 初始加载时也初始化
387
+ generateTOC();
388
+
389
+ // 添加resize事件监听,在窗口大小变化时重新计算高亮
390
+ window.addEventListener(
391
+ 'resize',
392
+ throttle(() => {
393
+ generateTOC(); // 重新生成TOC,以防窗口大小变化导致标题可见性变化
394
+ highlightActiveHeading();
395
+ }, 200)
396
+ );
397
+
398
+ // 立即触发一次高亮计算
399
+ setTimeout(generateTOC, 100);
400
+ setTimeout(highlightActiveHeading, 500);
401
+ </script>