commit
2be308b262
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -1,7 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
|
||||
@ -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: 前端开发工程师, 热爱前端, 热爱生活, 热爱互联网, 热爱技术, 热爱开源, 热爱生命。
|
||||
---
|
||||
|
||||
这里是自定义的内容,你可以随意添加你自己的内容
|
||||
|
||||
@ -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()
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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()
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import AutoLink from './AutoLink.vue'
|
||||
import AutoLink from '../AutoLink.vue'
|
||||
|
||||
defineProps<{
|
||||
postList: {
|
||||
@ -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()
|
||||
108
theme/src/client/components/EncryptForm.vue
Normal file
108
theme/src/client/components/EncryptForm.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
81
theme/src/client/components/Home/Home.vue
Normal file
81
theme/src/client/components/Home/Home.vue
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
59
theme/src/client/components/Home/HomeCustom.vue
Normal file
59
theme/src/client/components/Home/HomeCustom.vue
Normal 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>
|
||||
115
theme/src/client/components/Home/HomeFeature.vue
Normal file
115
theme/src/client/components/Home/HomeFeature.vue
Normal 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>
|
||||
152
theme/src/client/components/Home/HomeFeatures.vue
Normal file
152
theme/src/client/components/Home/HomeFeatures.vue
Normal 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>
|
||||
229
theme/src/client/components/Home/HomeHero.vue
Normal file
229
theme/src/client/components/Home/HomeHero.vue
Normal 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>
|
||||
91
theme/src/client/components/Home/HomeProfile.vue
Normal file
91
theme/src/client/components/Home/HomeProfile.vue
Normal 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>
|
||||
170
theme/src/client/components/Home/HomeTextImage.vue
Normal file
170
theme/src/client/components/Home/HomeTextImage.vue
Normal 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>
|
||||
@ -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<{
|
||||
@ -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[]
|
||||
@ -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 {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -316,6 +316,7 @@
|
||||
|
||||
:root {
|
||||
--vp-layout-max-width: 1440px;
|
||||
--content-width: var(--vp-layout-max-width);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user