perf: 改版博客文章列表页UI,贴合设计趋势

This commit is contained in:
pengzhanbo 2024-02-02 10:36:44 +08:00
parent 8d56e989d4
commit 8a969abb27
19 changed files with 291 additions and 86 deletions

View File

@ -77,8 +77,8 @@ function handleClick() {
.back-to-top-button {
position: fixed;
inset-inline-end: 1rem;
right: 20px;
bottom: 64px;
right: 24px;
bottom: calc(var(--vp-footer-height, 82px) - 18px);
z-index: var(--vp-z-index-back-to-top);
width: 36px;
height: 36px;
@ -140,6 +140,7 @@ function handleClick() {
@media (min-width: 768px) {
.back-to-top-button {
bottom: calc(var(--vp-footer-height, 88px) - 24px);
width: 48px;
height: 48px;
}

View File

@ -12,16 +12,22 @@ const page = usePageData<PlumeThemePageData>()
<template>
<div class="blog-wrapper">
<PostList v-if="page.type === 'blog'" />
<Tags v-if="page.type === 'blog-tags'" />
<Archives v-if="page.type === 'blog-archives'" />
<BlogAside />
<BlogExtract />
<div class="blog-container">
<PostList v-if="page.type === 'blog'" />
<Tags v-if="page.type === 'blog-tags'" />
<Archives v-if="page.type === 'blog-archives'" />
<BlogAside />
<BlogExtract />
</div>
</div>
</template>
<style scoped>
.blog-wrapper {
min-height: calc(100vh - var(--vp-footer-height, 0px));
}
.blog-container {
display: flex;
align-items: flex-start;
justify-content: flex-start;
@ -30,15 +36,31 @@ const page = usePageData<PlumeThemePageData>()
margin: 0 auto;
}
@media (min-width: 768px) {
.blog-wrapper {
min-height: calc(100vh + var(--vp-nav-height) - var(--vp-footer-height, 0px));
}
.blog-wrapper {
padding-top: var(--vp-nav-height);
margin-top: calc(var(--vp-nav-height) * -1);
background-color: var(--vp-c-bg-alt);
}
}
@media (min-width: 960px) {
.blog-wrapper {
min-height: calc(100vh - var(--vp-footer-height, 0px));
}
.blog-container {
max-width: 784px;
padding-top: 0;
}
}
@media (min-width: 1440px) {
.blog-wrapper {
.blog-container {
max-width: 1104px;
}
}

View File

@ -1,11 +1,14 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useBlogExtract, useThemeLocaleData } 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 theme = useThemeLocaleData()
const route = useRoute()
const avatar = computed(() => theme.value.avatar)
const { hasBlogExtract, tags, archives } = useBlogExtract()
@ -14,7 +17,7 @@ const { hasBlogExtract, tags, archives } = useBlogExtract()
<template>
<div v-if="avatar" class="blog-aside-wrapper">
<div class="avatar-profile">
<p v-if="avatar.url">
<p v-if="avatar.url" :class="{ circle: avatar.circle }">
<img :src="avatar.url" :alt="avatar.name">
</p>
<div>
@ -23,13 +26,25 @@ const { hasBlogExtract, tags, archives } = useBlogExtract()
</div>
</div>
<div v-if="hasBlogExtract" class="blog-nav">
<AutoLink class="nav-link" :href="tags.link">
<IconTag class="icon" />
<span>{{ tags.text }}</span>
<AutoLink
class="nav-link"
:class="{ active: route.path === tags.link }"
:href="tags.link"
>
<IconTag class="icon icon-logo" />
<span class="text">{{ tags.text }}</span>
<span class="total">{{ tags.total }}</span>
<IconChevronRight class="icon" />
</AutoLink>
<AutoLink class="nav-link" :href="archives.link">
<IconArchive class="icon" />
<span>{{ archives.text }}</span>
<AutoLink
class="nav-link"
:class="{ active: route.path === archives.link }"
:href="archives.link"
>
<IconArchive class="icon icon-logo" />
<span class="text">{{ archives.text }}</span>
<span class="total">{{ archives.total }}</span>
<IconChevronRight class="icon" />
</AutoLink>
</div>
</div>
@ -41,17 +56,15 @@ const { hasBlogExtract, tags, archives } = useBlogExtract()
top: calc(var(--vp-nav-height) + 2rem);
display: none;
width: 270px;
padding: 1rem 0;
margin-top: 2rem;
margin-bottom: 12rem;
margin-left: 2rem;
margin: 2rem 1rem 0 2rem;
text-align: center;
border-left: solid 1px var(--vp-c-divider);
}
.blog-aside-wrapper img {
width: 50%;
width: 60%;
margin: auto;
object-fit: cover;
}
.blog-aside-wrapper h3 {
@ -60,39 +73,74 @@ const { hasBlogExtract, tags, archives } = useBlogExtract()
font-weight: 600;
}
@media (min-width: 768px) {
.blog-aside-wrapper {
display: block;
}
.avatar-profile {
padding: 24px 20px;
margin-bottom: 24px;
background-color: var(--vp-c-bg);
border-radius: 8px;
box-shadow: var(--vp-shadow-2);
}
.avatar-profile .circle img {
overflow: hidden;
border-radius: 50%;
}
.blog-nav {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
padding: 10px 12px 0;
margin: 24px 24px 0;
border-top: solid 1px var(--vp-c-divider);
padding: 0;
text-align: left;
}
.nav-link {
display: flex;
align-items: center;
justify-content: center;
padding: 3px;
font-weight: 600;
color: var(--vp-c-brand-1);
justify-content: flex-start;
padding: 10px 14px 10px 20px;
margin-bottom: 20px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
background-color: var(--vp-c-bg);
border-radius: 8px;
transition: all var(--t-color);
box-shadow: var(--vp-shadow-2);
transition: color var(--t-color);
}
.nav-link:hover {
color: var(--vp-c-brand-2);
.nav-link:hover,
.nav-link.active {
color: var(--vp-c-brand-1);
}
.nav-link .text {
flex: 1;
min-width: 0;
padding-right: 14px;
}
.nav-link .total {
padding-right: 8px;
color: var(--vp-c-text-3);
}
.nav-link .icon {
width: 1em;
height: 1em;
margin-right: 4px;
font-size: 1.2em;
color: var(--vp-c-text-3);
}
.nav-link .icon-logo {
margin-right: 10px;
color: var(--vp-c-brand-1);
}
@media (min-width: 768px) {
.blog-aside-wrapper {
margin: 2rem 1rem 2rem 1.25rem;
}
.blog-aside-wrapper {
display: block;
}
}
</style>

View File

@ -129,7 +129,7 @@ export default {
display: flex;
align-items: center;
height: var(--vp-nav-height);
padding: 0 12px;
padding: 0 10px;
color: var(--vp-c-text-1);
transition: color 0.5s;
}

View File

@ -45,11 +45,18 @@ const list = computed(() => matter.value.list || [])
<style scoped>
.friends-wrapper {
width: 100%;
min-height: calc(100vh - var(--vp-footer-height, 0px));
padding-top: var(--vp-nav-height);
padding-bottom: 5rem;
margin: 0 auto;
}
@media (min-width: 960px) {
.friends-wrapper {
min-height: calc(100vh - var(--vp-nav-height) - var(--vp-footer-height, 0px));
}
}
.friends-wrapper .title {
padding-top: 3rem;
padding-left: 1rem;

View File

@ -32,7 +32,7 @@ const page = usePageData()
.navbar-menu-link {
display: flex;
align-items: center;
padding: 0 12px;
padding: 0 10px;
font-size: 14px;
font-weight: 500;
line-height: var(--vp-nav-height);

View File

@ -5,7 +5,7 @@ import type {
PlumeThemePageData,
PlumeThemePostFrontmatter,
} from '../../shared/index.js'
import { useReadingTime } from '../composables/index.js'
import { useExtraBlogData, useReadingTime } from '../composables/index.js'
import IconBooks from './icons/IconBooks.vue'
import IconClock from './icons/IconClock.vue'
import IconTag from './icons/IconTag.vue'
@ -13,6 +13,7 @@ import IconTag from './icons/IconTag.vue'
const page = usePageData<PlumeThemePageData>()
const matter = usePageFrontmatter<PlumeThemePostFrontmatter>()
const readingTime = useReadingTime()
const extraData = useExtraBlogData()
const createTime = computed(() => {
if (matter.value.createTime)
@ -26,8 +27,12 @@ const categoryList = computed(() => {
})
const tags = computed(() => {
if (matter.value.tags)
return matter.value.tags.slice(0, 4)
if (matter.value.tags) {
return matter.value.tags.slice(0, 4).map(tag => ({
name: tag,
colors: extraData.value.tagsColorsPreset[extraData.value.tagsColors[tag]],
}))
}
return []
})
@ -59,9 +64,13 @@ const hasMeta = computed(() => readingTime.value.times || tags.value.length || c
</p>
<p v-if="tags.length > 0">
<IconTag class="icon" />
<span v-for="(tag, index) in tags" :key="tag" class="tag">
{{ tag }}
<template v-if="index < tags.length - 1">,</template>
<span
v-for="tag in tags"
:key="tag.name"
class="tag"
:style="{ '--vp-tag-color': tag.colors[0], '--vp-tag-bg-color': tag.colors[2] }"
>
{{ tag.name }}
</span>
</p>
<p v-if="createTime" class="create-time">
@ -129,11 +138,12 @@ const hasMeta = computed(() => readingTime.value.times || tags.value.length || c
.page-meta-wrapper .tag {
display: inline-block;
padding: 3px;
padding: 3px 5px;
margin-right: 6px;
line-height: 1;
color: var(--vp-c-text-2);
background-color: var(--vp-c-mute);
border-radius: 4px;
color: var(--vp-tag-color);
background-color: var(--vp-tag-bg-color);
border-radius: 3px;
}
.page-meta-wrapper .tag:last-of-type {

View File

@ -1,6 +1,7 @@
<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'
@ -10,12 +11,19 @@ const props = defineProps<{
post: PlumeThemeBlogPostItem
}>()
const extraData = useExtraBlogData()
const categoryList = computed(() =>
props.post.categoryList ?? [],
)
const tags = computed(() =>
(props.post.tags ?? []).slice(0, 4),
(props.post.tags ?? [])
.slice(0, 4)
.map(tag => ({
name: tag,
colors: extraData.value.tagsColorsPreset[extraData.value.tagsColors[tag]],
})),
)
const createTime = computed(() =>
@ -46,9 +54,13 @@ const createTime = computed(() =>
</div>
<div v-if="tags.length" class="tag-list">
<IconTag class="icon" />
<template v-for="(tag, i) in tags" :key="tag">
<span class="tag">{{ tag }}</span>
<span v-if="i !== tags.length - 1">,</span>
<template v-for="tag in tags" :key="tag.name">
<span
class="tag"
:style="{ '--vp-tag-color': tag.colors[0], '--vp-tag-bg-color': tag.colors[2] }"
>
{{ tag.name }}
</span>
</template>
</div>
<div v-if="createTime" class="create-time">
@ -99,7 +111,21 @@ const createTime = computed(() =>
color: var(--vp-c-text-2);
}
@media (min-width: 768px) {
.post-item {
padding: 24px 20px;
margin: 0 0 24px 20px;
background-color: var(--vp-c-bg);
border-radius: 8px;
box-shadow: var(--vp-shadow-2);
}
}
@media (min-width: 960px) {
.post-item {
margin-left: 0;
}
.post-item h3 {
font-size: 20px;
}
@ -133,7 +159,17 @@ const createTime = computed(() =>
}
.post-meta .tag-list .tag {
margin: 0 0.2rem;
display: inline-block;
padding: 3px 5px;
margin-right: 6px;
line-height: 1;
color: var(--vp-tag-color);
background-color: var(--vp-tag-bg-color);
border-radius: 3px;
}
.post-meta .tag-list .tag:last-of-type {
margin-right: 0;
}
.post-meta .icon {

View File

@ -65,6 +65,7 @@ const {
padding: 0 4px;
font-weight: 500;
color: var(--vp-c-brand-1);
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-brand-1);
border-radius: 4px;
transition: all var(--t-color);

View File

@ -1,14 +1,25 @@
<script setup lang="ts">
import { useCssVar } from '@vueuse/core'
import { onMounted, ref } from 'vue'
import { useSidebar, useThemeLocaleData } from '../composables/index.js'
import { inBrowser } from '../utils/index.js'
const theme = useThemeLocaleData()
const { hasSidebar } = useSidebar()
const footerHeight = useCssVar('--vp-footer-height', inBrowser ? document.body : null)
const footer = ref<HTMLElement | null>(null)
onMounted(() => {
if (theme.value.footer && footer.value)
footerHeight.value = `${footer.value.offsetHeight}px`
})
</script>
<template>
<!-- eslint-disable vue/no-v-html -->
<footer
v-if="theme.footer"
ref="footer"
class="plume-footer"
:class="{ 'has-sidebar': hasSidebar }"
>
@ -37,10 +48,6 @@ const { hasSidebar } = useSidebar()
transition: all 0.25s;
}
.plume-footer.has-sidebar {
display: none;
}
.plume-footer :deep(a) {
text-decoration-line: underline;
text-underline-offset: 2px;

View File

@ -7,6 +7,7 @@
>
<path
d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -1,9 +1,16 @@
import { usePageLang } from 'vuepress/client'
import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client'
import { computed } from 'vue'
import { useExtraBlogData as _useExtraBlogData, useBlogPostData } from '@vuepress-plume/plugin-blog-data/client'
import { type Ref, computed } from 'vue'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
import { useLocaleLink, useRouteQuery, useThemeLocaleData } from '../composables/index.js'
import { getRandomColor, toArray } from '../utils/index.js'
import { toArray } from '../utils/index.js'
export const useExtraBlogData = _useExtraBlogData as () => Ref<{
tagsColorsPreset: (readonly [string, string, string])[]
tagsColors: Record<string, number>
}>
const DEFAULT_PER_PAGE = 10
export function useLocalePostList() {
const locale = usePageLang()
@ -49,7 +56,7 @@ export function usePostListControl() {
const totalPage = computed(() => {
if (blog.value.pagination === false)
return 0
const perPage = blog.value.pagination?.perPage || 20
const perPage = blog.value.pagination?.perPage || DEFAULT_PER_PAGE
return Math.ceil(postList.value.length / perPage)
})
const isLastPage = computed(() => page.value >= totalPage.value)
@ -60,7 +67,7 @@ export function usePostListControl() {
if (blog.value.pagination === false)
return postList.value
const perPage = blog.value.pagination?.perPage || 20
const perPage = blog.value.pagination?.perPage || DEFAULT_PER_PAGE
if (postList.value.length <= perPage)
return postList.value
@ -96,6 +103,8 @@ const extractLocales: Record<string, { tags: string, archives: string }> = {
export function useBlogExtract() {
const theme = useThemeLocaleData()
const locale = usePageLang()
const postList = useLocalePostList()
const { tags: tagsList } = useTags()
const hasBlogExtract = computed(() => theme.value.blog?.archives !== false || theme.value.blog?.tags !== false)
const tagsLink = useLocaleLink('blog/tags/')
@ -104,11 +113,13 @@ export function useBlogExtract() {
const tags = computed(() => ({
link: tagsLink.value,
text: extractLocales[locale.value]?.tags || extractLocales.en.tags,
total: tagsList.value.length,
}))
const archives = computed(() => ({
link: archiveLink.value,
text: extractLocales[locale.value]?.archives || extractLocales.en.archives,
total: postList.value.length,
}))
return {
@ -122,6 +133,9 @@ export type ShortPostItem = Pick<PlumeThemeBlogPostItem, 'title' | 'path' | 'cre
export function useTags() {
const list = useLocalePostList()
const extraData = useExtraBlogData()
const tags = computed(() => {
const tagMap: Record<string, number> = {}
list.value.forEach((item) => {
@ -137,7 +151,7 @@ export function useTags() {
return Object.keys(tagMap).map(tag => ({
name: tag,
count: tagMap[tag] > 99 ? '99+' : tagMap[tag],
color: getRandomColor(),
colors: extraData.value.tagsColorsPreset[extraData.value.tagsColors[tag]],
}))
})

View File

@ -17,13 +17,13 @@ export const readingTimeLocales = {
'zh-CN': {
word: '约$word字',
less1Minute: '小于1分钟',
time: '约$time分钟',
time: '约$time分钟',
},
'zh-TW': {
word: '約$word字',
less1Minute: '小於1分鐘',
time: '约$time分鐘',
time: '约$time分鐘',
},
'de': {

View File

@ -273,7 +273,6 @@
"Liberation Mono",
"Courier New",
monospace;
--vp-header-anchor-symbol: "#";
}
/**

View File

@ -5,4 +5,3 @@ export * from './dom.js'
export * from './resolveEditLink.js'
export * from './resolveRepoType.js'
export * from './base.js'
export * from './color.js'

View File

@ -0,0 +1,40 @@
import { hasOwn, random, toArray } from '@pengzhanbo/utils'
export type BlogTagsColorsItem = readonly [
string, // normal color
string, // hover color
string, // background color
]
export const BLOG_TAGS_COLORS_PRESET: BlogTagsColorsItem[] = [
['#6aa1b7', '#5086a1', 'rgba(131, 208, 218, 0.314)'],
['#299764', '#18794e', 'rgba(16, 185, 129, 0.14)'],
['#946300', '#915930', 'rgba(234, 179, 8, 0.14)'],
['#d5393e', '#b8272c', 'rgba(244, 63, 94, 0.14)'],
['#7e4cc9', '#6f42c1', 'rgba(159, 122, 234, 0.14)'],
['#3a5ccc', '#3451b2', 'rgba(100, 108, 255, 0.14)'],
['#f1c40f', '#f39c12', 'rgba(255, 213, 0, 0.14)'],
['#cc6699', '#c75191', 'rgba(255, 153, 204, 0.14)'],
]
const len = BLOG_TAGS_COLORS_PRESET.length
let prevIndex: number[] = []
function getRandom() {
let index: number
do
index = random(0, len - 1)
while (prevIndex.includes(index))
prevIndex.push(index)
prevIndex = prevIndex.slice(-5)
return index
}
export function generateBlogTagsColors(map: Record<string, any>, tags?: string[]) {
if (!tags || tags.length === 0)
return
toArray(tags).forEach((tag) => {
if (!hasOwn(map, tag))
map[tag] = getRandom()
})
}

View File

@ -15,12 +15,12 @@ import { caniusePlugin } from '@vuepress-plume/plugin-caniuse'
import { copyCodePlugin } from '@vuepress-plume/plugin-copy-code'
import { iconifyPlugin } from '@vuepress-plume/plugin-iconify'
import { notesDataPlugin } from '@vuepress-plume/plugin-notes-data'
import { shikijiPlugin } from '@vuepress-plume/plugin-shikiji'
import { shikiPlugin } from '@vuepress-plume/plugin-shikiji'
import { commentPlugin } from 'vuepress-plugin-comment2'
import { type MarkdownEnhanceOptions, mdEnhancePlugin } from 'vuepress-plugin-md-enhance'
import { readingTimePlugin } from 'vuepress-plugin-reading-time2'
import { seoPlugin } from 'vuepress-plugin-seo2'
import { sitemapPlugin } from 'vuepress-plugin-sitemap2'
import { seoPlugin } from '@vuepress/plugin-seo'
import { sitemapPlugin } from '@vuepress/plugin-sitemap'
import { contentUpdatePlugin } from '@vuepress-plume/plugin-content-update'
import type {
PlumeThemeLocaleOptions,
@ -32,6 +32,7 @@ import { pathJoin } from './utils.js'
import { resolveNotesList } from './resolveNotesList.js'
import { resolvedDocsearchOption, resolvedSearchOptions } from './searchPluginOptions.js'
import { customContainers } from './container.js'
import { BLOG_TAGS_COLORS_PRESET, generateBlogTagsColors } from './blogTags.js'
export function setupPlugins(
app: App,
@ -68,13 +69,21 @@ export function setupPlugins(
pageFilter: (page: any) => page.frontmatter.article !== undefined
? !!page.frontmatter.article
: true,
extendBlogData: (page: any) => ({
categoryList: page.data.categoryList,
tags: page.frontmatter.tags,
sticky: page.frontmatter.sticky,
createTime: page.data.frontmatter.createTime,
lang: page.lang,
}),
extraBlogData(extra) {
extra.tagsColorsPreset = BLOG_TAGS_COLORS_PRESET
extra.tagsColors = {}
},
extendBlogData: (page: any, extra) => {
const tags = page.frontmatter.tags
generateBlogTagsColors(extra.tagsColors, tags)
return {
categoryList: page.data.categoryList,
tags,
sticky: page.frontmatter.sticky,
createTime: page.data.frontmatter.createTime,
lang: page.lang,
}
},
}),
notesDataPlugin(notesList),
@ -144,10 +153,11 @@ export function setupPlugins(
plugins.push(searchPlugin(resolvedSearchOptions(app, options.search)))
}
if (options.shikiji !== false) {
plugins.push(shikijiPlugin({
const shikiOption = options.shiki || options.shikiji
if (shikiOption !== false) {
plugins.push(shikiPlugin({
theme: { light: 'vitesse-light', dark: 'vitesse-dark' },
...(options.shikiji ?? {}),
...(shikiOption ?? {}),
}))
}

View File

@ -15,6 +15,10 @@ export interface PlumeThemeAvatar {
*
*/
description?: string
/**
*
*/
circle?: boolean
}
export interface SocialLink {

View File

@ -4,7 +4,7 @@ import type { AutoFrontmatterOptions } from '@vuepress-plume/plugin-auto-frontma
import type { BaiduTongjiOptions } from '@vuepress-plume/plugin-baidu-tongji'
import type { CanIUsePluginOptions } from '@vuepress-plume/plugin-caniuse'
import type { CopyCodeOptions } from '@vuepress-plume/plugin-copy-code'
import type { ShikijiPluginOptions } from '@vuepress-plume/plugin-shikiji'
import type { ShikiPluginOptions } from '@vuepress-plume/plugin-shikiji'
import type { CommentPluginOptions } from 'vuepress-plugin-comment2'
import type { MarkdownEnhanceOptions } from 'vuepress-plugin-md-enhance'
import type { ReadingTimeOptions } from 'vuepress-plugin-reading-time2'
@ -31,9 +31,15 @@ export interface PlumeThemePluginOptions {
docsearch?: false | DocsearchOptions
/**
* @deprecated move to `shiki`
*
*/
shikiji?: false | ShikijiPluginOptions
shikiji?: false | ShikiPluginOptions
/**
*
*/
shiki?: false | ShikiPluginOptions
/**
* git