@sugarat/theme 0.1.29 → 0.1.31
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 +53 -3
- package/node.js +2 -2
- package/package.json +5 -4
- package/src/components/BlogFriendLink.vue +26 -3
- package/src/components/BlogRecommendArticle.vue +12 -8
- package/src/components/TimelinePage.vue +0 -4
- package/src/components/UserWorks.vue +545 -0
- package/src/composables/config/blog.ts +79 -3
- package/src/composables/config/index.ts +59 -5
- package/src/index.ts +4 -1
- package/src/node.ts +1 -1
- package/src/utils/index.ts +74 -0
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;
|
|
@@ -51,10 +59,12 @@ declare namespace Theme {
|
|
|
51
59
|
*/
|
|
52
60
|
recommend?: number | false;
|
|
53
61
|
/**
|
|
62
|
+
* TODO: 待开发
|
|
54
63
|
* 时间线
|
|
55
64
|
*/
|
|
56
65
|
timeline: string;
|
|
57
66
|
/**
|
|
67
|
+
* TODO: 待开发
|
|
58
68
|
* 专栏&合集
|
|
59
69
|
*/
|
|
60
70
|
album: string;
|
|
@@ -140,7 +150,37 @@ declare namespace Theme {
|
|
|
140
150
|
nickname: string;
|
|
141
151
|
des: string;
|
|
142
152
|
url: string;
|
|
143
|
-
avatar:
|
|
153
|
+
avatar: ThemeableImage;
|
|
154
|
+
}
|
|
155
|
+
interface UserWork {
|
|
156
|
+
title: string;
|
|
157
|
+
description: string;
|
|
158
|
+
time: string | {
|
|
159
|
+
start: string;
|
|
160
|
+
end?: string;
|
|
161
|
+
lastupdate?: string;
|
|
162
|
+
};
|
|
163
|
+
status?: {
|
|
164
|
+
text: string;
|
|
165
|
+
type?: 'tip' | 'warning' | 'danger';
|
|
166
|
+
};
|
|
167
|
+
url?: string;
|
|
168
|
+
github?: string | {
|
|
169
|
+
owner: string;
|
|
170
|
+
repo: string;
|
|
171
|
+
branch?: string;
|
|
172
|
+
path?: string;
|
|
173
|
+
};
|
|
174
|
+
cover?: string | string[] | {
|
|
175
|
+
urls: string[];
|
|
176
|
+
layout?: 'swiper' | 'list';
|
|
177
|
+
};
|
|
178
|
+
links?: {
|
|
179
|
+
title: string;
|
|
180
|
+
url: string;
|
|
181
|
+
}[];
|
|
182
|
+
tags?: string[];
|
|
183
|
+
top?: number;
|
|
144
184
|
}
|
|
145
185
|
type SearchConfig = boolean | 'pagefind' | {
|
|
146
186
|
btnPlaceholder?: string;
|
|
@@ -153,6 +193,12 @@ declare namespace Theme {
|
|
|
153
193
|
heading?: string;
|
|
154
194
|
mode?: boolean | 'pagefind';
|
|
155
195
|
};
|
|
196
|
+
interface UserWorks {
|
|
197
|
+
title: string;
|
|
198
|
+
description?: string;
|
|
199
|
+
topTitle?: string;
|
|
200
|
+
list: UserWork[];
|
|
201
|
+
}
|
|
156
202
|
interface BlogConfig {
|
|
157
203
|
blog?: false;
|
|
158
204
|
pagesData: PageData[];
|
|
@@ -172,7 +218,10 @@ declare namespace Theme {
|
|
|
172
218
|
* power by https://giscus.app/zh-CN
|
|
173
219
|
*/
|
|
174
220
|
comment?: GiscusConfig | false;
|
|
175
|
-
|
|
221
|
+
/**
|
|
222
|
+
* 阅读文章左侧的推荐文章(替代默认的sidebar)
|
|
223
|
+
*/
|
|
224
|
+
recommend?: RecommendArticle | false;
|
|
176
225
|
article?: ArticleConfig;
|
|
177
226
|
/**
|
|
178
227
|
* el-alert
|
|
@@ -185,7 +234,8 @@ declare namespace Theme {
|
|
|
185
234
|
* 启用 [vitepress-plugin-tabs](https://www.npmjs.com/package/vitepress-plugin-tabs)
|
|
186
235
|
* @default false
|
|
187
236
|
*/
|
|
188
|
-
tabs
|
|
237
|
+
tabs?: boolean;
|
|
238
|
+
works?: UserWorks;
|
|
189
239
|
}
|
|
190
240
|
interface Config extends DefaultTheme.Config {
|
|
191
241
|
blog?: BlogConfig;
|
package/node.js
CHANGED
|
@@ -41,7 +41,7 @@ var import_fs = __toESM(require("fs"));
|
|
|
41
41
|
var import_child_process = require("child_process");
|
|
42
42
|
var import_path = __toESM(require("path"));
|
|
43
43
|
|
|
44
|
-
// ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@1.0.0-
|
|
44
|
+
// ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@1.0.0-beta.2_vue@3.2.45/node_modules/vitepress-plugin-tabs/dist/index.js
|
|
45
45
|
var tabsMarker = "=tabs";
|
|
46
46
|
var tabsMarkerLen = tabsMarker.length;
|
|
47
47
|
var ruleBlockTabs = (state, startLine, endLine, silent) => {
|
|
@@ -352,7 +352,7 @@ function getThemeConfig(cfg) {
|
|
|
352
352
|
pagesData: data,
|
|
353
353
|
...cfg
|
|
354
354
|
},
|
|
355
|
-
...cfg?.blog !== false ? {
|
|
355
|
+
...cfg?.blog !== false && cfg?.recommend !== false ? {
|
|
356
356
|
sidebar: [
|
|
357
357
|
{
|
|
358
358
|
text: "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sugarat/theme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"url": "https://github.com/ATQQ/sugar-blog/issues"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@mdit-vue/shared": "^0.12.0",
|
|
36
37
|
"@vue/shared": "^3.2.45",
|
|
37
38
|
"@vueuse/core": "^9.6.0",
|
|
38
39
|
"fast-glob": "^3.2.12",
|
|
@@ -46,9 +47,9 @@
|
|
|
46
47
|
"sass": "^1.56.1",
|
|
47
48
|
"tsup": " ^6.5.0",
|
|
48
49
|
"typescript": "^4.8.2",
|
|
49
|
-
"vitepress": "1.0.0-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
50
|
+
"vitepress": "1.0.0-beta.2",
|
|
51
|
+
"vitepress-plugin-tabs": "^0.2.0",
|
|
52
|
+
"vue": "^3.2.45"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
55
|
"dev": "npm run build:node && npm run dev:docs",
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="card friend-wrapper" v-if="
|
|
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
|
|
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,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
class="card recommend"
|
|
4
|
-
v-if="recommendList.length || emptyText"
|
|
4
|
+
v-if="_recommend !== false && (recommendList.length || emptyText)"
|
|
5
5
|
data-pagefind-ignore="all"
|
|
6
6
|
>
|
|
7
7
|
<!-- 头部 -->
|
|
@@ -52,11 +52,15 @@ import { useRoute, withBase } from 'vitepress'
|
|
|
52
52
|
import { formatShowDate } from '../utils/index'
|
|
53
53
|
import { useArticles, useBlogConfig } from '../composables/config/blog'
|
|
54
54
|
|
|
55
|
-
const { recommend } = useBlogConfig()
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
const { recommend: _recommend } = useBlogConfig()
|
|
56
|
+
|
|
57
|
+
const recommend = computed(() =>
|
|
58
|
+
_recommend === false ? undefined : _recommend
|
|
59
|
+
)
|
|
60
|
+
const title = computed(() => recommend.value?.title || '🔍 相关文章')
|
|
61
|
+
const pageSize = computed(() => recommend.value?.pageSize || 9)
|
|
62
|
+
const nextText = computed(() => recommend.value?.nextText || '换一组')
|
|
63
|
+
const emptyText = computed(() => recommend.value?.empty ?? '暂无推荐文章')
|
|
60
64
|
|
|
61
65
|
const docs = useArticles()
|
|
62
66
|
|
|
@@ -80,12 +84,12 @@ const recommendList = computed(() => {
|
|
|
80
84
|
// 过滤掉自己
|
|
81
85
|
.filter(
|
|
82
86
|
(v) =>
|
|
83
|
-
(recommend?.showSelf ?? true) ||
|
|
87
|
+
(recommend.value?.showSelf ?? true) ||
|
|
84
88
|
v.route !== decodeURIComponent(route.path).replace(/.html$/, '')
|
|
85
89
|
)
|
|
86
90
|
// 过滤掉不需要展示的
|
|
87
91
|
.filter((v) => v.meta.recommend !== false)
|
|
88
|
-
.filter((v) => recommend?.filter?.(v) ?? true)
|
|
92
|
+
.filter((v) => recommend.value?.filter?.(v) ?? true)
|
|
89
93
|
|
|
90
94
|
const topList = origin.filter((v) => v.meta?.recommend)
|
|
91
95
|
topList.sort((a, b) => Number(a.meta.recommend) - Number(b.meta.recommend))
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="user-works-page VPDoc">
|
|
3
|
+
<div class="aside-container">
|
|
4
|
+
<!-- TODO:过滤,可吸顶 -->
|
|
5
|
+
<div class="filter">
|
|
6
|
+
<!-- 时间: -->
|
|
7
|
+
<div></div>
|
|
8
|
+
<!-- TODO: tags -->
|
|
9
|
+
<div></div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<!-- 作品列表 -->
|
|
13
|
+
<div class="works">
|
|
14
|
+
<h1>{{ works.title }}</h1>
|
|
15
|
+
<p v-if="works.description" class="description">
|
|
16
|
+
{{ works.description }}
|
|
17
|
+
</p>
|
|
18
|
+
<!-- 标题,描述信息,时间,线上链接,代码仓库,示例图片(几张,多种展示样式支持) -->
|
|
19
|
+
<div class="work" v-for="(work, idx) in workList" :key="idx">
|
|
20
|
+
<!-- 大日期标题 -->
|
|
21
|
+
<h2 :id="`work_${work.year}`" v-if="work.year">
|
|
22
|
+
<a :href="`#work_${work.year}`">{{ work.year }}</a>
|
|
23
|
+
</h2>
|
|
24
|
+
<!-- 作品标题 -->
|
|
25
|
+
<h3 class="title" :id="slugify(work.title)">
|
|
26
|
+
<a class="pin" :href="'#' + slugify(work.title)"></a>
|
|
27
|
+
<a v-if="work.url" rel="noopener" target="_blank" :href="work.url">{{
|
|
28
|
+
work.title
|
|
29
|
+
}}</a>
|
|
30
|
+
<span v-else>{{ work.title }}</span>
|
|
31
|
+
<Badge v-if="work.status" :type="work.status?.type || 'tip'">{{
|
|
32
|
+
work.status.text
|
|
33
|
+
}}</Badge>
|
|
34
|
+
</h3>
|
|
35
|
+
<!-- 补充信息 -->
|
|
36
|
+
<div class="info">
|
|
37
|
+
<!-- times -->
|
|
38
|
+
<div class="times">
|
|
39
|
+
<span class="icon">
|
|
40
|
+
<svg
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
43
|
+
viewBox="0 0 24 24"
|
|
44
|
+
>
|
|
45
|
+
<title>上线时间</title>
|
|
46
|
+
<path
|
|
47
|
+
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"
|
|
48
|
+
fill="currentColor"
|
|
49
|
+
></path>
|
|
50
|
+
</svg>
|
|
51
|
+
</span>
|
|
52
|
+
<span>{{ work.startTime }}</span>
|
|
53
|
+
<span v-if="work.endTime"> - {{ work.endTime }}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<!-- GitHub links-->
|
|
56
|
+
<div class="links" v-if="work.github">
|
|
57
|
+
<a
|
|
58
|
+
class="github-link"
|
|
59
|
+
v-if="work.github"
|
|
60
|
+
:href="(work.github as string)"
|
|
61
|
+
target="_blank"
|
|
62
|
+
rel="noopener"
|
|
63
|
+
>
|
|
64
|
+
<i class="icon">
|
|
65
|
+
<svg
|
|
66
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
67
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
68
|
+
viewBox="0 0 496 512"
|
|
69
|
+
>
|
|
70
|
+
<path
|
|
71
|
+
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"
|
|
72
|
+
fill="currentColor"
|
|
73
|
+
></path>
|
|
74
|
+
</svg>
|
|
75
|
+
</i>
|
|
76
|
+
<span class="lastupdate" v-if="work.lastUpdate"
|
|
77
|
+
>最后更新时间:{{ work.lastUpdate }}</span
|
|
78
|
+
>
|
|
79
|
+
</a>
|
|
80
|
+
</div>
|
|
81
|
+
<!-- 其它自定义链接 -->
|
|
82
|
+
<div class="links" v-if="work.links?.length">
|
|
83
|
+
<i class="icon" v-if="work.links?.length">
|
|
84
|
+
<svg
|
|
85
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
86
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
87
|
+
viewBox="0 0 24 24"
|
|
88
|
+
>
|
|
89
|
+
<g
|
|
90
|
+
fill="none"
|
|
91
|
+
stroke="currentColor"
|
|
92
|
+
stroke-width="2"
|
|
93
|
+
stroke-linecap="round"
|
|
94
|
+
stroke-linejoin="round"
|
|
95
|
+
>
|
|
96
|
+
<path
|
|
97
|
+
d="M10 14a3.5 3.5 0 0 0 5 0l4-4a3.5 3.5 0 0 0-5-5l-.5.5"
|
|
98
|
+
></path>
|
|
99
|
+
<path
|
|
100
|
+
d="M14 10a3.5 3.5 0 0 0-5 0l-4 4a3.5 3.5 0 0 0 5 5l.5-.5"
|
|
101
|
+
></path>
|
|
102
|
+
</g>
|
|
103
|
+
</svg>
|
|
104
|
+
</i>
|
|
105
|
+
<a
|
|
106
|
+
class="link"
|
|
107
|
+
v-for="link in work.links || []"
|
|
108
|
+
:href="link.url"
|
|
109
|
+
:key="link.url"
|
|
110
|
+
:title="link.title"
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener"
|
|
113
|
+
>
|
|
114
|
+
{{ link.title }}
|
|
115
|
+
</a>
|
|
116
|
+
</div>
|
|
117
|
+
<!-- tags -->
|
|
118
|
+
<div class="tags" v-if="work.tags?.length">
|
|
119
|
+
<i class="icon">
|
|
120
|
+
<svg
|
|
121
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
122
|
+
viewBox="0 0 1024 1024"
|
|
123
|
+
data-v-d328c40a=""
|
|
124
|
+
>
|
|
125
|
+
<path
|
|
126
|
+
fill="currentColor"
|
|
127
|
+
d="M256 128v698.88l196.032-156.864a96 96 0 0 1 119.936 0L768 826.816V128H256zm-32-64h576a32 32 0 0 1 32 32v797.44a32 32 0 0 1-51.968 24.96L531.968 720a32 32 0 0 0-39.936 0L243.968 918.4A32 32 0 0 1 192 893.44V96a32 32 0 0 1 32-32z"
|
|
128
|
+
></path>
|
|
129
|
+
</svg>
|
|
130
|
+
</i>
|
|
131
|
+
<span
|
|
132
|
+
@click="handleChooseTag(tag)"
|
|
133
|
+
class="tag"
|
|
134
|
+
v-for="tag in work.tags"
|
|
135
|
+
:key="tag"
|
|
136
|
+
>{{ tag }}
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<!-- 封面图 -->
|
|
141
|
+
<div class="images" v-if="work.covers?.length">
|
|
142
|
+
<!-- swiper -->
|
|
143
|
+
<div v-if="work.coverLayout === 'swiper'" class="swiper-mode">
|
|
144
|
+
<el-carousel
|
|
145
|
+
autoplay
|
|
146
|
+
height="260px"
|
|
147
|
+
:type="isCardMode && work.covers.length >= 3 ? 'card' : ''"
|
|
148
|
+
>
|
|
149
|
+
<el-carousel-item
|
|
150
|
+
style="text-align: center"
|
|
151
|
+
v-for="(url, idx) in work.covers"
|
|
152
|
+
:key="url"
|
|
153
|
+
>
|
|
154
|
+
<el-image
|
|
155
|
+
preview-teleported
|
|
156
|
+
:key="url"
|
|
157
|
+
:src="url"
|
|
158
|
+
loading="lazy"
|
|
159
|
+
:preview-src-list="work.covers"
|
|
160
|
+
:initial-index="idx"
|
|
161
|
+
hide-on-click-modal
|
|
162
|
+
:alt="work.title + '-' + idx"
|
|
163
|
+
/>
|
|
164
|
+
</el-carousel-item>
|
|
165
|
+
</el-carousel>
|
|
166
|
+
</div>
|
|
167
|
+
<!-- list -->
|
|
168
|
+
<div v-if="work.coverLayout === 'list'" class="list-mode">
|
|
169
|
+
<el-image
|
|
170
|
+
v-for="(url, idx) in work.covers"
|
|
171
|
+
:key="url"
|
|
172
|
+
:src="url"
|
|
173
|
+
loading="lazy"
|
|
174
|
+
:preview-src-list="work.covers"
|
|
175
|
+
:initial-index="idx"
|
|
176
|
+
hide-on-click-modal
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="description" v-html="work.description"></div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="aside-container">
|
|
184
|
+
<div class="aside-outline-container">
|
|
185
|
+
<VPDocAsideOutline />
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<script lang="ts" setup>
|
|
192
|
+
import { ElImage, ElCarousel, ElCarouselItem, ElMessage } from 'element-plus'
|
|
193
|
+
import VPDocAsideOutline from 'vitepress/dist/client/theme-default/components/VPDocAsideOutline.vue'
|
|
194
|
+
import { computed, reactive, ref, watch, watchEffect } from 'vue'
|
|
195
|
+
import { slugify } from '@mdit-vue/shared'
|
|
196
|
+
import { useWindowSize } from '@vueuse/core'
|
|
197
|
+
import {
|
|
198
|
+
getGithubUpdateTime,
|
|
199
|
+
formatDate,
|
|
200
|
+
getGithubDirUpdateTime
|
|
201
|
+
} from '../utils'
|
|
202
|
+
import {
|
|
203
|
+
useUserWorks,
|
|
204
|
+
useActiveAnchor,
|
|
205
|
+
useAutoUpdateAnchor
|
|
206
|
+
} from '../composables/config/blog'
|
|
207
|
+
import { Theme } from '../composables/config'
|
|
208
|
+
|
|
209
|
+
const currentAnchor = useAutoUpdateAnchor()
|
|
210
|
+
// 更新锚点的时候更新 url 中的 hash
|
|
211
|
+
watch(
|
|
212
|
+
() => currentAnchor.id,
|
|
213
|
+
(val) => {
|
|
214
|
+
if (val) {
|
|
215
|
+
window.history.replaceState(null, '', `#${val}`)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
const mountActiveAnchorEl = useActiveAnchor()
|
|
220
|
+
watch(mountActiveAnchorEl, () => {
|
|
221
|
+
const { value } = mountActiveAnchorEl
|
|
222
|
+
if (value) {
|
|
223
|
+
value.scroll({
|
|
224
|
+
behavior: 'smooth'
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const works = useUserWorks()
|
|
230
|
+
const workList = reactive<
|
|
231
|
+
(Theme.UserWork & {
|
|
232
|
+
year?: string | undefined
|
|
233
|
+
startTime: string
|
|
234
|
+
lastUpdate?: string
|
|
235
|
+
endTime?: string
|
|
236
|
+
covers?: string[]
|
|
237
|
+
coverLayout?: string
|
|
238
|
+
})[]
|
|
239
|
+
>([])
|
|
240
|
+
|
|
241
|
+
// 格式化数据
|
|
242
|
+
watch(
|
|
243
|
+
works,
|
|
244
|
+
(val) => {
|
|
245
|
+
const sortDate = [...val.list].map((v) => {
|
|
246
|
+
const { time } = v
|
|
247
|
+
|
|
248
|
+
// 格式化时间
|
|
249
|
+
const metaInfo =
|
|
250
|
+
typeof time === 'string'
|
|
251
|
+
? {
|
|
252
|
+
startTime: time,
|
|
253
|
+
endTime: '',
|
|
254
|
+
lastUpdate: ''
|
|
255
|
+
}
|
|
256
|
+
: {
|
|
257
|
+
startTime: time.start,
|
|
258
|
+
endTime: time.end,
|
|
259
|
+
lastUpdate: time.lastupdate
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 格式化封面信息
|
|
263
|
+
const covers: string[] = []
|
|
264
|
+
let coverLayout = 'swiper'
|
|
265
|
+
|
|
266
|
+
if (typeof v.cover === 'string') {
|
|
267
|
+
covers.push(v.cover)
|
|
268
|
+
} else if (Array.isArray(v.cover)) {
|
|
269
|
+
covers.push(...v.cover)
|
|
270
|
+
} else if (typeof v.cover === 'object') {
|
|
271
|
+
covers.push(...v.cover.urls)
|
|
272
|
+
coverLayout = v.cover.layout ?? coverLayout
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
...v,
|
|
276
|
+
...metaInfo,
|
|
277
|
+
covers,
|
|
278
|
+
coverLayout
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
// 过滤出置顶数据
|
|
282
|
+
const topDate = sortDate.filter((v) => v.top !== undefined)
|
|
283
|
+
const normalDate = sortDate.filter((v) => v.top === undefined)
|
|
284
|
+
// 数据排序
|
|
285
|
+
topDate.sort((a, b) => a.top! - b.top!)
|
|
286
|
+
normalDate.sort((a, b) => +new Date(b.startTime) - +new Date(a.startTime))
|
|
287
|
+
if (topDate.length) {
|
|
288
|
+
// @ts-ignore
|
|
289
|
+
topDate[0].year = works.value.topTitle ?? '置顶'
|
|
290
|
+
}
|
|
291
|
+
// 数据分组
|
|
292
|
+
const groupDate = normalDate.reduce((prev, cur) => {
|
|
293
|
+
const { startTime } = cur
|
|
294
|
+
const year = new Date(startTime).getFullYear()
|
|
295
|
+
const data = { ...cur }
|
|
296
|
+
if (!prev[year]) {
|
|
297
|
+
prev[year] = []
|
|
298
|
+
// 第一项数据加上year属性
|
|
299
|
+
// @ts-ignore
|
|
300
|
+
data.year = year
|
|
301
|
+
}
|
|
302
|
+
prev[year].push(data)
|
|
303
|
+
return prev
|
|
304
|
+
}, {} as Record<string, (Theme.UserWork & { year?: string; startTime: string })[]>)
|
|
305
|
+
workList.push(...topDate, ...Object.values(groupDate).reverse().flat())
|
|
306
|
+
},
|
|
307
|
+
{ immediate: true }
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
const init = ref(true)
|
|
311
|
+
// 更新时间信息
|
|
312
|
+
watchEffect(() => {
|
|
313
|
+
if (workList.length && init.value) {
|
|
314
|
+
init.value = false
|
|
315
|
+
workList.forEach((data) => {
|
|
316
|
+
// 接口获取最后更新时间
|
|
317
|
+
if (!data.lastUpdate && data.github) {
|
|
318
|
+
data.lastUpdate = '获取中...'
|
|
319
|
+
const { github } = data
|
|
320
|
+
if (typeof github === 'string') {
|
|
321
|
+
getGithubUpdateTime(github)
|
|
322
|
+
.then((time) => {
|
|
323
|
+
data.lastUpdate = formatDate(time, 'yyyy-MM-dd')
|
|
324
|
+
})
|
|
325
|
+
.catch(() => {
|
|
326
|
+
data.lastUpdate = '地址解析失败'
|
|
327
|
+
})
|
|
328
|
+
} else {
|
|
329
|
+
const { owner, repo, path, branch } = github
|
|
330
|
+
// 拼接Github链接
|
|
331
|
+
let githubUrl = `https://github.com/${owner}/${repo}`
|
|
332
|
+
if (path) {
|
|
333
|
+
githubUrl += `/tree/${branch || 'master'}/${path}`
|
|
334
|
+
} else if (branch) {
|
|
335
|
+
githubUrl += `/tree/${branch}`
|
|
336
|
+
}
|
|
337
|
+
data.github = githubUrl
|
|
338
|
+
getGithubDirUpdateTime(owner, repo, path ?? '', branch)
|
|
339
|
+
.then((time) => {
|
|
340
|
+
data.lastUpdate = formatDate(time, 'yyyy-MM-dd')
|
|
341
|
+
})
|
|
342
|
+
.catch(() => {
|
|
343
|
+
data.lastUpdate = '地址解析失败'
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
const { width } = useWindowSize()
|
|
352
|
+
const isCardMode = computed(() => width.value > 768)
|
|
353
|
+
const handleChooseTag = (tag: string) => {
|
|
354
|
+
ElMessage({
|
|
355
|
+
message: `点击了${tag}标签,标签过滤功能开发中ing...`,
|
|
356
|
+
type: 'warning'
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
</script>
|
|
360
|
+
|
|
361
|
+
<style lang="scss" scoped>
|
|
362
|
+
.user-works-page {
|
|
363
|
+
display: flex;
|
|
364
|
+
justify-content: center;
|
|
365
|
+
width: 100%;
|
|
366
|
+
margin: 20px auto;
|
|
367
|
+
padding: 16px;
|
|
368
|
+
h1 {
|
|
369
|
+
font-size: 32px;
|
|
370
|
+
font-weight: bold;
|
|
371
|
+
}
|
|
372
|
+
.description {
|
|
373
|
+
margin-top: 16px;
|
|
374
|
+
color: #999;
|
|
375
|
+
font-size: 16px;
|
|
376
|
+
}
|
|
377
|
+
a {
|
|
378
|
+
font-weight: 500;
|
|
379
|
+
color: var(--vp-c-brand);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
.works-container {
|
|
383
|
+
display: flex;
|
|
384
|
+
justify-content: center;
|
|
385
|
+
}
|
|
386
|
+
.work {
|
|
387
|
+
max-width: 900px;
|
|
388
|
+
|
|
389
|
+
h2 {
|
|
390
|
+
margin-top: 6px;
|
|
391
|
+
padding-top: 18px;
|
|
392
|
+
line-height: 32px;
|
|
393
|
+
font-size: 24px;
|
|
394
|
+
border-top: 1px solid var(--vp-c-divider);
|
|
395
|
+
a {
|
|
396
|
+
color: inherit;
|
|
397
|
+
}
|
|
398
|
+
&:hover {
|
|
399
|
+
a {
|
|
400
|
+
&::before {
|
|
401
|
+
opacity: 1;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
a {
|
|
406
|
+
position: relative;
|
|
407
|
+
&::before {
|
|
408
|
+
position: absolute;
|
|
409
|
+
left: -16px;
|
|
410
|
+
opacity: 0;
|
|
411
|
+
content: var(--vp-header-anchor-symbol);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
h3 {
|
|
416
|
+
margin: 32px 0 0;
|
|
417
|
+
line-height: 28px;
|
|
418
|
+
font-size: 20px;
|
|
419
|
+
position: relative;
|
|
420
|
+
&.title > a.pin {
|
|
421
|
+
position: absolute;
|
|
422
|
+
left: -16px;
|
|
423
|
+
&::before {
|
|
424
|
+
left: -16px;
|
|
425
|
+
opacity: 0;
|
|
426
|
+
content: var(--vp-header-anchor-symbol);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
&:hover > a.pin {
|
|
430
|
+
&::before {
|
|
431
|
+
opacity: 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
.info {
|
|
436
|
+
display: flex;
|
|
437
|
+
font-size: 14px;
|
|
438
|
+
margin-top: 10px;
|
|
439
|
+
flex-wrap: wrap;
|
|
440
|
+
}
|
|
441
|
+
.links,
|
|
442
|
+
.times,
|
|
443
|
+
.tags {
|
|
444
|
+
display: flex;
|
|
445
|
+
align-items: center;
|
|
446
|
+
.icon {
|
|
447
|
+
color: var(--vp-c-text-1);
|
|
448
|
+
display: block;
|
|
449
|
+
width: 20px;
|
|
450
|
+
height: 20px;
|
|
451
|
+
margin-right: 6px;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
.times {
|
|
455
|
+
margin-right: 18px;
|
|
456
|
+
}
|
|
457
|
+
.links {
|
|
458
|
+
a {
|
|
459
|
+
display: flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
}
|
|
462
|
+
a.github-link {
|
|
463
|
+
margin-right: 10px;
|
|
464
|
+
}
|
|
465
|
+
a.link {
|
|
466
|
+
margin-right: 0;
|
|
467
|
+
&::after {
|
|
468
|
+
content: ',';
|
|
469
|
+
color: var(--vp-c-text-1);
|
|
470
|
+
margin-right: 6px;
|
|
471
|
+
margin-left: 2px;
|
|
472
|
+
}
|
|
473
|
+
&:last-child::after {
|
|
474
|
+
content: '';
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
.tags {
|
|
479
|
+
span.tag {
|
|
480
|
+
cursor: pointer;
|
|
481
|
+
}
|
|
482
|
+
span.tag:not(:last-child) {
|
|
483
|
+
&::after {
|
|
484
|
+
content: '·';
|
|
485
|
+
display: inline-block;
|
|
486
|
+
padding: 0 4px;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
.aside-container {
|
|
492
|
+
display: none;
|
|
493
|
+
flex: 1;
|
|
494
|
+
padding-left: 32px;
|
|
495
|
+
width: 100%;
|
|
496
|
+
max-width: 256px;
|
|
497
|
+
}
|
|
498
|
+
@media screen and (min-width: 960px) {
|
|
499
|
+
.aside-container {
|
|
500
|
+
display: block;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
.aside-outline-container {
|
|
504
|
+
position: sticky;
|
|
505
|
+
top: calc(
|
|
506
|
+
var(--vp-nav-height) + var(--vp-layout-top-height, 0px) +
|
|
507
|
+
var(--vp-doc-top-height, 0px) + 32px
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
.lastupdate {
|
|
511
|
+
color: var(--vp-c-text-1);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.list-mode {
|
|
515
|
+
max-height: 370px;
|
|
516
|
+
overflow-y: auto;
|
|
517
|
+
margin: 10px auto;
|
|
518
|
+
display: flex;
|
|
519
|
+
flex-wrap: wrap;
|
|
520
|
+
justify-content: center;
|
|
521
|
+
.el-image {
|
|
522
|
+
:deep(img) {
|
|
523
|
+
object-fit: contain;
|
|
524
|
+
// max-height: 360px;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.swiper-mode {
|
|
530
|
+
margin-top: 16px;
|
|
531
|
+
.el-image {
|
|
532
|
+
:deep(img) {
|
|
533
|
+
object-fit: contain;
|
|
534
|
+
max-height: 260px;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
.split {
|
|
539
|
+
display: inline-block;
|
|
540
|
+
width: 1px;
|
|
541
|
+
height: 8px;
|
|
542
|
+
margin: 0 10px;
|
|
543
|
+
background-color: #4e5969;
|
|
544
|
+
}
|
|
545
|
+
</style>
|
|
@@ -7,9 +7,13 @@ import {
|
|
|
7
7
|
inject,
|
|
8
8
|
InjectionKey,
|
|
9
9
|
provide,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Ref,
|
|
11
|
+
onMounted,
|
|
12
|
+
onUnmounted,
|
|
13
|
+
reactive,
|
|
14
|
+
ref
|
|
12
15
|
} from 'vue'
|
|
16
|
+
|
|
13
17
|
import type { Theme } from './index'
|
|
14
18
|
|
|
15
19
|
const configSymbol: InjectionKey<Ref<Theme.Config>> = Symbol('theme-config')
|
|
@@ -19,6 +23,8 @@ const activeTagSymbol: InjectionKey<Ref<Theme.activeTag>> = Symbol('active-tag')
|
|
|
19
23
|
const currentPageNum: InjectionKey<Ref<number>> = Symbol('home-page-num')
|
|
20
24
|
const homeConfigSymbol: InjectionKey<Theme.HomeConfig> = Symbol('home-config')
|
|
21
25
|
|
|
26
|
+
const userWorks: InjectionKey<Ref<Theme.UserWorks>> = Symbol('user-works')
|
|
27
|
+
|
|
22
28
|
export function withConfigProvider(App: Component) {
|
|
23
29
|
return defineComponent({
|
|
24
30
|
name: 'ConfigProvider',
|
|
@@ -34,6 +40,16 @@ export function withConfigProvider(App: Component) {
|
|
|
34
40
|
const { theme } = useData()
|
|
35
41
|
const config = computed(() => resolveConfig(theme.value))
|
|
36
42
|
provide(configSymbol, config)
|
|
43
|
+
provide(
|
|
44
|
+
userWorks,
|
|
45
|
+
ref(
|
|
46
|
+
config.value.blog?.works || {
|
|
47
|
+
title: '',
|
|
48
|
+
description: '',
|
|
49
|
+
list: []
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
)
|
|
37
53
|
|
|
38
54
|
const activeTag = ref<Theme.activeTag>({
|
|
39
55
|
label: '',
|
|
@@ -43,7 +59,6 @@ export function withConfigProvider(App: Component) {
|
|
|
43
59
|
|
|
44
60
|
const pageNum = ref(1)
|
|
45
61
|
provide(currentPageNum, pageNum)
|
|
46
|
-
|
|
47
62
|
return () => h(App, null, slots)
|
|
48
63
|
}
|
|
49
64
|
})
|
|
@@ -105,6 +120,9 @@ export function useCurrentArticle() {
|
|
|
105
120
|
return currentArticle
|
|
106
121
|
}
|
|
107
122
|
|
|
123
|
+
export function useUserWorks() {
|
|
124
|
+
return inject(userWorks)!
|
|
125
|
+
}
|
|
108
126
|
function resolveConfig(config: Theme.Config): Theme.Config {
|
|
109
127
|
return {
|
|
110
128
|
...config,
|
|
@@ -114,3 +132,61 @@ function resolveConfig(config: Theme.Config): Theme.Config {
|
|
|
114
132
|
}
|
|
115
133
|
}
|
|
116
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 页面加载的时候定位到锚点内容
|
|
138
|
+
*/
|
|
139
|
+
export function useActiveAnchor() {
|
|
140
|
+
const el = ref<HTMLElement | null>(null)
|
|
141
|
+
onMounted(() => {
|
|
142
|
+
const { hash } = window.location
|
|
143
|
+
if (hash) {
|
|
144
|
+
el.value = document.querySelector(decodeURIComponent(hash))
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
return el
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 页面滚动的时候自动更新锚点
|
|
152
|
+
*/
|
|
153
|
+
export function useAutoUpdateAnchor() {
|
|
154
|
+
// 初始化当前锚点
|
|
155
|
+
const currentAnchor = reactive({
|
|
156
|
+
id: '',
|
|
157
|
+
top: -1
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// 定义计算当前锚点的方法
|
|
161
|
+
const calculateCurrentAnchor = () => {
|
|
162
|
+
// 获取页面中所有的锚点元素
|
|
163
|
+
const anchors = document.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
|
164
|
+
for (let i = 0; i < anchors.length; i += 1) {
|
|
165
|
+
const anchor = anchors[i]
|
|
166
|
+
const rect = anchor.getBoundingClientRect()
|
|
167
|
+
// 如果当前锚点距离顶部最近,且距离顶部小于等于100,则将其设置为当前锚点
|
|
168
|
+
if (rect.top <= 100 && anchor.id !== currentAnchor.id) {
|
|
169
|
+
currentAnchor.id = anchor.id
|
|
170
|
+
currentAnchor.top = rect.top
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 监听 window 对象的滚动事件
|
|
176
|
+
const onScroll = () => {
|
|
177
|
+
calculateCurrentAnchor()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 在组件挂载时启动监听滚动事件
|
|
181
|
+
onMounted(() => {
|
|
182
|
+
window.addEventListener('scroll', onScroll)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// 在组件卸载时移除监听滚动事件
|
|
186
|
+
onUnmounted(() => {
|
|
187
|
+
window.removeEventListener('scroll', onScroll)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// 返回当前锚点的响应式对象
|
|
191
|
+
return currentAnchor
|
|
192
|
+
}
|
|
@@ -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
|
|
@@ -55,12 +60,13 @@ export namespace Theme {
|
|
|
55
60
|
* 手动控制相关文章列表的顺序
|
|
56
61
|
*/
|
|
57
62
|
recommend?: number | false
|
|
58
|
-
// TODO: 待开发
|
|
59
63
|
/**
|
|
64
|
+
* TODO: 待开发
|
|
60
65
|
* 时间线
|
|
61
66
|
*/
|
|
62
67
|
timeline: string
|
|
63
68
|
/**
|
|
69
|
+
* TODO: 待开发
|
|
64
70
|
* 专栏&合集
|
|
65
71
|
*/
|
|
66
72
|
album: string
|
|
@@ -151,7 +157,45 @@ export namespace Theme {
|
|
|
151
157
|
nickname: string
|
|
152
158
|
des: string
|
|
153
159
|
url: string
|
|
154
|
-
avatar:
|
|
160
|
+
avatar: ThemeableImage
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface UserWork {
|
|
164
|
+
title: string
|
|
165
|
+
description: string
|
|
166
|
+
time:
|
|
167
|
+
| string
|
|
168
|
+
| {
|
|
169
|
+
start: string
|
|
170
|
+
end?: string
|
|
171
|
+
lastupdate?: string
|
|
172
|
+
}
|
|
173
|
+
status?: {
|
|
174
|
+
text: string
|
|
175
|
+
type?: 'tip' | 'warning' | 'danger'
|
|
176
|
+
}
|
|
177
|
+
url?: string
|
|
178
|
+
github?:
|
|
179
|
+
| string
|
|
180
|
+
| {
|
|
181
|
+
owner: string
|
|
182
|
+
repo: string
|
|
183
|
+
branch?: string
|
|
184
|
+
path?: string
|
|
185
|
+
}
|
|
186
|
+
cover?:
|
|
187
|
+
| string
|
|
188
|
+
| string[]
|
|
189
|
+
| {
|
|
190
|
+
urls: string[]
|
|
191
|
+
layout?: 'swiper' | 'list'
|
|
192
|
+
}
|
|
193
|
+
links?: {
|
|
194
|
+
title: string
|
|
195
|
+
url: string
|
|
196
|
+
}[]
|
|
197
|
+
tags?: string[]
|
|
198
|
+
top?: number
|
|
155
199
|
}
|
|
156
200
|
export type SearchConfig =
|
|
157
201
|
| boolean
|
|
@@ -168,6 +212,12 @@ export namespace Theme {
|
|
|
168
212
|
mode?: boolean | 'pagefind'
|
|
169
213
|
}
|
|
170
214
|
|
|
215
|
+
export interface UserWorks {
|
|
216
|
+
title: string
|
|
217
|
+
description?: string
|
|
218
|
+
topTitle?: string
|
|
219
|
+
list: UserWork[]
|
|
220
|
+
}
|
|
171
221
|
export interface BlogConfig {
|
|
172
222
|
blog?: false
|
|
173
223
|
pagesData: PageData[]
|
|
@@ -187,7 +237,10 @@ export namespace Theme {
|
|
|
187
237
|
* power by https://giscus.app/zh-CN
|
|
188
238
|
*/
|
|
189
239
|
comment?: GiscusConfig | false
|
|
190
|
-
|
|
240
|
+
/**
|
|
241
|
+
* 阅读文章左侧的推荐文章(替代默认的sidebar)
|
|
242
|
+
*/
|
|
243
|
+
recommend?: RecommendArticle | false
|
|
191
244
|
article?: ArticleConfig
|
|
192
245
|
/**
|
|
193
246
|
* el-alert
|
|
@@ -200,7 +253,8 @@ export namespace Theme {
|
|
|
200
253
|
* 启用 [vitepress-plugin-tabs](https://www.npmjs.com/package/vitepress-plugin-tabs)
|
|
201
254
|
* @default false
|
|
202
255
|
*/
|
|
203
|
-
tabs
|
|
256
|
+
tabs?: boolean
|
|
257
|
+
works?: UserWorks
|
|
204
258
|
}
|
|
205
259
|
|
|
206
260
|
export interface Config extends DefaultTheme.Config {
|
package/src/index.ts
CHANGED
|
@@ -12,13 +12,16 @@ 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
|
-
|
|
21
|
+
DefaultTheme.enhanceApp(ctx)
|
|
22
|
+
// @ts-ignore
|
|
21
23
|
ctx.app.component('TimelinePage', TimelinePage)
|
|
24
|
+
ctx.app.component('UserWorksPage', UserWorksPage)
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
|
package/src/node.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -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
|
+
}
|