From 495e453237933838bfbb2203687d3df1b6963af6 Mon Sep 17 00:00:00 2001 From: seaHi Date: Thu, 6 Aug 2026 12:14:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9C=A8=E6=96=87=E7=AB=A0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=B8=AD=E6=94=AF=E6=8C=81=E5=85=B3=E8=81=94=E7=B3=BB?= =?UTF-8?q?=E5=88=97=E5=8F=8A=E8=AE=BE=E7=BD=AE=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin/posts/+page.server.ts | 7 ++- src/routes/admin/posts/+page.svelte | 4 +- src/routes/admin/posts/[id]/+page.server.ts | 49 +++++++++++++----- src/routes/admin/posts/[id]/+page.svelte | 57 ++++++++++++++++++++- src/routes/admin/posts/new/+page.server.ts | 35 ++++++++++--- src/routes/admin/posts/new/+page.svelte | 57 ++++++++++++++++++++- 6 files changed, 185 insertions(+), 24 deletions(-) diff --git a/src/routes/admin/posts/+page.server.ts b/src/routes/admin/posts/+page.server.ts index 192b038..0abb5fd 100644 --- a/src/routes/admin/posts/+page.server.ts +++ b/src/routes/admin/posts/+page.server.ts @@ -7,6 +7,7 @@ interface PostListRow { date: string; author: string | null; category?: string; + series?: string; } export const load: PageServerLoad = async ({ platform }) => { const db = platform?.env?.DB; @@ -21,10 +22,12 @@ export const load: PageServerLoad = async ({ platform }) => { 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 + `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(); results = queryRes.results; @@ -36,7 +39,7 @@ export const load: PageServerLoad = async ({ platform }) => { LEFT JOIN users u ON p.author_id = u.id ORDER BY p.published_at DESC` ).all(); - results = queryRes.results.map(r => ({ ...r, category: '未分类' })); + results = queryRes.results.map(r => ({ ...r, category: '未分类', series: '' })); } // Format date for display diff --git a/src/routes/admin/posts/+page.svelte b/src/routes/admin/posts/+page.svelte index 70c39ca..74fe789 100644 --- a/src/routes/admin/posts/+page.svelte +++ b/src/routes/admin/posts/+page.svelte @@ -58,6 +58,7 @@ ID 标题 分类 + {$page.data.aliases?.series || '系列'} 别名 (Slug) 作者 发布时间 @@ -77,7 +78,8 @@ - {post.category || '未分类'} + {post.category || '未分类'} + {post.series || '—'} {post.slug} {post.author || '未知'} {post.dateStr} diff --git a/src/routes/admin/posts/[id]/+page.server.ts b/src/routes/admin/posts/[id]/+page.server.ts index 1c0170b..bd47b07 100644 --- a/src/routes/admin/posts/[id]/+page.server.ts +++ b/src/routes/admin/posts/[id]/+page.server.ts @@ -1,5 +1,11 @@ -import { error, fail, redirect } from '@sveltejs/kit'; +import { error, fail, isHttpError, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; +import { + parseSeriesId, + parseSeriesWeight, + resolveSeriesWeight, + SeriesValidationError +} from '$lib/server/series'; export const load: PageServerLoad = async ({ params, platform }) => { const db = platform?.env?.DB; @@ -11,8 +17,8 @@ export const load: PageServerLoad = async ({ params, platform }) => { try { const post = await db.prepare( - 'SELECT id, title, slug, content, cover, category_id, published_at, updated_at FROM posts WHERE id = ?' - ).bind(postId).first<{ id: number; title: string; slug: string; content: string; cover: string | null; category_id: number | null; published_at?: string; updated_at?: string }>(); + 'SELECT id, title, slug, content, cover, category_id, series_id, series_weight, published_at, updated_at FROM posts WHERE id = ?' + ).bind(postId).first<{ id: number; title: string; slug: string; content: string; cover: string | null; category_id: number | null; series_id: number | null; series_weight: number | null; published_at?: string; updated_at?: string }>(); if (!post) { throw error(404, '文章不存在'); @@ -20,18 +26,20 @@ export const load: PageServerLoad = async ({ params, platform }) => { const { results: allTags } = await db.prepare('SELECT id, name FROM tags ORDER BY name ASC').all(); const { results: allCategories } = await db.prepare('SELECT id, name FROM categories ORDER BY name ASC').all(); + const { results: allSeries } = await db.prepare('SELECT id, name, slug FROM series ORDER BY name COLLATE NOCASE ASC').all(); - const { results: selectedTagRows } = await db.prepare('SELECT tag_id FROM post_tags WHERE post_id = ?').bind(postId).all(); - const selectedTagIds = selectedTagRows.map((r: any) => r.tag_id as number); + const { results: selectedTagRows } = await db.prepare('SELECT tag_id FROM post_tags WHERE post_id = ?').bind(postId).all<{ tag_id: number }>(); + const selectedTagIds = selectedTagRows.map((row) => row.tag_id); return { post, tags: allTags as unknown as { id: number; name: string }[], categories: allCategories as unknown as { id: number; name: string }[], + series: allSeries as unknown as { id: number; name: string; slug: string }[], selectedTagIds }; - } catch (err: any) { - if (err?.status === 404) throw err; + } catch (err: unknown) { + if (isHttpError(err, 404)) throw err; console.error('Error loading post for edit:', err); throw error(500, '读取文章详情失败'); } @@ -58,20 +66,35 @@ export const actions: Actions = { return fail(400, { error: '标题、URL 别名和文章正文为必填项' }); } + let seriesId: number | null; + let seriesWeight: number | null; + try { + seriesId = parseSeriesId(formData.get('series_id')); + seriesWeight = await resolveSeriesWeight( + db, + seriesId, + parseSeriesWeight(formData.get('series_weight')), + postId + ); + } catch (err) { + if (err instanceof SeriesValidationError) return fail(400, { error: err.message }); + throw err; + } + try { // 更新文章表 if (publishedAt) { await db.prepare( `UPDATE posts - SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, published_at = ?, updated_at = ? + SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, series_id = ?, series_weight = ?, published_at = ?, updated_at = ? WHERE id = ?` - ).bind(title, slug, content, cover, categoryId, publishedAt, updatedAt, postId).run(); + ).bind(title, slug, content, cover, categoryId, seriesId, seriesWeight, publishedAt, updatedAt, postId).run(); } else { await db.prepare( `UPDATE posts - SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, updated_at = ? + SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, series_id = ?, series_weight = ?, updated_at = ? WHERE id = ?` - ).bind(title, slug, content, cover, categoryId, updatedAt, postId).run(); + ).bind(title, slug, content, cover, categoryId, seriesId, seriesWeight, updatedAt, postId).run(); } // 清除原有标签并重新绑定 @@ -82,9 +105,9 @@ export const actions: Actions = { await db.prepare('INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)').bind(postId, tagId).run(); } } - } catch (err: any) { + } catch (err: unknown) { console.error('Error updating post:', err); - if (err?.message?.includes('UNIQUE constraint failed')) { + if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) { return fail(400, { error: 'URL 别名 (Slug) 已被其他文章占用' }); } return fail(500, { error: '保存修改失败' }); diff --git a/src/routes/admin/posts/[id]/+page.svelte b/src/routes/admin/posts/[id]/+page.svelte index e07246d..1b373e6 100644 --- a/src/routes/admin/posts/[id]/+page.svelte +++ b/src/routes/admin/posts/[id]/+page.svelte @@ -16,9 +16,12 @@ let publishedAt = $state(data.post.published_at || ''); let updatedAt = $state(data.post.updated_at || ''); let selectedCategoryId = $state(data.post.category_id ?? ''); + let selectedSeriesId = $state(data.post.series_id ?? ''); + let seriesWeight = $state(data.post.series_weight ?? ''); let fileInput: HTMLInputElement; let selectedTags = $state(data.selectedTagIds || []); let tagsList = $state(data.tags || []); + let seriesList = $state(data.series || []); function toggleTag(tagId: number) { if (selectedTags.includes(tagId)) { @@ -169,7 +172,36 @@ } } - // 2. 标签处理:同步点选或即刻新增入参 + selectedSeriesId = ''; + seriesWeight = imported.series_weight ?? ''; + if (imported.series.length > 0) { + if (imported.series.length > 1) { + alert(`检测到多个系列:${imported.series.join('、')}。BlogFlare 每篇文章只能属于一个系列,本次将选择第一个系列。`); + } + + const seriesName = imported.series[0].trim(); + let matchedSeries = seriesList.find(item => item.name.trim().toLowerCase() === seriesName.toLowerCase()); + if (!matchedSeries && seriesName) { + const formData = new FormData(); + formData.append('name', seriesName); + const response = await fetch('/admin/series?/create', { + method: 'POST', + body: formData, + headers: { 'x-sveltekit-action': 'true' } + }); + const actionResult = deserialize(await response.text()) as any; + if (actionResult.type === 'success' && actionResult.data?.series) { + const createdSeries = actionResult.data.series as { id: number; name: string; slug: string }; + matchedSeries = createdSeries; + seriesList = [...seriesList, createdSeries]; + } else { + alert(actionResult.data?.error || `无法创建系列“${seriesName}”,请在系列管理中检查名称和 URL 别名。`); + } + } + if (matchedSeries) selectedSeriesId = matchedSeries.id; + } + + // 3. 标签处理:同步点选或即刻新增入参 for (const tagName of imported.tags) { const val = tagName.trim(); if (!val) continue; @@ -331,6 +363,25 @@ + +