Merge pull request #55 from pengzhanbo/new_home

全新的首页!
This commit is contained in:
pengzhanbo 2024-03-03 02:54:35 +08:00 committed by GitHub
commit 2be308b262
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1266 additions and 288 deletions

View File

@ -1,7 +1,6 @@
{
"recommendations": [
"vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint"
]
}

View File

@ -1,11 +1,76 @@
---
home: true
banner: https://file.mo7.cc/api/public/bz
bannerMask:
light: 0.1
dark: 0.3
hero:
name: 鹏展博
tagline: 前端开发工程师
text: 简单介绍专业技能信息相关的描述
config:
-
type: hero
full: true
background: filter
hero:
name: Theme Plume
tagline: Vuepress Next Theme
text: 一个简约的,功能丰富的 vuepress 文档&博客 主题
actions:
-
theme: brand
text: 快速开始 →
link: /
-
theme: alt
text: Github
link: /
-
type: features
title: 标题
description: 随便描述
features:
-
title: 特性1
icon: 🖨
details: 特性说明
-
title: 特性1
icon: 🖨
details: 特性说明
-
title: 特性1
icon: 🖨
details: 特性说明
-
title: 特性1
icon: 🖨
details: 特性说明
-
title: 特性1
icon: 🖨
details: 特性说明
-
type: custom
-
type: text-image
title: 标题
description: 随便描述
list:
-
title: 描述
description: 随便描述一下
- 随便描述一下
- 随便描述一下
image: /images/blogger.png
-
type: image-text
title: 标题
description: 随便描述
list:
-
title: 描述
description: 随便描述一下
- 随便描述一下
- 随便描述一下
image: /images/blogger.png
-
type: profile
name: 鹏展博
description: 前端开发工程师, 热爱前端, 热爱生活, 热爱互联网, 热爱技术, 热爱开源, 热爱生命。
---
这里是自定义的内容,你可以随意添加你自己的内容

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useArchives, useBlogExtract } from '../composables/index.js'
import IconArchive from './icons/IconArchive.vue'
import { useArchives, useBlogExtract } from '../../composables/index.js'
import IconArchive from '../icons/IconArchive.vue'
import ShortPostList from './ShortPostList.vue'
const { archives: archivesLink } = useBlogExtract()

View File

@ -1,11 +1,11 @@
<script lang="ts" setup>
import { usePageData } from 'vuepress/client'
import type { PlumeThemePageData } from '../../shared/index.js'
import { useThemeLocaleData } from '../composables/index.js'
import type { PlumeThemePageData } from '../../../shared/index.js'
import { useThemeLocaleData } from '../../composables/index.js'
import PostList from './PostList.vue'
import Archives from './Archives.vue'
import BlogAside from './BlogAside.vue'
import BlogExtract from './BlogExtract.vue'
import PostList from './PostList.vue'
import Tags from './Tags.vue'
import BlogNav from './BlogNav.vue'

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { useThemeLocaleData } from '../composables/index.js'
import { useThemeLocaleData } from '../../composables/index.js'
import BlogNav from './BlogNav.vue'
import BlogProfile from './BlogProfile.vue'

View File

@ -2,12 +2,12 @@
import { useScrollLock } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vuepress/client'
import { useBlogExtract, useThemeLocaleData } from '../composables/index.js'
import { inBrowser } from '../utils/index.js'
import AutoLink from './AutoLink.vue'
import IconArchive from './icons/IconArchive.vue'
import IconBlogExt from './icons/IconBlogExt.vue'
import IconTag from './icons/IconTag.vue'
import { useBlogExtract, useThemeLocaleData } from '../../composables/index.js'
import { inBrowser } from '../../utils/index.js'
import AutoLink from '../AutoLink.vue'
import IconArchive from '../icons/IconArchive.vue'
import IconBlogExt from '../icons/IconBlogExt.vue'
import IconTag from '../icons/IconTag.vue'
const theme = useThemeLocaleData()
const route = useRoute()

View File

@ -1,10 +1,10 @@
<script lang="ts" setup>
import { useRoute } from 'vuepress/client'
import { useBlogExtract } from '../composables/index.js'
import AutoLink from './AutoLink.vue'
import IconArchive from './icons/IconArchive.vue'
import IconTag from './icons/IconTag.vue'
import IconChevronRight from './icons/IconChevronRight.vue'
import { useBlogExtract } from '../../composables/index.js'
import AutoLink from '../AutoLink.vue'
import IconArchive from '../icons/IconArchive.vue'
import IconTag from '../icons/IconTag.vue'
import IconChevronRight from '../icons/IconChevronRight.vue'
const props = defineProps<{
isLocal?: boolean

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useThemeLocaleData } from '../composables/index.js'
import { useThemeLocaleData } from '../../composables/index.js'
const theme = useThemeLocaleData()
const avatar = computed(() => theme.value.avatar)

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { PlumeThemeBlog } from '../../shared/index.js'
import type { PlumeThemeBlog } from '../../../shared/index.js'
type NonFalseAndNullable<T> = T extends false | null | undefined ? never : T

View File

