@jet-w/astro-blog 0.1.6 → 0.2.1
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/dist/chunk-6D3XRDNY.js +145 -0
- package/dist/chunk-A2E2VSAQ.js +246 -0
- package/dist/{chunk-GYLSY3OJ.js → chunk-TJTPX2WP.js} +1 -1
- package/dist/config/index.d.ts +3 -47
- package/dist/config/index.js +18 -2
- package/dist/i18n-PgMCFBw0.d.ts +222 -0
- package/dist/index.d.ts +204 -7
- package/dist/index.js +255 -3
- package/dist/integration.d.ts +9 -1
- package/dist/integration.js +2 -1
- package/dist/{sidebar-DNdiCKBw.d.ts → sidebar-Da-W_4Lr.d.ts} +1 -1
- package/dist/utils/sidebar.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/blog/FloatingToc.vue +11 -3
- package/src/components/blog/Hero.astro +17 -2
- package/src/components/blog/NavigationTabs.vue +46 -15
- package/src/components/blog/PostCard.astro +28 -10
- package/src/components/blog/RelatedPosts.astro +23 -7
- package/src/components/blog/TableOfContents.astro +10 -4
- package/src/components/blog/TagCloud.astro +4 -3
- package/src/components/home/FeaturedPostsSection.astro +22 -6
- package/src/components/home/QuickNavSection.astro +33 -4
- package/src/components/home/RecentPostsSection.astro +22 -6
- package/src/components/home/StatsSection.astro +24 -6
- package/src/components/layout/Footer.astro +36 -20
- package/src/components/layout/Header.astro +75 -17
- package/src/components/layout/Sidebar.astro +40 -25
- package/src/components/ui/LanguageSwitcher.vue +183 -0
- package/src/components/ui/SearchBox.vue +13 -5
- package/src/components/ui/SearchInterface.vue +49 -25
- package/src/layouts/BaseLayout.astro +77 -52
- package/src/layouts/PageLayout.astro +22 -27
- package/src/layouts/SlidesLayout.astro +14 -2
- package/src/pages/archives/[year]/[month].astro +36 -17
- package/src/pages/archives/index.astro +36 -20
- package/src/pages/categories/[category].astro +33 -16
- package/src/pages/categories/index.astro +37 -14
- package/src/pages/posts/[...slug].astro +125 -18
- package/src/pages/posts/index.astro +59 -37
- package/src/pages/posts/page/[page].astro +65 -27
- package/src/pages/rss.xml.ts +18 -6
- package/src/pages/search.astro +50 -14
- package/src/pages/slides/index.astro +25 -6
- package/src/pages/tags/[tag].astro +32 -15
- package/src/pages/tags/index.astro +39 -16
- package/src/plugins/remark-containers.mjs +351 -322
- package/src/plugins/remark-protect-code.mjs +69 -0
- package/src/styles/global.css +35 -1
- package/templates/default/.claude/ralph-loop.local.md +48 -0
- package/templates/default/astro.config.mjs +33 -4
- package/templates/default/content/posts/blog_docs_en/01.get-started/01-intro.md +81 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/02-install.md +137 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/03-create-post.md +176 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/04-structure.md +173 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/05-deploy.md +208 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/README.md +52 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/02-containers.md +245 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/03-code-blocks.md +207 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/03-mermaid.md +194 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/04-icons.md +229 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/06-latex.md +233 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/07-video.md +184 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/08-slides.md +359 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/README.md +213 -0
- package/templates/default/content/posts/blog_docs_en/03.config/01-site.md +208 -0
- package/templates/default/content/posts/blog_docs_en/03.config/02-sidebar.md +240 -0
- package/templates/default/content/posts/blog_docs_en/03.config/03-i18n.md +349 -0
- package/templates/default/content/posts/blog_docs_en/03.config/README.md +85 -0
- package/templates/default/content/posts/blog_docs_en/README.md +79 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/01-intro.md +81 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/02-install.md +137 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/03-create-post.md +176 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/04-structure.md +173 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/05-deploy.md +208 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/README.md +52 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/02-containers.md +245 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/03-code-blocks.md +206 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/03-mermaid.md +194 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/04-icons.md +229 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/06-latex.md +233 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/07-video.md +184 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/08-slides.md +359 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/README.md +213 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/01-site.md +208 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/02-sidebar.md +240 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/03-i18n.md +348 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/README.md +85 -0
- package/templates/default/content/posts/blog_docs_zh/README.md +78 -0
- package/templates/default/package-lock.json +9667 -0
- package/templates/default/package.json +1 -1
- package/templates/default/src/config/footer.ts +14 -11
- package/templates/default/src/config/locales/en/footer.ts +17 -0
- package/templates/default/src/config/locales/en/index.ts +20 -0
- package/templates/default/src/config/locales/en/menu.ts +14 -0
- package/templates/default/src/config/locales/en/sidebar.ts +34 -0
- package/templates/default/src/config/locales/en/site.ts +7 -0
- package/templates/default/src/config/locales/en/ui.ts +29 -0
- package/templates/default/src/config/locales/index.ts +7 -0
- package/templates/default/src/config/locales/zh-CN/footer.ts +17 -0
- package/templates/default/src/config/locales/zh-CN/index.ts +20 -0
- package/templates/default/src/config/locales/zh-CN/menu.ts +14 -0
- package/templates/default/src/config/locales/zh-CN/sidebar.ts +34 -0
- package/templates/default/src/config/locales/zh-CN/site.ts +7 -0
- package/templates/default/src/config/locales/zh-CN/ui.ts +29 -0
- package/templates/default/src/config/sidebar.ts +10 -12
- package/templates/default/src/config/site.ts +2 -2
- package/templates/default/src/content.config.ts +15 -3
- package/templates/default/src/env.d.ts +7 -0
- package/dist/chunk-MQXPSOYB.js +0 -124
- package/templates/default/content/posts/blog_docs/01-quick-start.md +0 -162
- package/templates/default/content/posts/blog_docs/02-frontmatter.md +0 -277
- package/templates/default/content/posts/blog_docs/03-markdown-basic.md +0 -350
- package/templates/default/content/posts/blog_docs/04-containers.md +0 -331
- package/templates/default/content/posts/blog_docs/05-code-blocks.md +0 -388
- package/templates/default/content/posts/blog_docs/06-mermaid.md +0 -431
- package/templates/default/content/posts/blog_docs/07-video.md +0 -243
- package/templates/default/content/posts/blog_docs/08-latex.md +0 -382
- package/templates/default/content/posts/blog_docs/09-icons.md +0 -326
- package/templates/default/content/posts/blog_docs/10-sidebar.md +0 -445
- package/templates/default/content/posts/blog_docs/11-config.md +0 -334
- package/templates/default/content/posts/blog_docs/12-slides.mdx +0 -552
- package/templates/default/content/posts/blog_docs/README.md +0 -151
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
3
|
import Icon from '../ui/Icon.astro';
|
|
4
|
-
import { sidebarConfig } from '@jet-w/astro-blog/config';
|
|
5
4
|
import {
|
|
6
5
|
processSidebarConfig,
|
|
7
6
|
getRecentPosts,
|
|
@@ -9,36 +8,52 @@ import {
|
|
|
9
8
|
getArchives,
|
|
10
9
|
filterGroupsByPath,
|
|
11
10
|
} from '@jet-w/astro-blog/utils/sidebar';
|
|
11
|
+
import type { I18nConfig } from '../../config/i18n';
|
|
12
|
+
import { defaultI18nConfig } from '../../config/i18n';
|
|
13
|
+
import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, formatDate, filterPostsByLocale } from '../../utils/i18n';
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
currentPath?: string;
|
|
17
|
+
i18nConfig?: I18nConfig;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
const { currentPath = Astro.url.pathname } = Astro.props;
|
|
20
|
+
const { currentPath = Astro.url.pathname, i18nConfig = defaultI18nConfig } = Astro.props;
|
|
21
|
+
|
|
22
|
+
// Get current locale and translations
|
|
23
|
+
const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
24
|
+
const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
|
|
25
|
+
const ui = localeConfig.ui;
|
|
26
|
+
const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
|
|
27
|
+
|
|
28
|
+
// Get locale-specific sidebar config (already merged with defaults in getLocaleConfig)
|
|
29
|
+
const sidebarConfig = localeConfig.sidebar;
|
|
18
30
|
|
|
19
31
|
// 获取所有文章
|
|
20
32
|
const allPosts = await getCollection('posts', ({ data }) => !data.draft);
|
|
21
33
|
|
|
34
|
+
// Filter posts by current locale for sidebar widgets
|
|
35
|
+
const localePosts = filterPostsByLocale(allPosts, currentLocale, i18nConfig);
|
|
36
|
+
|
|
22
37
|
// 根据当前路径过滤侧边栏组
|
|
23
38
|
const filteredGroups = filterGroupsByPath(sidebarConfig.groups, currentPath);
|
|
24
39
|
|
|
25
40
|
// 创建过滤后的配置
|
|
26
41
|
const filteredConfig = { ...sidebarConfig, groups: filteredGroups };
|
|
27
42
|
|
|
28
|
-
// 处理侧边栏配置
|
|
29
|
-
const processedGroups = await processSidebarConfig(filteredConfig,
|
|
43
|
+
// 处理侧边栏配置 - 使用按语言过滤后的文章
|
|
44
|
+
const processedGroups = await processSidebarConfig(filteredConfig, localePosts);
|
|
30
45
|
|
|
31
|
-
// 获取其他数据
|
|
46
|
+
// 获取其他数据 - 使用按语言过滤后的文章
|
|
32
47
|
const recentPosts = sidebarConfig.showRecentPosts
|
|
33
|
-
? getRecentPosts(
|
|
48
|
+
? getRecentPosts(localePosts, sidebarConfig.recentPostsCount)
|
|
34
49
|
: [];
|
|
35
50
|
|
|
36
51
|
const popularTags = sidebarConfig.showPopularTags
|
|
37
|
-
? getPopularTags(
|
|
52
|
+
? getPopularTags(localePosts, sidebarConfig.popularTagsCount)
|
|
38
53
|
: [];
|
|
39
54
|
|
|
40
55
|
const archives = sidebarConfig.showArchives
|
|
41
|
-
? getArchives(
|
|
56
|
+
? getArchives(localePosts, sidebarConfig.archivesCount)
|
|
42
57
|
: [];
|
|
43
58
|
---
|
|
44
59
|
|
|
@@ -185,7 +200,7 @@ const archives = sidebarConfig.showArchives
|
|
|
185
200
|
</a>
|
|
186
201
|
) : (
|
|
187
202
|
<a
|
|
188
|
-
href={
|
|
203
|
+
href={`${localePrefix}/posts/${item.slug?.toLowerCase()}`}
|
|
189
204
|
class="tree-file flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
|
190
205
|
title={item.title}
|
|
191
206
|
>
|
|
@@ -226,7 +241,7 @@ const archives = sidebarConfig.showArchives
|
|
|
226
241
|
</a>
|
|
227
242
|
) : (
|
|
228
243
|
<a
|
|
229
|
-
href={
|
|
244
|
+
href={`${localePrefix}/posts/${grandChild.slug?.toLowerCase()}`}
|
|
230
245
|
class="tree-file flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
|
231
246
|
title={grandChild.title}
|
|
232
247
|
>
|
|
@@ -277,7 +292,7 @@ const archives = sidebarConfig.showArchives
|
|
|
277
292
|
</a>
|
|
278
293
|
) : (
|
|
279
294
|
<a
|
|
280
|
-
href={
|
|
295
|
+
href={`${localePrefix}/posts/${child.slug?.toLowerCase()}`}
|
|
281
296
|
class="tree-file flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
|
282
297
|
title={child.title}
|
|
283
298
|
>
|
|
@@ -328,7 +343,7 @@ const archives = sidebarConfig.showArchives
|
|
|
328
343
|
</a>
|
|
329
344
|
) : (
|
|
330
345
|
<a
|
|
331
|
-
href={
|
|
346
|
+
href={`${localePrefix}/posts/${node.slug?.toLowerCase()}`}
|
|
332
347
|
class="tree-file flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
|
333
348
|
title={node.title}
|
|
334
349
|
>
|
|
@@ -362,17 +377,17 @@ const archives = sidebarConfig.showArchives
|
|
|
362
377
|
)
|
|
363
378
|
))}
|
|
364
379
|
|
|
365
|
-
{/*
|
|
380
|
+
{/* Recent Posts */}
|
|
366
381
|
{sidebarConfig.showRecentPosts && recentPosts.length > 0 && (
|
|
367
382
|
<section>
|
|
368
383
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
|
|
369
|
-
|
|
384
|
+
{ui.recentPosts}
|
|
370
385
|
</h3>
|
|
371
386
|
<div class="space-y-3">
|
|
372
387
|
{recentPosts.map((post) => (
|
|
373
388
|
<article class="group">
|
|
374
389
|
<a
|
|
375
|
-
href={
|
|
390
|
+
href={`${localePrefix}/posts/${post.id.toLowerCase()}`}
|
|
376
391
|
class="block p-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
377
392
|
>
|
|
378
393
|
<h4 class="text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 line-clamp-2 mb-1">
|
|
@@ -380,7 +395,7 @@ const archives = sidebarConfig.showArchives
|
|
|
380
395
|
</h4>
|
|
381
396
|
{post.data.pubDate && (
|
|
382
397
|
<time class="text-xs text-slate-500 dark:text-slate-400">
|
|
383
|
-
{
|
|
398
|
+
{formatDate(post.data.pubDate, localeConfig.locale.dateLocale)}
|
|
384
399
|
</time>
|
|
385
400
|
)}
|
|
386
401
|
</a>
|
|
@@ -390,16 +405,16 @@ const archives = sidebarConfig.showArchives
|
|
|
390
405
|
</section>
|
|
391
406
|
)}
|
|
392
407
|
|
|
393
|
-
{/*
|
|
408
|
+
{/* Popular Tags */}
|
|
394
409
|
{sidebarConfig.showPopularTags && popularTags.length > 0 && (
|
|
395
410
|
<section>
|
|
396
411
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
|
|
397
|
-
|
|
412
|
+
{ui.popularTags}
|
|
398
413
|
</h3>
|
|
399
414
|
<div class="flex flex-wrap gap-2">
|
|
400
415
|
{popularTags.map((tag) => (
|
|
401
416
|
<a
|
|
402
|
-
href={
|
|
417
|
+
href={`${localePrefix}/tags/${tag.slug}`}
|
|
403
418
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors"
|
|
404
419
|
style={`font-size: ${Math.min(14, 10 + tag.count * 0.5)}px`}
|
|
405
420
|
>
|
|
@@ -411,20 +426,20 @@ const archives = sidebarConfig.showArchives
|
|
|
411
426
|
</section>
|
|
412
427
|
)}
|
|
413
428
|
|
|
414
|
-
{/*
|
|
429
|
+
{/* Archives */}
|
|
415
430
|
{sidebarConfig.showArchives && archives.length > 0 && (
|
|
416
431
|
<section>
|
|
417
432
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
|
|
418
|
-
|
|
433
|
+
{ui.archives}
|
|
419
434
|
</h3>
|
|
420
435
|
<div class="space-y-2">
|
|
421
436
|
{archives.map((archive) => (
|
|
422
437
|
<a
|
|
423
|
-
href={
|
|
438
|
+
href={`${localePrefix}/archives/${archive.year}/${String(archive.month).padStart(2, '0')}`}
|
|
424
439
|
class="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors group"
|
|
425
440
|
>
|
|
426
441
|
<span class="text-sm text-slate-700 dark:text-slate-300 group-hover:text-primary-500">
|
|
427
|
-
{archive.year
|
|
442
|
+
{new Date(archive.year, archive.month - 1).toLocaleDateString(localeConfig.locale.dateLocale, { year: 'numeric', month: 'long' })}
|
|
428
443
|
</span>
|
|
429
444
|
<span class="text-xs text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded-full">
|
|
430
445
|
{archive.count}
|
|
@@ -435,11 +450,11 @@ const archives = sidebarConfig.showArchives
|
|
|
435
450
|
</section>
|
|
436
451
|
)}
|
|
437
452
|
|
|
438
|
-
{/*
|
|
453
|
+
{/* Friend Links */}
|
|
439
454
|
{sidebarConfig.showFriendLinks && sidebarConfig.friendLinks && sidebarConfig.friendLinks.length > 0 && (
|
|
440
455
|
<section>
|
|
441
456
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
|
|
442
|
-
|
|
457
|
+
{ui.friendLinks}
|
|
443
458
|
</h3>
|
|
444
459
|
<div class="space-y-2">
|
|
445
460
|
{sidebarConfig.friendLinks.map((link) => (
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative" ref="dropdownRef">
|
|
3
|
+
<button
|
|
4
|
+
@click="toggleDropdown"
|
|
5
|
+
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
6
|
+
:aria-expanded="isOpen"
|
|
7
|
+
aria-haspopup="listbox"
|
|
8
|
+
:aria-label="ariaLabel"
|
|
9
|
+
>
|
|
10
|
+
<svg
|
|
11
|
+
class="w-4 h-4"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
stroke-linecap="round"
|
|
19
|
+
stroke-linejoin="round"
|
|
20
|
+
stroke-width="2"
|
|
21
|
+
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
<span>{{ currentLocaleName }}</span>
|
|
25
|
+
<svg
|
|
26
|
+
class="w-3 h-3 transition-transform"
|
|
27
|
+
:class="{ 'rotate-180': isOpen }"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
>
|
|
32
|
+
<path
|
|
33
|
+
stroke-linecap="round"
|
|
34
|
+
stroke-linejoin="round"
|
|
35
|
+
stroke-width="2"
|
|
36
|
+
d="M19 9l-7 7-7-7"
|
|
37
|
+
/>
|
|
38
|
+
</svg>
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<Transition
|
|
42
|
+
enter-active-class="transition ease-out duration-100"
|
|
43
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
44
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
45
|
+
leave-active-class="transition ease-in duration-75"
|
|
46
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
47
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
v-if="isOpen"
|
|
51
|
+
class="absolute right-0 mt-2 w-40 origin-top-right rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 dark:ring-gray-700 focus:outline-none z-50"
|
|
52
|
+
role="listbox"
|
|
53
|
+
:aria-label="listLabel"
|
|
54
|
+
>
|
|
55
|
+
<div class="py-1">
|
|
56
|
+
<a
|
|
57
|
+
v-for="locale in locales"
|
|
58
|
+
:key="locale.code"
|
|
59
|
+
:href="getLocalizedUrl(locale.code)"
|
|
60
|
+
class="flex items-center gap-2 px-4 py-2 text-sm transition-colors"
|
|
61
|
+
:class="[
|
|
62
|
+
locale.code === currentLocale
|
|
63
|
+
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
|
64
|
+
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
65
|
+
]"
|
|
66
|
+
role="option"
|
|
67
|
+
:aria-selected="locale.code === currentLocale"
|
|
68
|
+
@click="closeDropdown"
|
|
69
|
+
>
|
|
70
|
+
<span
|
|
71
|
+
v-if="locale.code === currentLocale"
|
|
72
|
+
class="w-4 h-4 flex items-center justify-center"
|
|
73
|
+
>
|
|
74
|
+
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
75
|
+
<path
|
|
76
|
+
fill-rule="evenodd"
|
|
77
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
78
|
+
clip-rule="evenodd"
|
|
79
|
+
/>
|
|
80
|
+
</svg>
|
|
81
|
+
</span>
|
|
82
|
+
<span v-else class="w-4 h-4" />
|
|
83
|
+
<span>{{ locale.name }}</span>
|
|
84
|
+
</a>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</Transition>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
93
|
+
|
|
94
|
+
interface LocaleInfo {
|
|
95
|
+
code: string;
|
|
96
|
+
name: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface Props {
|
|
100
|
+
/** Available locales */
|
|
101
|
+
locales: LocaleInfo[];
|
|
102
|
+
/** Current locale code */
|
|
103
|
+
currentLocale: string;
|
|
104
|
+
/** Current page path (without locale prefix) */
|
|
105
|
+
currentPath: string;
|
|
106
|
+
/** Default locale code */
|
|
107
|
+
defaultLocale: string;
|
|
108
|
+
/** Whether to prefix the default locale in URLs */
|
|
109
|
+
prefixDefaultLocale?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
113
|
+
prefixDefaultLocale: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const isOpen = ref(false);
|
|
117
|
+
const dropdownRef = ref<HTMLElement | null>(null);
|
|
118
|
+
|
|
119
|
+
const currentLocaleName = computed(() => {
|
|
120
|
+
const locale = props.locales.find((l) => l.code === props.currentLocale);
|
|
121
|
+
return locale?.name || props.currentLocale;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const ariaLabel = computed(() => {
|
|
125
|
+
return `Select language, current: ${currentLocaleName.value}`;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const listLabel = computed(() => {
|
|
129
|
+
return 'Available languages';
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function toggleDropdown() {
|
|
133
|
+
isOpen.value = !isOpen.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function closeDropdown() {
|
|
137
|
+
isOpen.value = false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getLocalizedUrl(targetLocale: string): string {
|
|
141
|
+
// Normalize the path
|
|
142
|
+
let basePath = props.currentPath;
|
|
143
|
+
if (!basePath.startsWith('/')) {
|
|
144
|
+
basePath = '/' + basePath;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If target is default locale and prefix is not required
|
|
148
|
+
if (targetLocale === props.defaultLocale && !props.prefixDefaultLocale) {
|
|
149
|
+
return basePath === '/' ? '/' : basePath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add locale prefix
|
|
153
|
+
if (basePath === '/') {
|
|
154
|
+
return `/${targetLocale}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return `/${targetLocale}${basePath}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Close dropdown when clicking outside
|
|
161
|
+
function handleClickOutside(event: MouseEvent) {
|
|
162
|
+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
163
|
+
closeDropdown();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Close dropdown on escape key
|
|
168
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
169
|
+
if (event.key === 'Escape') {
|
|
170
|
+
closeDropdown();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onMounted(() => {
|
|
175
|
+
document.addEventListener('click', handleClickOutside);
|
|
176
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
onUnmounted(() => {
|
|
180
|
+
document.removeEventListener('click', handleClickOutside);
|
|
181
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
182
|
+
});
|
|
183
|
+
</script>
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
<div class="relative">
|
|
3
3
|
<div class="relative">
|
|
4
4
|
<input
|
|
5
|
+
ref="searchInputRef"
|
|
5
6
|
v-model="searchQuery"
|
|
6
7
|
@input="handleInput"
|
|
7
8
|
@focus="handleFocus"
|
|
8
9
|
@blur="handleBlur"
|
|
9
10
|
type="text"
|
|
10
|
-
placeholder="搜索文章..."
|
|
11
|
+
:placeholder="props.placeholder || '搜索文章...'"
|
|
11
12
|
class="w-64 pl-10 pr-4 py-2 text-sm bg-slate-100 dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors"
|
|
12
13
|
/>
|
|
13
14
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
class="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-lg z-50 max-h-96 overflow-y-auto"
|
|
25
26
|
>
|
|
26
27
|
<div v-if="searchResults.length === 0 && searchQuery.length > 0" class="p-4 text-center text-slate-500 dark:text-slate-400">
|
|
27
|
-
没有找到相关文章
|
|
28
|
+
{{ props.noResultsText || '没有找到相关文章' }}
|
|
28
29
|
</div>
|
|
29
30
|
<div v-else class="py-2">
|
|
30
31
|
<a
|
|
@@ -55,6 +56,12 @@
|
|
|
55
56
|
<script setup lang="ts">
|
|
56
57
|
import { ref, onMounted } from 'vue'
|
|
57
58
|
|
|
59
|
+
const props = defineProps<{
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
searchLabel?: string;
|
|
62
|
+
noResultsText?: string;
|
|
63
|
+
}>();
|
|
64
|
+
|
|
58
65
|
interface SearchResult {
|
|
59
66
|
title: string
|
|
60
67
|
description: string
|
|
@@ -145,14 +152,15 @@ const highlightText = (text: string) => {
|
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
// 键盘快捷键支持
|
|
155
|
+
const searchInputRef = ref<HTMLInputElement | null>(null)
|
|
156
|
+
|
|
148
157
|
onMounted(() => {
|
|
149
158
|
// Ctrl/Cmd + K 打开搜索
|
|
150
159
|
document.addEventListener('keydown', (e) => {
|
|
151
160
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
152
161
|
e.preventDefault()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
input.focus()
|
|
162
|
+
if (searchInputRef.value) {
|
|
163
|
+
searchInputRef.value.focus()
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
})
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
v-model="searchQuery"
|
|
8
8
|
@input="handleSearch"
|
|
9
9
|
type="text"
|
|
10
|
-
placeholder="搜索文章标题、内容、标签..."
|
|
10
|
+
:placeholder="props.placeholder || '搜索文章标题、内容、标签...'"
|
|
11
11
|
class="w-full pl-12 pr-4 py-4 text-lg bg-white dark:bg-slate-800 border-2 border-slate-300 dark:border-slate-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors shadow-sm"
|
|
12
12
|
autofocus
|
|
13
13
|
/>
|
|
@@ -33,41 +33,41 @@
|
|
|
33
33
|
<div class="flex flex-wrap gap-4">
|
|
34
34
|
<!-- 标签筛选 -->
|
|
35
35
|
<div class="flex items-center space-x-2">
|
|
36
|
-
<label class="text-sm font-medium text-slate-700 dark:text-slate-300"
|
|
36
|
+
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ props.tags || '标签' }}:</label>
|
|
37
37
|
<select
|
|
38
38
|
v-model="selectedTag"
|
|
39
39
|
@change="handleSearch"
|
|
40
40
|
class="text-sm px-3 py-1 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
41
41
|
>
|
|
42
|
-
<option value=""
|
|
42
|
+
<option value="">{{ props.all || '全部' }}</option>
|
|
43
43
|
<option v-for="tag in allTags" :key="tag" :value="tag">{{ tag }}</option>
|
|
44
44
|
</select>
|
|
45
45
|
</div>
|
|
46
46
|
|
|
47
47
|
<!-- 分类筛选 -->
|
|
48
48
|
<div class="flex items-center space-x-2">
|
|
49
|
-
<label class="text-sm font-medium text-slate-700 dark:text-slate-300"
|
|
49
|
+
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ props.categories || '分类' }}:</label>
|
|
50
50
|
<select
|
|
51
51
|
v-model="selectedCategory"
|
|
52
52
|
@change="handleSearch"
|
|
53
53
|
class="text-sm px-3 py-1 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
54
54
|
>
|
|
55
|
-
<option value=""
|
|
55
|
+
<option value="">{{ props.all || '全部' }}</option>
|
|
56
56
|
<option v-for="category in allCategories" :key="category" :value="category">{{ category }}</option>
|
|
57
57
|
</select>
|
|
58
58
|
</div>
|
|
59
59
|
|
|
60
60
|
<!-- 排序 -->
|
|
61
61
|
<div class="flex items-center space-x-2">
|
|
62
|
-
<label class="text-sm font-medium text-slate-700 dark:text-slate-300"
|
|
62
|
+
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ props.sortLabel || '排序' }}:</label>
|
|
63
63
|
<select
|
|
64
64
|
v-model="sortBy"
|
|
65
65
|
@change="handleSearch"
|
|
66
66
|
class="text-sm px-3 py-1 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
67
67
|
>
|
|
68
|
-
<option value="relevance"
|
|
69
|
-
<option value="date"
|
|
70
|
-
<option value="title"
|
|
68
|
+
<option value="relevance">{{ props.relevance || '相关性' }}</option>
|
|
69
|
+
<option value="date">{{ props.publishTime || '发布时间' }}</option>
|
|
70
|
+
<option value="title">{{ props.titleLabel || '标题' }}</option>
|
|
71
71
|
</select>
|
|
72
72
|
</div>
|
|
73
73
|
</div>
|
|
@@ -75,15 +75,14 @@
|
|
|
75
75
|
<!-- 搜索状态 -->
|
|
76
76
|
<div class="flex items-center justify-between text-sm text-slate-600 dark:text-slate-400">
|
|
77
77
|
<div>
|
|
78
|
-
<span v-if="isSearching"
|
|
78
|
+
<span v-if="isSearching">{{ props.searching || '正在搜索...' }}</span>
|
|
79
79
|
<span v-else-if="searchQuery">
|
|
80
|
-
找到 {{ searchResults.length }}
|
|
81
|
-
<span v-if="searchQuery">包含 "{{ searchQuery }}"</span>
|
|
80
|
+
{{ props.foundResults || '找到' }} {{ searchResults.length }} {{ props.containing || '个结果包含' }} "{{ searchQuery }}"
|
|
82
81
|
</span>
|
|
83
|
-
<span v-else
|
|
82
|
+
<span v-else>{{ props.enterKeywordToSearch || '输入关键词开始搜索' }}</span>
|
|
84
83
|
</div>
|
|
85
|
-
<div v-if="
|
|
86
|
-
|
|
84
|
+
<div v-if="searchTimeValue">
|
|
85
|
+
{{ props.searchTime || '搜索用时' }}: {{ searchTimeValue }}ms
|
|
87
86
|
</div>
|
|
88
87
|
</div>
|
|
89
88
|
|
|
@@ -104,7 +103,7 @@
|
|
|
104
103
|
|
|
105
104
|
<div class="flex flex-wrap items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
|
|
106
105
|
<time :datetime="result.date">{{ formatDate(result.date) }}</time>
|
|
107
|
-
<span v-if="result.readingTime">{{ result.readingTime }}
|
|
106
|
+
<span v-if="result.readingTime">{{ result.readingTime }} {{ props.readMinutes || '分钟阅读' }}</span>
|
|
108
107
|
<div class="flex flex-wrap gap-1">
|
|
109
108
|
<span
|
|
110
109
|
v-for="tag in result.tags.slice(0, 3)"
|
|
@@ -119,7 +118,7 @@
|
|
|
119
118
|
|
|
120
119
|
<!-- 相关度评分 -->
|
|
121
120
|
<div class="lg:w-20 text-center">
|
|
122
|
-
<div class="text-xs text-slate-500 dark:text-slate-400 mb-1"
|
|
121
|
+
<div class="text-xs text-slate-500 dark:text-slate-400 mb-1">{{ props.relevanceLabel || '相关度' }}</div>
|
|
123
122
|
<div class="flex items-center justify-center">
|
|
124
123
|
<div class="w-12 h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
125
124
|
<div
|
|
@@ -138,16 +137,16 @@
|
|
|
138
137
|
<div v-else-if="searchQuery && !isSearching" class="text-center py-16">
|
|
139
138
|
<div class="text-6xl mb-4">🔍</div>
|
|
140
139
|
<h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-2">
|
|
141
|
-
没有找到相关结果
|
|
140
|
+
{{ props.noResultsTitle || '没有找到相关结果' }}
|
|
142
141
|
</h3>
|
|
143
142
|
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
|
144
|
-
尝试使用不同的关键词或调整筛选条件
|
|
143
|
+
{{ props.noResultsDesc || '尝试使用不同的关键词或调整筛选条件' }}
|
|
145
144
|
</p>
|
|
146
145
|
<button
|
|
147
146
|
@click="clearSearch"
|
|
148
147
|
class="btn-secondary"
|
|
149
148
|
>
|
|
150
|
-
清除搜索
|
|
149
|
+
{{ props.clearSearch || '清除搜索' }}
|
|
151
150
|
</button>
|
|
152
151
|
</div>
|
|
153
152
|
|
|
@@ -162,7 +161,7 @@
|
|
|
162
161
|
? 'bg-slate-100 dark:bg-slate-800 text-slate-400 border-slate-200 dark:border-slate-700'
|
|
163
162
|
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'"
|
|
164
163
|
>
|
|
165
|
-
上一页
|
|
164
|
+
{{ props.previousPage || '上一页' }}
|
|
166
165
|
</button>
|
|
167
166
|
|
|
168
167
|
<div class="flex items-center space-x-1">
|
|
@@ -187,7 +186,7 @@
|
|
|
187
186
|
? 'bg-slate-100 dark:bg-slate-800 text-slate-400 border-slate-200 dark:border-slate-700'
|
|
188
187
|
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'"
|
|
189
188
|
>
|
|
190
|
-
下一页
|
|
189
|
+
{{ props.nextPage || '下一页' }}
|
|
191
190
|
</button>
|
|
192
191
|
</nav>
|
|
193
192
|
</div>
|
|
@@ -198,13 +197,38 @@
|
|
|
198
197
|
import { ref, computed, onMounted } from 'vue'
|
|
199
198
|
import type { SearchResult } from '@jet-w/astro-blog/types'
|
|
200
199
|
|
|
200
|
+
const props = defineProps<{
|
|
201
|
+
placeholder?: string;
|
|
202
|
+
noResults?: string;
|
|
203
|
+
tags?: string;
|
|
204
|
+
categories?: string;
|
|
205
|
+
sortLabel?: string;
|
|
206
|
+
relevance?: string;
|
|
207
|
+
publishTime?: string;
|
|
208
|
+
titleLabel?: string;
|
|
209
|
+
all?: string;
|
|
210
|
+
searching?: string;
|
|
211
|
+
foundResults?: string;
|
|
212
|
+
containing?: string;
|
|
213
|
+
enterKeywordToSearch?: string;
|
|
214
|
+
searchTime?: string;
|
|
215
|
+
readMinutes?: string;
|
|
216
|
+
relevanceLabel?: string;
|
|
217
|
+
noResultsTitle?: string;
|
|
218
|
+
noResultsDesc?: string;
|
|
219
|
+
clearSearch?: string;
|
|
220
|
+
previousPage?: string;
|
|
221
|
+
nextPage?: string;
|
|
222
|
+
dateLocale?: string;
|
|
223
|
+
}>();
|
|
224
|
+
|
|
201
225
|
const searchQuery = ref('')
|
|
202
226
|
const selectedTag = ref('')
|
|
203
227
|
const selectedCategory = ref('')
|
|
204
228
|
const sortBy = ref('relevance')
|
|
205
229
|
const searchResults = ref<(SearchResult & { score: number; date: string; readingTime?: number })[]>([])
|
|
206
230
|
const isSearching = ref(false)
|
|
207
|
-
const
|
|
231
|
+
const searchTimeValue = ref(0)
|
|
208
232
|
const currentPage = ref(1)
|
|
209
233
|
const resultsPerPage = 10
|
|
210
234
|
|
|
@@ -339,7 +363,7 @@ const performSearch = () => {
|
|
|
339
363
|
|
|
340
364
|
searchResults.value = results
|
|
341
365
|
isSearching.value = false
|
|
342
|
-
|
|
366
|
+
searchTimeValue.value = Math.round(performance.now() - startTime)
|
|
343
367
|
currentPage.value = 1
|
|
344
368
|
}, 200)
|
|
345
369
|
}
|
|
@@ -364,7 +388,7 @@ const highlightText = (text: string) => {
|
|
|
364
388
|
}
|
|
365
389
|
|
|
366
390
|
const formatDate = (date: string) => {
|
|
367
|
-
return new Intl.DateTimeFormat('zh-CN', {
|
|
391
|
+
return new Intl.DateTimeFormat(props.dateLocale || 'zh-CN', {
|
|
368
392
|
year: 'numeric',
|
|
369
393
|
month: 'long',
|
|
370
394
|
day: 'numeric'
|