文章页优化
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
---
|
||||
type Props = {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
basePath?: string;
|
||||
};
|
||||
|
||||
const { currentPage, totalPages, basePath = '/' } = Astro.props;
|
||||
|
||||
const normalizedBasePath = basePath.endsWith('/') ? basePath : `${basePath}/`;
|
||||
const getPageHref = (page: number) => (page <= 1 ? normalizedBasePath : `${normalizedBasePath}page/${page}/`);
|
||||
const pages = Array.from({ length: totalPages }, (_, index) => index + 1);
|
||||
---
|
||||
|
||||
{
|
||||
totalPages > 1 && (
|
||||
<nav class="pagination" aria-label="分页导航">
|
||||
{currentPage > 1 ? (
|
||||
<a class="pagination-link pagination-prev" href={getPageHref(currentPage - 1)} aria-label="上一页">
|
||||
上一页
|
||||
</a>
|
||||
) : (
|
||||
<span class="pagination-link pagination-prev is-disabled" aria-disabled="true">
|
||||
上一页
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div class="pagination-pages" aria-label={`第 ${currentPage} 页,共 ${totalPages} 页`}>
|
||||
{pages.map((page) =>
|
||||
page === currentPage ? (
|
||||
<span class="pagination-number is-current" aria-current="page">
|
||||
{page}
|
||||
</span>
|
||||
) : (
|
||||
<a class="pagination-number" href={getPageHref(page)}>
|
||||
{page}
|
||||
</a>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentPage < totalPages ? (
|
||||
<a class="pagination-link pagination-next" href={getPageHref(currentPage + 1)} aria-label="下一页">
|
||||
下一页
|
||||
</a>
|
||||
) : (
|
||||
<span class="pagination-link pagination-next is-disabled" aria-disabled="true">
|
||||
下一页
|
||||
</span>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
import type { BlogPost } from '@/lib/posts';
|
||||
|
||||
interface Props {
|
||||
post: BlogPost;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const author = 'biss';
|
||||
const licenseName = 'CC BY-NC-SA 4.0';
|
||||
const licenseHref = 'https://creativecommons.org/licenses/by-nc-sa/4.0/';
|
||||
const postUrl = new URL(post.href, Astro.site ?? Astro.url.origin).toString();
|
||||
const displayPath = post.href;
|
||||
const tags = post.tags;
|
||||
---
|
||||
|
||||
<section class="post-copyright-card" aria-label="文章版权信息">
|
||||
<div class="post-copyright-mark" aria-hidden="true">
|
||||
<span>CC</span>
|
||||
</div>
|
||||
|
||||
<div class="post-copyright-main">
|
||||
<h2>{post.title}</h2>
|
||||
<a class="post-copyright-link" href={post.href}>{displayPath}</a>
|
||||
|
||||
<dl class="post-copyright-meta">
|
||||
<div>
|
||||
<dt>作者</dt>
|
||||
<dd>{author}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>发布于</dt>
|
||||
<dd><time datetime={post.date.toISOString()}>{post.dateText}</time></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>许可协议</dt>
|
||||
<dd><a href={licenseHref} target="_blank" rel="noreferrer">{licenseName}</a></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="post-copyright-footer">
|
||||
{tags.length > 0 && (
|
||||
<div class="post-copyright-tags" aria-label="文章标签">
|
||||
{tags.map((tag) => <a href={`/tags/${encodeURIComponent(tag)}/`}>{tag}</a>)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="post-copyright-actions" aria-label="文章操作">
|
||||
<button
|
||||
class="post-copyright-action is-primary post-share-button"
|
||||
type="button"
|
||||
data-share-title={post.title}
|
||||
data-share-url={postUrl}
|
||||
title="分享文章"
|
||||
>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7" />
|
||||
<path d="M12 16V4" />
|
||||
<path d="m7 9 5-5 5 5" />
|
||||
</svg>
|
||||
<span>分享</span>
|
||||
</button>
|
||||
<a class="post-copyright-action" href="/rss.xml" title="订阅 RSS">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M5 5a14 14 0 0 1 14 14" />
|
||||
<path d="M5 11a8 8 0 0 1 8 8" />
|
||||
<path d="M6 18h.01" />
|
||||
</svg>
|
||||
<span>订阅</span>
|
||||
</a>
|
||||
<a class="post-copyright-action" href="/about/" title="打赏作者">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M7 8h10" />
|
||||
<path d="M8 8V5h8v3" />
|
||||
<path d="M6 8h12l-1.5 11h-9L6 8Z" />
|
||||
<path d="M10 12h4" />
|
||||
</svg>
|
||||
<span>打赏</span>
|
||||
</a>
|
||||
<a class="post-copyright-icon" href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(postUrl)}&text=${encodeURIComponent(post.title)}`} target="_blank" rel="noreferrer" title="分享到 X">
|
||||
<span aria-hidden="true">x</span>
|
||||
</a>
|
||||
<a class="post-copyright-icon" href={`https://service.weibo.com/share/share.php?url=${encodeURIComponent(postUrl)}&title=${encodeURIComponent(post.title)}`} target="_blank" rel="noreferrer" title="分享到微博">
|
||||
<span aria-hidden="true">微</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
(() => {
|
||||
document.querySelectorAll('.post-share-button').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const title = button.dataset.shareTitle || document.title;
|
||||
const url = button.dataset.shareUrl || window.location.href;
|
||||
const label = button.querySelector('span');
|
||||
const originalText = label?.textContent || '分享';
|
||||
|
||||
try {
|
||||
if (navigator.share) {
|
||||
await navigator.share({ title, url });
|
||||
return;
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(url);
|
||||
if (label) label.textContent = '已复制';
|
||||
} catch {
|
||||
if (label) label.textContent = '复制失败';
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
if (label) label.textContent = originalText;
|
||||
}, 1800);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -9,6 +9,7 @@ interface Props {
|
||||
const siteTitle = "Bi's Blog";
|
||||
const { title = siteTitle, description = 'Personal blog by biss.' } = Astro.props;
|
||||
const pageTitle = title === siteTitle ? siteTitle : `${title} | ${siteTitle}`;
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -36,9 +37,81 @@ const pageTitle = title === siteTitle ? siteTitle : `${title} | ${siteTitle}`;
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<span>© {new Date().getFullYear()} Bi's Blog</span>
|
||||
<a href="/rss.xml">RSS</a>
|
||||
<footer class="site-footer" aria-label="站点页脚">
|
||||
<div class="footer-inner">
|
||||
<section class="footer-top" aria-label="站点信息">
|
||||
<a class="footer-brand" href="/" aria-label="Bi's Blog 首页">
|
||||
<img src="/images/Bi.ico" alt="" loading="lazy" />
|
||||
<span>Bi's Blog</span>
|
||||
</a>
|
||||
<p class="footer-slogan">好奇心转化为代码的空间。</p>
|
||||
</section>
|
||||
|
||||
<section class="footer-contact" aria-label="联系方式">
|
||||
<p>联系方式</p>
|
||||
<div class="footer-socials">
|
||||
<a href="mailto:bishsh2006@gmail.com" aria-label="发送邮件" title="发送邮件">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M4 6h16v12H4z" />
|
||||
<path d="m4 7 8 6 8-6" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/rss.xml" aria-label="订阅 RSS" title="订阅 RSS">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M6 17h.01" />
|
||||
<path d="M6 7a11 11 0 0 1 11 11" />
|
||||
<path d="M6 12a6 6 0 0 1 6 6" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://github.com/bishshi" target="_blank" rel="noreferrer" aria-label="GitHub" title="GitHub">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M12 3a9 9 0 0 0-3 17c.5.1.7-.2.7-.5v-1.8c-2.8.6-3.4-1.2-3.4-1.2-.4-1.1-1-1.4-1-1.4-.9-.6.1-.6.1-.6 1 0 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.2-.2-4.5-1.1-4.5-4.8 0-1.1.4-1.9 1-2.6-.1-.3-.4-1.3.1-2.6 0 0 .8-.3 2.7 1a9 9 0 0 1 4.9 0c1.8-1.3 2.6-1 2.6-1 .5 1.3.2 2.3.1 2.6.6.7 1 1.6 1 2.6 0 3.7-2.3 4.5-4.5 4.8.4.3.7 1 .7 2v2.5c0 .3.2.6.7.5A9 9 0 0 0 12 3Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<nav class="footer-contact-links" aria-label="页脚链接">
|
||||
<a href="/archives/">发展历程</a>
|
||||
<span aria-hidden="true">|</span>
|
||||
<a href="/privacy/">隐私政策</a>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<section class="footer-bottom" aria-label="版权信息">
|
||||
<div>
|
||||
<p>© 2025 - {currentYear} Bi's Blog</p>
|
||||
<p>blog.biss.click | 内容采用 CC BY-NC-SA 4.0 协议</p>
|
||||
</div>
|
||||
<div class="footer-meta">
|
||||
<p>本站已苟活:<span id="site-runtime">计算中...</span></p>
|
||||
<p>
|
||||
<a href="https://edgeone.ai/" target="_blank" rel="noreferrer">EdgeOne</a>
|
||||
<span aria-hidden="true">|</span>
|
||||
<a href="https://astro.build/" target="_blank" rel="noreferrer">ASTRO</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</footer>
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const runtime = document.getElementById('site-runtime');
|
||||
if (!runtime) return;
|
||||
|
||||
const start = new Date('2025-01-01T00:00:00+08:00').getTime();
|
||||
const pad = (value) => String(value).padStart(2, '0');
|
||||
|
||||
function updateRuntime() {
|
||||
const diff = Math.max(0, Date.now() - start);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
const hours = Math.floor((diff / 3600000) % 24);
|
||||
const minutes = Math.floor((diff / 60000) % 60);
|
||||
const seconds = Math.floor((diff / 1000) % 60);
|
||||
runtime.textContent = `${days} 天 ${pad(hours)} 时 ${pad(minutes)} 分 ${pad(seconds)} 秒`;
|
||||
}
|
||||
|
||||
updateRuntime();
|
||||
window.setInterval(updateRuntime, 1000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export const POSTS_PER_PAGE = 16;
|
||||
|
||||
export type PageSlice<T> = {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
items: T[];
|
||||
};
|
||||
|
||||
export function getPageSlice<T>(items: T[], currentPage: number, perPage = POSTS_PER_PAGE): PageSlice<T> {
|
||||
const totalPages = Math.max(1, Math.ceil(items.length / perPage));
|
||||
const safePage = Math.min(Math.max(currentPage, 1), totalPages);
|
||||
const start = (safePage - 1) * perPage;
|
||||
|
||||
return {
|
||||
currentPage: safePage,
|
||||
totalPages,
|
||||
items: items.slice(start, start + perPage),
|
||||
};
|
||||
}
|
||||
|
||||
export function getPaginationPaths<T>(items: T[], perPage = POSTS_PER_PAGE): number[] {
|
||||
const totalPages = Math.ceil(items.length / perPage);
|
||||
|
||||
return Array.from({ length: Math.max(0, totalPages - 1) }, (_, index) => index + 2);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
const linkTagPattern = /\{%\s*link\s+([\s\S]*?)\s*%\}/g;
|
||||
const markdownLinkPattern = /^\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)$/;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function splitLinkArgs(value) {
|
||||
const parts = [];
|
||||
let current = '';
|
||||
let bracketDepth = 0;
|
||||
let parenDepth = 0;
|
||||
|
||||
for (const char of value) {
|
||||
if (char === '[') bracketDepth += 1;
|
||||
if (char === ']' && bracketDepth > 0) bracketDepth -= 1;
|
||||
if (char === '(') parenDepth += 1;
|
||||
if (char === ')' && parenDepth > 0) parenDepth -= 1;
|
||||
|
||||
if (char === ',' && bracketDepth === 0 && parenDepth === 0 && parts.length < 2) {
|
||||
parts.push(current.trim());
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(current.trim());
|
||||
return parts;
|
||||
}
|
||||
|
||||
function normalizeUrl(value) {
|
||||
const trimmed = value.trim();
|
||||
const markdownLink = trimmed.match(markdownLinkPattern);
|
||||
return markdownLink ? markdownLink[1] : trimmed;
|
||||
}
|
||||
|
||||
function getHostname(url) {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderLinkCard(rawValue) {
|
||||
const [title, source, rawUrl] = splitLinkArgs(rawValue);
|
||||
const url = normalizeUrl(rawUrl ?? '');
|
||||
|
||||
if (!title || !url) return `{% link ${rawValue} %}`;
|
||||
|
||||
const host = getHostname(url);
|
||||
const label = source?.split('@')[0]?.trim() || host || 'Link';
|
||||
const initial = label.slice(0, 1).toUpperCase();
|
||||
|
||||
return `<a class="hexo-link-card" href="${escapeHtml(url)}" target="_blank" rel="external nofollow noopener noreferrer">
|
||||
<span class="hexo-link-card__icon" aria-hidden="true">${escapeHtml(initial)}</span>
|
||||
<span class="hexo-link-card__body">
|
||||
<strong>${escapeHtml(title)}</strong>
|
||||
<span>${escapeHtml(label)}${host ? ` · ${escapeHtml(host)}` : ''}</span>
|
||||
</span>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function transformText(value) {
|
||||
let matched = false;
|
||||
const transformed = value.replace(linkTagPattern, (_, rawValue) => {
|
||||
matched = true;
|
||||
return renderLinkCard(rawValue);
|
||||
});
|
||||
|
||||
return matched ? transformed : value;
|
||||
}
|
||||
|
||||
function inlineToSource(node) {
|
||||
if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';
|
||||
|
||||
if (node.type === 'link') {
|
||||
const text = Array.isArray(node.children) ? node.children.map(inlineToSource).join('') : node.url;
|
||||
return `[${text}](${node.url})`;
|
||||
}
|
||||
|
||||
if (Array.isArray(node.children)) return node.children.map(inlineToSource).join('');
|
||||
return '';
|
||||
}
|
||||
|
||||
function visit(node) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
|
||||
if (node.type === 'paragraph' && Array.isArray(node.children)) {
|
||||
const source = node.children.map(inlineToSource).join('');
|
||||
const transformed = transformText(source);
|
||||
|
||||
if (transformed !== source) {
|
||||
node.type = 'html';
|
||||
node.value = transformed;
|
||||
delete node.children;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'text' && typeof node.value === 'string') {
|
||||
const transformed = transformText(node.value);
|
||||
|
||||
if (transformed !== node.value) {
|
||||
node.type = 'html';
|
||||
node.value = transformed;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) visit(child);
|
||||
}
|
||||
}
|
||||
|
||||
export default function remarkHexoLinkCards() {
|
||||
return (tree) => {
|
||||
visit(tree);
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,25 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import Pagination from '@/components/Pagination.astro';
|
||||
import PageHeaderCard from '@/components/PageHeaderCard.astro';
|
||||
import { getAllPosts } from '@/lib/posts';
|
||||
import { getPageSlice } from '@/lib/pagination';
|
||||
|
||||
const posts = getAllPosts();
|
||||
const pageData = getPageSlice(posts, 1);
|
||||
---
|
||||
|
||||
<BaseLayout title="归档">
|
||||
<section class="content-wrap narrow">
|
||||
<PageHeaderCard title="归档" subtitle={`共 ${posts.length} 篇文章`} />
|
||||
<div class="archive-list">
|
||||
{posts.map((post) => (
|
||||
{pageData.items.map((post) => (
|
||||
<a href={post.href}>
|
||||
<time datetime={post.date.toISOString()}>{post.dateText}</time>
|
||||
<span>{post.title}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<Pagination currentPage={pageData.currentPage} totalPages={pageData.totalPages} basePath="/archives/" />
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import Pagination from '@/components/Pagination.astro';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import PageHeaderCard from '@/components/PageHeaderCard.astro';
|
||||
import { getAllPosts } from '@/lib/posts';
|
||||
import { getPageSlice, getPaginationPaths } from '@/lib/pagination';
|
||||
|
||||
export function getStaticPaths() {
|
||||
const posts = getAllPosts();
|
||||
|
||||
return getPaginationPaths(posts).map((page) => ({
|
||||
params: { page: String(page) },
|
||||
props: { page },
|
||||
}));
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const posts = getAllPosts();
|
||||
const pageData = getPageSlice(posts, page);
|
||||
---
|
||||
|
||||
<BaseLayout title={`归档 - 第 ${pageData.currentPage} 页`}>
|
||||
<section class="content-wrap narrow">
|
||||
<PageHeaderCard title="归档" subtitle={`共 ${posts.length} 篇文章,第 ${pageData.currentPage} 页`} />
|
||||
<div class="archive-list">
|
||||
{pageData.items.map((post) => (
|
||||
<a href={post.href}>
|
||||
<time datetime={post.date.toISOString()}>{post.dateText}</time>
|
||||
<span>{post.title}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<Pagination currentPage={pageData.currentPage} totalPages={pageData.totalPages} basePath="/archives/" />
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -1,12 +1,15 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import Pagination from '@/components/Pagination.astro';
|
||||
import HomeSidebar from '@/components/HomeSidebar.astro';
|
||||
import PostCard from '@/components/PostCard.astro';
|
||||
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/posts';
|
||||
import { getPageSlice } from '@/lib/pagination';
|
||||
|
||||
const posts = getAllPosts();
|
||||
const tags = getAllTags();
|
||||
const categories = getAllCategories();
|
||||
const pageData = getPageSlice(posts, 1);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
@@ -24,8 +27,9 @@ const categories = getAllCategories();
|
||||
<a href="/archives/">查看全部</a>
|
||||
</div>
|
||||
<div class="post-list">
|
||||
{posts.slice(0, 16).map((post) => <PostCard post={post} />)}
|
||||
{pageData.items.map((post) => <PostCard post={post} />)}
|
||||
</div>
|
||||
<Pagination currentPage={pageData.currentPage} totalPages={pageData.totalPages} />
|
||||
</div>
|
||||
<HomeSidebar posts={posts} tags={tags} categories={categories} />
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
import Pagination from '@/components/Pagination.astro';
|
||||
import HomeSidebar from '@/components/HomeSidebar.astro';
|
||||
import PostCard from '@/components/PostCard.astro';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getAllCategories, getAllPosts, getAllTags } from '@/lib/posts';
|
||||
import { getPageSlice, getPaginationPaths } from '@/lib/pagination';
|
||||
|
||||
export function getStaticPaths() {
|
||||
const posts = getAllPosts();
|
||||
|
||||
return getPaginationPaths(posts).map((page) => ({
|
||||
params: { page: String(page) },
|
||||
props: { page },
|
||||
}));
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const posts = getAllPosts();
|
||||
const tags = getAllTags();
|
||||
const categories = getAllCategories();
|
||||
const pageData = getPageSlice(posts, page);
|
||||
---
|
||||
|
||||
<BaseLayout title={`文章 - 第 ${pageData.currentPage} 页`}>
|
||||
<section class="hero compact-hero">
|
||||
<div>
|
||||
<p class="eyebrow">写给日常、技术和正在发生的自己</p>
|
||||
<h1>Bi's Blog</h1>
|
||||
<p>第 {pageData.currentPage} 页,继续翻看最近的记录。</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="content-wrap home-layout">
|
||||
<div class="home-main">
|
||||
<div class="section-heading">
|
||||
<h2>最新文章</h2>
|
||||
<a href="/archives/">查看全部</a>
|
||||
</div>
|
||||
<div class="post-list">
|
||||
{pageData.items.map((post) => <PostCard post={post} />)}
|
||||
</div>
|
||||
<Pagination currentPage={pageData.currentPage} totalPages={pageData.totalPages} />
|
||||
</div>
|
||||
<HomeSidebar posts={posts} tags={tags} categories={categories} />
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import ArticleSidebar from '@/components/ArticleSidebar.astro';
|
||||
import PostCopyrightCard from '@/components/PostCopyrightCard.astro';
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getAllPosts } from '@/lib/posts';
|
||||
|
||||
@@ -79,6 +80,7 @@ const relatedPosts = allPosts
|
||||
<div class="prose" data-toc={JSON.stringify(toc)}>
|
||||
<Content />
|
||||
</div>
|
||||
<PostCopyrightCard post={post} />
|
||||
</article>
|
||||
<ArticleSidebar post={post} toc={toc} relatedPosts={relatedPosts} />
|
||||
</section>
|
||||
|
||||
+521
-4
@@ -144,6 +144,10 @@ a {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.compact-hero {
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(3rem, 8vw, 6.5rem);
|
||||
@@ -756,6 +760,69 @@ time {
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.prose .hexo-link-card {
|
||||
display: grid;
|
||||
grid-template-columns: 46px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin: 1.2em 0;
|
||||
border: 1px solid rgba(15, 118, 110, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
color: #27313a;
|
||||
text-decoration: none;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.78), rgba(236, 253, 245, 0.62)),
|
||||
rgba(255, 253, 248, 0.88);
|
||||
box-shadow: 0 12px 28px rgba(44, 55, 63, 0.1);
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.prose .hexo-link-card:hover {
|
||||
color: #27313a;
|
||||
border-color: rgba(15, 118, 110, 0.38);
|
||||
box-shadow: 0 16px 34px rgba(15, 118, 110, 0.14);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.hexo-link-card__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
background: #3eb8be;
|
||||
}
|
||||
|
||||
.hexo-link-card__body {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hexo-link-card__body strong,
|
||||
.hexo-link-card__body span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hexo-link-card__body strong {
|
||||
font-size: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hexo-link-card__body span {
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
@@ -771,6 +838,216 @@ time {
|
||||
font-family: "JetBrains Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.post-copyright-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-top: 42px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.64);
|
||||
border-radius: 8px;
|
||||
padding: 24px 28px 20px;
|
||||
color: #33433f;
|
||||
background:
|
||||
radial-gradient(circle at 14% 0%, rgba(255, 255, 255, 0.82), transparent 32%),
|
||||
linear-gradient(135deg, rgba(255, 253, 248, 0.76), rgba(236, 253, 245, 0.58)),
|
||||
rgba(255, 253, 248, 0.72);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.88),
|
||||
0 16px 42px rgba(58, 51, 38, 0.12);
|
||||
backdrop-filter: blur(18px) saturate(165%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(165%);
|
||||
}
|
||||
|
||||
.post-copyright-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.44), transparent 44%, rgba(255, 255, 255, 0.22));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.post-copyright-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
height: 1px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.post-copyright-main,
|
||||
.post-copyright-footer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.post-copyright-mark {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 22px;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: clamp(74px, 12vw, 118px);
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid rgba(15, 118, 110, 0.12);
|
||||
border-radius: 999px;
|
||||
color: rgba(15, 118, 110, 0.16);
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(1.85rem, 5vw, 3.4rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.46), rgba(236, 253, 245, 0.2)),
|
||||
rgba(255, 255, 255, 0.16);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.72),
|
||||
0 12px 30px rgba(15, 118, 110, 0.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.post-copyright-mark span {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.post-copyright-card h2 {
|
||||
margin: 0;
|
||||
color: #27313a;
|
||||
font-size: clamp(1.08rem, 2.2vw, 1.35rem);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.post-copyright-link {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 0.94rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.post-copyright-link:hover,
|
||||
.post-copyright-meta a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.post-copyright-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.25fr 2fr;
|
||||
gap: 18px 28px;
|
||||
margin: 20px 0 0;
|
||||
border-top: 1px solid rgba(15, 118, 110, 0.18);
|
||||
border-bottom: 1px solid rgba(15, 118, 110, 0.12);
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.post-copyright-meta div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.post-copyright-meta dt {
|
||||
margin-bottom: 5px;
|
||||
color: #58645f;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.post-copyright-meta dd {
|
||||
margin: 0;
|
||||
color: #2f4a43;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.post-copyright-meta time {
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.post-copyright-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.post-copyright-tags,
|
||||
.post-copyright-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.post-copyright-tags a,
|
||||
.post-copyright-action,
|
||||
.post-copyright-icon {
|
||||
min-height: 38px;
|
||||
border: 1px solid rgba(15, 118, 110, 0.2);
|
||||
border-radius: 999px;
|
||||
color: var(--accent);
|
||||
background: rgba(236, 253, 245, 0.72);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.54);
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.post-copyright-tags a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 7px 13px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.post-copyright-action {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 15px;
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.post-copyright-action.is-primary {
|
||||
color: #fff;
|
||||
border-color: rgba(15, 118, 110, 0.3);
|
||||
background: #3eb8be;
|
||||
}
|
||||
|
||||
.post-copyright-action svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2.4;
|
||||
}
|
||||
|
||||
.post-copyright-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
font-size: 0.94rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.post-copyright-tags a:hover,
|
||||
.post-copyright-action:hover,
|
||||
.post-copyright-icon:hover {
|
||||
color: #fff;
|
||||
background: #3eb8be;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.archive-list {
|
||||
margin-top: 24px;
|
||||
display: grid;
|
||||
@@ -785,6 +1062,64 @@ time {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.pagination-pages {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-link,
|
||||
.pagination-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
border: 1px solid rgba(222, 216, 204, 0.78);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
color: #50637a;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
background: rgba(255, 253, 248, 0.82);
|
||||
box-shadow: 0 10px 28px rgba(58, 51, 38, 0.08);
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-link {
|
||||
min-width: 76px;
|
||||
}
|
||||
|
||||
.pagination-link:hover,
|
||||
.pagination-number:hover,
|
||||
.pagination-number.is-current {
|
||||
color: #fff;
|
||||
border-color: rgba(15, 118, 110, 0.34);
|
||||
background: #3eb8be;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pagination-link.is-disabled {
|
||||
color: rgba(104, 111, 120, 0.5);
|
||||
background: rgba(255, 253, 248, 0.46);
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.term-grid {
|
||||
margin-top: 24px;
|
||||
display: grid;
|
||||
@@ -804,13 +1139,159 @@ time {
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
width: min(var(--wide-width), calc(100% - (var(--page-gutter) * 2)));
|
||||
position: relative;
|
||||
margin-top: 64px;
|
||||
color: rgba(83, 91, 96, 0.72);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(242, 248, 236, 0.56), rgba(246, 250, 239, 0.72)),
|
||||
rgba(246, 250, 239, 0.72);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
|
||||
backdrop-filter: blur(18px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(150%);
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
width: min(1180px, calc(100% - (var(--page-gutter) * 2)));
|
||||
margin: 0 auto;
|
||||
padding: 32px 0 44px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 320px);
|
||||
gap: 34px 64px;
|
||||
padding: 42px 0 38px;
|
||||
}
|
||||
|
||||
.footer-top {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: fit-content;
|
||||
color: #3f4143;
|
||||
font-size: 1.22rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.footer-brand img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 999px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 8px 22px rgba(15, 118, 110, 0.18);
|
||||
}
|
||||
|
||||
.footer-slogan,
|
||||
.footer-contact p,
|
||||
.footer-bottom p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer-slogan {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.footer-contact {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-contact p {
|
||||
color: rgba(83, 91, 96, 0.64);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.footer-socials {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-socials a {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid rgba(178, 194, 170, 0.48);
|
||||
border-radius: 10px;
|
||||
color: rgba(76, 84, 88, 0.78);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.48), rgba(218, 231, 203, 0.42)),
|
||||
rgba(236, 244, 226, 0.72);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.7),
|
||||
0 10px 24px rgba(91, 107, 74, 0.1);
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-socials a:hover {
|
||||
color: var(--accent);
|
||||
border-color: rgba(15, 118, 110, 0.28);
|
||||
background-color: rgba(255, 255, 255, 0.84);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.footer-socials svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.footer-socials a:last-child svg {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.footer-contact-links,
|
||||
.footer-meta p:last-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-contact-links {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.footer-contact-links span,
|
||||
.footer-meta span {
|
||||
color: rgba(83, 91, 96, 0.35);
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
color: var(--muted);
|
||||
gap: 24px;
|
||||
border-top: 1px solid rgba(182, 197, 174, 0.34);
|
||||
padding-top: 22px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.footer-bottom > div {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
justify-items: end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
@@ -895,4 +1376,40 @@ time {
|
||||
.article {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.post-copyright-card {
|
||||
padding: 22px 18px 18px;
|
||||
}
|
||||
|
||||
.post-copyright-meta {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.post-copyright-footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 28px;
|
||||
padding: 36px 0 34px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-top {
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user