@ -1,12 +1,12 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useExtraBlogData } from '../composables/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'
import IconLock from './icons/IconLock.vue'
import type { PlumeThemeBlogPostItem } from '../../../shared/index.js'
import { useExtraBlogData } from '../../composables/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'
import IconLock from '../icons/IconLock.vue'
const props = defineProps<{
post: PlumeThemeBlogPostItem

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { usePostListControl } from '../composables/index.js'
import { usePostListControl } from '../../composables/index.js'
import PostItem from './PostItem.vue'
import Pagination from './Pagination.vue'

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import AutoLink from './AutoLink.vue'
import AutoLink from '../AutoLink.vue'
defineProps<{
postList: {

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useBlogExtract, useTags } from '../composables/index.js'
import IconTag from './icons/IconTag.vue'
import { useBlogExtract, useTags } from '../../composables/index.js'
import IconTag from '../icons/IconTag.vue'
import ShortPostList from './ShortPostList.vue'
const { tags, currentTag, postList, handleTagClick } = useTags()

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useThemeLocaleData } from '../composables/index.js'
import IconLock from './icons/IconLock.vue'
const props = defineProps<{
compare: (password: string) => boolean
info?: string
}>()
const theme = useThemeLocaleData()
const password = ref('')
const errorCode = ref(0) // 0: no error, 1: wrong password
function onSubmit() {
const result = props.compare(password.value)
if (!result) {
errorCode.value = 1
}
else {
errorCode.value = 0
password.value = ''
}
}
</script>
<template>
<div class="encrypt-form">
<p class="encrypt-text" v-html="info ?? 'Only Password can access this site'" />
<p class="encrypt-input-wrapper">
<IconLock class="icon icon-lock" />
<input
v-model="password"
class="encrypt-input"
:class="{ error: errorCode === 1 }"
type="password"
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
@keyup.enter="onSubmit"
@input="password && (errorCode = 0)"
>
</p>
<button class="encrypt-button" @click="onSubmit">
{{ theme.encryptButtonText ?? 'Confirm' }}
</button>
</div>
</template>
<style scoped>
.encrypt-form {
margin-top: 20px;
}
.encrypt-text {
margin-top: 40px;
margin-bottom: 30px;
color: var(--vp-c-text-1);
text-align: center;
}
.encrypt-input-wrapper {
position: relative;
}
.icon-lock {
position: absolute;
top: 10px;
left: 10px;
color: var(--vp-c-border);
}
.encrypt-input {
width: 100%;
padding: 8px 12px 8px 32px;
background-color: transparent;
border: 1px solid var(--vp-c-border);
border-radius: 4px;
outline: none;
transition: border-color var(--t-color), background-color var(--t-color);
}
.encrypt-input:focus {
border-color: var(--vp-c-brand-1);
}
.encrypt-input.error {
border-color: var(--vp-c-danger-3);
}
.encrypt-button {
width: 100%;
padding: 8px 12px;
margin-top: 20px;
font-weight: 500;
color: var(--vp-c-white);
cursor: pointer;
background-color: var(--vp-c-brand-1);
border: none;
border-radius: 4px;
outline: none;
transition: background-color var(--t-color);
}
.encrypt-button:hover {
background-color: var(--vp-c-brand-2);
}
</style>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useSiteLocaleData } from 'vuepress/client'
import { useThemeLocaleData } from '../composables/index.js'
import { useGlobalEncrypt } from '../composables/encrypt.js'
import IconLock from './icons/IconLock.vue'
import VFooter from './VFooter.vue'
import EncryptForm from './EncryptForm.vue'
const theme = useThemeLocaleData()
const siteData = useSiteLocaleData()
@ -12,21 +12,6 @@ const { compareGlobal } = useGlobalEncrypt()
const avatar = computed(() => theme.value.avatar)
const title = computed(() => avatar.value?.name || siteData.value.title)
const password = ref('')
const errorCode = ref(0) // 0: no error, 1: wrong password
function compare() {
const result = compareGlobal(password.value)
if (!result) {
errorCode.value = 1
}
else {
errorCode.value = 0
password.value = ''
}
}
</script>
<template>
@ -40,24 +25,7 @@ function compare() {
{{ title }}
</h3>
</div>
<div class="encrypt">
<p class="encrypt-text" v-html="theme.encryptGlobalText ?? 'Only Password can access this site'" />
<p class="encrypt-input-wrapper">
<IconLock class="icon icon-lock" />
<input
v-model="password"
class="encrypt-input"
:class="{ error: errorCode === 1 }"
type="password"
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
@keyup.enter="compare"
@input="password && (errorCode = 0)"
>
</p>
<button class="encrypt-button" @click="compare">
{{ theme.encryptButtonText ?? 'Confirm' }}
</button>
</div>
<EncryptForm :compare="compareGlobal" :info="theme.encryptGlobalText" />
</div>
</div>
<VFooter />
@ -131,62 +99,4 @@ function compare() {
text-align: center;
transition: color var(--t-color);
}
.encrypt {
margin-top: 20px;
}
.encrypt-text {
margin-top: 40px;
margin-bottom: 30px;
color: var(--vp-c-text-1);
text-align: center;
}
.encrypt-input-wrapper {
position: relative;
}
.icon-lock {
position: absolute;
top: 10px;
left: 10px;
color: var(--vp-c-border);
}
.encrypt-input {
width: 100%;
padding: 8px 12px 8px 32px;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-border);
border-radius: 4px;
outline: none;
transition: border-color var(--t-color), background-color var(--t-color);
}
.encrypt-input:focus {
border-color: var(--vp-c-brand-1);
}
.encrypt-input.error {
border-color: var(--vp-c-danger-3);
}
.encrypt-button {
width: 100%;
padding: 8px 12px;
margin-top: 20px;
font-weight: 500;
color: var(--vp-c-white);
cursor: pointer;
background-color: var(--vp-c-brand-1);
border: none;
border-radius: 4px;
outline: none;
transition: background-color var(--t-color);
}
.encrypt-button:hover {
background-color: var(--vp-c-brand-2);
}
</style>

