@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sugarat/theme",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -9,15 +9,36 @@
9
9
  预计:{{ readTime }} 分钟
10
10
  </span>
11
11
  </div>
12
- <div class="meta-des 123" ref="$des" id="hack-article-des">
13
- <span v-if="author">
14
- <el-icon><UserFilled /></el-icon>
15
- {{ author }}
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 { UserFilled, Clock, EditPen, AlarmClock } from '@element-plus/icons-vue'
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
- () => currentArticle.value?.meta.author || globalAuthor.value
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, onMounted } from 'vue'
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
- onMounted(() => {
74
- const url = new URL(window.location.href)
75
- activeTag.value.type = url.searchParams.get('type') || ''
76
- activeTag.value.label = url.searchParams.get('tag') || ''
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
- <div class="info-part">
6
- <!-- 标题 -->
7
- <p class="title">{{ title }}</p>
8
- <!-- 简短描述 -->
9
- <p class="description" v-if="!!description">{{ description }}</p>
10
- <div class="badge-list">
11
- <span class="split" v-if="author">{{ author }}</span>
12
- <span class="split">{{ showTime }}</span>
13
- <span class="split" v-if="tag?.length">{{ tag.join(' · ') }}</span>
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
- align-items: center;
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.author || '')
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) => a.meta!.top - b.meta!.top)
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
- // TODO: 本地全文搜索定制 pagefind || minisearch || flexsearch
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 {