@jet-w/astro-blog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/chunk-FXPGR372.js +0 -0
  2. package/dist/chunk-GYLSY3OJ.js +173 -0
  3. package/dist/config/index.d.ts +166 -0
  4. package/dist/config/index.js +38 -0
  5. package/dist/index.d.ts +34 -0
  6. package/dist/index.js +59 -0
  7. package/dist/types/index.d.ts +75 -0
  8. package/dist/types/index.js +1 -0
  9. package/package.json +84 -0
  10. package/src/components/EChartsCard.vue +118 -0
  11. package/src/components/Mermaid.vue +73 -0
  12. package/src/components/about/ContentCard.astro +27 -0
  13. package/src/components/about/IconCard.astro +77 -0
  14. package/src/components/about/SocialLinks.astro +54 -0
  15. package/src/components/about/TagCard.astro +65 -0
  16. package/src/components/about/TagGroup.astro +33 -0
  17. package/src/components/about/TimelineCard.astro +52 -0
  18. package/src/components/blog/FloatingToc.vue +198 -0
  19. package/src/components/blog/Hero.astro +147 -0
  20. package/src/components/blog/NavigationTabs.vue +245 -0
  21. package/src/components/blog/PostCard.astro +161 -0
  22. package/src/components/blog/PostNavigation.astro +106 -0
  23. package/src/components/blog/RelatedPosts.astro +175 -0
  24. package/src/components/blog/TableOfContents.astro +153 -0
  25. package/src/components/blog/TagCloud.astro +91 -0
  26. package/src/components/home/FeaturedPostsSection.astro +54 -0
  27. package/src/components/home/QuickNavSection.astro +81 -0
  28. package/src/components/home/RecentPostsSection.astro +52 -0
  29. package/src/components/home/StatsSection.astro +44 -0
  30. package/src/components/layout/Footer.astro +103 -0
  31. package/src/components/layout/Header.astro +68 -0
  32. package/src/components/layout/Sidebar.astro +594 -0
  33. package/src/components/media/Bilibili.astro +114 -0
  34. package/src/components/media/Slides.astro +313 -0
  35. package/src/components/media/Video.astro +111 -0
  36. package/src/components/media/VideoPlayer.astro +89 -0
  37. package/src/components/media/YouTube.astro +92 -0
  38. package/src/components/pte/StudyCalendar.vue +1348 -0
  39. package/src/components/ui/Icon.astro +187 -0
  40. package/src/components/ui/MobileMenu.vue +201 -0
  41. package/src/components/ui/Pagination.astro +143 -0
  42. package/src/components/ui/SearchBox.vue +179 -0
  43. package/src/components/ui/SearchInterface.vue +409 -0
  44. package/src/components/ui/SidebarToggle.vue +57 -0
  45. package/src/components/ui/ThemeToggle.vue +90 -0
  46. package/src/layouts/AboutLayout.astro +18 -0
  47. package/src/layouts/BaseLayout.astro +362 -0
  48. package/src/layouts/PageLayout.astro +217 -0
  49. package/src/layouts/SlidesLayout.astro +320 -0
  50. package/src/plugins/rehype-clean-containers.mjs +24 -0
  51. package/src/plugins/rehype-relative-links.mjs +43 -0
  52. package/src/plugins/rehype-tabs.mjs +116 -0
  53. package/src/plugins/remark-containers.mjs +407 -0
  54. package/src/plugins/remark-mermaid.mjs +46 -0
  55. package/src/styles/global.css +870 -0
  56. package/src/styles/slides.css +220 -0
  57. package/src/utils/sidebar.ts +492 -0
  58. package/templates/default/astro.config.mjs +51 -0
  59. package/templates/default/content/pages/about.mdx +93 -0
  60. package/templates/default/content/pages/index.mdx +20 -0
  61. package/templates/default/content/posts/blog_docs/01-quick-start.md +162 -0
  62. package/templates/default/content/posts/blog_docs/02-frontmatter.md +277 -0
  63. package/templates/default/content/posts/blog_docs/03-markdown-basic.md +350 -0
  64. package/templates/default/content/posts/blog_docs/04-containers.md +331 -0
  65. package/templates/default/content/posts/blog_docs/05-code-blocks.md +388 -0
  66. package/templates/default/content/posts/blog_docs/06-mermaid.md +431 -0
  67. package/templates/default/content/posts/blog_docs/07-video.md +243 -0
  68. package/templates/default/content/posts/blog_docs/08-latex.md +382 -0
  69. package/templates/default/content/posts/blog_docs/09-icons.md +326 -0
  70. package/templates/default/content/posts/blog_docs/10-sidebar.md +445 -0
  71. package/templates/default/content/posts/blog_docs/11-config.md +334 -0
  72. package/templates/default/content/posts/blog_docs/12-slides.mdx +552 -0
  73. package/templates/default/content/posts/blog_docs/README.md +151 -0
  74. package/templates/default/content/slides/demo.md +146 -0
  75. package/templates/default/content/slides/docs/basic-demo.md +35 -0
  76. package/templates/default/content/slides/docs/code-demo.md +62 -0
  77. package/templates/default/content/slides/docs/echarts-demo.md +139 -0
  78. package/templates/default/content/slides/docs/fragment-demo.md +35 -0
  79. package/templates/default/content/slides/docs/math-demo.md +48 -0
  80. package/templates/default/content/slides/docs/mermaid-demo.md +105 -0
  81. package/templates/default/content/slides/docs/theme-demo.md +38 -0
  82. package/templates/default/content/slides/docs/vertical-demo.md +50 -0
  83. package/templates/default/package.json +31 -0
  84. package/templates/default/public/favicon-bak.svg +4 -0
  85. package/templates/default/public/images/avatar.jpg +0 -0
  86. package/templates/default/public/images/avatar.svg +142 -0
  87. package/templates/default/public/js/mermaid-container.js +402 -0
  88. package/templates/default/public/js/mermaid-init.js +131 -0
  89. package/templates/default/public/js/mermaid-render.js +98 -0
  90. package/templates/default/public/js/mermaid-simple.js +95 -0
  91. package/templates/default/public/js/tabs-init.js +86 -0
  92. package/templates/default/public/media/individual_portfolio/INDIVIDUAL PORTFOLIO.png +0 -0
  93. package/templates/default/public/slides/plugin/highlight/highlight.js +5 -0
  94. package/templates/default/public/slides/plugin/highlight/monokai.css +71 -0
  95. package/templates/default/public/slides/plugin/markdown/markdown.js +7 -0
  96. package/templates/default/public/slides/plugin/math/math.js +1 -0
  97. package/templates/default/public/slides/plugin/notes/notes.js +1 -0
  98. package/templates/default/public/slides/reveal.css +9 -0
  99. package/templates/default/public/slides/reveal.js +9 -0
  100. package/templates/default/public/slides/theme/beige.css +366 -0
  101. package/templates/default/public/slides/theme/black-contrast.css +362 -0
  102. package/templates/default/public/slides/theme/black.css +359 -0
  103. package/templates/default/public/slides/theme/blood.css +392 -0
  104. package/templates/default/public/slides/theme/dracula.css +385 -0
  105. package/templates/default/public/slides/theme/league.css +368 -0
  106. package/templates/default/public/slides/theme/moon.css +362 -0
  107. package/templates/default/public/slides/theme/night.css +360 -0
  108. package/templates/default/public/slides/theme/serif.css +363 -0
  109. package/templates/default/public/slides/theme/simple.css +362 -0
  110. package/templates/default/public/slides/theme/sky.css +370 -0
  111. package/templates/default/public/slides/theme/solarized.css +363 -0
  112. package/templates/default/public/slides/theme/white-contrast.css +362 -0
  113. package/templates/default/public/slides/theme/white.css +359 -0
  114. package/templates/default/public/slides/theme/white_contrast_compact_verbatim_headers.css +360 -0
  115. package/templates/default/public/test-complete.html +43 -0
  116. package/templates/default/public/test-mermaid.html +124 -0
  117. package/templates/default/src/config/index.ts +114 -0
  118. package/templates/default/src/content.config.ts +96 -0
  119. package/templates/default/src/pages/[...slug].astro +27 -0
  120. package/templates/default/src/pages/archives/[year]/[month]/page/[page].astro +176 -0
  121. package/templates/default/src/pages/archives/[year]/[month].astro +158 -0
  122. package/templates/default/src/pages/archives/index.astro +210 -0
  123. package/templates/default/src/pages/categories/[category]/page/[page].astro +218 -0
  124. package/templates/default/src/pages/categories/[category].astro +198 -0
  125. package/templates/default/src/pages/categories/index.astro +190 -0
  126. package/templates/default/src/pages/container-test.astro +79 -0
  127. package/templates/default/src/pages/mermaid-direct.html +78 -0
  128. package/templates/default/src/pages/posts/[...slug].astro +335 -0
  129. package/templates/default/src/pages/posts/index.astro +541 -0
  130. package/templates/default/src/pages/posts/page/[page].astro +146 -0
  131. package/templates/default/src/pages/rss.xml.ts +28 -0
  132. package/templates/default/src/pages/search-index.json.ts +21 -0
  133. package/templates/default/src/pages/search.astro +50 -0
  134. package/templates/default/src/pages/slides/[...slug].astro +54 -0
  135. package/templates/default/src/pages/slides/index.astro +135 -0
  136. package/templates/default/src/pages/tags/[tag]/page/[page].astro +211 -0
  137. package/templates/default/src/pages/tags/[tag].astro +191 -0
  138. package/templates/default/src/pages/tags/index.astro +167 -0
  139. package/templates/default/tailwind.config.mjs +78 -0
  140. package/templates/default/tsconfig.json +9 -0
