This commit is contained in:
1 parent
2afd5f5c6c
commit
3b9ed8af71
5 files changed
+304
No files matched your search
@@ -52,6 +52,11 @@
|
||||
class={$page.url.pathname.startsWith('/series') ? 'active' : ''}
|
||||
>{$page.data.aliases?.series || '系列'}</a
|
||||
>
|
||||
<a
|
||||
href={resolve('/categories')}
|
||||
class={$page.url.pathname.startsWith('/categories') ? 'active' : ''}
|
||||
>{$page.data.aliases?.category || '分类'}</a
|
||||
>
|
||||
<a
|
||||
href={resolve('/about')}
|
||||
class={$page.url.pathname.startsWith('/about') ? 'active' : ''}>关于</a
|
||||
|
||||
@@ -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, '读取分类列表失败');
|
||||
}
|
||||
};
|
||||
@@ -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, '读取分类内容列表失败');
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user