Merge pull request #117 from pengzhanbo/RC-78

RC 78
This commit is contained in:
pengzhanbo 2024-07-14 03:13:25 +08:00 committed by GitHub
commit 9308355c04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 540 additions and 105 deletions

View File

@ -116,8 +116,8 @@ export const zhNotes = definePlumeNotesConfig({
{
text: 'plugin-netlify-functions',
dir: 'netlify-functions',
link: 'plugin-netlify-functions/',
collapsed: false,
link: '/plugins/plugin-netlify-functions/',
collapsed: true,
items: [
'介绍',
'使用',

View File

@ -98,6 +98,12 @@ interface BlogOptions {
* @default true
*/
archives?: boolean
/**
* 是否启用分类页
* @default true
*/
categories?: boolean
}
```

View File

@ -110,10 +110,12 @@ title: 标题
这里的内容不会被作为摘要
```
## 标签页和归档页
## 标签页,分类页和归档页
主题除了自动生成 **博客文章列表页** 以外,还会自动生成 **标签页****归档页**
主题除了自动生成 **博客文章列表页** 以外,还会自动生成 **标签页****分类页****归档页**
标签页 可以根据 标签 筛选并展示 博客文章。
分类页 可以根据 原始目录结构 分类展示 博客文章。
归档页根据文章的创建时间进行归档。

View File

@ -4,6 +4,7 @@ import VPBlogArchives from '@theme/Blog/VPBlogArchives.vue'
import VPBlogAside from '@theme/Blog/VPBlogAside.vue'
import VPBlogExtract from '@theme/Blog/VPBlogExtract.vue'
import VPBlogTags from '@theme/Blog/VPBlogTags.vue'
import VPBlogCategories from '@theme/Blog/VPBlogCategories.vue'
import VPBlogNav from '@theme/Blog/VPBlogNav.vue'
import VPTransitionFadeSlideY from '@theme/VPTransitionFadeSlideY.vue'
import { useData } from '../../composables/data.js'
@ -35,6 +36,14 @@ const { theme, page } = useData()
<slot name="blog-tags-after" />
</template>
</VPBlogTags>
<VPBlogCategories v-else-if="page.type === 'blog-categories'">
<template #blog-categories-before>
<slot name="blog-categories-before" />
</template>
<template #blog-categories-after>
<slot name="blog-tags-after" />
</template>
</VPBlogCategories>
<VPPostList v-else>
<template #blog-post-list-before>
<slot name="blog-post-list-before" />

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
import VPShortPostList from '@theme/Blog/VPShortPostList.vue'
import { useBlogExtract } from '../../composables/blog-extract.js'
import { useBlogNavTitle } from '../../composables/blog-extract.js'
import { useArchives } from '../../composables/blog-archives.js'
const { archives: archivesLink } = useBlogExtract()
const title = useBlogNavTitle('archive')
const { archives } = useArchives()
</script>
@ -13,7 +13,7 @@ const { archives } = useArchives()
<h2 class="archives-title">
<span class="vpi-archive icon" />
<span>{{ archivesLink.text }}</span>
<span>{{ title }}</span>
</h2>
<div v-if="archives.length" class="archives">
<template v-for="archive in archives" :key="archive.label">

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import VPCategories from '@theme/Blog/VPCategories.vue'
import { useBlogCategory } from '../../composables/blog-category.js'
import { useBlogNavTitle } from '../../composables/blog-extract.js'
const title = useBlogNavTitle('category')
const { categories } = useBlogCategory()
</script>
<template>
<div class="vp-blog-categories">
<slot name="blog-categories-before" />
<h2 class="categories-title">
<span class="vpi-category icon" />
<span>{{ title }}</span>
</h2>
<div class="content">
<VPCategories :items="categories" />
</div>
<slot name="blog-categories-after" />
</div>
</template>
<style scoped>
.vp-blog-categories {
flex: 1;
padding: 32px 0;
margin: 0 auto;
transition: background-color var(--t-color), box-shadow var(--t-color);
}
@media (min-width: 768px) {
.vp-blog-categories {
padding: 20px 0;
margin: 32px auto 32px 20px;
background-color: var(--vp-c-bg);
border-radius: 8px;
box-shadow: var(--vp-shadow-1);
}
.vp-blog-categories:hover {
box-shadow: var(--vp-shadow-2);
}
}
.categories-title {
display: flex;
align-items: center;
padding: 0 20px;
font-size: 20px;
font-weight: 700;
color: var(--vp-c-text-1);
transition: color var(--t-color);
}
.categories-title .icon {
margin-right: 8px;
}
@media (min-width: 768px) {
.categories-title {
padding-bottom: 20px;
margin-top: 0;
border-bottom: solid 1px var(--vp-c-divider);
transition: border-bottom var(--t-color);
}
}
@media (min-width: 1200px) {
.vp-blog-categories {
margin-left: 0;
}
}
.vp-blog-categories .content {
padding: 20px 20px 0;
}
</style>

View File

@ -21,7 +21,7 @@ const imageUrl = computed(() => {
return withBase(url)
})
const { hasBlogExtract, tags, archives } = useBlogExtract()
const { hasBlogExtract, tags, archives, categories } = useBlogExtract()
const open = ref(false)
const lazyOpen = ref(false)
@ -92,6 +92,10 @@ const showBlogExtract = computed(() => {
<span class="vpi-tag icon" />
<span>{{ tags.text }}</span>
</VPLink>
<VPLink class="nav-link" :href="categories.link" no-icon>
<span class="vpi-category icon" />
<span>{{ categories.text }}</span>
</VPLink>
<VPLink class="nav-link" :href="archives.link" no-icon>
<span class="vpi-archive icon" />
<span>{{ archives.text }}</span>

View File

@ -9,7 +9,7 @@ const props = defineProps<{
const route = useRoute()
const { hasBlogExtract, tags, archives } = useBlogExtract()
const { hasBlogExtract, tags, archives, categories } = useBlogExtract()
</script>
<template>
@ -24,6 +24,16 @@ const { hasBlogExtract, tags, archives } = useBlogExtract()
<span class="total">{{ tags.total }}</span>
<span class="icon vpi-chevron-right" />
</VPLink>
<VPLink
class="nav-link"
:class="{ active: route.path === categories.link }"
:href="categories.link"
>
<span class="icon icon-logo vpi-category" />
<span class="text">{{ categories.text }}</span>
<span class="total">{{ categories.total }}</span>
<span class="icon vpi-chevron-right" />
</VPLink>
<VPLink
class="nav-link"
:class="{ active: route.path === archives.link }"

View File

@ -1,10 +1,10 @@
<script lang="ts" setup>
import VPShortPostList from '@theme/Blog/VPShortPostList.vue'
import { useBlogExtract } from '../../composables/blog-extract.js'
import { useBlogNavTitle } from '../../composables/blog-extract.js'
import { useTags } from '../../composables/blog-tags.js'
const { tags, currentTag, postList, handleTagClick } = useTags()
const { tags: tagsLink } = useBlogExtract()
const title = useBlogNavTitle('tag')
</script>
<template>
@ -14,7 +14,7 @@ const { tags: tagsLink } = useBlogExtract()
<div class="tags-nav">
<h2 class="tags-title">
<span class="vpi-tag icon" />
<span>{{ tagsLink.text }}</span>
<span>{{ title }}</span>
</h2>
<div class="tags">
<p

View File

@ -0,0 +1,60 @@
<script lang="ts" setup>
import VPLink from '@theme/VPLink.vue'
import VPCategoriesGroup from '@theme/Blog/VPCategoriesGroup.vue'
import type { BlogCategoryItem, BlogCategoryItemWithPost } from '../../composables/blog-category.js'
defineProps < {
items: (BlogCategoryItem | BlogCategoryItemWithPost)[]
}>()
</script>
<template>
<ul class="vp-categories">
<li
v-for="item in items"
:key="(item as BlogCategoryItemWithPost).path || (item as BlogCategoryItem).id"
class="vp-categories-item"
>
<p v-if="item.type === 'post'" class="post">
<span class="vpi-post" />
<VPLink
:href="item.path"
:text="item.title"
/>
</p>
<VPCategoriesGroup v-else :item="item" />
</li>
</ul>
</template>
<style scoped>
.vp-categories-item {
margin: 8px 0;
font-size: 16px;
list-style: none;
}
.vp-categories-item .post {
display: flex;
align-items: center;
width: fit-content;
max-width: 100%;
color: var(--vp-c-text-1);
transition: color var(--t-color);
}
.vp-categories-item .post:hover {
color: var(--vp-c-brand-1);
}
.vp-categories-item .post .vpi-post {
display: inline-block;
width: 1em;
margin-right: 8px;
}
.vp-categories-item .post :deep(.vp-link) {
display: -webkit-box;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,115 @@
<script setup lang="ts">
import { useRoute } from 'vuepress/client'
import { onMounted, ref, watch } from 'vue'
import VPCategories from '@theme/Blog/VPCategories.vue'
import type { BlogCategoryItem } from '../../composables/blog-category.js'
const props = defineProps<{
item: BlogCategoryItem
}>()
const route = useRoute()
const el = ref<HTMLDivElement | null>(null)
const active = ref(true)
const isActive = ref(false)
watch(
() => [route.query, props.item],
() => {
const id = route.query.id as string
if (!id) {
active.value = true
}
else {
active.value = hasActive(props.item, id)
}
isActive.value = id ? props.item.id === id : false
},
{ immediate: true },
)
function hasActive(item: BlogCategoryItem, id: string) {
return item.id === id
|| item.items.filter(item => item.type === 'category').some(item => hasActive(item, id))
}
function toggle() {
active.value = !active.value
}
onMounted(() => {
if (el.value && isActive.value) {
el.value.scrollIntoView({ block: 'center' })
}
})
</script>
<template>
<div ref="el" class="vp-category-group" :class="{ active }">
<p class="folder" @click="toggle">
<span class="icon" :class="[active ? 'vpi-folder-open' : 'vpi-folder']" />
<span>{{ item.title }}</span>
</p>
<VPCategories v-if="item.items.length" class="group" :items="item.items" />
</div>
</template>
<style scoped>
.vp-category-group {
position: relative;
}
.vp-category-group::after {
position: absolute;
top: 30px;
bottom: 0;
left: 8px;
display: block;
content: "";
border-left: 1px solid var(--vp-c-divider);
transition: border var(--t-color);
}
.vp-category-group .folder {
display: flex;
align-items: center;
margin: 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-2);
cursor: pointer;
transition: color var(--t-color);
}
.vp-category-group .folder:hover {
color: var(--vp-c-text-1);
}
@media (min-width: 768px) {
.vp-category-group .folder {
font-size: 18px;
}
}
.vp-category-group .folder .icon {
display: inline-block;
width: 1em;
margin-right: 8px;
}
.vp-category-group > .group {
display: none;
margin-left: 22px;
}
@media (min-width: 768px) {
.vp-category-group > .group {
margin-left: 26px;
}
}
.vp-category-group.active > .group {
display: block;
}
</style>

View File

@ -10,6 +10,16 @@ const props = defineProps<{
const colors = useTagColors()
const sticky = computed(() => {
if (typeof props.post.sticky === 'boolean') {
return props.post.sticky
}
else if (typeof props.post.sticky === 'number') {
return props.post.sticky >= 0
}
return false
})
const categoryList = computed(() =>
props.post.categoryList ?? [],
)
@ -31,10 +41,7 @@ const createTime = computed(() =>
<template>
<div class="vp-blog-post-item">
<h3>
<div
v-if="typeof post.sticky === 'boolean' ? post.sticky : post.sticky >= 0"
class="sticky"
>
<div v-if="sticky" class="sticky">
TOP
</div>
<span v-if="post.encrypt" class="icon-lock vpi-lock" />

View File

@ -10,7 +10,7 @@ const { page, frontmatter } = useData()
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
const fixedInclude = ['blog', 'friends', 'blog-archives', 'blog-tags']
const fixedInclude = ['blog', 'friends', 'blog-archives', 'blog-tags', 'blog-categories']
const fixed = computed(() => {
return fixedInclude.includes(page.value.type as string)

View File

@ -35,7 +35,7 @@ const isLocked = useScrollLock(inBrowser ? document.body : null)
<style scoped>
.vp-nav-screen {
position: fixed;
top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 1px);
top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));
/* rtl:ignore */
right: 0;

View File

@ -32,7 +32,7 @@ function linkTo(e: Event) {
if (!isExternal.value) {
e.preventDefault()
if (link.value)
router.push({ path: link.value })
router.push(link.value)
}
}
</script>

View File

@ -17,7 +17,7 @@ const { frontmatter, page } = useData()
const isBlogLayout = computed(() => {
const { type } = page.value
return type === 'blog' || type === 'blog-archives' || type === 'blog-tags'
return type === 'blog' || type === 'blog-archives' || type === 'blog-tags' || type === 'blog-categories'
})
watch([isBlogLayout, () => frontmatter.value.pageLayout], () => nextTick(() =>

View File

@ -10,11 +10,13 @@ import { useEncrypt } from '../composables/encrypt.js'
import { useSidebar } from '../composables/sidebar.js'
import { useData } from '../composables/data.js'
import { useHeaders } from '../composables/outline.js'
import { useBlogPost } from '../composables/page.js'
const { page, theme, frontmatter, isDark } = useData()
const route = useRoute()
const { hasSidebar, hasAside, leftAside } = useSidebar()
const { isBlogPost } = useBlogPost()
const headers = useHeaders()
const { isPageDecrypted } = useEncrypt()
@ -66,7 +68,7 @@ watch(
:key="page.path" class="vp-doc-container" :class="{
'has-sidebar': hasSidebar,
'has-aside': enableAside,
'is-blog': page.isBlogPost,
'is-blog': isBlogPost,
'with-encrypt': !isPageDecrypted,
}"
>

View File

@ -1,13 +1,17 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useReadingTimeLocale } from '@vuepress/plugin-reading-time/client'
import VPLink from '@theme/VPLink.vue'
import { useData } from '../composables/data.js'
import { useTagColors } from '../composables/tag-colors.js'
import { useBlogPost } from '../composables/page.js'
import { useBlogExtract } from '../composables/blog-extract.js'
const { page, frontmatter: matter } = useData<'post'>()
const { isBlogPost } = useBlogPost()
const colors = useTagColors()
const readingTime = useReadingTimeLocale()
const { categories } = useBlogExtract()
const createTime = computed(() => {
if (matter.value.createTime)
@ -36,14 +40,16 @@ const hasMeta = computed(() => readingTime.value.time || tags.value.length || cr
<template>
<div
v-if="page.isBlogPost && categoryList.length"
v-if="isBlogPost && categoryList.length"
class="vp-doc-category"
>
<template
v-for="({ type, name }, index) in categoryList"
:key="`${index}-${type}`"
v-for="({ id, name }, index) in categoryList"
:key="id"
>
<span class="category">{{ name }}</span>
<VPLink :href="`${categories.link}?id=${id}`" class="category">
{{ name }}
</VPLink>
<span v-if="index !== categoryList.length - 1" class="dot">&rsaquo;</span>
</template>
</div>

View File

@ -21,7 +21,7 @@ function linkTo(e: Event) {
if (!isExternal.value) {
e.preventDefault()
if (link.value)
router.push({ path: link.value })
router.push(link.value)
}
}
</script>

View File

@ -5,6 +5,7 @@ import VPLocalNavOutlineDropdown from '@theme/VPLocalNavOutlineDropdown.vue'
import { useSidebar } from '../composables/sidebar.js'
import { useHeaders } from '../composables/outline.js'
import { useData } from '../composables/data.js'
import { useBlogPost } from '../composables/page.js'
const props = defineProps<{
open: boolean
@ -13,7 +14,8 @@ const props = defineProps<{
defineEmits<(e: 'openMenu') => void>()
const { page, theme } = useData()
const { theme } = useData()
const { isBlogPost } = useBlogPost()
const { hasSidebar } = useSidebar()
const { y } = useWindowScroll()
@ -39,21 +41,21 @@ const classes = computed(() => {
'vp-local-nav': true,
'fixed': empty.value,
'reached-top': y.value >= navHeight.value,
'is-blog': page.value.isBlogPost,
'is-blog': isBlogPost,
'with-outline': !props.showOutline,
}
})
const showLocalNav = computed(() => {
return (hasSidebar.value || page.value.isBlogPost) && (!empty.value || y.value >= navHeight.value)
return (hasSidebar.value || isBlogPost) && (!empty.value || y.value >= navHeight.value)
})
</script>
<template>
<div v-if="showLocalNav" :class="classes">
<button
class="menu" :class="{ hidden: page.isBlogPost }"
:disabled="page.isBlogPost"
class="menu" :class="{ hidden: isBlogPost }"
:disabled="isBlogPost"
:aria-expanded="open"
aria-controls="SidebarNav"
@click="$emit('openMenu')"

View File

@ -1,6 +1,6 @@
import { computed } from 'vue'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useLocalePostList } from './blog-post-list.js'
import { useLocalePostList } from './blog-data.js'
export type ShortPostItem = Pick<PlumeThemeBlogPostItem, 'title' | 'path' | 'createTime'>

View File

@ -0,0 +1,69 @@
import { computed } from 'vue'
import { useLocalePostList } from './blog-data.js'
export interface BlogCategoryItemWithPost {
type: 'post'
title: string
path: string
}
export interface BlogCategoryItem {
id: string
type: 'category'
sort: number
title: string
items: (BlogCategoryItem | BlogCategoryItemWithPost)[]
}
export type BlogCategory = (BlogCategoryItem | BlogCategoryItemWithPost)[]
export function useBlogCategory() {
const postList = useLocalePostList()
const categories = computed(() => {
const list: BlogCategory = []
postList.value.forEach((item) => {
const categoryList = item.categoryList
if (!categoryList || categoryList.length === 0) {
list.push({ type: 'post', title: item.title, path: item.path })
}
else {
let cate = list
let i = 0
while (i < categoryList.length) {
const { id, name, sort } = categoryList[i]
const current = cate.find(item => item.type === 'category' && item.id === id)
if (!current) {
const items = [] as BlogCategoryItem[]
cate.push({ type: 'category', title: name, id, sort, items })
cate = items
}
else {
cate = (current as BlogCategoryItem).items
}
i++
}
cate.push({ type: 'post', title: item.title, path: item.path })
}
})
return sortCategory(list)
})
return { categories }
}
function sortCategory(items: BlogCategory): BlogCategory {
for (const item of items) {
if (item.type === 'category' && item.items.length) {
item.items = sortCategory(item.items)
}
}
return items.sort((a, b) => {
if (a.type === 'category' && b.type === 'category') {
return a.sort < b.sort ? -1 : 1
}
return 0
})
}

View File

@ -1,18 +1,24 @@
import {
blogPostData as blogPostDataRaw,
} from '@internal/blogData'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import { usePageLang } from 'vuepress/client'
import type { PlumeThemeBlogPostData } from '../../shared/index.js'
export type BlogDataRef = Ref<PlumeThemeBlogPostData>
export const blogPostData: BlogDataRef = ref(blogPostDataRaw)
export function useBlogPostData(): BlogDataRef {
export function usePostList(): BlogDataRef {
return blogPostData as BlogDataRef
}
export function useLocalePostList() {
const locale = usePageLang()
return computed(() => blogPostData.value.filter(item => item.lang === locale.value))
}
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateBlogPostData = (data: PlumeThemeBlogPostData) => {
blogPostData.value = data

View File

@ -1,8 +1,9 @@
import { useRouteLocale } from 'vuepress/client'
import { computed } from 'vue'
import type { PresetLocale } from '../../shared/index.js'
import { useLocalePostList } from './blog-post-list.js'
import { useLocalePostList } from './blog-data.js'
import { useTags } from './blog-tags.js'
import { type BlogCategory, useBlogCategory } from './blog-category.js'
import { useData } from './data.js'
import { useLocaleLink } from './locale.js'
@ -10,16 +11,29 @@ declare const __PLUME_PRESET_LOCALE__: Record<string, PresetLocale>
const presetLocales = __PLUME_PRESET_LOCALE__
export function useBlogNavTitle(name: keyof PresetLocale) {
const locale = useRouteLocale()
return computed(() => presetLocales[locale.value]?.[name] || presetLocales['/'][name])
}
export function useBlogExtract() {
const { theme } = useData()
const locale = useRouteLocale()
const postList = useLocalePostList()
const { tags: tagsList } = useTags()
const { categories: categoryList } = useBlogCategory()
const blog = computed(() => theme.value.blog || {})
const hasBlogExtract = computed(() => blog.value.archives !== false || blog.value.tags !== false)
const hasBlogExtract = computed(() =>
blog.value.archives !== false
|| blog.value.tags !== false
|| blog.value.categories !== false,
)
const tagsLink = useLocaleLink(blog.value.tagsLink || 'blog/tags/')
const archiveLink = useLocaleLink(blog.value.archivesLink || 'blog/archives/')
const categoriesLink = useLocaleLink(blog.value.categoriesLink || 'blog/categories/')
const tags = computed(() => ({
link: tagsLink.value,
@ -33,9 +47,29 @@ export function useBlogExtract() {
total: postList.value.length,
}))
const categories = computed(() => ({
link: categoriesLink.value,
text: presetLocales[locale.value]?.category || presetLocales['/'].category,
total: getCategoriesTotal(categoryList.value),
}))
return {
hasBlogExtract,
tags,
archives,
categories,
}
}
function getCategoriesTotal(categories: BlogCategory): number {
let total = 0
for (const category of categories) {
if (category.type === 'category') {
total += 1
if (category.items.length) {
total += getCategoriesTotal(category.items)
}
}
}
return total
}

View File

@ -1,19 +1,12 @@
import { usePageLang } from 'vuepress/client'
import { computed } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useBlogPostData } from './blog-data.js'
import { useLocalePostList } from './blog-data.js'
import { useData } from './data.js'
import { useRouteQuery } from './route-query.js'
const DEFAULT_PER_PAGE = 10
export function useLocalePostList() {
const locale = usePageLang()
const list = useBlogPostData()
return computed(() => list.value.filter(item => item.lang === locale.value))
}
export function usePostListControl() {
const { theme } = useData()

View File

@ -2,7 +2,7 @@ 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 { useLocalePostList } from './blog-data.js'
import { useRouteQuery } from './route-query.js'
type ShortPostItem = Pick<PlumeThemeBlogPostItem, 'title' | 'path' | 'createTime'>

View File

@ -18,7 +18,9 @@ export * from './blog-post-list.js'
export * from './blog-extract.js'
export * from './blog-tags.js'
export * from './blog-archives.js'
export * from './blog-category.js'
export * from './tag-colors.js'
export * from './page.js'
export * from './encrypt-data.js'
export * from './encrypt.js'

View File

@ -4,6 +4,7 @@ import { normalizeLink } from '../utils/index.js'
import { useThemeData } from './theme-data.js'
import { useData } from './data.js'
import { getSidebarFirstLink, useSidebarData } from './sidebar.js'
import { useBlogPost } from './page.js'
export function useLangs({
removeCurrent = true,
@ -12,6 +13,7 @@ export function useLangs({
const { page } = useData()
const routeLocale = useRouteLocale()
const sidebar = useSidebarData()
const { isBlogPost } = useBlogPost()
const currentLang = computed(() => {
const link = routeLocale.value
@ -28,7 +30,7 @@ export function useLangs({
if (!notFound)
return path
const blog = theme.value.blog
if (page.value.isBlogPost)
if (isBlogPost.value)
return withBase(blog?.link || normalizeLink(locale, 'blog/'))
const sidebarList = sidebar.value

View File

@ -0,0 +1,16 @@
import { computed } from 'vue'
import { useData } from './data.js'
import { usePostList } from './blog-data.js'
export function useBlogPost() {
const { page } = useData()
const postList = usePostList()
const isBlogPost = computed(() => {
return postList.value.some(item => item.path === page.value.path)
})
return {
isBlogPost,
}
}

View File

@ -4,23 +4,25 @@ import { computed } from 'vue'
import type { Ref } from 'vue'
import type { NavItemWithLink, PlumeThemeBlogPostItem, SidebarItem } from '../../shared/index.js'
import { resolveNavLink } from '../utils/index.js'
import { useBlogPostData } from './blog-data.js'
import { usePostList } from './blog-data.js'
import { useSidebar } from './sidebar.js'
import { useData } from './data.js'
import { useBlogPost } from './page.js'
export function usePrevNext() {
const route = useRoute()
const { page, frontmatter } = useData()
const { frontmatter } = useData()
const { sidebar } = useSidebar()
const postList = useBlogPostData() as unknown as Ref<PlumeThemeBlogPostItem[]>
const postList = usePostList() as unknown as Ref<PlumeThemeBlogPostItem[]>
const locale = usePageLang()
const { isBlogPost } = useBlogPost()
const prevNavList = computed(() => {
const prevConfig = resolveFromFrontmatterConfig(frontmatter.value.prev)
if (prevConfig !== false)
return prevConfig
if (page.value.isBlogPost) {
if (isBlogPost.value) {
return resolveFromBlogPostData(
postList.value.filter(item => item.lang === locale.value),
route.path,
@ -37,7 +39,7 @@ export function usePrevNext() {
if (nextConfig !== false)
return nextConfig
if (page.value.isBlogPost) {
if (isBlogPost.value) {
return resolveFromBlogPostData(
postList.value.filter(item => item.lang === locale.value),
route.path,

View File

@ -121,6 +121,10 @@
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='currentColor' d='m213.66 82.34l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v176a16 16 0 0 0 16 16h36a4 4 0 0 0 4-4v-20h-7.73a8.17 8.17 0 0 1-8.27-7.47a8 8 0 0 1 8-8.53h8v-16h-7.73a8.17 8.17 0 0 1-8.27-7.47a8 8 0 0 1 8-8.53h8v-16h-7.73a8.17 8.17 0 0 1-8.27-7.47a8 8 0 0 1 8-8.53h8v-7.73a8.18 8.18 0 0 1 7.47-8.25a8 8 0 0 1 8.53 8v8h7.73a8.17 8.17 0 0 1 8.25 7.47a8 8 0 0 1-8 8.53h-8v16h7.73a8.17 8.17 0 0 1 8.25 7.47a8 8 0 0 1-8 8.53h-8v16h7.73a8.17 8.17 0 0 1 8.25 7.47a8 8 0 0 1-8 8.53h-8v20a4 4 0 0 0 4 4h84a16 16 0 0 0 16-16V88a8 8 0 0 0-2.28-5.66M152 88V44l44 44Z' /%3E%3C/svg%3E");
}
.vpi-category {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 20 20'%3E%3Cpath fill='%23000' d='M5 7h13v10H2V4h7l2 2H4v9h1z'/%3E%3C/svg%3E");
}
.vpi-blog-ext {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M22 3H5a2 2 0 0 0-2 2v4h2V5h17v14H5v-4H3v4a2 2 0 0 0 2 2h17a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2M7 15v-2H0v-2h7V9l4 3zm13-2h-7v-2h7zm0-4h-7V7h7zm-3 8h-4v-2h4z' /%3E%3C/svg%3E");
}
@ -136,3 +140,15 @@
.vpi-back-to-top {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' d='M24.008 14.1V42M12 26l12-12l12 12M12 6h24' /%3E%3C/svg%3E");
}
.vpi-folder {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 1024 1024'%3E%3Cpath fill='%23000' d='M880 298.4H521L403.7 186.2a8.15 8.15 0 0 0-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32'/%3E%3C/svg%3E");
}
.vpi-folder-open {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 1024 1024'%3E%3Cpath fill='%23000' d='M928 444H820V330.4c0-17.7-14.3-32-32-32H473L355.7 186.2a8.15 8.15 0 0 0-5.5-2.2H96c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h698c13 0 24.8-7.9 29.7-20l134-332c1.5-3.8 2.3-7.9 2.3-12c0-17.7-14.3-32-32-32m-180 0H238c-13 0-24.8 7.9-29.7 20L136 643.2V256h188.5l119.6 114.4H748z'/%3E%3C/svg%3E");
}
.vpi-post {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M20 22H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1m-1-2V4H5v16zM7 6h4v4H7zm0 6h10v2H7zm0 4h10v2H7zm6-9h4v2h-4z'/%3E%3C/svg%3E");
}

View File

@ -7,7 +7,16 @@ import { THEME_NAME } from '../utils/index.js'
const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
appearance: true,
blog: { link: '/blog/', pagination: { perPage: 15 }, tags: true, archives: true, tagsLink: '/blog/tags/', archivesLink: '/blog/archives/' },
blog: {
link: '/blog/',
pagination: { perPage: 15 },
tags: true,
archives: true,
categories: true,
tagsLink: '/blog/tags/',
archivesLink: '/blog/archives/',
categoriesLink: '/blog/categories/',
},
article: '/article/',
notes: { link: '/', dir: '/notes/', notes: [] },
navbarSocialInclude: ['github', 'twitter', 'discord', 'facebook'],

View File

@ -31,6 +31,7 @@ export const enPresetLocale: PresetLocale = {
blog: 'Blog',
tag: 'Tags',
archive: 'Archives',
category: 'Categories',
}
export const enSearchLocale: Partial<SearchLocaleOptions> = {

View File

@ -44,6 +44,7 @@ export const zhPresetLocale: PresetLocale = {
blog: '博客',
tag: '标签',
archive: '归档',
category: '分类',
}
export const zhDocsearchLocale: DocsearchLocaleOptions = {

View File

@ -2,19 +2,17 @@ import {
ensureLeadingSlash,
getRootLang,
getRootLangPath,
removeLeadingSlash,
} from '@vuepress/helper'
import type { App, Page } from 'vuepress/core'
import { createPage } from 'vuepress/core'
import { createFilter } from 'create-filter'
import type {
PageCategoryData,
PlumeThemeLocaleOptions,
PlumeThemePageData,
} from '../shared/index.js'
import { normalizePath, withBase } from './utils/index.js'
import { hash, withBase } from './utils/index.js'
import { PRESET_LOCALES } from './locales/index.js'
import { resolveNotesLinkList, resolveNotesOptions } from './config/index.js'
import { resolveNotesLinkList } from './config/index.js'
export async function setupPage(
app: App,
@ -54,39 +52,17 @@ export async function setupPage(
path: withBase(blog.archivesLink || `${link}/archives/`, localePath),
frontmatter: { lang, _pageLayout: 'blog-archives', title: getTitle(locale, 'archive') },
}))
// 添加分类页
blog.categories !== false && pageList.push(createPage(app, {
path: withBase(blog.categoriesLink || `${link}/categories/`, localePath),
frontmatter: { lang, _pageLayout: 'blog-categories', title: getTitle(locale, 'category') },
}))
}
app.pages.push(...await Promise.all(pageList))
}
const weakFilter = new WeakMap<PlumeThemeLocaleOptions, (id: string | undefined) => boolean>()
function createBlogFilter(localeOptions: PlumeThemeLocaleOptions) {
if (weakFilter.has(localeOptions))
return weakFilter.get(localeOptions)!
const blog = localeOptions.blog || {}
const notesList = resolveNotesOptions(localeOptions)
const notesDirList = notesList
.map(notes => removeLeadingSlash(normalizePath(`${notes.dir}/**`)))
.filter(Boolean)
const filter = createFilter(
blog.include ?? ['**/*.md'],
[
'**/{README,readme,index}.md',
'.vuepress/',
'node_modules/',
...(blog.exclude ?? []),
...notesDirList,
].filter(Boolean),
{ resolve: false },
)
weakFilter.set(localeOptions, filter)
return filter
}
export function extendsPageData(
page: Page<PlumeThemePageData>,
localeOptions: PlumeThemeLocaleOptions,
@ -94,10 +70,6 @@ export function extendsPageData(
page.data.filePathRelative = page.filePathRelative
page.routeMeta.title = page.frontmatter.title || page.title
if (createBlogFilter(localeOptions)(page.filePathRelative || '')) {
page.data.isBlogPost = true
}
if (page.frontmatter.icon) {
page.routeMeta.icon = page.frontmatter.icon
}
@ -133,7 +105,6 @@ export function extendsPageData(
}
autoCategory(page, localeOptions)
pageContentRendered(page)
}
let uuid = 10000
@ -157,26 +128,21 @@ export function autoCategory(
LOCALE_RE ??= new RegExp(
`^(${Object.keys(options.locales || {}).filter(l => l !== '/').join('|')})`,
)
const categoryList: PageCategoryData[] = ensureLeadingSlash(pagePath)
const list = ensureLeadingSlash(pagePath)
.replace(LOCALE_RE, '')
.replace(/^\//, '')
.split('/')
.slice(0, -1)
.map((category) => {
const categoryList: PageCategoryData[] = list
.map((category, index) => {
const match = category.match(RE_CATEGORY) || []
!cache[match[2]] && !match[1] && (cache[match[2]] = uuid++)
return {
type: Number(match[1] || cache[match[2]]),
id: hash(list.slice(0, index + 1).join('-')).slice(0, 6),
sort: Number(match[1] || cache[match[2]]),
name: match[2],
}
})
page.data.categoryList = categoryList
}
export function pageContentRendered(page: Page<PlumeThemePageData>) {
const EXCERPT_SPLIT = '<!-- more -->'
if (page.data.isBlogPost && page.contentRendered.includes(EXCERPT_SPLIT)) {
const [excerpt, content] = page.contentRendered.split(EXCERPT_SPLIT)
page.contentRendered = `<div class="excerpt">${excerpt}</div>${EXCERPT_SPLIT}${content}`
}
}

View File

@ -46,6 +46,7 @@ export interface PresetLocale {
blog: string
tag: string
archive: string
category: string
}
export interface ThemeTransition {

View File

@ -79,4 +79,17 @@ export interface PlumeThemeBlog {
* @default '/blog/archives/'
*/
archivesLink?: string
/**
*
* @default true
*/
categories?: boolean
/**
*
*
* @default '/blog/categories/'
*/
categoriesLink?: string
}

View File

@ -8,14 +8,14 @@ interface ReadingTime {
}
export interface PlumeThemePageData extends GitPluginPageData {
isBlogPost: boolean
type: 'blog' | 'friends' | 'blog-tags' | 'blog-archives'
type: 'blog' | 'friends' | 'blog-tags' | 'blog-archives' | 'blog-categories'
categoryList?: PageCategoryData[]
filePathRelative: string | null
readingTime?: ReadingTime
}
export interface PageCategoryData {
type: string | number
id: string
sort: number
name: string
}