chore: support sidebar

This commit is contained in:
pengzhanbo 2022-04-08 11:12:15 +08:00
parent de27dc9a8b
commit 1986ed13b6
14 changed files with 443 additions and 24 deletions

View File

@ -31,7 +31,13 @@ export default defineUserConfig<PlumeThemeOptions>({
link: 'typescript',
dir: 'typescript',
text: 'Typescript',
sidebar: [],
sidebar: [
'',
{
text: '123',
children: ['1', '2'],
},
],
},
],
},

View File

@ -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

View File

@ -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;
}
}
}
}

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -5,6 +5,7 @@ export * from './navLink'
export * from './resolveRouteWithRedirect'
export * from './postIndex'
export * from './sidebarIndex'
export * from './postList'
export * from './scrollPromise'

View 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
}
}

View File

@ -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 }
}

View File

@ -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 = (

View 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)
}

View File

@ -1 +1,3 @@
export * from './navbar'
export * from './sidebar'

View File

@ -0,0 +1,7 @@
export interface SidebarItem {
text: string
link?: string
children: SidebarOptions
}
export type SidebarOptions = SidebarItem[]

View File

@ -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[]
}