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}
+
+
+
+ | ID |
+ 标题 |
+ 分类 |
+ 别名 (Slug) |
+ 作者 |
+ 发布时间 |
+ 操作 |
+
+
+
+ {#each data.posts as post}
+
+ | {post.id} |
+
+ {post.title}
+
+
+
+ |
+ {post.category || '未分类'} |
+ {post.slug} |
+ {post.author || '未知'} |
+ {post.dateStr} |
+
+ 编辑
+
+ |
+
+ {/each}
+
+
+ {/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}
+
+
+
+
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}
+
+
+
+