blog / src /components /layout /PostPage.astro
cacode's picture
Upload 434 files
96dd062 verified
---
import type { CollectionEntry } from "astro:content";
import { getPostUrlBySlug } from "@utils/url-utils";
import PostCard from "@/components/layout/PostCard.astro";
import { sidebarLayoutConfig, siteConfig } from "@/config";
const { page } = Astro.props;
let delay = 0;
const interval = 50;
// 类型别名避免Fragment语法问题
type PostEntry = CollectionEntry<"posts">;
// 检查是否启用双侧边栏
const isBothSidebars = sidebarLayoutConfig.position === "both";
const masonryEnabled = siteConfig.postListLayout.grid.masonry;
const gridColumns = siteConfig.postListLayout.grid.columns || 2;
// 根据配置设置初始布局模式,避免闪烁
const defaultLayout = siteConfig.postListLayout.defaultMode || "list";
const gridCols =
!isBothSidebars && gridColumns === 3
? "md:grid-cols-2 lg:grid-cols-3"
: "md:grid-cols-2";
const initialLayoutClass =
defaultLayout === "grid"
? `grid grid-cols-1 ${gridCols} gap-4 grid-mode`
: "flex flex-col gap-4 md:gap-4 list-mode";
---
<div
id="post-list-container"
class={`transition-all duration-500 ease-in-out mb-4 ${initialLayoutClass}`}
data-default-layout={defaultLayout}
data-both-sidebars={isBothSidebars}
data-masonry-enabled={masonryEnabled}
data-grid-columns={gridColumns}
>
{
page.data.map((entry: PostEntry, index: number) => (
<PostCard
entry={entry}
title={entry.data.title}
tags={entry.data.tags}
category={entry.data.category}
published={entry.data.published}
updated={entry.data.updated}
url={getPostUrlBySlug(entry.id)}
image={entry.data.image}
description={entry.data.description}
draft={entry.data.draft}
pinned={entry.data.pinned}
loading={index < 2 ? "eager" : "lazy"}
class:list="onload-animation post-card-item"
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
/>
))
}
</div>
<!-- 立即执行脚本:防止刷新时的布局闪烁 -->
<script is:inline define:vars={{ defaultLayout, isBothSidebars, gridColumns }}>
(function() {
const savedLayout = localStorage.getItem('postListLayout');
// 如果有保存的布局且与默认布局不同,更新容器布局
if (savedLayout && savedLayout !== defaultLayout) {
const container = document.getElementById('post-list-container');
if (container) {
// 禁用过渡动画
container.style.transition = 'none';
// 移除所有布局类
container.classList.remove('list-mode', 'grid-mode', 'flex', 'flex-col', 'grid', 'grid-cols-1', 'md:grid-cols-2', 'lg:grid-cols-3', 'gap-4', 'md:gap-4');
if (savedLayout === 'grid') {
container.classList.add('grid-mode', 'grid', 'grid-cols-1', 'md:grid-cols-2', 'gap-4');
if (!isBothSidebars && gridColumns === 3) {
container.classList.add('lg:grid-cols-3');
}
} else {
container.classList.add('list-mode', 'flex', 'flex-col', 'gap-4', 'md:gap-4');
}
// 强制重排后恢复过渡动画
container.offsetHeight;
container.style.transition = '';
}
}
})();
</script>
<script>
// 动态布局切换脚本
function initLayout() {
const postListContainer = document.getElementById("post-list-container");
if (!postListContainer) return;
// 检查屏幕宽度
const screenWidth = window.innerWidth;
const isSmallScreen = screenWidth < 1200;
// 从localStorage读取用户偏好
const savedLayout = localStorage.getItem("postListLayout");
const defaultLayout =
postListContainer.getAttribute("data-default-layout") || "list";
let currentLayout = savedLayout || defaultLayout;
// 如果屏幕宽度小于1200px,强制使用列表模式
if (isSmallScreen) {
currentLayout = "list";
}
// 应用布局
updatePostListLayout(currentLayout);
}
function updatePostListLayout(layout: string) {
const postListContainer = document.getElementById("post-list-container");
if (!postListContainer) return;
// Check current layout state
const isCurrentGrid = postListContainer.classList.contains("grid-mode");
const isCurrentList = postListContainer.classList.contains("list-mode");
const currentLayout = isCurrentGrid ? "grid" : (isCurrentList ? "list" : null);
// Common logic vars
const isBothSidebars = postListContainer.getAttribute("data-both-sidebars") === "true";
const masonryEnabled = postListContainer.getAttribute("data-masonry-enabled") === "true";
const gridColumns = parseInt(postListContainer.getAttribute("data-grid-columns") || "2");
// Helper to apply layout classes (Pure logic, no animation)
const applyClasses = () => {
postListContainer.classList.remove("list-mode", "grid-mode");
if (layout === "grid") {
postListContainer.classList.add("grid-mode");
postListContainer.classList.remove("flex", "flex-col");
if (masonryEnabled) {
postListContainer.classList.remove("grid", "grid-cols-1", "md:grid-cols-2", "lg:grid-cols-3", "gap-4");
applyMasonryLayout();
} else {
postListContainer.classList.add("grid", "grid-cols-1", "md:grid-cols-2", "gap-4");
if (!isBothSidebars && gridColumns === 3) {
postListContainer.classList.add("lg:grid-cols-3");
}
resetMasonryLayout();
}
} else {
postListContainer.classList.add("list-mode");
postListContainer.classList.add("flex", "flex-col", "gap-4", "md:gap-4");
postListContainer.classList.remove("grid", "grid-cols-1", "md:grid-cols-2", "lg:grid-cols-3");
resetMasonryLayout();
}
};
// Case 1: First run (Initialization) - No animation
if (!currentLayout) {
applyClasses();
return;
}
// Case 2: Same layout (e.g. Resize) - No animation, just refresh masonry
if (currentLayout === layout) {
if (layout === "grid" && masonryEnabled) {
applyMasonryLayout();
}
return;
}
// Case 3: Layout Change - Smooth Animation
// 1. Fade OUT
postListContainer.classList.add("layout-switching");
// 2. Change Layout after fade out (200ms match CSS)
setTimeout(() => {
applyClasses();
// 3. Fade IN (Short delay to ensure DOM update)
requestAnimationFrame(() => {
postListContainer.classList.remove("layout-switching");
});
}, 200);
}
function resetMasonryLayout() {
const container = document.getElementById("post-list-container");
if (!container) return;
container.style.height = "";
container.style.position = "";
container.style.display = "";
const items = container.querySelectorAll(".post-card-item");
items.forEach((item) => {
// @ts-ignore
item.style.position = "";
// @ts-ignore
item.style.top = "";
// @ts-ignore
item.style.left = "";
// @ts-ignore
item.style.width = "";
});
}
function applyMasonryLayout() {
const container = document.getElementById("post-list-container");
if (!container) return;
const masonryEnabled = container.getAttribute("data-masonry-enabled") === "true";
if (!masonryEnabled) return;
// 仅在 grid 模式下应用
if (!container.classList.contains("grid-mode")) return;
const items = Array.from(container.querySelectorAll(".post-card-item"));
if (items.length === 0) return;
// 瀑布流配置
const gap = 16; // 1rem = 16px
const isBothSidebars = container.getAttribute("data-both-sidebars") === "true";
const gridColumns = parseInt(container.getAttribute("data-grid-columns") || "2");
let colCount = 2;
if (!isBothSidebars && gridColumns === 3 && window.innerWidth >= 1024) {
colCount = 3;
}
// 重置容器样式以允许绝对定位
container.style.position = "relative";
container.style.display = "block"; // 覆盖 grid display
// 使用 offsetWidth 避免 transform: scale 的影响
const containerWidth = container.offsetWidth;
const itemWidth = (containerWidth - (colCount - 1) * gap) / colCount;
const colHeights = new Array(colCount).fill(0);
items.forEach((item, index) => {
const colIndex = index % colCount; // Z字形分布:左右交替
// @ts-ignore
item.style.position = "absolute";
// @ts-ignore
item.style.width = `${itemWidth}px`;
// @ts-ignore
item.style.setProperty('height', 'auto', 'important');
// 获取高度,优先使用 offsetHeight 避免 transform 影响
// @ts-ignore
const height = item.offsetHeight;
const top = colHeights[colIndex];
const left = colIndex * (itemWidth + gap);
// @ts-ignore
item.style.top = `${top}px`;
// @ts-ignore
item.style.left = `${left}px`;
colHeights[colIndex] += height + gap;
});
container.style.height = `${Math.max(...colHeights)}px`;
}
// 页面加载时初始化布局
document.addEventListener("DOMContentLoaded", function () {
// 延迟一点确保DOM完全加载
setTimeout(initLayout, 50);
// 监听图片加载以重新计算布局
const imgs = document.querySelectorAll('#post-list-container img');
imgs.forEach(img => {
// @ts-ignore
if(img.complete) return;
img.addEventListener('load', () => {
applyMasonryLayout();
});
});
});
// 页面显示时也初始化布局(处理页面切换)
document.addEventListener("visibilitychange", function () {
if (!document.hidden) {
setTimeout(initLayout, 100);
}
});
// 监听布局变化事件
window.addEventListener("layoutChange", function (event) {
// @ts-ignore
const newLayout = event.detail.layout;
const postListContainer = document.getElementById("post-list-container");
if (!postListContainer) return;
// 检查屏幕宽度,如果小于1200px则强制使用列表模式
const screenWidth = window.innerWidth;
const isSmallScreen = screenWidth < 1200;
if (isSmallScreen) {
updatePostListLayout("list");
} else {
updatePostListLayout(newLayout);
}
});
// 监听窗口大小变化
let resizeTimeout: any;
window.addEventListener("resize", function () {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function () {
initLayout();
}, 250);
});
// 监听页面导航事件(Astro的客户端路由)
document.addEventListener("astro:page-load", function () {
setTimeout(initLayout, 50);
// 监听图片加载
const imgs = document.querySelectorAll('#post-list-container img');
imgs.forEach(img => {
// @ts-ignore
if(img.complete) return;
img.addEventListener('load', () => {
applyMasonryLayout();
});
});
});
document.addEventListener("astro:after-swap", function () {
setTimeout(initLayout, 50);
});
// 立即执行一次(处理页面刷新)
setTimeout(initLayout, 0);
</script>
<style>
/* 布局切换动画 */
#post-list-container {
/* 限制过渡属性为 opacity 和 transform,避免响应父元素位置变化 */
transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 文章卡片的过渡动画 */
#post-list-container > :global(*) {
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 布局切换时的动画效果 */
#post-list-container {
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
}
#post-list-container.layout-switching {
opacity: 0;
transform: translateY(10px);
}
/* 移除特定模式下的子元素动画,避免布局计算冲突 */
/*
#post-list-container.layout-switching > :global(*) {
transform: scale(0.98);
}
#post-list-container.list-mode > :global(*) {
animation: fadeInSlide 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
*/
/* List Mode Customization */
#post-list-container.list-mode :global(.post-card-title) {
font-size: 1.5rem !important;
line-height: 2rem !important;
}
#post-list-container.list-mode :global(.post-card-title::before) {
top: 2.25rem !important;
height: 1rem !important;
}
/* 列表模式下,有封面的文章,摘要最多显示两行 */
#post-list-container.list-mode :global(.has-cover .description) {
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
overflow: hidden !important;
-webkit-line-clamp: 2 !important;
line-clamp: 2 !important;
}
/* Grid Mode Customization */
#post-list-container.grid-mode :global(.post-card-wrapper) {
flex-direction: column-reverse !important;
}
#post-list-container.grid-mode :global(.post-card-image) {
width: 100% !important;
position: relative !important;
top: auto !important;
right: auto !important;
bottom: auto !important;
border-radius: var(--radius-large) var(--radius-large) 0 0 !important;
/*aspect-ratio: 16/9 !important;*/
}
#post-list-container.grid-mode :global(.post-card-content) {
width: 100% !important;
padding: 1rem !important;
}
#post-list-container.grid-mode :global(.no-cover .post-card-content) {
padding-right: 4.5rem !important;
}
#post-list-container.grid-mode :global(.post-card-title) {
font-size: 1.125rem !important;
line-height: 1.75rem !important;
margin-bottom: 0.5rem !important;
}
#post-list-container.grid-mode :global(.post-card-title::before) {
display: none !important;
}
#post-list-container.grid-mode :global(.description) {
font-size: 0.875rem !important;
margin-bottom: 0.75rem !important;
padding-right: 0 !important;
}
/* 网格模式下,有封面的文章,摘要最多显示两行 */
#post-list-container.grid-mode :global(.has-cover .description) {
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
overflow: hidden !important;
-webkit-line-clamp: 3 !important;
line-clamp: 3 !important;
}
#post-list-container.grid-mode :global(.post-meta) {
margin-bottom: 0.5rem !important;
gap: 0.5rem !important;
}
#post-list-container.grid-mode :global(.post-meta .text-xl) {
font-size: 1rem !important;
line-height: 1.25rem !important;
}
#post-list-container.grid-mode :global(.meta-icon) {
width: 1.5rem !important;
height: 1.5rem !important;
margin-right: 0.25rem !important;
}
#post-list-container.grid-mode :global(.post-meta .text-sm) {
font-size: 0.75rem !important;
line-height: 1rem !important;
}
#post-list-container.grid-mode :global(.post-meta .pinned-btn) {
padding: 0.25rem 0.375rem !important;
}
</style>