feat: 添加分类查看功能及对应的路由页面
continuous-integration/drone/push Build is failing

This commit is contained in:
seahi committed 2026-08-08 15:25:14 +08:00
1 parent 2afd5f5c6c
commit 3b9ed8af71
5 files changed
+304

No files matched your search

+5
View File
@@ -52,6 +52,11 @@
class={$page.url.pathname.startsWith('/series') ? 'active' : ''} class={$page.url.pathname.startsWith('/series') ? 'active' : ''}
>{$page.data.aliases?.series || '系列'}</a >{$page.data.aliases?.series || '系列'}</a
> >
<a
href={resolve('/categories')}
class={$page.url.pathname.startsWith('/categories') ? 'active' : ''}
>{$page.data.aliases?.category || '分类'}</a
>
<a <a
href={resolve('/about')} href={resolve('/about')}
class={$page.url.pathname.startsWith('/about') ? 'active' : ''}>关于</a class={$page.url.pathname.startsWith('/about') ? 'active' : ''}>关于</a
+31
View File
@@ -0,0 +1,31 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ platform }) => {
const db = platform?.env?.DB;
if (!db) throw error(500, '数据库未连接');
try {
const { results } = await db
.prepare(
`SELECT c.id, c.name, c.slug, COUNT(p.id) AS post_count
FROM categories c
LEFT JOIN posts p ON p.category_id = c.id
GROUP BY c.id, c.name, c.slug
ORDER BY c.name COLLATE NOCASE ASC`
)
.all();
return {
categories: results as unknown as Array<{
id: number;
name: string;
slug: string;
post_count: number;
}>
};
} catch (err: any) {
console.error('Error fetching public categories list:', err);
throw error(500, '读取分类列表失败');
}
};
+69
View File
@@ -0,0 +1,69 @@
<script lang="ts">
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<svelte:head>
<title>全部{$page.data.aliases?.category || '分类'} - {$page.data.site?.title || 'BlogFlare'}</title
>
</svelte:head>
<main id="pjax-container">
<div class="wrapper container narrow contents-wrap">
<article class="article-detail yue">
<div class="banner-title">
<h1 class="post-title">全部{$page.data.aliases?.category || '分类'}</h1>
<p class="post-meta">按{$page.data.aliases?.category || '分类'}探索不同主题的{$page.data.aliases?.post || '文章'}</p>
</div>
<div class="series-grid">
{#each data.categories as item (item.id)}
<a href={resolve('/categories/[slug]', { slug: item.slug })} class="series-card">
<h2>{item.name}</h2>
<span>{item.post_count} 篇{$page.data.aliases?.post || '文章'}</span>
</a>
{:else}
<p class="empty-state">暂无{$page.data.aliases?.category || '分类'}</p>
{/each}
</div>
</article>
</div>
</main>
<style>
.series-grid {
display: grid;
gap: 1.25rem;
}
.series-card {
display: block;
padding: 1.4rem;
border: 2px solid var(--text-color);
color: var(--text-color);
text-decoration: none;
transition:
border-color 0.2s,
transform 0.2s;
}
.series-card:hover {
border-color: var(--accent-color, #419b38);
transform: translateY(-2px);
}
.series-card h2 {
margin: 0 0 0.5rem;
border: 0;
padding: 0;
font-size: 1.35rem;
}
.series-card span {
font-size: 0.9rem;
color: var(--text-muted);
}
.empty-state {
text-align: center;
color: var(--text-muted);
}
</style>
@@ -0,0 +1,100 @@
import { error, isHttpError } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const PAGE_SIZE = 20;
type CategoryPostRow = {
id: number;
slug: string;
title: string;
cover: string | null;
date: string;
excerpt_source: string;
};
function createExcerpt(source: string): string {
const plainText = source
.replace(/\{\{<[\s\S]*?>\}\}/g, '')
.replace(/<[^>]+>/g, '')
.replace(/[#*`>~[\]()-]/g, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 150)
.trim();
return plainText ? `${plainText}...` : '';
}
export const load: PageServerLoad = async ({ platform, params, url }) => {
const db = platform?.env.DB;
if (!db) throw error(500, '数据库未连接');
try {
const category = await db
.prepare('SELECT id, name, slug FROM categories WHERE slug = ? COLLATE NOCASE')
.bind(params.slug)
.first<{ id: number; name: string; slug: string }>();
if (!category) throw error(404, '该分类不存在');
const countResult = await db
.prepare('SELECT COUNT(*) AS total FROM posts WHERE category_id = ?')
.bind(category.id)
.first<{ total: number }>();
const total = countResult?.total ?? 0;
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const requestedPage = Number(url.searchParams.get('page') ?? '1');
const page = Number.isInteger(requestedPage)
? Math.min(Math.max(requestedPage, 1), totalPages)
: 1;
const { results } = await db
.prepare(
`SELECT
p.id,
p.slug,
p.title,
p.cover,
p.published_at AS date,
CASE
WHEN instr(p.content, '<!--more-->') > 0
THEN substr(p.content, 1, instr(p.content, '<!--more-->') - 1)
ELSE substr(p.content, 1, 500)
END AS excerpt_source
FROM posts p
WHERE p.category_id = ?
ORDER BY p.published_at DESC, p.id DESC
LIMIT ? OFFSET ?`
)
.bind(category.id, PAGE_SIZE, (page - 1) * PAGE_SIZE)
.all<CategoryPostRow>();
const posts = results.map((post) => {
const date = new Date(post.date);
return {
id: post.id,
title: post.title,
slug: post.slug,
cover: post.cover,
date: Number.isNaN(date.getTime())
? post.date
: date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC'
}),
excerpt: createExcerpt(post.excerpt_source || '')
};
});
return {
category,
posts,
pagination: { page, pageSize: PAGE_SIZE, total, totalPages }
};
} catch (cause) {
if (isHttpError(cause, 404)) throw cause;
console.error('Error loading category posts:', cause);
throw error(500, '读取分类内容列表失败');
}
};
+99
View File
@@ -0,0 +1,99 @@
<script lang="ts">
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<svelte:head>
<title
>{$page.data.aliases?.category || '分类'}:{data.category.name} - {$page.data.site?.title ||
'BlogFlare'}</title
>
</svelte:head>
<main id="pjax-container">
<div class="wrapper container narrow contents-wrap">
<article class="article-detail yue" style="margin-bottom: 2rem;">
<div class="banner-title">
<h1 class="post-title">{$page.data.aliases?.category || '分类'}:{data.category.name}</h1>
<p class="post-meta">
共收录了 {data.pagination.total} 篇属于此{$page.data.aliases?.category || '分类'}的{$page.data.aliases?.post || '文章'}
</p>
</div>
</article>
</div>
<section id="index-list">
<div class="post-list">
{#each data.posts as post (post.id)}
<a href={resolve(`/posts/${post.slug}`)} class="card-link">
<article class="card article-content" class:has-cover={!!post.cover}>
{#if post.cover}
<div class="post-cover">
<img src={post.cover} alt={post.title} loading="lazy" />
</div>
{/if}
<div class="content-wrap">
<div class="post-meta-index">
<time>{post.date}</time>
</div>
<h1 class="title">{post.title}</h1>
<p class="excerpt">{post.excerpt}</p>
</div>
</article>
</a>
{:else}
<p class="empty-notice">现阶段尚未在此{$page.data.aliases?.category || '分类'}下发布任何{$page.data.aliases?.post || '文章'}。</p>
{/each}
</div>
{#if data.pagination.totalPages > 1}
<nav class="pagination" aria-label="分类文章分页">
{#if data.pagination.page > 1}
<a
href={resolve(
`/categories/${encodeURIComponent(data.category.slug)}?page=${data.pagination.page - 1}`
)}
rel="prev">上一页</a
>
{:else}
<span aria-hidden="true">上一页</span>
{/if}
<strong>{data.pagination.page} / {data.pagination.totalPages}</strong>
{#if data.pagination.page < data.pagination.totalPages}
<a
href={resolve(
`/categories/${encodeURIComponent(data.category.slug)}?page=${data.pagination.page + 1}`
)}
rel="next">下一页</a
>
{:else}
<span aria-hidden="true">下一页</span>
{/if}
</nav>
{/if}
</section>
</main>
<style>
.empty-notice {
text-align: center;
color: var(--text-muted, rgba(0, 0, 0, 0.5));
padding: 3rem 1rem;
font-size: 1rem;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1.25rem;
padding: 1.5rem 1rem 3rem;
}
.pagination span {
opacity: 0.35;
}
</style>