Merge pull request #99 from pengzhanbo/RC-68

RC 68
This commit is contained in:
pengzhanbo 2024-06-19 13:48:57 +08:00 committed by GitHub
commit 850d2005e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 1669 additions and 879 deletions

View File

@ -33,9 +33,10 @@
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.markdownlint": "explicit",
"source.organizeImports": "never"
},
"editor.formatOnPaste": true,
"editor.formatOnPaste": false,
"eslint.validate": [
"javascript",
"javascriptreact",

View File

@ -334,6 +334,20 @@ type NavItem = string | {
详细配置请查看 [此文档](/config/notes/)
### aside
- 类型: `boolean | 'left'`
- 默认值: `true`
- 详情:
是否显示侧边栏
- `false` 表示禁用 右侧边栏
- `true` 表示启用 右侧边栏
- `'left` 表示将有侧边栏移动到文章内容左侧sidebar 右侧
每个页面可以通过 [frontmatter aside](./frontmatter/basic.md#aside) 覆盖层级配置。
### outline
- 类型: `false | number | [number, number] | 'deep'`
@ -348,6 +362,8 @@ type NavItem = string | {
`'deep'``[2, 6]` 相同,将显示从 `<h2>``<h6>` 的所有标题。
当 [aside](#aside) 被禁用时,`outline` 也会被禁用
每个页面可以通过 [frontmatter outline](./frontmatter/basic.md#outline) 覆盖层级配置。
### selectLanguageName
@ -366,7 +382,7 @@ type NavItem = string | {
- 默认值: `''`
- 详情:
_选择语言菜单_ 的文
_选择语言菜单_ 的文
如果你在站点配置中设置了多个 [locales](#locales) ,那么 _选择语言菜单_ 就会显示在导航栏中仓库按钮的旁边。
@ -386,7 +402,7 @@ type NavItem = string | {
- 默认值: `'Menu'`
- 详情:
移动设备下的导航栏中 菜单选项的文
移动设备下的导航栏中 菜单选项的文
### returnToTopLabel
@ -394,7 +410,7 @@ type NavItem = string | {
- 默认值: `'return to top'`
- 详情:
移动设备下的导航栏中返回顶部的文
移动设备下的导航栏中返回顶部的文
### outlineLabel
@ -402,7 +418,7 @@ type NavItem = string | {
- 默认值: `'On this page'`
- 详情:
移动设备下的导航栏中大纲标题的文
移动设备下的导航栏中大纲标题的文
### editLink
@ -414,7 +430,7 @@ type NavItem = string | {
- 类型: `string`
- 默认值: `'Edit this page'`
- 详情: 编辑链接文
- 详情: 编辑链接文
### editLinkPattern
@ -428,7 +444,7 @@ type NavItem = string | {
- 类型: `string`
- 默认值: `''`
- 详情: 文档仓库配置, 用于生成 `Edit this page` 链接,如果为空,默认使用 `repo` 配置的值
- 详情: 文档仓库配置, 用于生成 `Edit this page` 链接
### docsBranch
@ -478,19 +494,19 @@ interface LastUpdatedOptions {
- 类型: `string`
- 默认值: `'Contributors'`
- 详情: 贡献者的文
- 详情: 贡献者的文
### prevPageLabel
- 类型: `string`
- 默认值: `'Previous Page'`
- 详情: 上一页的文
- 详情: 上一页的文
### nextPageLabel
- 类型: `string`
- 默认值: `'Next Page'`
- 详情: 下一页的文
- 详情: 下一页的文
### notFound

View File

@ -4,6 +4,9 @@ author: pengzhanbo
icon: fluent:markdown-20-filled
createTime: 2024/03/05 11:10:39
permalink: /guide/markdown/basic/
tags:
- 指南
- markdown
---
::: note

View File

@ -4,6 +4,9 @@ author: pengzhanbo
icon: fluent-mdl2:auto-enhance-on
createTime: 2024/03/05 23:29:07
permalink: /guide/markdown/extensions/
tags:
- 指南
- markdown
---
## 标题锚点

View File

@ -2,10 +2,448 @@
title: 更多
author: pengzhanbo
icon: ic:outline-auto-fix-high
outline: 2
createTime: 2024/03/05 16:27:59
permalink: /guide/markdown/advance/
tags:
- 指南
- markdown
---
## 卡片
对于想要突出显示的内容,可以将其放在 卡片容器 `::: card` 中。
如果需要在空间足够时并排显示多个卡片,可以使用 `card-grid` 容器,包裹 多个卡片。
### 语法
```md
<!-- 单个卡片 -->
::: card title="标题" icon="twemoji:astonished-face"
这里是卡片内容。
:::
<!-- 多个卡片 -->
:::: card-grid
::: card title="卡片标题 1" icon="twemoji:astonished-face"
这里是卡片内容。
:::
::: card title="卡片标题 2" icon="twemoji:astonished-face"
这里是卡片内容。
:::
::::
```
卡片 支持 可选的 `title``icon`。 其中, icon 支持传入 图片链接,也可以传入 iconify 图标名。
### 示例
**输入:**
```md
::: card title="卡片标题" icon="twemoji:astonished-face"
这里是卡片内容。
:::
```
**输出:**
::: card title="卡片标题" icon="twemoji:astonished-face"
这里是卡片内容。
:::
**输入:**
```md
:::: card-grid
::: card title="卡片标题 1" icon="twemoji:astonished-face"
这里是卡片内容。
:::
::: card title="卡片标题 2" icon="twemoji:astonished-face"
这里是卡片内容。
:::
::::
```
**输出:**
:::: card-grid
::: card title="卡片标题 1" icon="twemoji:astonished-face"
这里是卡片内容。
:::
::: card title="卡片标题 2" icon="twemoji:astonished-face"
这里是卡片内容。
:::
::::
::: info
如果你更喜欢通过组件的方式使用 卡片,你可以查看 [卡片组件](/guide/features/component/#card) 。
:::
## 步骤
有时候,你需要将内容 划分为递进的步骤展示,你可以使用 `steps` 容器 实现。
### 语法
`steps` 容器内,使用 有序列表 (或无序列表) 来表示步骤。你可以在 容器内使用 任意 markdown 语法。
````md
::: steps
1. 步骤 1
相关内容
2. 步骤 2
相关内容
:::
````
### 示例
输入:
````md
:::: steps
1. 步骤 1
```ts
console.log('Hello World!')
```
2. 步骤 2
这里是步骤 2 的相关内容
3. 步骤 3
::: tip
提示容器
:::
4. 结束
::::
````
输出:
:::: steps
1. 步骤 1
```ts
console.log('Hello World!')
```
2. 步骤 2
这里是步骤 2 的相关内容
3. 步骤 3
::: tip
提示容器
:::
4. 结束
::::
## 选项组
在 Markdown 中支持选项卡。
你需要将选项卡包装在 `tabs` 容器中。
你可以在 `tabs` 容器中添加一个 id 后缀,该后缀将用作选项卡 id。
所有具有相同 id 的选项卡将共享相同的切换事件。
```md
::: tabs#fruit
<!-- 这里fruit 将用作 id它是可选的 -->
<!-- 选项卡内容 -->
:::
```
在这个容器内,你应该使用 `@tab` 标记来标记和分隔选项卡内容。
`@tab` 标记后,你可以添加文本 `:active` 默认激活选项卡,之后的文本将被解析为选项卡标题。
```md
::: tabs
@tab 标题 1
<!-- tab 1 内容 -->
@tab 标题 2
<!-- tab 2 内容 -->
@tab:active 标题 3
<!-- tab 3 将会被默认激活 -->
<!-- tab 3 内容 -->
:::
```
默认情况下,标题将用作选项卡的值,但你可以使用 id 后缀覆盖它。
```md
::: tabs
@tab 标题 1
<!-- 此处,选项卡 1 的标题“标题 1”将用作值。 -->
<!-- tab 1 内容 -->
@tab 标题 2#值 2
<!-- 这里tab 2 的标题将是 “标题 2”但它会使用 “值 2” 作为选项卡的值-->
<!-- tab 2 内容 -->
:::
```
你可以在每个选项卡中使用 Vue 语法和组件,并且你可以访问 value 和 isActive
表示选项卡的绑定值和选项卡是否处于激活状态。
**输入:**
````
::: tabs
@tab npm
npm 应该与 Node.js 被一同安装。
@tab pnpm
```sh
corepack enable
corepack use pnpm@8
```
:::
````
**输出:**
::: tabs
@tab npm
npm 应该与 Node.js 被一同安装。
@tab pnpm
```sh
corepack enable
corepack use pnpm@8
```
:::
## “隐秘”文本
有时候,你不想直接把文本内容毫无保留的展示出来,想要保留一些 隐秘性,
可能是为了引起读者的好奇心,也可能纯粹是故意增加点阅读障碍,让文章变得更加有趣。
为了满足这种小小的心思,主题提供了一个 **“隐秘”文本** 的有趣小功能。它看起来像这样:
:::demo-wrapper
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,!!我在床上翻了个身!!
:::
读者不能直接阅读到完整的内容,部分的内容被 “遮住”,需要鼠标悬停到内容上,才能看到被遮住的内容。
### 配置
该功能默认不启用,你需要在 `theme` 配置中启用。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
plot: true,
},
}
})
})
```
:::
`markdownPower.plot` 支持传入 `boolean | PlotOptions` 类型
```ts
interface PlotOptions {
/**
* 是否启用 `!! !!` markdown (该标记为非标准标记,脱离插件将不生效)
* 如果设置为 false 则表示不启用该标记,只能使用 <Plot /> 组件
* @default true
*/
tag?: boolean
/**
* 遮罩层颜色
*/
mask?: string | { light: string, dark: string }
/**
* 文本颜色
*/
color?: string | { light: string, dark: string }
/**
* 触发方式
*
* @default 'hover'
*/
trigger?: 'hover' | 'click'
}
```
### 语法
```md
!!需要隐秘的内容!!
```
如果不想使用 非标准的 `!! !!` 标记语法,你可以将 `plot.tag` 设置为 `false`
然后使用 [`<Plot />`](/guide/features/component/#plot) 组件替代。
### 示例
输入:
```md
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,!!我在床上翻了个身!!
```
输出:
:::demo-wrapper
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,!!我在床上翻了个身!!
:::
## 示例容器
有时候,你可能需要在 内容中补充一些 示例,但期望能与 其它内容 分隔开来呈现。
主题支持在 Markdown 文件中添加示例容器。
### 语法
````md
::: demo-wrapper
添加你的示例
:::
````
### 选项
- `title="xxx"`:标题
- `no-padding`:不添加内边距
- `img`: 仅包含图片时使用
- `height="xxx"`: 高度
### 示例
仅包含图片:
```md
::: demo-wrapper img no-padding
![hero](/images/custom-hero.png)
:::
```
**输出:**
::: demo-wrapper img no-padding
![hero](/images/custom-hero.png)
:::
包含 markdown 语法:
```md
::: demo-wrapper title="标题"
### 三级标题
这是示例容器中的内容。
:::
```
**输出:**
::: demo-wrapper title="标题"
### 三级标题
这是示例容器中的内容。
:::
包含 html /vue 代码:
```md
::: demo-wrapper
<h1 class="your-demo-title">这是标题</h1>
<p class="your-demo-paragraph">这是段落</p>
<style>
.your-demo-title {
color: red;
}
.your-demo-paragraph {
color: blue;
}
</style>
:::
```
**输出:**
::: demo-wrapper
<h1 class="your-demo-title">这是标题</h1>
<p class="your-demo-paragraph">这是段落</p>
<style>
.your-demo-title {
color: red !important;
}
.your-demo-paragraph {
color: blue !important;
}
</style>
:::
## iconify 图标
在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。 主题虽然提供了
@ -108,284 +546,6 @@ github: :[tdesign:logo-github-filled]:
修改大小::[tdesign:logo-github-filled 36px]:
修改大小颜色::[tdesign:logo-github-filled 36px/#f00]:
## “隐秘”文本
有时候,你不想直接把文本内容毫无保留的展示出来,想要保留一些 隐秘性,
可能是为了引起读者的好奇心,也可能纯粹是故意增加点阅读障碍,让文章变得更加有趣。
为了满足这种小小的心思,主题提供了一个 **“隐秘”文本** 的有趣小功能。它看起来像这样:
:::demo-wrapper
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,!!我在床上翻了个身!!
:::
读者不能直接阅读到完整的内容,部分的内容被 “遮住”,需要鼠标悬停到内容上,才能看到被遮住的内容。
### 配置
该功能默认不启用,你需要在 `theme` 配置中启用。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
plot: true,
},
}
})
})
```
:::
`markdownPower.plot` 支持传入 `boolean | PlotOptions` 类型
```ts
interface PlotOptions {
/**
* 是否启用 `!! !!` markdown (该标记为非标准标记,脱离插件将不生效)
* 如果设置为 false 则表示不启用该标记,只能使用 <Plot /> 组件
* @default true
*/
tag?: boolean
/**
* 遮罩层颜色
*/
mask?: string | { light: string, dark: string }
/**
* 文本颜色
*/
color?: string | { light: string, dark: string }
/**
* 触发方式
*
* @default 'hover'
*/
trigger?: 'hover' | 'click'
}
```
### 语法
```md
!!需要隐秘的内容!!
```
如果不想使用 非标准的 `!! !!` 标记语法,你可以将 `plot.tag` 设置为 `false`
然后使用 [`<Plot />`](/guide/features/component/#plot) 组件替代。
### 示例
输入:
```md
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,!!我在床上翻了个身!!
```
输出:
:::demo-wrapper
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,!!我在床上翻了个身!!
:::
## 选项组
让你的 Markdown 文件支持选项卡。
你需要将选项卡包装在 `tabs` 容器中。
你可以在 `tabs` 容器中添加一个 id 后缀,该后缀将用作选项卡 id。
所有具有相同 id 的选项卡将共享相同的切换事件。
```md
::: tabs#fruit
<!-- 这里fruit 将用作 id它是可选的 -->
<!-- 选项卡内容 -->
:::
```
在这个容器内,你应该使用 `@tab` 标记来标记和分隔选项卡内容。
`@tab` 标记后,你可以添加文本 `:active` 默认激活选项卡,之后的文本将被解析为选项卡标题。
```md
::: tabs
@tab 标题 1
<!-- tab 1 内容 -->
@tab 标题 2
<!-- tab 2 内容 -->
@tab:active 标题 3
<!-- tab 3 将会被默认激活 -->
<!-- tab 3 内容 -->
:::
```
默认情况下,标题将用作选项卡的值,但你可以使用 id 后缀覆盖它。
```md
::: tabs
@tab 标题 1
<!-- 此处,选项卡 1 的标题“标题 1”将用作值。 -->
<!-- tab 1 内容 -->
@tab 标题 2#值 2
<!-- 这里tab 2 的标题将是 “标题 2”但它会使用 “值 2” 作为选项卡的值-->
<!-- tab 2 内容 -->
:::
```
你可以在每个选项卡中使用 Vue 语法和组件,并且你可以访问 value 和 isActive
表示选项卡的绑定值和选项卡是否处于激活状态。
**输入:**
````
::: tabs
@tab npm
npm 应该与 Node.js 被一同安装。
@tab pnpm
```sh
corepack enable
corepack use pnpm@8
```
:::
````
**输出:**
::: tabs
@tab npm
npm 应该与 Node.js 被一同安装。
@tab pnpm
```sh
corepack enable
corepack use pnpm@8
```
:::
## 示例容器
有时候,你可能需要在 内容中补充一些 示例,但期望能与 其它内容 分隔开来呈现。
主题支持在 Markdown 文件中添加示例容器。
### 语法
````md
::: demo-wrapper
添加你的示例
:::
````
### 选项
- `title="xxx"`:标题
- `no-padding`:不添加内边距
- `img`: 仅包含图片时使用
- `height="xxx"`: 高度
### 示例
仅包含图片:
```md
::: demo-wrapper img no-padding
![hero](/images/custom-hero.png)
:::
```
**输出:**
::: demo-wrapper img no-padding
![hero](/images/custom-hero.png)
:::
包含 markdown 语法:
```md
::: demo-wrapper title="标题"
### 三级标题
这是示例容器中的内容。
:::
```
**输出:**
::: demo-wrapper title="标题"
### 三级标题
这是示例容器中的内容。
:::
包含 html /vue 代码:
```md
::: demo-wrapper
<h1 class="your-demo-title">这是标题</h1>
<p class="your-demo-paragraph">这是段落</p>
<style>
.your-demo-title {
color: red;
}
.your-demo-paragraph {
color: blue;
}
</style>
:::
```
**输出:**
::: demo-wrapper
<h1 class="your-demo-title">这是标题</h1>
<p class="your-demo-paragraph">这是段落</p>
<style>
.your-demo-title {
color: red !important;
}
.your-demo-paragraph {
color: blue !important;
}
</style>
:::
## can I use
此功能默认不启用,你可以在配置文件中启用它。

View File

@ -14,9 +14,23 @@ permalink: /guide/markdown/experiment/
该功能由 [shiki](https://shiki.style/) 和 [@shikijs/twoslash](https://shiki.style/packages/twoslash) 提供支持,
并整合在 [@vuepress-plume/plugin-shikiji](https://github.com/pengzhanbo/vuepress-theme-plume/tree/main/plugins/plugin-shikiji) 中。
> [!important]
> 从 `vuepress@2.0.0-rc.12` 开始,不再需要对 `@vuepress/markdown` 进行额外的 hack 操作,
> 因此,现在你可以安全的使用这项功能了!
:::important
`vuepress@2.0.0-rc.12` 开始,不再需要对 `@vuepress/markdown` 进行额外的 hack 操作,
因此,现在你可以安全的使用这项功能了!
:::
::: warning
`twoslash` 是一个比较耗时的功能,由于它需要对代码进行类型编译,如果代码引入的包 比较大,会花费较长时间。
特别的,由于 vuepress 启动时,会预编译所有的 markdown 文件,因此它会直接影响 vuepress 的启动时间,
如果 包含了比较多的 `twoslash` 代码块,这可能会使 vuepress 启动时间变得很长。
比如,在未使用 `twoslash`vuepress 的启动时间区间大约在 `300ms ~ 1000ms` 之间,而在使用 `twoslash` 后,
可能某一个 `twoslash` 的代码块编译耗时就需要额外再等待 `500ms` 以上。
但不必担心 markdown 文件热更新时的编译耗时,主题针对 代码高亮编译 耗时做了优化,即使 单个 markdown 文件
中包含多个 代码块,主题也仅会对 **有变更的代码块** 进行编译,因此热更新的速度依然非常快。
:::
### 概述

View File

@ -8,11 +8,33 @@ permalink: /guide/code/intro/
## 概述
主题 使用 [Shiki](https://github.com/shikijs/shiki) 在 Markdown 代码块实现语法高亮。
主题 使用 [Shiki](https://shiki.style/) 在 Markdown 代码块实现语法高亮。
:::info
主题默认 加载了 [Shiki](https://shiki.style/) 支持的超过 190+ 的 语言,这可能导致 在启用 vuepress 服务时,
需要多等待 **300ms ~ 600ms** 左右的时间来加载所有的 语言。
因此,如果比较在意 vuepress 启动时间,建议修改配置为仅加载 您所需要的 语言。
示例:
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
shiki: {
languages: ['javascript', 'typescript', 'vue', 'bash', 'sh'], // [!code highlight]
}
}
})
})
```
:::
## 语言
[Shiki](https://github.com/shikijs/shiki) 支持 超过 190+ 种语言,
[Shiki](https://shiki.style/) 支持 超过 190+ 种语言,
你可以在 [languages](https://shiki.style/languages) 查看所有支持的语言列表。
你可以通过以下语法为你使用的 语言所编写的代码 实现高亮效果:
@ -40,7 +62,7 @@ console.log(a)
## 高亮主题
[Shiki](https://github.com/shikijs/shiki) 支持 超过 40+ 种高亮主题。
[Shiki](https://shiki.style/) 支持 超过 40+ 种高亮主题。
你可以在 [Themes](https://shiki.style/themes) 找到所有支持的主题列表,根据个人的喜欢自定义
高亮主题。
@ -63,7 +85,7 @@ export default defineUserConfig({
## 更多支持
得益于 [Shiki](https://github.com/shikijs/shiki) 的强大能力Theme Plume 还为 代码块
得益于 [Shiki](https://shiki.style/) 的强大能力Theme Plume 还为 代码块
提供了 更多的 [特性支持](/guide/code/features/),它们让 代码块具备更强的表达能力。
同时,为了方便 更好的 完成 代码演示Theme Plume 还提供了嵌入 [CodePen](/guide/code/code-pen/)

View File

@ -9,11 +9,15 @@ permalink: /guide/features/component/
## 概述
VuePress 支持在 Markdown 文件中使用 组件。
主题提供了一些具有通用性的组件,可以在任何地方使用。
## `<Badge />` <Badge type="tip" text="badge" />
## 徽章 <Badge type="tip" text="badge" />
标签,用于在页面中增加一些提示信息。
使用 `<Badge>` 组件来显示 行内信息,如状态或标签。
将你想显示的内容传递给 `<Badge>` 组件的 `text` 属性。
### Props
@ -38,37 +42,37 @@ permalink: /guide/features/component/
- VuePress - <Badge type="warning" text="v2" />
- VuePress - <Badge type="danger" text="v2" />
## `<Iconify />`
## 图标
支持 iconify 所有图标,支持通过 icon name 加载图标。
支持 iconify 所有图标,通过 icon name 加载图标。
可在 [iconify search](https://icon-sets.iconify.design/) 搜索图标使用。
### Props
| 名称 | 类型 | 默认值 | 说明 |
| ----- | ------ | ---------------- | ---------- |
| name | string | `''` | icon name |
| color | string | `'currentcolor'` | icon color |
| size | string | `'1em'` | icon size |
| 名称 | 类型 | 默认值 | 说明 |
| ----- | ------ | ---------------- | -------- |
| name | string | `''` | 图标名 |
| color | string | `'currentcolor'` | 图标颜色 |
| size | string | `'1em'` | 图标大小 |
**输入:**
```md
- home - <Iconify name="material-symbols:home" color="currentColor" size="1em" />
- vscode - <Iconify name="skill-icons:vscode-dark" size="2em" />
- twitter - <Iconify name="skill-icons:twitter" size="2em" />
- home - <Icon name="material-symbols:home" color="currentColor" size="1em" />
- vscode - <Icon name="skill-icons:vscode-dark" size="2em" />
- twitter - <Icon name="skill-icons:twitter" size="2em" />
```
**输出:**
- home - <Iconify name="material-symbols:home" color="currentColor" size="1em" />
- vscode - <Iconify name="skill-icons:vscode-dark" size="2em" />
- twitter - <Iconify name="skill-icons:twitter" size="2em" />
- home - <Icon name="material-symbols:home" color="currentColor" size="1em" />
- vscode - <Icon name="skill-icons:vscode-dark" size="2em" />
- twitter - <Icon name="skill-icons:twitter" size="2em" />
## `<Plot />`
## “隐秘”文本
[“隐秘”文本](/guide/markdown/advance/#隐秘-文本) 组件
使用 `<Plot>` 组件显示 [“隐秘”文本](/guide/markdown/advance/#隐秘-文本) ,能够更灵活的控制行为
该组件默认不启用,你需要在 theme 配置中启用。
@ -97,23 +101,139 @@ export default defineUserConfig({
| mask | `string \| { light: string, dark: string }` | `#000` | 遮罩颜色 |
| color | `string \| { light: string, dark: string }` | `#fff` | 文本颜色 |
输入:
**输入:**
```md
- 鼠标悬停 - <Plot>悬停时可见</Plot>
- 点击 - <Plot trigger="click">点击时可见</Plot>
```
输出:
**输出:**
- 鼠标悬停 - <Plot>悬停时可见</Plot>
- 点击 - <Plot trigger="click">点击时可见</Plot>
## `<HomeBox />`
## 卡片
- 类型:`Component`
使用 `<Card>` 组件在页面中显示卡片。
自定义首页时,提供给 区域 的 包装容器。
也可以使用 markdown [卡片容器](/guide/markdown/advance/#卡片) 语法,替代 `<Card>` 组件。
### Props
| 名称 | 类型 | 默认值 | 说明 |
| ----- | --------------------------- | ------ | ---------------------------------------------------------------- |
| title | `string` | `''` | 标题 |
| icon | `string \| { svg: string }` | `''` | 显示在标题左侧的图标,支持 iconify 所有图标,也可以使用 图片链接 |
### 插槽
| 名称 | 说明 |
| ------- | ---------- |
| default | 卡片内容 |
| title | 自定义标题 |
**输入:**
```md
<Card title="卡片标题" icon="twemoji:astonished-face">
这里是卡片内容。
</Card>
<Card>
<template #title>
<p style="color: red">卡片标题</p>
</template>
这里是卡片内容。
</Card>
```
**输出:**
<Card title="卡片标题" icon="twemoji:astonished-face">
这里是卡片内容。
</Card>
<Card>
<template #title>
<p style="color: red;margin:0">卡片标题</p>
</template>
这里是卡片内容。
</Card>
## 链接卡片
使用 `<LinkCard>` 组件在页面中显示链接卡片。
### Props
| 名称 | 类型 | 默认值 | 说明 |
| ----------- | --------------------------- | ------ | ---------------------------------------------------------------- |
| title | `string` | `''` | 标题 |
| icon | `string \| { svg: string }` | `''` | 显示在标题左侧的图标,支持 iconify 所有图标,也可以使用 图片链接 |
| href | `string` | `''` | 链接 |
| description | `string` | `''` | 详情 |
### 插槽
| 名称 | 说明 |
| ------- | ------------ |
| default | 卡片详情内容 |
| title | 自定义标题 |
**输入:**
```md
<LinkCard title="卡片标题" href="/" description="这里是卡片内容" />
<LinkCard icon="twemoji:astonished-face" title="卡片标题" href="/" />
```
**输出:**
<LinkCard title="卡片标题" href="/" description="这里是卡片内容" />
<LinkCard icon="twemoji:astonished-face" title="卡片标题" href="/" />
## 卡片排列容器
当你需要将多个卡片排列,可以使用 `<CardGrid>` 组件。在空间足够时,多个卡片会自动排列。
**输入:**
```md
<CardGrid>
<Card title="卡片标题" icon="twemoji:astonished-face">
这里是卡片内容。
</Card>
<Card title="卡片标题" icon="twemoji:astonished-face">
这里是卡片内容。
</Card>
</CardGrid>
<CardGrid>
<LinkCard title="卡片标题" href="/" />
<LinkCard icon="twemoji:astonished-face" title="卡片标题" href="/" />
</CardGrid>
```
**输出:**
<CardGrid>
<Card title="卡片标题" icon="twemoji:astonished-face">
这里是卡片内容。
</Card>
<Card title="卡片标题" icon="twemoji:astonished-face">
这里是卡片内容。
</Card>
</CardGrid>
<CardGrid>
<LinkCard title="链接卡片" href="/" />
<LinkCard icon="twemoji:astonished-face" title="链接卡片" href="/" />
</CardGrid>
## 首页布局容器
自定义首页时,使用 `<HomeBox>`提供给 区域 的 包装容器。
### Props

View File

@ -21,195 +21,185 @@ permalink: /guide/quick-start/
使用本主题,你需要首先新建一个项目,并安装`vuepress@next`以及本主题
### 步骤 1
:::: steps
**创建一个新文件夹,并进入目录:**
- ### 新建文件夹并进入目录
``` sh
mkdir my-blog
cd my-blog
```
``` sh
mkdir my-blog
cd my-blog
```
### 步骤 2
- ### 初始化项目
**初始化项目:**
::: code-tabs
@tab pnpm
::: code-tabs
@tab pnpm
``` sh
git init
pnpm init
```
``` sh
git init
pnpm init
```
@tab yarn
@tab yarn
``` sh
git init
yarn init
```
``` sh
git init
yarn init
```
@tab npm
@tab npm
``` sh
git init
npm init
```
``` sh
git init
npm init
```
:::
:::
- ### 安装相关依赖
### 步骤 3
安装 `vuepress@next``vuepress-theme-plume` 作为本地依赖。
**安装相关依赖:**
::: code-tabs
@tab pnpm
安装 `vuepress@next``vuepress-theme-plume` 作为本地依赖。
```sh
# 安装 vuepress
pnpm add -D vuepress@next vue
# 安装 主题和打包工具
pnpm add -D vuepress-theme-plume @vuepress/bundler-vite@next
```
::: code-tabs
@tab pnpm
@tab yarn
```sh
# 安装 vuepress
pnpm add -D vuepress@next vue
# 安装 主题和打包工具
pnpm add -D vuepress-theme-plume @vuepress/bundler-vite@next
```
``` sh
# 安装 vuepress
yarn add -D vuepress@next
# 安装 主题和打包工具
yarn add -D vuepress-theme-plume @vuepress/bundler-vite@next
```
@tab yarn
@tab npm
``` sh
# 安装 vuepress
yarn add -D vuepress@next
# 安装 主题和打包工具
yarn add -D vuepress-theme-plume @vuepress/bundler-vite@next
```
``` sh
# 安装 vuepress
npm i -D vuepress@next
# 安装 主题和打包工具
npm i -D vuepress-theme-plume @vuepress/bundler-vite@next
```
@tab npm
:::
``` sh
# 安装 vuepress
npm i -D vuepress@next
# 安装 主题和打包工具
npm i -D vuepress-theme-plume @vuepress/bundler-vite@next
```
:::warning
主题当前版本 已适配至 `vuepress@2.0.0-rc.13`,你应该安装这个版本的 VuePress。
高于或低于这个版本,可能会存在潜在的兼容性问题。
:::
:::
- ### 在 `package.json` 中添加 `script`
:::warning
主题当前版本 已适配至 `vuepress@2.0.0-rc.13`,你应该安装这个版本的 VuePress。
高于或低于这个版本,可能会存在潜在的兼容性问题。
:::
::: code-tabs
@tab package.json
### 步骤 4
**在 `package.json` 中添加 `script`**
::: code-tabs
@tab package.json
``` json
{
"scripts": {
"dev": "vuepress dev docs",
"build": "vuepress build docs"
``` json
{
"scripts": {
"dev": "vuepress dev docs",
"build": "vuepress build docs"
}
}
}
```
```
:::
:::
`vuepress` 默认将文档源码放在 `docs` 目录下。
`vuepress` 默认将文档源码放在 `docs` 目录下。
### 步骤 5
- ### 将默认的临时目录和缓存目录添加到`.gitignore` 文件中
**将默认的临时目录和缓存目录添加到`.gitignore` 文件中**
::: code-tabs
@tab .gitignore
::: code-tabs
@tab .gitignore
``` txt
node_modules
.temp
.cache
```
``` txt
node_modules
.temp
.cache
```
@tab sh
@tab sh
``` sh
echo 'node_modules' >> .gitignore
echo '.temp' >> .gitignore
echo '.cache' >> .gitignore
```
``` sh
echo 'node_modules' >> .gitignore
echo '.temp' >> .gitignore
echo '.cache' >> .gitignore
```
:::
:::
- ### 在 `docs/.vuepress/config.{js,ts}` 中配置主题
### 步骤 6
::: code-tabs
@tab docs/.vuepress/config.js
**在 `docs/.vuepress/config.{js,ts}` 中配置主题**
``` ts
import { defineUserConfig } from 'vuepress'
import { viteBundler } from '@vuepress/bundler-vite'
import { plumeTheme } from 'vuepress-theme-plume'
::: code-tabs
@tab docs/.vuepress/config.js
export default defineUserConfig({
// 请不要忘记设置默认语言
lang: 'zh-CN',
theme: plumeTheme({
// more...
}),
bundler: viteBundler(),
})
```
``` ts
import { defineUserConfig } from 'vuepress'
import { viteBundler } from '@vuepress/bundler-vite'
import { plumeTheme } from 'vuepress-theme-plume'
:::
export default defineUserConfig({
// 请不要忘记设置默认语言
lang: 'zh-CN',
theme: plumeTheme({
// more...
}),
bundler: viteBundler(),
})
```
:::warning
无论是否需要使用 **多语言** ,你都应该为 VuePress 配置 正确 `lang` 选项值。
主题需要根据 `lang` 选项来确定语言环境文本。
:::
:::
- ### 在 `docs` 目录下新建 `README.md` 文件
:::warning
无论是否需要使用 **多语言** ,你都应该为 VuePress 配置 正确 `lang` 选项值。
主题需要根据 `lang` 选项来确定语言环境文本。
:::
声明首页配置。
::: code-tabs
@tab README.md
### 步骤 7
``` md
---
home: true
---
```
**在 `docs` 目录下新建 `README.md` 文件**
:::
声明首页配置。
::: code-tabs
@tab README.md
- ### 在本地服务器启动你的文档站点
``` md
---
home: true
---
```
::: code-tabs
@tab pnpm
:::
```sh
pnpm dev
```
### 步骤 8
@tab yarn
**在本地服务器启动你的文档站点:**
``` sh
yarn dev
```
::: code-tabs
@tab pnpm
@tab npm
```sh
pnpm dev
```
``` sh
npm run dev
```
@tab yarn
:::
``` sh
yarn dev
```
Vuepress 会在 [http://localhost:8080](http://localhost:8080) 。启动一个热重载的开发服务器。当你修改你的 Markdown 文件时,浏览器中的内容也会自动更新。
@tab npm
- ### 完成
``` sh
npm run dev
```
:::
Vuepress 会在 [http://localhost:8080](http://localhost:8080) 。启动一个热重载的开发服务器。当你修改你的 Markdown 文件时,浏览器中的内容也会自动更新。
::::

View File

@ -112,7 +112,7 @@ home: true
config:
-
type: banner
banner: https://file.mo7.cc/api/public/bz
banner: https://api.pengzhanbo.cn/wallpaper/bing
bannerMask:
light: 0.1
dark: 0.3

View File

@ -38,6 +38,7 @@ export async function highlight(
defaultHighlightLang: defaultLang = '',
codeTransformers: userTransformers = [],
whitespace = false,
languages = Object.keys(bundledLanguages),
} = options
const highlighter = await getHighlighter({
@ -45,7 +46,7 @@ export async function highlight(
typeof theme === 'object' && 'light' in theme && 'dark' in theme
? [theme.light, theme.dark]
: [theme],
langs: [...Object.keys(bundledLanguages), ...(options.languages || [])],
langs: languages,
langAlias: options.languageAlias,
})

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import VPShortPostList from '@theme/Blog/VPShortPostList.vue'
import { useArchives, useBlogExtract } from '../../composables/blog.js'
import { useBlogExtract } from '../../composables/blog-extract.js'
import { useArchives } from '../../composables/blog-archives.js'
const { archives: archivesLink } = useBlogExtract()
const { archives } = useArchives()

View File

@ -4,7 +4,7 @@ import { computed, ref, watch } from 'vue'
import { useRoute, withBase } from 'vuepress/client'
import { isLinkHttp } from 'vuepress/shared'
import VPLink from '@theme/VPLink.vue'
import { useBlogExtract } from '../../composables/blog.js'
import { useBlogExtract } from '../../composables/blog-extract.js'
import { useData } from '../../composables/data.js'
import { inBrowser } from '../../utils/index.js'

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useRoute } from 'vuepress/client'
import VPLink from '@theme/VPLink.vue'
import { useBlogExtract } from '../../composables/blog.js'
import { useBlogExtract } from '../../composables/blog-extract.js'
const props = defineProps<{
isLocal?: boolean

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import VPShortPostList from '@theme/Blog/VPShortPostList.vue'
import { useBlogExtract, useTags } from '../../composables/blog.js'
import { useBlogExtract } from '../../composables/blog-extract.js'
import { useTags } from '../../composables/blog-tags.js'
const { tags, currentTag, postList, handleTagClick } = useTags()
const { tags: tagsLink } = useBlogExtract()

View File

@ -2,7 +2,7 @@
import VPTransitionDrop from '@theme/VPTransitionDrop.vue'
import VPPostItem from '@theme/Blog/VPPostItem.vue'
import VPPagination from '@theme/Blog/VPPagination.vue'
import { usePostListControl } from '../../composables/blog.js'
import { usePostListControl } from '../../composables/blog-post-list.js'
const {
postList,

View File

@ -191,14 +191,23 @@ useHomeHeroTintPlate(
height: calc(100% + var(--vp-footer-height, 0px));
}
@property --vp-home-hero-bg-filter {
inherits: false;
initial-value: #fff;
syntax: "<color>";
}
.bg-filter::after {
--vp-home-hero-bg-filter: var(--vp-c-bg);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background: linear-gradient(to bottom, var(--vp-c-bg) 0, transparent 40%, transparent 60%, var(--vp-c-bg) 140%);
background: linear-gradient(to bottom, var(--vp-home-hero-bg-filter) 0, transparent 45%, transparent 55%, var(--vp-home-hero-bg-filter) 140%);
transition: --vp-home-hero-bg-filter var(--t-color);
}
.bg-filter canvas {

View File

@ -37,7 +37,7 @@ const link = computed(() => {
.vp__img {
display: inline-block;
height: 1em;
margin: 0.3em;
margin: 0 0.3em;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import VPIcon from '@theme/VPIcon.vue'
import { computed } from 'vue'
const props = defineProps<{
title?: string
icon?: string | { svg: string }
}>()
const icon = computed<string | { svg: string } | undefined>(() => {
if (props.icon?.[0] === '{') {
try {
return JSON.parse(icon) as { svg: string }
}
catch {}
}
return props.icon
})
</script>
<template>
<article class="vp-card-wrapper">
<slot name="title">
<p v-if="title || icon" class="title">
<VPIcon v-if="icon" :name="icon" />
<span v-if="title" v-html="title" />
</p>
</slot>
<div class="body">
<slot />
</div>
</article>
</template>
<style scoped>
.vp-card-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 20px;
margin: 16px 0;
border: solid 1px var(--vp-c-divider);
border-radius: 8px;
box-shadow: var(--vp-shadow-1);
transition: border-color var(--t-color), box-shadow var(--t-color);
}
.vp-card-wrapper:hover {
box-shadow: var(--vp-shadow-2);
}
.vp-card-wrapper :deep(.vp-iconify) {
margin: 0;
}
.vp-card-wrapper .title {
display: flex;
gap: 8px;
align-items: center;
margin: 0;
font-size: 20px;
font-weight: 700;
color: var(--vp-c-text-1);
transition: color var(--t-color);
}
.vp-card-wrapper .body :first-child {
margin-top: 0;
}
.vp-card-wrapper .body :last-child {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<div class="vp-card-grid">
<slot />
</div>
</template>
<style scoped>
.vp-card-grid {
display: grid;
gap: 16px 20px;
margin: 16px 0;
}
.vp-card-grid > * {
margin: 0 !important;
}
@media (min-width: 768px) {
.vp-card-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>

View File

@ -0,0 +1,86 @@
<script setup lang="ts">
import VPLink from '@theme/VPLink.vue'
import VPIcon from '@theme/VPIcon.vue'
defineProps<{
href: string
title: string
icon?: string | { svg: string }
description?: string
}>()
</script>
<template>
<div class="vp-link-card">
<span class="body">
<VPLink :href="href" no-icon class="link">
<slot name="title">
<VPIcon v-if="icon" :name="icon" />
<span v-if="title" v-html="title" />
</slot>
</VPLink>
<slot>
<p v-if="description" v-html="description" />
</slot>
</span>
<span class="vpi-arrow-right" />
</div>
</template>
<style scoped>
.vp-link-card {
position: relative;
display: flex;
gap: 8px;
align-items: flex-start;
padding: 16px 20px;
margin: 16px 0;
background-color: transparent;
border: solid 1px var(--vp-c-divider);
border-radius: 8px;
box-shadow: var(--vp-shadow-1);
transition: border-color var(--t-color), box-shadow var(--t-color), background-color var(--t-color);
}
.vp-link-card:hover {
background-color: var(--vp-c-bg-soft);
border-color: var(--vp-c-brand-2);
box-shadow: var(--vp-shadow-2);
}
.vp-link-card .body {
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
}
.vp-link-card .body > :last-child {
margin-bottom: 0;
}
.vp-link-card .link {
display: flex;
gap: 8px;
align-items: center;
font-size: 18px;
font-weight: 700;
color: var(--vp-c-text-1);
text-decoration: none;
}
.vp-link-card .link::before {
position: absolute;
inset: 0;
content: "";
}
.vp-link-card .link :deep(.vp-iconify) {
margin: 0;
}
.vpi-arrow-right {
margin-top: 2px;
font-size: 20px;
}
</style>

View File

@ -0,0 +1,31 @@
import { computed } from 'vue'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useLocalePostList } from './blog-post-list.js'
export type ShortPostItem = Pick<PlumeThemeBlogPostItem, 'title' | 'path' | 'createTime'>
export function useArchives() {
const list = useLocalePostList()
const archives = computed(() => {
const archives: { label: string, list: ShortPostItem[] }[] = []
list.value.forEach((item) => {
const createTime = item.createTime.split(' ')[0]
const year = createTime.split('/')[0]
let current = archives.find(archive => archive.label === year)
if (!current) {
current = { label: year, list: [] }
archives.push(current)
}
current.list.push({
title: item.title,
path: item.path,
createTime: createTime.slice(year.length + 1).replace(/\//g, '-'),
})
})
return archives
})
return { archives }
}

View File

@ -0,0 +1,41 @@
import { useRouteLocale } from 'vuepress/client'
import { computed } from 'vue'
import type { PresetLocale } from '../../shared/index.js'
import { useLocalePostList } from './blog-post-list.js'
import { useTags } from './blog-tags.js'
import { useData } from './data.js'
import { useLocaleLink } from './locale.js'
declare const __PLUME_PRESET_LOCALE__: Record<string, PresetLocale>
const presetLocales = __PLUME_PRESET_LOCALE__
export function useBlogExtract() {
const { theme } = useData()
const locale = useRouteLocale()
const postList = useLocalePostList()
const { tags: tagsList } = useTags()
const blog = computed(() => theme.value.blog || {})
const hasBlogExtract = computed(() => blog.value.archives !== false || blog.value.tags !== false)
const tagsLink = useLocaleLink(blog.value.tagsLink || 'blog/tags/')
const archiveLink = useLocaleLink(blog.value.archivesLink || 'blog/archives/')
const tags = computed(() => ({
link: tagsLink.value,
text: presetLocales[locale.value]?.tag || presetLocales['/'].tag,
total: tagsList.value.length,
}))
const archives = computed(() => ({
link: archiveLink.value,
text: presetLocales[locale.value]?.archive || presetLocales['/'].archive,
total: postList.value.length,
}))
return {
hasBlogExtract,
tags,
archives,
}
}

View File

@ -3,9 +3,8 @@ import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client'
import { computed } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useData, useLocaleLink, useRouteQuery } from '../composables/index.js'
import { toArray } from '../utils/index.js'
import { useTagColors } from './tag-colors.js'
import { useData } from './data.js'
import { useRouteQuery } from './route-query.js'
const DEFAULT_PER_PAGE = 10
@ -133,121 +132,3 @@ export function usePostListControl() {
changePage,
}
}
const extractLocales: Record<string, { tags: string, archives: string }> = {
'zh-CN': { tags: '标签', archives: '归档' },
'en': { tags: 'Tags', archives: 'Archives' },
'zh-TW': { tags: '標籤', archives: '歸檔' },
}
export function useBlogExtract() {
const { theme } = useData()
const locale = usePageLang()
const postList = useLocalePostList()
const { tags: tagsList } = useTags()
const blog = computed(() => theme.value.blog || {})
const hasBlogExtract = computed(() => blog.value.archives !== false || blog.value.tags !== false)
const tagsLink = useLocaleLink(blog.value.tagsLink || 'blog/tags/')
const archiveLink = useLocaleLink(blog.value.archivesLink || 'blog/archives/')
const tags = computed(() => ({
link: tagsLink.value,
text: extractLocales[locale.value]?.tags || extractLocales.en.tags,
total: tagsList.value.length,
}))
const archives = computed(() => ({
link: archiveLink.value,
text: extractLocales[locale.value]?.archives || extractLocales.en.archives,
total: postList.value.length,
}))
return {
hasBlogExtract,
tags,
archives,
}
}
export type ShortPostItem = Pick<PlumeThemeBlogPostItem, 'title' | 'path' | 'createTime'>
export function useTags() {
const list = useLocalePostList()
const colors = useTagColors()
const tags = computed(() => {
const tagMap: Record<string, number> = {}
list.value.forEach((item) => {
if (item.tags) {
toArray(item.tags).forEach((tag) => {
if (tagMap[tag])
tagMap[tag] += 1
else
tagMap[tag] = 1
})
}
})
return Object.keys(tagMap).map(tag => ({
name: tag,
count: tagMap[tag] > 99 ? '99+' : tagMap[tag],
className: `vp-tag-${colors.value[tag]}`,
}))
})
const currentTag = useRouteQuery<string>('tag')
const postList = computed<ShortPostItem[]>(() => {
if (!currentTag.value)
return []
return list.value.filter((item) => {
if (item.tags)
return toArray(item.tags).includes(currentTag.value)
return false
}).map(item => ({
title: item.title,
path: item.path,
createTime: item.createTime.split(' ')[0].replace(/\//g, '-'),
}))
})
const handleTagClick = (tag: string) => {
currentTag.value = tag
}
return {
tags,
currentTag,
postList,
handleTagClick,
}
}
export function useArchives() {
const list = useLocalePostList()
const archives = computed(() => {
const archives: { label: string, list: ShortPostItem[] }[] = []
list.value.forEach((item) => {
const createTime = item.createTime.split(' ')[0]
const year = createTime.split('/')[0]
let current = archives.find(archive => archive.label === year)
if (!current) {
current = { label: year, list: [] }
archives.push(current)
}
current.list.push({
title: item.title,
path: item.path,
createTime: createTime.slice(year.length + 1).replace(/\//g, '-'),
})
})
return archives
})
return { archives }
}

View File

@ -0,0 +1,62 @@
import { computed } from 'vue'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { toArray } from '../utils/index.js'
import { useTagColors } from './tag-colors.js'
import { useLocalePostList } from './blog-post-list.js'
import { useRouteQuery } from './route-query.js'
type ShortPostItem = Pick<PlumeThemeBlogPostItem, 'title' | 'path' | 'createTime'>
export function useTags() {
const list = useLocalePostList()
const colors = useTagColors()
const tags = computed(() => {
const tagMap: Record<string, number> = {}
list.value.forEach((item) => {
if (item.tags) {
toArray(item.tags).forEach((tag) => {
if (tagMap[tag])
tagMap[tag] += 1
else
tagMap[tag] = 1
})
}
})
return Object.keys(tagMap).map(tag => ({
name: tag,
count: tagMap[tag] > 99 ? '99+' : tagMap[tag],
className: `vp-tag-${colors.value[tag]}`,
}))
})
const currentTag = useRouteQuery<string>('tag')
const postList = computed<ShortPostItem[]>(() => {
if (!currentTag.value)
return []
return list.value.filter((item) => {
if (item.tags)
return toArray(item.tags).includes(currentTag.value)
return false
}).map(item => ({
title: item.title,
path: item.path,
createTime: item.createTime.split(' ')[0].replace(/\//g, '-'),
}))
})
const handleTagClick = (tag: string) => {
currentTag.value = tag
}
return {
tags,
currentTag,
postList,
handleTagClick,
}
}

View File

@ -2,6 +2,7 @@ export * from './theme-data.js'
export * from './dark-mode.js'
export * from './data.js'
export * from './scroll-promise.js'
export * from './scroll-behavior.js'
export * from './sidebar.js'
export * from './aside.js'
@ -12,7 +13,10 @@ export * from './edit-link.js'
export * from './latest-updated.js'
export * from './contributors.js'
export * from './blog.js'
export * from './blog-post-list.js'
export * from './blog-extract.js'
export * from './blog-tags.js'
export * from './blog-archives.js'
export * from './tag-colors.js'
export * from './locale.js'

View File

@ -0,0 +1,25 @@
import type { Router } from 'vuepress/client'
import { nextTick } from 'vue'
import { inBrowser } from '../utils/index.js'
import { useScrollPromise } from './scroll-promise.js'
export function enhanceScrollBehavior(router: Router) {
// handle scrollBehavior with transition
const scrollBehavior = router.options.scrollBehavior!
router.options.scrollBehavior = async (...args) => {
await useScrollPromise().wait()
return scrollBehavior(...args)
}
router.beforeEach(() => {
if (inBrowser) {
document.documentElement.classList.remove('smooth')
}
})
router.afterEach(() => nextTick(() => {
if (inBrowser) {
document.documentElement.classList.add('smooth')
}
}))
}

View File

@ -2,61 +2,19 @@ import './styles/index.css'
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { h } from 'vue'
import VPBadge from './components/global/VPBadge.vue'
import { setupDarkMode, setupWatermark, useScrollPromise } from './composables/index.js'
import { enhanceScrollBehavior, setupDarkMode, setupWatermark } from './composables/index.js'
import { globalComponents } from './globalComponents.js'
import Layout from './layouts/Layout.vue'
import NotFound from './layouts/NotFound.vue'
import VPHomeBox from './components/Home/VPHomeBox.vue'
export default defineClientConfig({
enhance({ app, router }) {
setupDarkMode(app)
// global component
app.component('Badge', VPBadge)
app.component('VPBadge', VPBadge) // alias
app.component('DocSearch', () => {
const SearchComponent
= app.component('Docsearch') || app.component('SearchBox')
if (SearchComponent)
return h(SearchComponent)
return null
})
app.component('PageComment', (props) => {
const CommentService = app.component('CommentService')
if (CommentService)
return h(CommentService, props)
return null
})
app.component('Icon', (props) => {
const Iconify = app.component('Iconify')
if (Iconify)
return h(Iconify, props)
return null
})
/** @deprecated */
app.component('HomeBox', VPHomeBox)
app.component('VPHomeBox', VPHomeBox)
// handle scrollBehavior with transition
const scrollBehavior = router.options.scrollBehavior!
router.options.scrollBehavior = async (...args) => {
await useScrollPromise().wait()
return scrollBehavior(...args)
}
enhanceScrollBehavior(router)
globalComponents(app)
},
setup() {
setupWatermark()
},
layouts: {
Layout,
NotFound,
},
layouts: { Layout, NotFound },
}) as ClientConfig

View File

@ -0,0 +1,50 @@
import type { App } from 'vue'
import { h } from 'vue'
import VPHomeBox from '@theme/Home/VPHomeBox.vue'
import VPCard from '@theme/global/VPCard.vue'
import VPLinkCard from '@theme/global/VPLinkCard.vue'
import VPBadge from '@theme/global/VPBadge.vue'
import VPCardGrid from '@theme/global/VPCardGrid.vue'
export function globalComponents(app: App) {
app.component('Badge', VPBadge)
app.component('VPBadge', VPBadge)
app.component('VPCard', VPCard)
app.component('Card', VPCard)
app.component('VPCardGrid', VPCardGrid)
app.component('CardGrid', VPCardGrid)
app.component('VPLinkCard', VPLinkCard)
app.component('LinkCard', VPLinkCard)
app.component('DocSearch', () => {
const SearchComponent
= app.component('Docsearch') || app.component('SearchBox')
if (SearchComponent)
return h(SearchComponent)
return null
})
app.component('PageComment', (props) => {
const CommentService = app.component('CommentService')
if (CommentService)
return h(CommentService, props)
return null
})
app.component('Icon', (props) => {
const Iconify = app.component('Iconify')
if (Iconify)
return h(Iconify, props)
return null
})
/** @deprecated */
app.component('HomeBox', VPHomeBox)
app.component('VPHomeBox', VPHomeBox)
}

View File

@ -11,6 +11,9 @@ export { default as Layout } from './layouts/Layout.vue'
export { default as NotFound } from './layouts/NotFound.vue'
export { default as VPBadge } from './components/global/VPBadge.vue'
export { default as VPCard } from './components/global/VPCard.vue'
export { default as VPCardGrid } from './components/global/VPCardGrid.vue'
export { default as VPImage } from './components/VPImage.vue'
export { default as VPButton } from './components/VPButton.vue'
export { default as VPLink } from './components/VPLink.vue'

View File

@ -39,12 +39,21 @@
}
.vp-doc h3 {
margin: 32px 0 0;
margin: 32px 0 16px;
font-size: 20px;
line-height: 28px;
letter-spacing: -0.01em;
}
.vp-doc h4,
.vp-doc h5,
.vp-doc h6 {
margin: 24px 0 16px;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.01em;
}
.vp-doc .header-anchor {
position: relative;
color: inherit;
@ -89,6 +98,10 @@
}
}
.vp-doc img {
display: inline-block;
}
.vp-doc img + img {
margin-left: 0.5rem;
}
@ -148,10 +161,6 @@
font-weight: 600;
}
.vp-doc a > img {
display: inline-block;
}
/**
* Lists
* -------------------------------------------------------------------------- */

View File

@ -0,0 +1,208 @@
/* --------------------- demo-wrapper ------------------------ */
.vp-doc .demo-wrapper {
display: flex;
flex-direction: column;
min-height: 40px;
margin: 40px -16px;
border: solid 1px var(--vp-c-divider);
border-radius: 8px;
box-shadow: var(--vp-shadow-2);
transition: var(--t-color);
transition-property: border, box-shadow;
}
.vp-doc .demo-wrapper .demo-head {
display: flex;
align-items: center;
justify-content: flex-start;
min-height: 0;
border-bottom: solid 1px var(--vp-c-divider);
transition: border-bottom var(--t-color);
}
.vp-doc .demo-wrapper .demo-container {
min-height: 0;
padding: 20px;
font-size: 14px;
line-height: 22px;
background-color: var(--vp-c-bg-alt);
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
transition: background-color var(--t-color);
}
.vp-doc .demo-wrapper.has-title .demo-head {
border-bottom-color: transparent;
}
.vp-doc .demo-wrapper.only-img {
overflow: hidden;
}
.vp-doc .demo-wrapper.only-img img {
display: block;
}
.vp-doc .demo-wrapper.only-img .demo-container,
.vp-doc .demo-wrapper.no-padding .demo-container {
padding: 0;
}
.vp-doc .demo-wrapper.has-height .demo-container {
height: var(--demo-container-height);
overflow-y: auto;
}
.vp-doc .demo-wrapper .demo-ctrl {
display: flex;
gap: 5px;
align-items: center;
justify-content: flex-start;
padding: 5px 0 5px 8px;
}
.vp-doc .demo-wrapper .demo-ctrl i {
display: inline-block;
width: 10px;
height: 10px;
background-color: #ccc;
border-radius: 100%;
transition: background-color var(--t-color);
}
.vp-doc .demo-wrapper .demo-ctrl i:nth-child(1) {
background-color: var(--vp-c-danger-3);
}
.vp-doc .demo-wrapper .demo-ctrl i:nth-child(2) {
background-color: var(--vp-c-warning-3);
}
.vp-doc .demo-wrapper .demo-ctrl i:nth-child(3) {
background-color: var(--vp-c-green-3);
}
.vp-doc .demo-wrapper .demo-title {
position: relative;
min-width: 0;
padding: 0 16px;
margin: 0 20px -1px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
background-color: var(--vp-c-bg-alt);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
transition: var(--t-color);
transition-property: color, background-color;
}
.vp-doc .demo-wrapper .demo-title p {
max-width: 100%;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
.vp-doc .demo-wrapper .demo-title::after,
.vp-doc .demo-wrapper .demo-title::before {
position: absolute;
bottom: 0;
z-index: 1;
width: 8px;
height: 8px;
content: " ";
transition: background var(--t-color);
}
.vp-doc .demo-wrapper .demo-title::before {
left: 100%;
background: radial-gradient(16px at right top, transparent 50%, var(--vp-c-bg-alt) 50%);
}
.vp-doc .demo-wrapper .demo-title::after {
right: 100%;
background: radial-gradient(16px at left top, transparent 50%, var(--vp-c-bg-alt) 50%);
}
.vp-doc .demo-wrapper .demo-container > *:first-child {
margin-top: 0;
}
.vp-doc .demo-wrapper .demo-container > *:last-child {
margin-bottom: 0;
}
@media (min-width: 419px) {
.vp-doc .demo-wrapper {
margin: 40px 0;
}
}
/* ---------------------------- Steps --------------------------- */
.vp-doc .vp-steps {
margin: 16px 0;
}
.vp-doc .vp-steps > ol,
.vp-doc .vp-steps > ul {
padding-inline-start: 0;
list-style: none;
}
.vp-doc .vp-steps > ol > li,
.vp-doc .vp-steps > ul > li {
position: relative;
min-height: 22px;
padding-bottom: 1px;
padding-left: 44px;
}
.vp-doc .vp-steps > ol > li::before,
.vp-doc .vp-steps > ul > li::before {
position: absolute;
inset-inline-start: 0;
top: 0;
width: 28px;
height: 28px;
font-size: 16px;
font-weight: 400;
line-height: 28px;
color: var(--vp-c-text-1);
text-align: center;
content: counter(list-item);
background-color: var(--vp-c-bg-soft);
border: solid 1px var(--vp-c-divider);
border-radius: 100%;
transition: var(--t-color);
transition-property: color, background-color, border-color;
}
.vp-doc .vp-steps > ol > li:not(:last-of-type)::after,
.vp-doc .vp-steps > ul > li:not(:last-of-type)::after {
position: absolute;
inset-inline-start: 14px;
top: 34px;
bottom: 6px;
width: 1px;
content: "";
background-color: var(--vp-c-divider);
transition: background-color var(--t-color);
}
.vp-doc .vp-steps > ol > li > :first-child,
.vp-doc .vp-steps > ul > li > :first-child {
margin-top: 0;
}
.vp-doc .vp-steps > ol > li > :first-child:where(h1,h2,h3,h4,h5,h6),
.vp-doc .vp-steps > ul > li > :first-child:where(h1,h2,h3,h4,h5,h6) {
padding-top: 0;
border-top: none;
}
.vp-doc .vp-steps > ol > li + li,
.vp-doc .vp-steps > ul > li + li {
margin-top: 0;
}

View File

@ -8,6 +8,7 @@
@import url("./utils.css");
@import url("./content.css");
@import url("./code.css");
@import url("./custom-block.css");
@import url("./twoslash.css");
@import url("./md-enhance.css");

View File

@ -587,148 +587,6 @@
font-size: 0.8rem;
}
/* --------------------- demo-wrapper ------------------------ */
.vp-doc .demo-wrapper {
display: flex;
flex-direction: column;
min-height: 40px;
margin: 40px -16px;
border: solid 1px var(--vp-c-divider);
border-radius: 8px;
box-shadow: var(--vp-shadow-2);
transition: var(--t-color);
transition-property: border, box-shadow;
}
.vp-doc .demo-wrapper.has-title .demo-head {
border-bottom-color: transparent;
}
.vp-doc .demo-wrapper.only-img {
overflow: hidden;
}
.vp-doc .demo-wrapper.only-img img {
display: block;
}
.vp-doc .demo-wrapper.only-img .demo-container,
.vp-doc .demo-wrapper.no-padding .demo-container {
padding: 0;
}
.vp-doc .demo-wrapper.has-height .demo-container {
height: var(--demo-container-height);
overflow-y: auto;
}
.vp-doc .demo-wrapper .demo-head {
display: flex;
align-items: center;
justify-content: flex-start;
min-height: 0;
border-bottom: solid 1px var(--vp-c-divider);
transition: border-bottom var(--t-color);
}
.vp-doc .demo-wrapper .demo-ctrl {
display: flex;
gap: 5px;
align-items: center;
justify-content: flex-start;
padding: 5px 0 5px 8px;
}
.vp-doc .demo-wrapper .demo-ctrl i {
display: inline-block;
width: 10px;
height: 10px;
background-color: #ccc;
border-radius: 100%;
transition: background-color var(--t-color);
}
.vp-doc .demo-wrapper .demo-ctrl i:nth-child(1) {
background-color: var(--vp-c-danger-3);
}
.vp-doc .demo-wrapper .demo-ctrl i:nth-child(2) {
background-color: var(--vp-c-warning-3);
}
.vp-doc .demo-wrapper .demo-ctrl i:nth-child(3) {
background-color: var(--vp-c-green-3);
}
.vp-doc .demo-wrapper .demo-title {
position: relative;
min-width: 0;
padding: 0 16px;
margin: 0 20px -1px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
background-color: var(--vp-c-bg-alt);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
transition: var(--t-color);
transition-property: color, background-color;
}
.vp-doc .demo-wrapper .demo-title p {
max-width: 100%;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
.vp-doc .demo-wrapper .demo-title::after,
.vp-doc .demo-wrapper .demo-title::before {
position: absolute;
bottom: 0;
z-index: 1;
width: 8px;
height: 8px;
content: " ";
transition: background var(--t-color);
}
.vp-doc .demo-wrapper .demo-title::before {
left: 100%;
background: radial-gradient(16px at right top, transparent 50%, var(--vp-c-bg-alt) 50%);
}
.vp-doc .demo-wrapper .demo-title::after {
right: 100%;
background: radial-gradient(16px at left top, transparent 50%, var(--vp-c-bg-alt) 50%);
}
.vp-doc .demo-wrapper .demo-container {
min-height: 0;
padding: 20px;
font-size: 14px;
line-height: 22px;
background-color: var(--vp-c-bg-alt);
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
transition: background-color var(--t-color);
}
.vp-doc .demo-wrapper .demo-container > *:first-child {
margin-top: 0;
}
.vp-doc .demo-wrapper .demo-container > *:last-child {
margin-bottom: 0;
}
@media (min-width: 419px) {
.vp-doc .demo-wrapper {
margin: 40px 0;
}
}
/* --------------------- Markdown Enhance: Mermaid ------------------------ */
.mermaid-actions {
padding-right: 20px;

View File

@ -23,7 +23,6 @@ html {
font-size: 16px;
line-height: 1.4;
scroll-behavior: smooth;
scroll-padding-top: 80px;
}

View File

@ -12,6 +12,10 @@
margin: 0.3em;
}
.smooth {
scroll-behavior: smooth;
}
/* ----------------- Transition ------------------------ */
.fade-slide-y-enter-active {
transition: all 0.25s ease !important;

View File

@ -1,4 +1,4 @@
import { addViteConfig, addViteOptimizeDepsInclude, addViteSsrNoExternal } from '@vuepress/helper'
import { addViteConfig, addViteOptimizeDepsExclude, addViteOptimizeDepsInclude, addViteSsrNoExternal } from '@vuepress/helper'
import type { App } from 'vuepress'
export function extendsBundlerOptions(bundlerOptions: any, app: App): void {
@ -9,6 +9,7 @@ export function extendsBundlerOptions(bundlerOptions: any, app: App): void {
})
addViteOptimizeDepsInclude(bundlerOptions, app, '@vueuse/core', true)
addViteOptimizeDepsExclude(bundlerOptions, app, '@theme')
addViteSsrNoExternal(bundlerOptions, app, [
'@vuepress/helper',

View File

@ -1,5 +1,12 @@
export * from './resolveThemeOption.js'
export * from './resolveLocaleOptions.js'
export * from './resolveThemeData.js'
export * from './resolveProvideData.js'
export * from './resolveAlias.js'
export * from './extendsBundlerOptions.js'
export * from './templateBuildRenderer.js'
export * from './resolveSearchOptions.js'
export * from './resolvePageHead.js'
export * from './resolveEncrypt.js'

View File

@ -0,0 +1,18 @@
import { fs, path } from 'vuepress/utils'
import { resolve } from '../utils.js'
export function resolveAlias() {
return {
...Object.fromEntries(
fs.readdirSync(
resolve('client/components'),
{ encoding: 'utf-8', recursive: true },
)
.filter(file => file.endsWith('.vue'))
.map(file => [
path.join('@theme', file),
resolve('client/components', file),
]),
),
}
}

View File

@ -0,0 +1,28 @@
import type { App } from 'vuepress'
import { entries, fromEntries, getRootLangPath, isPlainObject } from '@vuepress/helper'
import type { PlumeThemeEncrypt, PlumeThemePluginOptions } from '../../shared/index.js'
import { PRESET_LOCALES } from '../locales/index.js'
import { resolveEncrypt } from './resolveEncrypt.js'
export function resolveProvideData(
app: App,
plugins: PlumeThemePluginOptions,
encrypt?: PlumeThemeEncrypt,
): Record<string, any> {
const root = getRootLangPath(app)
return {
// 注入 加密配置
...resolveEncrypt(encrypt),
// 注入水印配置
__PLUME_WM_FP__: isPlainObject(plugins.watermark)
? plugins.watermark.fullPage !== false
: true,
// 注入多语言配置
__PLUME_PRESET_LOCALE__: fromEntries(
entries(PRESET_LOCALES)
.map(([locale, value]) => [locale === root ? '/' : locale, value]),
),
}
}

View File

@ -4,7 +4,10 @@ import type { DocsearchPluginOptions } from '@vuepress/plugin-docsearch'
import type { SearchPluginOptions } from '@vuepress-plume/plugin-search'
import { DOCSEARCH_LOCALES, SEARCH_LOCALES } from '../locales/index.js'
export function resolveSearchOptions(app: App, { locales, ...options }: SearchPluginOptions = {}): SearchPluginOptions {
export function resolveSearchOptions(
app: App,
{ locales, ...options }: SearchPluginOptions = {},
): SearchPluginOptions {
return {
...options,
locales: getLocaleConfig({
@ -15,7 +18,10 @@ export function resolveSearchOptions(app: App, { locales, ...options }: SearchPl
}
}
export function resolveDocsearchOptions(app: App, { locales, ...options }: DocsearchPluginOptions = {}): DocsearchPluginOptions {
export function resolveDocsearchOptions(
app: App,
{ locales, ...options }: DocsearchPluginOptions = {},
): DocsearchPluginOptions {
return {
...options,
locales: getLocaleConfig({

View File

@ -0,0 +1,19 @@
import type { PlumeThemeOptions } from '../../shared/index.js'
import { logger } from '../utils.js'
export function resolveThemeOptions({ themePlugins, plugins, encrypt, hostname, ...localeOptions }: PlumeThemeOptions) {
const pluginOptions = plugins ?? themePlugins ?? {}
if (themePlugins) {
logger.warn(
`The 'themePlugins' option is deprecated. Please use 'plugins' instead.`,
)
}
return {
pluginOptions,
encrypt,
hostname,
localeOptions,
}
}

View File

@ -1,5 +1,5 @@
import { type TemplateRendererContext, templateRenderer } from 'vuepress/utils'
import { getThemePackage } from './utils.js'
import { getThemePackage } from '../utils.js'
export function templateBuildRenderer(template: string, context: TemplateRendererContext) {
const pkg = getThemePackage()

View File

@ -5,4 +5,7 @@ export * from '../shared/index.js'
export { plumeTheme }
/**
* @deprecated 使
*/
export default plumeTheme

View File

@ -1,6 +1,5 @@
import type { SearchLocaleOptions } from '@vuepress-plume/plugin-search'
import type { PlumeThemeLocaleData } from '../../shared/index.js'
import type { PresetLocale } from '../types.js'
import type { PlumeThemeLocaleData, PresetLocale } from '../../shared/index.js'
export const enLocale: PlumeThemeLocaleData = {
selectLanguageName: 'English',

View File

@ -1,7 +1,6 @@
import type { DocsearchLocaleOptions } from '@vuepress/plugin-docsearch'
import type { SearchLocaleOptions } from '@vuepress-plume/plugin-search'
import type { PlumeThemeLocaleData } from '../../shared/index.js'
import type { PresetLocale } from '../types.js'
import type { PlumeThemeLocaleData, PresetLocale } from '../../shared/index.js'
import { zhDocsearchLocale, zhLocale, zhPresetLocale, zhSearchLocale } from './zh.js'
import { enLocale, enPresetLocale, enSearchLocale } from './en.js'

View File

@ -1,7 +1,6 @@
import type { DocsearchLocaleOptions } from '@vuepress/plugin-docsearch'
import type { SearchLocaleOptions } from '@vuepress-plume/plugin-search'
import type { PlumeThemeLocaleData } from '../../shared/index.js'
import type { PresetLocale } from '../types.js'
import type { PlumeThemeLocaleData, PresetLocale } from '../../shared/index.js'
export const zhLocale: PlumeThemeLocaleData = {
selectLanguageName: '简体中文',

View File

@ -39,6 +39,60 @@ export const customContainerPlugins: Plugin[] = [
return '</div></div>\n'
},
}),
/**
* :::steps
* 1. 1
* xxx
* 2. 2
* xxx
* 3. ...
* :::
*/
containerPlugin({
type: 'steps',
before() {
return '<div class="vp-steps">'
},
after() {
return '</div>'
},
}),
/**
* ::: card title="xxx" icon="xxx"
* xxx
* :::
*/
containerPlugin({
type: 'card',
before(info) {
const title = resolveAttr(info, 'title')
const icon = resolveAttr(info, 'icon')
return `<VPCard${title ? ` title="${title}"` : ''}${icon ? ` icon="${icon}"` : ''}>`
},
after() {
return '</VPCard>'
},
}),
/**
* :::: card-grid
* ::: card
* xxx
* :::
* ::: card
* xxx
* :::
* ::::
*/
containerPlugin({
type: 'card-grid',
before() {
return '<VPCardGrid>'
},
after() {
return '</VPCardGrid>'
},
}),
]
/**

View File

@ -1,62 +1,40 @@
import type { Page, Theme } from 'vuepress/core'
import { fs, path } from 'vuepress/utils'
import { isPlainObject } from '@vuepress/helper'
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
import { getPlugins } from './plugins/index.js'
import { extendsPageData, setupPage } from './setupPages.js'
import { THEME_NAME, logger, resolve, templates } from './utils.js'
import { resolveEncrypt, resolveLocaleOptions, resolvePageHead } from './config/index.js'
import { extendsBundlerOptions } from './extendsBundlerOptions.js'
import { templateBuildRenderer } from './templateBuildRenderer.js'
import { THEME_NAME, resolve, templates } from './utils.js'
import {
extendsBundlerOptions,
resolveAlias,
resolveLocaleOptions,
resolvePageHead,
resolveProvideData,
resolveThemeOptions,
templateBuildRenderer,
} from './config/index.js'
import { setupPrepare, watchPrepare } from './prepare/index.js'
export function plumeTheme({
themePlugins,
plugins,
encrypt,
hostname,
...localeOptions
}: PlumeThemeOptions = {}): Theme {
const pluginOptions = plugins ?? themePlugins ?? {}
const watermarkFullPage = isPlainObject(pluginOptions.watermark)
? pluginOptions.watermark.fullPage !== false
: true
if (themePlugins) {
logger.warn(
`The 'themePlugins' option is deprecated. Please use 'plugins' instead.`,
)
}
export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
const {
localeOptions: rawLocaleOptions,
pluginOptions,
hostname,
encrypt,
} = resolveThemeOptions(options)
return (app) => {
localeOptions = resolveLocaleOptions(app, localeOptions)
const localeOptions = resolveLocaleOptions(app, rawLocaleOptions)
return {
name: THEME_NAME,
define: {
...resolveEncrypt(encrypt),
__PLUME_WM_FP__: watermarkFullPage,
},
define: resolveProvideData(app, pluginOptions, encrypt),
templateBuild: templates('build.html'),
clientConfigFile: resolve('client/config.js'),
alias: {
...Object.fromEntries(
fs.readdirSync(
resolve('client/components'),
{ encoding: 'utf-8', recursive: true },
)
.filter(file => file.endsWith('.vue'))
.map(file => [
path.join('@theme', file),
resolve('client/components', file),
]),
),
},
alias: resolveAlias(),
plugins: getPlugins({ app, pluginOptions, localeOptions, encrypt, hostname }),

View File

@ -1,6 +0,0 @@
export interface PresetLocale {
home: string
blog: string
tag: string
archive: string
}

View File

@ -38,3 +38,10 @@ export type SocialLinkIconUnion =
| 'xbox'
export type SocialLinkIcon = SocialLinkIconUnion | { svg: string }
export interface PresetLocale {
home: string
blog: string
tag: string
archive: string
}

View File

@ -48,9 +48,9 @@ export interface PlumeThemeLocaleData extends LocaleData {
social?: SocialLink[]
/**
* Navbar config
*
*
* Set to `false` to disable navbar in current locale
* `false`
*/
navbar?: false | NavItem[]
@ -75,114 +75,119 @@ export interface PlumeThemeLocaleData extends LocaleData {
/**
*
*
* notes配置到navbar中
* notes配置到navbar中
*/
notes?: false | NotesDataOptions
/**
*
*
*
*
*
*
* 'deep' [2, 6] <h2> <h6>
*
* @default [2, 3]
*/
outline?: ThemeOutline
/**
*
*
* - `false`
* - `true`
* - `'left` sidebar
*
* @default true
*/
aside?: boolean | 'left'
/**
* language text
*
*/
selectLanguageText?: string
/**
* language aria label
* `aria-label`
*/
selectLanguageAriaLabel?: string
/**
* language name
*
*
* locales locale
*/
selectLanguageName?: string
/**
* Page meta - edit link config
*
* Whether to show "Edit this page" or not
*
* "编辑此页"
*/
editLink?: boolean
/**
* Page meta - edit link config
* "编辑此页"
*
* The text to replace the default "Edit this page"
* @default "Edit this page"
*/
editLinkText?: string
/**
* Page meta - edit link config
*
* Pattern of edit link
* "编辑此页"
*
* @example ':repo/edit/:branch/:path'
*/
editLinkPattern?: string
/**
* Page meta - edit link config
*
* Use `repo` config by default
*
* Set this config if your docs is placed in a different repo
* , Edit this page
*/
docsRepo?: string
/**
* Page meta - edit link config
*
* Set this config if the branch of your docs is not 'main'
* `Edit this page`
*/
docsBranch?: string
/**
* Page meta - edit link config
*
* Set this config if your docs is placed in sub dir of your `docsRepo`
* `Edit this page`
*/
docsDir?: string
/**
* Page meta - last updated config
*
*
* Whether to show "Last Updated" or not
* @default { text: 'Last Updated', formatOptions: { dateStyle: 'short', timeStyle: 'short' } }
*/
lastUpdated?: false | LastUpdatedOptions
/**
* @deprecated Use `lastUpdated.text` instead.
* @deprecated 使 `lastUpdated.text` .
*
* Set custom last updated text.
* "最后更新时间"
*
* @default 'Last updated'
*/
lastUpdatedText?: string
/**
* Page meta - contributors config
*
* Whether to show "Contributors" or not
*
*/
contributors?: boolean
/**
* Page meta - contributors config
*
* The text to replace the default "Contributors"
*
*/
contributorsText?: string
// backToHome?: string
/**
* sidebar menu label
*
*
* @default 'Menu'
*/
sidebarMenuLabel?: string
/**
* return to top label
*
*
* @default 'return to top'
*/
returnToTopLabel?: string
@ -193,15 +198,30 @@ export interface PlumeThemeLocaleData extends LocaleData {
*/
outlineLabel?: string
/**
*
*
* @default 'Previous Page'
*/
prevPageLabel?: string
/**
*
*
* @default 'Next Page'
*/
nextPageLabel?: string
/**
*
*
* @default true
*/
externalLinkIcon?: string
/**
*
*/
footer?:
| false
| {
@ -210,7 +230,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
}
/**
* 404 page options
* 404
*/
notFound?: {
code?: string | number