chore: support sidebar
This commit is contained in:
parent
de27dc9a8b
commit
1986ed13b6
@ -31,7 +31,13 @@ export default defineUserConfig<PlumeThemeOptions>({
|
||||
link: 'typescript',
|
||||
dir: 'typescript',
|
||||
text: 'Typescript',
|
||||
sidebar: [],
|
||||
sidebar: [
|
||||
'',
|
||||
{
|
||||
text: '123',
|
||||
children: ['1', '2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -4,3 +4,37 @@ createTime: 2022/04/04 09:45:31
|
||||
author: pengzhanbo
|
||||
permalink: /note/typescript/
|
||||
---
|
||||
|
||||
## typescript
|
||||
|
||||
### haha
|
||||
|
||||
内容
|
||||
|
||||
::: tip
|
||||
提示
|
||||
:::
|
||||
|
||||
::: info
|
||||
信息
|
||||
:::
|
||||
|
||||
::: note
|
||||
注释
|
||||
:::
|
||||
|
||||
::: warning
|
||||
警告
|
||||
:::
|
||||
|
||||
::: danger
|
||||
危险
|
||||
:::
|
||||
|
||||
::: details
|
||||
详情
|
||||
:::
|
||||
|
||||
|
||||
- [ ] todo
|
||||
- [x] todo
|
||||
|
||||
@ -41,31 +41,33 @@ const navbarLinks = computed(() => [
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-items {
|
||||
--navbar-line-height: calc(
|
||||
var(--navbar-height) - 2 * var(--navbar-padding-v)
|
||||
);
|
||||
display: inline-block;
|
||||
|
||||
a {
|
||||
.navbar-wrapper {
|
||||
.navbar-items {
|
||||
--navbar-line-height: calc(
|
||||
var(--navbar-height) - 2 * var(--navbar-padding-v)
|
||||
);
|
||||
display: inline-block;
|
||||
line-height: 1.4rem;
|
||||
color: inherit;
|
||||
|
||||
&:hover,
|
||||
&.router-lint-active {
|
||||
color: var(--c-text-accent);
|
||||
a {
|
||||
display: inline-block;
|
||||
line-height: 1.4rem;
|
||||
color: inherit;
|
||||
|
||||
&:hover,
|
||||
&.router-lint-active {
|
||||
color: var(--c-text-accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: 1.5rem;
|
||||
line-height: var(--navbar-line-height);
|
||||
.navbar-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: 1.5rem;
|
||||
line-height: var(--navbar-line-height);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
|
||||
import PostMeta from '@theme-plume/PostMeta.vue'
|
||||
import Sidebar from '@theme-plume/Sidebar.vue'
|
||||
import { usePageData } from '@vuepress/client'
|
||||
import { computed } from 'vue'
|
||||
import { computed, nextTick, onUnmounted, watchEffect } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { PlumeThemePageData } from '../../shared'
|
||||
import { useDarkMode, useThemeLocaleData } from '../composables'
|
||||
import { getCssValue } from '../utils'
|
||||
import Toc from './Toc'
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const route = useRoute()
|
||||
@ -15,13 +17,38 @@ const isDarkMode = useDarkMode()
|
||||
const isNote = computed(() => {
|
||||
return page.value.isNote || false
|
||||
})
|
||||
|
||||
const enabledSidebar = computed(() => {
|
||||
return isNote.value
|
||||
})
|
||||
|
||||
let layout: HTMLElement | null
|
||||
watchEffect(async () => {
|
||||
await nextTick()
|
||||
if (!enabledSidebar.value) return
|
||||
layout = document.querySelector('.plume-theme')
|
||||
const footer: HTMLElement | null = document.querySelector(
|
||||
'.theme-plume-footer'
|
||||
)
|
||||
if (themeLocale.value.footer) {
|
||||
const h = getCssValue(footer, 'height')
|
||||
layout?.setAttribute('style', `padding-bottom: ${h}px`)
|
||||
} else {
|
||||
layout?.setAttribute('style', `padding-bottom: 0`)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
layout?.removeAttribute('style')
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DropdownTransition>
|
||||
<main class="page-wrapper">
|
||||
<slot name="top"></slot>
|
||||
<div class="page-container">
|
||||
<div class="page-container" :class="{ 'has-sidebar': enabledSidebar }">
|
||||
<main class="plume-theme-content">
|
||||
<Sidebar v-if="enabledSidebar" />
|
||||
<div class="page-content">
|
||||
<h1>{{ page.title }}</h1>
|
||||
<PostMeta :post="page" type="post" :border="true" />
|
||||
@ -68,6 +95,15 @@ const isNote = computed(() => {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.has-sidebar {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
.plume-theme-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.comment-container {
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import SidebarItems from '@theme-plume/SidebarItems.vue'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
import { useSidebarIndex } from '../composables'
|
||||
|
||||
const route = useRoute()
|
||||
const { sidebarList, initSidebarList } = useSidebarIndex()
|
||||
initSidebarList(route.path)
|
||||
onBeforeRouteUpdate((to) => {
|
||||
initSidebarList(to.path)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<aside class="plume-theme-sidebar-wrapper">
|
||||
<SidebarItems :sidebar-list="sidebarList" />
|
||||
</aside>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.plume-theme-sidebar-wrapper {
|
||||
position: sticky;
|
||||
top: calc(var(--navbar-height) + 1.25rem);
|
||||
width: 20rem;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
border-right: solid 1px var(--c-border);
|
||||
font-size: 18px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--c-brand) var(--c-border);
|
||||
background-color: var(--c-bg-container);
|
||||
transition: transform var(--t-color), background-color var(--t-color);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--c-border);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--c-brand);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import AutoLink from '@theme-plume/AutoLink.vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { SidebarOptions } from '../../shared'
|
||||
|
||||
defineProps({
|
||||
sidebarList: {
|
||||
type: Array as PropType<SidebarOptions>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ul class="sidebar-items">
|
||||
<li v-for="sidebar in sidebarList" :key="sidebar.text">
|
||||
<AutoLink
|
||||
v-if="sidebar.link"
|
||||
:item="{ text: sidebar.text, link: sidebar.link }"
|
||||
/>
|
||||
<p v-else>{{ sidebar.text }}</p>
|
||||
<SidebarItems :sidebar-list="sidebar.children" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.plume-theme-sidebar-wrapper {
|
||||
.sidebar-items {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
a {
|
||||
color: var(--c-text);
|
||||
margin: 0.25rem 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--c-text-accent);
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-weight: 600;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
.sidebar-items {
|
||||
font-size: 16px;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -5,6 +5,7 @@ export * from './navLink'
|
||||
export * from './resolveRouteWithRedirect'
|
||||
|
||||
export * from './postIndex'
|
||||
export * from './sidebarIndex'
|
||||
export * from './postList'
|
||||
export * from './scrollPromise'
|
||||
|
||||
|
||||
42
packages/theme/src/client/composables/sidebarIndex.ts
Normal file
42
packages/theme/src/client/composables/sidebarIndex.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { sidebarIndex as sidebarIndexRaw } from '@internal/sidebarIndex.js'
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { SidebarOptions } from '../../shared'
|
||||
import { useThemeLocaleData } from './themeData'
|
||||
|
||||
export type SidebarIndexRef = Ref<Record<string, SidebarOptions>>
|
||||
|
||||
export type SidebarRef = Ref<SidebarOptions>
|
||||
|
||||
export const sidebarIndex: SidebarIndexRef = ref(sidebarIndexRaw)
|
||||
|
||||
interface UseSidebarIndex {
|
||||
sidebarList: SidebarRef
|
||||
initSidebarList: (path: string) => void
|
||||
}
|
||||
|
||||
export const useSidebarIndex = (): UseSidebarIndex => {
|
||||
const sidebarList: SidebarRef = ref([])
|
||||
const themeLocale = useThemeLocaleData()
|
||||
const notes = themeLocale.value.notes
|
||||
function initSidebarList(path = ''): void {
|
||||
if (!notes) return
|
||||
const prefix = notes.link?.replace(/^\/|\/$/g, '')
|
||||
if (path.startsWith(`/${prefix}`)) {
|
||||
Object.keys(sidebarIndex.value).forEach((key) => {
|
||||
if (path.startsWith(key)) {
|
||||
sidebarList.value = sidebarIndex.value[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return { sidebarList, initSidebarList }
|
||||
}
|
||||
|
||||
if (import.meta.hot) {
|
||||
__VUE_HMR_RUNTIME__.updateSidebarIndex = (
|
||||
data: Record<string, SidebarOptions>
|
||||
) => {
|
||||
sidebarIndex.value = data
|
||||
}
|
||||
}
|
||||
6
packages/theme/src/client/shim.d.ts
vendored
6
packages/theme/src/client/shim.d.ts
vendored
@ -11,3 +11,9 @@ declare module '@internal/postIndex.js' {
|
||||
const postIndex: PostIndex
|
||||
export { postIndex }
|
||||
}
|
||||
|
||||
declare module '@internal/sidebarIndex.js' {
|
||||
import type { SidebarOptions } from '../shared'
|
||||
const sidebarIndex: Record<string, SidebarOptions>
|
||||
export { sidebarIndex }
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import type { App } from '@vuepress/core'
|
||||
import type { PlumeThemeLocaleOptions } from '../../shared'
|
||||
import { preparedPostIndex, watchPostIndex } from './postIndex'
|
||||
import { preparedSidebarIndex } from './sidebarIndex'
|
||||
|
||||
export const onPrepared = (
|
||||
app: App,
|
||||
localeOption: PlumeThemeLocaleOptions
|
||||
): void => {
|
||||
preparedPostIndex(app, localeOption)
|
||||
preparedSidebarIndex(app, localeOption)
|
||||
}
|
||||
|
||||
export const preparedWatch = (
|
||||
|
||||
176
packages/theme/src/node/prepared/sidebarIndex.ts
Normal file
176
packages/theme/src/node/prepared/sidebarIndex.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import type { App, Page } from '@vuepress/core'
|
||||
import { path } from '@vuepress/utils'
|
||||
import * as chokidar from 'chokidar'
|
||||
import type {
|
||||
PlumeThemeLocaleOptions,
|
||||
PlumeThemeNotesItem,
|
||||
PlumeThemeNotesOptions,
|
||||
PlumeThemePageData,
|
||||
PlumeThemeSidebarConfigOptions,
|
||||
SidebarItem,
|
||||
SidebarOptions,
|
||||
} from '../../shared'
|
||||
|
||||
const HMR_CODE = `
|
||||
if (import.meta.webpackHot) {
|
||||
import.meta.webpackHot.accept()
|
||||
if (__VUE_HMR_RUNTIME__.updateSidebarIndex) {
|
||||
__VUE_HMR_RUNTIME__.updatePostIndex(sidebarIndex)
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(({ sidebarIndex }) => {
|
||||
__VUE_HMR_RUNTIME__.updateSidebarIndex(sidebarIndex)
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
interface NotePage {
|
||||
relativePath: string[]
|
||||
title: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export const preparedSidebarIndex = (
|
||||
app: App,
|
||||
{ notes }: PlumeThemeLocaleOptions
|
||||
): void => {
|
||||
const pages = app.pages as Page<PlumeThemePageData>[]
|
||||
if (notes === false) return
|
||||
const {
|
||||
notes: noteList,
|
||||
dir: rootDir,
|
||||
link: rootLink,
|
||||
} = notes as PlumeThemeNotesOptions
|
||||
const notePageList: NotePage[] = pages
|
||||
.filter((page) => page.filePathRelative?.startsWith(rootDir as string))
|
||||
.map((page) => {
|
||||
return {
|
||||
relativePath: page.filePathRelative?.split('/').slice(1) || [],
|
||||
title: page.title,
|
||||
link: page.path,
|
||||
}
|
||||
})
|
||||
|
||||
const sidebarMap: Record<string, SidebarOptions> = {}
|
||||
noteList.forEach((note) => {
|
||||
sidebarMap[path.join('/', rootLink, note.link)] = noteSidebar(
|
||||
note,
|
||||
notePageList.filter(
|
||||
(page) =>
|
||||
page.relativePath?.[0] === note.dir.trim().replace(/^\/|\/$/g, '')
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
let content = `
|
||||
export const sidebarIndex = ${JSON.stringify(sidebarMap, null, 2)}
|
||||
`
|
||||
if (app.env.isDev) {
|
||||
content += HMR_CODE
|
||||
}
|
||||
|
||||
app.writeTemp('internal/sidebarIndex.js', content)
|
||||
}
|
||||
|
||||
function noteSidebar(
|
||||
note: PlumeThemeNotesItem,
|
||||
notePageList: NotePage[]
|
||||
): SidebarOptions {
|
||||
if (note.sidebar === undefined) return []
|
||||
if (note.sidebar === 'auto') {
|
||||
return autoSidebar(note, notePageList)
|
||||
}
|
||||
return sidebarByConfig(
|
||||
note.text,
|
||||
note.link,
|
||||
note.dir,
|
||||
note.sidebar,
|
||||
notePageList
|
||||
)
|
||||
}
|
||||
|
||||
function autoSidebar(
|
||||
note: PlumeThemeNotesItem,
|
||||
notePageList: NotePage[]
|
||||
): SidebarOptions {
|
||||
return []
|
||||
}
|
||||
|
||||
function sidebarByConfig(
|
||||
text: string,
|
||||
link: string | undefined,
|
||||
dir: string,
|
||||
sidebarConfig: PlumeThemeSidebarConfigOptions,
|
||||
notePageList: NotePage[]
|
||||
): SidebarOptions {
|
||||
return sidebarConfig.map((sidebar) => {
|
||||
if (typeof sidebar === 'string') {
|
||||
const current = findNotePage(sidebar, dir, notePageList)
|
||||
return {
|
||||
text: current ? current.title : text,
|
||||
link: current ? current.link : '',
|
||||
children: [],
|
||||
} as SidebarItem
|
||||
} else {
|
||||
const current = sidebar.link
|
||||
? findNotePage(sidebar.link, dir, notePageList)
|
||||
: undefined
|
||||
return {
|
||||
text: sidebar.text,
|
||||
link: current?.link || sidebar.link,
|
||||
children: sidebarByConfig(
|
||||
sidebar.text,
|
||||
sidebar.link,
|
||||
dir,
|
||||
sidebar.children,
|
||||
notePageList
|
||||
),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function findNotePage(
|
||||
sidebar: string,
|
||||
dir: string,
|
||||
notePageList: NotePage[]
|
||||
): NotePage | undefined {
|
||||
if (sidebar === '' || sidebar === 'README.md' || sidebar === 'index.md') {
|
||||
return notePageList.find((page) => {
|
||||
const relative = page.relativePath.join('/')
|
||||
return (
|
||||
relative === path.join(dir, 'README.md') ||
|
||||
relative === path.join(dir, 'index.md')
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return notePageList.find((page) => {
|
||||
const relative = page.relativePath.join('/')
|
||||
return (
|
||||
relative === path.join(dir, sidebar) ||
|
||||
relative === path.join(dir, sidebar + '.md') ||
|
||||
page.link === sidebar
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const watchSidebarIndex = (
|
||||
app: App,
|
||||
watchers: any[],
|
||||
localeOption: PlumeThemeLocaleOptions
|
||||
): void => {
|
||||
if (!localeOption.notes || !localeOption.notes.dir) return
|
||||
const dir = path.join('pages', localeOption.notes.dir, '**/*')
|
||||
const watcher = chokidar.watch(dir, {
|
||||
cwd: app.dir.temp(),
|
||||
ignoreInitial: true,
|
||||
})
|
||||
|
||||
watcher.on('add', () => preparedSidebarIndex(app, localeOption))
|
||||
watcher.on('change', () => preparedSidebarIndex(app, localeOption))
|
||||
watcher.on('unlink', () => preparedSidebarIndex(app, localeOption))
|
||||
watchers.push(watcher)
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from './navbar'
|
||||
|
||||
export * from './sidebar'
|
||||
|
||||
7
packages/theme/src/shared/layout/sidebar.ts
Normal file
7
packages/theme/src/shared/layout/sidebar.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface SidebarItem {
|
||||
text: string
|
||||
link?: string
|
||||
children: SidebarOptions
|
||||
}
|
||||
|
||||
export type SidebarOptions = SidebarItem[]
|
||||
@ -29,5 +29,15 @@ export interface PlumeThemeNotesItem {
|
||||
text: string
|
||||
link: string
|
||||
dir: string
|
||||
sidebar: string[]
|
||||
sidebar?: PlumeThemeSidebarConfigOptions | 'auto'
|
||||
}
|
||||
|
||||
export type PlumeThemeSidebarConfigOptions = (
|
||||
| PlumeThemeNotesConfigItem
|
||||
| string
|
||||
)[]
|
||||
export interface PlumeThemeNotesConfigItem {
|
||||
text: string
|
||||
link?: string
|
||||
children: PlumeThemeNotesConfigItem[]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user