clarity-theme 0.1.0 → 0.1.2

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 (154) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +70 -4
  3. package/README.zh-CN.md +67 -4
  4. package/nuxt.config.ts +217 -214
  5. package/package.json +35 -37
  6. package/{app → src}/app.config.ts +110 -110
  7. package/{app → src}/app.vue +5 -5
  8. package/{app → src}/components/blog/BlogAside.vue +25 -25
  9. package/{app → src}/components/blog/BlogFooter.vue +25 -25
  10. package/{app → src}/components/blog/BlogHeader.global.vue +48 -48
  11. package/{app → src}/components/blog/BlogPanel.vue +43 -43
  12. package/{app → src}/components/blog/BlogSidebar.vue +53 -53
  13. package/{app → src}/components/blog/BlogWidget.vue +42 -42
  14. package/{app → src}/components/blog/Mask.vue +25 -25
  15. package/{app → src}/components/blog/SkipToContent.vue +9 -9
  16. package/{app → src}/components/blog/ThemeToggle.vue +22 -22
  17. package/{app → src}/components/content/Badge.vue +45 -45
  18. package/{app → src}/components/content/Blur.vue +14 -14
  19. package/{app → src}/components/content/CardList.vue +8 -8
  20. package/{app → src}/components/content/Chat.vue +41 -41
  21. package/{app → src}/components/content/Copy.vue +71 -71
  22. package/{app → src}/components/content/EmojiClock.vue +49 -49
  23. package/{app → src}/components/content/FeedCard.vue +96 -96
  24. package/{app → src}/components/content/FeedGroup.vue +61 -61
  25. package/{app → src}/components/content/Folding.vue +19 -19
  26. package/{app → src}/components/content/Key.vue +143 -143
  27. package/{app → src}/components/content/LinkBanner.vue +27 -27
  28. package/{app → src}/components/content/LinkCard.vue +28 -28
  29. package/{app → src}/components/content/MdTitle.vue +8 -8
  30. package/{app → src}/components/content/Mermaid.vue +99 -99
  31. package/{app → src}/components/content/MusicScore.vue +63 -63
  32. package/{app → src}/components/content/Pic.vue +49 -49
  33. package/{app → src}/components/content/Poetry.vue +27 -27
  34. package/{app → src}/components/content/ProseA.vue +24 -24
  35. package/{app → src}/components/content/ProseCode.vue +16 -16
  36. package/{app → src}/components/content/ProsePre.vue +117 -117
  37. package/{app → src}/components/content/ProseTable.vue +23 -23
  38. package/{app → src}/components/content/Quote.vue +21 -21
  39. package/{app → src}/components/content/Tab.vue +32 -32
  40. package/{app → src}/components/content/Timeline.vue +33 -33
  41. package/{app → src}/components/content/Tip.vue +40 -40
  42. package/{app → src}/components/content/VideoEmbed.vue +112 -112
  43. package/{app → src}/components/partial/Button.vue +36 -36
  44. package/{app → src}/components/partial/DlGroup.vue +31 -31
  45. package/{app → src}/components/partial/Dropdown.vue +21 -21
  46. package/{app → src}/components/partial/Error.vue +39 -39
  47. package/{app → src}/components/partial/Expand.vue +26 -26
  48. package/{app → src}/components/partial/IconNavList.vue +24 -24
  49. package/{app → src}/components/partial/Pagination.vue +62 -62
  50. package/{app → src}/components/partial/RadioGroup.vue +30 -30
  51. package/{app → src}/components/partial/Secret.vue +11 -11
  52. package/{app → src}/components/partial/Slider.vue +48 -48
  53. package/{app → src}/components/partial/Toggle.vue +17 -17
  54. package/{app → src}/components/popover/Lightbox.vue +44 -44
  55. package/{app → src}/components/popover/Search.vue +143 -143
  56. package/{app → src}/components/popover/SearchItem.vue +28 -28
  57. package/{app → src}/components/post/Archive.vue +40 -40
  58. package/{app → src}/components/post/Article.vue +49 -49
  59. package/{app → src}/components/post/Comment.vue +119 -119
  60. package/{app → src}/components/post/Excerpt.vue +40 -40
  61. package/{app → src}/components/post/OrderToggle.vue +70 -70
  62. package/{app → src}/components/post/PostFooter.vue +52 -52
  63. package/{app → src}/components/post/PostHeader.vue +66 -66
  64. package/{app → src}/components/post/PostSurround.vue +51 -51
  65. package/{app → src}/components/post/Slide.vue +85 -85
  66. package/{app → src}/components/util/Date.vue +62 -62
  67. package/{app → src}/components/util/HydrateSafe.vue +10 -10
  68. package/{app → src}/components/util/Img.vue +38 -38
  69. package/{app → src}/components/util/Link.vue +11 -11
  70. package/{app → src}/components/widget/BlogLog.vue +23 -23
  71. package/{app → src}/components/widget/BlogStats.vue +38 -38
  72. package/{app → src}/components/widget/BlogTech.vue +57 -57
  73. package/{app → src}/components/widget/Empty.vue +3 -3
  74. package/{app → src}/components/widget/Toc.vue +67 -67
  75. package/{app → src}/composables/useArticle.ts +115 -115
  76. package/{app → src}/composables/useAvoid.ts +87 -87
  77. package/{app → src}/composables/useCopy.ts +21 -21
  78. package/{app → src}/composables/usePagination.ts +70 -70
  79. package/{app → src}/composables/useShiki.ts +129 -129
  80. package/{app → src}/composables/useToc.ts +57 -57
  81. package/{app → src}/composables/useWidgets.ts +56 -56
  82. package/{config → src/config}/app.ts +62 -62
  83. package/{config → src/config}/content.ts +75 -75
  84. package/{config → src/config}/define.ts +3 -3
  85. package/src/config/feeds.empty.ts +6 -0
  86. package/{config → src/config}/index.ts +1 -1
  87. package/{config → src/config}/schema.ts +89 -89
  88. package/{app → src}/error.vue +31 -31
  89. package/{img → src/img}/index.ts +2 -2
  90. package/{app → src}/layouts/default.vue +25 -25
  91. package/{modules → src/modules}/clarity-config/index.ts +168 -162
  92. package/src/modules/clarity-source-layout/index.ts +78 -0
  93. package/{app → src}/pages/[...slug].vue +59 -59
  94. package/{app → src}/pages/archive.vue +129 -129
  95. package/{app → src}/pages/index.vue +76 -76
  96. package/{app → src}/pages/link.vue +62 -62
  97. package/{app → src}/pages/preview.vue +46 -46
  98. package/{app → src}/plugins/easter-egg.ts +23 -23
  99. package/{app → src}/plugins/init.ts +6 -6
  100. package/{app → src}/plugins/tippy.ts +22 -22
  101. package/{public → src/public}/assets/atom.css +274 -274
  102. package/{public → src/public}/assets/atom.xsl +109 -109
  103. package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.ts +58 -58
  104. package/{remark-plugins → src/remark-plugins}/remark-code-component.ts +41 -41
  105. package/{server → src/server}/api/stats.get.ts +84 -83
  106. package/{server → src/server}/routes/atom.xml.get.ts +85 -85
  107. package/{server → src/server}/routes/subscriptions.opml.get.ts +64 -64
  108. package/{shared → src/shared}/utils/clarity.ts +24 -24
  109. package/{shared → src/shared}/utils/icon.ts +218 -218
  110. package/{shared → src/shared}/utils/link.ts +52 -52
  111. package/{shared → src/shared}/utils/str.ts +88 -88
  112. package/{shared → src/shared}/utils/time.ts +115 -115
  113. package/{app → src}/shiki.config.ts +8 -8
  114. package/{app → src}/stores/layout.ts +34 -34
  115. package/{app → src}/stores/search.ts +37 -37
  116. package/{app → src}/types/article.ts +16 -16
  117. package/src/types/feed.ts +1 -0
  118. package/{app → src}/types/index.ts +14 -14
  119. package/{app → src}/types/nav.ts +11 -11
  120. package/{app → src}/utils/anim.ts +37 -37
  121. package/{app → src}/utils/img.ts +83 -83
  122. package/app/types/feed.ts +0 -1
  123. package/config/feeds.empty.ts +0 -4
  124. package/{app → src}/assets/css/_variable.scss +0 -0
  125. package/{app → src}/assets/css/animation.scss +0 -0
  126. package/{app → src}/assets/css/article.scss +0 -0
  127. package/{app → src}/assets/css/color.scss +0 -0
  128. package/{app → src}/assets/css/font.scss +0 -0
  129. package/{app → src}/assets/css/main.scss +0 -0
  130. package/{app → src}/assets/css/reusable.scss +0 -0
  131. package/{app → src}/assets/icons/solar-rewind-back-bold-duotone.svg +0 -0
  132. package/{app → src}/assets/icons/solar-rewind-forward-bold-duotone.svg +0 -0
  133. package/{app → src}/components/content/Alert.vue +0 -0
  134. package/{config → src/config}/content.d.mts +0 -0
  135. package/{config → src/config}/content.mjs +0 -0
  136. package/{config → src/config}/define.mjs +0 -0
  137. package/{config → src/config}/feed.ts +0 -0
  138. package/{config → src/config}/index.d.mts +0 -0
  139. package/{config → src/config}/index.mjs +0 -0
  140. package/{config → src/config}/public.ts +1 -1
  141. package/{config → src/config}/schema.d.mts +0 -0
  142. package/{config → src/config}/schema.mjs +0 -0
  143. package/{config → src/config}/server.ts +1 -1
  144. /package/{img → src/img}/index.d.mts +0 -0
  145. /package/{img → src/img}/index.mjs +0 -0
  146. /package/{modules → src/modules}/clarity-config/anti-mirror-client.ts +0 -0
  147. /package/{public → src/public}/fonts/AlimamaFangYuanTi.woff2 +0 -0
  148. /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.d.mts +0 -0
  149. /package/{remark-plugins → src/remark-plugins}/rehype-meta-slots.mjs +0 -0
  150. /package/{remark-plugins → src/remark-plugins}/remark-code-component.d.mts +0 -0
  151. /package/{remark-plugins → src/remark-plugins}/remark-code-component.mjs +0 -0
  152. /package/{server → src/server}/utils/clarity.ts +0 -0
  153. /package/{shared → src/shared}/types/content.ts +0 -0
  154. /package/{app → src}/types/modal.ts +0 -0
