feat(theme): 更新主题样式

1. 新增首页首屏banner;2. 首页首屏个人信息;3. 新增文章列表banner;4. 更新backToTOP 新增首页大图banner   首页首屏首屏个人信息
文章列表新增文章列表banner 更新backToTop按钮样式;
This commit is contained in:
pengzhanbo 2022-05-07 02:23:27 +08:00
parent c330ced690
commit 335d273d2c
23 changed files with 339 additions and 45 deletions

View File

@ -6,7 +6,7 @@ const packages = fs.readdirSync(path.resolve(__dirname, 'packages'))
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-enum': [2, 'always', [...packages]],
'scope-enum': [2, 'always', ['docs', ...packages]],
'footer-max-line-length': [0],
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@ -7,8 +7,7 @@ tags:
- html
- css
- develop
top: false
type: null
banner: /images/big-banner.jpg
---
在日常移动端前端应用开发中,经常遇到一个问题就是 1px的线在移动端 Retina屏下的渲染并未达到预期。以下总几种不同场景下的 1px解决方案。

View File

@ -1,5 +1,5 @@
---
home: true
# banner: https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F2019-08-20%2F5d5bb3ec573e4.jpg&refer=http%3A%2F%2Fpic1.win4000.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651329706&t=5dac9f133df18a9cdcef5003e33f0b03
# mobileBanner: https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg9.51tietu.net%2Fpic%2F2019-091215%2Fg1s4d0voiqog1s4d0voiqo.jpg&refer=http%3A%2F%2Fimg9.51tietu.net&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1651329706&t=b0bc39d9dc448a3e77c6f5f0b544f516
banner: /images/big-banner.jpg
motto: 世间的美好总是不期而遇,恬静而自然。
---

View File

