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' : ''}
|
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
|
||||||
|
|||||||
@@ -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