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; category?: string;
series?: string; series?: string;
} }
export const load: PageServerLoad = async ({ platform }) => { export const load: PageServerLoad = async ({ url, platform }) => {
const db = platform?.env?.DB; const db = platform?.env?.DB;
if (!db) { if (!db) {
return { 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 { try {
let results: PostListRow[]; let results: PostListRow[];
try { try {
const queryRes = await db if (filterTag) {
.prepare( const queryRes = await db
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author, .prepare(
c.name as category, s.name as series `SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author,
FROM posts p c.name as category, s.name as series
LEFT JOIN users u ON p.author_id = u.id FROM posts p
LEFT JOIN categories c ON p.category_id = c.id INNER JOIN post_tags pt ON p.id = pt.post_id
LEFT JOIN series s ON p.series_id = s.id LEFT JOIN users u ON p.author_id = u.id
ORDER BY p.published_at DESC` LEFT JOIN categories c ON p.category_id = c.id
) LEFT JOIN series s ON p.series_id = s.id
.all<PostListRow>(); WHERE pt.tag_id = ?
results = queryRes.results; 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) { } catch (err: any) {
// Fallback query if categories table or category_id column is missing // Fallback query if categories table or category_id column is missing
const queryRes = await db if (filterTag) {
.prepare( const queryRes = await db
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author .prepare(
FROM posts p `SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author
LEFT JOIN users u ON p.author_id = u.id FROM posts p
ORDER BY p.published_at DESC` INNER JOIN post_tags pt ON p.id = pt.post_id
) LEFT JOIN users u ON p.author_id = u.id
.all<PostListRow>(); WHERE pt.tag_id = ?
results = queryRes.results.map((r) => ({ ...r, category: '', series: '' })); 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 // Format date for display
@@ -62,11 +114,12 @@ export const load: PageServerLoad = async ({ platform }) => {
}); });
return { return {
posts: formattedPosts posts: formattedPosts,
filterTag
}; };
} catch (err: any) { } catch (err: any) {
console.error('Error fetching admin posts list:', err); 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> <Button href={resolve('/admin/posts/new')} variant="primary">撰写</Button>
</div> </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} {#if data.posts.length > 0}
<div class="batch-actions"> <div class="batch-actions">
{#if selectedIds.length > 0} {#if selectedIds.length > 0}
@@ -66,7 +77,17 @@
{#if data.posts.length === 0} {#if data.posts.length === 0}
<div class="empty-state"> <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> </div>
{:else} {:else}
<DataTable> <DataTable>
@@ -197,4 +218,32 @@
text-decoration: underline; text-decoration: underline;
text-underline-offset: 4px; 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> </style>
+31 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/stores'; import { page } from '$app/stores';
import type { PageData, ActionData } from './$types'; import type { PageData, ActionData } from './$types';
import { confirmState } from '$lib/stores/confirm.svelte'; import { confirmState } from '$lib/stores/confirm.svelte';
@@ -76,7 +77,19 @@
<strong>{tag.name}</strong> <strong>{tag.name}</strong>
{/if} {/if}
</td> </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> <td>
{#if editingId === tag.id} {#if editingId === tag.id}
<form <form
@@ -201,4 +214,21 @@
text-align: center; text-align: center;
color: var(--text-muted, rgba(0, 0, 0, 0.6)); 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> </style>