diff --git a/src/routes/admin/series/+page.server.ts b/src/routes/admin/series/+page.server.ts new file mode 100644 index 0000000..4acd0bd --- /dev/null +++ b/src/routes/admin/series/+page.server.ts @@ -0,0 +1,126 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + +function makeSlug(value: string): string { + return value + .toLowerCase() + .trim() + .replace(/[^\w\u4e00-\u9fa5\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); +} + +function isUniqueConstraintError(err: unknown): boolean { + return err instanceof Error && err.message.includes('UNIQUE constraint failed'); +} + +export const load: PageServerLoad = async ({ platform }) => { + const db = platform?.env?.DB; + if (!db) return { series: [] }; + + try { + const { results } = await db + .prepare( + `SELECT s.id, s.name, s.slug, s.description, COUNT(p.id) AS post_count + FROM series s + LEFT JOIN posts p ON p.series_id = s.id + GROUP BY s.id, s.name, s.slug, s.description + ORDER BY s.name COLLATE NOCASE ASC` + ) + .all(); + + return { + series: results as unknown as Array<{ + id: number; + name: string; + slug: string; + description: string | null; + post_count: number; + }> + }; + } catch (err) { + console.error('Error fetching series:', err); + return { series: [] }; + } +}; + +export const actions: Actions = { + create: async ({ request, platform }) => { + const db = platform?.env?.DB; + if (!db) return fail(500, { error: '数据库未连接' }); + + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim() || ''; + const slug = makeSlug(formData.get('slug')?.toString() || name); + const description = formData.get('description')?.toString().trim() || null; + + if (!name || !slug) return fail(400, { error: '系列名称和 URL 别名不能为空' }); + + try { + const created = await db + .prepare( + 'INSERT INTO series (name, slug, description) VALUES (?, ?, ?) RETURNING id, name, slug, description' + ) + .bind(name, slug, description) + .first(); + return { success: true, series: created }; + } catch (err: unknown) { + console.error('Error creating series:', err); + if (isUniqueConstraintError(err)) { + return fail(400, { error: '系列名称或 URL 别名已存在' }); + } + return fail(500, { error: '创建系列失败' }); + } + }, + + update: async ({ request, platform }) => { + const db = platform?.env?.DB; + if (!db) return fail(500, { error: '数据库未连接' }); + + const formData = await request.formData(); + const id = Number(formData.get('id')); + const name = formData.get('name')?.toString().trim() || ''; + const slug = makeSlug(formData.get('slug')?.toString() || ''); + const description = formData.get('description')?.toString().trim() || null; + + if (!Number.isInteger(id) || id <= 0 || !name || !slug) { + return fail(400, { error: '系列信息不完整' }); + } + + try { + await db + .prepare('UPDATE series SET name = ?, slug = ?, description = ? WHERE id = ?') + .bind(name, slug, description, id) + .run(); + return { success: true }; + } catch (err: unknown) { + console.error('Error updating series:', err); + if (isUniqueConstraintError(err)) { + return fail(400, { error: '系列名称或 URL 别名已存在' }); + } + return fail(500, { error: '更新系列失败' }); + } + }, + + delete: async ({ request, platform }) => { + const db = platform?.env?.DB; + if (!db) return fail(500, { error: '数据库未连接' }); + + const formData = await request.formData(); + const id = Number(formData.get('id')); + if (!Number.isInteger(id) || id <= 0) return fail(400, { error: '系列 ID 无效' }); + + try { + await db.batch([ + db + .prepare('UPDATE posts SET series_id = NULL, series_weight = NULL WHERE series_id = ?') + .bind(id), + db.prepare('DELETE FROM series WHERE id = ?').bind(id) + ]); + return { success: true }; + } catch (err) { + console.error('Error deleting series:', err); + return fail(500, { error: '删除系列失败' }); + } + } +}; diff --git a/src/routes/admin/series/+page.svelte b/src/routes/admin/series/+page.svelte new file mode 100644 index 0000000..55f8d42 --- /dev/null +++ b/src/routes/admin/series/+page.svelte @@ -0,0 +1,241 @@ + + +