@sugarat/theme 0.2.30 → 0.3.1

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
@@ -93,7 +93,8 @@ declare namespace Theme {
93
93
  label: string;
94
94
  type: string;
95
95
  }
96
- interface CommentConfig extends GiscusConfig {
96
+ type CommentConfig = ((GiscusOption & CommentCommonConfig) | GiscusConfig | ArtalkConfig);
97
+ interface CommentCommonConfig {
97
98
  /**
98
99
  * @default '评论'
99
100
  */
@@ -109,7 +110,15 @@ declare namespace Theme {
109
110
  */
110
111
  mobileMinify?: boolean;
111
112
  }
112
- interface GiscusConfig {
113
+ interface GiscusConfig extends CommentCommonConfig {
114
+ type: 'giscus';
115
+ options: GiscusOption;
116
+ }
117
+ interface ArtalkConfig extends CommentCommonConfig {
118
+ type: 'artalk';
119
+ options: ArtalkOption;
120
+ }
121
+ interface GiscusOption {
113
122
  repo: Repo;
114
123
  repoId: string;
115
124
  category: string;
@@ -119,6 +128,10 @@ declare namespace Theme {
119
128
  lang?: string;
120
129
  loading?: 'lazy' | 'eager';
121
130
  }
131
+ interface ArtalkOption {
132
+ site: string;
133
+ server: string;
134
+ }
122
135
  interface HotArticle {
123
136
  title?: string;
124
137
  pageSize?: number;
@@ -319,7 +332,8 @@ declare namespace Theme {
319
332
  search?: SearchConfig;
320
333
  /**
321
334
  * 配置评论
322
- * power by https://giscus.app/zh-CN
335
+ * giscus: https://giscus.app/zh-CN
336
+ * artalk: https://artalk.js.org/
323
337
  */
324
338
  comment?: CommentConfig | false;
325
339
  /**
@@ -391,6 +405,13 @@ declare namespace Theme {
391
405
  */
392
406
  oml2d?: Options;
393
407
  homeTags?: boolean;
408
+ buttonAfterArticle?: ButtonAfterArticleConfig | false;
409
+ /**
410
+ * 是否开启深色模式过渡动画
411
+ * @reference https://vitepress.dev/zh/guide/extending-default-theme#on-appearance-toggle
412
+ * @default true
413
+ */
414
+ darkTransition?: boolean;
394
415
  }
395
416
  interface BackToTop {
396
417
  /**
@@ -470,6 +491,22 @@ declare namespace Theme {
470
491
  */
471
492
  handleChangeSlogan?: (oldSlogan: string) => string | Promise<string>;
472
493
  }
494
+ interface ButtonAfterArticleConfig {
495
+ openTitle?: string;
496
+ closeTitle?: string;
497
+ content?: string;
498
+ icon?: 'aliPay' | 'wechatPay' | string;
499
+ /**
500
+ * 按钮尺寸
501
+ * @default 'default'
502
+ */
503
+ size?: 'small' | 'default' | 'large';
504
+ /**
505
+ * 默认展开
506
+ * @default false
507
+ */
508
+ expand?: boolean;
509
+ }
473
510
  }
474
511
 
475
512
  /**
package/node.js CHANGED
@@ -40,7 +40,7 @@ module.exports = __toCommonJS(node_exports);
40
40
  // src/utils/node/mdPlugins.ts
41
41
  var import_module = require("module");
42
42
 
43
- // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@1.0.1_vue@3.4.21/node_modules/vitepress-plugin-tabs/dist/index.js
43
+ // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@1.1.4_@algolia+client-search@4.19.1_@types+node@20.6.3__ee2wap4ljxfukruj47sfuqlmqq/node_modules/vitepress-plugin-tabs/dist/index.js
44
44
  var tabsMarker = "=tabs";
45
45
  var tabsMarkerLen = tabsMarker.length;
46
46
  var ruleBlockTabs = (state, startLine, endLine, silent) => {
@@ -210,69 +210,9 @@ var import_vitepress_markdown_timeline = __toESM(require("vitepress-markdown-tim
210
210
  // src/utils/node/index.ts
211
211
  var import_node_child_process = require("child_process");
212
212
  var import_node_path = __toESM(require("path"));
213
-
214
- // src/utils/client/index.ts
215
- function formatDate(d, fmt = "yyyy-MM-dd hh:mm:ss") {
216
- if (!(d instanceof Date)) {
217
- d = new Date(d);
218
- }
219
- const o = {
220
- "M+": d.getMonth() + 1,
221
- // 月份
222
- "d+": d.getDate(),
223
- // 日
224
- "h+": d.getHours(),
225
- // 小时
226
- "m+": d.getMinutes(),
227
- // 分
228
- "s+": d.getSeconds(),
229
- // 秒
230
- "q+": Math.floor((d.getMonth() + 3) / 3),
231
- // 季度
232
- "S": d.getMilliseconds()
233
- // 毫秒
234
- };
235
- if (/(y+)/.test(fmt)) {
236
- fmt = fmt.replace(
237
- RegExp.$1,
238
- `${d.getFullYear()}`.substr(4 - RegExp.$1.length)
239
- );
240
- }
241
- for (const k in o) {
242
- if (new RegExp(`(${k})`).test(fmt))
243
- fmt = fmt.replace(
244
- RegExp.$1,
245
- RegExp.$1.length === 1 ? o[k] : `00${o[k]}`.substr(`${o[k]}`.length)
246
- );
247
- }
248
- return fmt;
249
- }
250
-
251
- // src/utils/node/index.ts
252
- function clearMatterContent(content) {
253
- let first___;
254
- let second___;
255
- const lines = content.split("\n").reduce((pre, line) => {
256
- if (!line.trim() && pre.length === 0) {
257
- return pre;
258
- }
259
- if (line.trim() === "---") {
260
- if (first___ === void 0) {
261
- first___ = pre.length;
262
- } else if (second___ === void 0) {
263
- second___ = pre.length;
264
- }
265
- }
266
- pre.push(line);
267
- return pre;
268
- }, []);
269
- return lines.slice(second___ || 0).join("\n");
270
- }
271
213
  function getDefaultTitle(content) {
272
- const title = clearMatterContent(content).split("\n")?.find((str) => {
273
- return str.startsWith("# ");
274
- })?.slice(2).replace(/^\s+|\s+$/g, "") || "";
275
- return title;
214
+ const match = content.match(/^(#+)\s+(.+)/m);
215
+ return match?.[2] || "";
276
216
  }
277
217
  function getFileBirthTime(url) {
278
218
  let date = /* @__PURE__ */ new Date();
@@ -282,12 +222,12 @@ function getFileBirthTime(url) {
282
222
  date = new Date(infoStr);
283
223
  }
284
224
  } catch (error) {
285
- return formatDate(date);
225
+ return date;
286
226
  }
287
- return formatDate(date);
227
+ return date;
288
228
  }
289
229
  function getTextSummary(text, count = 100) {
290
- return clearMatterContent(text).match(/^# ([\s\S]+)/m)?.[1]?.replace(/#/g, "")?.replace(/!\[.*?\]\(.*?\)/g, "")?.replace(/\[(.*?)\]\(.*?\)/g, "$1")?.replace(/\*\*(.*?)\*\*/g, "$1")?.split("\n")?.filter((v) => !!v)?.slice(1)?.join("\n")?.replace(/>(.*)/, "")?.slice(0, count);
230
+ return text?.replace(/^#+\s+.*/, "")?.replace(/#/g, "")?.replace(/!\[.*?\]\(.*?\)/g, "")?.replace(/\[(.*?)\]\(.*?\)/g, "$1")?.replace(/\*\*(.*?)\*\*/g, "$1")?.split("\n")?.filter((v) => !!v)?.join("\n")?.replace(/>(.*)/, "")?.trim()?.slice(0, count);
291
231
  }
292
232
  function aliasObjectToArray(obj) {
293
233
  return Object.entries(obj).map(([find, replacement]) => ({
@@ -317,6 +257,37 @@ function getFirstImagURLFromMD(content, route) {
317
257
  const relativePath = url.startsWith("/") ? url : import_node_path.default.join(paths.join("/") || "", url);
318
258
  return joinPath("/", relativePath);
319
259
  }
260
+ function debounce(func, delay = 1e3) {
261
+ let timeoutId;
262
+ return (...rest) => {
263
+ clearTimeout(timeoutId);
264
+ timeoutId = setTimeout(() => {
265
+ func(...rest);
266
+ }, delay);
267
+ };
268
+ }
269
+ function isEqual(obj1, obj2, excludeKeys = []) {
270
+ const keys1 = Object.keys(obj1).filter((key) => !excludeKeys.includes(key));
271
+ const keys2 = Object.keys(obj2).filter((key) => !excludeKeys.includes(key));
272
+ if (keys1.length !== keys2.length) {
273
+ return false;
274
+ }
275
+ for (const key of keys1) {
276
+ if (!keys2.includes(key)) {
277
+ return false;
278
+ }
279
+ const val1 = obj1[key];
280
+ const val2 = obj2[key];
281
+ const areObjects = isObject(val1) && isObject(val2);
282
+ if (areObjects && !isEqual(val1, val2, excludeKeys) || !areObjects && val1 !== val2) {
283
+ return false;
284
+ }
285
+ }
286
+ return true;
287
+ }
288
+ function isObject(obj) {
289
+ return obj != null && typeof obj === "object";
290
+ }
320
291
 
321
292
  // src/utils/node/mdPlugins.ts
322
293
  var import_meta = {};
@@ -384,6 +355,45 @@ var import_node_path2 = __toESM(require("path"));
384
355
  var import_node_process = __toESM(require("process"));
385
356
  var import_fast_glob = __toESM(require("fast-glob"));
386
357
  var import_gray_matter = __toESM(require("gray-matter"));
358
+
359
+ // src/utils/client/index.ts
360
+ function formatDate(d, fmt = "yyyy-MM-dd hh:mm:ss") {
361
+ if (!(d instanceof Date)) {
362
+ d = new Date(d);
363
+ }
364
+ const o = {
365
+ "M+": d.getMonth() + 1,
366
+ // 月份
367
+ "d+": d.getDate(),
368
+ // 日
369
+ "h+": d.getHours(),
370
+ // 小时
371
+ "m+": d.getMinutes(),
372
+ // 分
373
+ "s+": d.getSeconds(),
374
+ // 秒
375
+ "q+": Math.floor((d.getMonth() + 3) / 3),
376
+ // 季度
377
+ "S": d.getMilliseconds()
378
+ // 毫秒
379
+ };
380
+ if (/(y+)/.test(fmt)) {
381
+ fmt = fmt.replace(
382
+ RegExp.$1,
383
+ `${d.getFullYear()}`.substr(4 - RegExp.$1.length)
384
+ );
385
+ }
386
+ for (const k in o) {
387
+ if (new RegExp(`(${k})`).test(fmt))
388
+ fmt = fmt.replace(
389
+ RegExp.$1,
390
+ RegExp.$1.length === 1 ? o[k] : `00${o[k]}`.substr(`${o[k]}`.length)
391
+ );
392
+ }
393
+ return fmt;
394
+ }
395
+
396
+ // src/utils/node/theme.ts
387
397
  function patchDefaultThemeSideBar(cfg) {
388
398
  return cfg?.blog !== false && cfg?.recommend !== false ? {
389
399
  sidebar: [
@@ -394,64 +404,79 @@ function patchDefaultThemeSideBar(cfg) {
394
404
  ]
395
405
  } : void 0;
396
406
  }
397
- var pageMap = /* @__PURE__ */ new Map();
407
+ function getPageRoute(filepath, srcDir) {
408
+ let route = filepath.replace(".md", "");
409
+ if (route.startsWith("./")) {
410
+ route = route.replace(
411
+ new RegExp(
412
+ `^\\.\\/${import_node_path2.default.join(srcDir, "/").replace(new RegExp(`\\${import_node_path2.default.sep}`, "g"), "/")}`
413
+ ),
414
+ ""
415
+ );
416
+ } else {
417
+ route = route.replace(
418
+ new RegExp(
419
+ `^${import_node_path2.default.join(srcDir, "/").replace(new RegExp(`\\${import_node_path2.default.sep}`, "g"), "/")}`
420
+ ),
421
+ ""
422
+ );
423
+ }
424
+ return `/${route}`;
425
+ }
426
+ var defaultTimeZoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset() / -60;
427
+ function getArticleMeta(filepath, route, timeZone = defaultTimeZoneOffset) {
428
+ const fileContent = import_node_fs.default.readFileSync(filepath, "utf-8");
429
+ const { data: frontmatter, excerpt, content } = (0, import_gray_matter.default)(fileContent, {
430
+ excerpt: true
431
+ });
432
+ const meta = {
433
+ ...frontmatter
434
+ };
435
+ if (!meta.title) {
436
+ meta.title = getDefaultTitle(content);
437
+ }
438
+ if (!meta.date) {
439
+ meta.date = formatDate(getFileBirthTime(filepath));
440
+ } else {
441
+ meta.date = formatDate(
442
+ /* @__PURE__ */ new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`)
443
+ );
444
+ }
445
+ meta.categories = typeof meta.categories === "string" ? [meta.categories] : meta.categories;
446
+ meta.tags = typeof meta.tags === "string" ? [meta.tags] : meta.tags;
447
+ meta.tag = [meta.tag || []].flat().concat([
448
+ .../* @__PURE__ */ new Set([...meta.categories || [], ...meta.tags || []])
449
+ ]);
450
+ meta.description = meta.description || getTextSummary(content, 100) || excerpt;
451
+ meta.cover = meta.cover ?? getFirstImagURLFromMD(fileContent, route);
452
+ if (meta.publish === false) {
453
+ meta.hidden = true;
454
+ meta.recommend = false;
455
+ }
456
+ return meta;
457
+ }
398
458
  function getArticles(cfg) {
399
459
  const srcDir = cfg?.srcDir || import_node_process.default.argv.slice(2)?.[1] || ".";
400
460
  const files = import_fast_glob.default.sync(`${srcDir}/**/*.md`, { ignore: ["node_modules"] });
401
- const data = files.map((v) => {
402
- let route = v.replace(".md", "");
403
- if (route.startsWith("./")) {
404
- route = route.replace(
405
- new RegExp(
406
- `^\\.\\/${import_node_path2.default.join(srcDir, "/").replace(new RegExp(`\\${import_node_path2.default.sep}`, "g"), "/")}`
407
- ),
408
- ""
409
- );
410
- } else {
411
- route = route.replace(
412
- new RegExp(
413
- `^${import_node_path2.default.join(srcDir, "/").replace(new RegExp(`\\${import_node_path2.default.sep}`, "g"), "/")}`
414
- ),
415
- ""
416
- );
417
- }
418
- pageMap.set(`/${route}`, v);
419
- const fileContent = import_node_fs.default.readFileSync(v, "utf-8");
420
- const { data: frontmatter, excerpt } = (0, import_gray_matter.default)(fileContent, {
421
- excerpt: true
422
- });
423
- const meta = {
424
- ...frontmatter
425
- };
426
- if (!meta.title) {
427
- meta.title = getDefaultTitle(fileContent);
428
- }
429
- if (!meta.date) {
430
- meta.date = getFileBirthTime(v);
431
- } else {
432
- const timeZone = cfg?.timeZone ?? 8;
433
- meta.date = formatDate(
434
- /* @__PURE__ */ new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`)
435
- );
436
- }
437
- meta.categories = typeof meta.categories === "string" ? [meta.categories] : meta.categories;
438
- meta.tags = typeof meta.tags === "string" ? [meta.tags] : meta.tags;
439
- meta.tag = [meta.tag || []].flat().concat([
440
- .../* @__PURE__ */ new Set([...meta.categories || [], ...meta.tags || []])
441
- ]);
442
- const wordCount = 100;
443
- meta.description = meta.description || getTextSummary(fileContent, wordCount);
444
- meta.cover = meta.cover ?? getFirstImagURLFromMD(fileContent, `/${route}`);
445
- if (meta.publish === false) {
446
- meta.hidden = true;
447
- meta.recommend = false;
448
- }
461
+ const pageData = files.map((filepath) => {
462
+ const route = getPageRoute(filepath, srcDir);
463
+ const meta = getArticleMeta(filepath, route, cfg?.timeZone);
449
464
  return {
450
- route: `/${route}`,
465
+ route,
451
466
  meta
452
467
  };
453
468
  }).filter((v) => v.meta.layout !== "home");
454
- return data;
469
+ return pageData;
470
+ }
471
+ function patchVPConfig(vpConfig, cfg) {
472
+ vpConfig.head = vpConfig.head || [];
473
+ if (cfg?.comment && "type" in cfg.comment && cfg?.comment?.type === "artalk") {
474
+ const server = cfg.comment?.options?.server;
475
+ if (server) {
476
+ vpConfig.head.push(["link", { href: `${server}/dist/Artalk.css`, rel: "stylesheet" }]);
477
+ vpConfig.head.push(["script", { src: `${server}/dist/Artalk.js`, id: "artalk-script" }]);
478
+ }
479
+ }
455
480
  }
456
481
  function patchVPThemeConfig(cfg, vpThemeConfig = {}) {
457
482
  vpThemeConfig.sidebar = patchDefaultThemeSideBar(cfg)?.sidebar;
@@ -468,11 +493,64 @@ var import_node_fs2 = require("fs");
468
493
  var import_node_buffer = require("buffer");
469
494
  var import_vitepress_plugin_pagefind = require("vitepress-plugin-pagefind");
470
495
  var import_vitepress_plugin_rss = require("vitepress-plugin-rss");
496
+
497
+ // src/utils/node/hot-reload-plugin.ts
498
+ function themeReloadPlugin() {
499
+ let blogConfig;
500
+ let vitepressConfig;
501
+ let docsDir;
502
+ const generateRoute = (filepath) => {
503
+ return filepath.replace(docsDir, "").replace(".md", "");
504
+ };
505
+ return {
506
+ name: "@sugarat/theme-reload",
507
+ apply: "serve",
508
+ configureServer(server) {
509
+ const restart = debounce(() => {
510
+ server.restart();
511
+ }, 500);
512
+ server.watcher.on("add", (path4) => {
513
+ const route = generateRoute(path4);
514
+ const meta = getArticleMeta(path4, route, blogConfig?.timeZone);
515
+ blogConfig.pagesData.push({
516
+ route,
517
+ meta
518
+ });
519
+ restart();
520
+ });
521
+ server.watcher.on("change", (path4) => {
522
+ const route = generateRoute(path4);
523
+ const meta = getArticleMeta(path4, route, blogConfig?.timeZone);
524
+ const matched = blogConfig.pagesData.find((v) => v.route === route);
525
+ if (matched && !isEqual(matched.meta, meta, ["date", "description"])) {
526
+ matched.meta = meta;
527
+ restart();
528
+ }
529
+ });
530
+ server.watcher.on("unlink", (path4) => {
531
+ const route = generateRoute(path4);
532
+ const idx = blogConfig.pagesData.findIndex((v) => v.route === route);
533
+ if (idx >= 0) {
534
+ blogConfig.pagesData.splice(idx, 1);
535
+ restart();
536
+ }
537
+ });
538
+ },
539
+ configResolved(config) {
540
+ vitepressConfig = config.vitepress;
541
+ docsDir = vitepressConfig.srcDir;
542
+ blogConfig = config.vitepress.site.themeConfig.blog;
543
+ }
544
+ };
545
+ }
546
+
547
+ // src/utils/node/vitePlugins.ts
471
548
  function getVitePlugins(cfg) {
472
549
  const plugins = [];
473
550
  const buildEndFn = [];
474
551
  plugins.push(inlineBuildEndPlugin(buildEndFn));
475
552
  plugins.push(coverImgTransform());
553
+ plugins.push(themeReloadPlugin());
476
554
  if (cfg && cfg.search !== false) {
477
555
  const ops = cfg.search instanceof Object ? cfg.search : {};
478
556
  plugins.push(
@@ -574,6 +652,7 @@ function getThemeConfig(cfg) {
574
652
  registerMdPlugins(extraVPConfig, markdownPlugin);
575
653
  patchMermaidPluginCfg(extraVPConfig);
576
654
  patchOptimizeDeps(extraVPConfig);
655
+ patchVPConfig(extraVPConfig, cfg);
577
656
  return {
578
657
  themeConfig: {
579
658
  blog: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sugarat/theme",
3
- "version": "0.2.30",
3
+ "version": "0.3.1",
4
4
  "description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
5
5
  "author": "sugar",
6
6
  "license": "MIT",
@@ -52,12 +52,14 @@
52
52
  },
53
53
  "devDependencies": {
54
54
  "@element-plus/icons-vue": "^2.1.0",
55
+ "artalk": "^2.8.3",
55
56
  "element-plus": "^2.3.4",
56
57
  "javascript-stringify": "^2.1.0",
57
58
  "pagefind": "1.0.3",
58
59
  "sass": "^1.56.1",
59
60
  "typescript": "^4.8.2",
60
- "vitepress": "1.0.1",
61
+ "vite": "^5",
62
+ "vitepress": "1.1.4",
61
63
  "vue": "^3.4.21"
62
64
  },
63
65
  "scripts": {
@@ -2,13 +2,12 @@
2
2
  import Theme from 'vitepress/theme'
3
3
  import { useData } from 'vitepress'
4
4
  import { computed } from 'vue'
5
+ import { useDarkTransition } from '../hooks/useDarkTransition'
5
6
  import { useOml2d } from '../hooks/useOml2d'
6
- import { useBlogThemeMode } from '../composables/config/blog'
7
+ import { useBlogThemeMode, useDarkTransitionConfig } from '../composables/config/blog'
7
8
  import BlogHomeInfo from './BlogHomeInfo.vue'
8
9
  import BlogHomeBanner from './BlogHomeBanner.vue'
9
10
  import BlogList from './BlogList.vue'
10
- import BlogComment from './BlogComment.vue'
11
-
12
11
  import BlogSidebar from './BlogSidebar.vue'
13
12
  import BlogImagePreview from './BlogImagePreview.vue'
14
13
  import BlogArticleAnalyze from './BlogArticleAnalyze.vue'
@@ -17,6 +16,11 @@ import BlogPopover from './BlogPopover.vue'
17
16
  import BlogFooter from './BlogFooter.vue'
18
17
  import BlogHomeHeaderAvatar from './BlogHomeHeaderAvatar.vue'
19
18
  import BlogBackToTop from './BlogBackToTop.vue'
19
+ import CommentGiscus from './CommentGiscus.vue'
20
+
21
+ import CommentArtalk from './CommentArtalk.vue'
22
+ import BlogButtonAfterArticle from './BlogButtonAfterArticle.vue'
23
+ import BlogCommentWrapper from './BlogCommentWrapper.vue'
20
24
 
21
25
  const { frontmatter } = useData()
22
26
  const layout = computed(() => frontmatter.value.layout)
@@ -25,14 +29,20 @@ const { Layout } = Theme
25
29
 
26
30
  // oh-my-live2d 扩展
27
31
  useOml2d()
32
+ // 切换深色模式过渡
33
+ // https://vitepress.dev/zh/guide/extending-default-theme#on-appearance-toggle
34
+ useDarkTransition()
35
+ const openTransition = useDarkTransitionConfig()
28
36
  </script>
29
37
 
30
38
  <template>
31
- <Layout>
39
+ <Layout :class="{ 'blog-theme-layout': openTransition }">
32
40
  <template #layout-top>
33
41
  <slot name="layout-top" />
34
- <BlogAlert />
35
- <BlogPopover />
42
+ <ClientOnly>
43
+ <BlogAlert />
44
+ <BlogPopover />
45
+ </ClientOnly>
36
46
  </template>
37
47
 
38
48
  <template #doc-before>
@@ -74,8 +84,12 @@ useOml2d()
74
84
  <slot name="doc-after" />
75
85
  <!-- 评论 -->
76
86
  <ClientOnly>
87
+ <BlogButtonAfterArticle />
77
88
  <BlogBackToTop />
78
- <BlogComment />
89
+ <BlogCommentWrapper>
90
+ <CommentArtalk />
91
+ <CommentGiscus />
92
+ </BlogCommentWrapper>
79
93
  </ClientOnly>
80
94
  </template>
81
95
  <template #layout-bottom>
@@ -191,6 +205,7 @@ useOml2d()
191
205
  .blog-list-wrapper {
192
206
  width: 100%;
193
207
  }
208
+
194
209
  .blog-info-wrapper {
195
210
  margin-left: 16px;
196
211
  position: sticky;
@@ -216,3 +231,7 @@ useOml2d()
216
231
  }
217
232
  }
218
233
  </style>
234
+
235
+ <style>
236
+ @import url(./../styles/dark-transition.css);
237
+ </style>
@@ -0,0 +1,122 @@
1
+ <script lang="ts" setup>
2
+ import { ElButton } from 'element-plus'
3
+ import { computed, ref, watch } from 'vue'
4
+ import { useData } from 'vitepress'
5
+ import { useBlogConfig } from '../composables/config/blog'
6
+ import { aliPaySVG, weChatPaySVG } from '../constants/svg'
7
+
8
+ const { buttonAfterArticle: _buttonAfterArticle } = useBlogConfig()
9
+ const { frontmatter } = useData()
10
+ const frontmatterConfig = computed(() => frontmatter.value.buttonAfterArticle)
11
+
12
+ const buttonAfterArticleConfig = computed(() => {
13
+ if (frontmatterConfig.value === false || (!frontmatterConfig.value && !_buttonAfterArticle)) {
14
+ return false
15
+ }
16
+
17
+ return { ..._buttonAfterArticle, ...frontmatterConfig.value }
18
+ })
19
+
20
+ const showContent = ref(false)
21
+
22
+ watch(buttonAfterArticleConfig, () => {
23
+ showContent.value = !!buttonAfterArticleConfig.value?.expand
24
+ }, {
25
+ immediate: true
26
+ })
27
+
28
+ const svg = computed(() => {
29
+ const icon = buttonAfterArticleConfig.value?.icon
30
+ if (icon === 'aliPay') {
31
+ return aliPaySVG
32
+ }
33
+ else if (icon === 'wechatPay') {
34
+ return weChatPaySVG
35
+ }
36
+ else {
37
+ return icon as string
38
+ }
39
+ })
40
+
41
+ function toggleContent() {
42
+ showContent.value = !showContent.value
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <div v-if="buttonAfterArticleConfig" class="appreciation-container">
48
+ <ElButton :size="buttonAfterArticleConfig.size || 'default'" class="content-button" :type="showContent ? 'danger' : 'primary'" @click="toggleContent">
49
+ <span class="content-icon" v-html="svg" />
50
+ {{ showContent ? buttonAfterArticleConfig.closeTitle : buttonAfterArticleConfig.openTitle }}
51
+ </ElButton>
52
+ <transition name="content">
53
+ <div v-if="showContent" class="content-container" v-html="buttonAfterArticleConfig.content" />
54
+ </transition>
55
+ </div>
56
+ </template>
57
+
58
+ <style scoped lang="scss">
59
+ .appreciation-container {
60
+ text-align: center;
61
+ padding: 20px;
62
+ font-size: 14px;
63
+ color: #606266;
64
+
65
+ :deep(.el-button.el-button--primary){
66
+ background-color: var(--vp-c-brand-2);
67
+ border-color: var(--vp-c-brand-2);
68
+ }
69
+ }
70
+
71
+ .content-container {
72
+ position: relative;
73
+ display: flex;
74
+ justify-content: center;
75
+ align-items: center;
76
+ margin-top: 20px;
77
+ :deep(img){
78
+ height: 260px;
79
+ }
80
+ }
81
+
82
+ .content-icon {
83
+ font-family: "iconfont" !important;
84
+ font-size: 16px;
85
+ margin-right: 8px;
86
+ font-style: normal;
87
+ -webkit-font-smoothing: antialiased;
88
+ -moz-osx-font-smoothing: grayscale;
89
+ }
90
+
91
+ /* 进入动画 */
92
+ .content-enter-active {
93
+ animation: fadeIn 0.5s ease forwards;
94
+ }
95
+
96
+ /* 离开动画 */
97
+ .content-leave-active {
98
+ animation: fadeOut 0.3s ease forwards;
99
+ }
100
+
101
+ /* 淡入 */
102
+ @keyframes fadeIn {
103
+ from {
104
+ opacity: 0;
105
+ }
106
+
107
+ to {
108
+ opacity: 1;
109
+ }
110
+ }
111
+
112
+ /* 淡出 */
113
+ @keyframes fadeOut {
114
+ from {
115
+ opacity: 1;
116
+ }
117
+
118
+ to {
119
+ opacity: 0;
120
+ }
121
+ }
122
+ </style>