clarity-theme 0.1.2 → 0.1.4
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.
- package/README.md +55 -271
- package/README.zh-CN.md +56 -290
- package/nuxt.config.ts +17 -1
- package/package.json +10 -2
- package/src/app.config.ts +8 -104
- package/src/assets/icons/zhilu.svg +8 -0
- package/src/blog.config.ts +71 -0
- package/src/components/blog/BlogFooter.vue +3 -3
- package/src/components/blog/BlogHeader.global.vue +9 -9
- package/src/components/blog/BlogSidebar.vue +3 -3
- package/src/components/blog/ThemeToggle.vue +2 -2
- package/src/components/content/Alert.vue +2 -2
- package/src/components/content/FeedCard.vue +2 -2
- package/src/components/content/Mermaid.vue +1 -1
- package/src/components/content/ProseCode.vue +4 -32
- package/src/components/content/ProsePre.vue +2 -2
- package/src/components/content/ProseTable.vue +1 -1
- package/src/components/popover/Search.vue +3 -3
- package/src/components/post/Comment.vue +3 -15
- package/src/components/post/Excerpt.vue +3 -3
- package/src/components/post/OrderToggle.vue +3 -3
- package/src/components/post/PostFooter.vue +3 -4
- package/src/components/post/PostHeader.vue +4 -4
- package/src/components/post/PostSurround.vue +2 -2
- package/src/components/post/Slide.vue +2 -2
- package/src/components/util/Img.vue +1 -1
- package/src/components/widget/BlogLog.vue +9 -19
- package/src/components/widget/BlogStats.vue +4 -4
- package/src/components/widget/BlogTech.vue +18 -17
- package/src/components/widget/CommGroup.vue +43 -0
- package/src/composables/useArticle.ts +9 -9
- package/src/composables/usePagination.ts +2 -2
- package/src/composables/useWidgets.ts +2 -0
- package/src/config/app.ts +50 -4
- package/src/config/content.ts +6 -0
- package/src/config/define.mjs +1 -1
- package/src/config/define.ts +1 -1
- package/src/config/feed.ts +2 -42
- package/src/config/pnpm-workspace.ts +19 -0
- package/src/config/public.ts +1 -1
- package/src/config/ui.ts +111 -0
- package/src/generated/blog.config.mjs +3 -0
- package/src/generated/package-json.mjs +4 -0
- package/src/generated/pnpm-workspace.mjs +1 -0
- package/src/modules/anti-mirror/index.ts +33 -0
- package/src/modules/{clarity-config/anti-mirror-client.ts → anti-mirror/runtime/client.ts} +3 -3
- package/src/modules/clarity-config/index.ts +335 -77
- package/src/modules/clarity-source-layout/index.ts +13 -0
- package/src/pages/archive.vue +4 -4
- package/src/pages/index.vue +4 -3
- package/src/pages/link.vue +5 -5
- package/src/pages/preview.vue +2 -2
- package/src/server/api/stats.get.ts +9 -19
- package/src/server/routes/atom.xml.get.ts +48 -50
- package/src/server/routes/subscriptions.opml.get.ts +14 -31
- package/src/shared/utils/time.ts +13 -18
- package/src/types/article.ts +2 -2
- package/src/types/feed.ts +35 -1
- package/src/types/upstream-compat.d.ts +17 -0
- package/src/types/yaml-modules.d.ts +26 -0
- package/src/shared/types/content.ts +0 -30
- package/src/types/modal.ts +0 -16
- /package/src/assets/icons/{solar-rewind-back-bold-duotone.svg → SolarRewindBackBoldDuotone.svg} +0 -0
- /package/src/assets/icons/{solar-rewind-forward-bold-duotone.svg → SolarRewindForwardBoldDuotone.svg} +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上游 `blog.config.ts` 兼容适配层(Layer 边界的唯一转换点)。
|
|
3
|
+
*
|
|
4
|
+
* blog-v3 的组件与 server 路由通过 `~~/blog.config` 直接读取站点配置;
|
|
5
|
+
* Clarity 的消费项目使用 `clarity.config.ts`(嵌套 schema),
|
|
6
|
+
* 因此由 src/modules/clarity-config 注入 `~~/blog.config` 别名指向本文件,
|
|
7
|
+
* 将 clarity.config.ts 机械映射回上游的扁平配置形状(含 myFeed 导出)。
|
|
8
|
+
* 上游同步文件因此可以保持 byte-identical,不再逐文件改写。
|
|
9
|
+
*/
|
|
10
|
+
import type { FeedEntry } from './types/feed'
|
|
11
|
+
import config from '#clarity/config'
|
|
12
|
+
|
|
13
|
+
const { site, article, feed, stats, integrations } = config
|
|
14
|
+
|
|
15
|
+
const blogConfig = {
|
|
16
|
+
title: site.title,
|
|
17
|
+
subtitle: site.subtitle ?? '',
|
|
18
|
+
description: site.description,
|
|
19
|
+
author: {
|
|
20
|
+
name: site.author.name,
|
|
21
|
+
avatar: site.author.avatar ?? '',
|
|
22
|
+
email: site.author.email ?? '',
|
|
23
|
+
homepage: site.author.homepage ?? '/',
|
|
24
|
+
},
|
|
25
|
+
copyright: site.copyright ?? { abbr: '', name: '', url: '' },
|
|
26
|
+
favicon: site.favicon,
|
|
27
|
+
language: site.language,
|
|
28
|
+
timeEstablished: site.established ?? '',
|
|
29
|
+
timeZone: site.timezone,
|
|
30
|
+
url: site.url,
|
|
31
|
+
defaultCategory: article.defaultCategory,
|
|
32
|
+
|
|
33
|
+
article: {
|
|
34
|
+
categories: article.categories,
|
|
35
|
+
types: article.types,
|
|
36
|
+
order: article.order,
|
|
37
|
+
/** 仅供上游 pnpm new 脚本使用,Layer 消费场景不涉及 */
|
|
38
|
+
useRandomPremalat: false,
|
|
39
|
+
hidePostPrefix: article.hidePostPrefix,
|
|
40
|
+
robotsNotIndex: article.robotsNotIndex,
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/** 博客 Atom 订阅源 */
|
|
44
|
+
feed,
|
|
45
|
+
|
|
46
|
+
/** 向 <head> 中添加脚本 */
|
|
47
|
+
scripts: integrations.scripts,
|
|
48
|
+
|
|
49
|
+
/** 文章统计配置 */
|
|
50
|
+
stats,
|
|
51
|
+
|
|
52
|
+
/** 自己部署的 Twikoo 服务 */
|
|
53
|
+
twikoo: integrations.twikoo ?? { envId: '', preload: '' },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 用于生成 OPML 和友链页面配置(与上游 blog.config.ts 的导出保持一致) */
|
|
57
|
+
export const myFeed: FeedEntry = {
|
|
58
|
+
author: blogConfig.author.name,
|
|
59
|
+
sitenick: blogConfig.title,
|
|
60
|
+
title: blogConfig.title,
|
|
61
|
+
desc: blogConfig.subtitle || blogConfig.description,
|
|
62
|
+
link: blogConfig.url,
|
|
63
|
+
feed: new URL('/atom.xml', blogConfig.url).toString(),
|
|
64
|
+
icon: blogConfig.favicon,
|
|
65
|
+
avatar: blogConfig.author.avatar,
|
|
66
|
+
archs: ['Nuxt'],
|
|
67
|
+
date: blogConfig.timeEstablished,
|
|
68
|
+
comment: '这是我自己',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default blogConfig
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const
|
|
2
|
+
const appConfig = useAppConfig()
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
6
|
<footer class="blog-footer">
|
|
7
7
|
<nav class="footer-nav">
|
|
8
|
-
<div v-for="(group, groupIndex) in
|
|
8
|
+
<div v-for="(group, groupIndex) in appConfig.footer.nav" :key="groupIndex">
|
|
9
9
|
<hgroup class="text-creative" v-text="group.title" />
|
|
10
10
|
<menu>
|
|
11
11
|
<li v-for="(item, itemIndex) in group.items" :key="itemIndex">
|
|
@@ -17,7 +17,7 @@ const clarity = useClarityConfig()
|
|
|
17
17
|
</menu>
|
|
18
18
|
</div>
|
|
19
19
|
</nav>
|
|
20
|
-
<p v-html="
|
|
20
|
+
<p v-html="appConfig.footer.copyright" />
|
|
21
21
|
</footer>
|
|
22
22
|
</template>
|
|
23
23
|
|
|
@@ -4,14 +4,14 @@ withDefaults(defineProps<{
|
|
|
4
4
|
}>(), {
|
|
5
5
|
tag: 'div',
|
|
6
6
|
})
|
|
7
|
-
const
|
|
7
|
+
const appConfig = useAppConfig()
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
10
|
<template>
|
|
11
11
|
<UtilLink class="blog-header">
|
|
12
|
-
<div v-if="
|
|
12
|
+
<div v-if="appConfig.header.emojiTail" class="emoji-tail">
|
|
13
13
|
<span
|
|
14
|
-
v-for="(emoji, emojiIndex) in
|
|
14
|
+
v-for="(emoji, emojiIndex) in appConfig.header.emojiTail"
|
|
15
15
|
:key="emojiIndex"
|
|
16
16
|
class="split-char"
|
|
17
17
|
:style="getFixedDelay(emojiIndex * .6 - 3)"
|
|
@@ -20,16 +20,16 @@ const clarity = useClarityConfig()
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<NuxtImg
|
|
23
|
-
:src="
|
|
23
|
+
:src="appConfig.header.logo"
|
|
24
24
|
class="blog-logo round-cobblestone"
|
|
25
|
-
:class="{ circle:
|
|
26
|
-
:alt="
|
|
25
|
+
:class="{ circle: appConfig.header.showTitle }"
|
|
26
|
+
:alt="appConfig.title"
|
|
27
27
|
/>
|
|
28
28
|
|
|
29
|
-
<div v-if="
|
|
29
|
+
<div v-if="appConfig.header.showTitle" class="blog-text">
|
|
30
30
|
<component :is="tag" class="header-title">
|
|
31
31
|
<span
|
|
32
|
-
v-for="(char, charIndex) in
|
|
32
|
+
v-for="(char, charIndex) in appConfig.title"
|
|
33
33
|
:key="charIndex"
|
|
34
34
|
class="split-char"
|
|
35
35
|
:style="getFixedDelay((charIndex + 1) * .1)"
|
|
@@ -38,7 +38,7 @@ const clarity = useClarityConfig()
|
|
|
38
38
|
</component>
|
|
39
39
|
|
|
40
40
|
<div class="header-subtitle">
|
|
41
|
-
{{
|
|
41
|
+
{{ appConfig.header.subtitle }}
|
|
42
42
|
</div>
|
|
43
43
|
</div>
|
|
44
44
|
</UtilLink>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const
|
|
2
|
+
const appConfig = useAppConfig()
|
|
3
3
|
const layoutStore = useLayoutStore()
|
|
4
4
|
const searchStore = useSearchStore()
|
|
5
5
|
|
|
@@ -25,7 +25,7 @@ const debouncedSelection = refDebounced(text)
|
|
|
25
25
|
<Key class="keycut" code="K" cmd prevent @press="layoutStore.toggle('search')" />
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
|
-
<template v-for="(group, groupIndex) in
|
|
28
|
+
<template v-for="(group, groupIndex) in appConfig.nav" :key="groupIndex">
|
|
29
29
|
<h3 v-if="group.title">
|
|
30
30
|
{{ group.title }}
|
|
31
31
|
</h3>
|
|
@@ -44,7 +44,7 @@ const debouncedSelection = refDebounced(text)
|
|
|
44
44
|
|
|
45
45
|
<footer class="sidebar-footer">
|
|
46
46
|
<BlogThemeToggle />
|
|
47
|
-
<ZIconNavList :list="
|
|
47
|
+
<ZIconNavList :list="appConfig.footer.iconNav" />
|
|
48
48
|
</footer>
|
|
49
49
|
</aside>
|
|
50
50
|
</template>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const
|
|
2
|
+
const appConfig = useAppConfig()
|
|
3
3
|
const colorMode = useColorMode()
|
|
4
4
|
</script>
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
7
|
<div class="theme-toggle">
|
|
8
8
|
<button
|
|
9
|
-
v-for="(themeData, themeName) in
|
|
9
|
+
v-for="(themeData, themeName) in appConfig.themes"
|
|
10
10
|
:key="themeName"
|
|
11
11
|
v-tip="themeData.tip"
|
|
12
12
|
:aria-label="themeData.tip"
|
|
@@ -11,8 +11,8 @@ const props = withDefaults(defineProps<{
|
|
|
11
11
|
type: 'tip',
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
const card = computed(() =>
|
|
14
|
+
const appConfig = useAppConfig()
|
|
15
|
+
const card = computed(() => appConfig.component.alert.defaultStyle === 'flat' ? props.card : !props.flat)
|
|
16
16
|
|
|
17
17
|
const typeMap = {
|
|
18
18
|
tip: {
|
|
@@ -6,7 +6,7 @@ import { Temporal } from 'temporal-polyfill'
|
|
|
6
6
|
|
|
7
7
|
const props = defineProps<FeedEntry>()
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const appConfig = useAppConfig()
|
|
10
10
|
const route = useRoute()
|
|
11
11
|
const isInspect = computed(() => import.meta.dev && route.query.inspect !== undefined)
|
|
12
12
|
|
|
@@ -50,7 +50,7 @@ function getInspectStyle(src: string): CSSProperties {
|
|
|
50
50
|
</ClientOnly>
|
|
51
51
|
|
|
52
52
|
<NuxtImg v-else class="round-cobblestone" :src="avatar" :alt="author" loading="lazy" />
|
|
53
|
-
<Icon v-if="
|
|
53
|
+
<Icon v-if="appConfig.link.remindNoFeed && !feed" class="no-feed" name="tabler:bell-off" />
|
|
54
54
|
</div>
|
|
55
55
|
|
|
56
56
|
<span class="author">{{ author }}</span>
|
|
@@ -1,37 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { VNode } from 'vue'
|
|
3
|
-
|
|
4
2
|
const props = defineProps<{
|
|
5
3
|
language?: string
|
|
6
|
-
|
|
7
|
-
* 代码原文。
|
|
8
|
-
* MDC 原生行为中 inline code 通过默认插槽(text VNode)传递,
|
|
9
|
-
* 上游 @nuxtjs/mdc patch(inline code props.code 传入原文)存在时走此 prop,
|
|
10
|
-
* fenced code(ProsePre 内部)也始终通过此 prop 传递。
|
|
11
|
-
*/
|
|
12
|
-
code?: string
|
|
4
|
+
code: string
|
|
13
5
|
copy?: boolean
|
|
14
6
|
}>()
|
|
15
7
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
/** 从插槽 VNode 中递归提取纯文本 */
|
|
19
|
-
function extractText(vnode: VNode): string {
|
|
20
|
-
if (typeof vnode.children === 'string')
|
|
21
|
-
return vnode.children
|
|
22
|
-
if (Array.isArray(vnode.children))
|
|
23
|
-
return (vnode.children as VNode[]).map(extractText).join('')
|
|
24
|
-
return ''
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** 无 patch 时以插槽文本作为原文,有 patch 时优先使用 code prop */
|
|
28
|
-
const rawCode = computed(() => {
|
|
29
|
-
if (props.code !== undefined)
|
|
30
|
-
return props.code
|
|
31
|
-
return (slots.default?.() ?? []).map(extractText).join('')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const { copy: copyCode, copied } = useCopy(rawCode.value)
|
|
8
|
+
const { copy: copyCode, copied } = useCopy(props.code)
|
|
35
9
|
const shiki = useShiki()
|
|
36
10
|
const codeElement = useTemplateRef('code')
|
|
37
11
|
const highlighted = ref(false)
|
|
@@ -39,7 +13,7 @@ const highlighted = ref(false)
|
|
|
39
13
|
onMounted(async () => {
|
|
40
14
|
if (!props.language)
|
|
41
15
|
return
|
|
42
|
-
await shiki.mountInline(codeElement.value!,
|
|
16
|
+
await shiki.mountInline(codeElement.value!, props.code, {
|
|
43
17
|
language: props.language,
|
|
44
18
|
transformerOptions: ['ignoreColorizedBrackets'],
|
|
45
19
|
})
|
|
@@ -49,9 +23,7 @@ onMounted(async () => {
|
|
|
49
23
|
|
|
50
24
|
<template>
|
|
51
25
|
<code ref="code" :class="{ copyable: copy }">
|
|
52
|
-
<template v-if="!language || !highlighted">
|
|
53
|
-
<slot>{{ rawCode }}</slot>
|
|
54
|
-
</template>
|
|
26
|
+
<template v-if="!language || !highlighted">{{ code }}</template>
|
|
55
27
|
<Icon v-if="copy" v-show="false" name="tabler:check" />
|
|
56
28
|
<button v-if="copy" type="button" class="copy-button" aria-label="复制" @click="copyCode()">
|
|
57
29
|
<Icon :name="copied ? 'tabler:check' : 'tabler:copy'" />
|
|
@@ -27,8 +27,8 @@ const meta = computed(() => props.meta.split(' ').reduce((acc: CodeblockMeta, it
|
|
|
27
27
|
return acc
|
|
28
28
|
}, {}))
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
const compConf = computed(() =>
|
|
30
|
+
const appConfig = useAppConfig()
|
|
31
|
+
const compConf = computed(() => appConfig.component.codeblock)
|
|
32
32
|
|
|
33
33
|
const rows = computed(() => props.code.split('\n').length - 1)
|
|
34
34
|
const collapsible = computed(() => !meta.value.expand && rows.value > compConf.value.triggerRows)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { ModalEmits, ModalProps } from '
|
|
2
|
+
import type { ModalEmits, ModalProps } from '#modals'
|
|
3
3
|
import MiniSearch from 'minisearch'
|
|
4
4
|
|
|
5
5
|
const props = defineProps<ModalProps>()
|
|
6
6
|
|
|
7
7
|
defineEmits<ModalEmits>()
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const segmenter = Intl.Segmenter && new Intl.Segmenter(
|
|
9
|
+
const appConfig = useAppConfig()
|
|
10
|
+
const segmenter = Intl.Segmenter && new Intl.Segmenter(appConfig.language, { granularity: 'word' })
|
|
11
11
|
|
|
12
12
|
// await useAsyncData() 会阻塞渲染
|
|
13
13
|
const { data, status } = await useLazyAsyncData(
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script setup lang="tsx">
|
|
2
2
|
import type { TippyComponent } from 'vue-tippy'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const enabled = computed(() => !!clarity.integrations.twikoo?.envId)
|
|
4
|
+
const appConfig = useAppConfig()
|
|
6
5
|
|
|
7
6
|
const commentEl = useTemplateRef('comment')
|
|
8
7
|
const popoverEl = useTemplateRef<TippyComponent>('popover')
|
|
@@ -53,11 +52,8 @@ function confirmOpen() {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
onMounted(() => {
|
|
56
|
-
if (!enabled.value)
|
|
57
|
-
return
|
|
58
|
-
|
|
59
55
|
window.twikoo?.init?.({
|
|
60
|
-
envId:
|
|
56
|
+
envId: appConfig.twikoo?.envId,
|
|
61
57
|
// twikoo 会把挂载后的元素变为 #twikoo
|
|
62
58
|
el: '#twikoo',
|
|
63
59
|
})
|
|
@@ -108,12 +104,9 @@ onMounted(() => {
|
|
|
108
104
|
</template>
|
|
109
105
|
</Tooltip>
|
|
110
106
|
|
|
111
|
-
<div
|
|
107
|
+
<div id="twikoo">
|
|
112
108
|
<p>评论加载中...</p>
|
|
113
109
|
</div>
|
|
114
|
-
<p v-else class="no-comment">
|
|
115
|
-
本文暂未开启评论
|
|
116
|
-
</p>
|
|
117
110
|
</section>
|
|
118
111
|
</template>
|
|
119
112
|
|
|
@@ -215,11 +208,6 @@ onMounted(() => {
|
|
|
215
208
|
}
|
|
216
209
|
}
|
|
217
210
|
|
|
218
|
-
.no-comment {
|
|
219
|
-
margin: 2em 0;
|
|
220
|
-
color: var(--c-text-3);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
211
|
:deep(:where(.tk-preview-container,.tk-content)) {
|
|
224
212
|
pre {
|
|
225
213
|
overflow: auto;
|
|
@@ -5,15 +5,15 @@ const props = defineProps<{
|
|
|
5
5
|
excerpt: string
|
|
6
6
|
}>()
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const appConfig = useAppConfig()
|
|
9
9
|
|
|
10
10
|
const excerpt = ref(props.excerpt)
|
|
11
11
|
const caret = ref('')
|
|
12
12
|
|
|
13
|
-
if (
|
|
13
|
+
if (appConfig.component.excerpt?.animation !== false) {
|
|
14
14
|
excerpt.value = ''
|
|
15
15
|
onMounted(async () => {
|
|
16
|
-
caret.value =
|
|
16
|
+
caret.value = appConfig.component.excerpt?.caret ?? '_'
|
|
17
17
|
for (const char of props.excerpt) {
|
|
18
18
|
excerpt.value += char
|
|
19
19
|
await delay(50)
|
|
@@ -9,10 +9,10 @@ const props = defineProps<{
|
|
|
9
9
|
secretDelay?: string
|
|
10
10
|
}>()
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const orderMap = computed(() =>
|
|
12
|
+
const appConfig = useAppConfig()
|
|
13
|
+
const orderMap = computed(() => appConfig.article.order)
|
|
14
14
|
// 配置文件中允许升序时,且未明确禁用升序时,允许升序
|
|
15
|
-
const allowAscending = computed(() =>
|
|
15
|
+
const allowAscending = computed(() => appConfig.pagination.allowAscending ? !props.disableAscending : props.enableAscending)
|
|
16
16
|
|
|
17
17
|
const category = defineModel<string>('category')
|
|
18
18
|
const sortOrder = defineModel<ArticleOrderType>('sortOrder', { default: 'date' })
|
|
@@ -8,7 +8,7 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
|
|
|
8
8
|
title: string
|
|
9
9
|
}>({ inheritAttrs: false })
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const appConfig = useAppConfig()
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<template>
|
|
@@ -38,10 +38,9 @@ const clarity = useClarityConfig()
|
|
|
38
38
|
<ReuseTemplate :title="meta?.slots?.copyright?.props?.title as string || '许可协议'">
|
|
39
39
|
<ContentRenderer v-if="meta?.slots?.copyright" :value="meta?.slots?.copyright" />
|
|
40
40
|
<p v-else>
|
|
41
|
-
本文采用 <ProseA
|
|
42
|
-
{{
|
|
41
|
+
本文采用 <ProseA :href="appConfig.copyright.url">
|
|
42
|
+
{{ appConfig.copyright.name }}
|
|
43
43
|
</ProseA>
|
|
44
|
-
<span v-else-if="clarity.site.copyright?.name">{{ clarity.site.copyright.name }}</span>
|
|
45
44
|
许可协议,转载请注明出处。
|
|
46
45
|
</p>
|
|
47
46
|
</ReuseTemplate>
|
|
@@ -4,13 +4,13 @@ import type { ArticleProps } from '../../types/article'
|
|
|
4
4
|
defineOptions({ inheritAttrs: false })
|
|
5
5
|
const props = defineProps<ArticleProps>()
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const appConfig = useAppConfig()
|
|
8
8
|
|
|
9
9
|
const coverFilter = computed(() => props.meta?.coverFilter || (props.meta?.coverDim && 'brightness(0.75)') || undefined)
|
|
10
10
|
|
|
11
|
-
const shareText = `【${
|
|
11
|
+
const shareText = `【${appConfig.title}】${props.title}\n\n${
|
|
12
12
|
props.description ? `${props.description}\n\n` : ''}${
|
|
13
|
-
new URL(props.path!,
|
|
13
|
+
new URL(props.path!, appConfig.url).href}`
|
|
14
14
|
|
|
15
15
|
const { copy, copied } = useCopy(shareText)
|
|
16
16
|
</script>
|
|
@@ -38,7 +38,7 @@ const { copy, copied } = useCopy(shareText)
|
|
|
38
38
|
/>
|
|
39
39
|
|
|
40
40
|
<UtilDate
|
|
41
|
-
v-if="updated && isTimeDiffSignificant(date, updated,
|
|
41
|
+
v-if="updated && isTimeDiffSignificant(date, updated, 1)"
|
|
42
42
|
v-tip
|
|
43
43
|
:tip-transform="d => `修改于${d}`"
|
|
44
44
|
:date="updated"
|
|
@@ -36,11 +36,11 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
|
|
|
36
36
|
|
|
37
37
|
<div v-if="prev || next" class="surround-post" dir="ltr">
|
|
38
38
|
<ReuseTemplate
|
|
39
|
-
:post="next" icon="
|
|
39
|
+
:post="next" icon="zi:solar-rewind-back-bold-duotone"
|
|
40
40
|
fallback-icon="line-md:coffee-twotone-loop" fallback-text="新故事即将发生"
|
|
41
41
|
/>
|
|
42
42
|
<ReuseTemplate
|
|
43
|
-
:post="prev" icon="
|
|
43
|
+
:post="prev" icon="zi:solar-rewind-forward-bold-duotone"
|
|
44
44
|
fallback-icon="line-md:construction-twotone" fallback-text="已抵达博客尽头"
|
|
45
45
|
align-end
|
|
46
46
|
/>
|
|
@@ -6,8 +6,8 @@ import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures'
|
|
|
6
6
|
|
|
7
7
|
defineProps<{ list: ArticleProps[] }>()
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const compConf = computed(() =>
|
|
9
|
+
const appConfig = useAppConfig()
|
|
10
|
+
const compConf = computed(() => appConfig.component.slide)
|
|
11
11
|
|
|
12
12
|
// @keep-sorted
|
|
13
13
|
const [carouselEl, carouselApi] = emblaCarouselVue({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { joinURL, withLeadingSlash, withTrailingSlash } from 'ufo'
|
|
3
2
|
import ImageComponent from '#build/mdc-image-component.mjs'
|
|
3
|
+
import { joinURL, withLeadingSlash, withTrailingSlash } from 'ufo'
|
|
4
4
|
|
|
5
5
|
export interface UtilImgProps {
|
|
6
6
|
src: string
|
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const
|
|
2
|
+
const { timeEstablished } = useAppConfig()
|
|
3
3
|
|
|
4
|
-
const blogLog =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
const blogLog = [
|
|
5
|
+
{ label: '2025-07-26', value: '重构到 Nuxt 4 + Nuxt Content v3,启用 zhilu.site 域名' },
|
|
6
|
+
{ label: '2024-08-11', value: '重构到 Nuxt 3 + Nuxt Content v2' },
|
|
7
|
+
{ label: '2023-05-24', value: '更换框架为 Hexo,升级博客' },
|
|
8
|
+
{ label: '2020-08-24', value: '使用 zhilu.cyou 域名' },
|
|
9
|
+
{ label: timeEstablished, value: '发布第一篇文章' },
|
|
10
|
+
]
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
<template>
|
|
14
14
|
<BlogWidget card title="更新日志">
|
|
15
|
-
<ZDlGroup
|
|
16
|
-
<p v-else class="no-log">
|
|
17
|
-
暂无更新日志,可在 clarity.config.ts 的 changelog 中配置。
|
|
18
|
-
</p>
|
|
15
|
+
<ZDlGroup size="large" :items="blogLog" />
|
|
19
16
|
</BlogWidget>
|
|
20
17
|
</template>
|
|
21
|
-
|
|
22
|
-
<style lang="scss" scoped>
|
|
23
|
-
.no-log {
|
|
24
|
-
padding: 1em;
|
|
25
|
-
color: var(--c-text-3);
|
|
26
|
-
}
|
|
27
|
-
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { UtilDate } from '#components'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const appConfig = useAppConfig()
|
|
5
5
|
const runtimeConfig = useRuntimeConfig()
|
|
6
6
|
|
|
7
7
|
// 响应头不正确时,stats.value 可能会是字符串,首次属性访问可能为 undefined
|
|
@@ -16,8 +16,8 @@ const yearlyTip = computed(() => Object
|
|
|
16
16
|
|
|
17
17
|
const blogStats = [{
|
|
18
18
|
label: '运营时长',
|
|
19
|
-
value:
|
|
20
|
-
tip:
|
|
19
|
+
value: timeElapse(appConfig.timeEstablished),
|
|
20
|
+
tip: `博客于${appConfig.timeEstablished}上线`,
|
|
21
21
|
}, {
|
|
22
22
|
label: '上次更新',
|
|
23
23
|
value: () => h(UtilDate, {
|
|
@@ -26,7 +26,7 @@ const blogStats = [{
|
|
|
26
26
|
tipPrefix: '构建于',
|
|
27
27
|
}),
|
|
28
28
|
}, {
|
|
29
|
-
label:
|
|
29
|
+
label: appConfig.stats.includePaths.length ? '文章字数' : '总字数',
|
|
30
30
|
value: computed(() => formatNumber(stats.value?.total?.words) || '--'),
|
|
31
31
|
tip: yearlyTip,
|
|
32
32
|
}]
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { Icon } from '#components'
|
|
3
|
+
import { merge } from 'es-toolkit/object'
|
|
4
|
+
import { packageManager, version } from '~~/package.json'
|
|
5
|
+
import pnpmWorkspace from '~~/pnpm-workspace.yaml'
|
|
3
6
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
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
|
|
7
|
+
const appConfig = useAppConfig()
|
|
8
|
+
const { public: { arch, ci, nodeVersion, platform } } = useRuntimeConfig()
|
|
15
9
|
|
|
16
10
|
const ciPlatform = computed(() => {
|
|
17
11
|
const iconName = ciIcons[ci]
|
|
@@ -25,18 +19,25 @@ const ciPlatform = computed(() => {
|
|
|
25
19
|
return h('span', {}, [iconNode, ` ${ci.split(' ')[0]}`])
|
|
26
20
|
})
|
|
27
21
|
|
|
22
|
+
// @ts-expect-error pnpm-workspace.yaml 无类型定义
|
|
23
|
+
const packages = merge(...Object.values(pnpmWorkspace.catalogs))
|
|
24
|
+
const [pm, pmVersion] = packageManager.split('@') as [string, string]
|
|
25
|
+
|
|
28
26
|
const service = computed(() => ([
|
|
29
27
|
...ci ? [{ label: '构建平台', value: ciPlatform }] : [],
|
|
30
|
-
{ label: '
|
|
31
|
-
{ label: '
|
|
28
|
+
{ label: '图片存储', value: () => [h(Icon, { name: 'devicon:cloudflare' }), ' R2'] },
|
|
29
|
+
{ label: '软件协议', value: 'MIT' },
|
|
30
|
+
{ label: '文章许可', value: appConfig.copyright.abbr },
|
|
31
|
+
{ label: '规范域名', value: getDomain(appConfig.url) },
|
|
32
32
|
]))
|
|
33
33
|
|
|
34
34
|
const techstack = computed(() => ([
|
|
35
|
-
{ label: 'Blog', value:
|
|
36
|
-
{ label: '
|
|
37
|
-
{ label: '
|
|
38
|
-
{ label: '
|
|
35
|
+
{ label: 'Blog', value: version },
|
|
36
|
+
{ label: 'Vue', value: packages.vue },
|
|
37
|
+
{ label: 'Nuxt', value: packages.nuxt },
|
|
38
|
+
{ label: 'Content', value: packages['@nuxt/content'] },
|
|
39
39
|
{ label: 'Node', value: nodeVersion },
|
|
40
|
+
{ label: pm, value: pmVersion },
|
|
40
41
|
{ label: 'OS', value: platform },
|
|
41
42
|
{ label: 'Arch', value: arch },
|
|
42
43
|
]))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BlogWidget
|
|
3
|
+
card
|
|
4
|
+
dim
|
|
5
|
+
title="博客/技术社区"
|
|
6
|
+
bg-img="https://p.qlogo.cn/gh/169994096/169994096/100/"
|
|
7
|
+
bg-aside
|
|
8
|
+
>
|
|
9
|
+
<div class="title text-creative">
|
|
10
|
+
纸网接入点
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<Tip copy icon :tip-options="{ placement: 'left' }">
|
|
14
|
+
<Icon name="ri:qq-fill" />
|
|
15
|
+
<span>169994096</span>
|
|
16
|
+
</Tip>
|
|
17
|
+
</BlogWidget>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style lang="scss" scoped>
|
|
21
|
+
.title {
|
|
22
|
+
background-clip: text;
|
|
23
|
+
background-image: linear-gradient(60deg, var(--c-accent) -30%, var(--c-primary), var(--c-text-1));
|
|
24
|
+
background-position: 100% 0;
|
|
25
|
+
background-size: 200%;
|
|
26
|
+
font-size: 1.8em;
|
|
27
|
+
color: transparent;
|
|
28
|
+
transition: background-position 0.2s;
|
|
29
|
+
|
|
30
|
+
.blog-widget:hover & {
|
|
31
|
+
background-position: 0 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.tip {
|
|
36
|
+
font-size: 0.9em;
|
|
37
|
+
line-height: 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:deep(.bg-img).bg-img.bg-img {
|
|
41
|
+
opacity: 0.5;
|
|
42
|
+
}
|
|
43
|
+
</style>
|