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,53 +1,53 @@
1
- <script setup lang="ts">
2
- import { Temporal } from 'temporal-polyfill'
3
-
4
- const props = defineProps<{
5
- datetime?: string
6
- rotate?: boolean
7
- }>()
8
-
9
- const emojiStatic = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥', '🕚', '🕦']
10
- const emojiRotate = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚']
11
-
12
- const now = ref(Temporal.Now.zonedDateTimeISO())
13
-
14
- const datetime = computed(() => props.datetime
15
- ? toZonedTemporal(props.datetime)
16
- : now.value)
17
-
18
- const status = computed(() => {
19
- const { hour, minute } = datetime.value
20
-
21
- if (!props.rotate) {
22
- const emojiIndex = (hour * 2 + Math.round(minute / 30)) % emojiStatic.length
23
- return { emoji: emojiStatic[emojiIndex] }
24
- }
25
-
26
- const minuteAt = Math.round(minute / 5)
27
- const emojiIndex = (hour % 12 - minuteAt + emojiRotate.length) % emojiRotate.length
28
- return { rotate: minuteAt * 30, emoji: emojiRotate[emojiIndex] }
29
- })
30
-
31
- // 定时器只能在客户端运行,否则 nuxt generate 不能自动退出
32
- const { resume } = useIntervalFn(() => {
33
- now.value = Temporal.Now.zonedDateTimeISO()
34
- }, 30000, { immediate: false })
35
-
36
- whenever(() => !props.datetime, resume)
37
- </script>
38
-
39
- <template>
40
- <span
41
- class="emoji-clock"
42
- :class="{ rotate }"
43
- :style="{ '--deg': rotate ? `${status.rotate}deg` : undefined }"
44
- v-text="status.emoji"
45
- />
46
- </template>
47
-
48
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import { Temporal } from 'temporal-polyfill'
3
+
4
+ const props = defineProps<{
5
+ datetime?: string
6
+ rotate?: boolean
7
+ }>()
8
+
9
+ const emojiStatic = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥', '🕚', '🕦']
10
+ const emojiRotate = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚']
11
+
12
+ const now = ref(Temporal.Now.zonedDateTimeISO())
13
+
14
+ const datetime = computed(() => props.datetime
15
+ ? toZonedTemporal(props.datetime)
16
+ : now.value)
17
+
18
+ const status = computed(() => {
19
+ const { hour, minute } = datetime.value
20
+
21
+ if (!props.rotate) {
22
+ const emojiIndex = (hour * 2 + Math.round(minute / 30)) % emojiStatic.length
23
+ return { emoji: emojiStatic[emojiIndex] }
24
+ }
25
+
26
+ const minuteAt = Math.round(minute / 5)
27
+ const emojiIndex = (hour % 12 - minuteAt + emojiRotate.length) % emojiRotate.length
28
+ return { rotate: minuteAt * 30, emoji: emojiRotate[emojiIndex] }
29
+ })
30
+
31
+ // 定时器只能在客户端运行,否则 nuxt generate 不能自动退出
32
+ const { resume } = useIntervalFn(() => {
33
+ now.value = Temporal.Now.zonedDateTimeISO()
34
+ }, 30000, { immediate: false })
35
+
36
+ whenever(() => !props.datetime, resume)
37
+ </script>
38
+
39
+ <template>
40
+ <span
41
+ class="emoji-clock"
42
+ :class="{ rotate }"
43
+ :style="{ '--deg': rotate ? `${status.rotate}deg` : undefined }"
44
+ v-text="status.emoji"
45
+ />
46
+ </template>
47
+
48
+ <style lang="scss" scoped>
49
49
  .emoji-clock.rotate {
50
50
  display: inline-block;
51
51
  transform: rotate(var(--deg, 0deg));
52
52
  }
