@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,187 @@
1
+ ---
2
+ /**
3
+ * 通用图标组件
4
+ * 支持多种图标格式:
5
+ *
6
+ * 1. Emoji: 直接使用 emoji 字符,如 "📁" "🎉"
7
+ *
8
+ * 2. Font Awesome:
9
+ * - "fa:folder" → <i class="fa-solid fa-folder"></i>
10
+ * - "fa-brands:github" → <i class="fa-brands fa-github"></i>
11
+ * - "fa-regular:star" → <i class="fa-regular fa-star"></i>
12
+ *
13
+ * 3. Material Icons:
14
+ * - "mi:folder" → <span class="material-icons">folder</span>
15
+ * - "mi-outlined:folder" → <span class="material-icons-outlined">folder</span>
16
+ * - "mi-round:folder" → <span class="material-icons-round">folder</span>
17
+ *
18
+ * 4. Bootstrap Icons:
19
+ * - "bi:folder" → <i class="bi bi-folder"></i>
20
+ *
21
+ * 5. Remix Icon:
22
+ * - "ri:folder-line" → <i class="ri-folder-line"></i>
23
+ * - "ri:folder-fill" → <i class="ri-folder-fill"></i>
24
+ *
25
+ * 6. Ionicons:
26
+ * - "ion:folder" → <ion-icon name="folder"></ion-icon>
27
+ * - "ion:folder-outline" → <ion-icon name="folder-outline"></ion-icon>
28
+ */
29
+
30
+ export interface Props {
31
+ icon: string;
32
+ class?: string;
33
+ size?: string;
34
+ }
35
+
36
+ const { icon, class: className = '', size = '1em' } = Astro.props;
37
+
38
+ // 解析图标格式
39
+ function parseIcon(iconStr: string) {
40
+ if (!iconStr) return { type: 'none', value: '' };
41
+
42
+ // 检测是否有前缀
43
+ const hasPrefix = /^[a-z-]+:/i.test(iconStr);
44
+
45
+ if (!hasPrefix) {
46
+ // 检测是否为 emoji(包含非 ASCII 字符)
47
+ const isEmoji = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/u.test(iconStr);
48
+ if (isEmoji) {
49
+ return { type: 'emoji', value: iconStr };
50
+ }
51
+
52
+ // 没有前缀且不是 emoji,默认当作 Font Awesome solid 图标
53
+ return { type: 'fa', style: 'solid', value: iconStr };
54
+ }
55
+
56
+ const [prefix, ...rest] = iconStr.split(':');
57
+ const value = rest.join(':');
58
+
59
+ switch (prefix.toLowerCase()) {
60
+ // Font Awesome
61
+ case 'fa':
62
+ return { type: 'fa', style: 'solid', value };
63
+ case 'fa-solid':
64
+ case 'fas':
65
+ return { type: 'fa', style: 'solid', value };
66
+ case 'fa-regular':
67
+ case 'far':
68
+ return { type: 'fa', style: 'regular', value };
69
+ case 'fa-brands':
70
+ case 'fab':
71
+ return { type: 'fa', style: 'brands', value };
72
+ case 'fa-light':
73
+ case 'fal':
74
+ return { type: 'fa', style: 'light', value };
75
+ case 'fa-thin':
76
+ case 'fat':
77
+ return { type: 'fa', style: 'thin', value };
78
+ case 'fa-duotone':
79
+ case 'fad':
80
+ return { type: 'fa', style: 'duotone', value };
81
+
82
+ // Material Icons
83
+ case 'mi':
84
+ case 'material':
85
+ return { type: 'material', style: 'filled', value };
86
+ case 'mi-outlined':
87
+ case 'material-outlined':
88
+ return { type: 'material', style: 'outlined', value };
89
+ case 'mi-round':
90
+ case 'material-round':
91
+ return { type: 'material', style: 'round', value };
92
+
93
+ // Bootstrap Icons
94
+ case 'bi':
95
+ case 'bootstrap':
96
+ return { type: 'bootstrap', value };
97
+
98
+ // Remix Icon
99
+ case 'ri':
100
+ case 'remix':
101
+ return { type: 'remix', value };
102
+
103
+ // Ionicons
104
+ case 'ion':
105
+ case 'ionicon':
106
+ return { type: 'ionicon', value };
107
+
108
+ default:
109
+ // 未知前缀,当作 emoji 处理
110
+ return { type: 'emoji', value: iconStr };
111
+ }
112
+ }
113
+
114
+ const parsed = parseIcon(icon);
115
+ ---
116
+
117
+ {parsed.type === 'emoji' && (
118
+ <span class:list={['icon-emoji', className]} style={`font-size: ${size};`}>{parsed.value}</span>
119
+ )}
120
+
121
+ {parsed.type === 'fa' && (
122
+ <i
123
+ class:list={[`fa-${parsed.style}`, `fa-${parsed.value}`, className]}
124
+ style={`font-size: ${size};`}
125
+ ></i>
126
+ )}
127
+
128
+ {parsed.type === 'material' && parsed.style === 'filled' && (
129
+ <span
130
+ class:list={['material-icons', className]}
131
+ style={`font-size: ${size};`}
132
+ >{parsed.value}</span>
133
+ )}
134
+
135
+ {parsed.type === 'material' && parsed.style === 'outlined' && (
136
+ <span
137
+ class:list={['material-icons-outlined', className]}
138
+ style={`font-size: ${size};`}
139
+ >{parsed.value}</span>
140
+ )}
141
+
142
+ {parsed.type === 'material' && parsed.style === 'round' && (
143
+ <span
144
+ class:list={['material-icons-round', className]}
145
+ style={`font-size: ${size};`}
146
+ >{parsed.value}</span>
147
+ )}
148
+
149
+ {parsed.type === 'bootstrap' && (
150
+ <i
151
+ class:list={['bi', `bi-${parsed.value}`, className]}
152
+ style={`font-size: ${size};`}
153
+ ></i>
154
+ )}
155
+
156
+ {parsed.type === 'remix' && (
157
+ <i
158
+ class:list={[`ri-${parsed.value}`, className]}
159
+ style={`font-size: ${size};`}
160
+ ></i>
161
+ )}
162
+
163
+ {parsed.type === 'ionicon' && (
164
+ <ion-icon
165
+ name={parsed.value}
166
+ class={className}
167
+ style={`font-size: ${size};`}
168
+ ></ion-icon>
169
+ )}
170
+
171
+ <style>
172
+ .icon-emoji {
173
+ display: inline-flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ line-height: 1;
177
+ }
178
+
179
+ /* 统一图标垂直对齐 */
180
+ i, span.material-icons, span.material-icons-outlined, span.material-icons-round, ion-icon {
181
+ display: inline-flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ vertical-align: middle;
185
+ line-height: 1;
186
+ }
187
+ </style>
@@ -0,0 +1,201 @@
1
+ <template>
2
+ <div>
3
+ <!-- 菜单按钮 -->
4
+ <button
5
+ @click="isOpen = !isOpen"
6
+ class="p-2 rounded-lg bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 transition-colors"
7
+ aria-label="菜单"
8
+ >
9
+ <transition name="rotate" mode="out-in">
10
+ <svg
11
+ v-if="!isOpen"
12
+ key="menu"
13
+ class="w-5 h-5 text-slate-700 dark:text-slate-300"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ viewBox="0 0 24 24"
17
+ >
18
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
19
+ </svg>
20
+ <svg
21
+ v-else
22
+ key="close"
23
+ class="w-5 h-5 text-slate-700 dark:text-slate-300"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ viewBox="0 0 24 24"
27
+ >
28
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
29
+ </svg>
30
+ </transition>
31
+ </button>
32
+
33
+ <!-- 使用 Teleport 将菜单渲染到 body -->
34
+ <Teleport to="body" v-if="isMounted">
35
+ <!-- 遮罩层 -->
36
+ <transition name="fade">
37
+ <div
38
+ v-if="isOpen"
39
+ @click="isOpen = false"
40
+ class="mobile-menu-overlay"
41
+ ></div>
42
+ </transition>
43
+
44
+ <!-- 侧边菜单 -->
45
+ <transition name="slide">
46
+ <div
47
+ v-if="isOpen"
48
+ class="mobile-menu-panel"
49
+ >
50
+ <div class="p-6">
51
+ <!-- 头部 -->
52
+ <div class="flex items-center justify-between mb-8">
53
+ <h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100">菜单</h2>
54
+ <button
55
+ @click="isOpen = false"
56
+ class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
57
+ >
58
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
59
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
60
+ </svg>
61
+ </button>
62
+ </div>
63
+
64
+ <!-- 搜索框 -->
65
+ <div class="mb-8 sm:hidden">
66
+ <SearchBox />
67
+ </div>
68
+
69
+ <!-- 导航链接 -->
70
+ <nav class="space-y-2">
71
+ <a
72
+ v-for="item in navigation"
73
+ :key="item.href"
74
+ :href="item.href"
75
+ @click="isOpen = false"
76
+ class="flex items-center px-4 py-3 rounded-lg text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
77
+ :class="{ 'bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400': isActive(item.href) }"
78
+ >
79
+ <span class="font-medium">{{ item.name }}</span>
80
+ </a>
81
+ </nav>
82
+
83
+ <!-- 分隔线 -->
84
+ <hr class="my-8 border-slate-200 dark:border-slate-700" />
85
+
86
+ <!-- 快捷功能 -->
87
+ <div class="space-y-4">
88
+ <div class="flex items-center justify-between">
89
+ <span class="text-sm font-medium text-slate-700 dark:text-slate-300">深色模式</span>
90
+ <ThemeToggle />
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </transition>
96
+ </Teleport>
97
+ </div>
98
+ </template>
99
+
100
+ <script setup lang="ts">
101
+ import { ref, onMounted, onUnmounted } from 'vue'
102
+ import type { NavigationItem } from '../../types'
103
+ import SearchBox from './SearchBox.vue'
104
+ import ThemeToggle from './ThemeToggle.vue'
105
+
106
+ interface Props {
107
+ navigation: NavigationItem[]
108
+ }
109
+
110
+ const props = defineProps<Props>()
111
+ const isOpen = ref(false)
112
+ const currentPath = ref('')
113
+ const isMounted = ref(false)
114
+
115
+ const isActive = (href: string) => {
116
+ return currentPath.value === href || (href !== '/' && currentPath.value.startsWith(href))
117
+ }
118
+
119
+ const handleEscape = (event: KeyboardEvent) => {
120
+ if (event.key === 'Escape') {
121
+ isOpen.value = false
122
+ }
123
+ }
124
+
125
+ onMounted(() => {
126
+ isMounted.value = true
127
+ currentPath.value = window.location.pathname
128
+ document.addEventListener('keydown', handleEscape)
129
+
130
+ // 监听路由变化(如果使用客户端路由)
131
+ window.addEventListener('popstate', () => {
132
+ currentPath.value = window.location.pathname
133
+ })
134
+ })
135
+
136
+ onUnmounted(() => {
137
+ document.removeEventListener('keydown', handleEscape)
138
+ })
139
+ </script>
140
+
141
+ <style scoped>
142
+ .rotate-enter-active,
143
+ .rotate-leave-active {
144
+ transition: transform 0.2s ease;
145
+ }
146
+
147
+ .rotate-enter-from,
148
+ .rotate-leave-to {
149
+ transform: rotate(90deg);
150
+ }
151
+ </style>
152
+
153
+ <style>
154
+ /* 非 scoped 样式,用于 Teleport 渲染的内容 */
155
+ .mobile-menu-overlay {
156
+ position: fixed;
157
+ top: 0;
158
+ left: 0;
159
+ right: 0;
160
+ bottom: 0;
161
+ background-color: rgba(0, 0, 0, 0.5);
162
+ z-index: 99998;
163
+ }
164
+
165
+ .mobile-menu-panel {
166
+ position: fixed;
167
+ top: 0;
168
+ right: 0;
169
+ height: 100%;
170
+ width: 20rem;
171
+ max-width: 80vw;
172
+ background-color: white;
173
+ box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
174
+ z-index: 99999;
175
+ overflow-y: auto;
176
+ }
177
+
178
+ .dark .mobile-menu-panel {
179
+ background-color: rgb(15 23 42);
180
+ }
181
+
182
+ .fade-enter-active,
183
+ .fade-leave-active {
184
+ transition: opacity 0.3s ease;
185
+ }
186
+
187
+ .fade-enter-from,
188
+ .fade-leave-to {
189
+ opacity: 0;
190
+ }
191
+
192
+ .slide-enter-active,
193
+ .slide-leave-active {
194
+ transition: transform 0.3s ease;
195
+ }
196
+
197
+ .slide-enter-from,
198
+ .slide-leave-to {
199
+ transform: translateX(100%);
200
+ }
201
+ </style>
@@ -0,0 +1,143 @@
1
+ ---
2
+ export interface Props {
3
+ currentPage: number;
4
+ totalPages: number;
5
+ baseUrl: string;
6
+ }
7
+
8
+ const { currentPage, totalPages, baseUrl } = Astro.props;
9
+
10
+ // 生成页码数组
11
+ const generatePageNumbers = () => {
12
+ const pages = [];
13
+ const showPages = 5; // 显示的页码数量
14
+ const halfShow = Math.floor(showPages / 2);
15
+
16
+ let start = Math.max(1, currentPage - halfShow);
17
+ let end = Math.min(totalPages, start + showPages - 1);
18
+
19
+ // 调整起始位置
20
+ if (end - start < showPages - 1) {
21
+ start = Math.max(1, end - showPages + 1);
22
+ }
23
+
24
+ for (let i = start; i <= end; i++) {
25
+ pages.push(i);
26
+ }
27
+
28
+ return pages;
29
+ };
30
+
31
+ const pageNumbers = generatePageNumbers();
32
+ const prevPage = currentPage > 1 ? currentPage - 1 : null;
33
+ const nextPage = currentPage < totalPages ? currentPage + 1 : null;
34
+
35
+ const getPageUrl = (page: number) => {
36
+ if (page === 1) {
37
+ return baseUrl;
38
+ }
39
+ return `${baseUrl}/page/${page}`;
40
+ };
41
+ ---
42
+
43
+ {totalPages > 1 && (
44
+ <nav class="flex justify-center" aria-label="分页导航">
45
+ <div class="flex items-center space-x-2">
46
+ <!-- 上一页 -->
47
+ {prevPage ? (
48
+ <a
49
+ href={getPageUrl(prevPage)}
50
+ class="flex items-center space-x-1 px-3 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
51
+ aria-label="上一页"
52
+ >
53
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
55
+ </svg>
56
+ <span>上一页</span>
57
+ </a>
58
+ ) : (
59
+ <span class="flex items-center space-x-1 px-3 py-2 text-sm font-medium text-slate-400 dark:text-slate-600 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg cursor-not-allowed">
60
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
61
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
62
+ </svg>
63
+ <span>上一页</span>
64
+ </span>
65
+ )}
66
+
67
+ <!-- 第一页 -->
68
+ {pageNumbers[0] > 1 && (
69
+ <>
70
+ <a
71
+ href={getPageUrl(1)}
72
+ class="px-3 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
73
+ >
74
+ 1
75
+ </a>
76
+ {pageNumbers[0] > 2 && (
77
+ <span class="px-2 py-2 text-slate-400 dark:text-slate-600">...</span>
78
+ )}
79
+ </>
80
+ )}
81
+
82
+ <!-- 页码 -->
83
+ {pageNumbers.map((page) => (
84
+ page === currentPage ? (
85
+ <span
86
+ class="px-3 py-2 text-sm font-medium text-white bg-primary-500 border border-primary-500 rounded-lg"
87
+ aria-current="page"
88
+ >
89
+ {page}
90
+ </span>
91
+ ) : (
92
+ <a
93
+ href={getPageUrl(page)}
94
+ class="px-3 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
95
+ >
96
+ {page}
97
+ </a>
98
+ )
99
+ ))}
100
+
101
+ <!-- 最后一页 -->
102
+ {pageNumbers[pageNumbers.length - 1] < totalPages && (
103
+ <>
104
+ {pageNumbers[pageNumbers.length - 1] < totalPages - 1 && (
105
+ <span class="px-2 py-2 text-slate-400 dark:text-slate-600">...</span>
106
+ )}
107
+ <a
108
+ href={getPageUrl(totalPages)}
109
+ class="px-3 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
110
+ >
111
+ {totalPages}
112
+ </a>
113
+ </>
114
+ )}
115
+
116
+ <!-- 下一页 -->
117
+ {nextPage ? (
118
+ <a
119
+ href={getPageUrl(nextPage)}
120
+ class="flex items-center space-x-1 px-3 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
121
+ aria-label="下一页"
122
+ >
123
+ <span>下一页</span>
124
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
125
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
126
+ </svg>
127
+ </a>
128
+ ) : (
129
+ <span class="flex items-center space-x-1 px-3 py-2 text-sm font-medium text-slate-400 dark:text-slate-600 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg cursor-not-allowed">
130
+ <span>下一页</span>
131
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
132
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
133
+ </svg>
134
+ </span>
135
+ )}
136
+ </div>
137
+
138
+ <!-- 页面信息 -->
139
+ <div class="hidden sm:flex items-center ml-8 text-sm text-slate-600 dark:text-slate-400">
140
+ 第 {currentPage} 页,共 {totalPages} 页
141
+ </div>
142
+ </nav>
143
+ )}