View File

@ -1,26 +1,11 @@
<script setup lang="ts">
import { ref } from 'vue'
import { usePageEncrypt } from '../composables/encrypt.js'
import { useThemeLocaleData } from '../composables/index.js'
import IconLock from './icons/IconLock.vue'
import EncryptForm from './EncryptForm.vue'
const theme = useThemeLocaleData()
const { comparePage } = usePageEncrypt()
const password = ref('')
const errorCode = ref(0) // 0: no error, 1: wrong password
function compare() {
const result = comparePage(password.value)
if (!result) {
errorCode.value = 1
}
else {
errorCode.value = 0
password.value = ''
}
}
</script>
<template>
@ -28,24 +13,7 @@ function compare() {
<div class="logo">
<IconLock class="icon icon-lock-head" />
</div>
<div class="encrypt">
<p class="encrypt-text" v-html="theme.encryptPageText ?? 'Only Password can access this page'" />
<p class="encrypt-input-wrapper">
<IconLock class="icon icon-lock" />
<input
v-model="password"
class="encrypt-input"
:class="{ error: errorCode === 1 }"
type="password"
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
@keyup.enter="compare"
@input="password && (errorCode = 0)"
>
</p>
<button class="encrypt-button" @click="compare">
{{ theme.encryptButtonText ?? 'Confirm' }}
</button>
</div>
<EncryptForm :compare="comparePage" :info="theme.encryptPageText" />
</div>
</template>
@ -68,64 +36,4 @@ function compare() {
transition-property: box-shadow, background-color;
}
}
.encrypt {
margin-top: 20px;
}
.encrypt-text {
margin-top: 20px;
margin-bottom: 30px;
color: var(--vp-c-text-1);
text-align: center;
transition: color var(--t-color);
}
.encrypt-input-wrapper {
position: relative;
}
.icon-lock {
position: absolute;
top: 10px;
left: 10px;
color: var(--vp-c-border);
transition: color var(--t-color);
}
.encrypt-input {
width: 100%;
padding: 8px 12px 8px 32px;
background-color: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-border);
border-radius: 4px;
outline: none;
transition: border-color var(--t-color), background-color var(--t-color);
}
.encrypt-input:focus {
border-color: var(--vp-c-brand-1);
}
.encrypt-input.error {
border-color: var(--vp-c-danger-3);
}
.encrypt-button {
width: 100%;
padding: 8px 12px;
margin-top: 20px;
font-weight: 500;
color: var(--vp-c-white);
cursor: pointer;
background-color: var(--vp-c-brand-1);
border: none;
border-radius: 4px;
outline: none;
transition: background-color var(--t-color);
}
.encrypt-button:hover {
background-color: var(--vp-c-brand-2);
}
</style>

View File

@ -0,0 +1,81 @@
<script lang="ts" setup>
import { usePageFrontmatter } from 'vuepress/client'
import { type Component, computed, resolveComponent } from 'vue'
import type { PlumeThemeHomeFrontmatter } from '../../../shared/index.js'
import HomeBanner from './HomeBanner.vue'
import HomeHero from './HomeHero.vue'
import HomeFeatures from './HomeFeatures.vue'
import HomeTextImage from './HomeTextImage.vue'
import HomeProfile from './HomeProfile.vue'
import HomeCustom from './HomeCustom.vue'
const components: Record<string, Component<any, any, any>> = {
'banner': HomeBanner,
'hero': HomeHero,
'features': HomeFeatures,
'text-image': HomeTextImage,
'image-text': HomeTextImage,
'profile': HomeProfile,
'custom': HomeCustom,
}
const matter = usePageFrontmatter<PlumeThemeHomeFrontmatter>()
const config = computed(() => {
const config = matter.value.config
if (config && config.length)
return config
return [{
type: 'hero',
full: true,
background: 'filter',
hero: matter.value.hero ?? {
name: 'Theme Plume',
tagline: 'VuePress Next Theme',
text: '一个简约的,功能丰富的 vuepress 文档&博客 主题',
},
}]
})
const onlyOnce = computed(() => config.value.length === 1)
function resolveComponentName(type: string) {
return components[type] ?? resolveComponent(type)
}
</script>
<template>
<div class="plume-home">
<template
v-for="(item, index) in config"
:key="item.type + index"
>
<div :class="{ layout: index > 0 && item.type !== 'features' }">
<component
:is="resolveComponentName(item.type)"
v-bind="item"
:only-once="onlyOnce"
/>
</div>
</template>
</div>
</template>
<style scoped>
.plume-home {
min-height: calc(100vh - var(--vp-nav-height) - var(--vp-footer-height, 0px));
}
.plume-home .layout {
transition: background-color var(--t-color);
}
.plume-home .layout:nth-child(odd) {
background-color: var(--vp-c-bg-alt);
}
.plume-home .layout:nth-child(even) {
background-color: var(--vp-c-bg);
}
</style>

View File