@@ -1,10 +1,10 @@
1
- <!-- SSG 下让客户端抛弃预渲染结果重新 CSR -->
2
- <template>
3
- <ClientOnly>
4
- <slot />
5
-
6
- <template #fallback>
7
- <slot />
8
- </template>
9
- </ClientOnly>
10
- </template>
1
+ <!-- SSG 下让客户端抛弃预渲染结果重新 CSR -->
2
+ <template>
3
+ <ClientOnly>
4
+ <slot />
5
+
6
+ <template #fallback>
7
+ <slot />
8
+ </template>
9
+ </ClientOnly>
10
+ </template>
@@ -1,38 +1,38 @@
1
- <script setup lang="ts">
2
- import { joinURL, withLeadingSlash, withTrailingSlash } from 'ufo'
3
- import ImageComponent from '#build/mdc-image-component.mjs'
4
-
5
- export interface UtilImgProps {
6
- src: string
7
- width?: string | number
8
- height?: string | number
9
- alt?: string
10
- densities?: string
11
- mirror?: ImgService
12
- filter?: string
13
- }
14
-
15
- const props = withDefaults(defineProps<UtilImgProps>(), {
16
- alt: '',
17
- })
18
-
19
- const src = computed(() => {
20
- if (props.src.startsWith('/') && !props.src.startsWith('//')) {
21
- const _base = withLeadingSlash(withTrailingSlash(useRuntimeConfig().app.baseURL))
22
- if (_base !== '/' && !props.src.startsWith(_base))
23
- return joinURL(_base, props.src)
24
- }
25
- if (props.mirror)
26
- return getImgUrl(props.src, props.mirror)
27
- return props.src
28
- })
29
- </script>
30
-
31
- <template>
32
- <component
33
- :is="ImageComponent"
34
- :src :alt :width :height :densities
35
- :style="{ filter }"
36
- :referrerpolicy="mirror ? 'no-referrer' : undefined"
37
- />
38
- </template>
1
+ <script setup lang="ts">
2
+ import { joinURL, withLeadingSlash, withTrailingSlash } from 'ufo'
3
+ import ImageComponent from '#build/mdc-image-component.mjs'
4
+
5
+ export interface UtilImgProps {
6
+ src: string
7
+ width?: string | number
8
+ height?: string | number
9
+ alt?: string
10
+ densities?: string
11
+ mirror?: ImgService
12
+ filter?: string
13
+ }
14
+
15
+ const props = withDefaults(defineProps<UtilImgProps>(), {
16
+ alt: '',
17
+ })
18
+
19
+ const src = computed(() => {
20
+ if (props.src.startsWith('/') && !props.src.startsWith('//')) {
21
+ const _base = withLeadingSlash(withTrailingSlash(useRuntimeConfig().app.baseURL))
22
+ if (_base !== '/' && !props.src.startsWith(_base))
23
+ return joinURL(_base, props.src)
24
+ }
25
+ if (props.mirror)
26
+ return getImgUrl(props.src, props.mirror)
27
+ return props.src
28
+ })
29
+ </script>
30
+
31
+ <template>
32
+ <component
33
+ :is="ImageComponent"
34
+ :src :alt :width :height :densities
35
+ :style="{ filter }"
36
+ :referrerpolicy="mirror ? 'no-referrer' : undefined"
37
+ />
38
+ </template>
@@ -1,11 +1,11 @@
1
- <script setup lang="ts">
2
- defineProps<{ to?: string }>()
3
- </script>
4
-
5
- <template>
6
- <a v-if="to?.startsWith('#')" :href="to"><slot /></a>
7
- <span v-else-if="typeof to === 'undefined'"><slot /></span>
8
- <NuxtLink v-else :to :target="isExtLink(to) ? '_blank' : undefined">
9
- <slot />
10
- </NuxtLink>
11
- </template>
1
+ <script setup lang="ts">
2
+ defineProps<{ to?: string }>()
3
+ </script>
4
+
5
+ <template>
6
+ <a v-if="to?.startsWith('#')" :href="to"><slot /></a>
7
+ <span v-else-if="typeof to === 'undefined'"><slot /></span>
8
+ <NuxtLink v-else :to :target="isExtLink(to) ? '_blank' : undefined">
9
+ <slot />
10
+ </NuxtLink>
11
+ </template>
@@ -1,27 +1,27 @@
1
- <script setup lang="ts">
2
- const clarity = useClarityConfig()
3
-
4
- const blogLog = computed(() => {
5
- const entries = clarity.changelog.map(({ date, text }) => ({ label: date, value: text }))
6
- if (clarity.site.established) {
7
- entries.push({ label: clarity.site.established, value: '发布第一篇文章' })
8
- }
9
- return entries
10
- })
11
- </script>
12
-
13
- <template>
14
- <BlogWidget card title="更新日志">
15
- <ZDlGroup v-if="blogLog.length" size="large" :items="blogLog" />
16
- <p v-else class="no-log">
17
- 暂无更新日志,可在 clarity.config.ts 的 changelog 中配置。
18
- </p>
19
- </BlogWidget>
20
- </template>
21
-
22
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ const clarity = useClarityConfig()
3
+
4
+ const blogLog = computed(() => {
5
+ const entries = clarity.changelog.map(({ date, text }) => ({ label: date, value: text }))
6
+ if (clarity.site.established) {
7
+ entries.push({ label: clarity.site.established, value: '发布第一篇文章' })
8
+ }
9
+ return entries
10
+ })
11
+ </script>
12
+
13
+ <template>
14
+ <BlogWidget card title="更新日志">
15
+ <ZDlGroup v-if="blogLog.length" size="large" :items="blogLog" />
16
+ <p v-else class="no-log">
17
+ 暂无更新日志,可在 clarity.config.ts 的 changelog 中配置。
18
+ </p>
19
+ </BlogWidget>
20
+ </template>
21
+
22
+ <style lang="scss" scoped>
23
23
  .no-log {
24
24
  padding: 1em;
25
25
  color: var(--c-text-3);
26
26
  }
