Files
BA-VitePress-Pages/.vitepress/theme/components/Posts-List.vue
2025-11-26 15:17:27 +08:00

613 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="container posts-content">
<TransitionGroup class="posts-list" name="list" tag="div">
<article class="post" v-for="post in postsList" :key="post.href">
<span v-if="post.pinned" class="pinned"></span>
<header class="post-header">
<div v-if="post.cover" class="cover-container">
<img
:src="post.cover"
class="cover-image"
:alt="post.title + '-cover'"
loading="lazy"
/>
</div>
<div class="header-content">
<div class="title">
<div class="title-dot" v-if="!post.cover"></div>
<h1 class="name">
<a :href="base + post.href">{{ post.title }}</a>
</h1>
</div>
<div class="meta-info-bar">
<span class="iconfont icon-time time"></span>
<div class="time-info">
<time datetime="">{{ formatDate(post.create) }}</time>
</div>
<div class="wordcount seperator">{{ post.wordCount }}</div>
</div>
<ul class="tags">
<li v-for="tag in post.tags">
<a :href="`${base}tags/`" @click="state.currTag = tag"
><i class="iconfont icon-tag"></i> {{ tag }}</a
>
</li>
</ul>
<div class="excerpt">
<p>{{ post.excerpt }}</p>
</div>
</div>
</header>
</article>
</TransitionGroup>
<div v-if="totalPage != 1" class="pagination">
<button
:disabled="currPage === 1"
:class="{ hide: currPage === 1 }"
id="up"
@click="goToPage(currPage - 1)"
>
<i class="iconfont icon-arrow"></i>
</button>
<div class="page-numbers">
<!-- 第一页 -->
<button class="page-number" :class="{ active: currPage === 1 }" @click="goToPage(1)">
1
</button>
<!-- 页码省略号 -->
<span v-if="showLeftEllipsis" class="ellipsis">...</span>
<!-- 当前页码 -->
<button
v-for="page in visiblePageNumbers"
:key="page"
class="page-number"
:class="{ active: currPage === page }"
@click="goToPage(page)"
>
{{ page }}
</button>
<!-- 页码省略号 -->
<span v-if="showRightEllipsis" class="ellipsis">...</span>
<!-- 尾页 -->
<button
v-if="totalPage > 1"
class="page-number"
:class="{ active: currPage === totalPage }"
@click="goToPage(totalPage)"
>
{{ totalPage }}
</button>
</div>
<button
:disabled="currPage >= totalPage"
:class="{ hide: currPage >= totalPage }"
id="next"
@click="goToPage(currPage + 1)"
>
<i class="iconfont icon-arrow"></i>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useData } from 'vitepress'
import { ref, computed, onMounted, watch } from 'vue'
import { data as posts } from '../utils/posts.data'
import { useStore } from '../store'
const { state } = useStore()
const { page } = useData()
const base = useData().site.value.base
// 日期格式化
function formatDate(timestamp: number): string {
const date = new Date(timestamp)
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date)
}
// 文章传值
const finalPosts = computed(() => {
if (page.value.filePath === 'index.md') {
return posts
} else if (page.value.filePath === 'tags/index.md') {
return state.selectedPosts
}
return []
})
// 文章列表长度
const pageSize = ref(5)
// 使用store中的currPage
const currPage = computed({
get: () => state.currPage,
set: (value) => (state.currPage = value),
})
onMounted(() => {
// 获取URL信息
updatePageFromUrl()
// 监听前进后退
window.addEventListener('popstate', () => {
updatePageFromUrl()
})
})
function updatePageFromUrl() {
const urlParams = new URLSearchParams(window.location.search)
const pageParam = urlParams.get('page')
if (
pageParam &&
!isNaN(parseInt(pageParam)) &&
parseInt(pageParam) > 0 &&
parseInt(pageParam) <= totalPage.value
) {
state.currPage = parseInt(pageParam)
} else {
state.currPage = 1
}
}
// 更新页码逻辑
function goToPage(page: number) {
if (page < 1 || page > totalPage.value) return
state.currPage = page
// 获取URL信息
const url = new URL(window.location.href)
// 非首页时获取URL页码
if (page > 1) {
url.searchParams.set('page', page.toString())
} else {
url.searchParams.delete('page')
}
// Tag页面页码逻辑
const tagParam = url.searchParams.get('tag')
if (tagParam) {
url.searchParams.set('tag', tagParam)
}
window.history.pushState({}, '', url.toString())
}
// 计算要显示的页码
const maxVisiblePages = 3 // 省略号两边显示的页码按钮数量
const visiblePageNumbers = computed(() => {
if (totalPage.value <= 7)
return Array.from({ length: totalPage.value - 2 }, (_, i) => i + 2).filter(
(p) => p > 1 && p < totalPage.value,
)
let startPage = Math.max(2, currPage.value - Math.floor(maxVisiblePages / 2))
let endPage = Math.min(totalPage.value - 1, startPage + maxVisiblePages - 1)
if (endPage - startPage < maxVisiblePages - 1) {
startPage = Math.max(2, endPage - maxVisiblePages + 1)
}
return Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
})
// 省略号显示逻辑
const showLeftEllipsis = computed(() => {
return totalPage.value > 7 && visiblePageNumbers.value[0] > 2
})
const showRightEllipsis = computed(() => {
return (
totalPage.value > 7 &&
visiblePageNumbers.value[visiblePageNumbers.value.length - 1] < totalPage.value - 1
)
})
const postsList = computed(() => {
return finalPosts.value.slice(
(currPage.value - 1) * pageSize.value,
currPage.value * pageSize.value,
)
})
const totalPage = computed(() => {
return Math.ceil(finalPosts.value.length / pageSize.value) || 1
})
// 监听文章列表
watch(
() => state.selectedPosts,
() => {
// 标签页逻辑获取URL页码
const urlParams = new URLSearchParams(window.location.search)
const pageParam = urlParams.get('page')
// 标签更改时重置页码
const newTotalPages = Math.ceil(state.selectedPosts.length / pageSize.value) || 1
if (!pageParam || currPage.value > newTotalPages) {
currPage.value = 1
// 更新URL
if (pageParam) {
const url = new URL(window.location.href)
url.searchParams.delete('page')
window.history.pushState({}, '', url.toString())
}
}
},
)
</script>
<style scoped lang="less">
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.5s;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
}
.list-leave-active {
position: absolute;
right: 0;
left: 0;
}
.posts-content {
article,
h1,
ul {
margin: 0;
padding: 0;
}
}
.posts-list {
position: relative;
overflow-wrap: break-word;
.post {
display: flex;
flex-direction: column;
margin: 0 0 50px 0;
padding-bottom: 16px;
background-color: var(--foreground-color);
border-radius: 32px;
border-left: solid 16px var(--pot-border-left);
background-image: var(--deco1);
background-size: contain;
background-position: right;
background-repeat: no-repeat;
box-shadow: 0px 0px 8px rgb(var(--blue-shadow-color), 0.8);
transition: all 0.5s;
.pinned {
position: absolute;
width: 42px;
height: 42px;
top: -8px;
right: -8px;
border-radius: 50px;
background: var(--icon-pinned) no-repeat;
background-size: contain;
box-shadow: 0 0 6px rgba(var(--blue-shadow-color), 0.65);
}
.post-header {
display: flex;
gap: 24px;
padding: 32px 40px 0;
// flex-direction: row-reverse;
position: relative;
align-items: stretch;
.cover-container {
flex: 0 0 180px;
height: 140px;
border-radius: 12px;
overflow: hidden;
position: relative;
margin-left: -8px;
margin-bottom: 15px;
align-self: center;
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
}
}
.header-content {
flex: 1;
min-width: 0;
flex-direction: column;
.title {
position: relative;
margin-bottom: 8px;
}
.excerpt {
flex: 1;
display: flex;
align-items: flex-end;
}
}
}
@media (max-width: 768px) {
.post-header {
flex-direction: column;
gap: 16px;
padding: 24px 20px 0;
.cover-container {
flex: none;
width: 100%;
height: 240px;
margin-left: 0;
}
}
}
}
}
.post-header {
padding: 32px 40px 0;
.title {
position: relative;
margin-bottom: 8px;
.title-dot {
width: 4px;
height: 20px;
position: absolute;
left: -16px;
top: 9.5px;
background: var(--pot-border-left);
border-radius: 2px;
transition: background 0.5s;
}
.name {
display: flex;
align-items: center;
gap: 15px;
}
a {
color: var(--font-color-grey);
transition: text-shadow 0.5s, color 0.5s;
&:hover {
text-shadow: 0 0 3px var(--font-color-grey);
}
}
}
.meta-info-bar {
display: flex;
margin-bottom: 7px;
opacity: 0.75;
.time {
font-size: 13px;
color: var(--font-color-grey);
margin: 3px 2px 0 0;
font-weight: bold;
}
.seperator::before {
content: '';
display: inline-block;
border-radius: 50%;
height: 4px;
width: 4px;
vertical-align: middle;
background-color: var(--font-color-grey);
margin: 0 16px;
}
}
}
.tags {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 0;
margin-bottom: 6px;
li {
display: flex;
justify-content: center;
align-items: center;
padding-top: 6px;
margin-right: 16px;
a {
color: var(--font-color-grey);
padding: 3px 5px;
color: var(--font-color-gold);
background-color: var(--btn-background);
border-radius: 5px;
transition: all 0.5s;
&:hover {
background-color: var(--btn-hover);
color: var(--font-color-gold);
}
}
}
}
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 50px;
padding: 0;
button {
background-color: transparent;
border-style: none;
cursor: pointer;
}
.hide {
opacity: 0;
cursor: auto;
}
.icon-arrow {
font-size: 36px;
color: var(--icon-color);
}
#up {
animation: arrow-pre 1s ease-in-out infinite alternate;
}
#next {
animation: arrow-next 1s ease-in-out infinite alternate;
}
.page-numbers {
display: flex;
align-items: center;
gap: 8px;
}
.page-number {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
font-size: 16px;
border-radius: 6px;
color: var(--icon-color);
transition: all 0.3s;
&:hover {
background-color: var(--btn-hover);
color: var(--font-color-gold);
}
&.active {
background-color: var(--btn-hover);
color: var(--font-color-gold);
font-weight: bold;
}
}
.ellipsis {
margin: 0 4px;
color: var(--icon-color);
}
}
@keyframes arrow-pre {
from {
transform: translateX(0) rotate(-0.25turn);
}
to {
transform: translateX(10px) rotate(-0.25turn);
}
}
@keyframes arrow-next {
from {
transform: translateX(0) rotate(0.25turn);
}
to {
transform: translateX(-10px) rotate(0.25turn);
}
}
@media (max-width: 768px) {
.posts-list {
.post {
margin: 0 8px 30px 8px;
background-size: cover;
border-left: solid 1.5vh var(--pot-border-left);
.pinned {
width: 27px;
height: 27px;
top: -2px;
right: 12px;
}
}
}
.post-header {
padding: 20px 35px 0;
.name {
font-size: 24px;
}
.title {
margin-bottom: 6px;
.title-dot {
height: 18px;
top: 6px;
}
}
.meta-info-bar {
margin-bottom: 4px;
font-size: 12px;
.time {
font-size: 8px !important;
margin: 3px 2px 0 0 !important;
}
.seperator::before {
margin: 0 8px;
}
}
}
.tags {
li {
padding-top: 4px;
margin-right: 8px;
a {
font-size: 12px;
padding: 4px 6px;
.icon-tag {
font-size: 12px;
}
}
}
}
.excerpt {
padding: 0;
margin-bottom: 4px;
font-size: 12px;
}
.pagination {
margin-top: 32px;
.icon-arrow {
font-size: 32px;
}
.page-number {
width: 28px;
height: 28px;
font-size: 14px;
}
.page-numbers {
gap: 4px;
}
}
}
</style>