@@ -0,0 +1,179 @@
1
+ <template>
2
+ <div class="relative">
3
+ <div class="relative">
4
+ <input
5
+ v-model="searchQuery"
6
+ @input="handleInput"
7
+ @focus="handleFocus"
8
+ @blur="handleBlur"
9
+ type="text"
10
+ placeholder="搜索文章..."
11
+ 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
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
14
+ <svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
16
+ </svg>
17
+ </div>
18
+ </div>
19
+
20
+ <!-- 搜索结果 -->
21
+ <transition name="fade">
22
+ <div
23
+ v-if="showResults && (searchResults.length > 0 || searchQuery.length > 0)"
24
+ 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
+ <div v-if="searchResults.length === 0 && searchQuery.length > 0" class="p-4 text-center text-slate-500 dark:text-slate-400">
27
+ 没有找到相关文章
28
+ </div>
29
+ <div v-else class="py-2">
30
+ <a
31
+ v-for="result in searchResults"
32
+ :key="result.url"
33
+ :href="result.url"
34
+ class="block px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors border-b border-slate-100 dark:border-slate-700 last:border-b-0"
35
+ @click="handleResultClick"
36
+ >
37
+ <h4 class="text-sm font-medium text-slate-900 dark:text-slate-100 mb-1" v-html="highlightText(result.title)"></h4>
38
+ <p class="text-xs text-slate-600 dark:text-slate-400 line-clamp-2" v-html="highlightText(result.description)"></p>
39
+ <div class="flex flex-wrap gap-1 mt-2">
40
+ <span
41
+ v-for="tag in result.tags.slice(0, 3)"
42
+ :key="tag"
43
+ class="text-xs px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full"
44
+ >
45
+ {{ tag }}
46
+ </span>
47
+ </div>
48
+ </a>
49
+ </div>
50
+ </div>
51
+ </transition>
52
+ </div>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { ref, onMounted } from 'vue'
57
+
58
+ interface SearchResult {
59
+ title: string
60
+ description: string
61
+ url: string
62
+ content: string
63
+ tags: string[]
64
+ categories: string[]
65
+ }
66
+
67
+ const searchQuery = ref('')
68
+ const searchResults = ref<SearchResult[]>([])
69
+ const showResults = ref(false)
70
+ const searchIndex = ref<SearchResult[]>([])
71
+ const isLoading = ref(false)
72
+ const isLoaded = ref(false)
73
+
74
+ // 加载搜索索引
75
+ const loadSearchIndex = async () => {
76
+ if (isLoaded.value || isLoading.value) return
77
+
78
+ isLoading.value = true
79
+ try {
80
+ const response = await fetch('/search-index.json')
81
+ if (response.ok) {
82
+ searchIndex.value = await response.json()
83
+ isLoaded.value = true
84
+ }
85
+ } catch (error) {
86
+ console.error('Failed to load search index:', error)
87
+ } finally {
88
+ isLoading.value = false
89
+ }
90
+ }
91
+
92
+ const handleInput = async () => {
93
+ // 首次输入时加载索引
94
+ if (!isLoaded.value) {
95
+ await loadSearchIndex()
96
+ }
97
+
98
+ if (searchQuery.value.length === 0) {
99
+ searchResults.value = []
100
+ return
101
+ }
102
+
103
+ // 简单的文本搜索实现
104
+ const query = searchQuery.value.toLowerCase()
105
+ searchResults.value = searchIndex.value.filter(item =>
106
+ item.title.toLowerCase().includes(query) ||
107
+ item.description.toLowerCase().includes(query) ||
108
+ item.content.toLowerCase().includes(query) ||
109
+ item.tags.some(tag => tag.toLowerCase().includes(query))
110
+ ).slice(0, 8) // 限制结果数量
111
+ }
112
+
113
+ const handleFocus = async () => {
114
+ showResults.value = true
115
+ // 聚焦时预加载索引
116
+ if (!isLoaded.value) {
117
+ await loadSearchIndex()
118
+ }
119
+ }
120
+
121
+ const handleBlur = () => {
122
+ // 延迟隐藏结果,允许点击结果
123
+ setTimeout(() => {
124
+ showResults.value = false
125
+ }, 200)
126
+ }
127
+
128
+ const handleResultClick = () => {
129
+ showResults.value = false
130
+ searchQuery.value = ''
131
+ searchResults.value = []
132
+ }
133
+
134
+ const highlightText = (text: string) => {
135
+ if (!searchQuery.value || !text) return text || ''
136
+
137
+ try {
138
+ // 转义特殊字符
139
+ const escapedQuery = searchQuery.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
140
+ const regex = new RegExp(`(${escapedQuery})`, 'gi')
141
+ return text.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-800 px-1 rounded">$1</mark>')
142
+ } catch {
143
+ return text
144
+ }
145
+ }
146
+
147
+ // 键盘快捷键支持
148
+ onMounted(() => {
149
+ // Ctrl/Cmd + K 打开搜索
150
+ document.addEventListener('keydown', (e) => {
151
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
152
+ e.preventDefault()
153
+ const input = document.querySelector('input[placeholder="搜索文章..."]') as HTMLInputElement
154
+ if (input) {
155
+ input.focus()
156
+ }
157
+ }
158
+ })
159
+ })
160
+ </script>
161
+
162
+ <style scoped>
163
+ .fade-enter-active,
164
+ .fade-leave-active {
165
+ transition: opacity 0.2s ease;
166
+ }
167
+
168
+ .fade-enter-from,
169
+ .fade-leave-to {
170
+ opacity: 0;
171
+ }
172
+
173
+ .line-clamp-2 {
174
+ display: -webkit-box;
175
+ -webkit-line-clamp: 2;
176
+ -webkit-box-orient: vertical;
177
+ overflow: hidden;
178
+ }
179
+ </style>
@@ -0,0 +1,409 @@
1
+ <template>
2
+ <div class="space-y-8">
3
+ <!-- 搜索框 -->
4
+ <div class="relative">
5
+ <div class="relative">
6
+ <input
7
+ v-model="searchQuery"
8
+ @input="handleSearch"
9
+ type="text"
10
+ placeholder="搜索文章标题、内容、标签..."
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
+ autofocus
13
+ />
14
+ <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
15
+ <svg class="w-6 h-6 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
17
+ </svg>
18
+ </div>
19
+ <div v-if="searchQuery" class="absolute inset-y-0 right-0 pr-4 flex items-center">
20
+ <button
21
+ @click="clearSearch"
22
+ class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
23
+ >
24
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
25
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
26
+ </svg>
27
+ </button>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- 筛选器 -->
33
+ <div class="flex flex-wrap gap-4">
34
+ <!-- 标签筛选 -->
35
+ <div class="flex items-center space-x-2">
36
+ <label class="text-sm font-medium text-slate-700 dark:text-slate-300">标签:</label>
37
+ <select
38
+ v-model="selectedTag"
39
+ @change="handleSearch"
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
+ >
42
+ <option value="">全部</option>
43
+ <option v-for="tag in allTags" :key="tag" :value="tag">{{ tag }}</option>
44
+ </select>
45
+ </div>
46
+
47
+ <!-- 分类筛选 -->
48
+ <div class="flex items-center space-x-2">
49
+ <label class="text-sm font-medium text-slate-700 dark:text-slate-300">分类:</label>
50
+ <select
51
+ v-model="selectedCategory"
52
+ @change="handleSearch"
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
+ >
55
+ <option value="">全部</option>
56
+ <option v-for="category in allCategories" :key="category" :value="category">{{ category }}</option>
57
+ </select>
58
+ </div>
59
+
60
+ <!-- 排序 -->
61
+ <div class="flex items-center space-x-2">
62
+ <label class="text-sm font-medium text-slate-700 dark:text-slate-300">排序:</label>
63
+ <select
64
+ v-model="sortBy"
65
+ @change="handleSearch"
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
+ >
68
+ <option value="relevance">相关性</option>
69
+ <option value="date">发布时间</option>
70
+ <option value="title">标题</option>
71
+ </select>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- 搜索状态 -->
76
+ <div class="flex items-center justify-between text-sm text-slate-600 dark:text-slate-400">
77
+ <div>
78
+ <span v-if="isSearching">正在搜索...</span>
79
+ <span v-else-if="searchQuery">
80
+ 找到 {{ searchResults.length }} 个结果
81
+ <span v-if="searchQuery">包含 "{{ searchQuery }}"</span>
82
+ </span>
83
+ <span v-else>输入关键词开始搜索</span>
84
+ </div>
85
+ <div v-if="searchTime">
86
+ 搜索用时: {{ searchTime }}ms
87
+ </div>
88
+ </div>
89
+
90
+ <!-- 搜索结果 -->
91
+ <div v-if="searchResults.length > 0" class="space-y-6">
92
+ <article
93
+ v-for="result in paginatedResults"
94
+ :key="result.url"
95
+ class="card hover:shadow-lg transition-shadow duration-200"
96
+ >
97
+ <a :href="result.url" class="block group">
98
+ <div class="flex flex-col lg:flex-row lg:items-center gap-4">
99
+ <!-- 文章信息 -->
100
+ <div class="flex-1">
101
+ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 group-hover:text-primary-500 transition-colors mb-2" v-html="highlightText(result.title)"></h3>
102
+
103
+ <p class="text-slate-600 dark:text-slate-400 mb-3 line-clamp-2" v-html="highlightText(result.description)"></p>
104
+
105
+ <div class="flex flex-wrap items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
106
+ <time :datetime="result.date">{{ formatDate(result.date) }}</time>
107
+ <span v-if="result.readingTime">{{ result.readingTime }}分钟阅读</span>
108
+ <div class="flex flex-wrap gap-1">
109
+ <span
110
+ v-for="tag in result.tags.slice(0, 3)"
111
+ :key="tag"
112
+ class="px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full text-xs"
113
+ >
114
+ {{ tag }}
115
+ </span>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- 相关度评分 -->
121
+ <div class="lg:w-20 text-center">
122
+ <div class="text-xs text-slate-500 dark:text-slate-400 mb-1">相关度</div>
123
+ <div class="flex items-center justify-center">
124
+ <div class="w-12 h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
125
+ <div
126
+ class="h-full bg-primary-500 rounded-full transition-all duration-300"
127
+ :style="{ width: `${Math.round(result.score * 100)}%` }"
128
+ ></div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </a>
134
+ </article>
135
+ </div>
136
+
137
+ <!-- 空状态 -->
138
+ <div v-else-if="searchQuery && !isSearching" class="text-center py-16">
139
+ <div class="text-6xl mb-4">🔍</div>
140
+ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-2">
141
+ 没有找到相关结果
142
+ </h3>
143
+ <p class="text-slate-600 dark:text-slate-400 mb-6">
144
+ 尝试使用不同的关键词或调整筛选条件
145
+ </p>
146
+ <button
147
+ @click="clearSearch"
148
+ class="btn-secondary"
149
+ >
150
+ 清除搜索
151
+ </button>
152
+ </div>
153
+
154
+ <!-- 分页 -->
155
+ <div v-if="totalPages > 1" class="flex justify-center">
156
+ <nav class="flex items-center space-x-2">
157
+ <button
158
+ @click="currentPage = Math.max(1, currentPage - 1)"
159
+ :disabled="currentPage === 1"
160
+ class="px-3 py-2 text-sm font-medium rounded-lg border transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
161
+ :class="currentPage === 1
162
+ ? 'bg-slate-100 dark:bg-slate-800 text-slate-400 border-slate-200 dark:border-slate-700'
163
+ : '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
+ >
165
+ 上一页
166
+ </button>
167
+
168
+ <div class="flex items-center space-x-1">
169
+ <button
170
+ v-for="page in getPageNumbers()"
171
+ :key="page"
172
+ @click="currentPage = page"
173
+ class="px-3 py-2 text-sm font-medium rounded-lg transition-colors"
174
+ :class="page === currentPage
175
+ ? 'bg-primary-500 text-white'
176
+ : 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700'"
177
+ >
178
+ {{ page }}
179
+ </button>
180
+ </div>
181
+
182
+ <button
183
+ @click="currentPage = Math.min(totalPages, currentPage + 1)"
184
+ :disabled="currentPage === totalPages"
185
+ class="px-3 py-2 text-sm font-medium rounded-lg border transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
186
+ :class="currentPage === totalPages
187
+ ? 'bg-slate-100 dark:bg-slate-800 text-slate-400 border-slate-200 dark:border-slate-700'
188
+ : '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
+ >
190
+ 下一页
191
+ </button>
192
+ </nav>
193
+ </div>
194
+ </div>
195
+ </template>
196
+
197
+ <script setup lang="ts">
198
+ import { ref, computed, onMounted } from 'vue'
199
+ import type { SearchResult } from '../../types'
200
+
201
+ const searchQuery = ref('')
202
+ const selectedTag = ref('')
203
+ const selectedCategory = ref('')
204
+ const sortBy = ref('relevance')
205
+ const searchResults = ref<(SearchResult & { score: number; date: string; readingTime?: number })[]>([])
206
+ const isSearching = ref(false)
207
+ const searchTime = ref(0)
208
+ const currentPage = ref(1)
209
+ const resultsPerPage = 10
210
+
211
+ // 模拟搜索数据
212
+ const mockSearchData = [
213
+ {
214
+ title: '欢迎来到Astro技术博客',
215
+ description: '这是使用Astro构建的现代化技术博客的第一篇文章,介绍博客的特性和使用方法。',
216
+ url: '/posts/welcome-to-astro',
217
+ content: 'Astro Vue TypeScript Tailwind CSS 博客 技术分享 现代化 静态站点生成',
218
+ tags: ['Astro', 'Blog', 'TypeScript', 'Tailwind CSS'],
219
+ categories: ['技术分享'],
220
+ date: '2025-01-08',
221
+ readingTime: 5
222
+ },
223
+ {
224
+ title: 'Astro vs Next.js:静态站点生成器的对比',
225
+ description: '深入比较Astro和Next.js在性能、开发体验和生态系统方面的差异。',
226
+ url: '/posts/astro-vs-nextjs',
227
+ content: 'Astro Next.js 对比 性能 SSG 静态站点生成 开发体验',
228
+ tags: ['Astro', 'Next.js', 'SSG'],
229
+ categories: ['技术比较'],
230
+ date: '2025-01-07',
231
+ readingTime: 8
232
+ },
233
+ {
234
+ title: 'Tailwind CSS最佳实践指南',
235
+ description: '总结使用Tailwind CSS开发时的最佳实践和常见问题解决方案。',
236
+ url: '/posts/tailwind-best-practices',
237
+ content: 'Tailwind CSS 最佳实践 响应式设计 工具类 优化',
238
+ tags: ['Tailwind CSS', 'CSS', '最佳实践'],
239
+ categories: ['前端开发'],
240
+ date: '2025-01-06',
241
+ readingTime: 6
242
+ }
243
+ ]
244
+
245
+ const allTags = computed(() => {
246
+ const tags = new Set<string>()
247
+ mockSearchData.forEach(item => {
248
+ item.tags.forEach(tag => tags.add(tag))
249
+ })
250
+ return Array.from(tags).sort()
251
+ })
252
+
253
+ const allCategories = computed(() => {
254
+ const categories = new Set<string>()
255
+ mockSearchData.forEach(item => {
256
+ item.categories.forEach(category => categories.add(category))
257
+ })
258
+ return Array.from(categories).sort()
259
+ })
260
+
261
+ const totalPages = computed(() => {
262
+ return Math.ceil(searchResults.value.length / resultsPerPage)
263
+ })
264
+
265
+ const paginatedResults = computed(() => {
266
+ const start = (currentPage.value - 1) * resultsPerPage
267
+ const end = start + resultsPerPage
268
+ return searchResults.value.slice(start, end)
269
+ })
270
+
271
+ const performSearch = () => {
272
+ const startTime = performance.now()
273
+ isSearching.value = true
274
+
275
+ // 模拟搜索延迟
276
+ setTimeout(() => {
277
+ let results = [...mockSearchData]
278
+
279
+ // 文本搜索
280
+ if (searchQuery.value) {
281
+ const query = searchQuery.value.toLowerCase()
282
+ results = results.map(item => {
283
+ let score = 0
284
+
285
+ // 标题匹配 (权重最高)
286
+ if (item.title.toLowerCase().includes(query)) {
287
+ score += 10
288
+ }
289
+
290
+ // 描述匹配
291
+ if (item.description.toLowerCase().includes(query)) {
292
+ score += 5
293
+ }
294
+
295
+ // 内容匹配
296
+ if (item.content.toLowerCase().includes(query)) {
297
+ score += 3
298
+ }
299
+
300
+ // 标签匹配
301
+ if (item.tags.some(tag => tag.toLowerCase().includes(query))) {
302
+ score += 7
303
+ }
304
+
305
+ // 分类匹配
306
+ if (item.categories.some(category => category.toLowerCase().includes(query))) {
307
+ score += 6
308
+ }
309
+
310
+ return { ...item, score: Math.min(score / 10, 1) }
311
+ }).filter(item => item.score > 0)
312
+ } else {
313
+ results = results.map(item => ({ ...item, score: 1 }))
314
+ }
315
+
316
+ // 标签筛选
317
+ if (selectedTag.value) {
318
+ results = results.filter(item => item.tags.includes(selectedTag.value))
319
+ }
320
+
321
+ // 分类筛选
322
+ if (selectedCategory.value) {
323
+ results = results.filter(item => item.categories.includes(selectedCategory.value))
324
+ }
325
+
326
+ // 排序
327
+ switch (sortBy.value) {
328
+ case 'date':
329
+ results.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
330
+ break
331
+ case 'title':
332
+ results.sort((a, b) => a.title.localeCompare(b.title))
333
+ break
334
+ case 'relevance':
335
+ default:
336
+ results.sort((a, b) => b.score - a.score)
337
+ break
338
+ }
339
+
340
+ searchResults.value = results
341
+ isSearching.value = false
342
+ searchTime.value = Math.round(performance.now() - startTime)
343
+ currentPage.value = 1
344
+ }, 200)
345
+ }
346
+
347
+ const handleSearch = () => {
348
+ performSearch()
349
+ }
350
+
351
+ const clearSearch = () => {
352
+ searchQuery.value = ''
353
+ selectedTag.value = ''
354
+ selectedCategory.value = ''
355
+ searchResults.value = []
356
+ currentPage.value = 1
357
+ }
358
+
359
+ const highlightText = (text: string) => {
360
+ if (!searchQuery.value) return text
361
+
362
+ const regex = new RegExp(`(${searchQuery.value})`, 'gi')
363
+ return text.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-800 px-1 rounded">$1</mark>')
364
+ }
365
+
366
+ const formatDate = (date: string) => {
367
+ return new Intl.DateTimeFormat('zh-CN', {
368
+ year: 'numeric',
369
+ month: 'long',
370
+ day: 'numeric'
371
+ }).format(new Date(date))
372
+ }
373
+
374
+ const getPageNumbers = () => {
375
+ const maxPages = 5
376
+ const half = Math.floor(maxPages / 2)
377
+ let start = Math.max(1, currentPage.value - half)
378
+ let end = Math.min(totalPages.value, start + maxPages - 1)
379
+
380
+ if (end - start < maxPages - 1) {
381
+ start = Math.max(1, end - maxPages + 1)
382
+ }
383
+
384
+ const pages = []
385
+ for (let i = start; i <= end; i++) {
386
+ pages.push(i)
387
+ }
388
+ return pages
389
+ }
390
+
391
+ onMounted(() => {
392
+ // 如果URL中有搜索参数,自动执行搜索
393
+ const urlParams = new URLSearchParams(window.location.search)
394
+ const query = urlParams.get('q')
395
+ if (query) {
396
+ searchQuery.value = query
397
+ performSearch()
398
+ }
399
+ })
400
+ </script>
401
+
402
+ <style scoped>
403
+ .line-clamp-2 {
404
+ display: -webkit-box;
405
+ -webkit-line-clamp: 2;
406
+ -webkit-box-orient: vertical;
407
+ overflow: hidden;
408
+ }
409
+ </style>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <button
3
+ @click="toggleSidebar"
4
+ :title="isCollapsed ? '显示侧边栏' : '隐藏侧边栏'"
5
+ class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors text-slate-600 hover:text-primary-500 dark:text-slate-400 dark:hover:text-primary-400"
6
+ >
7
+ <svg
8
+ class="w-4 h-4 transition-transform duration-200"
9
+ :class="{ 'rotate-180': isCollapsed }"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ viewBox="0 0 24 24"
13
+ >
14
+ <path
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ stroke-width="2"
18
+ d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
19
+ />
20
+ </svg>
21
+ </button>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { ref, onMounted, watch } from 'vue';
26
+
27
+ const isCollapsed = ref(false);
28
+
29
+ // 从localStorage恢复状态
30
+ onMounted(() => {
31
+ setTimeout(() => {
32
+ const saved = localStorage.getItem('sidebar-collapsed');
33
+ if (saved) {
34
+ isCollapsed.value = JSON.parse(saved);
35
+ }
36
+ applySidebarState();
37
+ }, 100);
38
+ });
39
+
40
+ // 监听状态变化
41
+ watch(isCollapsed, (newValue) => {
42
+ localStorage.setItem('sidebar-collapsed', JSON.stringify(newValue));
43
+ applySidebarState();
44
+ });
45
+
46
+ function toggleSidebar() {
47
+ isCollapsed.value = !isCollapsed.value;
48
+ }
49
+
50
+ function applySidebarState() {
51
+ if (isCollapsed.value) {
52
+ document.body.classList.add('sidebar-collapsed');
53
+ } else {
54
+ document.body.classList.remove('sidebar-collapsed');
55
+ }
56
+ }
57
+ </script>