27
- </style>
27
+ </style>
@@ -1,39 +1,39 @@
1
- <script setup lang="ts">
2
- import { UtilDate } from '#components'
3
-
4
- const clarity = useClarityConfig()
5
- const runtimeConfig = useRuntimeConfig()
6
-
7
- // 响应头不正确时,stats.value 可能会是字符串,首次属性访问可能为 undefined
8
- const { data: stats } = useFetch('/api/stats')
9
-
10
- const yearlyTip = computed(() => Object
11
- .entries(stats.value?.annual || {})
12
- .reverse()
13
- .map(([year, item]) => `${year}年:${item.posts}篇,${formatNumber(item.words)}字`)
14
- .join('\n') || '数据获取失败',
15
- )
16
-
17
- const blogStats = [{
18
- label: '运营时长',
19
- value: clarity.site.established ? timeElapse(clarity.site.established) : '--',
20
- tip: clarity.site.established ? `博客于${clarity.site.established}上线` : '未配置建站日期',
21
- }, {
22
- label: '上次更新',
23
- value: () => h(UtilDate, {
24
- date: runtimeConfig.public.buildTime,
25
- relative: true,
26
- tipPrefix: '构建于',
27
- }),
28
- }, {
1
+ <script setup lang="ts">
2
+ import { UtilDate } from '#components'
3
+
4
+ const clarity = useClarityConfig()
5
+ const runtimeConfig = useRuntimeConfig()
6
+
7
+ // 响应头不正确时,stats.value 可能会是字符串,首次属性访问可能为 undefined
8
+ const { data: stats } = useFetch('/api/stats')
9
+
10
+ const yearlyTip = computed(() => Object
11
+ .entries(stats.value?.annual || {})
12
+ .reverse()
13
+ .map(([year, item]) => `${year}年:${item.posts}篇,${formatNumber(item.words)}字`)
14
+ .join('\n') || '数据获取失败',
15
+ )
16
+
17
+ const blogStats = [{
18
+ label: '运营时长',
19
+ value: clarity.site.established ? timeElapse(clarity.site.established) : '--',
20
+ tip: clarity.site.established ? `博客于${clarity.site.established}上线` : '未配置建站日期',
21
+ }, {
22
+ label: '上次更新',
23
+ value: () => h(UtilDate, {
24
+ date: runtimeConfig.public.buildTime,
25
+ relative: true,
26
+ tipPrefix: '构建于',
27
+ }),
28
+ }, {
29
29
  label: clarity.stats.postsOnly ? '文章字数' : '总字数',
30
- value: computed(() => formatNumber(stats.value?.total?.words) || '--'),
31
- tip: yearlyTip,
32
- }]
33
- </script>
34
-
35
- <template>
36
- <BlogWidget card title="博客统计">
37
- <ZDlGroup :items="blogStats" size="small" />
38
- </BlogWidget>
39
- </template>
30
+ value: computed(() => formatNumber(stats.value?.total?.words) || '--'),
31
+ tip: yearlyTip,
32
+ }]
33
+ </script>
34
+
35
+ <template>
36
+ <BlogWidget card title="博客统计">
37
+ <ZDlGroup :items="blogStats" size="small" />
38
+ </BlogWidget>
39
+ </template>
@@ -1,59 +1,59 @@
1
- <script setup lang="ts">
2
- import { Icon } from '#components'
3
-
4
- const clarity = useClarityConfig()
5
- const runtimeConfig = useRuntimeConfig()
6
- const buildInfo = runtimeConfig.public.clarity as {
7
- theme: string
8
- themeVersion: string
9
- siteVersion: string
10
- sitePackageManager: string
11
- nuxtVersion: string
12
- vueVersion: string
13
- }
14
- const { public: { arch, ci, nodeVersion, platform } } = runtimeConfig
15
-
16
- const ciPlatform = computed(() => {
17
- const iconName = ciIcons[ci]
18
- if (!iconName)
19
- return ''
20
-
21
- const iconNode = iconName.startsWith('http')
22
- ? h('img', { src: iconName, alt: '' })
23
- : h(Icon, { name: iconName })
24
-
25
- return h('span', {}, [iconNode, ` ${ci.split(' ')[0]}`])
26
- })
27
-
28
- const service = computed(() => ([
29
- ...ci ? [{ label: '构建平台', value: ciPlatform }] : [],
30
- { label: '文章许可', value: clarity.site.copyright?.abbr || '未配置' },
31
- { label: '规范域名', value: getDomain(clarity.site.url) },
32
- ]))
33
-
34
- const techstack = computed(() => ([
35
- { label: 'Blog', value: buildInfo.siteVersion || '--' },
36
- { label: 'Theme', value: `${buildInfo.theme} ${buildInfo.themeVersion}` },
37
- { label: 'Vue', value: buildInfo.vueVersion },
38
- { label: 'Nuxt', value: buildInfo.nuxtVersion },
39
- { label: 'Node', value: nodeVersion },
40
- { label: 'OS', value: platform },
41
- { label: 'Arch', value: arch },
42
- ]))
43
-
44
- const expand = ref(false)
45
- </script>
46
-
47
- <template>
48
- <BlogWidget card grayscale title="技术信息">
49
- <ZDlGroup :items="service" />
50
- <ZExpand v-model="expand" in-place name="构建信息">
51
- <ZDlGroup size="small" :items="techstack" />
52
- </ZExpand>
53
- </BlogWidget>
54
- </template>
55
-
56
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import { Icon } from '#components'
3
+
4
+ const clarity = useClarityConfig()
5
+ const runtimeConfig = useRuntimeConfig()
6
+ const buildInfo = runtimeConfig.public.clarity as {
7
+ theme: string
8
+ themeVersion: string
9
+ siteVersion: string
10
+ sitePackageManager: string
11
+ nuxtVersion: string
12
+ vueVersion: string
13
+ }
14
+ const { public: { arch, ci, nodeVersion, platform } } = runtimeConfig
15
+
16
+ const ciPlatform = computed(() => {
17
+ const iconName = ciIcons[ci]
18
+ if (!iconName)
19
+ return ''
20
+
21
+ const iconNode = iconName.startsWith('http')
22
+ ? h('img', { src: iconName, alt: '' })
23
+ : h(Icon, { name: iconName })
24
+
25
+ return h('span', {}, [iconNode, ` ${ci.split(' ')[0]}`])
26
+ })
27
+
28
+ const service = computed(() => ([
29
+ ...ci ? [{ label: '构建平台', value: ciPlatform }] : [],
30
+ { label: '文章许可', value: clarity.site.copyright?.abbr || '未配置' },
31
+ { label: '规范域名', value: getDomain(clarity.site.url) },
32
+ ]))
33
+
34
+ const techstack = computed(() => ([
35
+ { label: 'Blog', value: buildInfo.siteVersion || '--' },
36
+ { label: 'Theme', value: `${buildInfo.theme} ${buildInfo.themeVersion}` },
37
+ { label: 'Vue', value: buildInfo.vueVersion },
38
+ { label: 'Nuxt', value: buildInfo.nuxtVersion },
39
+ { label: 'Node', value: nodeVersion },
40
+ { label: 'OS', value: platform },
41
+ { label: 'Arch', value: arch },
42
+ ]))
43
+
44
+ const expand = ref(false)
45
+ </script>
46
+
47
+ <template>
48
+ <BlogWidget card grayscale title="技术信息">
49
+ <ZDlGroup :items="service" />
50
+ <ZExpand v-model="expand" in-place name="构建信息">
51
+ <ZDlGroup size="small" :items="techstack" />
52
+ </ZExpand>
53
+ </BlogWidget>
54
+ </template>
55
+
56
+ <style lang="scss" scoped>
57
57
  .z-expand {
58
58
  margin-top: 0.2em;
59
59
  }