53
- </style>
53
+ </style>
@@ -1,98 +1,98 @@
1
- <script setup lang="ts">
2
- import type { CSSProperties } from 'vue'
3
- import type { FeedEntry } from '../../types/feed'
4
- // eslint-disable-next-line unused-imports/no-unused-imports
5
- import { Temporal } from 'temporal-polyfill'
6
-
7
- const props = defineProps<FeedEntry>()
8
-
9
- const clarity = useClarityConfig()
10
- const route = useRoute()
11
- const isInspect = computed(() => import.meta.dev && route.query.inspect !== undefined)
12
-
13
- const title = computed(() => props.title ?? props.sitenick ?? props.author)
14
- const domainTip = computed(() => getDomainType(getMainDomain(props.link, true)))
15
- const domainIcon = computed(() => getDomainIcon(props.link))
16
-
17
- function getInspectStyle(src: string): CSSProperties {
18
- src = getMainDomain(src)
19
- let color = 'red'
20
-
21
- if (src === getMainDomain(props.link))
22
- color = 'transparent' // 来自源站
23
- else if (src === 'gstatic.cn' || src === 'webp.se')
24
- color = 'yellow' // 来自API获取
25
- else if (src === 'qlogo.cn')
26
- color = 'lightblue' // 来自QQ头像
27
- else if (src === ('wsrv.nl'))
28
- color = 'lightgreen' // 来自GitHub头像
29
-
30
- return {
31
- border: `2px solid ${color}`,
32
- boxSizing: 'content-box',
33
- }
34
- }
35
- </script>
36
-
37
- <template>
38
- <Tooltip :delay="200" interactive ^role="link" hide-on-click="toggle">
39
- <UtilLink
40
- class="feed-card gradient-card"
41
- :to="error ? undefined : link"
42
- rel="noopener"
43
- :data-error="error"
44
- >
45
- <div class="avatar" :title="feed ? undefined : '无订阅源'">
46
- <ClientOnly v-if="isInspect">
47
- <span style="position: absolute; left: 100%; white-space: nowrap;" v-text="title" />
48
- <NuxtImg :src="icon" :title="icon" :style="getInspectStyle(icon)" />
49
- <NuxtImg :src="avatar" :title="avatar" :style="getInspectStyle(avatar)" />
50
- </ClientOnly>
51
-
52
- <NuxtImg v-else class="round-cobblestone" :src="avatar" :alt="author" loading="lazy" />
53
- <Icon v-if="clarity.link.remindNoFeed && !feed" class="no-feed" name="tabler:bell-off" />
54
- </div>
55
-
56
- <span class="author">{{ author }}</span>
57
- <span class="sitenick">{{ sitenick }}</span>
58
- </UtilLink>
59
-
60
- <template #content>
61
- <div class="site-content">
62
- <NuxtImg class="site-icon" :src="icon" :alt="title" loading="lazy" />
63
-
64
- <div class="site-info">
65
- <h3 class="text-creative">
66
- {{ title }}
67
- </h3>
68
-
69
- <code class="domain" :title="domainTip">
70
- <span>{{ getDomain(link) }}</span>
71
- <Icon v-if="domainIcon" class="domain-mark" :name="domainIcon" />
72
- </code>
73
- </div>
74
-
75
- <Icon
76
- v-for="arch in archs" :key="arch"
77
- class="arch" :name="getArchIcon(arch)" :title="arch"
78
- />
79
- </div>
80
- <div class="desc-content">
81
- <div class="date">
82
- {{ Temporal.PlainDate.from(date).toLocaleString() }}
83
- </div>
84
-
85
- <p>{{ error ?? desc }}</p>
86
-
87
- <p v-if="comment">
88
- <Icon name="tabler:message-dots" /> {{ comment }}
89
- </p>
90
- </div>
91
- </template>
92
- </Tooltip>
93
- </template>
94
-
95
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { CSSProperties } from 'vue'
3
+ import type { FeedEntry } from '../../types/feed'
4
+ // eslint-disable-next-line unused-imports/no-unused-imports
5
+ import { Temporal } from 'temporal-polyfill'
6
+
7
+ const props = defineProps<FeedEntry>()
8
+
9
+ const clarity = useClarityConfig()
10
+ const route = useRoute()
11
+ const isInspect = computed(() => import.meta.dev && route.query.inspect !== undefined)
12
+
13
+ const title = computed(() => props.title ?? props.sitenick ?? props.author)
14
+ const domainTip = computed(() => getDomainType(getMainDomain(props.link, true)))
15
+ const domainIcon = computed(() => getDomainIcon(props.link))
16
+
17
+ function getInspectStyle(src: string): CSSProperties {
18
+ src = getMainDomain(src)
19
+ let color = 'red'
20
+
21
+ if (src === getMainDomain(props.link))
22
+ color = 'transparent' // 来自源站
23
+ else if (src === 'gstatic.cn' || src === 'webp.se')
24
+ color = 'yellow' // 来自API获取
25
+ else if (src === 'qlogo.cn')
26
+ color = 'lightblue' // 来自QQ头像
27
+ else if (src === ('wsrv.nl'))
28
+ color = 'lightgreen' // 来自GitHub头像
29
+
30
+ return {
31
+ border: `2px solid ${color}`,
32
+ boxSizing: 'content-box',
33
+ }
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <Tooltip :delay="200" interactive ^role="link" hide-on-click="toggle">
39
+ <UtilLink
40
+ class="feed-card gradient-card"
41
+ :to="error ? undefined : link"
42
+ rel="noopener"
43
+ :data-error="error"
44
+ >
45
+ <div class="avatar" :title="feed ? undefined : '无订阅源'">
46
+ <ClientOnly v-if="isInspect">
47
+ <span style="position: absolute; left: 100%; white-space: nowrap;" v-text="title" />
48
+ <NuxtImg :src="icon" :title="icon" :style="getInspectStyle(icon)" />
49
+ <NuxtImg :src="avatar" :title="avatar" :style="getInspectStyle(avatar)" />
50
+ </ClientOnly>
51
+
52
+ <NuxtImg v-else class="round-cobblestone" :src="avatar" :alt="author" loading="lazy" />
53
+ <Icon v-if="clarity.link.remindNoFeed && !feed" class="no-feed" name="tabler:bell-off" />
54
+ </div>
55
+
56
+ <span class="author">{{ author }}</span>
57
+ <span class="sitenick">{{ sitenick }}</span>
58
+ </UtilLink>
59
+
60
+ <template #content>
61
+ <div class="site-content">
62
+ <NuxtImg class="site-icon" :src="icon" :alt="title" loading="lazy" />
63
+
64
+ <div class="site-info">
65
+ <h3 class="text-creative">
66
+ {{ title }}
67
+ </h3>
68
+
69
+ <code class="domain" :title="domainTip">
70
+ <span>{{ getDomain(link) }}</span>
71
+ <Icon v-if="domainIcon" class="domain-mark" :name="domainIcon" />
72
+ </code>
73
+ </div>
74
+
75
+ <Icon
76
+ v-for="arch in archs" :key="arch"
77
+ class="arch" :name="getArchIcon(arch)" :title="arch"
78
+ />
79
+ </div>
80
+ <div class="desc-content">
81
+ <div class="date">
82
+ {{ Temporal.PlainDate.from(date).toLocaleString() }}
83
+ </div>
84
+
85
+ <p>{{ error ?? desc }}</p>
86
+
87
+ <p v-if="comment">
88
+ <Icon name="tabler:message-dots" /> {{ comment }}
89
+ </p>
90
+ </div>
91
+ </template>
92
+ </Tooltip>
93
+ </template>
94
+
95
+ <style lang="scss" scoped>
96
96
  .feed-card {
97
97
  display: flex;
98
98
  align-items: center;
@@ -209,4 +209,4 @@ function getInspectStyle(src: string): CSSProperties {
209
209
  pointer-events: none;
210
210
  }
211
211
  }
212
- </style>
212
+ </style>
@@ -1,63 +1,63 @@
1
- <script setup lang="ts">
2
- import type { FeedEntry, FeedGroup } from '../../types/feed'
3
- import { shuffle } from 'es-toolkit/array'
4
-
5
- const props = defineProps<FeedGroup & { shuffle?: boolean }>()
6
- const route = useRoute()
7
- const entries = ref(props.entries)
8
-
9
- // 友链浮现随机延迟
10
- function getCardDelay(feed: FeedEntry) {
11
- let hash = 0
12
- for (const char of feed.link) {
13
- hash = hash * 31 + char.charCodeAt(0)
14
- }
15
- return (hash % 1000) / 1000
16
- }
17
-
18
- const shuffleEntries = () => entries.value = shuffle(entries.value)
19
- const unshuffleEntries = () => entries.value = props.entries
20
-
21
- onMounted(() => {
22
- if (props.shuffle && route.query.shuffle !== 'false')
23
- shuffleEntries()
24
- })
25
-
26
- if (import.meta.dev) {
27
- watch(() => props.entries, (newEntries) => {
28
- entries.value = newEntries
29
- })
30
- }
31
- </script>
32
-
33
- <template>
34
- <section class="feed-group">
35
- <h3 class="feed-title">
36
- <button
37
- v-if="props.shuffle"
38
- role="button"
39
- title="点击随机排序,按住修饰键点击可取消随机排序"
40
- @click="unshuffleEntries"
41
- @click.exact="shuffleEntries"
42
- v-text="name"
43
- />
44
- <span v-else v-text="name" />
45
- </h3>
46
- <p class="feed-desc" v-text="desc" />
47
-
48
- <TransitionGroup tag="menu" class="feed-list" name="float-in">
49
- <li
50
- v-for="entry in entries"
51
- :key="entry.link"
52
- :style="`--delay: ${getCardDelay(entry)}s;`"
53
- >
54
- <FeedCard v-bind="entry" />
55
- </li>
56
- </TransitionGroup>
57
- </section>
58
- </template>
59
-
60
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { FeedEntry, FeedGroup } from '../../types/feed'
3
+ import { shuffle } from 'es-toolkit/array'
4
+
5
+ const props = defineProps<FeedGroup & { shuffle?: boolean }>()
6
+ const route = useRoute()
7
+ const entries = ref(props.entries)
8
+
9
+ // 友链浮现随机延迟
10
+ function getCardDelay(feed: FeedEntry) {
11
+ let hash = 0
12
+ for (const char of feed.link) {
13
+ hash = hash * 31 + char.charCodeAt(0)
14
+ }
15
+ return (hash % 1000) / 1000
16
+ }
17
+
18
+ const shuffleEntries = () => entries.value = shuffle(entries.value)
19
+ const unshuffleEntries = () => entries.value = props.entries
20
+
21
+ onMounted(() => {
22
+ if (props.shuffle && route.query.shuffle !== 'false')
23
+ shuffleEntries()
24
+ })
25
+
26
+ if (import.meta.dev) {
27
+ watch(() => props.entries, (newEntries) => {
28
+ entries.value = newEntries
29
+ })
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <section class="feed-group">
35
+ <h3 class="feed-title">
36
+ <button
37
+ v-if="props.shuffle"
38
+ role="button"
39
+ title="点击随机排序,按住修饰键点击可取消随机排序"
40
+ @click="unshuffleEntries"
41
+ @click.exact="shuffleEntries"
42
+ v-text="name"
43
+ />
44
+ <span v-else v-text="name" />
45
+ </h3>
46
+ <p class="feed-desc" v-text="desc" />
47
+
48
+ <TransitionGroup tag="menu" class="feed-list" name="float-in">
49
+ <li
50
+ v-for="entry in entries"
51
+ :key="entry.link"
52
+ :style="`--delay: ${getCardDelay(entry)}s;`"
53
+ >
54
+ <FeedCard v-bind="entry" />
55
+ </li>
56
+ </TransitionGroup>
57
+ </section>
58
+ </template>
59
+
60
+ <style lang="scss" scoped>
61
61
  .feed-group {
62
62
  container-type: inline-size;
63
63
  margin: 2em 1em;
@@ -127,4 +127,4 @@ if (import.meta.dev) {
127
127
  contain: paint; // 防止移动时出现滚动条
128
128
  pointer-events: none; // 阻止触发错误定位的气泡
129
129
  }
130
- </style>
130
+ </style>
@@ -1,21 +1,21 @@
1
- <script setup lang="ts">
2
- defineProps<{
3
- title?: string
4
- }>()
5
- </script>
6
-
7
- <template>
8
- <details>
9
- <summary>
10
- <slot name="title">
11
- {{ title }}
12
- </slot>
13
- </summary>
14
- <slot />
15
- </details>
16
- </template>
17
-
18
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title?: string
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <details>
9
+ <summary>
10
+ <slot name="title">
11
+ {{ title }}
12
+ </slot>
13
+ </summary>
14
+ <slot />
15
+ </details>
16
+ </template>
17
+
18
+ <style lang="scss" scoped>
19
19
  details {
20
20
  margin: 1em 0;
21
21
  padding: 0.5em 0.8em;
@@ -66,4 +66,4 @@ details {
66
66
  margin: 0 -0.8em -0.5em;
67
67
  }
68
68
  }
69
- </style>
69
+ </style>