feat: 支持在后台标签页面通过点击文章数量按标签筛选文章
continuous-integration/drone/push Build is passing

This commit is contained in:
seahi committed 2026-10-07 12:30:30 +08:00
1 parent 5df6fe45e8
commit 7b8358005f
3 files changed
+159 -27

No files matched your search

+78 -25
View File
@@ -11,41 +11,93 @@ interface PostListRow {
category?: string;
series?: string;
}
export const load: PageServerLoad = async ({ platform }) => {
export const load: PageServerLoad = async ({ url, platform }) => {
const db = platform?.env?.DB;
if (!db) {
return {
posts: []
posts: [],
filterTag: null
};
}
const tagIdParam = url.searchParams.get('tagId');
const tagId = tagIdParam ? parseInt(tagIdParam, 10) : null;
let filterTag: { id: number; name: string } | null = null;
if (tagId && !Number.isNaN(tagId)) {
try {
const tagRes = await db
.prepare('SELECT id, name FROM tags WHERE id = ?')
.bind(tagId)
.first<{ id: number; name: string }>();
if (tagRes) {
filterTag = tagRes;
}
} catch (err: any) {
console.error('Error fetching filter tag:', err);
}
}
try {
let results: PostListRow[];
try {
const queryRes = await db
.prepare(
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author,
c.name as category, s.name as series
FROM posts p
LEFT JOIN users u ON p.author_id = u.id
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN series s ON p.series_id = s.id
ORDER BY p.published_at DESC`
)
.all<PostListRow>();
results = queryRes.results;
if (filterTag) {
const queryRes = await db
.prepare(
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author,
c.name as category, s.name as series
FROM posts p
INNER JOIN post_tags pt ON p.id = pt.post_id
LEFT JOIN users u ON p.author_id = u.id
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN series s ON p.series_id = s.id
WHERE pt.tag_id = ?
ORDER BY p.published_at DESC`
)
.bind(filterTag.id)
.all<PostListRow>();
results = queryRes.results;
} else {
const queryRes = await db
.prepare(
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author,
c.name as category, s.name as series
FROM posts p
LEFT JOIN users u ON p.author_id = u.id
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN series s ON p.series_id = s.id
ORDER BY p.published_at DESC`
)
.all<PostListRow>();
results = queryRes.results;
}
} catch (err: any) {
// Fallback query if categories table or category_id column is missing
const queryRes = await db
.prepare(
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author
FROM posts p
LEFT JOIN users u ON p.author_id = u.id
ORDER BY p.published_at DESC`
)
.all<PostListRow>();
results = queryRes.results.map((r) => ({ ...r, category: '', series: '' }));
if (filterTag) {
const queryRes = await db
.prepare(
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author
FROM posts p
INNER JOIN post_tags pt ON p.id = pt.post_id
LEFT JOIN users u ON p.author_id = u.id
WHERE pt.tag_id = ?
ORDER BY p.published_at DESC`
)
.bind(filterTag.id)
.all<PostListRow>();
results = queryRes.results.map((r) => ({ ...r, category: '', series: '' }));
} else {
const queryRes = await db
.prepare(
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author
FROM posts p
LEFT JOIN users u ON p.author_id = u.id
ORDER BY p.published_at DESC`
)
.all<PostListRow>();
results = queryRes.results.map((r) => ({ ...r, category: '', series: '' }));
}
}
// Format date for display
@@ -62,11 +114,12 @@ export const load: PageServerLoad = async ({ platform }) => {
});
return {
posts: formattedPosts
posts: formattedPosts,
filterTag
};
} catch (err: any) {
console.error('Error fetching admin posts list:', err);
return { posts: [] };
return { posts: [], filterTag };
}
};
+50 -1
View File
@@ -28,6 +28,17 @@
<Button href={resolve('/admin/posts/new')} variant="primary">撰写</Button>
</div>
{#if data.filterTag}
<div class="filter-banner">
<div class="filter-info">
<span>正在筛选{$page.data.aliases?.tag || '标签'}:</span>
<span class="filter-tag-badge">#{data.filterTag.name}</span>
<span class="filter-count">(共 {data.posts.length} 篇)</span>
</div>
<Button href={resolve('/admin/posts')} variant="action" size="sm">清除筛选</Button>
</div>
{/if}
{#if data.posts.length > 0}
<div class="batch-actions">
{#if selectedIds.length > 0}
@@ -66,7 +77,17 @@
{#if data.posts.length === 0}
<div class="empty-state">
<p>还没有发布任何{$page.data.aliases?.post || '文章'}</p>
{#if data.filterTag}
<p>
{$page.data.aliases?.tag || '标签'} "{data.filterTag.name}" 下暂无关联{$page.data.aliases
?.post || '文章'}
</p>
<Button href={resolve('/admin/posts')} variant="action" size="sm"
>查看所有{$page.data.aliases?.post || '文章'}</Button
>
{:else}
<p>还没有发布任何{$page.data.aliases?.post || '文章'}</p>
{/if}
</div>
{:else}
<DataTable>
@@ -197,4 +218,32 @@
text-decoration: underline;
text-underline-offset: 4px;
}
.filter-banner {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 10px 16px;
background: rgba(0, 0, 0, 0.03);
border-left: 3px solid var(--accent-color, #419b38);
}
.filter-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
color: var(--text-color);
}
.filter-tag-badge {
font-weight: 600;
color: var(--accent-color, #419b38);
}
.filter-count {
color: var(--text-muted, rgba(0, 0, 0, 0.6));
font-size: 0.85rem;
}
</style>
+31 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import type { PageData, ActionData } from './$types';
import { confirmState } from '$lib/stores/confirm.svelte';
@@ -76,7 +77,19 @@
<strong>{tag.name}</strong>
{/if}
</td>
<td><span class="badge">{tag.post_count}</span></td>
<td>
{#if tag.post_count > 0}
<a
href="{resolve('/admin/posts')}?tagId={tag.id}"
class="badge badge-link"
title={`查看关联的 ${tag.post_count} 篇${$page.data.aliases?.post || '文章'}`}
>
{tag.post_count}
</a>
{:else}
<span class="badge badge-empty">{tag.post_count}</span>
{/if}
</td>
<td>
{#if editingId === tag.id}
<form
@@ -201,4 +214,21 @@
text-align: center;
color: var(--text-muted, rgba(0, 0, 0, 0.6));
}
.badge-link {
color: var(--accent-color, #419b38);
text-decoration: none;
cursor: pointer;
font-weight: 600;
transition: all 0.2s ease;
}
.badge-link:hover {
background-color: var(--accent-color, #419b38);
color: #ffffff;
}
.badge-empty {
color: var(--text-muted, rgba(0, 0, 0, 0.4));
}
</style>