@ -1,9 +1,12 @@
<script lang="ts" setup>
import { usePageFrontmatter } from '@vuepress/client'
import { debounce } from 'ts-debounce'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { getScrollTop, scrollTo } from '../utils'
import { BackTopIcon } from './icons'
const frontmatter = usePageFrontmatter()
const opacity = ref<number>(0)
const MAX_TOP = 300
@ -11,8 +14,20 @@ const canShow = debounce((): void => {
opacity.value = getScrollTop(document) >= MAX_TOP ? 1 : 0
})
const top = computed(() => {
if (__VUEPRESS_SSR__) return 0
if (
frontmatter.value.home &&
(frontmatter.value.banner || frontmatter.value.mobileBanner)
) {
return document.documentElement.clientHeight - 58
} else {
return 0
}
})
const scrollToTop = (): void => {
scrollTo(document, 0)
scrollTo(document, top.value)
}
onMounted(() => {
@ -30,7 +45,7 @@ onMounted(() => {
.btn-back-top {
position: fixed;
right: 3rem;
bottom: 4.35rem;
bottom: 2.1rem;
width: 3rem;
height: 3rem;
text-align: center;

View File

@ -3,13 +3,16 @@ import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import { isLinkHttp, isLinkMailto } from '@vuepress/shared'
import type { FunctionalComponent, Ref } from 'vue'
import { ref } from 'vue'
import { useThemeLocaleData } from '../composables'
import { usePostStat, useThemeLocaleData } from '../composables'
import {
EmailIcon,
FacebookIcon,
FolderIcon,
GithubIcon,
LinkedinIcon,
PostIcon,
QQIcon,
TagIcon,
TwitterIcon,
WeiBoIcon,
ZhiHuIcon,
@ -64,6 +67,10 @@ const useSocialList = (): SocialRef => {
return list
}
const socialList = useSocialList()
const postStat = usePostStat()
console.log(postStat)
</script>
<template>
<DropdownTransition>
@ -83,6 +90,20 @@ const socialList = useSocialList()
<Component :is="item.icon" />
</a>
</p>
<div class="post-stat">
<div class="post-stat-item">
<PostIcon />
<span>{{ postStat.postTotal }}</span>
</div>
<div class="post-stat-item">
<FolderIcon />
<span>{{ postStat.categoryTotal }}</span>
</div>
<div class="post-stat-item">
<TagIcon />
<span>{{ postStat.tagTotal }}</span>
</div>
</div>
</section>
</DropdownTransition>
</template>
@ -131,5 +152,30 @@ const socialList = useSocialList()
height: 24px;
}
}
.post-stat {
display: flex;
justify-content: space-around;
align-items: center;
border-top: 1px solid var(--c-border);
margin-top: 0.75rem;
padding-top: 1rem;
.post-stat-item {
text-align: center;
color: var(--c-text-quote);
.icon {
width: 32px;
height: 32px;
color: var(--c-text-lightest);
}
span {
display: inline-block;
width: 100%;
font-size: 20px;
font-weight: 500;
}
}
}
}
</style>

View File

@ -2,15 +2,19 @@
import { usePageFrontmatter, withBase } from '@vuepress/client'
import { isLinkHttp } from '@vuepress/shared'
import { computed, onMounted, ref } from 'vue'
import type { PlumeThemeHomeFrontmatter } from '../../shared'
import type {
PlumeThemeHomeFrontmatter,
PlumeThemeLocaleOptions,
} from '../../shared'
import { useThemeLocaleData } from '../composables'
import { scrollTo } from '../utils'
import { ArrowBottomIcon } from './icons'
const frontmatter = usePageFrontmatter<PlumeThemeHomeFrontmatter>()
const MOBILE_WIDTH = 716
const bannerImg = ref(frontmatter.value.banner || '')
const hasBanner = computed(
() => !!(frontmatter.value.banner || frontmatter.value.mobileBanner)
)
const hasBanner = computed(() => !!bannerImg.value)
const bannerStyle = computed(() => {
if (!hasBanner.value) return ''
const url = isLinkHttp(bannerImg.value)
@ -37,21 +41,112 @@ onMounted(() => {
window.addEventListener('resize', handleResize, false)
window.addEventListener('orientationchange', handleResize, false)
})
let screenHeight = 0
const arrowHandle = (): void => {
if (!screenHeight) {
screenHeight =
document.documentElement.clientHeight || document.body.clientHeight
screenHeight -=
document.querySelector<HTMLElement>('.navbar-wrapper')?.offsetHeight || 0
}
scrollTo(document, screenHeight)
}
const themeLocale = useThemeLocaleData()
const avatar = themeLocale.value.avatar || {}
</script>
<template>
<div
v-if="hasBanner"
class="home-big-banner-wrapper"
:style="bannerStyle"
></div>
<div v-if="hasBanner" class="home-big-banner-wrapper" :style="bannerStyle">
<ArrowBottomIcon @click="arrowHandle" />
<div class="home-blogger-info">
<div class="blogger-img">
<img :src="avatar.url" :alt="avatar.name" />
</div>
<h3>{{ avatar.name }}</h3>
<p v-if="frontmatter.motto" class="blogger-motto">
{{ frontmatter.motto }}
</p>
</div>
</div>
</template>
<style lang="scss">
.home-big-banner-wrapper {
position: relative;
display: flex;
width: 100%;
height: calc(100vh - var(--navbar-height));
background-color: transparent;
background-position: 0 0;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
.arrow-bottom-icon {
position: absolute;
bottom: 1.25rem;
left: 50%;
width: 48px;
height: 48px;
color: var(--c-home-arrow-bottom);
animation: home-banner-arrow 1.5s ease 0.3s infinite;
cursor: pointer;
}
.home-blogger-info {
margin: auto;
text-align: center;
.blogger-img {
width: 240px;
height: 240px;
border-radius: 50%;
overflow: hidden;
padding: 1.25rem;
background-color: rgba(0, 0, 0, 0.25);
margin: auto;
img {
width: 100%;
border-radius: 50%;
}
}
h3 {
display: inline-block;
font-size: 64px;
max-width: var(--content-width);
color: rgba(255, 255, 255, 0.85);
padding: 0 1.25rem;
margin: 1rem 0;
}
.blogger-motto {
max-width: var(--content-width);
font-size: 32px;
color: rgba(255, 255, 255, 0.75);
padding: 0 1.25rem;
border-radius: var(--p-around);
}
}
}
@keyframes home-banner-arrow {
0% {
opacity: 0;
transform: translateY(-10px);
}
10% {
opacity: 0.45;
}
95% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0.25;
transform: translateY(-7px);
}
}
</style>

View File

@ -3,11 +3,11 @@ import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import PostMeta from '@theme-plume/PostMeta.vue'
import Sidebar from '@theme-plume/Sidebar.vue'
import { usePageData } from '@vuepress/client'
import { computed, nextTick, onUnmounted, watchEffect } from 'vue'
import { computed } from 'vue'
import type { PlumeThemePageData } from '../../shared'
import { useDarkMode, useThemeLocaleData } from '../composables'
import { getCssValue } from '../utils'
import Toc from './Toc'
const page = usePageData<PlumeThemePageData>()
const themeLocale = useThemeLocaleData()
const isDarkMode = useDarkMode()
@ -29,7 +29,7 @@ const enabledSidebar = computed(() => {
<Sidebar v-if="enabledSidebar" />
<div class="page-content">
<h1>{{ page.title }}</h1>
<PostMeta :post="page" type="post" :border="true" />
<PostMeta :post="page" type="post" border />
<Content />
<div class="comment-container">
<Comment :darkmode="isDarkMode" />

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import DropdownTransition from '@theme-plume/DropdownTransition.vue'
import type { PropType } from 'vue'
import { useRouter } from 'vue-router'
import type { PostItem } from '../../shared'
import AutoLink from './AutoLink.vue'
import { TopIcon } from './icons'
@ -16,19 +17,42 @@ defineProps({
default: 0,
},
})
const router = useRouter()
const handlePost = (path: string): void => {
router.push({ path })
}
</script>
<template>
<DropdownTransition :delay="index * 0.04">
<section :key="post.path" class="post-list-item">
<TopIcon v-if="post.sticky" />
<h3>
<AutoLink :item="{ text: post.title, link: post.path }" />
</h3>
<PostMeta :post="post" />
<!--eslint-disable vue/no-v-html-->
<div v-if="post.excerpt" class="post-excerpt" v-html="post.excerpt"></div>
<div v-if="post.excerpt" class="post-more">
<AutoLink :item="{ text: '阅读全文 >>', link: post.path }" />
<div>
<TopIcon v-if="post.sticky" />
<div
v-if="post.banner"
class="post-banner"
@click="handlePost(post.path)"
>
<div
:style="{
'background-image': `url(${post.banner})`,
}"
></div>
</div>
<h3>
<AutoLink :item="{ text: post.title, link: post.path }" />
</h3>
<PostMeta :post="post" />
<!--eslint-disable vue/no-v-html-->
<div
v-if="post.excerpt"
class="post-excerpt"
v-html="post.excerpt"
></div>
<div v-if="post.excerpt" class="post-more">
<AutoLink :item="{ text: '阅读全文', link: post.path }" />
</div>
</div>
</section>
</DropdownTransition>

View File

@ -81,12 +81,20 @@ const togglePage = (currentPage: number): void => {
flex: 1;
.post-list-item {
position: relative;
padding: 1.25rem 1.5rem;
background-color: var(--c-bg-container);
border-radius: var(--p-around);
margin-bottom: 1.25rem;
box-shadow: var(--shadow);
> div {
position: relative;
padding: 1.25rem 1.5rem;
background-color: var(--c-bg-container);
border-radius: var(--p-around);
margin-bottom: 1.25rem;
box-shadow: var(--shadow);
transition: box-shadow var(--t-color);
overflow: hidden;
&:hover {
box-shadow: var(--shadow-lg);
}
}
.top-icon {
position: absolute;
@ -98,6 +106,40 @@ const togglePage = (currentPage: number): void => {
}
}
.post-banner {
position: relative;
height: 300px;
margin: -1.25rem -1.5rem 1.25rem -1.5rem;
overflow: hidden;
cursor: pointer;
> div {
width: 100%;
height: 100%;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
transform: scale(100%);
transition: transform var(--t-transform);
&:hover {
transform: scale(120%);
}
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 1.5rem;
width: 0;
height: 0;
border: solid 20px;
border-color: transparent transparent var(--c-bg-container) transparent;
z-index: 1;
}
}
h3 {
width: 100%;
margin-top: 0;
@ -122,6 +164,14 @@ const togglePage = (currentPage: number): void => {
.post-more {
text-align: right;
a {
display: inline-block;
padding: 0.5rem 0.75rem;
border-radius: var(--p-around);
background-color: var(--c-bg);
color: var(--c-brand);
}
}
}

View File

@ -24,7 +24,11 @@ watchEffect(() => {
</script>
<template>
<aside class="plume-theme-sidebar-wrapper">
<SidebarItems class="aside-navbar" :sidebar-list="aside" />
<SidebarItems
v-if="aside.length"
class="aside-navbar"
:sidebar-list="aside"
/>
<SidebarItems :sidebar-list="sidebarList" />
</aside>
</template>

View File

@ -65,10 +65,26 @@ export const ArrowRightIcon: FunctionalComponent = () =>
)
ArrowRightIcon.displayName = 'ArrowRightIcon'
export const ArrowBottomIcon: FunctionalComponent = () =>
h(IconBase, { name: 'arrow-bottom', viewBox: '0 0 1024 1024' }, () =>
h('path', {
d: 'M150.001 502.111a22.487 22.487 0 0 1 13.185 4.245l348.86 250.152 348.858-250.152a22.577 22.577 0 0 1 26.28 36.665L525.14 802.656a22.577 22.577 0 0 1-26.28 0L136.816 543.02a22.577 22.577 0 0 1 13.185-40.91z m737.183-257.196L525.14 504.55a22.577 22.577 0 0 1-26.28 0L136.816 244.915a22.577 22.577 0 1 1 26.28-36.665l348.859 250.152L860.814 208.25a22.577 22.577 0 1 1 26.28 36.665z',
})
)
ArrowBottomIcon.displayName = 'ArrowBottomIcon'
export const BackTopIcon: FunctionalComponent = () =>
h(IconBase, { name: 'back-top', viewBox: '0 0 1024 1024' }, () =>
h('path', {
d: 'M832 64H192c-17.6 0-32 14.4-32 32s14.4 32 32 32h640c17.6 0 32-14.4 32-32s-14.4-32-32-32zM852.484 519.469L538.592 205.577a30.79 30.79 0 0 0-3.693-4.476c-6.241-6.241-14.556-9.258-22.899-9.09-8.343-0.168-16.658 2.849-22.899 9.09a30.778 30.778 0 0 0-3.693 4.476L171.419 519.566C164.449 525.448 160 534.228 160 544c0 0.058 0.004 0.115 0.004 0.172-0.124 8.285 2.899 16.529 9.096 22.727 6.202 6.202 14.453 9.224 22.743 9.096 0.066 0 0.131 0.005 0.197 0.005H352v320c0 35.2 28.8 64 64 64h192c35.2 0 64-28.8 64-64V576h160c0.058 0 0.115-0.004 0.172-0.004 8.285 0.124 16.529-2.899 22.727-9.096 6.198-6.198 9.22-14.442 9.096-22.727 0-0.058 0.004-0.115 0.004-0.172 0.001-9.826-4.489-18.65-11.515-24.532z',
d: 'M725.902 498.916c18.205-251.45-93.298-410.738-205.369-475.592l-6.257-3.982-6.258 3.414c-111.502 64.853-224.711 224.142-204.8 475.59-55.751 53.476-80.214 116.623-80.214 204.8v15.36l179.2-35.27c11.378 40.39 58.596 69.973 113.21 69.973 54.613 0 101.262-29.582 112.64-68.836l180.337 36.41v-15.36c-0.569-89.885-25.031-153.6-82.489-206.507zM571.733 392.533c-33.564 31.29-87.04 28.445-118.329-5.12s-28.444-87.04 5.12-117.76c33.565-31.289 87.04-28.444 118.33 5.12s28.444 86.471-5.12 117.76z m-56.32 368.64c-35.84 0-64.284 29.014-64.284 64.285 0 35.84 54.044 182.613 64.284 182.613s64.285-146.773 64.285-182.613c0-35.271-29.014-64.285-64.285-64.285z',
})
)
BackTopIcon.displayName = 'BackTopIcon'
export const PostIcon: FunctionalComponent = () =>
h(IconBase, { name: 'post', viewBox: '0 0 1024 1024' }, () =>
h('path', {
d: 'M805.376 81.0496 188.7232 81.0496c-52.6336 0-94.8736 42.3936-94.8736 94.6176l0 664.576c0 52.2752 42.496 94.6176 94.8736 94.6176L805.376 934.8608c52.6336 0 94.8736-42.3936 94.8736-94.6176L900.2496 175.7184C900.2496 123.392 857.8048 81.0496 805.376 81.0496zM288.768 204.8c39.3216 0 71.168 31.5904 71.168 71.168 0 39.3216-31.5904 71.168-71.168 71.168-39.3216 0-71.168-31.5904-71.168-71.168C217.6 236.6464 249.1904 204.8 288.768 204.8zM506.368 741.0176 217.6 741.0176l0-47.4112L506.368 693.6064 506.368 741.0176zM671.3344 617.2672 217.6 617.2672 217.6 569.856l453.7344 0L671.3344 617.2672zM671.3344 493.568 217.6 493.568 217.6 446.1056l453.7344 0L671.3344 493.568z',
})
)
PostIcon.displayName = 'PostIcon'

View File

@ -9,6 +9,7 @@ export * from './sidebarIndex'
export * from './postList'
export * from './scrollPromise'
export * from './asideNavbar'
export * from './postStat'
export * from './tag'
export * from './category'

View File

@ -17,6 +17,9 @@ export const usePostIndex = (): PostIndexRef => {
return ref(postList)
}
export type PostTotalRef = Ref<number>
export const postTotal: PostTotalRef = ref(0)
if (import.meta.hot) {
__VUE_HMR_RUNTIME__.updatePostIndex = (data: PostIndex) => {
postIndex.value = data

View File

@ -0,0 +1,31 @@
import { reactive } from 'vue'
import { usePostAllIndex } from './postIndex'
import { useTagList } from './tag'
export interface PostStatData {
postTotal: number
tagTotal: number
categoryTotal: number
}
export const usePostStat = (): PostStatData => {
const data: PostStatData = Object.create(null)
const postIndex = usePostAllIndex()
const tagList = useTagList()
data.postTotal = postIndex.value.length
data.tagTotal = tagList.value.length
const categorySet = new Set()
postIndex.value.forEach((post) => {
const category = post.category || []
category.forEach((cate) => categorySet.add(cate.name))
})
data.categoryTotal = categorySet.size
const stat = reactive<PostStatData>(data)
return stat
}

View File

@ -5,7 +5,8 @@ html.dark {
--c-bg: #22272e;
--c-bg-light: #2b313a;
--c-bg-lighter: #262c34;
--c-bg-container: #262c34;
--c-bg-container: rgb(38, 44, 52);
--c-bg-navbar: rgba(38, 44, 52, 0.95);
--c-text: #adbac7;
--c-text-light: #96a7b7;
@ -27,6 +28,8 @@ html.dark {
--c-details-bg: #323843;
--c-hl-bg-color: #363b46;
--c-home-arrow-bottom: rgba(196, 205, 216, 0.75);
}
html.dark .DocSearch {

View File

@ -8,7 +8,7 @@
--c-bg-light: #e5e7eb;
--c-bg-lighter: #d1d5db;
--c-bg-container: #fff;
--c-bg-navbar: var(--c-bg-container);
--c-bg-navbar: rgba(255, 255, 255, 0.9);
--c-bg-sidebar: var(--c-bg-container);
--c-bg-arrow: #ccc;
@ -47,6 +47,8 @@
--c-badge-warning: var(--c-warning);
--c-badge-danger: var(--c-danger);
--c-home-arrow-bottom: rgba(255, 255, 255, 0.75);
// transition
--t-color: 0.3s ease;
--t-transform: 0.3s ease;
@ -85,7 +87,7 @@
--content-width: 740px;
// search box vars
--search-bg-color: var(--c-bg-container);
--search-bg-color: transparent;
--search-accent-color: var(--c-text-accent);
--search-text-color: var(--c-text);
--search-border-color: var(--c-border);

View File

@ -61,6 +61,7 @@ export const preparedPostIndex = (
article: frontmatter.article,
category: page.data.category,
isNote: page.data.isNote,
banner: frontmatter.banner,
} as PostItem
})
postIndex = [

View File

@ -14,4 +14,5 @@ export interface PlumeThemeHomeFrontmatter extends PlumeThemeNormalFrontmatter {
banner?: string
mobileBanner?: string
productList?: PlumeThemeProductList
motto?: string
}

View File

@ -5,4 +5,6 @@ export interface PlumeThemePostFrontmatter {
tags?: string[]
sticky?: boolean | number
article?: boolean
banner?: string
bgBanner?: string
}

View File

@ -92,21 +92,21 @@ export interface PlumeThemeLocaleData extends LocaleData {
/**
*
*
* /article/
* @default /article/
*/
article?: string
/**
* navbar配置
*
* { text: '标签', link: '/tag/' }
* @def{ text: '标签', link: '/tag/' }
*/
tag?: false | NavLink
/**
* navbar配置
*
* { text: '分类', link: '/category/ }
* @default { text: '分类', link: '/category/ }
*/
category?: false | NavLink
@ -115,7 +115,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
*
* ( timeline timeline )
*
* { text: '归档', link: '/timeline/' }
* @default { text: '归档', link: '/timeline/' }
*/
archive?: false | NavLink

View File

@ -16,6 +16,7 @@ export interface PostItem {
article?: boolean
category: CategoryData
isNote?: boolean
banner?: string
}
export type PostIndex = PostItem[]