From 5bb5e56b89d26219026eac55eccbb245957645d3 Mon Sep 17 00:00:00 2001 From: seaHi Date: Wed, 29 Jul 2026 10:49:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=E5=8F=8A=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin/files/+page.server.ts | 123 ++++++++ src/routes/admin/files/+page.svelte | 316 ++++++++++++++++++++ src/routes/admin/pages/+page.server.ts | 70 +++++ src/routes/admin/pages/+page.svelte | 247 +++++++++++++++ src/routes/admin/pages/[id]/+page.server.ts | 63 ++++ src/routes/admin/pages/[id]/+page.svelte | 309 +++++++++++++++++++ src/routes/admin/pages/new/+page.server.ts | 37 +++ src/routes/admin/pages/new/+page.svelte | 312 +++++++++++++++++++ 8 files changed, 1477 insertions(+) create mode 100644 src/routes/admin/files/+page.server.ts create mode 100644 src/routes/admin/files/+page.svelte create mode 100644 src/routes/admin/pages/+page.server.ts create mode 100644 src/routes/admin/pages/+page.svelte create mode 100644 src/routes/admin/pages/[id]/+page.server.ts create mode 100644 src/routes/admin/pages/[id]/+page.svelte create mode 100644 src/routes/admin/pages/new/+page.server.ts create mode 100644 src/routes/admin/pages/new/+page.svelte diff --git a/src/routes/admin/files/+page.server.ts b/src/routes/admin/files/+page.server.ts new file mode 100644 index 0000000..0064aa8 --- /dev/null +++ b/src/routes/admin/files/+page.server.ts @@ -0,0 +1,123 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + +async function ensureAttachmentsTable(db: any) { + try { + await db.prepare(` + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + key TEXT NOT NULL UNIQUE, + mime_type TEXT, + size INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `).run(); + } catch (e) { + console.error('Failed to ensure attachments table:', e); + } +} + +export const load: PageServerLoad = async ({ platform }) => { + const db = platform?.env?.DB; + + if (!db) { + return { files: [], r2Domain: '' }; + } + + try { + // Get R2 domain setting + const domainRes = await db.prepare('SELECT value FROM settings WHERE key = ?').bind('r2.public_domain').first(); + const r2Domain = domainRes?.value || 'https://assets.yourdomain.com'; + + const { results } = await db.prepare( + `SELECT id, filename, key, mime_type, size, created_at + FROM attachments + ORDER BY created_at DESC` + ).all(); + + return { + files: results as { id: number; filename: string; key: string; mime_type: string; size: number; created_at: string }[], + r2Domain + }; + } catch (err: any) { + if (err?.message?.includes('no such table')) { + await ensureAttachmentsTable(db); + return { files: [], r2Domain: 'https://assets.yourdomain.com' }; + } + console.error('Error fetching attachments:', err); + return { files: [], r2Domain: 'https://assets.yourdomain.com' }; + } +}; + +export const actions: Actions = { + upload: async ({ request, platform }) => { + const db = platform?.env?.DB; + const r2 = platform?.env?.R2; + + if (!db || !r2) return fail(500, { error: '数据库或 R2 存储未连接' }); + + const formData = await request.formData(); + const file = formData.get('file') as File; + + if (!file || !(file instanceof File)) { + return fail(400, { error: '没有提供有效的文件' }); + } + + // Generate unique key (timestamp + random string + original name) + const ext = file.name.split('.').pop() || ''; + const randomStr = Math.random().toString(36).substring(2, 8); + const key = `${Date.now()}-${randomStr}.${ext}`; + + try { + // Upload to R2 + await r2.put(key, file.stream(), { + httpMetadata: { contentType: file.type } + }); + + // Save metadata to DB + await ensureAttachmentsTable(db); // Defensive checking + await db.prepare( + 'INSERT INTO attachments (filename, key, mime_type, size) VALUES (?, ?, ?, ?)' + ).bind(file.name, key, file.type, file.size).run(); + + return { success: true }; + } catch (err: any) { + console.error('Error uploading file:', err); + return fail(500, { error: '文件上传失败: ' + err.message }); + } + }, + + delete: async ({ request, platform }) => { + const db = platform?.env?.DB; + const r2 = platform?.env?.R2; + + if (!db || !r2) return fail(500, { error: '数据库或 R2 存储未连接' }); + + const formData = await request.formData(); + const id = formData.get('id'); + + if (!id) return fail(400, { error: '缺少文件 ID' }); + + try { + // Get key first + const fileRes = await db.prepare('SELECT key FROM attachments WHERE id = ?').bind(id).first(); + if (!fileRes) { + return fail(404, { error: '文件未找到' }); + } + + const key = fileRes.key as string; + + // Delete from R2 + await r2.delete(key); + + // Delete from DB + await db.prepare('DELETE FROM attachments WHERE id = ?').bind(id).run(); + + return { success: true }; + } catch (err: any) { + console.error('Error deleting file:', err); + return fail(500, { error: '删除文件失败' }); + } + } +}; diff --git a/src/routes/admin/files/+page.svelte b/src/routes/admin/files/+page.svelte new file mode 100644 index 0000000..81285a3 --- /dev/null +++ b/src/routes/admin/files/+page.svelte @@ -0,0 +1,316 @@ + + + + 文件管理 - BlogFlare Admin + + +
+

文件管理

+
+ +{#if form?.error} +
{form.error}
+{/if} + +{#if form?.success} +
操作成功!
+{/if} + +
+
{ + isUploading = true; + return async ({ update }) => { + isUploading = false; + await update(); + }; + }}> + +
+
+ +
+ {#each data.files as file} + {@const url = `${data.r2Domain}/${file.key}`} + {@const isImage = file.mime_type?.startsWith('image/')} +
+
+ {#if isImage} + {file.filename} + {:else} +
+ + + + +
+ {/if} +
+ +
+
{file.filename}
+
+ {formatBytes(file.size)} + {new Date(file.created_at).toLocaleDateString()} +
+
+ +
+ + +
{ + return async ({ update }) => { + await update(); + }; + }}> + + +
+
+
+ {/each} + + {#if data.files.length === 0} +
+ 暂无文件,点击上方按钮上传 +
+ {/if} +
+ + diff --git a/src/routes/admin/pages/+page.server.ts b/src/routes/admin/pages/+page.server.ts new file mode 100644 index 0000000..cb6d1a7 --- /dev/null +++ b/src/routes/admin/pages/+page.server.ts @@ -0,0 +1,70 @@ +import type { PageServerLoad } from './$types'; + +interface PageListRow { + id: number; + title: string; + slug: string; + date: string; + author: string | null; +} +export const load: PageServerLoad = async ({ platform }) => { + const db = platform?.env?.DB; + + if (!db) { + return { + pages: [] + }; + } + + try { + const queryRes = await db.prepare( + `SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author + FROM pages p + LEFT JOIN users u ON p.author_id = u.id + ORDER BY p.published_at DESC` + ).all(); + + const results = queryRes.results; + + // Format date for display + const formattedPages = results.map((page) => { + const d = new Date(page.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 { + ...page, + dateStr: `${year}-${month}-${day}` + }; + }); + + return { + pages: formattedPages + }; + } catch (err) { + console.error('Error fetching admin pages list:', err); + return { pages: [] }; + } +}; + +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 ids = data.getAll('ids'); + + if (!ids || ids.length === 0) return { success: false, error: 'Missing IDs' }; + + try { + const placeholders = ids.map(() => '?').join(','); + await db.prepare(`DELETE FROM pages WHERE id IN (${placeholders})`).bind(...ids).run(); + return { success: true }; + } catch (err) { + console.error('Error deleting page:', err); + return { success: false, error: 'Failed to delete page' }; + } + } +}; diff --git a/src/routes/admin/pages/+page.svelte b/src/routes/admin/pages/+page.svelte new file mode 100644 index 0000000..83bc00f --- /dev/null +++ b/src/routes/admin/pages/+page.svelte @@ -0,0 +1,247 @@ + + +
+

{$page.data.aliases?.page || '页面'}管理

+ 新建{$page.data.aliases?.page || '页面'} +
+ + + {#if data.pages.length > 0} +
+ {#if selectedIds.length > 0} + 已选择 {selectedIds.length} 项 +
{ + if(!confirm(`确定要删除选中的 ${selectedIds.length} 篇${$page.data.aliases?.page || '项目'}吗?操作不可恢复。`)) { + e.preventDefault(); + } + }}> + {#each selectedIds as id} + + {/each} + +
+ {:else} + 未选择项目 + {/if} +
+ {/if} + +
+ {#if data.pages.length === 0} +
+

还没有发布任何{$page.data.aliases?.page || '页面'},现在就开始写作吧!

+
+ {:else} + + + + + + + + + + + + + + {#each data.pages as pageItem} + + + + + + + + + + {/each} + +
+ + ID标题别名 (Slug)作者发布时间
+ + {pageItem.id} + {pageItem.title} + + + + {pageItem.slug}{pageItem.author || '未知'}{pageItem.dateStr}
+ {/if} +
+ + diff --git a/src/routes/admin/pages/[id]/+page.server.ts b/src/routes/admin/pages/[id]/+page.server.ts new file mode 100644 index 0000000..b11d91b --- /dev/null +++ b/src/routes/admin/pages/[id]/+page.server.ts @@ -0,0 +1,63 @@ +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 pageId = params.id; + + if (!db) { + throw error(500, '数据库未连接'); + } + + try { + const page = await db.prepare( + 'SELECT id, title, slug, content FROM pages WHERE id = ?' + ).bind(pageId).first<{ id: number; title: string; slug: string; content: string; }>(); + + if (!page) { + throw error(404, '页面不存在'); + } + + return { + page + }; + } catch (err: any) { + if (err?.status === 404) throw err; + console.error('Error loading page for edit:', err); + throw error(500, '读取页面详情失败'); + } +}; + +export const actions: Actions = { + update: async ({ params, request, platform }) => { + const db = platform?.env?.DB; + const pageId = 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 content = formData.get('content')?.toString() || ''; + + if (!title || !slug || !content) { + return fail(400, { error: '标题、URL 别名和正文为必填项' }); + } + + try { + await db.prepare( + `UPDATE pages + SET title = ?, slug = ?, content = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?` + ).bind(title, slug, content, pageId).run(); + } catch (err: any) { + console.error('Error updating page:', err); + if (err?.message?.includes('UNIQUE constraint failed')) { + return fail(400, { error: 'URL 别名 (Slug) 已被其他页面占用' }); + } + return fail(500, { error: '保存修改失败' }); + } + + throw redirect(303, '/admin/pages'); + } +}; diff --git a/src/routes/admin/pages/[id]/+page.svelte b/src/routes/admin/pages/[id]/+page.svelte new file mode 100644 index 0000000..9a9bde4 --- /dev/null +++ b/src/routes/admin/pages/[id]/+page.svelte @@ -0,0 +1,309 @@ + + +
+

编辑{$page.data.aliases?.page || '页面'}

+ +
+ +{#if form?.error} +
+ {form.error} +
+{/if} + +
+
+
+
+ + +
+ +
+
+ +
+ + +
+
+ +
+ {#if mode === 'edit'} + + {:else} +
+ +
+ {/if} +
+
+
+ +
+ + + +
+
+
+ + diff --git a/src/routes/admin/pages/new/+page.server.ts b/src/routes/admin/pages/new/+page.server.ts new file mode 100644 index 0000000..9b1b6bf --- /dev/null +++ b/src/routes/admin/pages/new/+page.server.ts @@ -0,0 +1,37 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + return {}; +}; + +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 content = formData.get('content')?.toString() || ''; + + if (!title || !slug || !content) { + return fail(400, { error: '标题、URL 别名和正文为必填项' }); + } + + try { + await db.prepare( + `INSERT INTO pages (title, slug, content, author_id, published_at, updated_at) + VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)` + ).bind(title, slug, content).run(); + } catch (err: any) { + console.error('Error creating page:', err); + if (err?.message?.includes('UNIQUE constraint failed')) { + return fail(400, { error: 'URL 别名 (Slug) 已被占用,请使用其他别名' }); + } + return fail(500, { error: '保存页面失败' }); + } + + throw redirect(303, '/admin/pages'); + } +}; diff --git a/src/routes/admin/pages/new/+page.svelte b/src/routes/admin/pages/new/+page.svelte new file mode 100644 index 0000000..2ca3e98 --- /dev/null +++ b/src/routes/admin/pages/new/+page.svelte @@ -0,0 +1,312 @@ + + +
+

新建独立{$page.data.aliases?.page || '页面'}

+ ← 返回 +
+ +{#if form?.error} +
+ {form.error} +
+{/if} + +
+
+
+
+ + +
+ +
+
+ +
+ + +
+
+ +
+ {#if mode === 'edit'} + + {:else} +
+ +
+ {/if} +
+
+
+ +
+ + + +
+
+
+ +