Merge pull request #64 from pengzhanbo/rc50

RC-50
This commit is contained in:
pengzhanbo 2024-04-15 05:41:51 +08:00 committed by GitHub
commit c6c1d5406a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 2165 additions and 1270 deletions

View File

@ -15,7 +15,6 @@
"scss.validate": false,
"less.validate": false,
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.experimental.useFlatConfig": true,
"stylelint.packageManager": "pnpm",
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },

View File

@ -50,7 +50,7 @@ export const zhNotes = definePlumeNotesConfig({
icon: 'lucide:box',
collapsed: false,
dir: '功能',
items: ['代码复制', '内容搜索', '评论', '加密', '组件', '友情链接页', 'seo', 'sitemap'],
items: ['代码复制', '内容搜索', '评论', '加密', '组件', '文章水印', '友情链接页', 'seo', 'sitemap'],
},
{
text: '自定义',

View File

@ -41,6 +41,11 @@ export const theme: Theme = themePlume({
{ icon: 'xbox', link: 'https://pengzhanbo.cn' },
],
watermark: {
global: false,
content: 'VuePress Plume',
},
footer: { copyright: 'Copyright © 2021-present pengzhanbo' },
locales: {

View File

@ -16,11 +16,14 @@ Markdown 的目标是实现「易读易写」。
## 概述
不过最需要强调的便是它的可读性。一份使用 Markdown 格式撰写的文件应该可以直接以纯文字发佈并且看起来不会像是由许多标签或是格式指令所构成。Markdown 语法受到一些既有 text-to-HTML 格式的影响,包括 [Setext][1]、[atx][2]、[Textile][3]、[reStructuredText][4]、[Grutatext][5] 和 [EtText][6],然而最大灵感来源其实是纯文字的电子邮件格式。
不过最需要强调的便是它的可读性。一份使用 Markdown 格式撰写的文件应该可以直接以纯文字发佈并且看起来不会像是由许多标签或是格式指令所构成。Markdown 语法受到一些既有 text-to-HTML 格式的影响,
包括 [Setext][1]、[atx][2]、[Textile][3]、[reStructuredText][4]、[Grutatext][5] 和 [EtText][6],然而最大灵感来源其实是纯文字的电子邮件格式。
因此 Markdown 的语法全由标点符号所组成,并经过严谨慎选,是为了让它们看起来就像所要表达的意思。像是在文字两旁加上星号,看起来就像\*强调\*。Markdown 的列表看起来,嗯,就是列表。假如你有使用过电子邮件,引言写法看起来就真的像是引用一段文字。
Markdown 具有一系列衍生版本,用于扩展 Markdown 的功能 (如表格、脚注、内嵌 HTML 等等) ,这些功能原初的 Markdown 尚不具备,它们能让 Markdown 转换成更多的格式,例如 LaTeXDocbook。Markdown 增强版中比较有名的有 Markdown Extra、MultiMarkdown、 Maruku 等。这些衍生版本要么基于工具,如 Pandoc要么基于网站如 GitHub 和 Wikipedia在语法上基本兼容但在一些语法和渲染效果上有改动。
Markdown 具有一系列衍生版本,用于扩展 Markdown 的功能 (如表格、脚注、内嵌 HTML 等等)
这些功能原初的 Markdown 尚不具备,它们能让 Markdown 转换成更多的格式,例如 LaTeXDocbook。
Markdown 增强版中比较有名的有 Markdown Extra、MultiMarkdown、 Maruku 等。这些衍生版本要么基于工具,如 Pandoc要么基于网站如 GitHub 和 Wikipedia在语法上基本兼容但在一些语法和渲染效果上有改动。
## 用途
@ -36,7 +39,8 @@ Markdown 的语法简洁明了、学习容易,而且功能比纯文本更强
不在 Markdown 涵盖范围之外的标签,都可以直接在文件里面用 HTML 撰写。不需要额外标注这是 HTML 或是 Markdown只要直接加标签就可以了。
只有块元素 ── 比如 `<div>``<table>``<pre>``<p>` 等标签,必须在前后加上空行,以利与内容区隔。而且这些 (元素) 的开始与结尾标签,不可以用 tab 或是空白来缩进。Markdown 的解析器有智慧型判断,可以避免在块标签前后加上没有必要的 `<p>` 标签。
只有块元素 ── 比如 `<div>``<table>``<pre>``<p>` 等标签,必须在前后加上空行,以利与内容区隔。
而且这些 (元素) 的开始与结尾标签,不可以用 tab 或是空白来缩进。Markdown 的解析器有智慧型判断,可以避免在块标签前后加上没有必要的 `<p>` 标签。
举例来说,在 Markdown 文件里加上一段 HTML 表格:
@ -98,7 +102,9 @@ Markdown 将会把它转换为:
4 &lt; 5
```
不过需要注意的是code 范围内,不论是行内还是块, `<``&` 两个符号都*一定*会被转换成 HTML 实体,这项特性让你可以很容易地用 Markdown 写 HTML code (和 HTML 相对而言, HTML 语法中,你要把所有的 `<``&` 都转换为 HTML 实体,才能在 HTML 文件里面写出 HTML code。)
不过需要注意的是code 范围内,不论是行内还是块, `<``&` 两个符号都*一定*会被转换成 HTML 实体,
这项特性让你可以很容易地用 Markdown 写 HTML code (和 HTML 相对而言, HTML 语法中,
你要把所有的 `<``&` 都转换为 HTML 实体,才能在 HTML 文件里面写出 HTML code。)
---
@ -108,7 +114,9 @@ Markdown 将会把它转换为:
一个段落是由一个以上相连接的行句组成,而一个以上的空行则会切分出不同的段落 (空行的定义是显示上看起来像是空行,便会被视为空行。比方说,若某一行只包含空白和 tab则该行也会被视为空行) ,一般的段落不需要用空白或断行缩进。
「一个以上相连接的行句组成」这句话其实暗示了 Markdown 允许段落内的强迫断行,这个特性和其他大部分的 text-to-HTML 格式不一样 (包括 MovableType 的「Convert Line Breaks」选项) ,其它的格式会把每个断行都转成 `<br />` 标签。
「一个以上相连接的行句组成」这句话其实暗示了 Markdown 允许段落内的强迫断行,
这个特性和其他大部分的 text-to-HTML 格式不一样 (包括 MovableType 的「Convert Line Breaks」选项)
其它的格式会把每个断行都转成 `<br />` 标签。
<!-- markdownlint-disable MD038 -->

View File

@ -0,0 +1,108 @@
---
title: 全屏水印
author: Plume Theme
createTime: 2024/04/10 20:28:18
permalink: /article/97s6ha1e/
watermark:
fullPage: true
width: 150
---
## 概述
不过最需要强调的便是它的可读性。一份使用 Markdown 格式撰写的文件应该可以直接以纯文字发佈并且看起来不会像是由许多标签或是格式指令所构成。Markdown 语法受到一些既有 text-to-HTML 格式的影响,
包括 [Setext][1]、[atx][2]、[Textile][3]、[reStructuredText][4]、[Grutatext][5] 和 [EtText][6],然而最大灵感来源其实是纯文字的电子邮件格式。
因此 Markdown 的语法全由标点符号所组成,并经过严谨慎选,是为了让它们看起来就像所要表达的意思。像是在文字两旁加上星号,看起来就像\*强调\*。Markdown 的列表看起来,嗯,就是列表。假如你有使用过电子邮件,引言写法看起来就真的像是引用一段文字。
Markdown 具有一系列衍生版本,用于扩展 Markdown 的功能 (如表格、脚注、内嵌 HTML 等等)
这些功能原初的 Markdown 尚不具备,它们能让 Markdown 转换成更多的格式,例如 LaTeXDocbook。
Markdown 增强版中比较有名的有 Markdown Extra、MultiMarkdown、 Maruku 等。这些衍生版本要么基于工具,如 Pandoc要么基于网站如 GitHub 和 Wikipedia在语法上基本兼容但在一些语法和渲染效果上有改动。
## 用途
Markdown 的语法有个主要的目的: 用来作为一种网络内容的*写作*用语言。Markdown 的重点在于它能让文件更容易阅读、编写。因此Markdown 的格式语法只涵盖纯文字可以涵盖的范围。
Markdown 的语法简洁明了、学习容易,而且功能比纯文本更强,因此有很多人用它写博客。世界上最流行的博客平台 WordPress 能很好的支持 Markdown。
用于编写说明文档,并且以 `README.md` 的文件名保存在软件的目录下面。
除此之外,我们还可以快速将 Markdown 转化为演讲 PPT、Word 产品文档、LaTex 论文甚至是用非常少量的代码完成最小可用原型。在数据科学领域Markdown 已经广泛使用,极大地推进了动态可重复性研究的历史进程。
### 行内 HTML
不在 Markdown 涵盖范围之外的标签,都可以直接在文件里面用 HTML 撰写。不需要额外标注这是 HTML 或是 Markdown只要直接加标签就可以了。
只有块元素 ── 比如 `<div>``<table>``<pre>``<p>` 等标签,必须在前后加上空行,以利与内容区隔。
而且这些 (元素) 的开始与结尾标签,不可以用 tab 或是空白来缩进。Markdown 的解析器有智慧型判断,可以避免在块标签前后加上没有必要的 `<p>` 标签。
举例来说,在 Markdown 文件里加上一段 HTML 表格:
```md
This is a regular paragraph.
<table>
<tr>
<td>Foo</td>
</tr>
</table>
This is another regular paragraph.
```
请注意Markdown 语法在 HTML 块标签中将不会被进行处理。例如,你无法在 HTML 块内使用 Markdown 形式的 `*强调*`
### 特殊字元自动转换
在 HTML 文件中,有两个字元需要特殊处理: `<``&``<` 符号用于起始标签,`&` 符号则用于标记 HTML 实体,如果你只是想要使用这些符号,你必须要使用实体的形式,像是 `&lt;``&amp;`
`&` 符号其实很容易让写作网络文件的人感到困扰如果你要打「AT&T」 ,你必须要写成「`AT&amp;T`」 ,还得转换网址内的 `&` 符号,如果你要链接到 `http://images.google.com/images?num=30&q=larry+bird`
你必须要把网址转成:
```html
http://images.google.com/images?num=30&amp;q=larry+bird
```
才能放到链接标签的 `href` 属性里。不用说也知道这很容易忘记,这也可能是 HTML 标准检查所检查到的错误中,数量最多的。
Markdown 允许你直接使用这些符号,但是你要小心跳脱字元的使用,如果你是在 HTML 实体中使用 `&` 符号的话,它不会被转换,而在其它情形下,它则会被转换成 `&amp;`。所以你如果要在文件中插入一个著作权的符号,你可以这样写:
```md
&copy;
```
Markdown 将不会对这段文字做修改,但是如果你这样写:
```md
AT&T
```
Markdown 就会将它转为:
```html
AT&amp;T
```
类似的状况也会发生在 `<` 符号上,因为 Markdown 支持 [行内 HTML](#行内-html) ,如果你是使用 `<` 符号作为 HTML 标签使用,那 Markdown 也不会对它做任何转换,但是如果你是写:
```md
4 < 5
```
Markdown 将会把它转换为:
```html
4 &lt; 5
```
不过需要注意的是code 范围内,不论是行内还是块, `<``&` 两个符号都*一定*会被转换成 HTML 实体,
这项特性让你可以很容易地用 Markdown 写 HTML code (和 HTML 相对而言, HTML 语法中,
你要把所有的 `<``&` 都转换为 HTML 实体,才能在 HTML 文件里面写出 HTML code。)
[1]: http://docutils.sourceforge.net/mirror/setext.html
[2]: http://www.aaronsw.com/2002/atx/
[3]: http://textism.com/tools/textile/
[4]: http://docutils.sourceforge.net/rst.html
[5]: http://www.triptico.com/software/grutatxt.html
[6]: http://ettext.taint.org/doc/

View File

@ -0,0 +1,108 @@
---
title: 内容水印
author: Plume Theme
createTime: 2024/04/10 20:28:32
permalink: /article/2z59hh8g/
watermark:
fullPage: false
width: 150
---
## 概述
不过最需要强调的便是它的可读性。一份使用 Markdown 格式撰写的文件应该可以直接以纯文字发佈并且看起来不会像是由许多标签或是格式指令所构成。Markdown 语法受到一些既有 text-to-HTML 格式的影响,
包括 [Setext][1]、[atx][2]、[Textile][3]、[reStructuredText][4]、[Grutatext][5] 和 [EtText][6],然而最大灵感来源其实是纯文字的电子邮件格式。
因此 Markdown 的语法全由标点符号所组成,并经过严谨慎选,是为了让它们看起来就像所要表达的意思。像是在文字两旁加上星号,看起来就像\*强调\*。Markdown 的列表看起来,嗯,就是列表。假如你有使用过电子邮件,引言写法看起来就真的像是引用一段文字。
Markdown 具有一系列衍生版本,用于扩展 Markdown 的功能 (如表格、脚注、内嵌 HTML 等等)
这些功能原初的 Markdown 尚不具备,它们能让 Markdown 转换成更多的格式,例如 LaTeXDocbook。
Markdown 增强版中比较有名的有 Markdown Extra、MultiMarkdown、 Maruku 等。这些衍生版本要么基于工具,如 Pandoc要么基于网站如 GitHub 和 Wikipedia在语法上基本兼容但在一些语法和渲染效果上有改动。
## 用途
Markdown 的语法有个主要的目的: 用来作为一种网络内容的*写作*用语言。Markdown 的重点在于它能让文件更容易阅读、编写。因此Markdown 的格式语法只涵盖纯文字可以涵盖的范围。
Markdown 的语法简洁明了、学习容易,而且功能比纯文本更强,因此有很多人用它写博客。世界上最流行的博客平台 WordPress 能很好的支持 Markdown。
用于编写说明文档,并且以 `README.md` 的文件名保存在软件的目录下面。
除此之外,我们还可以快速将 Markdown 转化为演讲 PPT、Word 产品文档、LaTex 论文甚至是用非常少量的代码完成最小可用原型。在数据科学领域Markdown 已经广泛使用,极大地推进了动态可重复性研究的历史进程。
### 行内 HTML
不在 Markdown 涵盖范围之外的标签,都可以直接在文件里面用 HTML 撰写。不需要额外标注这是 HTML 或是 Markdown只要直接加标签就可以了。
只有块元素 ── 比如 `<div>``<table>``<pre>``<p>` 等标签,必须在前后加上空行,以利与内容区隔。
而且这些 (元素) 的开始与结尾标签,不可以用 tab 或是空白来缩进。Markdown 的解析器有智慧型判断,可以避免在块标签前后加上没有必要的 `<p>` 标签。
举例来说,在 Markdown 文件里加上一段 HTML 表格:
```md
This is a regular paragraph.
<table>
<tr>
<td>Foo</td>
</tr>
</table>
This is another regular paragraph.
```
请注意Markdown 语法在 HTML 块标签中将不会被进行处理。例如,你无法在 HTML 块内使用 Markdown 形式的 `*强调*`
### 特殊字元自动转换
在 HTML 文件中,有两个字元需要特殊处理: `<``&``<` 符号用于起始标签,`&` 符号则用于标记 HTML 实体,如果你只是想要使用这些符号,你必须要使用实体的形式,像是 `&lt;``&amp;`
`&` 符号其实很容易让写作网络文件的人感到困扰如果你要打「AT&T」 ,你必须要写成「`AT&amp;T`」 ,还得转换网址内的 `&` 符号,如果你要链接到 `http://images.google.com/images?num=30&q=larry+bird`
你必须要把网址转成:
```html
http://images.google.com/images?num=30&amp;q=larry+bird
```
才能放到链接标签的 `href` 属性里。不用说也知道这很容易忘记,这也可能是 HTML 标准检查所检查到的错误中,数量最多的。
Markdown 允许你直接使用这些符号,但是你要小心跳脱字元的使用,如果你是在 HTML 实体中使用 `&` 符号的话,它不会被转换,而在其它情形下,它则会被转换成 `&amp;`。所以你如果要在文件中插入一个著作权的符号,你可以这样写:
```md
&copy;
```
Markdown 将不会对这段文字做修改,但是如果你这样写:
```md
AT&T
```
Markdown 就会将它转为:
```html
AT&amp;T
```
类似的状况也会发生在 `<` 符号上,因为 Markdown 支持 [行内 HTML](#行内-html) ,如果你是使用 `<` 符号作为 HTML 标签使用,那 Markdown 也不会对它做任何转换,但是如果你是写:
```md
4 < 5
```
Markdown 将会把它转换为:
```html
4 &lt; 5
```
不过需要注意的是code 范围内,不论是行内还是块, `<``&` 两个符号都*一定*会被转换成 HTML 实体,
这项特性让你可以很容易地用 Markdown 写 HTML code (和 HTML 相对而言, HTML 语法中,
你要把所有的 `<``&` 都转换为 HTML 实体,才能在 HTML 文件里面写出 HTML code。)
[1]: http://docutils.sourceforge.net/mirror/setext.html
[2]: http://www.aaronsw.com/2002/atx/
[3]: http://textism.com/tools/textile/
[4]: http://docutils.sourceforge.net/rst.html
[5]: http://www.triptico.com/software/grutatxt.html
[6]: http://ettext.taint.org/doc/

View File

@ -0,0 +1,108 @@
---
title: 图片水印
author: Plume Theme
createTime: 2024/04/11 06:07:50
permalink: /article/i4cuuonn/
watermark:
fullPage: true
image: /plume.png
---
## 概述
不过最需要强调的便是它的可读性。一份使用 Markdown 格式撰写的文件应该可以直接以纯文字发佈并且看起来不会像是由许多标签或是格式指令所构成。Markdown 语法受到一些既有 text-to-HTML 格式的影响,
包括 [Setext][1]、[atx][2]、[Textile][3]、[reStructuredText][4]、[Grutatext][5] 和 [EtText][6],然而最大灵感来源其实是纯文字的电子邮件格式。
因此 Markdown 的语法全由标点符号所组成,并经过严谨慎选,是为了让它们看起来就像所要表达的意思。像是在文字两旁加上星号,看起来就像\*强调\*。Markdown 的列表看起来,嗯,就是列表。假如你有使用过电子邮件,引言写法看起来就真的像是引用一段文字。
Markdown 具有一系列衍生版本,用于扩展 Markdown 的功能 (如表格、脚注、内嵌 HTML 等等)
这些功能原初的 Markdown 尚不具备,它们能让 Markdown 转换成更多的格式,例如 LaTeXDocbook。
Markdown 增强版中比较有名的有 Markdown Extra、MultiMarkdown、 Maruku 等。这些衍生版本要么基于工具,如 Pandoc要么基于网站如 GitHub 和 Wikipedia在语法上基本兼容但在一些语法和渲染效果上有改动。
## 用途
Markdown 的语法有个主要的目的: 用来作为一种网络内容的*写作*用语言。Markdown 的重点在于它能让文件更容易阅读、编写。因此Markdown 的格式语法只涵盖纯文字可以涵盖的范围。
Markdown 的语法简洁明了、学习容易,而且功能比纯文本更强,因此有很多人用它写博客。世界上最流行的博客平台 WordPress 能很好的支持 Markdown。
用于编写说明文档,并且以 `README.md` 的文件名保存在软件的目录下面。
除此之外,我们还可以快速将 Markdown 转化为演讲 PPT、Word 产品文档、LaTex 论文甚至是用非常少量的代码完成最小可用原型。在数据科学领域Markdown 已经广泛使用,极大地推进了动态可重复性研究的历史进程。
### 行内 HTML
不在 Markdown 涵盖范围之外的标签,都可以直接在文件里面用 HTML 撰写。不需要额外标注这是 HTML 或是 Markdown只要直接加标签就可以了。
只有块元素 ── 比如 `<div>``<table>``<pre>``<p>` 等标签,必须在前后加上空行,以利与内容区隔。
而且这些 (元素) 的开始与结尾标签,不可以用 tab 或是空白来缩进。Markdown 的解析器有智慧型判断,可以避免在块标签前后加上没有必要的 `<p>` 标签。
举例来说,在 Markdown 文件里加上一段 HTML 表格:
```md
This is a regular paragraph.
<table>
<tr>
<td>Foo</td>
</tr>
</table>
This is another regular paragraph.
```
请注意Markdown 语法在 HTML 块标签中将不会被进行处理。例如,你无法在 HTML 块内使用 Markdown 形式的 `*强调*`
### 特殊字元自动转换
在 HTML 文件中,有两个字元需要特殊处理: `<``&``<` 符号用于起始标签,`&` 符号则用于标记 HTML 实体,如果你只是想要使用这些符号,你必须要使用实体的形式,像是 `&lt;``&amp;`
`&` 符号其实很容易让写作网络文件的人感到困扰如果你要打「AT&T」 ,你必须要写成「`AT&amp;T`」 ,还得转换网址内的 `&` 符号,如果你要链接到 `http://images.google.com/images?num=30&q=larry+bird`
你必须要把网址转成:
```html
http://images.google.com/images?num=30&amp;q=larry+bird
```
才能放到链接标签的 `href` 属性里。不用说也知道这很容易忘记,这也可能是 HTML 标准检查所检查到的错误中,数量最多的。
Markdown 允许你直接使用这些符号,但是你要小心跳脱字元的使用,如果你是在 HTML 实体中使用 `&` 符号的话,它不会被转换,而在其它情形下,它则会被转换成 `&amp;`。所以你如果要在文件中插入一个著作权的符号,你可以这样写:
```md
&copy;
```
Markdown 将不会对这段文字做修改,但是如果你这样写:
```md
AT&T
```
Markdown 就会将它转为:
```html
AT&amp;T
```
类似的状况也会发生在 `<` 符号上,因为 Markdown 支持 [行内 HTML](#行内-html) ,如果你是使用 `<` 符号作为 HTML 标签使用,那 Markdown 也不会对它做任何转换,但是如果你是写:
```md
4 < 5
```
Markdown 将会把它转换为:
```html
4 &lt; 5
```
不过需要注意的是code 范围内,不论是行内还是块, `<``&` 两个符号都*一定*会被转换成 HTML 实体,
这项特性让你可以很容易地用 Markdown 写 HTML code (和 HTML 相对而言, HTML 语法中,
你要把所有的 `<``&` 都转换为 HTML 实体,才能在 HTML 文件里面写出 HTML code。)
[1]: http://docutils.sourceforge.net/mirror/setext.html
[2]: http://www.aaronsw.com/2002/atx/
[3]: http://textism.com/tools/textile/
[4]: http://docutils.sourceforge.net/rst.html
[5]: http://www.triptico.com/software/grutatxt.html
[6]: http://ettext.taint.org/doc/

View File

@ -8,7 +8,7 @@ config:
background: filter-blur
hero:
name: Theme Plume
tagline: Vuepress Next Theme
tagline: VuePress Next Theme
text: 一个简约的,功能丰富的 vuepress 文档&博客 主题
actions:
-

View File

@ -124,3 +124,26 @@ permalink: /config/frontmatter/basic/
- 默认值: `true`
当前文章是否 显示 文章编辑 按钮。
### watermark
- 类型: `boolean | WatermarkConfig`
- 默认值: `undefined` 主题不启用水印,或不启用全局水印时,默认值为 `false`,启用全局水印则为 `true`
配置当前文章 水印。
```ts
interface WatermarkConfig {
content?: string // 水印文字内容,可传入 html 内容
textColor?: string // 水印文本颜色
image?: string // 水印图片路径,优先于 content
opacity?: number // 水印透明度
rotate?: number // 水印旋转角度
width?: number // 水印宽度
height?: number // 水印高度
gapX?: number // 水印横向间距
gapY?: number // 水印纵向间距
fullPage?: boolean // 是否全屏
onlyPrint?: boolean // 是否仅在打印时显示
}
```

View File

@ -501,3 +501,78 @@ interface NotFound {
linkText?: string
}
```
### watermark
- 类型: `boolean | WatermarkOptions`
- 默认值: `false`
- 详情: 文章水印配置
```ts
interface WatermarkOptions {
/**
* 是否全局启用, 在不全局启用时,可以通过 `frontmatter.watermark` 为当前页面启用
* @default false
*/
global?: boolean
/**
* 非全局启用时,可以通过该字段根据文件路径或页面链接 进行匹配, 来启用水印。
* 以 `^` 的将被认为为类似于正则表达式进行匹配。
*/
matches?: string | string[]
/**
* 水印之间的水平间隔
* @default 0
*/
gapX?: number
/**
* 水印之间的垂直间隔
* @default 0
*/
gapY?: number
/**
* 图片水印的内容,如果与 content 同时传入,优先使用图片水印
*/
image?: PlumeThemeImage
/**
* 水印宽度
* @default 100
*/
width?: number
/**
* 水印高度
* @default 100
*/
height?: number
/**
* 水印旋转角度
* @default -22
*/
rotate?: number
/**
* 水印的内容,如果与 image 同时传入,优先使用图片水印
*/
content?: string
/**
* 水印是否全屏显示
*/
fullPage?: boolean
/**
* 水印透明度
* @default 0.1
*/
opacity?: number
/**
* 水印的文本颜色
*/
textColor?: string | { dark: string, light: string }
/**
* 是否只在打印时显示
* @default false
*/
onlyPrint?: boolean
}
```

View File

@ -0,0 +1,173 @@
---
title: 文章水印
author: pengzhanbo
icon: material-symbols-light:branding-watermark-outline
createTime: 2024/04/10 20:14:57
permalink: /guide/features/watermark/
---
## 概述
主题支持在文章中添加水印。支持 全屏水印 和 内容水印,同时还支持 图片水印 和 文字水印 。
水印 仅在 文章页面 生效。如首页、博客页等其他页面不会注入水印。
## 启用水印
主题默认不启用水印功能。你需要在主题配置中开启。
::: code-tabs
@tab .vuepress/config.ts
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
// watermark: true, // 使用默认配置的水印
watermark: {
global: true, // 全局开启水印
image: '/images/watermark.png', // 水印图片
content: 'vuepress plume', // 水印内容, 如果配置了 image, 则优先使用 image
opacity: 0.1, // 透明度
rotate: -22, // 旋转角度
width: 100, // 水印宽度
height: 100, // 水印高度
textColor: '#fff', // 文字颜色
fullPage: true, // 是否全屏, 非全屏时水印仅覆盖文章内容
gapX: 20, // 水印横向间距
gapY: 20, // 水印纵向间距
onlyPrint: true, // 只在打印时生效
matches: ['/article/xxx', '^/note/', 'notes/guide/xx.md'], // 非全局启用时,匹配页面路径或文件路径来启用水印
}
})
})
```
:::
### 全局启用
`watermark` 配置为 `true` 时, 主题全局开启水印。还可以通过 `watermark.global` 配置是否开启全局水印。
### 部分页面启用
`watermark.global``false` 时,主题虽然启用了水印功能,但是需要自行控制哪些页面显示水印。
主题提供了两种方式来控制水印的显示:
#### watermark.matches
```ts
export default defineUserConfig({
theme: plumeTheme({
// watermark: true, // 使用默认配置的水印
watermark: {
global: false,
matches: [
// 可以是 md 文件的相对路径
'notes/guide/xx.md',
// 可以是 文件夹的路径
'/notes/vuepress-theme-plume/',
// 可以是 访问地址的请求路径,对该访问路径下所有文章 都生效
'/vuepress-theme-plume/',
// 可以是 具体的某个页面的请求路径
'/article/f8dnci3/',
// 如果是 `^` 开头,则匹配该正则表达式的页面
'^/(a|b)/',
],
}
})
})
```
#### frontmatter.watermark
在 md 文件中添加 `frontmatter.watermark``true`
```md
---
watermark: true
---
```
还可以个性化配置当前页面的水印:
```md
---
watermark:
content: My Custom Content
opacity: 0.2
rotate: 45
---
```
## 图片水印
主题支持使用 图片 作为水印。
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
watermark: {
image: '/images/watermark.png', // 水印图片
width: 100, // 水印宽度
height: 100, // 水印高度
}
})
})
```
也可以在 md 文件中添加配置,为当前页面设置水印:
```md
---
watermark:
image: /images/watermark.png
width: 100
height: 100
---
```
### 示例
[图片水印](/article/i4cuuonn/)
## 文字水印
主题支持使用 图片 作为水印。
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
watermark: {
content: '自定义文字',
textColor: '#fff', // 文字颜色
}
})
})
```
也可以在 md 文件中添加配置,为当前页面设置水印:
```md
---
watermark:
content: 自定义文字
textColor: #fff
---
```
当同时设置了 `image``content` 时, `image` 优先于 `content`
## 示例
- [内容水印](/article/2z59hh8g/)
- [全屏水印](/article/97s6ha1e/)

View File

@ -12,7 +12,7 @@
"vuepress": "2.0.0-rc.9"
},
"dependencies": {
"@iconify/json": "^2.2.196",
"@iconify/json": "^2.2.200",
"@vuepress/bundler-vite": "2.0.0-rc.9",
"anywhere": "^1.6.0",
"chart.js": "^4.4.2",

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.0.0-rc.49",
"private": true,
"packageManager": "pnpm@8.15.5",
"packageManager": "pnpm@8.15.7",
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
"license": "MIT",
"keywords": [
@ -39,10 +39,10 @@
"release:version": "bumpp package.json plugins/*/package.json theme/package.json --execute=\"pnpm release:changelog\" --commit \"build: publish v%s\" --all --tag --push"
},
"devDependencies": {
"@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^19.1.0",
"@pengzhanbo/eslint-config-vue": "^1.7.0",
"@pengzhanbo/stylelint-config": "^1.7.0",
"@commitlint/cli": "^19.2.2",
"@commitlint/config-conventional": "^19.2.2",
"@pengzhanbo/eslint-config-vue": "^1.8.1",
"@pengzhanbo/stylelint-config": "^1.8.1",
"@types/lodash.merge": "^4.6.9",
"@types/node": "20.9.1",
"@types/webpack-env": "^1.18.4",
@ -52,14 +52,14 @@
"conventional-changelog-cli": "^4.1.0",
"cpx2": "^7.0.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0",
"eslint": "^9.0.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"rimraf": "^5.0.5",
"stylelint": "^16.3.1",
"tsconfig-vuepress": "^4.5.0",
"typescript": "^5.4.3",
"vite": "^5.2.7"
"typescript": "^5.4.5",
"vite": "^5.2.8"
},
"pnpm": {
"patchedDependencies": {

View File

@ -49,7 +49,7 @@
"markdown-it-container": "^4.0.0"
},
"devDependencies": {
"@types/markdown-it": "^13.0.7"
"@types/markdown-it": "^14.0.1"
},
"publishConfig": {
"access": "public"

View File

@ -50,12 +50,12 @@
"@vueuse/core": "^10.9.0",
"local-pkg": "^0.5.0",
"markdown-it-container": "^4.0.0",
"nanoid": "^5.0.6",
"nanoid": "^5.0.7",
"vue": "^3.4.21"
},
"devDependencies": {
"@iconify/json": "^2.2.196",
"@types/markdown-it": "^13.0.7"
"@iconify/json": "^2.2.200",
"@types/markdown-it": "^14.0.1"
},
"publishConfig": {
"access": "public"

View File

@ -52,11 +52,11 @@
"dotenv": "^16.4.5",
"esbuild": "^0.20.2",
"execa": "^8.0.1",
"netlify-cli": "^17.21.1",
"netlify-cli": "^17.22.1",
"portfinder": "^1.0.32"
},
"devDependencies": {
"@types/node": "^20.12.2"
"@types/node": "^20.12.7"
},
"publishConfig": {
"access": "public"

View File

@ -40,14 +40,14 @@
"vuepress": "2.0.0-rc.9"
},
"dependencies": {
"@vuepress/helper": "2.0.0-rc.21",
"@vuepress/helper": "2.0.0-rc.24",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"chokidar": "^3.6.0",
"focus-trap": "^7.5.4",
"mark.js": "^8.11.1",
"minisearch": "^6.3.0",
"p-map": "^7.0.1",
"p-map": "^7.0.2",
"vue": "^3.4.21"
},
"publishConfig": {

View File

@ -36,15 +36,15 @@
"vuepress": "2.0.0-rc.9"
},
"dependencies": {
"@shikijs/transformers": "^1.2.2",
"@shikijs/twoslash": "^1.2.2",
"@shikijs/transformers": "^1.3.0",
"@shikijs/twoslash": "^1.3.0",
"@types/hast": "^3.0.4",
"floating-vue": "^5.2.2",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-gfm": "^3.0.0",
"mdast-util-to-hast": "^13.1.0",
"nanoid": "^5.0.6",
"shiki": "^1.2.2",
"nanoid": "^5.0.7",
"shiki": "^1.3.0",
"twoslash": "^0.2.5",
"twoslash-vue": "^0.2.5"
},

1673
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -65,26 +65,27 @@
"@vuepress-plume/plugin-notes-data": "workspace:*",
"@vuepress-plume/plugin-search": "workspace:*",
"@vuepress-plume/plugin-shikiji": "workspace:*",
"@vuepress/helper": "2.0.0-rc.24",
"@vuepress/plugin-active-header-links": "2.0.0-rc.21",
"@vuepress/plugin-comment": "2.0.0-rc.21",
"@vuepress/plugin-comment": "2.0.0-rc.24",
"@vuepress/plugin-container": "2.0.0-rc.21",
"@vuepress/plugin-docsearch": "2.0.0-rc.21",
"@vuepress/plugin-external-link-icon": "2.0.0-rc.21",
"@vuepress/plugin-git": "2.0.0-rc.21",
"@vuepress/plugin-medium-zoom": "2.0.0-rc.21",
"@vuepress/plugin-docsearch": "2.0.0-rc.24",
"@vuepress/plugin-external-link-icon": "2.0.0-rc.24",
"@vuepress/plugin-git": "2.0.0-rc.22",
"@vuepress/plugin-medium-zoom": "2.0.0-rc.24",
"@vuepress/plugin-nprogress": "2.0.0-rc.21",
"@vuepress/plugin-palette": "2.0.0-rc.21",
"@vuepress/plugin-reading-time": "2.0.0-rc.21",
"@vuepress/plugin-seo": "2.0.0-rc.21",
"@vuepress/plugin-sitemap": "2.0.0-rc.21",
"@vuepress/plugin-reading-time": "2.0.0-rc.24",
"@vuepress/plugin-seo": "2.0.0-rc.24",
"@vuepress/plugin-sitemap": "2.0.0-rc.24",
"@vuepress/plugin-theme-data": "2.0.0-rc.21",
"@vuepress/plugin-toc": "2.0.0-rc.21",
"@vuepress/plugin-toc": "2.0.0-rc.24",
"@vueuse/core": "^10.9.0",
"bcrypt-ts": "^5.0.2",
"date-fns": "^3.6.0",
"katex": "^0.16.10",
"lodash.merge": "^4.6.2",
"nanoid": "^5.0.6",
"nanoid": "^5.0.7",
"vue": "^3.4.21",
"vue-router": "4.3.0",
"vuepress-plugin-md-enhance": "2.0.0-rc.32",

View File

@ -62,6 +62,16 @@ const showBlogExtract = computed(() => {
<p class="desc">
{{ avatar.description }}
</p>
<div class="avatar-info">
<div v-if="avatar.location" class="avatar-location">
<span class="vpi-location" />
<p v-if="avatar.location" v-html="avatar.location" />
</div>
<div v-if="avatar.organization" class="avatar-organization">
<span class="vpi-organization" />
<p v-if="avatar.organization" v-html="avatar.organization" />
</div>
</div>
</div>
</div>
<div v-if="hasBlogExtract" class="blog-nav" :class="{ 'no-avatar': !avatar }">
@ -87,7 +97,7 @@ const showBlogExtract = computed(() => {
bottom: 30%;
z-index: calc(var(--vp-z-index-nav) - 1);
display: block;
padding: 4px 10px;
padding: 6px 10px;
cursor: pointer;
background-color: var(--vp-c-bg);
border: solid 1px var(--vp-c-divider);
@ -212,4 +222,26 @@ const showBlogExtract = computed(() => {
height: 1em;
margin-right: 4px;
}
.avatar-info {
display: flex;
flex-wrap: wrap;
gap: 0 20px;
align-items: center;
}
.avatar-location,
.avatar-organization {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: var(--vp-c-text-3);
transition: color var(--t-color);
}
.avatar-location p,
.avatar-organization p {
margin: 0 4px;
}
</style>

View File

@ -38,13 +38,11 @@ const avatar = computed(() => theme.value.avatar)
border-radius: 8px;
box-shadow: var(--vp-shadow-1);
transition: var(--t-color);
transition-property: background-color, color, box-shadow, transform;
transform: scale(1);
transition-property: background-color, color, box-shadow;
}
.avatar-profile:hover {
box-shadow: var(--vp-shadow-2);
transform: scale(1.002);
}
.avatar-profile img {

View File

@ -1,5 +1,6 @@
<script lang="ts" setup>
import { usePostListControl } from '../../composables/index.js'
import TransitionDrop from '../TransitionDrop.vue'
import PostItem from './PostItem.vue'
import Pagination from './Pagination.vue'
@ -18,11 +19,14 @@ const {
<template>
<div class="post-list">
<PostItem
v-for="post in postList"
:key="post.path"
:post="post"
/>
<template v-for="(post, index) in postList" :key="post.path">
<TransitionDrop appear :delay="index * 0.04">
<PostItem
:key="post.path"
:post="post"
/>
</TransitionDrop>
</template>
<Pagination
v-if="isPaginationEnabled"
:pagination="pagination"

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { usePageFrontmatter, withBase } from 'vuepress/client'
import { isLinkHttp } from 'vuepress/shared'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import VButton from '../VButton.vue'
import { useDarkMode } from '../../composables/index.js'
import { useHomeHeroFilterBackground } from '../../composables/home.js'
import type { PlumeThemeHomeFrontmatter, PlumeThemeHomeHero } from '../../../shared/index.js'
const props = defineProps<PlumeThemeHomeHero>()
@ -34,6 +35,9 @@ const heroBackground = computed(() => {
const hero = computed(() => props.hero ?? matter.value.hero ?? {})
const actions = computed(() => hero.value.actions ?? [])
const canvas = ref<HTMLCanvasElement>()
useHomeHeroFilterBackground(canvas, computed(() => props.background === 'filter-blur'))
</script>
<template>
@ -41,9 +45,7 @@ const actions = computed(() => hero.value.actions ?? [])
<div v-if="heroBackground" class="home-hero-bg" :style="heroBackground" />
<div v-if="background === 'filter-blur'" class="bg-filter">
<div class="g g-1" />
<div class="g g-2" />
<div class="g g-3" />
<canvas ref="canvas" width="32" height="32" />
</div>
<div class="container">
@ -181,52 +183,19 @@ const actions = computed(() => hero.value.actions ?? [])
height: calc(100% + var(--vp-footer-height, 0px));
}
.bg-filter::before {
.bg-filter::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
height: 20%;
content: "";
backdrop-filter: blur(150px);
transform: translate3d(0, 0, 0);
background: linear-gradient(var(--vp-c-bg) 0, transparent 100%);
}
.g {
position: absolute;
opacity: 0.5;
transform: translate3d(0, 0, 0);
}
.g-1 {
bottom: 100px;
left: 50%;
width: 714px;
height: 390px;
clip-path: polygon(0 10%, 30% 0, 100% 40%, 70% 100%, 20% 90%);
background: var(--vp-c-yellow-3);
transform: translate(-50%, 0);
}
.g-2 {
bottom: 0;
left: 30%;
width: 1000px;
height: 450px;
clip-path: polygon(10% 0, 100% 70%, 100% 100%, 20% 90%);
background: var(--vp-c-red-3);
transform: translate(-50%, 0);
}
.g-3 {
bottom: 0;
left: 70%;
width: 1000px;
height: 450px;
clip-path: polygon(80% 0, 100% 70%, 100% 100%, 20% 90%);
background: var(--vp-c-purple-3);
transform: translate(-50%, 0);
.bg-filter canvas {
width: 100%;
height: 100%;
}
/* =========== background filter end ======= */

View File

@ -11,6 +11,7 @@ import PageFooter from './PageFooter.vue'
import PageMeta from './PageMeta.vue'
import EncryptPage from './EncryptPage.vue'
import TransitionFadeSlideY from './TransitionFadeSlideY.vue'
import Watermark from './Watermark.vue'
const { hasSidebar, hasAside } = useSidebar()
const isDark = useDarkMode()
@ -56,8 +57,10 @@ onContentUpdated(() => zoom?.refresh())
<PageMeta />
<EncryptPage v-if="!isPageDecrypted" />
<template v-else>
<Content class="plume-content" />
<div style="position: relative;">
<Content class="plume-content" />
<Watermark />
</div>
<PageFooter />
<PageComment v-if="hasComments" :darkmode="isDark" />
</template>

View File

@ -4,12 +4,13 @@ import { ref, watch } from 'vue'
import { useSidebar } from '../composables/sidebar.js'
import { inBrowser } from '../utils/index.js'
import SidebarItem from './SidebarItem.vue'
import TransitionFadeSlideY from './TransitionFadeSlideY.vue'
const props = defineProps<{
open: boolean
}>()
const { sidebarGroups, hasSidebar } = useSidebar()
const { sidebarGroups, hasSidebar, sidebarKey } = useSidebar()
// a11y: focus Nav element when menu has opened
const navEl = ref<HTMLElement | null>(null)
@ -39,20 +40,23 @@ watch(
>
<div class="curtain" />
<nav
id="SidebarNav"
class="nav"
aria-labelledby="sidebar-aria-label"
tabindex="-1"
>
<span id="sidebar-aria-label" class="visually-hidden">
Sidebar Navigation
</span>
<TransitionFadeSlideY>
<nav
id="SidebarNav"
:key="sidebarKey"
class="nav"
aria-labelledby="sidebar-aria-label"
tabindex="-1"
>
<span id="sidebar-aria-label" class="visually-hidden">
Sidebar Navigation
</span>
<div v-for="item in sidebarGroups" :key="item.text" class="group">
<SidebarItem :item="item" :depth="0" />
</div>
</nav>
<div v-for="item in sidebarGroups" :key="item.text" class="group">
<SidebarItem :item="item" :depth="0" />
</div>
</nav>
</TransitionFadeSlideY>
</aside>
</Transition>
</template>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
interface Props {
delay?: number
duration?: number
appear?: boolean
}
const props = withDefaults(defineProps<Props>(), {
delay: 0,
duration: 0.25,
})
function setStyle(item: Element) {
const el = item as HTMLElement
el.style.transition = `transform ${props.duration}s ease-in-out ${props.delay}s, opacity ${props.duration}s ease-in-out ${props.delay}s`
el.style.transform = 'translateY(-20px)'
el.style.opacity = '0'
}
function unsetStyle(item: Element) {
const el = item as HTMLElement
el.style.transform = 'translateY(0)'
el.style.opacity = '1'
}
</script>
<template>
<Transition
name="drop"
:appear="appear"
@appear="setStyle"
@after-appear="unsetStyle"
@enter="setStyle"
@after-enter="unsetStyle"
@before-leave="setStyle"
>
<slot />
</Transition>
</template>

View File

@ -0,0 +1,105 @@
<script setup lang="ts">
import { useWaterMark } from '../composables/index.js'
const {
enableWatermark,
isFullPage,
svgElRef,
svgRect,
imageUrl,
content,
textColor,
rotateStyle,
imageBase64,
watermarkUrl,
onlyPrint,
} = useWaterMark()
</script>
<template>
<div
v-if="enableWatermark"
class="watermark-wrapper"
:class="{ full: isFullPage, print: onlyPrint }"
:style="{ backgroundImage: `url(${watermarkUrl})` }"
>
<div ref="svgElRef" class="watermark">
<svg
:viewBox="`0 0 ${svgRect.svgWidth} ${svgRect.svgHeight}`"
:width="svgRect.svgWidth"
:height="svgRect.svgHeight"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
:style="{
padding: `0 ${svgRect.gapX}px ${svgRect.gapY}px 0`,
opacity: svgRect.opacity,
}"
>
<image
v-if="imageUrl"
:href="imageBase64"
:xlink:href="imageBase64"
x="0"
y="0"
:width="svgRect.width"
:height="svgRect.height"
:style="rotateStyle"
/>
<foreignObject
v-else
x="0"
y="0"
:width="svgRect.width"
:height="svgRect.height"
>
<div
xmlns="http://www.w3.org/1999/xhtml"
:style="rotateStyle"
>
<p class="watermark-content" :style="{ color: textColor }" v-html="content" />
</div>
</foreignObject>
</svg>
</div>
</div>
</template>
<style scoped>
.watermark-wrapper {
position: absolute;
top: 0;
left: 0;
z-index: 19;
width: 100%;
height: 100%;
pointer-events: none;
background-color: transparent;
background-repeat: repeat;
}
.watermark-wrapper .watermark {
display: none;
}
.watermark-wrapper.full {
position: fixed;
top: var(--vp-nav-height);
z-index: 9999;
}
.watermark-wrapper .watermark-content {
display: inline-block;
margin: 0;
}
.watermark-wrapper.print {
display: none;
}
@media print {
.watermark-wrapper.print {
display: block;
}
}
</style>

View File

@ -0,0 +1,56 @@
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted } from 'vue'
import { useDarkMode } from './darkMode.js'
export function useHomeHeroFilterBackground(
canvas: Ref<HTMLCanvasElement | undefined>,
enable: Ref<boolean>,
) {
const isDark = useDarkMode()
let ctx: CanvasRenderingContext2D | null = null
let t = 0
let timer: number
const F = computed(() => isDark.value ? 32 : 220)
onMounted(() => {
if (canvas.value && enable.value) {
ctx = canvas.value.getContext('2d')!
timer && window.cancelAnimationFrame(timer)
run()
}
})
onUnmounted(() => {
timer && window.cancelAnimationFrame(timer)
})
function run() {
for (let x = 0; x <= 35; x++) {
for (let y = 0; y <= 35; y++)
col(x, y, R(x, y, t), G(x, y, t), B(x, y, t))
}
t = t + 0.020
timer = window.requestAnimationFrame(run)
}
function col(x: number, y: number, r: number, g: number, b: number) {
if (!ctx)
return
ctx.fillStyle = `rgb(${r},${g},${b})`
ctx.fillRect(x, y, 1, 1)
}
function R(x: number, y: number, t: number) {
return (Math.floor(F.value + 36 * Math.cos((x * x - y * y) / 300 + t)))
}
function G(x: number, y: number, t: number) {
return (Math.floor(F.value + 36 * Math.sin((x * x * Math.cos(t / 4) + y * y * Math.sin(t / 3)) / 300)))
}
function B(x: number, y: number, t: number) {
return (Math.floor(F.value + 36 * Math.sin(5 * Math.sin(t / 9) + ((x - 100) * (x - 100) + (y - 100) * (y - 100)) / 1100)))
}
}

View File

@ -5,7 +5,7 @@ export * from './useNavLink.js'
export * from './sidebar.js'
export * from './aside.js'
export * from './page.js'
// export * from './readingTime.js'
export * from './blog.js'
export * from './locale.js'
export * from './useRouteQuery.js'
export * from './waterMark.js'

View File

@ -1,154 +0,0 @@
import { usePageData } from 'vuepress/client'
import { computed } from 'vue'
import type {
PlumeThemePageData,
} from '../../shared/index.js'
/**
* Default locale config for `vuepress-plugin-reading-time2` plugin
*/
export const readingTimeLocales = {
'en': {
word: 'About $word words',
less1Minute: 'Less than 1 minute',
time: 'About $time min',
},
'zh-CN': {
word: '约$word字',
less1Minute: '小于1分钟',
time: '约$time分钟',
},
'zh-TW': {
word: '約$word字',
less1Minute: '小於1分鐘',
time: '约$time分鐘',
},
'de': {
word: 'Ungefähr $word Wörter',
less1Minute: 'Weniger als eine Minute',
time: 'Ungefähr $time min',
},
'de-at': {
word: 'Um die $word Wörter',
less1Minute: 'Weniger als eine Minute',
time: 'Ungefähr $time min',
},
'vi': {
word: 'Khoảng $word từ',
less1Minute: 'Ít hơn 1 phút',
time: 'Khoảng $time phút',
},
'uk': {
word: 'Про $word слова',
less1Minute: 'Менше 1 хвилини',
time: 'Приблизно $time хв',
},
'ru': {
word: 'Около $word слов',
less1Minute: 'Меньше 1 минуты',
time: 'Около $time мин',
},
'br': {
word: 'Por volta de $word palavras',
less1Minute: 'Menos de 1 minuto',
time: 'Por volta de $time min',
},
'pl': {
word: 'Około $word słów',
less1Minute: 'Mniej niż 1 minuta',
time: 'Około $time minut',
},
'sk': {
word: 'Okolo $word slov',
less1Minute: 'Menej ako 1 minúta',
time: 'Okolo $time minút',
},
'fr': {
word: 'Environ $word mots',
less1Minute: 'Moins de 1 minute',
time: 'Environ $time min',
},
'es': {
word: 'Alrededor de $word palabras',
less1Minute: 'Menos de 1 minuto',
time: 'Alrededor de $time min',
},
'ja': {
word: '$word字程度',
less1Minute: '1分以内',
time: '約$time分',
},
'tr': {
word: 'Yaklaşık $word kelime',
less1Minute: '1 dakikadan az',
time: 'Yaklaşık $time dakika',
},
'ko': {
word: '약 $word 단어',
less1Minute: '1분 미만',
time: '약 $time 분',
},
'fi': {
word: 'Noin $word sanaa',
less1Minute: 'Alle minuutti',
time: 'Noin $time minuuttia',
},
'hu': {
word: 'Körülbelül $word szó',
less1Minute: 'Kevesebb, mint 1 perc',
time: 'Körülbelül $time perc',
},
'id': {
word: 'Sekitar $word kata',
less1Minute: 'Kurang dari 1 menit',
time: 'Sekitar $time menit',
},
'nl': {
word: 'Ongeveer $word woorden',
less1Minute: 'Minder dan 1 minuut',
time: 'Ongeveer $time minuten',
},
}
export function useReadingTime() {
const page = usePageData<PlumeThemePageData>()
return computed<{ times: string, words: string }>(() => {
if (!page.value.readingTime)
return { times: '', words: '' }
const locale = readingTimeLocales[page.value.lang] ?? readingTimeLocales.en
const minutes = page.value.readingTime.minutes
const words = page.value.readingTime.words
const times = (minutes < 1 ? locale.less1Minute : locale.time).replace(
'$time',
Math.round(minutes),
)
return {
times,
words: locale.word.replace('$word', words),
}
})
}

View File

@ -47,6 +47,13 @@ export function useSidebar() {
const isOpen = ref(false)
const sidebarKey = computed(() => {
const link = Object.keys(notesData.value).find(link =>
route.path.startsWith(normalizePath(withBase(link))),
)
return link
})
const sidebar = computed(() => {
return theme.value.notes ? getSidebarList(route.path, notesData.value) : []
})
@ -88,6 +95,7 @@ export function useSidebar() {
hasAside,
isSidebarEnabled,
sidebarGroups,
sidebarKey,
open,
close,
toggle,

View File

@ -0,0 +1,191 @@
import { computed, nextTick, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue'
import { isLinkHttp } from 'vuepress/shared'
import { usePageData, usePageFrontmatter, useRoute, useSiteLocaleData, withBase } from 'vuepress/client'
import type { PlumeThemePageData, PlumeThemePageFrontmatter, WatermarkOptions } from '../../shared/index.js'
import { toArray } from '../utils/base.js'
import { useDarkMode } from './darkMode.js'
import { useThemeLocaleData } from './themeData.js'
const defaultWatermarkOptions: WatermarkOptions = {
global: true,
matches: [],
width: 150,
height: 100,
rotate: -22,
fullPage: true,
gapX: 20,
gapY: 20,
opacity: 0.1,
onlyPrint: false,
}
export function useWaterMark() {
const isDark = useDarkMode()
const site = useSiteLocaleData()
const theme = useThemeLocaleData()
const page = usePageData<PlumeThemePageData>()
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
const route = useRoute()
const watermark = computed<WatermarkOptions>(() => {
if (!theme.value.watermark)
return {}
const pageWatermark = typeof frontmatter.value.watermark === 'object' ? frontmatter.value.watermark : {}
const content = site.value.title || theme.value.avatar?.name
return {
content,
...defaultWatermarkOptions,
...theme.value.watermark === true ? {} : theme.value.watermark,
...pageWatermark,
}
})
const enableWatermark = computed(() => {
if (!theme.value.watermark)
return false
const pageWatermark = frontmatter.value.watermark
if (watermark.value.global)
return pageWatermark !== false
if (pageWatermark)
return true
const matches = toArray(watermark.value.matches!)
return matches.some(toMatch)
})
function toMatch(match: string) {
const relativePath = page.value.filePathRelative || ''
if (match[0] === '^') {
const regex = new RegExp(match)
return regex.test(route.path) || (relativePath && regex.test(relativePath))
}
if (match.endsWith('.md'))
return !!relativePath && relativePath.endsWith(match)
return route.path.startsWith(match) || relativePath.startsWith(match)
}
const isFullPage = computed(() => !!watermark.value.fullPage)
const onlyPrint = computed(() => !!watermark.value.onlyPrint)
const svgRect = computed(() => ({
width: watermark.value.width!,
height: watermark.value.height!,
gapX: watermark.value.gapX!,
gapY: watermark.value.gapY!,
svgWidth: watermark.value.width! + watermark.value.gapX!,
svgHeight: watermark.value.height! + watermark.value.gapY!,
opacity: watermark.value.opacity,
}))
const rotateStyle = computed(() => ({
transformOrigin: 'center',
transform: `rotate(${watermark.value.rotate}deg)`,
}))
const imageUrl = computed(() => {
if (!enableWatermark)
return ''
const image = watermark.value.image || ''
const source = typeof image === 'string' ? image : image[isDark.value ? 'dark' : 'light']
return !source ? '' : isLinkHttp(source) ? source : withBase(source)
})
const svgElRef = ref<HTMLDivElement>()
const watermarkUrl = ref('')
const imageBase64 = ref('')
const defaultTextColor = ref('')
const content = computed(() => watermark.value.content)
const textColor = computed(() => {
if (!enableWatermark)
return ''
const textColor = watermark.value.textColor || defaultTextColor.value
return typeof textColor === 'string' ? textColor : textColor[isDark.value ? 'dark' : 'light']
})
const makeImageToBase64 = (url: string) => {
const canvas = document.createElement('canvas')
const image = new Image()
image.crossOrigin = 'anonymous'
image.referrerPolicy = 'no-referrer'
image.onload = () => {
canvas.width = image.naturalWidth
canvas.height = image.naturalHeight
const ctx = canvas.getContext('2d')
ctx?.drawImage(image, 0, 0)
imageBase64.value = canvas.toDataURL()
}
image.src = url
}
const makeSvgToBlobUrl = (svgStr: string) => {
// svg MIME type: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const svgBlob = new Blob([svgStr], {
type: 'image/svg+xml',
})
return URL.createObjectURL(svgBlob)
}
const getDefaultTextColor = () => {
const color = typeof document !== 'undefined' && typeof window !== 'undefined'
? window.getComputedStyle(document.documentElement).getPropertyValue('--vp-c-text-1')
: ''
defaultTextColor.value = color
}
watch(() => isDark.value, () => nextTick(getDefaultTextColor))
onMounted(getDefaultTextColor)
watchEffect(() => {
if (imageUrl.value && enableWatermark.value)
makeImageToBase64(imageUrl.value)
})
watch(
() => [
watermark.value,
imageBase64.value,
enableWatermark.value,
textColor.value,
],
() => {
if (!enableWatermark.value)
return
nextTick(() => {
if (svgElRef.value) {
if (watermarkUrl.value)
URL.revokeObjectURL(watermarkUrl.value)
watermarkUrl.value = makeSvgToBlobUrl(svgElRef.value.innerHTML)
}
})
},
{ immediate: true },
)
onUnmounted(() => {
if (watermarkUrl.value)
URL.revokeObjectURL(watermarkUrl.value)
})
return {
enableWatermark,
isFullPage,
imageUrl,
content,
textColor,
svgElRef,
svgRect,
rotateStyle,
imageBase64,
watermarkUrl,
onlyPrint,
}
}

View File

@ -1,25 +1,5 @@
/* webfont-marker-begin */
@import "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap";
html body {
font-synthesis: style;
}
/* webfont-marker-end */
@font-face {
font-family: "Inter var";
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-cyrillic.woff2") format("woff2");
font-display: swap;
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
font-named-instance: "Regular";
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-cyrillic-ext.woff2") format("woff2");
@ -31,34 +11,86 @@ html body {
U+2DE0-2DFF,
U+A640-A69F,
U+FE2E-FE2F;
font-named-instance: "Regular";
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-greek.woff2") format("woff2");
src: url("../fonts/inter-roman-cyrillic.woff2") format("woff2");
font-display: swap;
unicode-range: U+0370-03FF;
font-named-instance: "Regular";
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-greek-ext.woff2") format("woff2");
font-display: swap;
unicode-range: U+1F00-1FFF;
font-named-instance: "Regular";
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-greek.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0370-0377,
U+037A-037F,
U+0384-038A,
U+038C,
U+038E-03A1,
U+03A3-03FF;
}
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-vietnamese.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0102-0103,
U+0110-0111,
U+0128-0129,
U+0168-0169,
U+01A0-01A1,
U+01AF-01B0,
U+0300-0301,
U+0303-0304,
U+0308-0309,
U+0323,
U+0329,
U+1EA0-1EF9,
U+20AB;
}
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-latin-ext.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0100-02AF,
U+0304,
U+0308,
U+0329,
U+1E00-1E9F,
U+1EF2-1EFF,
U+2020,
U+20A0-20AB,
U+20AD-20C0,
U+2113,
U+2C60-2C7F,
U+A720-A7FF;
}
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-latin.woff2") format("woff2");
@ -71,6 +103,9 @@ html body {
U+02C6,
U+02DA,
U+02DC,
U+0304,
U+0308,
U+0329,
U+2000-206F,
U+2074,
U+20AC,
@ -81,62 +116,10 @@ html body {
U+2215,
U+FEFF,
U+FFFD;
font-named-instance: "Regular";
}
@font-face {
font-family: "Inter var";
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-latin-ext.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0100-024F,
U+0259,
U+1E00-1EFF,
U+2020,
U+20A0-20AB,
U+20AD-20CF,
U+2113,
U+2C60-2C7F,
U+A720-A7FF;
font-named-instance: "Regular";
}
@font-face {
font-family: "Inter var";
font-style: normal;
font-weight: 100 900;
src: url("../fonts/inter-roman-vietnamese.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0102-0103,
U+0110-0111,
U+0128-0129,
U+0168-0169,
U+01A0-01A1,
U+01AF-01B0,
U+1EA0-1EF9,
U+20AB;
font-named-instance: "Regular";
}
@font-face {
font-family: "Inter var";
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-cyrillic.woff2") format("woff2");
font-display: swap;
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
font-named-instance: "Italic";
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-cyrillic-ext.woff2") format("woff2");
@ -148,34 +131,86 @@ html body {
U+2DE0-2DFF,
U+A640-A69F,
U+FE2E-FE2F;
font-named-instance: "Italic";
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-greek.woff2") format("woff2");
src: url("../fonts/inter-italic-cyrillic.woff2") format("woff2");
font-display: swap;
unicode-range: U+0370-03FF;
font-named-instance: "Italic";
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-greek-ext.woff2") format("woff2");
font-display: swap;
unicode-range: U+1F00-1FFF;
font-named-instance: "Italic";
}
@font-face {
font-family: "Inter var";
font-family: Inter;
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-greek.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0370-0377,
U+037A-037F,
U+0384-038A,
U+038C,
U+038E-03A1,
U+03A3-03FF;
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-vietnamese.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0102-0103,
U+0110-0111,
U+0128-0129,
U+0168-0169,
U+01A0-01A1,
U+01AF-01B0,
U+0300-0301,
U+0303-0304,
U+0308-0309,
U+0323,
U+0329,
U+1EA0-1EF9,
U+20AB;
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-latin-ext.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0100-02AF,
U+0304,
U+0308,
U+0329,
U+1E00-1E9F,
U+1EF2-1EFF,
U+2020,
U+20A0-20AB,
U+20AD-20C0,
U+2113,
U+2C60-2C7F,
U+A720-A7FF;
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-latin.woff2") format("woff2");
@ -188,6 +223,9 @@ html body {
U+02C6,
U+02DA,
U+02DC,
U+0304,
U+0308,
U+0329,
U+2000-206F,
U+2074,
U+20AC,
@ -198,47 +236,6 @@ html body {
U+2215,
U+FEFF,
U+FFFD;
font-named-instance: "Italic";
}
@font-face {
font-family: "Inter var";
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-latin-ext.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0100-024F,
U+0259,
U+1E00-1EFF,
U+2020,
U+20A0-20AB,
U+20AD-20CF,
U+2113,
U+2C60-2C7F,
U+A720-A7FF;
font-named-instance: "Italic";
}
@font-face {
font-family: "Inter var";
font-style: italic;
font-weight: 100 900;
src: url("../fonts/inter-italic-vietnamese.woff2") format("woff2");
font-display: swap;
unicode-range:
U+0102-0103,
U+0110-0111,
U+0128-0129,
U+0168-0169,
U+01A0-01A1,
U+01AF-01B0,
U+1EA0-1EF9,
U+20AB;
font-named-instance: "Italic";
}
/* Chinese quotes rendering fix. 中英文弯引号共享 Unicode 码位,确保引号使用中文字体渲染 */
@ -251,3 +248,5 @@ html body {
local("Source Han Sans SC");
unicode-range: U+2018, U+2019, U+201C, U+201D; /* 分别是 ‘’“” */
}
/* Generate the subsetted fonts using: `pyftsubset <file>.woff2 --unicodes="<range>" --output-file="inter-<style>-<subset>.woff2" --flavor=woff2` */

View File

@ -248,33 +248,29 @@
:root {
--vp-font-family-base:
"Chinese Quotes",
"Inter var",
"Inter",
inter,
ui-sans-serif,
system-ui,
-apple-system,
blinkmacsystemfont,
"Segoe UI",
roboto,
"Helvetica Neue",
helvetica,
arial,
"Noto Sans",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji";
--vp-font-family-mono:
ui-monospace,
sfmono-regular,
"SF Mono",
menlo,
monaco,
consolas,
"Liberation Mono",
"Courier New",
monospace;
font-optical-sizing: auto;
}
/**

View File

@ -1,4 +1,5 @@
import { hasOwn, random, toArray } from '@pengzhanbo/utils'
export type BlogTagsColorsItem = readonly [
string, // normal color
string, // hover color

View File

@ -1,5 +1,6 @@
import type { Page, Theme } from 'vuepress/core'
import { logger, templateRenderer } from 'vuepress/utils'
import { addViteConfig } from '@vuepress/helper'
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
import { mergeLocaleOptions } from './defaultOptions.js'
import { setupPlugins } from './plugins.js'
@ -72,6 +73,11 @@ export function plumeTheme({
.replace(/\n/g, '')
return templateRenderer(template, context)
},
extendsBundlerOptions: (options, app) => {
addViteConfig(options, app, {
server: { fs: { cachedChecks: false } },
})
},
}
}
}

View File

@ -1,4 +1,4 @@
import type { NavItemWithLink, PlumeThemeImage } from '.'
import type { NavItemWithLink, PlumeThemeImage, WatermarkOptions } from '.'
/* =============================== Home begin ==================================== */
export interface PlumeThemeHomeFrontmatter extends Omit<PlumeThemeHomeBanner, 'type'> {
@ -110,6 +110,7 @@ export interface PlumeThemePageFrontmatter {
backToTop?: boolean
externalLink?: boolean
readingTime?: boolean
watermark?: boolean | Omit<WatermarkOptions, 'global' | 'matches'>
}
export interface PlumeThemePostFrontmatter extends PlumeThemePageFrontmatter {

View File

@ -1,5 +1,6 @@
import type { LocaleData } from 'vuepress/core'
import type { NotesDataOptions } from '@vuepress-plume/plugin-notes-data'
import type { PlumeThemeImage } from '../base.js'
import type { NavItem } from './navbar.js'
export interface PlumeThemeAvatar {
@ -170,6 +171,73 @@ export interface LastUpdatedOptions {
formatOptions?: Intl.DateTimeFormatOptions & { forceLocale?: boolean }
}
export interface WatermarkOptions {
/**
* , `frontmatter.watermark`
* @default false
*/
global?: boolean
/**
*
* `^`
*/
matches?: string | string[]
/**
*
* @default 0
*/
gapX?: number
/**
*
* @default 0
*/
gapY?: number
/**
* content 使
*/
image?: PlumeThemeImage
/**
*
* @default 100
*/
width?: number
/**
*
* @default 100
*/
height?: number
/**
*
* @default -22
*/
rotate?: number
/**
* image 使
*/
content?: string
/**
*
*/
fullPage?: boolean
/**
*
* @default 0.1
*/
opacity?: number
/**
*
*/
textColor?: string | { dark: string, light: string }
/**
*
* @default false
*/
onlyPrint?: boolean
}
export interface PlumeThemeLocaleData extends LocaleData {
/**
*
@ -376,6 +444,11 @@ export interface PlumeThemeLocaleData extends LocaleData {
linkLabel?: string
linkText?: string
}
/**
*
*/
watermark?: boolean | WatermarkOptions
/**
*
*/