613 lines
13 KiB
Vue
613 lines
13 KiB
Vue
<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>
|