@sugarat/theme 0.1.29 → 0.1.30

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
@@ -27,6 +27,14 @@ declare namespace BlogPopover {
27
27
  }
28
28
  type Value = Title | Text | Image | Button;
29
29
  }
30
+ type ThemeableImage = string | {
31
+ src: string;
32
+ alt?: string;
33
+ } | {
34
+ light: string;
35
+ dark: string;
36
+ alt?: string;
37
+ };
30
38
  declare namespace Theme {
31
39
  interface PageMeta {
32
40
  title: string;
@@ -140,7 +148,36 @@ declare namespace Theme {
140
148
  nickname: string;
141
149
  des: string;
142
150
  url: string;
143
- avatar: string;
151
+ avatar: ThemeableImage;
152
+ }
153
+ interface UserWork {
154
+ title: string;
155
+ description: string;
156
+ time: string | {
157
+ start: string;
158
+ end?: string;
159
+ lastupdate?: string;
160
+ };
161
+ status?: 'active' | 'negative' | 'off' | {
162
+ text: string;
163
+ };
164
+ url?: string;
165
+ github?: string | {
166
+ owner: string;
167
+ repo: string;
168
+ branch?: string;
169
+ path?: string;
170
+ };
171
+ cover?: string | string[] | {
172
+ urls: string[];
173
+ layout?: 'swiper' | 'list' | 'card';
174
+ };
175
+ links?: {
176
+ title: string;
177
+ url: string;
178
+ }[];
179
+ tags?: string[];
180
+ top?: number;
144
181
  }
145
182
  type SearchConfig = boolean | 'pagefind' | {
146
183
  btnPlaceholder?: string;
@@ -153,6 +190,11 @@ declare namespace Theme {
153
190
  heading?: string;
154
191
  mode?: boolean | 'pagefind';
155
192
  };
193
+ interface UserWorks {
194
+ title: string;
195
+ description?: string;
196
+ list: UserWork[];
197
+ }
156
198
  interface BlogConfig {
157
199
  blog?: false;
158
200
  pagesData: PageData[];
@@ -185,7 +227,8 @@ declare namespace Theme {
185
227
  * 启用 [vitepress-plugin-tabs](https://www.npmjs.com/package/vitepress-plugin-tabs)
186
228
  * @default false
187
229
  */
188
- tabs: boolean;
230
+ tabs?: boolean;
231
+ works?: UserWorks;
189
232
  }
190
233
  interface Config extends DefaultTheme.Config {
191
234
  blog?: BlogConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sugarat/theme",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,14 +1,14 @@
1
1
  <template>
2
- <div class="card friend-wrapper" v-if="friend?.length">
2
+ <div class="card friend-wrapper" v-if="friendList?.length">
3
3
  <!-- 头部 -->
4
4
  <div class="card-header">
5
5
  <span class="title">🤝 友情链接</span>
6
6
  </div>
7
7
  <!-- 文章列表 -->
8
8
  <ol class="friend-list">
9
- <li v-for="v in friend" :key="v.nickname">
9
+ <li v-for="v in friendList" :key="v.nickname">
10
10
  <a :href="v.url" target="_blank">
11
- <el-avatar :size="50" :src="v.avatar" />
11
+ <el-avatar :size="50" :src="v.avatar" :alt="v.alt" />
12
12
  <div>
13
13
  <span class="nickname">{{ v.nickname }}</span>
14
14
  <p class="des">{{ v.des }}</p>
@@ -21,9 +21,32 @@
21
21
 
22
22
  <script lang="ts" setup>
23
23
  import { ElAvatar } from 'element-plus'
24
+ import { useDark } from '@vueuse/core'
25
+ import { computed } from 'vue'
24
26
  import { useBlogConfig } from '../composables/config/blog'
27
+ import { getImageUrl } from '../utils'
28
+
29
+ const isDark = useDark({
30
+ storageKey: 'vitepress-theme-appearance'
31
+ })
25
32
 
26
33
  const { friend } = useBlogConfig()
34
+ const friendList = computed(() => {
35
+ return friend?.map((v) => {
36
+ const { avatar, nickname } = v
37
+ const avatarUrl = getImageUrl(avatar, isDark.value)
38
+ let alt = nickname
39
+ if (typeof avatar !== 'string') {
40
+ alt = avatar.alt || ''
41
+ }
42
+
43
+ return {
44
+ ...v,
45
+ avatar: avatarUrl,
46
+ alt
47
+ }
48
+ })
49
+ })
27
50
  </script>
28
51
 
29
52
  <style lang="scss" scoped>
@@ -1,7 +1,3 @@
1
1
  <template>
2
2
  <div>时间线页面</div>
3
3
  </template>
4
-
5
- <script lang="ts" setup></script>
6
-
7
- <style lang="scss"></style>
@@ -0,0 +1,340 @@
1
+ <template>
2
+ <div class="user-works-page">
3
+ <h1>{{ works.title }}</h1>
4
+ <p v-if="works.description" class="description">{{ works.description }}</p>
5
+ <!-- TODO:侧导筛选时间 -->
6
+ <!-- 过滤,可吸顶 -->
7
+ <div class="filter">
8
+ <!-- 时间: -->
9
+ <div></div>
10
+ <!-- TODO: tags -->
11
+ <div></div>
12
+ </div>
13
+ <!-- 作品列表 -->
14
+ <div class="works">
15
+ <!-- 标题,描述信息,时间,线上链接,代码仓库,示例图片(几张,多种展示样式支持) -->
16
+ <div class="work" v-for="(work, idx) in workList" :key="idx">
17
+ <!-- 大日期标题 -->
18
+ <!-- TODO: 支持锚点 -->
19
+ <h2 v-if="work.year">{{ work.year }}</h2>
20
+ <!-- 作品标题 -->
21
+ <h3 class="title">
22
+ <a v-if="work.url" rel="noopener" target="_blank" :href="work.url">{{
23
+ work.title
24
+ }}</a>
25
+ <span v-else>{{ work.title }}</span>
26
+ </h3>
27
+ <!-- 补充信息 -->
28
+ <div class="info">
29
+ <!-- times -->
30
+ <div class="times">
31
+ <span class="icon">
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ xmlns:xlink="http://www.w3.org/1999/xlink"
35
+ viewBox="0 0 24 24"
36
+ >
37
+ <title>上线时间</title>
38
+ <path
39
+ d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8zm-.22-13h-.06c-.4 0-.72.32-.72.72v4.72c0 .35.18.68.49.86l4.15 2.49c.34.2.78.1.98-.24a.71.71 0 0 0-.25-.99l-3.87-2.3V7.72c0-.4-.32-.72-.72-.72z"
40
+ fill="currentColor"
41
+ ></path>
42
+ </svg>
43
+ </span>
44
+ <span>{{ work.startTime }}</span>
45
+ <span v-if="work.endTime"> - {{ work.endTime }}</span>
46
+ </div>
47
+ <!-- GitHub links-->
48
+ <div class="links" v-if="work.github">
49
+ <a
50
+ class="github-link"
51
+ v-if="work.github"
52
+ :href="(work.github as string)"
53
+ target="_blank"
54
+ rel="noopener"
55
+ >
56
+ <i class="icon">
57
+ <svg
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ xmlns:xlink="http://www.w3.org/1999/xlink"
60
+ viewBox="0 0 496 512"
61
+ >
62
+ <path
63
+ d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6c-3.3.3-5.6-1.3-5.6-3.6c0-2 2.3-3.6 5.2-3.6c3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9c2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9c.3 2 2.9 3.3 5.9 2.6c2.9-.7 4.9-2.6 4.6-4.6c-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2c12.8 2.3 17.3-5.6 17.3-12.1c0-6.2-.3-40.4-.3-61.4c0 0-70 15-84.7-29.8c0 0-11.4-29.1-27.8-36.6c0 0-22.9-15.7 1.6-15.4c0 0 24.9 2 38.6 25.8c21.9 38.6 58.6 27.5 72.9 20.9c2.3-16 8.8-27.1 16-33.7c-55.9-6.2-112.3-14.3-112.3-110.5c0-27.5 7.6-41.3 23.6-58.9c-2.6-6.5-11.1-33.3 2.6-67.9c20.9-6.5 69 27 69 27c20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27c13.7 34.7 5.2 61.4 2.6 67.9c16 17.7 25.8 31.5 25.8 58.9c0 96.5-58.9 104.2-114.8 110.5c9.2 7.9 17 22.9 17 46.4c0 33.7-.3 75.4-.3 83.6c0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252C496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2c1.6 1.6 3.9 2.3 5.2 1c1.3-1 1-3.3-.7-5.2c-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9c1.6 1 3.6.7 4.3-.7c.7-1.3-.3-2.9-2.3-3.9c-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2c2.3 2.3 5.2 2.6 6.5 1c1.3-1.3.7-4.3-1.3-6.2c-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9c1.6 2.3 4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2c-1.4-2.3-4-3.3-5.6-2z"
64
+ fill="currentColor"
65
+ ></path>
66
+ </svg>
67
+ </i>
68
+ <span class="lastupdate" v-if="work.lastUpdate"
69
+ >最后更新时间:{{ work.lastUpdate }}</span
70
+ >
71
+ </a>
72
+ </div>
73
+ <!-- 其它链接 -->
74
+ <div class="links" v-if="work.links?.length">
75
+ <i class="icon" v-if="work.links?.length">
76
+ <svg
77
+ xmlns="http://www.w3.org/2000/svg"
78
+ xmlns:xlink="http://www.w3.org/1999/xlink"
79
+ viewBox="0 0 24 24"
80
+ >
81
+ <g
82
+ fill="none"
83
+ stroke="currentColor"
84
+ stroke-width="2"
85
+ stroke-linecap="round"
86
+ stroke-linejoin="round"
87
+ >
88
+ <path
89
+ d="M10 14a3.5 3.5 0 0 0 5 0l4-4a3.5 3.5 0 0 0-5-5l-.5.5"
90
+ ></path>
91
+ <path
92
+ d="M14 10a3.5 3.5 0 0 0-5 0l-4 4a3.5 3.5 0 0 0 5 5l.5-.5"
93
+ ></path>
94
+ </g>
95
+ </svg>
96
+ </i>
97
+ <a
98
+ class="link"
99
+ v-for="link in work.links || []"
100
+ :href="link.url"
101
+ :key="link.url"
102
+ :title="link.title"
103
+ target="_blank"
104
+ rel="noopener"
105
+ >
106
+ {{ link.title }}
107
+ </a>
108
+ </div>
109
+ </div>
110
+ <!-- 封面图 -->
111
+ <div class="images">
112
+ <!-- swiper -->
113
+ <!-- list -->
114
+ <div class="list-mode">
115
+ <el-image
116
+ v-for="(url, idx) in covers"
117
+ :key="url"
118
+ :src="url"
119
+ loading="lazy"
120
+ :preview-src-list="covers"
121
+ :initial-index="idx"
122
+ hide-on-click-modal
123
+ />
124
+ </div>
125
+ <!-- card -->
126
+ </div>
127
+ <div class="description" v-html="work.description"></div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </template>
132
+
133
+ <script lang="ts" setup>
134
+ import { ElImage } from 'element-plus'
135
+ import { reactive, ref, watch, watchEffect } from 'vue'
136
+ import {
137
+ getGithubUpdateTime,
138
+ formatDate,
139
+ getGithubDirUpdateTime
140
+ } from '../utils'
141
+ import { useUserWorks } from '../composables/config/blog'
142
+ import { Theme } from '../composables/config'
143
+
144
+ const works = useUserWorks()
145
+ const workList = reactive<
146
+ (Theme.UserWork & {
147
+ year?: string | undefined
148
+ startTime: string
149
+ lastUpdate?: string
150
+ endTime?: string
151
+ })[]
152
+ >([])
153
+
154
+ // 格式化数据
155
+ watch(
156
+ works,
157
+ (val) => {
158
+ const sortDate = [...val.list].map((v) => {
159
+ const { time } = v
160
+ const metaInfo =
161
+ typeof time === 'string'
162
+ ? {
163
+ startTime: time,
164
+ endTime: '',
165
+ lastUpdate: ''
166
+ }
167
+ : {
168
+ startTime: time.start,
169
+ endTime: time.end,
170
+ lastUpdate: time.lastupdate
171
+ }
172
+
173
+ return {
174
+ ...v,
175
+ ...metaInfo
176
+ }
177
+ })
178
+ // 数据排序
179
+ sortDate.sort((a, b) => +new Date(b.startTime) - +new Date(a.startTime))
180
+
181
+ // 数据分组
182
+ const groupDate = sortDate.reduce((prev, cur) => {
183
+ const { startTime } = cur
184
+ const year = new Date(startTime).getFullYear()
185
+ const data = { ...cur }
186
+ if (!prev[year]) {
187
+ prev[year] = []
188
+ // 第一项数据加上year属性
189
+ // @ts-ignore
190
+ data.year = year
191
+ }
192
+ prev[year].push(data)
193
+ return prev
194
+ }, {} as Record<string, (Theme.UserWork & { year?: string; startTime: string })[]>)
195
+ workList.push(...Object.values(groupDate).reverse().flat())
196
+ },
197
+ { immediate: true }
198
+ )
199
+
200
+ const init = ref(true)
201
+ // 更新时间信息
202
+ watchEffect(() => {
203
+ if (workList.length && init.value) {
204
+ init.value = false
205
+ workList.forEach((data) => {
206
+ // 接口获取最后更新时间
207
+ if (!data.lastUpdate && data.github) {
208
+ data.lastUpdate = '获取中...'
209
+ const { github } = data
210
+ if (typeof github === 'string') {
211
+ getGithubUpdateTime(github)
212
+ .then((time) => {
213
+ data.lastUpdate = formatDate(time, 'yyyy-MM-dd')
214
+ })
215
+ .catch(() => {
216
+ data.lastUpdate = '地址解析失败'
217
+ })
218
+ } else {
219
+ const { owner, repo, path, branch } = github
220
+ // 拼接Github链接
221
+ let githubUrl = `https://github.com/${owner}/${repo}`
222
+ if (path) {
223
+ githubUrl += `/tree/${branch || 'master'}/${path}`
224
+ } else {
225
+ githubUrl += `/tree/${branch}`
226
+ }
227
+ data.github = githubUrl
228
+ getGithubDirUpdateTime(owner, repo, path ?? '', branch)
229
+ .then((time) => {
230
+ data.lastUpdate = formatDate(time, 'yyyy-MM-dd')
231
+ })
232
+ .catch(() => {
233
+ data.lastUpdate = '地址解析失败'
234
+ })
235
+ }
236
+ }
237
+ })
238
+ }
239
+ })
240
+
241
+ const covers = [
242
+ 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
243
+ 'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
244
+ 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
245
+ 'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
246
+ 'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
247
+ 'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
248
+ 'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
249
+ ]
250
+ </script>
251
+
252
+ <style lang="scss" scoped>
253
+ .user-works-page {
254
+ max-width: 900px;
255
+ margin: 20px auto;
256
+ padding: 16px;
257
+ h1 {
258
+ font-size: 32px;
259
+ font-weight: bold;
260
+ }
261
+ .description {
262
+ margin-top: 10px;
263
+ color: #999;
264
+ font-size: 16px;
265
+ }
266
+ a {
267
+ font-weight: 500;
268
+ color: var(--vp-c-brand);
269
+ }
270
+ }
271
+ .work {
272
+ h2 {
273
+ padding-top: 24px;
274
+ line-height: 32px;
275
+ font-size: 24px;
276
+ }
277
+ h3 {
278
+ margin: 32px 0 0;
279
+ line-height: 28px;
280
+ font-size: 20px;
281
+ }
282
+ .info {
283
+ display: flex;
284
+ font-size: 14px;
285
+ margin-top: 10px;
286
+ flex-wrap: wrap;
287
+ }
288
+ .links,
289
+ .times {
290
+ display: flex;
291
+ align-items: center;
292
+ .icon {
293
+ color: var(--vp-c-text-1);
294
+ display: block;
295
+ width: 20px;
296
+ height: 20px;
297
+ margin-right: 6px;
298
+ }
299
+ }
300
+ .times {
301
+ margin-right: 18px;
302
+ }
303
+ .links {
304
+ a {
305
+ display: flex;
306
+ align-items: center;
307
+ }
308
+ a.github-link {
309
+ margin-right: 10px;
310
+ }
311
+ a.link {
312
+ margin-right: 0;
313
+ &::after {
314
+ content: ',';
315
+ color: var(--vp-c-text-1);
316
+ margin-right: 6px;
317
+ margin-left: 2px;
318
+ }
319
+ &:last-child::after {
320
+ content: '';
321
+ }
322
+ }
323
+ }
324
+ }
325
+ .lastupdate {
326
+ color: var(--vp-c-text-1);
327
+ }
328
+ .list-mode {
329
+ height: 360px;
330
+ margin: 10px auto;
331
+ overflow-y: auto;
332
+ }
333
+ .split {
334
+ display: inline-block;
335
+ width: 1px;
336
+ height: 8px;
337
+ margin: 0 10px;
338
+ background-color: #4e5969;
339
+ }
340
+ </style>
@@ -19,6 +19,8 @@ const activeTagSymbol: InjectionKey<Ref<Theme.activeTag>> = Symbol('active-tag')
19
19
  const currentPageNum: InjectionKey<Ref<number>> = Symbol('home-page-num')
20
20
  const homeConfigSymbol: InjectionKey<Theme.HomeConfig> = Symbol('home-config')
21
21
 
22
+ const userWorks: InjectionKey<Ref<Theme.UserWorks>> = Symbol('user-works')
23
+
22
24
  export function withConfigProvider(App: Component) {
23
25
  return defineComponent({
24
26
  name: 'ConfigProvider',
@@ -34,6 +36,16 @@ export function withConfigProvider(App: Component) {
34
36
  const { theme } = useData()
35
37
  const config = computed(() => resolveConfig(theme.value))
36
38
  provide(configSymbol, config)
39
+ provide(
40
+ userWorks,
41
+ ref(
42
+ config.value.blog?.works || {
43
+ title: '',
44
+ description: '',
45
+ list: []
46
+ }
47
+ )
48
+ )
37
49
 
38
50
  const activeTag = ref<Theme.activeTag>({
39
51
  label: '',
@@ -43,7 +55,6 @@ export function withConfigProvider(App: Component) {
43
55
 
44
56
  const pageNum = ref(1)
45
57
  provide(currentPageNum, pageNum)
46
-
47
58
  return () => h(App, null, slots)
48
59
  }
49
60
  })
@@ -105,6 +116,9 @@ export function useCurrentArticle() {
105
116
  return currentArticle
106
117
  }
107
118
 
119
+ export function useUserWorks() {
120
+ return inject(userWorks)!
121
+ }
108
122
  function resolveConfig(config: Theme.Config): Theme.Config {
109
123
  return {
110
124
  ...config,
@@ -1,5 +1,5 @@
1
1
  import type { ElButton } from 'element-plus'
2
- import { DefaultTheme } from 'vitepress'
2
+ import type { DefaultTheme } from 'vitepress'
3
3
 
4
4
  export namespace BlogPopover {
5
5
  export interface Title {
@@ -31,6 +31,11 @@ export namespace BlogPopover {
31
31
  export type Value = Title | Text | Image | Button
32
32
  }
33
33
 
34
+ export type ThemeableImage =
35
+ | string
36
+ | { src: string; alt?: string }
37
+ | { light: string; dark: string; alt?: string }
38
+
34
39
  export namespace Theme {
35
40
  export interface PageMeta {
36
41
  title: string
@@ -151,7 +156,48 @@ export namespace Theme {
151
156
  nickname: string
152
157
  des: string
153
158
  url: string
154
- avatar: string
159
+ avatar: ThemeableImage
160
+ }
161
+
162
+ export interface UserWork {
163
+ title: string
164
+ description: string
165
+ time:
166
+ | string
167
+ | {
168
+ start: string
169
+ end?: string
170
+ lastupdate?: string
171
+ }
172
+ status?:
173
+ | 'active'
174
+ | 'negative'
175
+ | 'off'
176
+ | {
177
+ text: string
178
+ }
179
+ url?: string
180
+ github?:
181
+ | string
182
+ | {
183
+ owner: string
184
+ repo: string
185
+ branch?: string
186
+ path?: string
187
+ }
188
+ cover?:
189
+ | string
190
+ | string[]
191
+ | {
192
+ urls: string[]
193
+ layout?: 'swiper' | 'list' | 'card'
194
+ }
195
+ links?: {
196
+ title: string
197
+ url: string
198
+ }[]
199
+ tags?: string[]
200
+ top?: number
155
201
  }
156
202
  export type SearchConfig =
157
203
  | boolean
@@ -168,6 +214,11 @@ export namespace Theme {
168
214
  mode?: boolean | 'pagefind'
169
215
  }
170
216
 
217
+ export interface UserWorks {
218
+ title: string
219
+ description?: string
220
+ list: UserWork[]
221
+ }
171
222
  export interface BlogConfig {
172
223
  blog?: false
173
224
  pagesData: PageData[]
@@ -200,7 +251,8 @@ export namespace Theme {
200
251
  * 启用 [vitepress-plugin-tabs](https://www.npmjs.com/package/vitepress-plugin-tabs)
201
252
  * @default false
202
253
  */
203
- tabs: boolean
254
+ tabs?: boolean
255
+ works?: UserWorks
204
256
  }
205
257
 
206
258
  export interface Config extends DefaultTheme.Config {
package/src/index.ts CHANGED
@@ -12,13 +12,15 @@ import { withConfigProvider } from './composables/config/blog'
12
12
 
13
13
  // page
14
14
  import TimelinePage from './components/TimelinePage.vue'
15
+ import UserWorksPage from './components/UserWorks.vue'
15
16
 
16
17
  export const BlogTheme: Theme = {
17
18
  ...DefaultTheme,
18
19
  Layout: withConfigProvider(BlogApp),
19
20
  enhanceApp(ctx) {
20
- // TODO: 优化到自定义组件中注册
21
+ // @ts-ignore
21
22
  ctx.app.component('TimelinePage', TimelinePage)
23
+ ctx.app.component('UserWorksPage', UserWorksPage)
22
24
  }
23
25
  }
24
26
 
@@ -1,3 +1,5 @@
1
+ import type { ThemeableImage } from '../composables/config'
2
+
1
3
  export function formatDate(d: any, fmt = 'yyyy-MM-dd hh:mm:ss') {
2
4
  if (!(d instanceof Date)) {
3
5
  d = new Date(d)
@@ -89,3 +91,75 @@ export function chineseSearchOptimize(input: string) {
89
91
  .replace(/\s+/g, ' ')
90
92
  .trim()
91
93
  }
94
+
95
+ /**
96
+ * 根据Github地址跨域获取最后更新时间
97
+ * @param url
98
+ * @returns
99
+ */
100
+ export function getGithubUpdateTime(url: string) {
101
+ // 提取Github url中的用户名和仓库名
102
+ const match = url.match(/github.com\/(.+)/)
103
+ if (!match?.[1]) {
104
+ return Promise.reject(new Error('Github地址格式错误'))
105
+ }
106
+ const [owner, repo] = match[1].split('/')
107
+ return fetch(`https://api.github.com/repos/${owner}/${repo}`)
108
+ .then((res) => res.json())
109
+ .then((res) => {
110
+ return res.updated_at
111
+ })
112
+ }
113
+
114
+ /**
115
+ * 跨域获取某个Github仓库的指定目录最后更新时间
116
+ */
117
+ export function getGithubDirUpdateTime(
118
+ owner: string,
119
+ repo: string,
120
+ dir?: string,
121
+ branch?: string
122
+ ) {
123
+ let baseUrl = `https://api.github.com/repos/${owner}/${repo}/commits`
124
+ if (branch) {
125
+ baseUrl += `/${branch}`
126
+ }
127
+ if (dir) {
128
+ baseUrl += `?path=${dir}`
129
+ }
130
+ return fetch(baseUrl)
131
+ .then((res) => res.json())
132
+ .then((res) => {
133
+ return [res].flat()[0].commit.committer.date
134
+ })
135
+ }
136
+
137
+ // 解析页面获取最后更新时间(跨域)
138
+ // export async function getGithubUpdateTime(url: string) {
139
+ // const res = await fetch(url)
140
+ // const html = await res.text()
141
+ // const match = html.match(/<relative-time datetime="(.+?)"/)
142
+ // if (match) {
143
+ // return match[1]
144
+ // }
145
+ // return ''
146
+ // }
147
+
148
+ export function getImageUrl(
149
+ image: ThemeableImage,
150
+ isDarkMode: boolean
151
+ ): string {
152
+ if (typeof image === 'string') {
153
+ // 如果 ThemeableImage 类型为 string,则直接返回字符串
154
+ return image
155
+ }
156
+ if ('src' in image) {
157
+ // 如果 ThemeableImage 类型是一个对象,并且对象有 src 属性,则返回 src 属性对应的字符串
158
+ return image.src
159
+ }
160
+ if ('light' in image && 'dark' in image) {
161
+ // 如果 ThemeableImage 类型是一个对象,并且对象同时有 light 和 dark 属性,则根据 isDarkMode 返回对应的 URL
162
+ return isDarkMode ? image.dark : image.light
163
+ } // 如果 ThemeableImage 类型不是上述情况,则返回空字符串
164
+ return ''
165
+ }