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}
+
+
+
+
+ {#each data.files as file}
+ {@const url = `${data.r2Domain}/${file.key}`}
+ {@const isImage = file.mime_type?.startsWith('image/')}
+
+
+ {#if isImage}
+

+ {:else}
+
+ {/if}
+
+
+
+
{file.filename}
+
+ {formatBytes(file.size)}
+ {new Date(file.created_at).toLocaleDateString()}
+
+
+
+
+
+
+
+
+
+ {/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 @@
+
+
+
+
+
+ {#if data.pages.length > 0}
+
+ {#if selectedIds.length > 0}
+ 已选择 {selectedIds.length} 项
+
+ {:else}
+ 未选择项目
+ {/if}
+
+ {/if}
+
+
+ {#if data.pages.length === 0}
+
+
还没有发布任何{$page.data.aliases?.page || '页面'},现在就开始写作吧!
+
+ {:else}
+
+ {/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 @@
+
+
+
+
+{#if form?.error}
+
+ {form.error}
+
+{/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 @@
+
+
+
+
+{#if form?.error}
+
+ {form.error}
+
+{/if}
+
+
+
+