@ -1,48 +1,43 @@
<script lang="ts" setup>
<script setup lang="ts">
import { usePageFrontmatter, withBase } from 'vuepress/client'
import { isLinkHttp } from 'vuepress/shared'
import { computed } from 'vue'
import type { PlumeThemeHomeFrontmatter } from '../../shared/index.js'
import { useDarkMode } from '../composables/darkMode.js'
import VButton from './VButton.vue'
import type { PlumeThemeHomeBanner, PlumeThemeHomeFrontmatter } from '../../../shared/index.js'
import { useDarkMode } from '../../composables/darkMode.js'
import VButton from '../VButton.vue'
const props = defineProps<PlumeThemeHomeBanner & { onlyOnce: boolean }>()
const DEFAULT_BANNER = 'http://file.mo7.cc/api/public/bz'
const matter = usePageFrontmatter<PlumeThemeHomeFrontmatter>()
const isDark = useDarkMode()
const mask = computed(() => {
if (typeof matter.value.bannerMask !== 'object')
return matter.value.bannerMask || 0
const mask = props.bannerMask ?? matter.value.bannerMask
if (typeof mask !== 'object')
return mask || 0
return (
(isDark.value
? matter.value.bannerMask.dark
: matter.value.bannerMask.light) || 0
)
return (isDark.value ? mask.dark : mask.light) || 0
})
const homeStyle = computed(() => {
const bannerStyle = computed(() => {
const banner = props.banner ?? matter.value.banner
const link = banner ? isLinkHttp(banner) ? banner : withBase(banner) : DEFAULT_BANNER
return {
'background-image': [
mask.value
? `linear-gradient(rgba(0, 0, 0, ${mask.value}), rgba(0, 0, 0, ${mask.value}))`
: '',
`url(${withBase(matter.value.banner ?? 'http://file.mo7.cc/api/public/bz')})`,
]
.filter(Boolean)
.join(','),
'background-image': `url(${link})`,
}
})
const name = computed(() => matter.value.hero?.name ?? 'Plume')
const tagline = computed(() => matter.value.hero?.tagline ?? 'A VuePress Theme')
const text = computed(() => matter.value.hero?.text)
const actions = computed(() => {
return matter.value.hero?.actions ?? []
})
const name = computed(() => props.hero?.name ?? matter.value.hero?.name ?? 'Plume')
const tagline = computed(() => props.hero?.tagline ?? matter.value.hero?.tagline ?? 'A VuePress Theme')
const text = computed(() => props.hero?.text ?? matter.value.hero?.text)
const actions = computed(() => props.hero?.actions ?? matter.value.hero?.actions ?? [])
</script>
<template>
<div class="plume-home" :style="homeStyle">
<div class="home-banner" :style="bannerStyle">
<div class="banner-mask" :style="{ opacity: mask }" />
<div class="container">
<div class="content">
<h2 v-if="name" class="hero-name">
@ -54,7 +49,7 @@ const actions = computed(() => {
<p v-if="text" class="hero-text">
{{ text }}
</p>
<div v-if="actions" class="actions">
<div v-if="actions.length" class="actions">
<div v-for="action in actions" :key="action.link" class="action">
<VButton
tag="a"
@ -71,17 +66,29 @@ const actions = computed(() => {
</template>
<style scoped>
.plume-home {
.home-banner {
position: relative;
width: 100%;
min-height: calc(100vh - var(--vp-nav-height));
filter: var(--vp-home-hero-image-filter);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
transition: all var(--t-color);
}
.plume-home .container {
.home-banner .banner-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
transition: opacity var(--t-color);
}
.home-banner .container {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: flex-start;
@ -90,19 +97,19 @@ const actions = computed(() => {
margin: 0 auto;
}
.plume-home .content {
.home-banner .content {
width: 100%;
padding: 0 2rem;
}
.plume-home .content .hero-name {
.home-banner .content .hero-name {
font-size: 72px;
font-weight: 600;
line-height: 1;
color: var(--vp-c-text-hero-name);
}
.plume-home .content .hero-tagline {
.home-banner .content .hero-tagline {
display: flex;
align-items: center;
margin-top: 1rem;
@ -112,7 +119,7 @@ const actions = computed(() => {
color: var(--vp-c-text-hero-tagline);
}
.plume-home .content .hero-tagline .line {
.home-banner .content .hero-tagline .line {
display: inline-block;
width: 80px;
height: 0;
@ -120,7 +127,7 @@ const actions = computed(() => {
border-top: solid 1px var(--vp-c-text-hero-tagline);
}
.plume-home .content .hero-text {
.home-banner .content .hero-text {
width: 100%;
max-width: 700px;
margin-top: 1.5rem;
@ -135,23 +142,23 @@ const actions = computed(() => {
}
@media (min-width: 960px) {
.plume-home .container {
.home-banner .container {
max-width: 768px;
padding-top: 8rem;
}
.plume-home .content .hero-name {
.home-banner .content .hero-name {
font-size: 100px;
}
}
@media (min-width: 1440px) {
.plume-home .container {
.home-banner .container {
max-width: 1104px;
padding-top: 8rem;
}
.plume-home .content .hero-tagline {
.home-banner .content .hero-tagline {
font-size: 32px;
}
}

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { Content, withBase } from 'vuepress/client'
import { computed } from 'vue'
import { isLinkHttp } from 'vuepress/shared'
import type { PlumeThemeHomeCustom } from '../../../shared/index.js'
import { useDarkMode } from '../../composables/index.js'
const props = defineProps<PlumeThemeHomeCustom & { onlyOnce?: boolean }>()
const isDark = useDarkMode()
const styles = computed(() => {
if (!props.backgroundImage)
return null
const image = typeof props.backgroundImage === 'string' ? props.backgroundImage : (props.backgroundImage[isDark.value ? 'dark' : 'light'] ?? props.backgroundImage.light)
const link = isLinkHttp(image) ? props.backgroundImage : withBase(image)
return {
'background-image': `url(${link})`,
'background-size': 'cover',
'background-position': 'center',
'background-repeat': 'no-repeat',
'background-attachment': props.backgroundAttachment || '',
}
})
</script>
<template>
<div class="home-custom" :style="styles">
<div class="container">
<Content class="plume-content" />
</div>
</div>
</template>
<style scoped>
.home-custom {
position: relative;
padding: 24px;
}
@media (min-width: 640px) {
.home-custom {
padding: 32px 48px;
}
}
@media (min-width: 960px) {
.home-custom {
padding: 48px;
}
}
.container {
max-width: 1152px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,115 @@
<script setup lang="ts">
import type { PlumeThemeHomeFeature } from '../../../shared/index.js'
import AutoLink from '../AutoLink.vue'
import VImage from '../VImage.vue'
defineProps<PlumeThemeHomeFeature>()
</script>
<template>
<AutoLink
class="home-feature"
:href="link"
:rel="rel"
:target="target"
:no-icon="true"
:tag="link ? 'a' : 'div'"
>
<article class="box">
<div v-if="typeof icon === 'object' && icon.wrap" class="icon">
<VImage
:image="icon"
:alt="icon.alt"
:height="icon.height || 48"
:width="icon.width || 48"
/>
</div>
<VImage
v-else-if="typeof icon === 'object'"
:image="icon"
:alt="icon.alt"
:height="icon.height || 48"
:width="icon.width || 48"
/>
<div v-else-if="icon" class="icon" v-html="icon" />
<h2 class="title" v-html="title" />
<p v-if="details" class="details" v-html="details" />
<div v-if="linkText" class="link-text">
<p class="link-text-value">
{{ linkText }} <span class="vpi-arrow-right link-text-icon" />
</p>
</div>
</article>
</AutoLink>
</template>
<style scoped>
.home-feature {
display: block;
height: 100%;
background-color: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-bg-soft);
border-radius: 12px;
transition: border-color var(--t-color), background-color var(--t-color);
}
.home-feature.link:hover {
border-color: var(--vp-c-brand-1);
}
.box {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
}
.box > :deep(.VPImage) {
margin-bottom: 20px;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
margin-bottom: 20px;
font-size: 24px;
background-color: var(--vp-c-default-soft);
border-radius: 6px;
transition: background-color var(--t-color);
}
.title {
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.details {
flex-grow: 1;
padding-top: 8px;
font-size: 14px;
font-weight: 500;
line-height: 24px;
color: var(--vp-c-text-2);
}
.link-text {
padding-top: 8px;
}
.link-text-value {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
.link-text-icon {
margin-left: 6px;
}
</style>

View File

@ -0,0 +1,152 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { PlumeThemeHomeFeatures } from '../../../shared/index.js'
import HomeFeature from './HomeFeature.vue'
const props = defineProps<{
onlyOnce?: boolean
} & PlumeThemeHomeFeatures>()
const grid = computed(() => {
const length = props.features?.length
if (!length)
return undefined
else if (length === 2)
return 'grid-2'
else if (length === 3)
return 'grid-3'
else if (length % 3 === 0)
return 'grid-6'
else if (length > 3)
return 'grid-4'
return undefined
})
</script>
<template>
<div v-if="features" class="home-features">
<div class="container">
<h2 v-if="title" class="title" v-html="title" />
<p v-if="description" class="description" v-html="description" />
<div class="items">
<div
v-for="feature in features"
:key="feature.title"
class="item"
:class="[grid]"
>
<HomeFeature
:icon="feature.icon"
:title="feature.title"
:details="feature.details"
:link="feature.link"
:link-text="feature.linkText"
:rel="feature.rel"
:target="feature.target"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.home-features {
position: relative;
padding: 24px;
}
@media (min-width: 640px) {
.home-features {
padding: 24px 48px 48px;
}
}
@media (min-width: 960px) {
.home-features {
padding: 48px 64px 64px;
}
}
.container {
max-width: 1152px;
margin: 0 auto;
}
.container .title {
margin-bottom: 20px;
font-size: 20px;
font-weight: 900;
color: var(--vp-c-text-1);
text-align: center;
transition: color var(--t-color);
}
.container .description {
margin-bottom: 20px;
font-size: 16px;
line-height: 1.7;
color: var(--vp-c-text-1);
text-align: center;
transition: color var(--t-color);
}
@media (min-width: 768px) {
.container .title {
font-size: 24px;
}
.container .description {
font-size: 18px;
}
}
@media (min-width: 960px) {
.container .title {
font-size: 28px;
}
}
.items {
display: flex;
flex-wrap: wrap;
margin: -8px;
}
.item {
width: 100%;
padding: 8px;
}
@media (min-width: 640px) {
.item.grid-2,
.item.grid-4,
.item.grid-6 {
width: calc(100% / 2);
}
}
@media (min-width: 768px) {
.item.grid-2,
.item.grid-4 {
width: calc(100% / 2);
}
.item.grid-3,
.item.grid-6 {
width: calc(100% / 3);
}
}
@media (min-width: 960px) {
.item.grid-4 {
width: calc(100% / 4);
}
}
</style>

View File

@ -0,0 +1,229 @@
<script setup lang="ts">
import { usePageFrontmatter, withBase } from 'vuepress/client'
import { isLinkHttp } from 'vuepress/shared'
import { computed } from 'vue'
import VButton from '../VButton.vue'
import type { PlumeThemeHomeFrontmatter, PlumeThemeHomeHero } from '../../../shared/index.js'
const props = defineProps<PlumeThemeHomeHero & { onlyOnce: boolean }>()
const matter = usePageFrontmatter<PlumeThemeHomeFrontmatter>()
const background = computed(() => {
const background = props.background !== 'filter' ? props.background : ''
const link = background ? isLinkHttp(background) ? background : withBase(background) : ''
return link
? { 'background-image': `url(${link})` }
: null
})
const hero = computed(() => props.hero ?? matter.value.hero ?? {})
const actions = computed(() => hero.value.actions ?? [])
</script>
<template>
<div class="home-hero" :class="{ full: props.full, once: props.onlyOnce }">
<div v-if="background" class="home-hero-bg" :style="background" />
<div v-if="props.background === 'filter'" class="bg-filter">
<div class="g g-1" />
<div class="g g-2" />
<div class="g g-3" />
</div>
<div class="container">
<div class="content">
<h1 v-if="hero.name" class="hero-name" v-html="hero.name" />
<p v-if="hero.tagline" class="hero-tagline" v-html="hero.tagline" />
<p v-if="hero.text" class="hero-text" v-html="hero.text" />
<div v-if="actions.length" class="actions">
<div class="action">
<VButton
v-for="action in actions"
:key="action.link"
tag="a"
size="medium"
:theme="action.theme"
:text="action.text"
:href="action.link"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.home-hero {
position: relative;
width: 100%;
}
.home-hero.full {
height: calc(100vh - var(--vp-nav-height));
}
.home-hero.full.once {
height: calc(100vh - var(--vp-nav-height) - var(--vp-footer-height, 0px));
}
.container {
position: relative;
z-index: 1;
display: flex;
width: 100%;
height: 100%;
}
.home-hero.full .container {
align-items: center;
justify-content: center;
}
.home-hero:not(.full) .container {
padding-top: 80px;
padding-bottom: 80px;
}
.content {
max-width: 960px;
padding: 0 20px;
margin: 0 auto;
text-align: center;
}
.home-hero.full .container .content {
margin-top: -40px;
}
.hero-name,
.hero-tagline {
font-size: 48px;
font-weight: 900;
line-height: 1.25;
letter-spacing: -0.5px;
}
.hero-name {
background: linear-gradient(315deg, var(--vp-c-purple-1) 15%, var(--vp-c-brand-2) 65%, var(--vp-c-brand-2) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-tagline {
color: var(--vp-c-text-2);
transition: color var(--t-color);
}
.hero-text {
margin: 18px 0 30px;
font-size: 18px;
font-weight: 500;
color: var(--vp-c-text-3);
white-space: pre-wrap;
transition: color var(--t-color);
}
.actions {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
margin: 30px 0 0;
}
.action :deep(.VPButton) {
margin-right: 24px;
}
.action :deep(.VPButton:last-of-type) {
margin-right: 0;
}
/* =========== background filter begin ======= */
.bg-filter {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
}
.home-hero.full.once .bg-filter {
height: calc(100% + var(--vp-footer-height, 0px));
}
.bg-filter::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
content: "";
backdrop-filter: blur(150px);
}
.g {
position: absolute;
opacity: 0.5;
}
.g-1 {
bottom: 100px;
left: 50%;
width: 714px;
height: 390px;
clip-path: polygon(0 10%, 30% 0, 100% 40%, 70% 100%, 20% 90%);
background: #fe5;
transform: translate(-50%, 0);
}
.g-2 {
bottom: 0;
left: 30%;
width: 1000px;
height: 450px;
clip-path: polygon(10% 0, 100% 70%, 100% 100%, 20% 90%);
background: #e950d1;
transform: translate(-50%, 0);
}
.g-3 {
bottom: 0;
left: 70%;
width: 1000px;
height: 450px;
clip-path: polygon(80% 0, 100% 70%, 100% 100%, 20% 90%);
background: rgba(87, 80, 233);
transform: translate(-50%, 0);
}
/* =========== background filter end ======= */
@media (min-width: 768px) {
.hero-name,
.hero-tagline {
font-size: 64px;
}
.hero-text {
font-size: 20px;
}
}
@media (min-width: 960px) {
.hero-name,
.hero-tagline {
font-size: 72px;
}
.hero-text {
font-size: 24px;
}
}
</style>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { PlumeThemeHomeProfile } from '../../../shared/index.js'
import VImage from '../VImage.vue'
import { useThemeLocaleData } from '../../composables/index.js'
const props = defineProps<PlumeThemeHomeProfile & { onlyOnce?: boolean }>()
const theme = useThemeLocaleData()
const avatar = computed(() => theme.value.avatar)
const profile = computed(() => {
return {
name: props.name || avatar.value?.name,
description: props.description || avatar.value?.description,
avatar: props.avatar || avatar.value?.url,
circle: props.circle || avatar.value?.circle,
}
})
</script>
<template>
<div class="home-profile">
<div class="container">
<VImage v-if="profile.avatar" :image="profile.avatar" :class="{ circle: profile.circle }" />
<h3 v-if="profile.name">
{{ profile.name }}
</h3>
<p v-if="profile.description">
{{ profile.description }}
</p>
</div>
</div>
</template>
<style scoped>
.home-profile {
position: relative;
padding: 24px;
}
@media (min-width: 640px) {
.home-profile {
padding: 32px 48px;
}
}
@media (min-width: 960px) {
.home-profile {
padding: 48px 64px;
}
}
.container {
max-width: 1152px;
margin: 0 auto;
overflow: hidden;
}
.container :deep(img) {
float: left;
width: 64px;
margin-right: 24px;
}
.container :deep(img.circle) {
border-radius: 50%;
}
@media (min-width: 960px) {
.container :deep(img) {
width: 96px;
}
}
.container :deep(h3) {
margin-bottom: 12px;
font-size: 20px;
font-weight: 500;
}
.container :deep(p) {
font-size: 16px;
font-weight: 400;
line-height: 1.5;
color: var(--vp-c-text-2);
white-space: pre-wrap;
transition: color var(--t-color);
}
</style>

View File

@ -0,0 +1,170 @@
<script setup lang="ts">
import { computed } from 'vue'
import { isLinkHttp } from 'vuepress/shared'
import { withBase } from 'vuepress/client'
import type { PlumeThemeHomeTextImage } from '../../../shared/index.js'
import VImage from '../VImage.vue'
import { useDarkMode } from '../../composables/index.js'
const props = defineProps<PlumeThemeHomeTextImage & { onlyOnce?: boolean }>()
const isDark = useDarkMode()
const maxWidth = computed(() => {
const width = props.width
if (typeof width === 'number')
return `${width}px`
return width
})
const styles = computed(() => {
if (!props.backgroundImage)
return null
const image = typeof props.backgroundImage === 'string' ? props.backgroundImage : (props.backgroundImage[isDark.value ? 'dark' : 'light'] ?? props.backgroundImage.light)
const link = isLinkHttp(image) ? props.backgroundImage : withBase(image)
return {
'background-image': `url(${link})`,
'background-size': 'cover',
'background-position': 'center',
'background-repeat': 'no-repeat',
'background-attachment': props.backgroundAttachment || '',
}
})
</script>
<template>
<div class="home-text-image" :style="styles">
<div class="container" :class="{ reverse: type === 'text-image' }">
<div class="content-image">
<VImage :image="image" :style="{ maxWidth }" />
</div>
<div class="content-text plume-content">
<section>
<h2 v-if="title" class="title">
{{ title }}
</h2>
<p v-if="description" class="description" v-html="description" />
<ul v-if="list && list.length" class="list">
<li v-for="(item, index) in list" :key="index">
<template v-if="typeof item === 'object'">
<h3 v-if="item.title" v-html="item.title" />
<p v-if="item.description" v-html="item.description" />
</template>
<p v-else v-html="item" />
</li>
</ul>
</section>
</div>
</div>
</div>
</template>
<style scoped>
.home-text-image {
position: relative;
padding: 24px;
}
@media (min-width: 640px) {
.home-text-image {
padding: 32px 48px;
}
}
@media (min-width: 960px) {
.home-text-image {
padding: 48px 64px;
}
}
.container {
display: flex;
flex-direction: column;
gap: 24px;
align-items: center;
justify-content: space-around;
max-width: 1152px;
margin: 0 auto;
}
@media (min-width: 960px) {
.container {
flex-direction: row;
}
.container.reverse {
flex-direction: row-reverse;
}
}
.content-image :deep(.plume-image) {
width: 100%;
max-width: 128px;
margin: 0 auto;
}
.content-text h2 {
margin-top: 0;
}
.content-text ul {
margin-left: -20px;
}
.content-text .description {
font-size: 16px;
font-weight: 500;
color: var(--vp-c-text-1);
transition: color var(--t-color);
}
.content-text ul h3 {
margin: 0;
font-size: 16px;
color: var(--vp-c-text-1);
transition: color var(--t-color);
}
.content-text ul p {
margin: 0;
color: var(--vp-c-text-1);
transition: color var(--t-color);
}
.content-text ul li :only-child {
font-weight: 500;
}
@media (min-width: 768px) {
.content-image :deep(.plume-image) {
max-width: 160px;
margin: 0 48px;
}
.content-text {
flex: 1;
width: 100%;
}
}
@media (min-width: 960px) {
.container {
gap: 48px;
}
.content-image :deep(.plume-image) {
max-width: 180px;
margin: 0 96px;
}
.container .content-text {
display: flex;
justify-content: center;
max-width: 80%;
}
}
</style>

View File

@ -4,9 +4,9 @@ import { useWindowScroll } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import type {
PlumeThemePageData,
} from '../../shared/index.js'
import { useSidebar, useThemeLocaleData } from '../composables/index.js'
import IconAlignLeft from './icons/IconAlignLeft.vue'
} from '../../../shared/index.js'
import { useSidebar, useThemeLocaleData } from '../../composables/index.js'
import IconAlignLeft from '../icons/IconAlignLeft.vue'
import LocalNavOutlineDropdown from './LocalNavOutlineDropdown.vue'
const props = defineProps<{

View File

@ -2,9 +2,9 @@
import type { PageHeader } from 'vuepress/client'
import { onClickOutside } from '@vueuse/core'
import { nextTick, ref, watch } from 'vue'
import { useThemeLocaleData } from '../composables/index.js'
import { useThemeLocaleData } from '../../composables/index.js'
import IconChevronRight from '../icons/IconChevronRight.vue'
import DocOutlineItem from './DocOutlineItem.vue'
import IconChevronRight from './icons/IconChevronRight.vue'
const props = defineProps<{
headers: PageHeader[]

View File

@ -43,7 +43,6 @@ onMounted(() => {
position: relative;
z-index: var(--vp-z-index-footer);
padding: 24px;
background-color: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-gutter);
transition: all var(--t-color);
}
@ -71,6 +70,10 @@ onMounted(() => {
.plume-footer {
padding: 24px;
}
.plume-footer.has-sidebar {
margin-right: calc(0px - ((100vw - var(--vp-layout-max-width)) / 2));
}
}
.container {

View File

@ -3,11 +3,11 @@ import { usePageData, useRoute } from 'vuepress/client'
import { computed, provide, watch } from 'vue'
import type { PlumeThemePageData } from '../../shared/index.js'
import Backdrop from '../components/Backdrop.vue'
import Blog from '../components/Blog.vue'
import Blog from '../components/Blog/Blog.vue'
import Friends from '../components/Friends.vue'
import Home from '../components/Home.vue'
import Home from '../components/Home/Home.vue'
import LayoutContent from '../components/LayoutContent.vue'
import LocalNav from '../components/LocalNav.vue'
import LocalNav from '../components/Nav/LocalNav.vue'
import Nav from '../components/Nav/index.vue'
import Page from '../components/Page.vue'
import Sidebar from '../components/Sidebar.vue'

View File

@ -30,7 +30,7 @@
line-height: 32px;
letter-spacing: -0.02em;
border-top: 1px solid var(--vp-c-divider);
transition: border-top var(--t-color);
transition: border-top var(--t-color), color var(--t-color);
}
.plume-content h2:first-of-type {

View File

@ -316,6 +316,7 @@
:root {
--vp-layout-max-width: 1440px;
--content-width: var(--vp-layout-max-width);
}
/**

View File

@ -49,7 +49,7 @@ const defaultLocales: NonNullable<PlumeThemeLocaleOptions['locales']> = {
export const fallbackLocaleOption: Partial<PlumeThemeLocaleOptions> = {
article: '/article/',
notes: { link: '/note', dir: 'notes', notes: [] },
notes: { link: '/', dir: 'notes', notes: [] },
appearance: true,
// page meta
editLink: true,

View File

@ -1,15 +1,18 @@
import type { NavItemWithLink } from '.'
import type { NavItemWithLink, PlumeThemeImage } from '.'
export interface PlumeThemeHomeFrontmatter {
/* =============================== Home begin ==================================== */
export interface PlumeThemeHomeFrontmatter extends Omit<PlumeThemeHomeBanner, 'type'> {
home?: true
banner?: string
bannerMask?: number | { light?: number, dark?: number }
hero: {
name: string
tagline?: string
text?: string
actions: PlumeThemeHeroAction[]
}
config?: PlumeThemeHomeConfig[]
}
export type PlumeThemeHomeConfig = PlumeThemeHomeBanner | PlumeThemeHomeTextImage | PlumeThemeHomeFeatures | PlumeThemeHomeProfile
export interface PlumeThemeHero {
name: string
tagline?: string
text?: string
actions: PlumeThemeHeroAction[]
}
export interface PlumeThemeHeroAction {
@ -18,6 +21,83 @@ export interface PlumeThemeHeroAction {
link?: string
}
export interface PlumeHomeConfigBase {
type: 'banner' | 'hero' | 'text-image' | 'image-text' | 'features' | 'profile' | 'custom'
}
export interface PlumeThemeHomeBanner extends PlumeHomeConfigBase {
type: 'banner'
banner?: string
bannerMask?: number | { light?: number, dark?: number }
hero: PlumeThemeHero
}
export interface PlumeThemeHomeHero extends PlumeHomeConfigBase {
type: 'hero'
hero: PlumeThemeHero
full?: boolean
background?: 'filter' | (string & { zz_IGNORE?: never })
}
export interface PlumeThemeHomeTextImage extends PlumeHomeConfigBase {
type: 'text-image' | 'image-text'
image: PlumeThemeImage
width?: number | string
title?: string
description?: string
list: (string | { title?: string, description?: string })[]
backgroundImage?: string | { light: string, dark: string }
backgroundAttachment?: 'fixed' | 'local'
}
export interface PlumeThemeHomeFeatures extends PlumeHomeConfigBase {
type: 'features'
title?: string
description?: string
features: PlumeThemeHomeFeature[]
}
export interface PlumeThemeHomeFeature {
icon?: FeatureIcon
title: string
details?: string
link?: string
linkText?: string
rel?: string
target?: string
}
export type FeatureIcon = string | {
src: string
alt?: string | undefined
width?: string | undefined
height?: string | undefined
wrap?: boolean | undefined
} | {
light: string
dark: string
alt?: string | undefined
width?: string | undefined
height?: string | undefined
wrap?: boolean | undefined
}
export interface PlumeThemeHomeProfile extends PlumeHomeConfigBase {
type: 'profile'
name?: string
description?: string
avatar?: PlumeThemeImage
circle?: boolean
}
export interface PlumeThemeHomeCustom extends PlumeHomeConfigBase {
type: 'custom'
backgroundImage?: string | { light: string, dark: string }
backgroundAttachment?: 'fixed' | 'local'
}
/* =============================== Home end ==================================== */
export interface PlumeThemePageFrontmatter {
comments?: boolean
editLink?: boolean