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,25 +1,25 @@
1
- <script setup lang="ts">
2
- const [scroll, toggleScroll] = useToggle(true)
3
- </script>
4
-
5
- <template>
6
- <Tooltip class="md-table" tag="figure" interactive :delay="500">
7
- <template #content>
8
- <Icon v-show="false" :name="scroll ? 'tabler:text-wrap-disabled' : 'tabler:text-wrap'" />
9
- <ZButton
10
- variant="text"
11
- :icon="scroll ? 'tabler:text-wrap' : 'tabler:text-wrap-disabled'"
12
- :text="scroll ? '自动换行' : '横向滚动'"
13
- @click="toggleScroll()"
14
- />
15
- </template>
16
- <table class="scrollcheck-x" :class="{ scroll }">
17
- <slot />
18
- </table>
19
- </Tooltip>
20
- </template>
21
-
22
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ const [scroll, toggleScroll] = useToggle(true)
3
+ </script>
4
+
5
+ <template>
6
+ <Tooltip class="md-table" tag="figure" interactive :delay="500">
7
+ <template #content>
8
+ <Icon v-show="false" :name="scroll ? 'tabler:text-wrap-disabled' : 'tabler:text-wrap'" />
9
+ <ZButton
10
+ variant="text"
11
+ :icon="scroll ? 'tabler:text-wrap' : 'tabler:text-wrap-disabled'"
12
+ :text="scroll ? '自动换行' : '横向滚动'"
13
+ @click="toggleScroll()"
14
+ />
15
+ </template>
16
+ <table class="scrollcheck-x" :class="{ scroll }">
17
+ <slot />
18
+ </table>
19
+ </Tooltip>
20
+ </template>
21
+
22
+ <style lang="scss" scoped>
23
23
  .md-table {
24
24
  position: relative;
25
25
  margin: 1rem 0;
@@ -67,4 +67,4 @@ const [scroll, toggleScroll] = useToggle(true)
67
67
  border: 1px solid var(--c-border);
68
68
  }
69
69
  }
70
- </style>
70
+ </style>
@@ -1,23 +1,23 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{
3
- icon?: string
4
- }>()
5
-
6
- const icon = computed(() => props.icon || 'tabler:message-2')
7
- </script>
8
-
9
- <template>
10
- <div class="quote title-like">
11
- <div class="icon-line">
12
- <slot name="icon">
13
- <Icon :name="icon" />
14
- </slot>
15
- </div>
16
- <slot />
17
- </div>
18
- </template>
19
-
20
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ icon?: string
4
+ }>()
5
+
6
+ const icon = computed(() => props.icon || 'tabler:message-2')
7
+ </script>
8
+
9
+ <template>
10
+ <div class="quote title-like">
11
+ <div class="icon-line">
12
+ <slot name="icon">
13
+ <Icon :name="icon" />
14
+ </slot>
15
+ </div>
16
+ <slot />
17
+ </div>
18
+ </template>
19
+
20
+ <style lang="scss" scoped>
21
21
  .quote {
22
22
  font-size: 1.2rem;
23
23
  line-height: 1.5;
@@ -48,4 +48,4 @@ const icon = computed(() => props.icon || 'tabler:message-2')
48
48
  transform: translateY(-0.5rem);
49
49
  }
50
50
  }
