feat(theme): add home components
This commit is contained in:
parent
764c58693e
commit
2c5bc7d29e
@ -38,6 +38,7 @@ export default defineUserConfig({
|
||||
// },
|
||||
notes,
|
||||
navbar: [
|
||||
{ text: 'Blog', link: '/blog/', activeMatch: '/blog/' },
|
||||
{
|
||||
text: 'VuePress',
|
||||
items: [
|
||||
@ -57,7 +58,7 @@ export default defineUserConfig({
|
||||
],
|
||||
footer: {
|
||||
copyright: 'Copyright © 2022-present pengzhanbo',
|
||||
content: '',
|
||||
message: '',
|
||||
},
|
||||
themePlugins: {
|
||||
search: {
|
||||
|
||||
BIN
docs/.vuepress/public/images/bg-home.jpg
Normal file
BIN
docs/.vuepress/public/images/bg-home.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@ -1,7 +1,17 @@
|
||||
---
|
||||
home: true
|
||||
banner: /images/big-banner.jpg
|
||||
motto: 世间的美好总是不期而遇,恬静而自然。
|
||||
author: pengzhanbo
|
||||
createTime: 2022/03/26 07:46:50
|
||||
banner: /images/bg-home.jpg
|
||||
hero:
|
||||
name: 鹏展博
|
||||
profession: 前端开发工程师
|
||||
text: 简单介绍专业技能信息相关的描述
|
||||
actions:
|
||||
-
|
||||
theme: brand
|
||||
text: Blog
|
||||
link: /
|
||||
-
|
||||
theme: alt
|
||||
text: Github
|
||||
link: /
|
||||
---
|
||||
|
||||
BIN
docs/home-banner.jpg
Normal file
BIN
docs/home-banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"docs:build": "vuepress-cli build --clean-cache",
|
||||
"docs:clean": "rimraf .vuepress/.temp .vuepress/.cache .vuepress/dist",
|
||||
"docs:dev": "vuepress-cli dev --clean-cache",
|
||||
"docs:dev": "vuepress-cli dev --clean-cache --clean-temp",
|
||||
"docs:serve": "anywhere -s -h localhost -d .vuepress/dist"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { App } from '@vuepress/core'
|
||||
import type { App, Page } from '@vuepress/core'
|
||||
import type { BlogPostData, BlogPostDataItem } from '../shared/index.js'
|
||||
import type { PluginOption } from './plugin.js'
|
||||
|
||||
@ -21,6 +21,8 @@ const getTimestamp = (time: Date): number => {
|
||||
return new Date(time).getTime()
|
||||
}
|
||||
|
||||
const EXCERPT_SPLIT = '<!-- more -->'
|
||||
|
||||
export const preparedBlogData = async (
|
||||
app: App,
|
||||
pageFilter: (id: string) => boolean,
|
||||
@ -45,7 +47,7 @@ export const preparedBlogData = async (
|
||||
})
|
||||
}
|
||||
|
||||
const blogData: BlogPostData = pages.map((page) => {
|
||||
const blogData: BlogPostData = pages.map((page: Page) => {
|
||||
let extended: Partial<BlogPostDataItem> = {}
|
||||
if (typeof options.extendBlogData === 'function') {
|
||||
extended = options.extendBlogData(page)
|
||||
@ -56,7 +58,10 @@ export const preparedBlogData = async (
|
||||
...extended,
|
||||
}
|
||||
|
||||
if (options.excerpt) data.excerpt = (page as any).excerpt
|
||||
if (options.excerpt && page.contentRendered.includes(EXCERPT_SPLIT)) {
|
||||
const contents = page.contentRendered.split(EXCERPT_SPLIT)
|
||||
data.excerpt = contents[0]
|
||||
}
|
||||
|
||||
return data as BlogPostDataItem
|
||||
})
|
||||
|
||||
@ -3,7 +3,7 @@ export interface BlogDataPluginOptions {
|
||||
exclude?: string | string[]
|
||||
sortBy?: 'createTime' | false | (<T>(prev: T, next: T) => boolean)
|
||||
excerpt?: boolean
|
||||
extendBlogData?: <T>(page: T) => Partial<BlogPostDataItem>
|
||||
extendBlogData?: <T = any>(page: T) => Record<string, any>
|
||||
}
|
||||
|
||||
export type BlogPostData<T extends object = object> = BlogPostDataItem<T>[]
|
||||
|
||||
@ -94,7 +94,6 @@ export const watchNotesData = (
|
||||
}
|
||||
|
||||
function initSidebar(note: NotesItem, pages: NotePage[]): NotesSidebarItem[] {
|
||||
console.log('pages:', pages)
|
||||
if (!note.sidebar) return []
|
||||
if (note.sidebar === 'auto') return []
|
||||
return initSidebarByConfig(note, pages)
|
||||
@ -105,7 +104,6 @@ function initSidebarByConfig(
|
||||
pages: NotePage[]
|
||||
): NotesSidebarItem[] {
|
||||
return (sidebar as NotesSidebar).map((item) => {
|
||||
console.log('text: ', text, 's-item: ', item, 'dir: ', dir)
|
||||
if (typeof item === 'string') {
|
||||
const current = findNotePage(item, dir, pages)
|
||||
return {
|
||||
|
||||
41
packages/theme/src/client/components/Backdrop.vue
Normal file
41
packages/theme/src/client/components/Backdrop.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div v-if="show" class="backdrop" />
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/*rtl:ignore*/
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/*rtl:ignore*/
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-backdrop);
|
||||
background: var(--vp-backdrop-bg-color);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.backdrop.fade-enter-from,
|
||||
.backdrop.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.backdrop.fade-leave-active {
|
||||
transition-duration: 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.backdrop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
packages/theme/src/client/components/Blog.vue
Normal file
34
packages/theme/src/client/components/Blog.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import BlogAvatar from './BlogAvatar.vue'
|
||||
import PostList from './PostList.vue'
|
||||
</script>
|
||||
<template>
|
||||
<div class="blog-wrapper">
|
||||
<PostList />
|
||||
<BlogAvatar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.blog-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding-top: var(--vp-nav-height);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.blog-wrapper {
|
||||
max-width: 784px;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.blog-wrapper {
|
||||
max-width: 1104px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
packages/theme/src/client/components/BlogAvatar.vue
Normal file
52
packages/theme/src/client/components/BlogAvatar.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useThemeLocaleData } from '../composables/index.js'
|
||||
|
||||
const theme = useThemeLocaleData()
|
||||
|
||||
const avatar = computed(() => theme.value.avatar)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="avatar" class="blog-avatar-wrapper">
|
||||
<div class="avatar-profile">
|
||||
<p v-if="avatar.url">
|
||||
<img :src="avatar.url" :alt="avatar.name" />
|
||||
</p>
|
||||
<div>
|
||||
<h3>{{ avatar.name }}</h3>
|
||||
<p>{{ avatar.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.blog-avatar-wrapper {
|
||||
position: sticky;
|
||||
top: calc(var(--vp-nav-height) + 2rem);
|
||||
width: 270px;
|
||||
margin-left: 2rem;
|
||||
margin-top: 2rem;
|
||||
border-left: solid 1px var(--vp-c-divider);
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
|
||||
img {
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.blog-avatar-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,28 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import Page from './Page.vue'
|
||||
</script>
|
||||
<template>
|
||||
<div class="plume-blog-page">
|
||||
<Page />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plume-blog-page {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
margin: var(--vp-layout-top-height, 0px) auto 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.plume-blog-page {
|
||||
padding-top: var(--vp-nav-height);
|
||||
}
|
||||
|
||||
.plume-blog-page.has-sidebar {
|
||||
margin: var(--vp-layout-top-height, 0px) 0 0;
|
||||
padding-left: var(--vp-sidebar-width);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,3 +1,138 @@
|
||||
<script lang="ts" setup>
|
||||
import { usePageFrontmatter, withBase } from '@vuepress/client'
|
||||
import { computed } from 'vue'
|
||||
import type { PlumeThemeHomeFrontmatter } from '../../shared/index.js'
|
||||
import { useThemeLocaleData } from '../composables/index.js'
|
||||
import VButton from './VButton.vue'
|
||||
|
||||
const matter = usePageFrontmatter<PlumeThemeHomeFrontmatter>()
|
||||
const theme = useThemeLocaleData()
|
||||
|
||||
const homeStyle = computed(() => {
|
||||
return {
|
||||
'background-image': `url(${withBase(matter.value.banner || '')})`,
|
||||
}
|
||||
})
|
||||
|
||||
const name = computed(() => matter.value.hero?.name)
|
||||
const profession = computed(() => matter.value.hero?.profession)
|
||||
const text = computed(() => matter.value.hero?.text)
|
||||
|
||||
const actions = computed(() => {
|
||||
return matter.value.hero?.actions ?? []
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>home</div>
|
||||
<div class="plume-home" :style="homeStyle">
|
||||
<div class="container">
|
||||
<div v-if="matter.hero" class="content">
|
||||
<h2 v-if="name" class="hero-name">{{ name }}</h2>
|
||||
<p v-if="profession" class="hero-profession">
|
||||
<span class="line"></span> <span>{{ profession }}</span>
|
||||
</p>
|
||||
<p v-if="text" class="hero-text">{{ text }}</p>
|
||||
<div v-if="actions" class="actions">
|
||||
<div v-for="action in actions" :key="action.link" class="action">
|
||||
<VButton
|
||||
tag="a"
|
||||
size="medium"
|
||||
:theme="action.theme"
|
||||
:text="action.text"
|
||||
:href="action.link"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plume-home {
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-height: calc(100vh - var(--vp-nav-height));
|
||||
filter: var(--vp-home-hero-image-filter);
|
||||
}
|
||||
|
||||
.plume-home .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.plume-home .content {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.plume-home .container {
|
||||
max-width: 768px;
|
||||
padding-top: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.plume-home .container {
|
||||
max-width: 1104px;
|
||||
padding-top: 8rem;
|
||||
}
|
||||
|
||||
.plume-home .content .hero-profession {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.plume-home .content .hero-name {
|
||||
font-size: 100px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--vp-c-text-hero-name);
|
||||
}
|
||||
.plume-home .content .hero-profession {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
color: var(--vp-c-text-hero-profession);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.plume-home .content .hero-profession .line {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 0;
|
||||
border-top: solid 1px var(--vp-c-text-hero-profession);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.plume-home .content .hero-text {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-top: 1.5rem;
|
||||
color: var(--vp-c-text-hero-text);
|
||||
padding: 6px 20px;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -6px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,7 +4,11 @@ import { useSidebar } from '../composables/index.js'
|
||||
const { hasSidebar } = useSidebar()
|
||||
</script>
|
||||
<template>
|
||||
<div class="layout-content" :class="{ 'has-sidebar': hasSidebar }">
|
||||
<div
|
||||
id="LayoutContent"
|
||||
class="layout-content"
|
||||
:class="{ 'has-sidebar': hasSidebar }"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
109
packages/theme/src/client/components/LocalNav.vue
Normal file
109
packages/theme/src/client/components/LocalNav.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSidebar, useThemeLocaleData } from '../composables/index.js'
|
||||
import IconAlignLeft from './icons/IconAlignLeft.vue'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'open-menu'): void
|
||||
}>()
|
||||
|
||||
const theme = useThemeLocaleData()
|
||||
const { hasSidebar } = useSidebar()
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasSidebar" class="local-nav">
|
||||
<button
|
||||
class="menu"
|
||||
:aria-expanded="open"
|
||||
aria-controls="SidebarNav"
|
||||
@click="$emit('open-menu')"
|
||||
>
|
||||
<IconAlignLeft class="menu-icon" />
|
||||
<span class="menu-text"> Menu </span>
|
||||
</button>
|
||||
|
||||
<a class="top-link" href="#" @click="scrollToTop"> Return to top </a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.local-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
/*rtl:ignore*/
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-local-nav);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--vp-c-gutter);
|
||||
padding-top: var(--vp-layout-top-height, 0px);
|
||||
width: 100%;
|
||||
background-color: var(--vp-local-nav-bg-color);
|
||||
transition: border-color 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.local-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 24px 11px;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.menu:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menu {
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.top-link {
|
||||
display: block;
|
||||
padding: 12px 24px 11px;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.top-link:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.top-link {
|
||||
padding: 12px 32px 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -6,6 +6,7 @@ import NavBarAppearance from './NavBarAppearance.vue'
|
||||
import NavBarExtra from './NavBarExtra.vue'
|
||||
import NavBarHamburger from './NavBarHamburger.vue'
|
||||
import NavBarMenu from './NavBarMenu.vue'
|
||||
import NavBarSearch from './NavBarSearch.vue'
|
||||
import NavBarSocialLinks from './NavBarSocialLinks.vue'
|
||||
import NavBarTitle from './NavBarTitle.vue'
|
||||
|
||||
@ -35,7 +36,7 @@ const classes = computed(() => ({
|
||||
<div class="content">
|
||||
<div class="curtain"></div>
|
||||
<div class="content-body">
|
||||
<NavbarSearch class="search" />
|
||||
<NavBarSearch class="search" />
|
||||
<NavBarMenu class="menu" />
|
||||
<NavBarAppearance class="appearance" />
|
||||
<NavBarSocialLinks class="social-links" />
|
||||
|
||||
209
packages/theme/src/client/components/Nav/NavBarSearch.vue
Normal file
209
packages/theme/src/client/components/Nav/NavBarSearch.vue
Normal file
@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div class="navbar-search">
|
||||
<DocSearch />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.navbar-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar-search {
|
||||
flex-grow: 1;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.navbar-search {
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand);
|
||||
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||
--docsearch-text-color: var(--vp-c-text-1);
|
||||
--docsearch-muted-color: var(--vp-c-text-2);
|
||||
--docsearch-searchbox-shadow: none;
|
||||
--docsearch-searchbox-focus-background: transparent;
|
||||
--docsearch-key-gradient: transparent;
|
||||
--docsearch-key-shadow: none;
|
||||
--docsearch-modal-background: var(--vp-c-bg-soft);
|
||||
--docsearch-footer-background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.dark .DocSearch {
|
||||
--docsearch-modal-shadow: none;
|
||||
--docsearch-footer-shadow: none;
|
||||
--docsearch-logo-color: var(--vp-c-text-2);
|
||||
--docsearch-hit-background: var(--vp-c-bg-soft-mute);
|
||||
--docsearch-hit-color: var(--vp-c-text-2);
|
||||
--docsearch-hit-shadow: none;
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 55px;
|
||||
background: transparent;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.DocSearch-Button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.DocSearch-Button:focus:not(:focus-visible) {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px 0 12px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Search-Icon {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
fill: currentColor;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Search-Icon {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Search-Icon {
|
||||
top: 1px;
|
||||
margin-right: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Placeholder {
|
||||
display: none;
|
||||
margin-top: 2px;
|
||||
padding: 0 16px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Button-Placeholder {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Keys {
|
||||
/*rtl:ignore*/
|
||||
direction: ltr;
|
||||
display: none;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Button-Keys {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key {
|
||||
display: block;
|
||||
margin: 2px 0 0 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
/*rtl:begin:ignore*/
|
||||
border-right: none;
|
||||
border-radius: 4px 0 0 4px;
|
||||
padding-left: 6px;
|
||||
/*rtl:end:ignore*/
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-family: var(--vp-font-family-base);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: color 0.5s, border-color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
|
||||
/*rtl:begin:ignore*/
|
||||
border-right: 1px solid var(--vp-c-divider);
|
||||
border-left: none;
|
||||
border-radius: 0 4px 4px 0;
|
||||
padding-left: 2px;
|
||||
padding-right: 6px;
|
||||
/*rtl:end:ignore*/
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key:first-child {
|
||||
font-size: 1px;
|
||||
letter-spacing: -12px;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key:first-child:after {
|
||||
font-size: 12px;
|
||||
letter-spacing: normal;
|
||||
color: var(--docsearch-muted-color);
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key:first-child > * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark .DocSearch-Footer {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.DocSearch-Form {
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
background-color: var(--vp-c-white);
|
||||
}
|
||||
|
||||
.dark .DocSearch-Form {
|
||||
background-color: var(--vp-c-bg-soft-mute);
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSiteLocaleData } from '@vuepress/client'
|
||||
import { useSiteLocaleData, withBase } from '@vuepress/client'
|
||||
import { useSidebar } from '../../composables/index.js'
|
||||
import { useThemeLocaleData } from '../../composables/themeData.js'
|
||||
import AutoLink from '../AutoLink.vue'
|
||||
@ -12,7 +12,7 @@ const { hasSidebar } = useSidebar()
|
||||
|
||||
<template>
|
||||
<div class="navbar-title" :class="{ 'has-sidebar': hasSidebar }">
|
||||
<AutoLink class="title" :href="theme.home ? theme.home.link : ''">
|
||||
<AutoLink class="title" :href="theme.home || withBase('/')">
|
||||
<VImage
|
||||
v-if="theme.logo"
|
||||
class="logo"
|
||||
|
||||
@ -1,15 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { provide } from 'vue'
|
||||
import { usePageData } from '@vuepress/client'
|
||||
import { computed, provide } from 'vue'
|
||||
import type { PlumeThemePageData } from '../../../shared/index.js'
|
||||
import { useNav } from '../../composables/nav.js'
|
||||
import Navbar from './NavBar.vue'
|
||||
import NavScreen from './NavScreen.vue'
|
||||
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
|
||||
const { isScreenOpen, closeScreen, toggleScreen } = useNav()
|
||||
|
||||
const fixed = computed(() => {
|
||||
return page.value.isBlogPost || page.value.type === 'blog'
|
||||
})
|
||||
|
||||
provide('close-screen', closeScreen)
|
||||
</script>
|
||||
<template>
|
||||
<div class="nav-wrapper">
|
||||
<div class="nav-wrapper" :class="{ fixed }">
|
||||
<Navbar :is-screen-open="isScreenOpen" @toggle-screen="toggleScreen" />
|
||||
<NavScreen :open="isScreenOpen" />
|
||||
</div>
|
||||
@ -27,6 +35,15 @@ provide('close-screen', closeScreen)
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
.nav-wrapper.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.nav-wrapper.fixed :deep(.navbar-wrapper) {
|
||||
border-bottom-color: var(--vp-c-gutter);
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.nav-wrapper {
|
||||
position: fixed;
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSidebar } from '../composables/index.js'
|
||||
import { usePageData } from '@vuepress/client'
|
||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||
import { useDarkMode, useSidebar } from '../composables/index.js'
|
||||
import PageAside from './PageAside.vue'
|
||||
import PageMeta from './PageMeta.vue'
|
||||
|
||||
const { hasSidebar, hasAside } = useSidebar()
|
||||
const isDark = useDarkMode()
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="plume-page"
|
||||
:class="{ 'has-sidebar': hasSidebar, 'has-aside': hasAside }"
|
||||
:class="{
|
||||
'has-sidebar': hasSidebar,
|
||||
'has-aside': hasAside,
|
||||
'is-blog': page.isBlogPost,
|
||||
}"
|
||||
>
|
||||
<div class="container">
|
||||
<div v-if="hasAside" class="aside">
|
||||
@ -21,7 +30,9 @@ const { hasSidebar, hasAside } = useSidebar()
|
||||
<div class="content">
|
||||
<div class="content-container">
|
||||
<main class="main">
|
||||
<PageMeta />
|
||||
<Content class="plume-content" />
|
||||
<PageComment :darkmode="isDark" />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,6 +50,10 @@ const { hasSidebar, hasAside } = useSidebar()
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plume-page.is-blog {
|
||||
padding-top: calc(var(--vp-nav-height) + 32px);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.plume-page {
|
||||
padding: 48px 32px 128px;
|
||||
@ -46,7 +61,8 @@ const { hasSidebar, hasAside } = useSidebar()
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.plume-page {
|
||||
.plume-page,
|
||||
.plume-page.is-blog {
|
||||
padding: 32px 32px 0;
|
||||
}
|
||||
|
||||
|
||||
131
packages/theme/src/client/components/PageMeta.vue
Normal file
131
packages/theme/src/client/components/PageMeta.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<script lang="ts" setup>
|
||||
import { usePageData, usePageFrontmatter } from '@vuepress/client'
|
||||
import { computed } from 'vue'
|
||||
import type {
|
||||
PlumeThemePageData,
|
||||
PlumeThemePostFrontmatter,
|
||||
} from '../../shared/index.js'
|
||||
import IconClock from './icons/IconClock.vue'
|
||||
import IconTag from './icons/IconTag.vue'
|
||||
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const matter = usePageFrontmatter<PlumeThemePostFrontmatter>()
|
||||
|
||||
const createTime = computed(() => {
|
||||
if (matter.value.createTime) {
|
||||
return matter.value.createTime.split(' ')[0].replace(/\//g, '-')
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const categoryList = computed(() => {
|
||||
return page.value.categoryList ?? []
|
||||
})
|
||||
|
||||
const tags = computed(() => {
|
||||
if (matter.value.tags) {
|
||||
return matter.value.tags.slice(0, 4)
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const hasMeta = computed(() => tags.value.length || createTime.value)
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="page.isBlogPost && categoryList.length"
|
||||
class="page-category-wrapper"
|
||||
>
|
||||
<template
|
||||
v-for="({ type, name }, index) in categoryList"
|
||||
:key="index + '-' + type"
|
||||
>
|
||||
<span class="category">{{ name }}</span>
|
||||
<span v-if="index !== categoryList.length - 1" class="dot">›</span>
|
||||
</template>
|
||||
</div>
|
||||
<h2 v-if="page.isBlogPost" class="page-title" :class="{ padding: !hasMeta }">
|
||||
{{ page.title }}
|
||||
</h2>
|
||||
<div v-if="hasMeta" class="page-meta-wrapper">
|
||||
<p v-if="tags.length > 0">
|
||||
<IconTag class="icon" />
|
||||
<span v-for="tag in tags" :key="tag" class="tag">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="createTime">
|
||||
<IconClock class="icon" /><span>{{ createTime }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-category-wrapper {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 2rem;
|
||||
border-left: solid 4px var(--vp-c-brand);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.page-category-wrapper .category {
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color var(--t-color);
|
||||
}
|
||||
|
||||
.page-category-wrapper .category:hover {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.page-category-wrapper .dot {
|
||||
margin: 0 0.2rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-title.padding {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
.page-meta-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 14px;
|
||||
border-bottom: solid 1px var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.page-meta-wrapper p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.page-meta-wrapper .icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.page-meta-wrapper .tag {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
margin-right: 0.3rem;
|
||||
padding: 3px 6px;
|
||||
color: var(--vp-c-text-2);
|
||||
background-color: var(--vp-c-mute);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.page-meta-wrapper .tag:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
126
packages/theme/src/client/components/PostItem.vue
Normal file
126
packages/theme/src/client/components/PostItem.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
|
||||
import AutoLink from './AutoLink.vue'
|
||||
import IconClock from './icons/IconClock.vue'
|
||||
import IconFolder from './icons/IconFolder.vue'
|
||||
import IconTag from './icons/IconTag.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
post: PlumeThemeBlogPostItem
|
||||
}>()
|
||||
|
||||
const categoryList = computed(() => {
|
||||
return props.post.categoryList ?? []
|
||||
})
|
||||
|
||||
const tags = computed(() => {
|
||||
return (props.post.tags ?? []).slice(0, 4)
|
||||
})
|
||||
|
||||
const createTime = computed(() => {
|
||||
return props.post.createTime?.split(' ')[0].replace(/\//g, '-')
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="post-item">
|
||||
<h3>
|
||||
<AutoLink :href="post.path">{{ post.title }}</AutoLink>
|
||||
<div
|
||||
v-if="typeof post.sticky === 'boolean' ? post.sticky : post.sticky >= 0"
|
||||
class="sticky"
|
||||
>
|
||||
TOP
|
||||
</div>
|
||||
</h3>
|
||||
<div class="post-meta">
|
||||
<div v-if="categoryList.length" class="category-list">
|
||||
<IconFolder class="icon" />
|
||||
<template v-for="(cate, i) in categoryList" :key="i">
|
||||
<span>{{ cate.name }}</span>
|
||||
<span v-if="i !== categoryList.length - 1">/</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="tags.length" class="tag-list">
|
||||
<IconTag class="icon" />
|
||||
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
|
||||
</div>
|
||||
<div v-if="createTime" class="create-time">
|
||||
<IconClock class="icon" />
|
||||
<span>{{ createTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div v-if="post.excerpt" class="plume-content" v-html="post.excerpt"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.post-item {
|
||||
padding-top: 1rem;
|
||||
margin: 0 1.75rem 3rem;
|
||||
// border-bottom: solid 1px var(--vp-c-divider);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
transition: color var(--t-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h3:hover {
|
||||
color: var(--vp-c-brand);
|
||||
|
||||
.sticky {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
}
|
||||
|
||||
.sticky {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
padding: 3px 6px;
|
||||
margin-left: 0.5rem;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
background-color: var(--vp-c-bg-soft-mute);
|
||||
}
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-right: 1rem;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0.3rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
packages/theme/src/client/components/PostList.vue
Normal file
38
packages/theme/src/client/components/PostList.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import { useBlogPostData } from '@vuepress-plume/vuepress-plugin-blog-data/client'
|
||||
import { computed } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
|
||||
import PostItem from './PostItem.vue'
|
||||
|
||||
const list = useBlogPostData() as unknown as Ref<PlumeThemeBlogPostItem[]>
|
||||
|
||||
const postList = computed(() => {
|
||||
const stickyList = list.value.filter((item) =>
|
||||
typeof item.sticky === 'boolean' ? item.sticky : item.sticky >= 0
|
||||
)
|
||||
const otherList = list.value.filter(
|
||||
(item) => item.sticky === undefined || item.sticky === false
|
||||
)
|
||||
|
||||
return [
|
||||
...stickyList.sort((prev, next) => {
|
||||
if (next.sticky === true && prev.sticky === true) return 0
|
||||
return next.sticky > prev.sticky ? 1 : -1
|
||||
}),
|
||||
...otherList,
|
||||
]
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="post-list">
|
||||
<PostItem v-for="post in postList" :key="post.path" :post="post" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.post-list {
|
||||
padding-top: 2rem;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
75
packages/theme/src/client/components/SkipLink.vue
Normal file
75
packages/theme/src/client/components/SkipLink.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const backToTop = ref()
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => backToTop.value.focus()
|
||||
)
|
||||
|
||||
function focusOnTargetAnchor({ target }: Event) {
|
||||
const el = document.querySelector<HTMLAnchorElement>(
|
||||
(target as HTMLAnchorElement).hash
|
||||
)
|
||||
|
||||
if (el) {
|
||||
const removeTabIndex = () => {
|
||||
el.removeAttribute('tabindex')
|
||||
el.removeEventListener('blur', removeTabIndex)
|
||||
}
|
||||
|
||||
el.setAttribute('tabindex', '-1')
|
||||
el.addEventListener('blur', removeTabIndex)
|
||||
el.focus()
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span ref="backToTop" tabindex="-1" />
|
||||
<a
|
||||
href="#LayoutContent"
|
||||
class="skip-link visually-hidden"
|
||||
@click="focusOnTargetAnchor"
|
||||
>
|
||||
Skip to content
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.skip-link {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 8px 16px;
|
||||
z-index: 999;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: var(--vp-c-brand);
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
height: auto;
|
||||
width: auto;
|
||||
clip: auto;
|
||||
clip-path: none;
|
||||
}
|
||||
|
||||
.dark .skip-link {
|
||||
color: var(--vp-c-green);
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.skip-link {
|
||||
top: 14px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
packages/theme/src/client/components/VButton.vue
Normal file
122
packages/theme/src/client/components/VButton.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { EXTERNAL_URL_RE } from '../utils/index.js'
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string
|
||||
size?: 'medium' | 'big'
|
||||
theme?: 'brand' | 'alt' | 'sponsor'
|
||||
text: string
|
||||
href?: string
|
||||
}>()
|
||||
|
||||
const classes = computed(() => [props.size ?? 'medium', props.theme ?? 'brand'])
|
||||
|
||||
const isExternal = computed(
|
||||
() => props.href && EXTERNAL_URL_RE.test(props.href)
|
||||
)
|
||||
|
||||
const component = computed(() => {
|
||||
if (props.tag) {
|
||||
return props.tag
|
||||
}
|
||||
|
||||
return props.href ? 'a' : 'button'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component
|
||||
:is="component"
|
||||
class="VPButton"
|
||||
:class="classes"
|
||||
:href="href"
|
||||
:target="isExternal ? '_blank' : undefined"
|
||||
:rel="isExternal ? 'noreferrer' : undefined"
|
||||
>
|
||||
{{ text }}
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPButton {
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPButton:active {
|
||||
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
|
||||
}
|
||||
|
||||
.VPButton.medium {
|
||||
border-radius: 20px;
|
||||
padding: 0 20px;
|
||||
line-height: 38px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.VPButton.big {
|
||||
border-radius: 24px;
|
||||
padding: 0 24px;
|
||||
line-height: 46px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.VPButton.brand {
|
||||
border-color: var(--vp-button-brand-border);
|
||||
color: var(--vp-button-brand-text);
|
||||
background-color: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
.VPButton.brand:hover {
|
||||
border-color: var(--vp-button-brand-hover-border);
|
||||
color: var(--vp-button-brand-hover-text);
|
||||
background-color: var(--vp-button-brand-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.brand:active {
|
||||
border-color: var(--vp-button-brand-active-border);
|
||||
color: var(--vp-button-brand-active-text);
|
||||
background-color: var(--vp-button-brand-active-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt {
|
||||
border-color: var(--vp-button-alt-border);
|
||||
color: var(--vp-button-alt-text);
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor {
|
||||
border-color: var(--vp-button-sponsor-border);
|
||||
color: var(--vp-button-sponsor-text);
|
||||
background-color: var(--vp-button-sponsor-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor:hover {
|
||||
border-color: var(--vp-button-sponsor-hover-border);
|
||||
color: var(--vp-button-sponsor-hover-text);
|
||||
background-color: var(--vp-button-sponsor-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor:active {
|
||||
border-color: var(--vp-button-sponsor-active-border);
|
||||
color: var(--vp-button-sponsor-active-text);
|
||||
background-color: var(--vp-button-sponsor-active-bg);
|
||||
}
|
||||
</style>
|
||||
70
packages/theme/src/client/components/VFooter.vue
Normal file
70
packages/theme/src/client/components/VFooter.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useSidebar, useThemeLocaleData } from '../composables/index.js'
|
||||
|
||||
const theme = useThemeLocaleData()
|
||||
const { hasSidebar } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<footer
|
||||
v-if="theme.footer"
|
||||
class="plume-footer"
|
||||
:class="{ 'has-sidebar': hasSidebar }"
|
||||
>
|
||||
<div class="container">
|
||||
<p
|
||||
v-if="theme.footer.message"
|
||||
class="message"
|
||||
v-html="theme.footer.message"
|
||||
></p>
|
||||
<p
|
||||
v-if="theme.footer.copyright"
|
||||
class="copyright"
|
||||
v-html="theme.footer.copyright"
|
||||
></p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plume-footer {
|
||||
position: relative;
|
||||
z-index: var(--vp-z-index-footer);
|
||||
border-top: 1px solid var(--vp-c-gutter);
|
||||
padding: 32px 24px;
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.plume-footer.has-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.plume-footer {
|
||||
padding: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: var(--vp-layout-max-width);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message,
|
||||
.copyright {
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.message {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
order: 1;
|
||||
}
|
||||
</style>
|
||||
11
packages/theme/src/client/components/icons/IconClock.vue
Normal file
11
packages/theme/src/client/components/icons/IconClock.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<g fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
11
packages/theme/src/client/components/icons/IconFolder.vue
Normal file
11
packages/theme/src/client/components/icons/IconFolder.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<g fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
11
packages/theme/src/client/components/icons/IconTag.vue
Normal file
11
packages/theme/src/client/components/icons/IconTag.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<g fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
@ -9,7 +9,7 @@ import './styles/index.scss'
|
||||
export default defineClientConfig({
|
||||
enhance({ app }) {
|
||||
// eslint-disable-next-line vue/match-component-file-name
|
||||
app.component('NavbarSearch', () => {
|
||||
app.component('DocSearch', () => {
|
||||
const SearchComponent =
|
||||
app.component('Docsearch') || app.component('SearchBox')
|
||||
if (SearchComponent) {
|
||||
@ -17,6 +17,15 @@ export default defineClientConfig({
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// eslint-disable-next-line vue/match-component-file-name
|
||||
app.component('PageComment', (props) => {
|
||||
const CommentService = app.component('CommentService')
|
||||
if (CommentService) {
|
||||
return h(CommentService, props)
|
||||
}
|
||||
return null
|
||||
})
|
||||
},
|
||||
setup() {
|
||||
setupDarkMode()
|
||||
|
||||
@ -3,15 +3,20 @@ import { usePageData } from '@vuepress/client'
|
||||
import { provide, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||
import Backdrop from '../components/Backdrop.vue'
|
||||
import Blog from '../components/Blog.vue'
|
||||
import Home from '../components/Home.vue'
|
||||
import LayoutContent from '../components/LayoutContent.vue'
|
||||
import LocalNav from '../components/LocalNav.vue'
|
||||
import Nav from '../components/Nav/index.vue'
|
||||
import Page from '../components/Page.vue'
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
import SkipLink from '../components/SkipLink.vue'
|
||||
import VFooter from '../components/VFooter.vue'
|
||||
import {
|
||||
useCloseSidebarOnEscape,
|
||||
useScrollPromise,
|
||||
useSidebar,
|
||||
useThemeLocaleData,
|
||||
} from '../composables/index.js'
|
||||
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
@ -37,10 +42,16 @@ const onBeforeLeave = scrollPromise.pending
|
||||
</script>
|
||||
<template>
|
||||
<div class="theme-plume">
|
||||
<SkipLink />
|
||||
<Backdrop :show="isSidebarOpen" @click="closeSidebar" />
|
||||
<Nav />
|
||||
<LocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
|
||||
<Sidebar :open="isSidebarOpen" />
|
||||
<LayoutContent>
|
||||
<Page />
|
||||
<Home v-if="page.frontmatter.home" />
|
||||
<Blog v-else-if="page.type === 'blog'" />
|
||||
<Page v-else />
|
||||
<VFooter />
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -165,10 +165,10 @@ pre[class*='language-'] {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-tabs-nav {
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
.code-tabs-nav {
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
div[class*='language-'] {
|
||||
@ -274,4 +274,8 @@ div[class*='language-'] {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.code-tabs-nav {
|
||||
margin-bottom: -0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,10 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.plume-content h2:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.plume-content h3 {
|
||||
margin: 32px 0 0;
|
||||
letter-spacing: -0.01em;
|
||||
@ -68,6 +72,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.plume-content img {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.plume-content img + img {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.plume-content a img + span {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paragraph and inline elements
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
@ -7,3 +7,8 @@
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@ -84,6 +84,10 @@
|
||||
--vp-c-text-inverse-2: var(--vp-c-text-dark-2);
|
||||
--vp-c-text-inverse-3: var(--vp-c-text-dark-3);
|
||||
|
||||
--vp-c-text-hero-name: var(--vp-c-text-dark-1);
|
||||
--vp-c-text-hero-profession: var(--vp-c-text-dark-2);
|
||||
--vp-c-text-hero-text: var(--vp-c-text-dark-1);
|
||||
|
||||
--vp-c-text-code: #476582;
|
||||
|
||||
--vp-c-brand: var(--vp-c-green);
|
||||
|
||||
@ -47,6 +47,16 @@ export const setupPlugins = (
|
||||
blogDataPlugin({
|
||||
include: ['**/*.md'],
|
||||
exclude: ['**/{README,index}.md', 'notes/**'],
|
||||
sortBy: 'createTime',
|
||||
excerpt: true,
|
||||
extendBlogData(page: any) {
|
||||
return {
|
||||
categoryList: page.data.categoryList,
|
||||
tags: page.frontmatter.tags,
|
||||
sticky: page.frontmatter.sticky,
|
||||
createTime: page.data.frontmatter.createTime,
|
||||
}
|
||||
},
|
||||
}),
|
||||
localeOptions.notes ? notesDataPlugin(localeOptions.notes) : [],
|
||||
activeHeaderLinksPlugin({
|
||||
|
||||
52
packages/theme/src/node/setupPages.ts
Normal file
52
packages/theme/src/node/setupPages.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import type { App, Page } from '@vuepress/core'
|
||||
import { createPage } from '@vuepress/core'
|
||||
import type {
|
||||
PageCategoryData,
|
||||
PlumeThemeLocaleOptions,
|
||||
PlumeThemePageData,
|
||||
} from '../shared/index.js'
|
||||
|
||||
export async function setupPage(app: App) {
|
||||
const blogPage = await createPage(app, {
|
||||
path: '/blog/',
|
||||
})
|
||||
const productPage = await createPage(app, {
|
||||
path: '/product/',
|
||||
})
|
||||
|
||||
app.pages.push(blogPage, productPage)
|
||||
}
|
||||
|
||||
let uuid = 10000
|
||||
const cache: Record<string, number> = {}
|
||||
|
||||
export function autoCategory(
|
||||
page: Page<PlumeThemePageData>,
|
||||
options: PlumeThemeLocaleOptions
|
||||
) {
|
||||
const pagePath = page.filePathRelative
|
||||
if (page.data.type || !pagePath) return
|
||||
|
||||
const { notes } = options
|
||||
if (notes && notes.link && page.path.startsWith(notes.link)) return
|
||||
const categoryList: PageCategoryData[] = pagePath
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.map((category) => {
|
||||
const match = category.match(/^(\d+)?(?:\.?)([^]+)$/) || []
|
||||
!cache[match[2]] && !match[1] && (cache[match[2]] = uuid++)
|
||||
return {
|
||||
type: 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}`
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import type { App, Theme } from '@vuepress/core'
|
||||
import type { App, Page, Theme } from '@vuepress/core'
|
||||
import { fs, getDirname, path } from '@vuepress/utils'
|
||||
import type { PlumeThemeOptions } from '../shared/index.js'
|
||||
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
|
||||
import { setupPlugins } from './plugins.js'
|
||||
import { autoCategory, pageContentRendered, setupPage } from './setupPages.js'
|
||||
|
||||
const __dirname = getDirname(import.meta.url)
|
||||
|
||||
@ -26,6 +27,19 @@ export const plumeTheme = ({
|
||||
},
|
||||
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
|
||||
plugins: setupPlugins(app, themePlugins, localeOptions),
|
||||
onInitialized: async (app) => {
|
||||
await setupPage(app)
|
||||
},
|
||||
extendsPage: (page: Page<PlumeThemePageData>) => {
|
||||
if (page.path === '/blog/') {
|
||||
page.data.type = 'blog'
|
||||
}
|
||||
if (page.path === '/product/') {
|
||||
page.data.type = 'product'
|
||||
}
|
||||
autoCategory(page, localeOptions)
|
||||
pageContentRendered(page)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
packages/theme/src/shared/blog.ts
Normal file
11
packages/theme/src/shared/blog.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { BlogPostDataItem } from '@vuepress-plume/vuepress-plugin-blog-data'
|
||||
import type { PageCategoryData } from './page.js'
|
||||
|
||||
export interface PlumeThemeBlogPostItem extends BlogPostDataItem {
|
||||
tags: string[]
|
||||
sticky: boolean
|
||||
categoryLost: PageCategoryData[]
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export type PlumeThemeBlogPostData = PlumeThemeBlogPostItem[]
|
||||
@ -1,7 +1,19 @@
|
||||
export interface PlumeThemeHomeFrontmatter {
|
||||
home?: true
|
||||
banner?: string
|
||||
hero: {
|
||||
name: string
|
||||
profession?: string
|
||||
text?: string
|
||||
actions: PlumeThemeHeroAction[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlumeThemeHeroAction {
|
||||
theme?: 'brand' | 'alt'
|
||||
text: string
|
||||
link?: string
|
||||
}
|
||||
export interface PlumeThemePostFrontmatter {
|
||||
createTime?: string
|
||||
author?: string
|
||||
|
||||
@ -3,3 +3,4 @@ export * from './frontmatter.js'
|
||||
export * from './note.js'
|
||||
export * from './options/index.js'
|
||||
export * from './page.js'
|
||||
export * from './blog.js'
|
||||
|
||||
@ -40,7 +40,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
/**
|
||||
* 网站站点首页
|
||||
*/
|
||||
home?: false | NavItemWithLink
|
||||
home?: string
|
||||
/**
|
||||
* 网站站点logo
|
||||
*/
|
||||
@ -110,8 +110,6 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
*/
|
||||
notes?: false | NotesDataOptions
|
||||
|
||||
footer?: false | { content: string; copyright: string }
|
||||
|
||||
/**
|
||||
* language text
|
||||
*/
|
||||
@ -147,4 +145,11 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
notFound?: string[]
|
||||
|
||||
backToHome?: string
|
||||
|
||||
footer?:
|
||||
| false
|
||||
| {
|
||||
message?: string
|
||||
copyright?: string
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,4 +4,11 @@ export type PlumeThemePageData = {
|
||||
updateTime: number
|
||||
}
|
||||
isBlogPost: boolean
|
||||
type: 'blog' | 'product'
|
||||
categoryList?: PageCategoryData[]
|
||||
}
|
||||
|
||||
export type PageCategoryData = {
|
||||
type: string | number
|
||||
name: string
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user