chore: reorganize project into monorepo

This commit is contained in:
2026-03-28 10:40:22 +08:00
parent 60367a5f51
commit 1455d93246
201 changed files with 30081 additions and 93 deletions

View File

@@ -0,0 +1,258 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import TerminalWindow from '../../components/ui/TerminalWindow.astro';
import CommandPrompt from '../../components/ui/CommandPrompt.astro';
import FilterPill from '../../components/ui/FilterPill.astro';
import PostCard from '../../components/PostCard.astro';
import { terminalConfig } from '../../lib/config/terminal';
import { api } from '../../lib/api/client';
import type { Category, Post, Tag } from '../../lib/types';
export const prerender = false;
let allPosts: Post[] = [];
let allTags: Tag[] = [];
let allCategories: Category[] = [];
const url = new URL(Astro.request.url);
const selectedSearch = url.searchParams.get('search') || '';
try {
allPosts = selectedSearch ? await api.searchPosts(selectedSearch) : await api.getPosts();
allCategories = await api.getCategories();
const rawTags = await api.getTags();
const seenTagIds = new Set<string>();
allTags = rawTags.filter(tag => {
const key = `${tag.slug}:${tag.name}`.toLowerCase();
if (seenTagIds.has(key)) return false;
seenTagIds.add(key);
return true;
});
} catch (error) {
console.error('API Error:', error);
}
const selectedType = url.searchParams.get('type') || 'all';
const selectedTag = url.searchParams.get('tag') || '';
const selectedCategory = url.searchParams.get('category') || '';
const currentPage = parseInt(url.searchParams.get('page') || '1');
const postsPerPage = 10;
const normalizedSelectedTag = selectedTag.trim().toLowerCase();
const isMatchingTag = (value: string) => value.trim().toLowerCase() === normalizedSelectedTag;
const isSelectedTag = (tag: Tag) =>
tag.name.trim().toLowerCase() === normalizedSelectedTag || tag.slug.trim().toLowerCase() === normalizedSelectedTag;
const filteredPosts = allPosts.filter(post => {
if (selectedType !== 'all' && post.type !== selectedType) return false;
if (selectedTag && !post.tags?.some(isMatchingTag)) return false;
if (selectedCategory && post.category?.toLowerCase() !== selectedCategory.toLowerCase()) return false;
return true;
});
const totalPosts = filteredPosts.length;
const totalPages = Math.ceil(totalPosts / postsPerPage);
const startIndex = (currentPage - 1) * postsPerPage;
const paginatedPosts = filteredPosts.slice(startIndex, startIndex + postsPerPage);
const postTypeFilters = [
{ id: 'all', name: '全部', icon: 'fa-stream' },
{ id: 'article', name: terminalConfig.postTypes.article.label, icon: 'fa-file-alt' },
{ id: 'tweet', name: terminalConfig.postTypes.tweet.label, icon: 'fa-comment-dots' }
];
const typePromptCommand = `./filter --type ${selectedType || 'all'}`;
const categoryPromptCommand = `./filter --category ${selectedCategory ? `"${selectedCategory}"` : 'all'}`;
const tagPromptCommand = `./filter --tag ${selectedTag ? `"${selectedTag}"` : 'all'}`;
const buildArticlesUrl = ({
type = selectedType,
search = selectedSearch,
tag = selectedTag,
category = selectedCategory,
page,
}: {
type?: string;
search?: string;
tag?: string;
category?: string;
page?: number;
}) => {
const params = new URLSearchParams();
if (type && type !== 'all') params.set('type', type);
if (search) params.set('search', search);
if (tag) params.set('tag', tag);
if (category) params.set('category', category);
if (page && page > 1) params.set('page', String(page));
const queryString = params.toString();
return queryString ? `/articles?${queryString}` : '/articles';
};
---
<BaseLayout title="文章列表 - Termi">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<TerminalWindow title="~/articles/index" class="w-full">
<div class="px-4 pb-2">
<CommandPrompt command="fd . ./content/posts --full-path" />
<div class="ml-4 mt-4 space-y-3">
<h1 class="text-3xl font-bold tracking-tight text-[var(--title-color)]">文章索引</h1>
<p class="max-w-2xl text-sm leading-7 text-[var(--text-secondary)]">
按类型、分类和标签筛选内容。这里保留更轻的 prompt 标题结构,下方筛选拆成独立区域。
</p>
<div class="flex flex-wrap gap-2">
<span class="terminal-stat-pill">
<i class="fas fa-file-lines text-[var(--primary)]"></i>
共 {filteredPosts.length} 篇
</span>
{selectedSearch && (
<span class="terminal-stat-pill">
<i class="fas fa-magnifying-glass text-[var(--primary)]"></i>
grep: {selectedSearch}
</span>
)}
{selectedCategory && (
<span class="terminal-stat-pill">
<i class="fas fa-folder-open text-[var(--primary)]"></i>
{selectedCategory}
</span>
)}
{selectedTag && (
<span class="terminal-stat-pill">
<i class="fas fa-hashtag text-[var(--primary)]"></i>
{selectedTag}
</span>
)}
</div>
</div>
</div>
<div class="px-4 pb-2 space-y-4">
<div class="ml-4">
<CommandPrompt command={typePromptCommand} typing={false} />
<div class="mt-3 flex flex-wrap gap-3">
{postTypeFilters.map(filter => (
<FilterPill
href={buildArticlesUrl({ type: filter.id, page: 1 })}
tone="blue"
active={selectedType === filter.id}
>
<i class={`fas ${filter.icon}`}></i>
<span class="font-medium">{filter.name}</span>
</FilterPill>
))}
</div>
</div>
{allCategories.length > 0 && (
<div class="ml-4">
<CommandPrompt command={categoryPromptCommand} typing={false} />
<div class="mt-3 flex flex-wrap gap-3">
<FilterPill
href={buildArticlesUrl({ category: '', page: 1 })}
tone="amber"
active={!selectedCategory}
>
<i class="fas fa-folder-tree"></i>
<span class="font-medium">全部分类</span>
</FilterPill>
{allCategories.map(category => (
<FilterPill
href={buildArticlesUrl({ category: category.name, page: 1 })}
tone="amber"
active={selectedCategory.toLowerCase() === category.name.toLowerCase()}
>
<i class="fas fa-folder-open"></i>
<span class="font-medium">{category.name}</span>
<span class="text-xs text-[var(--text-tertiary)]">{category.count}</span>
</FilterPill>
))}
</div>
</div>
)}
{allTags.length > 0 && (
<div class="ml-4">
<CommandPrompt command={tagPromptCommand} typing={false} />
<div class="mt-3 flex flex-wrap gap-3">
<FilterPill
href={buildArticlesUrl({ tag: '', page: 1 })}
tone="teal"
active={!selectedTag}
>
<i class="fas fa-hashtag"></i>
<span class="font-medium">全部标签</span>
</FilterPill>
{allTags.map(tag => (
<FilterPill
href={buildArticlesUrl({ tag: tag.slug || tag.name, page: 1 })}
tone="teal"
active={isSelectedTag(tag)}
>
<i class="fas fa-hashtag"></i>
<span class="font-medium">{tag.name}</span>
</FilterPill>
))}
</div>
</div>
)}
</div>
<div class="px-4">
{paginatedPosts.length > 0 ? (
<div class="ml-4 mt-4 space-y-4">
{paginatedPosts.map(post => (
<PostCard post={post} selectedTag={selectedTag} highlightTerm={selectedSearch} />
))}
</div>
) : (
<div class="terminal-empty ml-4 mt-4">
<div class="mx-auto flex max-w-md flex-col items-center gap-3">
<span class="terminal-section-icon">
<i class="fas fa-folder-open"></i>
</span>
<h2 class="text-xl font-semibold text-[var(--title-color)]">没有匹配结果</h2>
<p class="text-sm leading-7 text-[var(--text-secondary)]">
当前筛选条件下没有找到文章。可以清空标签或关键字,重新浏览整个内容目录。
</p>
<a href="/articles" class="terminal-action-button terminal-action-button-primary">
<i class="fas fa-rotate-left"></i>
<span>reset filters</span>
</a>
</div>
</div>
)}
</div>
{totalPages > 1 && (
<div class="px-4 py-6">
<div class="terminal-panel-muted ml-4 mt-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<span class="text-sm text-[var(--text-secondary)]">
page {currentPage}/{totalPages} · {totalPosts} results
</span>
<div class="flex flex-wrap gap-2">
{currentPage > 1 && (
<a
href={buildArticlesUrl({ page: currentPage - 1 })}
class="terminal-action-button"
>
<i class="fas fa-chevron-left"></i>
<span>prev</span>
</a>
)}
{currentPage < totalPages && (
<a
href={buildArticlesUrl({ page: currentPage + 1 })}
class="terminal-action-button terminal-action-button-primary"
>
<span>next</span>
<i class="fas fa-chevron-right"></i>
</a>
)}
</div>
</div>
</div>
)}
</TerminalWindow>
</div>
</BaseLayout>