51
- </style>
51
+ </style>
@@ -1,34 +1,34 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{
3
- /** tab 下标从 1 开始 */
4
- tabs: string[]
5
- center?: boolean
6
- active?: string | number
7
- }>()
8
-
9
- const activeTab = ref(Number(props.active) || 1)
10
- </script>
11
-
12
- <template>
13
- <!-- BUG: MDC Tab插槽块内会吞代码缩进 -->
14
- <div :class="{ center }">
15
- <div class="tabs">
16
- <button
17
- v-for="(tab, tabIndex) in tabs"
18
- :key="tabIndex"
19
- :class="{ active: activeTab === tabIndex + 1 }"
20
- @click="activeTab = tabIndex + 1"
21
- >
22
- {{ tab }}
23
- </button>
24
- </div>
25
- <div v-for="tabIndex in tabs.length" v-show="activeTab === tabIndex" :key="tabIndex" class="tab-content">
26
- <slot :name="`tab${tabIndex}`" />
27
- </div>
28
- </div>
29
- </template>
30
-
31
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ /** tab 下标从 1 开始 */
4
+ tabs: string[]
5
+ center?: boolean
6
+ active?: string | number
7
+ }>()
8
+
9
+ const activeTab = ref(Number(props.active) || 1)
10
+ </script>
11
+
12
+ <template>
13
+ <!-- BUG: MDC Tab插槽块内会吞代码缩进 -->
14
+ <div :class="{ center }">
15
+ <div class="tabs">
16
+ <button
17
+ v-for="(tab, tabIndex) in tabs"
18
+ :key="tabIndex"
19
+ :class="{ active: activeTab === tabIndex + 1 }"
20
+ @click="activeTab = tabIndex + 1"
21
+ >
22
+ {{ tab }}
23
+ </button>
24
+ </div>
25
+ <div v-for="tabIndex in tabs.length" v-show="activeTab === tabIndex" :key="tabIndex" class="tab-content">
26
+ <slot :name="`tab${tabIndex}`" />
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <style lang="scss" scoped>
32
32
  .float-in-leave-active {
33
33
  /* stylelint-disable-next-line declaration-no-important */
34
34
  position: revert !important;
@@ -97,4 +97,4 @@ button {
97
97
  .tab-content {
98
98
  margin: 1em 0;
99
99
  }
100
- </style>
100
+ </style>
@@ -1,35 +1,35 @@
1
- <script lang="tsx" setup>
2
- const slots = defineSlots<{
3
- default: () => VNode[]
4
- }>()
5
-
6
- const timelineRegex = /^\{(?<caption>.*)\}$/
7
-
8
- function render() {
9
- const slotContent = slots.default()
10
- if (!slotContent)
11
- return <span>时间线为空</span>
12
-
13
- return slotContent.map((node: VNode) => {
14
- // WARN: 此处使用了非标准的 v-slot:default
15
- const textContent = (node.children as any)?.default?.()[0].children
16
- const matchGroups = typeof textContent === 'string'
17
- ? textContent.match(timelineRegex)?.groups
18
- : undefined
19
- return matchGroups
20
- ? <dt class="timeline-caption">{matchGroups.caption}</dt>
21
- : <dd class="timeline-body card">{node}</dd>
22
- })
23
- }
24
- </script>
25
-
26
- <template>
27
- <dl class="timeline">
28
- <render />
29
- </dl>
30
- </template>
31
-
32
- <style lang="scss" scoped>
1
+ <script lang="tsx" setup>
2
+ const slots = defineSlots<{
3
+ default: () => VNode[]
4
+ }>()
5
+
6
+ const timelineRegex = /^\{(?<caption>.*)\}$/
7
+
8
+ function render() {
9
+ const slotContent = slots.default()
10
+ if (!slotContent)
11
+ return <span>时间线为空</span>
12
+
13
+ return slotContent.map((node: VNode) => {
14
+ // WARN: 此处使用了非标准的 v-slot:default
15
+ const textContent = (node.children as any)?.default?.()[0].children
16
+ const matchGroups = typeof textContent === 'string'
17
+ ? textContent.match(timelineRegex)?.groups
18
+ : undefined
19
+ return matchGroups
20
+ ? <dt class="timeline-caption">{matchGroups.caption}</dt>
21
+ : <dd class="timeline-body card">{node}</dd>
22
+ })
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <dl class="timeline">
28
+ <render />
29
+ </dl>
30
+ </template>
31
+
32
+ <style lang="scss" scoped>
33
33
  // TODO: 优化时间线样式
34
34
  .timeline {
35
35
  position: relative;
@@ -70,4 +70,4 @@ function render() {
70
70
  padding: 0 1em;
71
71
  }
72
72
  }
73
- </style>
73
+ </style>
@@ -1,42 +1,42 @@
1
- <script setup lang="ts">
2
- import type { TippyOptions } from 'vue-tippy'
3
-
4
- const props = withDefaults(defineProps<{
5
- text?: string
6
- tip?: string
7
- icon?: string | boolean
8
- copy?: boolean
9
- tipOptions?: TippyOptions
10
- }>(), {
11
- icon: undefined,
12
- })
13
-
14
- const tip = computed(() => ({
15
- content: props.tip || (props.copy ? '点击复制' : ''),
16
- inlinePositioning: true,
17
- ...props.tipOptions,
18
- }))
19
- const tipSource = useTemplateRef('tip-text')
20
-
21
- const { copy, copied } = useCopy(tipSource)
22
- const icon = computed(() => props.icon ?? (copied.value ? 'tabler:check' : props.copy && 'tabler:copy'))
23
- </script>
24
-
25
- <template>
26
- <span
27
- ref="tip-text"
28
- v-tip="tip"
29
- class="tip"
30
- tabindex="0"
31
- @keypress.enter="props.copy && copy()"
32
- @click="props.copy && copy()"
33
- >
34
- <slot>{{ text }}</slot>
35
- <Icon v-if="typeof icon === 'string'" :name="icon" class="tip-icon" />
36
- </span>
37
- </template>
38
-
39
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { TippyOptions } from 'vue-tippy'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ text?: string
6
+ tip?: string
7
+ icon?: string | boolean
8
+ copy?: boolean
9
+ tipOptions?: TippyOptions
10
+ }>(), {
11
+ icon: undefined,
12
+ })
13
+
14
+ const tip = computed(() => ({
15
+ content: props.tip || (props.copy ? '点击复制' : ''),
16
+ inlinePositioning: true,
17
+ ...props.tipOptions,
18
+ }))
19
+ const tipSource = useTemplateRef('tip-text')
20
+
21
+ const { copy, copied } = useCopy(tipSource)
22
+ const icon = computed(() => props.icon ?? (copied.value ? 'tabler:check' : props.copy && 'tabler:copy'))
23
+ </script>
24
+
25
+ <template>
26
+ <span
27
+ ref="tip-text"
28
+ v-tip="tip"
29
+ class="tip"
30
+ tabindex="0"
31
+ @keypress.enter="props.copy && copy()"
32
+ @click="props.copy && copy()"
33
+ >
34
+ <slot>{{ text }}</slot>
35
+ <Icon v-if="typeof icon === 'string'" :name="icon" class="tip-icon" />
36
+ </span>
37
+ </template>
38
+
39
+ <style lang="scss" scoped>
40
40
  .tip {
41
41
  position: relative;
42
42
  text-decoration: underline dashed var(--c-text-3);
@@ -49,4 +49,4 @@ const icon = computed(() => props.icon ?? (copied.value ? 'tabler:check' : props
49
49
  font-size: 1em;
50
50
  vertical-align: top;
51
51
  }
52
- </style>
52
+ </style>
@@ -1,114 +1,114 @@
1
- <script setup lang="ts">
2
- const props = withDefaults(defineProps<{
3
- /** 视频类型,数字过长时需通过双引号传入字符串 */
4
- type?: 'raw' | 'bilibili' | 'bilibili-nano' | 'youtube' | 'douyin' | 'douyin-wide' | 'tiktok'
5
- id: string
6
- autoplay?: boolean
7
- /** 视频宽高比,如 `16 / 9` `1.6` */
8
- ratio?: string | number
9
- /** 仅 type 为 `raw` 时可用 */
10
- poster?: string
11
- /** 视频宽度 */
12
- width?: string
13
- /** 视频高度 */
14
- height?: string
15
- /** CSS Zoom 比例 */
16
- zoom?: number
17
- }>(), {
18
- type: 'raw',
19
- height: '80vh',
20
- })
21
-
22
- const ratio = computed(() => {
23
- if (props.ratio) {
24
- return props.ratio
25
- }
26
- switch (props.type) {
27
- case 'raw':
28
- return undefined
29
- case 'douyin':
30
- return '27 / 56'
31
- case 'douyin-wide':
32
- return '1198 / 731'
33
- default:
34
- return '16 / 9'
35
- }
36
- })
37
-
38
- const src = computed(() => {
39
- switch (props.type) {
40
- case 'bilibili':
41
- return `https://player.bilibili.com/player.html?bvid=${props.id}&autoplay=${props.autoplay}`
42
- case 'bilibili-nano':
43
- return `https://www.bilibili.com/blackboard/newplayer.html?bvid=${props.id}&autoplay=${props.autoplay}`
44
- case 'youtube':
45
- return `https://www.youtube.com/embed/${props.id}?rel=0&disablekb=1&playsinline=1&autoplay=${props.autoplay}`
46
- case 'douyin':
47
- return `https://open.douyin.com/player/video?vid=${props.id}`
48
- case 'douyin-wide':
49
- return `https://open.douyin.com/player/video?vid=${props.id}`
50
- case 'tiktok':
51
- return `https://www.tiktok.com/embed/v3/${props.id}`
52
- default:
53
- return props.id
54
- }
55
- })
56
-
57
- const videoPlayer = useTemplateRef('video-player')
58
-
59
- // 抖音会预检测宽度是否小于 730px,再返回竖屏或横屏模式
60
- // douyin-wide 需要阻塞加载,等进入视口后通过 zoom: 0.1 触发横屏模式
61
- // 已优化为 loading="lazy"
62
- const zoom = ref<number>()
63
- const zoomFactorForFixed = {
64
- 'douyin': 0.0031,
65
- 'douyin-wide': 0.00134,
66
- }
67
-
68
- function useZoomHelper(resizeType: keyof typeof zoomFactorForFixed) {
69
- useResizeObserver(videoPlayer, ([entry]) => {
70
- if (!entry)
71
- return
72
- const { width } = entry.contentRect
73
- const zoomFactor = zoomFactorForFixed[resizeType]
74
- zoom.value = width * zoomFactor
75
- })
76
- }
77
-
78
- onMounted(() => {
79
- if (props.type in zoomFactorForFixed)
80
- useZoomHelper(props.type as keyof typeof zoomFactorForFixed)
81
- })
82
- </script>
83
-
84
- <template>
85
- <div
86
- ref="video-player"
87
- class="video"
88
- :style="{
89
- aspectRatio: ratio,
90
- maxWidth: width,
91
- maxHeight: height,
92
- }"
93
- >
94
- <video
95
- v-if="type === 'raw'"
96
- :poster
97
- :src
98
- controls
99
- />
100
- <iframe
101
- v-else
102
- :src
103
- :style="{ zoom }"
104
- scrolling="no"
105
- loading="lazy"
106
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture"
107
- />
108
- </div>
109
- </template>
110
-
111
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ /** 视频类型,数字过长时需通过双引号传入字符串 */
4
+ type?: 'raw' | 'bilibili' | 'bilibili-nano' | 'youtube' | 'douyin' | 'douyin-wide' | 'tiktok'
5
+ id: string
6
+ autoplay?: boolean
7
+ /** 视频宽高比,如 `16 / 9` `1.6` */
8
+ ratio?: string | number
9
+ /** 仅 type 为 `raw` 时可用 */
10
+ poster?: string
11
+ /** 视频宽度 */
12
+ width?: string
13
+ /** 视频高度 */
14
+ height?: string
15
+ /** CSS Zoom 比例 */
16
+ zoom?: number
17
+ }>(), {
18
+ type: 'raw',
19
+ height: '80vh',
20
+ })
21
+
22
+ const ratio = computed(() => {
23
+ if (props.ratio) {
24
+ return props.ratio
25
+ }
26
+ switch (props.type) {
27
+ case 'raw':
28
+ return undefined
29
+ case 'douyin':
30
+ return '27 / 56'
31
+ case 'douyin-wide':
32
+ return '1198 / 731'
33
+ default:
34
+ return '16 / 9'
35
+ }
36
+ })
37
+
38
+ const src = computed(() => {
39
+ switch (props.type) {
40
+ case 'bilibili':
41
+ return `https://player.bilibili.com/player.html?bvid=${props.id}&autoplay=${props.autoplay}`
42
+ case 'bilibili-nano':
43
+ return `https://www.bilibili.com/blackboard/newplayer.html?bvid=${props.id}&autoplay=${props.autoplay}`
44
+ case 'youtube':
45
+ return `https://www.youtube.com/embed/${props.id}?rel=0&disablekb=1&playsinline=1&autoplay=${props.autoplay}`
46
+ case 'douyin':
47
+ return `https://open.douyin.com/player/video?vid=${props.id}`
48
+ case 'douyin-wide':
49
+ return `https://open.douyin.com/player/video?vid=${props.id}`
50
+ case 'tiktok':
51
+ return `https://www.tiktok.com/embed/v3/${props.id}`
52
+ default:
53
+ return props.id
54
+ }
55
+ })
56
+
57
+ const videoPlayer = useTemplateRef('video-player')
58
+
59
+ // 抖音会预检测宽度是否小于 730px,再返回竖屏或横屏模式
60
+ // douyin-wide 需要阻塞加载,等进入视口后通过 zoom: 0.1 触发横屏模式
61
+ // 已优化为 loading="lazy"
62
+ const zoom = ref<number>()
63
+ const zoomFactorForFixed = {
64
+ 'douyin': 0.0031,
65
+ 'douyin-wide': 0.00134,
66
+ }
67
+
68
+ function useZoomHelper(resizeType: keyof typeof zoomFactorForFixed) {
69
+ useResizeObserver(videoPlayer, ([entry]) => {
70
+ if (!entry)
71
+ return
72
+ const { width } = entry.contentRect
73
+ const zoomFactor = zoomFactorForFixed[resizeType]
74
+ zoom.value = width * zoomFactor
75
+ })
76
+ }
77
+
78
+ onMounted(() => {
79
+ if (props.type in zoomFactorForFixed)
80
+ useZoomHelper(props.type as keyof typeof zoomFactorForFixed)
81
+ })
82
+ </script>
83
+
84
+ <template>
85
+ <div
86
+ ref="video-player"
87
+ class="video"
88
+ :style="{
89
+ aspectRatio: ratio,
90
+ maxWidth: width,
91
+ maxHeight: height,
92
+ }"
93
+ >
94
+ <video
95
+ v-if="type === 'raw'"
96
+ :poster
97
+ :src
98
+ controls
99
+ />
100
+ <iframe
101
+ v-else
102
+ :src
103
+ :style="{ zoom }"
104
+ scrolling="no"
105
+ loading="lazy"
106
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture"
107
+ />
108
+ </div>
109
+ </template>
110
+
111
+ <style lang="scss" scoped>
112
112
  .video {
113
113
  contain: paint;
114
114
  border-radius: 0.8rem;
@@ -124,4 +124,4 @@ iframe, video {
124
124
  width: 100%;
125
125
  height: 100%;
126
126
  }
127
- </style>
127
+ </style>
@@ -1,38 +1,38 @@
1
- <script setup lang="ts">
2
- import { UtilLink } from '#components'
3
-
4
- export interface ButtonProps {
5
- variant?: 'button' | 'text'
6
- icon?: string
7
- text?: string
8
- to?: string
9
- desc?: string
10
- primary?: boolean
11
- }
12
-
13
- withDefaults(defineProps<ButtonProps>(), {
14
- variant: 'button',
15
- })
16
- </script>
17
-
18
- <template>
19
- <component
20
- :is="to ? UtilLink : 'button'"
21
- :to
22
- class="z-button"
23
- :class="[variant, { primary }]"
24
- >
25
- <div class="button-main">
26
- <Icon v-if="icon" :name="icon" />
27
- <slot>{{ text }}</slot>
28
- </div>
29
- <div v-if="desc" class="button-desc">
30
- {{ desc }}
31
- </div>
32
- </component>
33
- </template>
34
-
35
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import { UtilLink } from '#components'
3
+
4
+ export interface ButtonProps {
5
+ variant?: 'button' | 'text'
6
+ icon?: string
7
+ text?: string
8
+ to?: string
9
+ desc?: string
10
+ primary?: boolean
11
+ }
12
+
13
+ withDefaults(defineProps<ButtonProps>(), {
14
+ variant: 'button',
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <component
20
+ :is="to ? UtilLink : 'button'"
21
+ :to
22
+ class="z-button"
23
+ :class="[variant, { primary }]"
24
+ >
25
+ <div class="button-main">
26
+ <Icon v-if="icon" :name="icon" />
27
+ <slot>{{ text }}</slot>
28
+ </div>
29
+ <div v-if="desc" class="button-desc">
30
+ {{ desc }}
31
+ </div>
32
+ </component>
33
+ </template>
34
+
35
+ <style lang="scss" scoped>
36
36
  .z-button {
37
37
  display: inline-block;
38
38
  transition: color 0.1s, background-color 0.2s;
@@ -95,4 +95,4 @@ withDefaults(defineProps<ButtonProps>(), {
95
95
  text-align: center;
96
96
  color: var(--c-text-2);
97
97
  }
98
- </style>
98
+ </style>