@sugarat/theme 0.1.28 → 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 +50 -1
- package/node.js +176 -3
- package/package.json +3 -2
- package/src/components/BlogFriendLink.vue +26 -3
- package/src/components/TimelinePage.vue +0 -4
- package/src/components/UserWorks.vue +340 -0
- package/src/composables/config/blog.ts +15 -1
- package/src/composables/config/index.ts +59 -2
- package/src/index.ts +3 -1
- package/src/node.ts +11 -1
- package/src/utils/index.ts +74 -0
package/node.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DefaultTheme, UserConfig } from 'vitepress';
|
|
2
2
|
import { ElButton } from 'element-plus';
|
|
3
|
+
export { tabsMarkdownPlugin } from 'vitepress-plugin-tabs';
|
|
3
4
|
|
|
4
5
|
declare namespace BlogPopover {
|
|
5
6
|
interface Title {
|
|
@@ -26,6 +27,14 @@ declare namespace BlogPopover {
|
|
|
26
27
|
}
|
|
27
28
|
type Value = Title | Text | Image | Button;
|
|
28
29
|
}
|
|
30
|
+
type ThemeableImage = string | {
|
|
31
|
+
src: string;
|
|
32
|
+
alt?: string;
|
|
33
|
+
} | {
|
|
34
|
+
light: string;
|
|
35
|
+
dark: string;
|
|
36
|
+
alt?: string;
|
|
37
|
+
};
|
|
29
38
|
declare namespace Theme {
|
|
30
39
|
interface PageMeta {
|
|
31
40
|
title: string;
|
|
@@ -139,7 +148,36 @@ declare namespace Theme {
|
|
|
139
148
|
nickname: string;
|
|
140
149
|
des: string;
|
|
141
150
|
url: string;
|
|
142
|
-
avatar:
|
|
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;
|
|
143
181
|
}
|
|
144
182
|
type SearchConfig = boolean | 'pagefind' | {
|
|
145
183
|
btnPlaceholder?: string;
|
|
@@ -152,6 +190,11 @@ declare namespace Theme {
|
|
|
152
190
|
heading?: string;
|
|
153
191
|
mode?: boolean | 'pagefind';
|
|
154
192
|
};
|
|
193
|
+
interface UserWorks {
|
|
194
|
+
title: string;
|
|
195
|
+
description?: string;
|
|
196
|
+
list: UserWork[];
|
|
197
|
+
}
|
|
155
198
|
interface BlogConfig {
|
|
156
199
|
blog?: false;
|
|
157
200
|
pagesData: PageData[];
|
|
@@ -180,6 +223,12 @@ declare namespace Theme {
|
|
|
180
223
|
popover?: Popover;
|
|
181
224
|
friend?: FriendLink[];
|
|
182
225
|
authorList?: Omit<FriendLink, 'avatar'>[];
|
|
226
|
+
/**
|
|
227
|
+
* 启用 [vitepress-plugin-tabs](https://www.npmjs.com/package/vitepress-plugin-tabs)
|
|
228
|
+
* @default false
|
|
229
|
+
*/
|
|
230
|
+
tabs?: boolean;
|
|
231
|
+
works?: UserWorks;
|
|
183
232
|
}
|
|
184
233
|
interface Config extends DefaultTheme.Config {
|
|
185
234
|
blog?: BlogConfig;
|
package/node.js
CHANGED
|
@@ -31,7 +31,8 @@ __export(node_exports, {
|
|
|
31
31
|
getDefaultTitle: () => getDefaultTitle,
|
|
32
32
|
getFileBirthTime: () => getFileBirthTime,
|
|
33
33
|
getGitTimestamp: () => getGitTimestamp,
|
|
34
|
-
getThemeConfig: () => getThemeConfig
|
|
34
|
+
getThemeConfig: () => getThemeConfig,
|
|
35
|
+
tabsMarkdownPlugin: () => tabsPlugin
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(node_exports);
|
|
37
38
|
var import_fast_glob = __toESM(require("fast-glob"));
|
|
@@ -40,6 +41,170 @@ var import_fs = __toESM(require("fs"));
|
|
|
40
41
|
var import_child_process = require("child_process");
|
|
41
42
|
var import_path = __toESM(require("path"));
|
|
42
43
|
|
|
44
|
+
// ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@1.0.0-alpha.75_vue@3.2.45/node_modules/vitepress-plugin-tabs/dist/index.js
|
|
45
|
+
var tabsMarker = "=tabs";
|
|
46
|
+
var tabsMarkerLen = tabsMarker.length;
|
|
47
|
+
var ruleBlockTabs = (state, startLine, endLine, silent) => {
|
|
48
|
+
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
52
|
+
let max = state.eMarks[startLine];
|
|
53
|
+
if (pos + 3 > max) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const marker = state.src.charCodeAt(pos);
|
|
57
|
+
if (marker !== 58) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const mem = pos;
|
|
61
|
+
pos = state.skipChars(pos, marker);
|
|
62
|
+
let len = pos - mem;
|
|
63
|
+
if (len < 3) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (state.src.slice(pos, pos + tabsMarkerLen) !== tabsMarker) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
pos += tabsMarkerLen;
|
|
70
|
+
if (silent) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
const markup = state.src.slice(mem, pos);
|
|
74
|
+
const params = state.src.slice(pos, max);
|
|
75
|
+
let nextLine = startLine;
|
|
76
|
+
let haveEndMarker = false;
|
|
77
|
+
for (; ; ) {
|
|
78
|
+
nextLine++;
|
|
79
|
+
if (nextLine >= endLine) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
83
|
+
const mem2 = pos;
|
|
84
|
+
max = state.eMarks[nextLine];
|
|
85
|
+
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (state.src.charCodeAt(pos) !== marker) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (state.sCount[nextLine] - state.blkIndent >= 4) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
pos = state.skipChars(pos, marker);
|
|
95
|
+
if (pos - mem2 < len) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
pos = state.skipSpaces(pos);
|
|
99
|
+
if (pos < max) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
haveEndMarker = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
len = state.sCount[startLine];
|
|
106
|
+
state.line = nextLine + (haveEndMarker ? 1 : 0);
|
|
107
|
+
const token = state.push("tabs", "div", 0);
|
|
108
|
+
token.info = params;
|
|
109
|
+
token.content = state.getLines(startLine + 1, nextLine, len, true);
|
|
110
|
+
token.markup = markup;
|
|
111
|
+
token.map = [startLine, state.line];
|
|
112
|
+
return true;
|
|
113
|
+
};
|
|
114
|
+
var tabBreakRE = /^\s*::(.+)$/;
|
|
115
|
+
var forbiddenCharsInSlotNames = /[ '"]/;
|
|
116
|
+
var parseTabBreakLine = (line) => {
|
|
117
|
+
const m = line.match(tabBreakRE);
|
|
118
|
+
if (!m)
|
|
119
|
+
return null;
|
|
120
|
+
const trimmed = m[1].trim();
|
|
121
|
+
if (forbiddenCharsInSlotNames.test(trimmed)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`contains forbidden chars in slot names (space and quotes) (${JSON.stringify(
|
|
124
|
+
line
|
|
125
|
+
)})`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return trimmed;
|
|
129
|
+
};
|
|
130
|
+
var lastLineBreakRE = /\n$/;
|
|
131
|
+
var parseTabsContent = (content) => {
|
|
132
|
+
const lines = content.replace(lastLineBreakRE, "").split("\n");
|
|
133
|
+
const tabInfos = [];
|
|
134
|
+
const tabLabels = /* @__PURE__ */ new Set();
|
|
135
|
+
let currentTab = null;
|
|
136
|
+
const createTabInfo = (label) => {
|
|
137
|
+
if (tabLabels.has(label)) {
|
|
138
|
+
throw new Error(`a tab labelled ${JSON.stringify(label)} already exists`);
|
|
139
|
+
}
|
|
140
|
+
const newTab = { label, content: [] };
|
|
141
|
+
tabInfos.push(newTab);
|
|
142
|
+
tabLabels.add(label);
|
|
143
|
+
return newTab;
|
|
144
|
+
};
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const tabLabel = parseTabBreakLine(line);
|
|
147
|
+
if (currentTab === null) {
|
|
148
|
+
if (tabLabel === null) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`tabs should start with \`::\${tabLabel}\` (e.g. "::foo"). (received: ${JSON.stringify(
|
|
151
|
+
line
|
|
152
|
+
)})`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
currentTab = createTabInfo(tabLabel);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (tabLabel === null) {
|
|
159
|
+
currentTab.content.push(line);
|
|
160
|
+
} else {
|
|
161
|
+
currentTab = createTabInfo(tabLabel);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (tabInfos.length < 0) {
|
|
165
|
+
throw new Error("tabs should include at least one tab");
|
|
166
|
+
}
|
|
167
|
+
return tabInfos.map((info) => ({
|
|
168
|
+
label: info.label,
|
|
169
|
+
content: info.content.join("\n").replace(lastLineBreakRE, "")
|
|
170
|
+
}));
|
|
171
|
+
};
|
|
172
|
+
var parseParams = (input) => {
|
|
173
|
+
if (!input.startsWith("=")) {
|
|
174
|
+
return {
|
|
175
|
+
shareStateKey: void 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const splitted = input.split("=");
|
|
179
|
+
return {
|
|
180
|
+
shareStateKey: splitted[1]
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
var tabsPlugin = (md) => {
|
|
184
|
+
md.block.ruler.before("fence", "=tabs", ruleBlockTabs, {
|
|
185
|
+
alt: ["paragraph", "reference", "blockquote", "list"]
|
|
186
|
+
});
|
|
187
|
+
md.renderer.rules.tabs = (tokens, index, _options, env) => {
|
|
188
|
+
const token = tokens[index];
|
|
189
|
+
const tabs = parseTabsContent(token.content);
|
|
190
|
+
const renderedTabs = tabs.map((tab) => ({
|
|
191
|
+
label: tab.label,
|
|
192
|
+
content: md.render(tab.content, env)
|
|
193
|
+
}));
|
|
194
|
+
const params = parseParams(token.info);
|
|
195
|
+
const tabLabelsProp = `:tabLabels="${md.utils.escapeHtml(
|
|
196
|
+
JSON.stringify(tabs.map((tab) => tab.label))
|
|
197
|
+
)}"`;
|
|
198
|
+
const shareStateKeyProp = params.shareStateKey ? `sharedStateKey="${md.utils.escapeHtml(params.shareStateKey)}"` : "";
|
|
199
|
+
const slots = renderedTabs.map(
|
|
200
|
+
(tab) => `<template #${tab.label}>${tab.content}</template>`
|
|
201
|
+
);
|
|
202
|
+
return `<PluginTabs ${tabLabelsProp} ${shareStateKeyProp}>${slots.join(
|
|
203
|
+
""
|
|
204
|
+
)}</PluginTabs>`;
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
43
208
|
// src/utils/index.ts
|
|
44
209
|
function formatDate(d, fmt = "yyyy-MM-dd hh:mm:ss") {
|
|
45
210
|
if (!(d instanceof Date)) {
|
|
@@ -174,6 +339,13 @@ function getThemeConfig(cfg) {
|
|
|
174
339
|
]
|
|
175
340
|
};
|
|
176
341
|
}
|
|
342
|
+
if (cfg?.tabs) {
|
|
343
|
+
extraConfig.markdown = {
|
|
344
|
+
config(md) {
|
|
345
|
+
tabsPlugin(md);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
177
349
|
return {
|
|
178
350
|
themeConfig: {
|
|
179
351
|
blog: {
|
|
@@ -195,7 +367,7 @@ function getThemeConfig(cfg) {
|
|
|
195
367
|
function getDefaultTitle(content) {
|
|
196
368
|
const title = clearMatterContent(content).split("\n")?.find((str) => {
|
|
197
369
|
return str.startsWith("# ");
|
|
198
|
-
})?.slice(2).replace(
|
|
370
|
+
})?.slice(2).replace(/^\s+|\s+$/g, "") || "";
|
|
199
371
|
return title;
|
|
200
372
|
}
|
|
201
373
|
function clearMatterContent(content) {
|
|
@@ -266,5 +438,6 @@ function defineConfig(config) {
|
|
|
266
438
|
getDefaultTitle,
|
|
267
439
|
getFileBirthTime,
|
|
268
440
|
getGitTimestamp,
|
|
269
|
-
getThemeConfig
|
|
441
|
+
getThemeConfig,
|
|
442
|
+
tabsMarkdownPlugin
|
|
270
443
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sugarat/theme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"tsup": " ^6.5.0",
|
|
48
48
|
"typescript": "^4.8.2",
|
|
49
49
|
"vitepress": "1.0.0-alpha.75",
|
|
50
|
-
"vue": "^3.2.45"
|
|
50
|
+
"vue": "^3.2.45",
|
|
51
|
+
"vitepress-plugin-tabs": "^0.2.0"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
54
|
"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>
|
|
@@ -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:
|
|
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[]
|
|
@@ -196,6 +247,12 @@ export namespace Theme {
|
|
|
196
247
|
popover?: Popover
|
|
197
248
|
friend?: FriendLink[]
|
|
198
249
|
authorList?: Omit<FriendLink, 'avatar'>[]
|
|
250
|
+
/**
|
|
251
|
+
* 启用 [vitepress-plugin-tabs](https://www.npmjs.com/package/vitepress-plugin-tabs)
|
|
252
|
+
* @default false
|
|
253
|
+
*/
|
|
254
|
+
tabs?: boolean
|
|
255
|
+
works?: UserWorks
|
|
199
256
|
}
|
|
200
257
|
|
|
201
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
|
-
//
|
|
21
|
+
// @ts-ignore
|
|
21
22
|
ctx.app.component('TimelinePage', TimelinePage)
|
|
23
|
+
ctx.app.component('UserWorksPage', UserWorksPage)
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
package/src/node.ts
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'fs'
|
|
|
5
5
|
import { execSync, spawn, spawnSync } from 'child_process'
|
|
6
6
|
import path from 'path'
|
|
7
7
|
import type { SiteConfig, UserConfig } from 'vitepress'
|
|
8
|
+
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
|
|
8
9
|
import { formatDate } from './utils/index'
|
|
9
10
|
import type { Theme } from './composables/config/index'
|
|
10
11
|
|
|
@@ -161,6 +162,13 @@ export function getThemeConfig(cfg?: Partial<Theme.BlogConfig>) {
|
|
|
161
162
|
]
|
|
162
163
|
}
|
|
163
164
|
}
|
|
165
|
+
if (cfg?.tabs) {
|
|
166
|
+
extraConfig.markdown = {
|
|
167
|
+
config(md: any) {
|
|
168
|
+
tabsMarkdownPlugin(md)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
164
172
|
return {
|
|
165
173
|
themeConfig: {
|
|
166
174
|
blog: {
|
|
@@ -190,7 +198,7 @@ export function getDefaultTitle(content: string) {
|
|
|
190
198
|
return str.startsWith('# ')
|
|
191
199
|
})
|
|
192
200
|
?.slice(2)
|
|
193
|
-
.replace(
|
|
201
|
+
.replace(/^\s+|\s+$/g, '') || ''
|
|
194
202
|
return title
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -295,3 +303,5 @@ export function defineConfig(config: UserConfig<Theme.Config>) {
|
|
|
295
303
|
}
|
|
296
304
|
return config
|
|
297
305
|
}
|
|
306
|
+
|
|
307
|
+
export { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
|
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
|
+
}
|