补齐后端最小功能和前端

This commit is contained in:
ChuNa7a committed 2026-10-16 23:36:56 +08:00
1 parent 0e347aad9e
commit 3e7c9420fe
60 files changed
+8181 -329

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
---
import Base from '../layouts/Base.astro';
import { readApi, postUrl, displayDate, assetUrl, type Metadata } from '../lib/api';
const query = (Astro.url.searchParams.get('q') || '').trim().slice(0, 100);
const selectedTag = Astro.url.searchParams.get('tag') || '';
let articles: Metadata[] = [];
let failed = false;
try { articles = (await readApi<{ posts: Metadata[] }>('/api/posts')).posts; }
catch { failed = true; Astro.response.status = 503; }
const tags = [...new Set(articles.flatMap((article) => article.tags))].sort((a, b) => a.localeCompare(b, 'zh-CN'));
const filtered = articles.filter((article) => (!selectedTag || article.tags.includes(selectedTag)) && (!query || `${article.title} ${article.description} ${article.author} ${article.tags.join(' ')}`.toLocaleLowerCase().includes(query.toLocaleLowerCase())));
const totalPages = Math.max(1, Math.ceil(filtered.length / 15));
const page = Math.min(totalPages, Math.max(1, Number.parseInt(Astro.url.searchParams.get('page') || '1', 10) || 1));
const visible = filtered.slice((page - 1) * 15, page * 15);
const urlFor = (page: number) => { const params = new URLSearchParams(); if (query) params.set('q', query); if (selectedTag) params.set('tag', selectedTag); params.set('page', String(page)); return `/articles?${params}`; };
---
<Base title="文章">
<div class="page-shell">
<header class="page-heading"><h1>文章</h1><p>写下的文字,按时间排列。</p></header>
<form class="search-form" action="/articles" method="get" role="search">
<label class="visually-hidden" for="search">搜索文章</label>
<input id="search" type="search" autocomplete="off" name="q" value={query} placeholder="搜索标题、简介或标签" maxlength="100" />
{selectedTag && <input type="hidden" name="tag" value={selectedTag} />}
<button type="submit" class="button button-quiet">搜索</button>
</form>
{tags.length > 0 && <nav class="tag-filter" aria-label="按标签筛选">
<a href="/articles" aria-current={!selectedTag ? 'true' : undefined}>全部</a>
{tags.map((tag) => <a href={`/articles?tag=${encodeURIComponent(tag)}`} aria-current={tag === selectedTag ? 'true' : undefined}>{tag}</a>)}
</nav>}
{failed ? <div class="empty-state" role="status"><h2>文章暂时无法加载</h2><p>请稍后重试。</p><a class="text-link" href={Astro.url.pathname + Astro.url.search}>重新加载</a></div>
: visible.length === 0 ? <div class="empty-state"><h2>{query || selectedTag ? '没有匹配的文章' : '还没有公开文章'}</h2><p>{query || selectedTag ? '换个关键词,或查看全部文章。' : '文章发布后会出现在这里。'}</p><a class="text-link" href={query || selectedTag ? '/articles' : '/account'}>{query || selectedTag ? '查看全部文章' : '前往账户'}</a></div>
: <ol class="article-list">{visible.map((article) => <li>
<article class:list={{ 'article-with-cover': article.cover }}>
<div class="article-summary">
<div class="article-meta"><time datetime={article.date}>{displayDate(article.date)}</time><span>{article.author}</span></div>
<h2><a href={postUrl(article.slug)}>{article.title}</a></h2>
{article.description && <p>{article.description}</p>}
{article.tags.length > 0 && <div class="article-tags">{article.tags.map((tag) => <a href={`/articles?tag=${encodeURIComponent(tag)}`}>{tag}</a>)}</div>}
</div>
{article.cover && <a href={postUrl(article.slug)} class="article-cover" aria-label={`阅读 ${article.title}`}><img src={assetUrl(article.slug, article.cover)} alt="" width="160" height="112" loading="lazy" /></a>}
</article>
</li>)}</ol>}
{totalPages > 1 && <nav class="pagination" aria-label="文章分页">{page > 1 ? <a href={urlFor(page - 1)}>上一页</a> : <span /> }<span>{page} / {totalPages}</span>{page < totalPages ? <a href={urlFor(page + 1)}>下一页</a> : <span />}</nav>}
</div>
</Base>