@@ -62,4 +62,4 @@ const expand = ref(false)
62
62
  height: 1.2em;
63
63
  vertical-align: sub;
64
64
  }
65
- </style>
65
+ </style>
@@ -1,3 +1,3 @@
1
- <template>
2
- <div />
3
- </template>
1
+ <template>
2
+ <div />
3
+ </template>
@@ -1,69 +1,69 @@
1
- <script setup lang="ts">
2
- import type { TocLink } from '@nuxt/content'
3
-
4
- const [DefineTemplate, ReuseTemplate] = createReusableTemplate({
5
- props: {
6
- tocTree: { type: Array as PropType<TocLink[]> },
7
- },
8
- })
9
-
10
- const { toc } = useArticle()
11
- const scrollableEl = useTemplateRef('toc')
12
- const { tocOffsets, activeHeadingId } = useToc(toc, computed(() => scrollableEl.value?.body))
13
-
14
- function hasHeading(tocTree: TocLink, heading?: string): boolean {
15
- return tocTree.id === heading || !!tocTree.children?.some(child => hasHeading(child, heading))
16
- }
17
- </script>
18
-
19
- <template>
20
- <BlogWidget
21
- ref="toc"
22
- title="文章目录"
23
- shrink
24
- :style="{ minHeight: `clamp(4rem, ${tocOffsets.length}rem, 20rem)` }"
25
- >
26
- <template #action>
27
- <!-- use <a> for anchor -->
28
- <a href="#main-content" aria-label="返回开头">
29
- <Icon name="tabler:arrow-bar-to-up" />
30
- </a>
31
-
32
- <a href="#twikoo" aria-label="评论区">
33
- <Icon name="tabler:message-dots" />
34
- </a>
35
- </template>
36
-
37
- <!-- 放在顶层会导致 Transition 失效 -->
38
- <DefineTemplate v-slot="{ tocTree }">
39
- <ol>
40
- <li
41
- v-for="(entry, index) in tocTree"
42
- :key="index"
43
- :class="{
44
- 'has-active': hasHeading(entry, activeHeadingId),
45
- 'active': entry.id === activeHeadingId,
46
- }"
47
- >
48
- <!-- 使用 <a> 确保键盘焦点切换 -->
49
- <a :href="`#${entry?.id}`" :title="entry.text">{{ entry.text }}</a>
50
- <ReuseTemplate v-if="entry.children" :toc-tree="entry.children" />
51
- </li>
52
- </ol>
53
- </DefineTemplate>
54
-
55
- <ReuseTemplate
56
- v-if="toc?.links.length"
57
- class="toc"
58
- :toc-tree="toc.links"
59
- />
60
- <p v-else class="no-toc">
61
- 暂无目录信息
62
- </p>
63
- </BlogWidget>
64
- </template>
65
-
66
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { TocLink } from '@nuxt/content'
3
+
4
+ const [DefineTemplate, ReuseTemplate] = createReusableTemplate({
5
+ props: {
6
+ tocTree: { type: Array as PropType<TocLink[]> },
7
+ },
8
+ })
9
+
10
+ const { toc } = useArticle()
11
+ const scrollableEl = useTemplateRef('toc')
12
+ const { tocOffsets, activeHeadingId } = useToc(toc, computed(() => scrollableEl.value?.body))
13
+
14
+ function hasHeading(tocTree: TocLink, heading?: string): boolean {
15
+ return tocTree.id === heading || !!tocTree.children?.some(child => hasHeading(child, heading))
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <BlogWidget
21
+ ref="toc"
22
+ title="文章目录"
23
+ shrink
24
+ :style="{ minHeight: `clamp(4rem, ${tocOffsets.length}rem, 20rem)` }"
25
+ >
26
+ <template #action>
27
+ <!-- use <a> for anchor -->
28
+ <a href="#main-content" aria-label="返回开头">
29
+ <Icon name="tabler:arrow-bar-to-up" />
30
+ </a>
31
+
32
+ <a href="#twikoo" aria-label="评论区">
33
+ <Icon name="tabler:message-dots" />
34
+ </a>
35
+ </template>
36
+
37
+ <!-- 放在顶层会导致 Transition 失效 -->
38
+ <DefineTemplate v-slot="{ tocTree }">
39
+ <ol>
40
+ <li
41
+ v-for="(entry, index) in tocTree"
42
+ :key="index"
43
+ :class="{
44
+ 'has-active': hasHeading(entry, activeHeadingId),
45
+ 'active': entry.id === activeHeadingId,
46
+ }"
47
+ >
48
+ <!-- 使用 <a> 确保键盘焦点切换 -->
49
+ <a :href="`#${entry?.id}`" :title="entry.text">{{ entry.text }}</a>
50
+ <ReuseTemplate v-if="entry.children" :toc-tree="entry.children" />
51
+ </li>
52
+ </ol>
53
+ </DefineTemplate>
54
+
55
+ <ReuseTemplate
56
+ v-if="toc?.links.length"
57
+ class="toc"
58
+ :toc-tree="toc.links"
59
+ />
60
+ <p v-else class="no-toc">
61
+ 暂无目录信息
62
+ </p>
63
+ </BlogWidget>
64
+ </template>
65
+
66
+ <style lang="scss" scoped>
67
67
  .toc {
68
68
  position: relative;
69
69
 
@@ -126,4 +126,4 @@ li {
126
126
  text-align: center;
127
127
  color: var(--c-text-3);
128
128
  }
129
- </style>
129
+ </style>