Merge pull request #163 from pengzhanbo/perf-blog

feat(theme): add support blog as home page
This commit is contained in:
pengzhanbo 2024-09-04 00:53:00 +08:00 committed by GitHub
commit bc527fdd4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 924 additions and 485 deletions

View File

@ -35,7 +35,7 @@
"cac": "^6.7.14",
"execa": "^9.3.1",
"handlebars": "^4.7.8",
"picocolors": "^1.0.1"
"picocolors": "^1.1.0"
},
"theme-plume": {
"vuepress": "2.0.0-rc.15"

View File

@ -1,5 +1,4 @@
import process from 'node:process'
import path from 'node:path'
import { createRequire } from 'node:module'
import { cancel, confirm, group, select, text } from '@clack/prompts'
import { setLang, t } from './translate.js'
@ -132,34 +131,3 @@ export async function prompt(mode: Mode, root?: string): Promise<PromptResult> {
return result
}
export async function getTargetDir(cwd: string, dir?: string) {
if (dir === '.')
return cwd
if (typeof dir === 'string' && dir) {
return path.resolve(cwd, dir)
}
const DEFAULT_DIR = 'my-project'
const dirPath = await text({
message: t('question.root'),
placeholder: DEFAULT_DIR,
validate(value) {
if (value && REG_DIR_CHAR.test(value))
return t('hint.root.illegal')
return undefined
},
defaultValue: DEFAULT_DIR,
})
if (typeof dirPath === 'string') {
if (dirPath === '.')
return cwd
return path.join(cwd, dirPath || DEFAULT_DIR)
}
return dirPath
}

View File

@ -1,6 +1,5 @@
---
pageLayout: home
externalLinkIcon: false
config:
-
type: hero

View File

@ -25,41 +25,47 @@ VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_
与 vuepress 默认主题相比:
- **更好的用户体验**
### 更好的用户体验
大幅度优化了界面、交互,更为美观、简洁,易用。
大幅度优化了界面、交互,更为美观、简洁,易用。
- **更多的功能**
### 更多的功能
代码分组、提示容器、任务列表、数学公式、代码演示、内容搜索、文章评论、加密、文件树 等。
- 灵活的,可完全自定义的 ==首页==
- **更好的开发体验**
- 可选的 ==博客== 、==文档== 、==知识笔记==
增加编译缓存,缓存 markdown 文件编译、缓存 复杂 代码块 内容解析结果。
- 内置支持 ==全文搜索== 、==文章评论== 、==内容加密== 、 ==文章水印== 等;
- **更好的配置**
- 可对 ==代码块== 进行 分组、折叠、聚焦、行高亮、差异对比 等,还可以 嵌入 CodePen、JSFiddle、CodeSandbox 等作为 ==代码演示==
支持使用单独的主题配置文件,避免修改配置导致频繁重启 VuePress 服务。
- 内置支持 [iconify](https://icon-sets.iconify.design/) **200k+** ==图标==
大幅度简化了配置,更易于使用,同时还保留了丰富灵活的配置项,满足个性化的需求。
- 支持 ==嵌入 PDF==、==嵌入 Bilibili/Youtube 视频==
- 支持 chart.js、Echarts、Mermaid、flowchart 等多种可选的 ==图表==
- 灵活的 markdown 容器语法,支持 ==提示容器==、==文件树==、==示例容器== 等;
- 支持 ==布局插槽==、==组件覆写== ==自定义样式== 等,你可以灵活地扩展组件,实现 个性化的 布局。
更多的功能等待你的发现
### 更好的开发体验
增加编译缓存,缓存 markdown 文件编译、缓存 复杂 代码块 内容解析结果。
### 更好的配置
支持使用单独的主题配置文件,避免修改配置导致频繁重启 VuePress 服务。
大幅度简化了配置,更易于使用,同时还保留了丰富灵活的配置项,满足个性化的需求。
### 更多
==plume 主题== 尽可能的内置你可能需要的功能,以及搭建站点所需要的一般性配置,您无需关注这些细节。
目的是,让您更专注于 内容的创作,更好的表达你的想法,享受 Markdown 增强语法带来的便利。
## 功能
- 💻 响应式布局,适配不同的屏幕尺寸
- 📖 博客 & 文档
- 🔗 自动生成文章永久链接
- ⚖ 支持多语言
- 🔑 支持 全站加密、部分加密
- 👀 支持 搜索、文章评论
- 👨‍💻‍ 支持 浅色/深色 主题 (包括代码高亮)
- 📠 markdown 增强,支持 代码块分组、提示容器、任务列表、数学公式、代码演示、文件树 等
- 📚 代码演示,支持 CodePen, Replit, JSFiddle, CodeSandbox
- 📊 嵌入图标,支持 chart.jsEchartsMermaidflowchart 等
- 🎛 资源嵌入,支持 PDF, bilibili视频youtube视频等
::: tip
本主题 基于 [vuepress-next](https://github.com/vuepress/vuepress-next), 目前处于 RC 阶段。

View File

@ -17,10 +17,12 @@ import VPBlogProfile from 'vuepress-theme-plume/components/Blog/VPBlogProfile.vu
主题默认启用 博客功能,通常您无需进行额外的配置。
主题默认会将 `{sourceDir}` 目录下的,除了一些特定的目录(如 `notes` 目录将作为笔记所在目录),
主题默认会将 `{sourceDir}` 目录下的,除了特定的目录(如 `notes` 目录将作为笔记所在目录),
所有 md 文件作为博客文章。
同时,主题会生成一个 链接地址为 `/blog/` 的 博客文章列表页。
主题还会根据 md 文件 所在的 文件目录结构,以 **目录名** 作为 博客文章所属的 **分类**
主题默认会生成一个 链接地址为 `/blog/` 的 博客文章列表页。
展示所有的博客文章,以及 博主的相关信息。
## 国际化支持
@ -112,8 +114,92 @@ title: 标题
主题除了自动生成 **博客文章列表页** 以外,还会自动生成 **标签页****分类页** 和 **归档页**
标签页 可以根据 标签 筛选并展示 博客文章
标签页 可以根据 标签 筛选并展示 博客文章 默认地址为 `/blog/tags/`
分类页 可以根据 原始目录结构 分类展示 博客文章
分类页 可以根据 原始目录结构 分类展示 博客文章, 默认地址为 `/blog/categories/`
归档页根据文章的创建时间进行归档。
归档页根据文章的创建时间进行归, 默认地址为 `/blog/archives/`
## 设置博客为主页
默认情况下,主题 将 **首页****博客页** 分离。
但对于 希望只建立一个 博客站点 的用户而言,可能直接将 博客页 作为 **首页** 是个更好的选择。
主题提供了两种方式来设置博客为主页,满足不同的需求场景:
- **方式一:配置 主页的 `pageLayout` 属性为 `blog`**
::: code-tabs
@tab docs/README.md
```md
---
pageLayout: blog
---
```
:::
此配置会直接将页面应用 博客布局,显示博客文章列表。
这是将主页修改为 博客页的 最简单的方式,但缺点是 缺少灵活性。
- **方式二:配置 主页的 `pageLayout` 属性为 `home`, 添加 `type: blog` 的首页区域类型**
::: code-tabs
@tab docs/README.md
```md
---
pageLayout: home
config:
- type: blog
---
```
:::
使用这种方式,你不仅可以在首页中添加 博客文章列表,还可以灵活的在页面的其他区域添加不同的内容。
比如,配置首屏为 `banner`,然后紧跟着 博客文章列表:
::: code-tabs
@tab docs/README.md
```md
---
pageLayout: home
config:
- type: banner
- type: blog
---
```
:::
更多自定义配置,请参考 [自定义首页](./自定义首页.md)。
当使用以上两种方式 将首页配置为 博客页后,由于主题默认依然会生成 地址为`/blog/` 的博客文章列表页,
这导致存在了重复功能的页面,为此,你需要 [主题配置 > 博客配置](../config/主题配置.md#blog) 中,
**关闭自动生成博客文章列表页**
(还可以在重新修改 分类页/标签页/归档页的链接地址)
```ts
import { plumeTheme } from 'vuepress-theme-plume'
import { defineUserConfig } from 'vuepress'
export default defineUserConfig({
theme: plumeTheme({
blog: {
postList: false, // 禁止生成博客文章列表页
// tagsLink: '/blog/tags/',
// categoriesLink: '/blog/categories/',
// archiveLink: '/blog/archives/',
}
})
})
```

View File

@ -270,23 +270,26 @@ cd open-source # 进入 D: 分区下的 open-source 目录
- docs \# 文档源目录
- .vuepress
- public/ \# 静态资源目录
- client.ts
- config.ts \# vuepress 配值文件
- client.ts \# 客户端配置
- config.ts \# vuepress 配值
- navbar.ts \# 导航栏配置
- notes.ts \# notes 配置
- plume.config.ts \# 主题配置文件
- notes
- plume.config.ts \# 主题配置
- notes \# 系列文档、知识笔记
- demo
- foo.md
- bar.md
- preview
- preview \# 博客分类
- markdown.md
- README.md \# 首页
- package.json
- package-lock.json
- pnpm-lock.yaml
- .gitignore
- README.md
:::
`docs` 目录中, 除 `.vuepress` 目录外,目录中的 所有 markdown 文件都会被识别为文档。
其中,除 `notes` 目录外的 `markdown` 文件会被识别为 博客文章,而 `notes` 目录下 `markdown` 文件会被识别为 文档笔记。
- 除 `notes` 目录外的 `markdown` 文件会被识别为 博客文章,并根据其所在的目录结构,作为 文章分类。
- `notes` 目录下 `markdown` 文件会被识别为 文档笔记。

View File

@ -11,52 +11,55 @@ tags:
## 概述
在本主题满足了 Blog 的基本功能后,期望能够 以 note 或者 book 的形式聚合文章,形式上类似于 vuepress 默认主题。
同时也减少配置的复杂度。
主题提供了 `笔记` 的功能,它用于聚合 同一个系列的文章、或者作为站点的 **子文档**
它能够让你以更加友好的方式,组织管理你的文档。
`笔记` 以 文件结构 作为划分依据,默认以 `notes/` 作为根目录,
存放在 `notes` 目录下的 文档不会作为 博客文章,不会出现在 博客文章列表页中。
## 目录
## 文件结构与配置
文档/知识笔记 默认存放在 `{sourceDir}/notes` 目录下。
示例:
我们有一个项目中,有以下文件结构:
::: file-tree
- \{sourceDir\}
- docs
- notes
- typescript
- foo.md
- rust
- foo.md
- typescript \# typescript 笔记
- basic.md
- types.md
- rust \# rust 笔记
- tuple.md
- struct.md
- blog-post.md \# 博客文章
- README.md \# 站点首页
:::
其中,`typescript``rust` 为目录名,各自独立保存与之相关的 markdown 文件。
`docs/notes` 目录下,有两个子目录,分别用于存放 `typescript``rust` 的系列内容
## 配置
接下来,在配置文件中配置 `notes`
```js
```js :collapsed-lines=20
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
theme: plumeTheme({
notes: {
dir: '/notes/', // 声明所有笔记的目录
link: '/', // 声明所有笔记默认的链接前缀, 默认为 '/'
// 声明所有笔记的目录,(默认配置,通常您不需要声明它)
dir: '/notes/',
link: '/', // 声明所有笔记默认的链接前缀, 默认为 '/' (默认配置,通常您不需要声明它)
notes: [
// 每个笔记都是 `notes` 数组中的一个对象
{
dir: 'typescript', // 声明笔记的目录,相对于 `notes.dir`
link: '/typescript/', // 声明笔记的链接前缀
sidebar: [ // 配置侧边栏
{
text: '简介',
icon: 'mdi:language-typescript', // 侧边栏图标
items: ['foo'] // 简化写法,主题会自动补全为 `foo.md`
}
]
// 声明笔记的目录,相对于 `notes.dir`,这里表示 `notes/typescript` 目录
dir: 'typescript',
// 声明笔记的链接前缀,与 `notes.link` 拼接,这里表示 `/typescript/`
// 笔记内的所有文章会以 `/typescript/` 作为访问链接前缀。
link: '/typescript/',
// 配置 笔记侧边导航栏,用于导航向笔记内的所有文档
// 声明为 `auto` 的,将根据目录结构自动生成侧边栏导航
sidebar: 'auto'
},
{
dir: 'rust',
@ -71,13 +74,241 @@ export default defineUserConfig({
})
```
主题会根据配置,为对应目录中的 md 文件,生成 永久链接,以及侧边栏。
::: tip
你应该在创建文件之前,先把笔记的目录和链接前缀等配置好,主题需要根据配置,
为目录中的 md 文件生成永久链接,以及侧边栏。
你应该在创建文件之前,建议先把笔记的目录和链接前缀等配置好。
主题默认启用了 [auto-frontmatter](../config/主题配置.md#autofrontmatter)
需要根据配置,为目录中的 md 文件生成永久链接,以及侧边栏。
:::
完整配置查看 [notes配置](../config/notes配置.md)
### 侧边栏配置
`typescript` 目录为例,它拥有如下的文件结构:
::: file-tree
- typescript
- guide
- intro.md
- getting-start.md
- config
- config-file.md
- configuration.md
- reference
- basic.md
- syntax.md
- modules.md
- built-in
- types
- Required.md
- Omit.md
- README.md
:::
#### 自动生成侧边栏
一种最简单的配置方式是 `sidebar: 'auto'` 主题会自动根据 文件结构生成侧边栏,并根据 首个字符的编码 来排序。
如果想要修改 自动生成的侧边栏的顺序,可以直接在 目录名 或 文件名之前,添加 `1.``2.` 等前缀。
::: file-tree
- typescript
- 1.guide
- 1.intro.md
- 2.getting-start.md
- 2.config
- 1.config-file.md
- 2.configuration.md
- …
:::
主题将根据 这部分的前缀的 数字 进行排序,前缀部分不会显示在侧边栏中。
#### 自定义侧边栏
有时候自动生成侧边栏 不能完全满足需求,你可以自定义侧边栏。
以下是 侧边栏的 类型定义:
```ts
type Sidebar = (string | SidebarItem)[]
interface SidebarItem {
/**
* 侧边栏文本
*/
text?: string
/**
* 侧边栏链接
*/
link?: string
/**
* 侧边栏图标
*/
icon?: ThemeIcon
/**
* 当前分组的链接前缀,链接前缀会拼接在 `items` 内的 `link` 之前
* 当且仅当 `items` 内的 `link` 为 相对路径时,才会拼接
*/
prefix?: string
/**
* 次级侧边栏分组
*/
items?: 'auto' | (string | SidebarItem)[]
/**
* 如果未指定,组不可折叠。
* 如果为`true`,组可折叠,并默认折叠。
* 如果为`false`,组可折叠,但默认展开。
*/
collapsed?: boolean
}
```
当 传入类型为 `string` 时,表示 markdown 文件的路径:
```ts
const sidebar: Sidebar = [
'/guide/intro.md',
'/guide/getting-start.md',
'/config/config-file.md',
// ...
]
```
你也可以省略 `.md` 文件后缀,简写为 `/guide/intro` 。主题会解析 对应的文件,获取 **标题** 和 **页面链接地址**
并将其转换为 `{ text: string, link: string }` 的数据形式。
当传入类型为 `SidebarItem` 时:
```ts
const sidebar: Sidebar = [
{ text: '介绍', link: '/guide/intro' },
{ text: '快速上手', link: '/guide/getting-start' },
// ...
]
```
也可以进行多层嵌套:
```ts
const sidebar: Sidebar = [
{
text: '指南',
prefix: '/guide', // 使用 prefix 拼接,可以简写 下面的 items 中的 link 为相对路径
items: [
// 可以混用 string 和 SidebarItem
{ text: '介绍', link: 'intro' },
'getting-start',
],
},
{
text: '配置',
prefix: '/config',
items: 'auto', // items 为 'auto',会根据 prefix 的文件结构自动生成侧边栏
},
]
```
:::warning
不建议 侧边栏的层级过深,超过 3 层的侧边栏 可能会导致 糟糕的 UI 效果。
:::
### 侧边栏图标
为侧边栏添加 图标 有助于 侧边栏更好的呈现。得益于 [iconify](https://iconify.design/) 这个强大的开源图标库,
你可以使用超过 `200k` 的图标,仅需要添加 `icon` 配置即可。
```ts
const sidebar: Sidebar = [
{
text: '指南',
prefix: '/guide',
icon: 'ep:guide', // iconify icon name // [!code hl]
items: [
{ text: '介绍', link: 'intro', icon: 'ph:info-light' }, // [!code hl]
],
},
]
```
也可以使用本地图标,或者本地图片:
```ts
const sidebar: Sidebar = [
{
text: '指南',
prefix: '/guide',
icon: '/images/guide.png', // iconify icon name // [!code hl]
items: [
{ text: '介绍', link: 'intro', icon: '/images/info.png' }, // [!code hl]
// 也可以是一个远程图片
{ text: '快速上手', link: 'getting-start', icon: 'https://cn.vuejs.org/images/logo.png' },
],
},
]
```
**请注意,使用本地图片必须以 `/` 开头,表示为 静态资源路径,它将从 `.vuepress/public/` 目录中加载。**
::: file-tree
- docs
- .vuepress
- public \# 在这个位置保存静态资源
- images
- guide.png
- info.png
- …
:::
你可能已经注意到,`sidebar: auto` 时,该如何配置 侧边栏图标,事实上很简单,直接在 文件的 `frontmatter` 部分,
添加 一个 `icon` 字段即可。
::: code-tabs
@tab typescript/guide/intro.md
```md
---
title: 介绍
icon: ep:guide
---
```
:::
## 笔记首页
你可能注意到,在 `typescript` 目录下,有一个 `README.md` 文件,它会被作为笔记首页显示。
::: file-tree
- typescript
- README.md
- …
- …
:::
默认情况下,它与 普通的文档页面 没有区别,这是因为 主题 默认对 所有页面 设置了 `pageLayout: docs`
但你可以直接配置 `pageLayout: 'home'`,就像配置 [站点首页](./自定义首页.md) 一样,为 笔记配置一个个性化的首页!
::: code-tabs
@tab typescript/README.md
```md
---
pageLayout: home
config:
- type: hero
- type: features
---
```
:::

View File

@ -436,6 +436,10 @@ config:
<img src="/images/custom-text-image.png" alt="text-image" />
:::
### blog
将 博客文章列表页 作为一个单独区域,插入到 首页中。
### profile
- 类型: `PlumeThemeHomeProfile`

View File

@ -19,8 +19,8 @@
"echarts": "^5.5.1",
"flowchart.ts": "^3.0.1",
"http-server": "^14.1.1",
"mermaid": "^11.0.2",
"vue": "^3.4.38",
"mermaid": "^11.1.0",
"vue": "^3.5.0",
"vuepress-theme-plume": "workspace:*"
},
"devDependencies": {

View File

@ -40,7 +40,7 @@
"vuepress": "2.0.0-rc.15"
},
"dependencies": {
"vue": "^3.4.38"
"vue": "^3.5.0"
},
"publishConfig": {
"access": "public"

View File

@ -44,10 +44,10 @@
"@vueuse/core": "^11.0.3",
"markdown-it-container": "^4.0.0",
"nanoid": "^5.0.7",
"shiki": "^1.15.2",
"tm-grammars": "^1.17.11",
"shiki": "^1.16.1",
"tm-grammars": "^1.17.14",
"tm-themes": "^1.8.1",
"vue": "^3.4.38"
"vue": "^3.5.0"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2"

View File

@ -67,20 +67,16 @@ function runCode() {
</p>
<div v-if="stderr.length" class="stderr">
<h4>Stderr:</h4>
<p
<pre
v-for="(item, index) in stderr" :key="index"
:class="{ error: lang === 'rust' && item.startsWith('error') }"
>
<pre>{{ item }}</pre>
</p>
>{{ item }}</pre>
</div>
<div v-if="stdout.length" class="stdout">
<h4 v-if="stderr.length">
Stdout:
</h4>
<p v-for="(item, index) in stdout" :key="index">
<pre>{{ item }}</pre>
</p>
<pre v-for="(item, index) in stdout" :key="index">{{ item }}</pre>
</div>
</div>
</div>
@ -214,13 +210,13 @@ function runCode() {
font-size: 16px;
}
.output-content p {
.output-content pre {
margin: 0;
font-size: 14px;
line-height: 20px;
}
.output-content p pre {
.output-content pre {
width: fit-content;
padding: 0 20px 0 0;
margin: 0;
@ -228,13 +224,13 @@ function runCode() {
}
.output-content .error,
.output-content .stderr p,
.output-content.rust .stderr p.error {
.output-content .stderr pre,
.output-content.rust .stderr pre.error {
color: var(--vp-c-danger-1, #b8272c);
transition: color var(--t-color);
}
.output-content.rust .stderr p {
.output-content.rust .stderr pre {
color: var(--vp-c-text-1);
}

View File

@ -48,7 +48,7 @@
"mark.js": "^8.11.1",
"minisearch": "^7.1.0",
"p-map": "^7.0.2",
"vue": "^3.4.38"
"vue": "^3.5.0"
},
"publishConfig": {
"access": "public"

View File

@ -36,8 +36,8 @@
"vuepress": "2.0.0-rc.15"
},
"dependencies": {
"@shikijs/transformers": "^1.15.2",
"@shikijs/twoslash": "^1.15.2",
"@shikijs/transformers": "^1.16.1",
"@shikijs/twoslash": "^1.16.1",
"@types/hast": "^3.0.4",
"@vuepress/helper": "2.0.0-rc.42",
"@vueuse/core": "^11.0.3",
@ -46,9 +46,9 @@
"mdast-util-gfm": "^3.0.0",
"mdast-util-to-hast": "^13.2.0",
"nanoid": "^5.0.7",
"shiki": "^1.15.2",
"twoslash": "^0.2.9",
"twoslash-vue": "^0.2.9"
"shiki": "^1.16.1",
"twoslash": "^0.2.10",
"twoslash-vue": "^0.2.10"
},
"publishConfig": {
"access": "public"

728
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -103,12 +103,12 @@
"katex": "^0.16.11",
"local-pkg": "^0.5.0",
"nanoid": "^5.0.7",
"vue": "^3.4.38",
"vue-router": "^4.4.3",
"vue": "^3.5.0",
"vuepress-plugin-md-enhance": "2.0.0-rc.52",
"vuepress-plugin-md-power": "workspace:*"
},
"devDependencies": {
"@iconify/json": "^2.2.243"
"@iconify/json": "^2.2.243",
"vue-router": "^4.4.3"
}
}

View File

@ -9,11 +9,17 @@ import VPBlogNav from '@theme/Blog/VPBlogNav.vue'
import VPTransitionFadeSlideY from '@theme/VPTransitionFadeSlideY.vue'
import { useData } from '../../composables/index.js'
defineProps<{
homeBlog?: boolean
type?: string
onlyOnce?: boolean
}>()
const { theme, page } = useData()
</script>
<template>
<div class="vp-blog">
<div class="vp-blog" :class="{ 'home-blog': homeBlog }">
<slot name="blog-top" />
<div class="blog-container" :class="{ 'no-profile': !theme.profile }">
@ -53,7 +59,7 @@ const { theme, page } = useData()
<slot name="blog-categories-content-before" />
</template>
</VPBlogCategories>
<VPPostList v-else>
<VPPostList v-else :home-blog="homeBlog">
<template #blog-post-list-before>
<slot name="blog-post-list-before" />
</template>
@ -96,6 +102,12 @@ const { theme, page } = useData()
transition: background-color var(--t-color);
}
@media(min-width: 419px) {
.vp-blog.home-blog {
background-color: var(--vp-c-bg-alt);
}
}
.blog-container {
display: flex;
align-items: flex-start;

View File

@ -1,9 +1,14 @@
<script lang="ts" setup>
import { computed } from 'vue'
import VPTransitionDrop from '@theme/VPTransitionDrop.vue'
import VPPostItem from '@theme/Blog/VPPostItem.vue'
import VPPagination from '@theme/Blog/VPPagination.vue'
import { usePostListControl } from '../../composables/index.js'
const props = defineProps<{
homeBlog?: boolean
}>()
const {
postList,
page,
@ -13,7 +18,7 @@ const {
isFirstPage,
isPaginationEnabled,
changePage,
} = usePostListControl()
} = usePostListControl(computed(() => !!props.homeBlog))
</script>
<template>

View File

@ -1,13 +1,33 @@
<script lang="ts" setup>
import { type Component, computed, nextTick, onUnmounted, resolveComponent, watch } from 'vue'
import type { Component } from 'vue'
import { computed, h, nextTick, onUnmounted, resolveComponent, watch } from 'vue'
import VPHomeBanner from '@theme/Home/VPHomeBanner.vue'
import VPHomeHero from '@theme/Home/VPHomeHero.vue'
import VPHomeFeatures from '@theme/Home/VPHomeFeatures.vue'
import VPHomeTextImage from '@theme/Home/VPHomeTextImage.vue'
import VPHomeProfile from '@theme/Home/VPHomeProfile.vue'
import VPHomeCustom from '@theme/Home/VPHomeCustom.vue'
import VPBlog from '@theme/Blog/VPBlog.vue'
import { useData } from '../../composables/index.js'
const slots = defineSlots<{
'blog-top': () => any
'blog-bottom': () => any
'blog-post-list-before': () => any
'blog-post-list-after': () => any
'blog-post-list-pagination-after': () => any
}>()
function VPHomeBlog() {
return h(VPBlog, { homeBlog: true }, {
'blog-top': () => slots['blog-top']?.(),
'blog-bottom': () => slots['blog-bottom']?.(),
'blog-post-list-before': () => slots['blog-post-list-before']?.(),
'blog-post-list-after': () => slots['blog-post-list-after']?.(),
'blog-post-list-pagination-after': () => slots['blog-post-list-pagination-after']?.(),
})
}
const components: Record<string, Component<any, any, any>> = {
'banner': VPHomeBanner,
'hero': VPHomeHero,
@ -15,6 +35,7 @@ const components: Record<string, Component<any, any, any>> = {
'text-image': VPHomeTextImage,
'image-text': VPHomeTextImage,
'profile': VPHomeProfile,
'blog': VPHomeBlog,
'custom': VPHomeCustom,
}

View File

@ -99,7 +99,23 @@ watch([isBlogLayout, () => frontmatter.value.pageLayout], () => nextTick(() =>
<VPFriends v-else-if="frontmatter.pageLayout === 'friends'" />
<VPHome v-else-if="frontmatter.pageLayout === 'home'" />
<VPHome v-else-if="frontmatter.pageLayout === 'home'">
<template #blog-top>
<slot name="blog-top" />
</template>
<template #blog-bottom>
<slot name="blog-bottom" />
</template>
<template #blog-post-list-before>
<slot name="blog-post-list-before" />
</template>
<template #blog-post-list-after>
<slot name="blog-post-list-after" />
</template>
<template #blog-post-list-pagination-after>
<slot name="blog-post-list-pagination-after" />
</template>
</VPHome>
<component :is="frontmatter.pageLayout" v-else-if="frontmatter.pageLayout && frontmatter.pageLayout !== 'doc'" />

View File

@ -1,4 +1,4 @@
import { computed } from 'vue'
import { type Ref, computed } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useLocalePostList } from './blog-data.js'
@ -7,7 +7,7 @@ import { useRouteQuery } from './route-query.js'
const DEFAULT_PER_PAGE = 10
export function usePostListControl() {
export function usePostListControl(homePage: Ref<boolean>) {
const { theme } = useData()
const list = useLocalePostList()
@ -111,7 +111,15 @@ export function usePostListControl() {
if (page.value === current)
return
page.value = current
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
setTimeout(() => {
let top = 0
if (homePage.value) {
top = document.querySelector('.vp-blog')?.getBoundingClientRect().top || 0
top += window.scrollY - 64
}
window.scrollTo({ top, behavior: 'instant' })
}, 0)
}
return {

View File

@ -67,3 +67,15 @@ export function scrollTo(
window.scrollTo({ top, behavior: 'smooth' })
}
}
export function getOffsetTop<T extends HTMLElement = HTMLElement>(target: T | null): number {
if (!target)
return 0
let parent: HTMLElement | null = target
let top = 0
while (parent) {
top += parent.offsetTop
parent = parent.offsetParent as HTMLElement
}
return top
}

View File

@ -1,6 +1,6 @@
export * from './shared.js'
export * from './dom.js'
// export * from './dom.js'
export * from './resolveEditLink.js'
export * from './resolveRepoType.js'
export * from './resolveNavLink.js'
export * from './animate.js'
// export * from './animate.js'

View File

@ -8,11 +8,12 @@ const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
appearance: true,
blog: {
link: '/blog/',
pagination: { perPage: 15 },
postList: true,
tags: true,
archives: true,
categories: true,
link: '/blog/',
tagsLink: '/blog/tags/',
archivesLink: '/blog/archives/',
categoriesLink: '/blog/categories/',

View File

@ -39,10 +39,12 @@ export async function setupPage(
const locale = localePath === '/' ? rootPath : localePath
// 添加 博客页面
pageList.push(createPage(app, {
path: withBase(link, localePath),
frontmatter: { lang, _pageLayout: 'blog', title: getTitle(locale, 'blog') },
}))
if (blog.postList !== false) {
pageList.push(createPage(app, {
path: withBase(link, localePath),
frontmatter: { lang, _pageLayout: 'blog', title: getTitle(locale, 'blog') },
}))
}
// 添加 标签页
if (blog.tags !== false) {
@ -108,6 +110,11 @@ export function extendsPageData(
delete page.frontmatter._pageLayout
}
if (page.frontmatter.pageLayout === 'blog') {
page.frontmatter.draft = true
page.data.type = 'blog'
}
if ('externalLink' in page.frontmatter) {
page.frontmatter.externalLinkIcon = page.frontmatter.externalLink
delete page.frontmatter.externalLink

View File

@ -16,13 +16,6 @@ export type PlumeThemeBlogPostData = PlumeThemeBlogPostItem[]
export interface PlumeThemeBlog {
/**
*
*
* @default '/blog/'
*/
link?: string
/**
* glob string
*
@ -55,6 +48,19 @@ export interface PlumeThemeBlog {
perPage?: number
}
/**
*
*
* @default '/blog/'
*/
link?: string
/**
*
* @default true
*/
postList?: boolean
/**
*
* @default true

View File

@ -19,7 +19,7 @@ export interface PlumeNormalFrontmatter extends PageFrontmatter {
/**
* page layout
*/
pageLayout?: false | 'home' | 'doc' | 'custom' | 'page' | 'friends'
pageLayout?: false | 'home' | 'blog' | 'doc' | 'custom' | 'page' | 'friends'
/**
* class