From b4b2907d77168773aa3d0c01842d7a8de9b20107 Mon Sep 17 00:00:00 2001 From: seaHi Date: Sun, 26 Jul 2026 17:52:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E3=80=81=E7=BC=96=E8=BE=91=E4=B8=8E=E5=88=A0?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin/posts/+page.server.ts | 74 +++++ src/routes/admin/posts/+page.svelte | 194 +++++++++++ src/routes/admin/posts/[id]/+page.server.ts | 85 +++++ src/routes/admin/posts/[id]/+page.svelte | 350 ++++++++++++++++++++ src/routes/admin/posts/new/+page.server.ts | 64 ++++ src/routes/admin/posts/new/+page.svelte | 333 +++++++++++++++++++ 6 files changed, 1100 insertions(+) create mode 100644 src/routes/admin/posts/+page.server.ts create mode 100644 src/routes/admin/posts/+page.svelte create mode 100644 src/routes/admin/posts/[id]/+page.server.ts create mode 100644 src/routes/admin/posts/[id]/+page.svelte create mode 100644 src/routes/admin/posts/new/+page.server.ts create mode 100644 src/routes/admin/posts/new/+page.svelte diff --git a/src/routes/admin/posts/+page.server.ts b/src/routes/admin/posts/+page.server.ts new file mode 100644 index 0000000..ee073ff --- /dev/null +++ b/src/routes/admin/posts/+page.server.ts @@ -0,0 +1,74 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ platform }) => { + const db = platform?.env?.DB; + + if (!db) { + return { + posts: [] + }; + } + + try { + let results; + 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 + FROM posts p + LEFT JOIN users u ON p.author_id = u.id + LEFT JOIN categories c ON p.category_id = c.id + ORDER BY p.published_at DESC` + ).all(); + results = queryRes.results; + } catch (e) { + // 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(); + results = queryRes.results.map(r => ({ ...r, category: '未分类' })); + } + + // Format date for display + const formattedPosts = results.map((post) => { + const d = new Date(post.date as string); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + + return { + ...post, + dateStr: `${year}-${month}-${day}` + }; + }); + + return { + posts: formattedPosts + }; + } catch (err) { + console.error('Error fetching admin posts list:', err); + return { posts: [] }; + } +}; + +export const actions = { + delete: async ({ request, platform }) => { + const db = platform?.env?.DB; + if (!db) return { success: false, error: 'Database not found' }; + + const data = await request.formData(); + const id = data.get('id'); + + if (!id) return { success: false, error: 'Missing ID' }; + + try { + await db.prepare('DELETE FROM posts WHERE id = ?').bind(id).run(); + return { success: true }; + } catch (err) { + console.error('Error deleting post:', err); + return { success: false, error: 'Failed to delete post' }; + } + } +}; diff --git a/src/routes/admin/posts/+page.svelte b/src/routes/admin/posts/+page.svelte new file mode 100644 index 0000000..780e39f --- /dev/null +++ b/src/routes/admin/posts/+page.svelte @@ -0,0 +1,194 @@ + + +
+

文章管理

+ 发布新文章 +
+ +
+ {#if data.posts.length === 0} +
+

还没有发布任何文章,现在就开始写作吧!

+
+ {:else} + + + + + + + + + + + + + + {#each data.posts as post} + + + + + + + + + + {/each} + +
ID标题分类别名 (Slug)作者发布时间操作
{post.id} + {post.title} + + + + {post.category || '未分类'}{post.slug}{post.author || '未知'}{post.dateStr} + 编辑 +
{ + if(confirm('确定要删除这篇文章吗?操作不可恢复。')) e.currentTarget.submit(); + }}> + + +
+
+ {/if} +
+ + diff --git a/src/routes/admin/posts/[id]/+page.server.ts b/src/routes/admin/posts/[id]/+page.server.ts new file mode 100644 index 0000000..95c97bf --- /dev/null +++ b/src/routes/admin/posts/[id]/+page.server.ts @@ -0,0 +1,85 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params, platform }) => { + const db = platform?.env?.DB; + const postId = params.id; + + if (!db) { + throw error(500, '数据库未连接'); + } + + try { + const post = await db.prepare( + 'SELECT id, title, slug, excerpt, content, category_id FROM posts WHERE id = ?' + ).bind(postId).first<{ id: number; title: string; slug: string; excerpt: string; content: string; category_id: number | null }>(); + + if (!post) { + throw error(404, '文章不存在'); + } + + 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: 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); + + return { + post, + tags: allTags as unknown as { id: number; name: string }[], + categories: allCategories as unknown as { id: number; name: string }[], + selectedTagIds + }; + } catch (err: any) { + if (err?.status === 404) throw err; + console.error('Error loading post for edit:', err); + throw error(500, '读取文章详情失败'); + } +}; + +export const actions: Actions = { + update: async ({ params, request, platform }) => { + const db = platform?.env?.DB; + const postId = params.id; + + if (!db) return fail(500, { error: '数据库未连接' }); + + const formData = await request.formData(); + const title = formData.get('title')?.toString().trim(); + const slug = formData.get('slug')?.toString().trim(); + const excerpt = formData.get('excerpt')?.toString().trim() || ''; + const content = formData.get('content')?.toString() || ''; + const categoryId = formData.get('category_id') ? Number(formData.get('category_id')) : null; + const selectedTags = formData.getAll('tags').map(t => Number(t)); + + if (!title || !slug || !content) { + return fail(400, { error: '标题、URL 别名和文章正文为必填项' }); + } + + try { + // 更新文章表 + await db.prepare( + `UPDATE posts + SET title = ?, slug = ?, excerpt = ?, content = ?, category_id = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?` + ).bind(title, slug, excerpt, content, categoryId, postId).run(); + + // 清除原有标签并重新绑定 + await db.prepare('DELETE FROM post_tags WHERE post_id = ?').bind(postId).run(); + + if (selectedTags.length > 0) { + for (const tagId of selectedTags) { + await db.prepare('INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)').bind(postId, tagId).run(); + } + } + } catch (err: any) { + console.error('Error updating post:', err); + if (err?.message?.includes('UNIQUE constraint failed')) { + return fail(400, { error: 'URL 别名 (Slug) 已被其他文章占用' }); + } + return fail(500, { error: '保存修改失败' }); + } + + throw redirect(303, '/admin/posts'); + } +}; diff --git a/src/routes/admin/posts/[id]/+page.svelte b/src/routes/admin/posts/[id]/+page.svelte new file mode 100644 index 0000000..72fbd61 --- /dev/null +++ b/src/routes/admin/posts/[id]/+page.svelte @@ -0,0 +1,350 @@ + + +
+

编辑

+ ← 返回 +
+ +{#if form?.error} +
+ {form.error} +
+{/if} + +{#if form?.success} +
+ 文章更新成功! + 预览文章 ↗ +
+{/if} + +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + + + + + {#if data.tags.length > 0} + + {/if} +
+
+
+ + diff --git a/src/routes/admin/posts/new/+page.server.ts b/src/routes/admin/posts/new/+page.server.ts new file mode 100644 index 0000000..053e181 --- /dev/null +++ b/src/routes/admin/posts/new/+page.server.ts @@ -0,0 +1,64 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ platform }) => { + const db = platform?.env?.DB; + let tags: { id: number; name: string }[] = []; + let categories: { id: number; name: string }[] = []; + + if (db) { + try { + const { results: tagResults } = await db.prepare('SELECT id, name FROM tags ORDER BY name ASC').all(); + const { results: catResults } = await db.prepare('SELECT id, name FROM categories ORDER BY name ASC').all(); + + tags = tagResults as unknown as { id: number; name: string }[]; + categories = catResults as unknown as { id: number; name: string }[]; + } catch (err) { + console.error('Error fetching tags or categories:', err); + } + } + return { tags, categories }; +}; + +export const actions: Actions = { + default: async ({ request, platform }) => { + const db = platform?.env?.DB; + if (!db) return fail(500, { error: '数据库未连接' }); + + const formData = await request.formData(); + const title = formData.get('title')?.toString().trim(); + const slug = formData.get('slug')?.toString().trim(); + const excerpt = formData.get('excerpt')?.toString().trim() || ''; + const content = formData.get('content')?.toString() || ''; + const categoryId = formData.get('category_id') ? Number(formData.get('category_id')) : null; + const selectedTags = formData.getAll('tags').map(t => Number(t)); + + if (!title || !slug || !content) { + return fail(400, { error: '标题、URL 别名和文章正文为必填项' }); + } + + try { + const res = await db.prepare( + `INSERT INTO posts (title, slug, excerpt, content, author_id, category_id, published_at, updated_at) + VALUES (?, ?, ?, ?, 1, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + RETURNING id` + ).bind(title, slug, excerpt, content, categoryId).first<{ id: number }>(); + + const postId = res?.id; + + if (postId && selectedTags.length > 0) { + for (const tagId of selectedTags) { + await db.prepare('INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)').bind(postId, tagId).run(); + } + } + } catch (err: any) { + console.error('Error creating post:', err); + if (err?.message?.includes('UNIQUE constraint failed')) { + return fail(400, { error: 'URL 别名 (Slug) 已被占用,请使用其他别名' }); + } + return fail(500, { error: '保存文章失败' }); + } + + throw redirect(303, '/admin/posts'); + } +}; diff --git a/src/routes/admin/posts/new/+page.svelte b/src/routes/admin/posts/new/+page.svelte new file mode 100644 index 0000000..628304f --- /dev/null +++ b/src/routes/admin/posts/new/+page.svelte @@ -0,0 +1,333 @@ + + +
+

新文章

+ ← 返回 +
+ +{#if form?.error} +
+ {form.error} +
+{/if} + +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + + + + + {#if data.tags.length > 0} + + {/if} +
+
+
+ +