@sugarat/theme 0.1.24 → 0.1.25
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/node.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/BlogArticleAnalyze.vue +77 -9
- package/src/components/BlogDocCover.vue +25 -0
- package/src/components/BlogHomeTags.vue +16 -7
- package/src/components/BlogItem.vue +37 -16
- package/src/components/BlogLazyImage.vue +63 -0
- package/src/components/BlogList.vue +6 -6
- package/src/composables/config/index.ts +10 -1
package/node.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ declare namespace Theme {
|
|
|
33
33
|
tag?: string[];
|
|
34
34
|
description?: string;
|
|
35
35
|
cover?: string;
|
|
36
|
+
hiddenCover?: boolean;
|
|
37
|
+
readingTime?: boolean;
|
|
36
38
|
sticky?: number;
|
|
37
39
|
author?: string;
|
|
38
40
|
hidden?: boolean;
|
|
@@ -100,6 +102,7 @@ declare namespace Theme {
|
|
|
100
102
|
}
|
|
101
103
|
interface ArticleConfig {
|
|
102
104
|
readingTime?: boolean;
|
|
105
|
+
hiddenCover?: boolean;
|
|
103
106
|
}
|
|
104
107
|
interface Alert {
|
|
105
108
|
type: 'success' | 'warning' | 'info' | 'error';
|
|
@@ -156,6 +159,12 @@ declare namespace Theme {
|
|
|
156
159
|
author?: string;
|
|
157
160
|
hotArticle?: HotArticle;
|
|
158
161
|
home?: HomeBlog;
|
|
162
|
+
/**
|
|
163
|
+
* 本地全文搜索定制
|
|
164
|
+
* 内置pagefind 实现,
|
|
165
|
+
* VitePress 官方提供 minisearch 实现,
|
|
166
|
+
* 社区提供 flexsearch 实现
|
|
167
|
+
*/
|
|
159
168
|
search?: SearchConfig;
|
|
160
169
|
/**
|
|
161
170
|
* 配置评论
|
|
@@ -170,6 +179,7 @@ declare namespace Theme {
|
|
|
170
179
|
alert?: Alert;
|
|
171
180
|
popover?: Popover;
|
|
172
181
|
friend?: FriendLink[];
|
|
182
|
+
authorList?: Omit<FriendLink, 'avatar'>[];
|
|
173
183
|
}
|
|
174
184
|
interface Config extends DefaultTheme.Config {
|
|
175
185
|
blog?: BlogConfig;
|
package/package.json
CHANGED
|
@@ -9,15 +9,36 @@
|
|
|
9
9
|
预计:{{ readTime }} 分钟
|
|
10
10
|
</span>
|
|
11
11
|
</div>
|
|
12
|
-
<div class="meta-des
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
<div class="meta-des" ref="$des" id="hack-article-des">
|
|
13
|
+
<!-- TODO:是否需要原创?转载等标签,理论上可以添加标签解决 -->
|
|
14
|
+
<span v-if="author && !hiddenAuthor" class="author">
|
|
15
|
+
<el-icon title="本文作者"><UserFilled /></el-icon>
|
|
16
|
+
<a
|
|
17
|
+
class="link"
|
|
18
|
+
:href="currentAuthorInfo.url"
|
|
19
|
+
:title="currentAuthorInfo.des"
|
|
20
|
+
v-if="currentAuthorInfo"
|
|
21
|
+
>
|
|
22
|
+
{{ currentAuthorInfo.nickname }}
|
|
23
|
+
</a>
|
|
24
|
+
<template v-else>
|
|
25
|
+
{{ author }}
|
|
26
|
+
</template>
|
|
16
27
|
</span>
|
|
17
|
-
<span>
|
|
18
|
-
<el-icon><Clock /></el-icon>
|
|
28
|
+
<span v-if="publishDate && !hiddenTime" class="publishDate">
|
|
29
|
+
<el-icon :title="timeTitle"><Clock /></el-icon>
|
|
19
30
|
{{ publishDate }}
|
|
20
31
|
</span>
|
|
32
|
+
<span v-if="tags.length" class="tags">
|
|
33
|
+
<el-icon :title="timeTitle"><CollectionTag /></el-icon>
|
|
34
|
+
<a class="link" :href="`/?tag=${tag}`" v-for="tag in tags" :key="tag"
|
|
35
|
+
>{{ tag }}
|
|
36
|
+
</a>
|
|
37
|
+
</span>
|
|
38
|
+
<!-- 封面展示 -->
|
|
39
|
+
<ClientOnly>
|
|
40
|
+
<BlogDocCover />
|
|
41
|
+
</ClientOnly>
|
|
21
42
|
</div>
|
|
22
43
|
</template>
|
|
23
44
|
|
|
@@ -27,13 +48,31 @@
|
|
|
27
48
|
import { useData, useRoute } from 'vitepress'
|
|
28
49
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
29
50
|
import { ElIcon } from 'element-plus'
|
|
30
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
UserFilled,
|
|
53
|
+
Clock,
|
|
54
|
+
EditPen,
|
|
55
|
+
AlarmClock,
|
|
56
|
+
CollectionTag
|
|
57
|
+
} from '@element-plus/icons-vue'
|
|
31
58
|
import { useBlogConfig, useCurrentArticle } from '../composables/config/blog'
|
|
32
59
|
import countWord, { formatShowDate } from '../utils/index'
|
|
33
60
|
import { Theme } from '../composables/config'
|
|
61
|
+
import BlogDocCover from './BlogDocCover.vue'
|
|
34
62
|
|
|
35
|
-
const { article } = useBlogConfig()
|
|
63
|
+
const { article, authorList } = useBlogConfig()
|
|
36
64
|
const { frontmatter } = useData()
|
|
65
|
+
const tags = computed(() => {
|
|
66
|
+
const { tag, tags, categories } = frontmatter.value
|
|
67
|
+
return [
|
|
68
|
+
...new Set(
|
|
69
|
+
[]
|
|
70
|
+
.concat(tag, tags, categories)
|
|
71
|
+
.flat()
|
|
72
|
+
.filter((v) => !!v)
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
})
|
|
37
76
|
const showAnalyze = computed(
|
|
38
77
|
() => frontmatter.value?.readingTime ?? article?.readingTime ?? true
|
|
39
78
|
)
|
|
@@ -103,11 +142,22 @@ const publishDate = computed(() => {
|
|
|
103
142
|
return formatShowDate(currentArticle.value?.meta?.date || '')
|
|
104
143
|
})
|
|
105
144
|
|
|
145
|
+
const timeTitle = computed(() =>
|
|
146
|
+
frontmatter.value.date ? '发布时间' : '最近修改时间'
|
|
147
|
+
)
|
|
148
|
+
const hiddenTime = computed(() => frontmatter.value.date === false)
|
|
149
|
+
|
|
106
150
|
const { theme } = useData<Theme.Config>()
|
|
107
151
|
const globalAuthor = computed(() => theme.value.blog?.author || '')
|
|
108
152
|
const author = computed(
|
|
109
|
-
() =>
|
|
153
|
+
() =>
|
|
154
|
+
(frontmatter.value.author || currentArticle.value?.meta.author) ??
|
|
155
|
+
globalAuthor.value
|
|
156
|
+
)
|
|
157
|
+
const currentAuthorInfo = computed(() =>
|
|
158
|
+
authorList?.find((v) => author.value === v.nickname)
|
|
110
159
|
)
|
|
160
|
+
const hiddenAuthor = computed(() => frontmatter.value.author === false)
|
|
111
161
|
|
|
112
162
|
watch(
|
|
113
163
|
() => route.path,
|
|
@@ -143,6 +193,7 @@ watch(
|
|
|
143
193
|
font-size: 14px;
|
|
144
194
|
margin-top: 6px;
|
|
145
195
|
display: flex;
|
|
196
|
+
flex-wrap: wrap;
|
|
146
197
|
span {
|
|
147
198
|
margin-right: 16px;
|
|
148
199
|
display: flex;
|
|
@@ -151,5 +202,22 @@ watch(
|
|
|
151
202
|
margin-right: 4px;
|
|
152
203
|
}
|
|
153
204
|
}
|
|
205
|
+
|
|
206
|
+
.link {
|
|
207
|
+
color: var(--vp-c-text-2);
|
|
208
|
+
&:hover {
|
|
209
|
+
color: var(--vp-c-brand);
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
.tags {
|
|
215
|
+
a.link:not(:last-child) {
|
|
216
|
+
&::after {
|
|
217
|
+
content: '·';
|
|
218
|
+
display: inline-block;
|
|
219
|
+
padding: 0 4px;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
154
222
|
}
|
|
155
223
|
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<img class="blog-doc-cover" v-if="cover && !hiddenCover" :src="cover" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script lang="ts" setup>
|
|
6
|
+
import { useData } from 'vitepress'
|
|
7
|
+
import { computed } from 'vue'
|
|
8
|
+
import { useBlogConfig } from '../composables/config/blog'
|
|
9
|
+
|
|
10
|
+
const { frontmatter } = useData()
|
|
11
|
+
const cover = computed(() => frontmatter.value.cover)
|
|
12
|
+
const { article } = useBlogConfig()
|
|
13
|
+
const hiddenCover = computed(
|
|
14
|
+
() => frontmatter.value?.hiddenCover ?? article?.hiddenCover ?? false
|
|
15
|
+
)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<style lang="scss" scoped>
|
|
19
|
+
img.blog-doc-cover.blog-doc-cover.blog-doc-cover {
|
|
20
|
+
width: 100%;
|
|
21
|
+
object-fit: cover;
|
|
22
|
+
max-height: none;
|
|
23
|
+
margin-top: 20px;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
</template>
|
|
30
30
|
|
|
31
31
|
<script lang="ts" setup>
|
|
32
|
-
import { computed,
|
|
32
|
+
import { computed, watch } from 'vue'
|
|
33
33
|
import { ElTag } from 'element-plus'
|
|
34
|
-
import { useDark } from '@vueuse/core'
|
|
34
|
+
import { useBrowserLocation, useDark } from '@vueuse/core'
|
|
35
35
|
import { useRouter } from 'vitepress'
|
|
36
36
|
import { useActiveTag, useArticles } from '../composables/config/blog'
|
|
37
37
|
|
|
@@ -70,11 +70,20 @@ const handleTagClick = (tag: string, type: string) => {
|
|
|
70
70
|
`${window.location.origin}${router.route.path}?tag=${tag}&type=${type}`
|
|
71
71
|
)
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
const location = useBrowserLocation()
|
|
74
|
+
watch(
|
|
75
|
+
() => location.value,
|
|
76
|
+
() => {
|
|
77
|
+
if (location.value.href) {
|
|
78
|
+
const url = new URL(location.value.href!)
|
|
79
|
+
activeTag.value.type = url.searchParams.get('type') || ''
|
|
80
|
+
activeTag.value.label = url.searchParams.get('tag') || ''
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
immediate: true
|
|
85
|
+
}
|
|
86
|
+
)
|
|
78
87
|
</script>
|
|
79
88
|
|
|
80
89
|
<style lang="scss" scoped>
|
|
@@ -1,31 +1,46 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<a class="blog-item" :href="withBase(route)">
|
|
3
3
|
<i class="pin" v-if="!!pin"></i>
|
|
4
|
-
<!--
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
4
|
+
<!-- 标题 -->
|
|
5
|
+
<p class="title" v-if="inMobile">{{ title }}</p>
|
|
6
|
+
<div class="info-container">
|
|
7
|
+
<!-- 左侧信息 -->
|
|
8
|
+
<div class="info-part">
|
|
9
|
+
<!-- 标题 -->
|
|
10
|
+
<p class="title" v-if="!inMobile">{{ title }}</p>
|
|
11
|
+
<!-- 简短描述 -->
|
|
12
|
+
<p class="description" v-if="!!description">{{ description }}</p>
|
|
13
|
+
<!-- 底部补充描述 -->
|
|
14
|
+
<div class="badge-list" v-if="!inMobile">
|
|
15
|
+
<span class="split" v-if="author">{{ author }}</span>
|
|
16
|
+
<span class="split">{{ showTime }}</span>
|
|
17
|
+
<span class="split" v-if="tag?.length">{{ tag.join(' · ') }}</span>
|
|
18
|
+
</div>
|
|
14
19
|
</div>
|
|
20
|
+
<!-- 右侧封面图 -->
|
|
21
|
+
<div
|
|
22
|
+
v-if="cover"
|
|
23
|
+
class="cover-img"
|
|
24
|
+
:style="`background-image: url(${cover});`"
|
|
25
|
+
></div>
|
|
26
|
+
</div>
|
|
27
|
+
<!-- 底部补充描述 -->
|
|
28
|
+
<div class="badge-list" v-if="inMobile">
|
|
29
|
+
<span class="split" v-if="author">{{ author }}</span>
|
|
30
|
+
<span class="split">{{ showTime }}</span>
|
|
31
|
+
<span class="split" v-if="tag?.length">{{ tag.join(' · ') }}</span>
|
|
15
32
|
</div>
|
|
16
|
-
<div
|
|
17
|
-
v-if="cover"
|
|
18
|
-
class="cover-img"
|
|
19
|
-
:style="`background-image: url(${cover});`"
|
|
20
|
-
></div>
|
|
21
33
|
</a>
|
|
22
34
|
</template>
|
|
23
35
|
|
|
24
36
|
<script lang="ts" setup>
|
|
25
37
|
import { withBase } from 'vitepress'
|
|
26
38
|
import { computed } from 'vue'
|
|
39
|
+
import { useWindowSize } from '@vueuse/core'
|
|
27
40
|
import { formatShowDate } from '../utils/index'
|
|
28
41
|
|
|
42
|
+
const { width } = useWindowSize()
|
|
43
|
+
const inMobile = computed(() => width.value <= 500)
|
|
29
44
|
const props = defineProps<{
|
|
30
45
|
route: string
|
|
31
46
|
title: string
|
|
@@ -86,11 +101,16 @@ const showTime = computed(() => {
|
|
|
86
101
|
background-color: rgba(var(--bg-gradient));
|
|
87
102
|
cursor: pointer;
|
|
88
103
|
display: flex;
|
|
89
|
-
|
|
104
|
+
flex-direction: column;
|
|
90
105
|
&:hover {
|
|
91
106
|
box-shadow: var(--box-shadow-hover);
|
|
92
107
|
}
|
|
93
108
|
}
|
|
109
|
+
.info-container {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: flex-start;
|
|
113
|
+
}
|
|
94
114
|
|
|
95
115
|
.info-part {
|
|
96
116
|
flex: 1;
|
|
@@ -114,6 +134,7 @@ const showTime = computed(() => {
|
|
|
114
134
|
.badge-list {
|
|
115
135
|
font-size: 13px;
|
|
116
136
|
color: var(--badge-font-color);
|
|
137
|
+
margin-top: 8px;
|
|
117
138
|
.split:not(:last-child) {
|
|
118
139
|
&::after {
|
|
119
140
|
content: '';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useData, useRoute } from 'vitepress'
|
|
3
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
4
|
+
import { useBlogConfig } from '../composables/config/blog'
|
|
5
|
+
|
|
6
|
+
const routeData = useRoute()
|
|
7
|
+
const { frontmatter } = useData()
|
|
8
|
+
const { article } = useBlogConfig()
|
|
9
|
+
const openLazy = computed(() => {
|
|
10
|
+
return (frontmatter.value.lazy || article?.lazy) ?? true
|
|
11
|
+
})
|
|
12
|
+
const imageList = ref<HTMLImageElement[]>([])
|
|
13
|
+
const currentObserver = ref<IntersectionObserver>()
|
|
14
|
+
const refresh = () => {
|
|
15
|
+
if (openLazy.value) {
|
|
16
|
+
const docDomContainer = window.document.querySelector('#VPContent')
|
|
17
|
+
const imgs = docDomContainer?.querySelectorAll<HTMLImageElement>(
|
|
18
|
+
'.content-container .main img'
|
|
19
|
+
)
|
|
20
|
+
// 销毁旧的
|
|
21
|
+
imageList.value.forEach((v) => currentObserver.value?.unobserve(v))
|
|
22
|
+
|
|
23
|
+
// 存新的
|
|
24
|
+
imageList.value = Array.from(imgs as unknown as HTMLImageElement[])
|
|
25
|
+
|
|
26
|
+
// 初始化
|
|
27
|
+
imageList.value.forEach((img) => {
|
|
28
|
+
const src = img.getAttribute('src')
|
|
29
|
+
if (src) {
|
|
30
|
+
img.removeAttribute('src')
|
|
31
|
+
img.setAttribute('data-src', src)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const observer = new IntersectionObserver((entries) => {
|
|
36
|
+
entries.forEach((entry) => {
|
|
37
|
+
if (entry.isIntersecting) {
|
|
38
|
+
const img = entry.target
|
|
39
|
+
const src = img.getAttribute('data-src')
|
|
40
|
+
if (src) {
|
|
41
|
+
img.setAttribute('src', src)
|
|
42
|
+
}
|
|
43
|
+
observer.unobserve(img)
|
|
44
|
+
|
|
45
|
+
console.log('render', src)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// 监听
|
|
51
|
+
imageList.value.forEach((img) => {
|
|
52
|
+
observer.observe(img)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
watch(routeData, () => {
|
|
57
|
+
refresh()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
onMounted(() => {
|
|
61
|
+
refresh()
|
|
62
|
+
})
|
|
63
|
+
</script>
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { Theme } from '../composables/config'
|
|
37
37
|
|
|
38
38
|
const { theme, frontmatter } = useData<Theme.Config>()
|
|
39
|
-
const globalAuthor = computed(() => theme.value.blog
|
|
39
|
+
const globalAuthor = computed(() => theme.value.blog?.author || '')
|
|
40
40
|
const docs = useArticles()
|
|
41
41
|
|
|
42
42
|
const activeTag = useActiveTag()
|
|
@@ -45,7 +45,11 @@ const activeTagLabel = computed(() => activeTag.value.label)
|
|
|
45
45
|
|
|
46
46
|
const wikiList = computed(() => {
|
|
47
47
|
const topList = docs.value.filter((v) => !!v.meta.top)
|
|
48
|
-
topList.sort((a, b) =>
|
|
48
|
+
topList.sort((a, b) => {
|
|
49
|
+
const aTop = a?.meta?.top
|
|
50
|
+
const bTop = b?.meta.top
|
|
51
|
+
return Number(aTop) - Number(bTop)
|
|
52
|
+
})
|
|
49
53
|
const data = docs.value.filter(
|
|
50
54
|
(v) => v.meta.date && v.meta.title && !v.meta.top && !v.meta.hidden
|
|
51
55
|
)
|
|
@@ -72,7 +76,3 @@ const currentWikiData = computed(() => {
|
|
|
72
76
|
return filterData.value.slice(startIdx, endIdx)
|
|
73
77
|
})
|
|
74
78
|
</script>
|
|
75
|
-
<style lang="scss" scoped>
|
|
76
|
-
.blog-list {
|
|
77
|
-
}
|
|
78
|
-
</style>
|
|
@@ -38,6 +38,8 @@ export namespace Theme {
|
|
|
38
38
|
tag?: string[]
|
|
39
39
|
description?: string
|
|
40
40
|
cover?: string
|
|
41
|
+
hiddenCover?: boolean
|
|
42
|
+
readingTime?: boolean
|
|
41
43
|
sticky?: number
|
|
42
44
|
author?: string
|
|
43
45
|
hidden?: boolean
|
|
@@ -111,6 +113,7 @@ export namespace Theme {
|
|
|
111
113
|
|
|
112
114
|
export interface ArticleConfig {
|
|
113
115
|
readingTime?: boolean
|
|
116
|
+
hiddenCover?: boolean
|
|
114
117
|
}
|
|
115
118
|
export interface Alert {
|
|
116
119
|
type: 'success' | 'warning' | 'info' | 'error'
|
|
@@ -172,7 +175,12 @@ export namespace Theme {
|
|
|
172
175
|
author?: string
|
|
173
176
|
hotArticle?: HotArticle
|
|
174
177
|
home?: HomeBlog
|
|
175
|
-
|
|
178
|
+
/**
|
|
179
|
+
* 本地全文搜索定制
|
|
180
|
+
* 内置pagefind 实现,
|
|
181
|
+
* VitePress 官方提供 minisearch 实现,
|
|
182
|
+
* 社区提供 flexsearch 实现
|
|
183
|
+
*/
|
|
176
184
|
search?: SearchConfig
|
|
177
185
|
/**
|
|
178
186
|
* 配置评论
|
|
@@ -187,6 +195,7 @@ export namespace Theme {
|
|
|
187
195
|
alert?: Alert
|
|
188
196
|
popover?: Popover
|
|
189
197
|
friend?: FriendLink[]
|
|
198
|
+
authorList?: Omit<FriendLink, 'avatar'>[]
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
export interface Config extends DefaultTheme.Config {
|