diff --git a/src/routes/admin/+layout.server.ts b/src/routes/admin/+layout.server.ts index 6cf1be7..63bf973 100644 --- a/src/routes/admin/+layout.server.ts +++ b/src/routes/admin/+layout.server.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async ({ locals, platform }) => { const db = platform?.env?.DB; - - let aliases = { + + const aliases = { post: '文章', page: '页面', category: '分类', @@ -11,17 +13,17 @@ export const load: LayoutServerLoad = async ({ locals, platform }) => { series: '系列' }; - let site = { + const site = { title: 'BlogFlare', description: 'A high-performance blog powered by Cloudflare D1, R2, and KV.' }; if (db) { try { - const { results } = await db.prepare( - "SELECT key, value FROM settings WHERE key LIKE 'alias.%' OR key LIKE 'site.%'" - ).all(); - + const { results } = await db + .prepare("SELECT key, value FROM settings WHERE key LIKE 'alias.%' OR key LIKE 'site.%'") + .all(); + if (results && results.length > 0) { results.forEach((row: any) => { if (row.key.startsWith('alias.')) { @@ -37,8 +39,8 @@ export const load: LayoutServerLoad = async ({ locals, platform }) => { } }); } - } catch (e) { - console.error('Failed to load settings', e); + } catch (err: any) { + console.error('Failed to load settings', err); } } diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte index d5dad17..9442b39 100644 --- a/src/routes/admin/+layout.svelte +++ b/src/routes/admin/+layout.svelte @@ -1,8 +1,9 @@

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

- 新建{$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 || '项目'}吗?操作不可恢复。`)) { +{#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} - + }} + > + {#each selectedIds as id (id)} + + {/each} + + + {:else} + 未选择项目 + {/if} +
+{/if} +
{#if data.pages.length === 0}
@@ -60,26 +71,52 @@ 别名 (Slug) 作者 发布时间 - - {#each data.pages as pageItem} + {#each data.pages as pageItem (pageItem.id || pageItem)} - + {pageItem.id} - {pageItem.title} - - + {pageItem.title} + + {pageItem.slug} {pageItem.author || '未知'} {pageItem.dateStr} - {/each} @@ -140,9 +177,10 @@ text-align: left; } - .data-table th, .data-table td { + .data-table th, + .data-table td { padding: 10px 16px; - border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.08)); + border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.08)); } .data-table th { @@ -158,7 +196,7 @@ } .data-table tbody tr:hover { - background-color: rgba(0,0,0,0.02); + background-color: rgba(0, 0, 0, 0.02); } .preview-link { @@ -174,12 +212,10 @@ opacity: 1; } - - .empty-state { padding: 50px 20px; text-align: center; - color: var(--text-muted, rgba(0,0,0,0.6)); + color: var(--text-muted, rgba(0, 0, 0, 0.6)); } .batch-actions { @@ -188,7 +224,7 @@ gap: 15px; margin-bottom: 15px; padding: 10px 15px; - background: rgba(0,0,0,0.02); + background: rgba(0, 0, 0, 0.02); height: 54px; box-sizing: border-box; } @@ -200,7 +236,7 @@ } .selected-count.empty { - color: var(--text-muted, rgba(0,0,0,0.4)); + color: var(--text-muted, rgba(0, 0, 0, 0.4)); font-weight: normal; } @@ -209,7 +245,7 @@ text-align: center; } - .checkbox-col input[type="checkbox"] { + .checkbox-col input[type='checkbox'] { width: 16px; height: 16px; accent-color: var(--accent-color, #419b38); @@ -238,10 +274,9 @@ background-color: #d9534f; color: white; } - + .btn-sm { padding: 4px 12px; font-size: 0.85rem; } - diff --git a/src/routes/admin/pages/[id]/+page.server.ts b/src/routes/admin/pages/[id]/+page.server.ts index b11d91b..f6aaec5 100644 --- a/src/routes/admin/pages/[id]/+page.server.ts +++ b/src/routes/admin/pages/[id]/+page.server.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { error, fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; @@ -10,9 +12,10 @@ export const load: PageServerLoad = async ({ params, platform }) => { } 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; }>(); + 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, '页面不存在'); @@ -21,8 +24,8 @@ export const load: PageServerLoad = async ({ params, platform }) => { return { page }; - } catch (err: any) { - if (err?.status === 404) throw err; + } catch (err: unknown) { + if ((err as any)?.status === 404) throw err; console.error('Error loading page for edit:', err); throw error(500, '读取页面详情失败'); } @@ -45,14 +48,17 @@ export const actions: Actions = { } try { - await db.prepare( - `UPDATE pages + await db + .prepare( + `UPDATE pages SET title = ?, slug = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?` - ).bind(title, slug, content, pageId).run(); - } catch (err: any) { + ) + .bind(title, slug, content, pageId) + .run(); + } catch (err: unknown) { console.error('Error updating page:', err); - if (err?.message?.includes('UNIQUE constraint failed')) { + if ((err as any)?.message?.includes('UNIQUE constraint failed')) { return fail(400, { error: 'URL 别名 (Slug) 已被其他页面占用' }); } return fail(500, { error: '保存修改失败' }); diff --git a/src/routes/admin/pages/[id]/+page.svelte b/src/routes/admin/pages/[id]/+page.svelte index 9a9bde4..e267f7b 100644 --- a/src/routes/admin/pages/[id]/+page.svelte +++ b/src/routes/admin/pages/[id]/+page.svelte @@ -1,21 +1,29 @@

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

@@ -44,11 +52,19 @@
- - + +
- +
{#if mode === 'edit'} + required> {:else}
@@ -222,7 +237,7 @@ .form-grid { grid-template-columns: 1fr; } - + .form-sidebar { position: static; flex-direction: row; @@ -245,7 +260,7 @@ letter-spacing: 0.5px; } - .form-group input[type="text"], + .form-group input[type='text'], .form-group textarea { width: 100%; padding: 12px 14px; @@ -256,7 +271,9 @@ font-size: 1.05rem; color: var(--text-color); box-sizing: border-box; - transition: border-color 0.3s ease, box-shadow 0.3s ease; + transition: + border-color 0.3s ease, + box-shadow 0.3s ease; } .form-group textarea { @@ -265,7 +282,7 @@ min-height: 120px; } - .form-group input[type="text"]:focus, + .form-group input[type='text']:focus, .form-group textarea:focus { outline: none; border-color: var(--accent-color, #419b38); diff --git a/src/routes/admin/pages/new/+page.server.ts b/src/routes/admin/pages/new/+page.server.ts index 9b1b6bf..9d906ea 100644 --- a/src/routes/admin/pages/new/+page.server.ts +++ b/src/routes/admin/pages/new/+page.server.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; @@ -20,13 +22,16 @@ export const actions: Actions = { } try { - await db.prepare( - `INSERT INTO pages (title, slug, content, author_id, published_at, updated_at) + 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) { + ) + .bind(title, slug, content) + .run(); + } catch (err: unknown) { console.error('Error creating page:', err); - if (err?.message?.includes('UNIQUE constraint failed')) { + if ((err as any)?.message?.includes('UNIQUE constraint failed')) { return fail(400, { error: 'URL 别名 (Slug) 已被占用,请使用其他别名' }); } return fail(500, { error: '保存页面失败' }); diff --git a/src/routes/admin/pages/new/+page.svelte b/src/routes/admin/pages/new/+page.svelte index 2ca3e98..98f99b2 100644 --- a/src/routes/admin/pages/new/+page.svelte +++ b/src/routes/admin/pages/new/+page.svelte @@ -1,4 +1,5 @@ - - {#if data.posts.length > 0} -
- {#if selectedIds.length > 0} - 已选择 {selectedIds.length} 项 -
{ - if(!confirm(`确定要删除选中的 ${selectedIds.length} 篇${$page.data.aliases?.post || '项目'}吗?操作不可恢复。`)) { +{#if data.posts.length > 0} +
+ {#if selectedIds.length > 0} + 已选择 {selectedIds.length} 项 + { + if ( + !confirm( + `确定要删除选中的 ${selectedIds.length} 篇${$page.data.aliases?.post || '项目'}吗?操作不可恢复。` + ) + ) { e.preventDefault(); } - }}> - {#each selectedIds as id} - - {/each} - - - {:else} - 未选择项目 - {/if} -
- {/if} - + }} + > + {#each selectedIds as id (id)} + + {/each} + + + {:else} + 未选择项目 + {/if} +
+{/if} +
{#if data.posts.length === 0}
@@ -62,28 +73,54 @@ 别名 (Slug) 作者 发布时间 - - {#each data.posts as post} + {#each data.posts as post (post.id || post)} - + {post.id} - {post.title} - - + {post.title} + + - {post.category || '未分类'} - {post.series || '—'} + {post.category || '未分类'} + {post.series || '—'} {post.slug} {post.author || '未知'} {post.dateStr} - {/each} @@ -144,9 +181,10 @@ text-align: left; } - .data-table th, .data-table td { + .data-table th, + .data-table td { padding: 10px 16px; - border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.08)); + border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.08)); } .data-table th { @@ -162,7 +200,7 @@ } .data-table tbody tr:hover { - background-color: rgba(0,0,0,0.02); + background-color: rgba(0, 0, 0, 0.02); } .preview-link { @@ -178,12 +216,10 @@ opacity: 1; } - - .empty-state { padding: 50px 20px; text-align: center; - color: var(--text-muted, rgba(0,0,0,0.6)); + color: var(--text-muted, rgba(0, 0, 0, 0.6)); } .batch-actions { @@ -192,7 +228,7 @@ gap: 15px; margin-bottom: 15px; padding: 10px 15px; - background: rgba(0,0,0,0.02); + background: rgba(0, 0, 0, 0.02); height: 54px; box-sizing: border-box; } @@ -204,7 +240,7 @@ } .selected-count.empty { - color: var(--text-muted, rgba(0,0,0,0.4)); + color: var(--text-muted, rgba(0, 0, 0, 0.4)); font-weight: normal; } @@ -213,7 +249,7 @@ text-align: center; } - .checkbox-col input[type="checkbox"] { + .checkbox-col input[type='checkbox'] { width: 16px; height: 16px; accent-color: var(--accent-color, #419b38); @@ -242,10 +278,9 @@ background-color: #d9534f; color: white; } - + .btn-sm { padding: 4px 12px; font-size: 0.85rem; } - diff --git a/src/routes/admin/posts/[id]/+page.server.ts b/src/routes/admin/posts/[id]/+page.server.ts index bd47b07..0dcc8a7 100644 --- a/src/routes/admin/posts/[id]/+page.server.ts +++ b/src/routes/admin/posts/[id]/+page.server.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { error, fail, isHttpError, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { @@ -16,19 +18,42 @@ export const load: PageServerLoad = async ({ params, platform }) => { } try { - const post = await db.prepare( - 'SELECT id, title, slug, content, cover, category_id, series_id, series_weight, published_at, updated_at FROM posts WHERE id = ?' - ).bind(postId).first<{ id: number; title: string; slug: string; content: string; cover: string | null; category_id: number | null; series_id: number | null; series_weight: number | null; published_at?: string; updated_at?: string }>(); + const post = await db + .prepare( + 'SELECT id, title, slug, content, cover, category_id, series_id, series_weight, published_at, updated_at FROM posts WHERE id = ?' + ) + .bind(postId) + .first<{ + id: number; + title: string; + slug: string; + content: string; + cover: string | null; + category_id: number | null; + series_id: number | null; + series_weight: number | null; + published_at?: string; + updated_at?: string; + }>(); 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: allSeries } = await db.prepare('SELECT id, name, slug FROM series ORDER BY name COLLATE NOCASE ASC').all(); + 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: allSeries } = await db + .prepare('SELECT id, name, slug FROM series ORDER BY name COLLATE NOCASE ASC') + .all(); - const { results: selectedTagRows } = await db.prepare('SELECT tag_id FROM post_tags WHERE post_id = ?').bind(postId).all<{ tag_id: number }>(); + const { results: selectedTagRows } = await db + .prepare('SELECT tag_id FROM post_tags WHERE post_id = ?') + .bind(postId) + .all<{ tag_id: number }>(); const selectedTagIds = selectedTagRows.map((row) => row.tag_id); return { @@ -58,7 +83,7 @@ export const actions: Actions = { const content = formData.get('content')?.toString() || ''; const cover = formData.get('cover')?.toString().trim() || null; const categoryId = formData.get('category_id') ? Number(formData.get('category_id')) : null; - const selectedTags = formData.getAll('tags').map(t => Number(t)); + const selectedTags = formData.getAll('tags').map((t) => Number(t)); const publishedAt = formData.get('published_at')?.toString().trim(); const updatedAt = formData.get('updated_at')?.toString().trim() || new Date().toISOString(); @@ -76,35 +101,68 @@ export const actions: Actions = { parseSeriesWeight(formData.get('series_weight')), postId ); - } catch (err) { + } catch (err: any) { if (err instanceof SeriesValidationError) return fail(400, { error: err.message }); throw err; } try { - // 更新文章表 + // 改为 D1 batch 事务批量更新正文、清除老标签并原子绑定新标签 + const stmts = []; if (publishedAt) { - await db.prepare( - `UPDATE posts - SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, series_id = ?, series_weight = ?, published_at = ?, updated_at = ? - WHERE id = ?` - ).bind(title, slug, content, cover, categoryId, seriesId, seriesWeight, publishedAt, updatedAt, postId).run(); + stmts.push( + db + .prepare( + `UPDATE posts + SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, series_id = ?, series_weight = ?, published_at = ?, updated_at = ? + WHERE id = ?` + ) + .bind( + title, + slug, + content, + cover, + categoryId, + seriesId, + seriesWeight, + publishedAt, + updatedAt, + postId + ) + ); } else { - await db.prepare( - `UPDATE posts - SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, series_id = ?, series_weight = ?, updated_at = ? - WHERE id = ?` - ).bind(title, slug, content, cover, categoryId, seriesId, seriesWeight, updatedAt, postId).run(); + stmts.push( + db + .prepare( + `UPDATE posts + SET title = ?, slug = ?, content = ?, cover = ?, category_id = ?, series_id = ?, series_weight = ?, updated_at = ? + WHERE id = ?` + ) + .bind( + title, + slug, + content, + cover, + categoryId, + seriesId, + seriesWeight, + updatedAt, + postId + ) + ); } - // 清除原有标签并重新绑定 - await db.prepare('DELETE FROM post_tags WHERE post_id = ?').bind(postId).run(); - + stmts.push(db.prepare('DELETE FROM post_tags WHERE post_id = ?').bind(postId)); + 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(); + stmts.push( + db.prepare('INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)').bind(postId, tagId) + ); } } + + await db.batch(stmts); } catch (err: unknown) { console.error('Error updating post:', err); if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) { diff --git a/src/routes/admin/posts/[id]/+page.svelte b/src/routes/admin/posts/[id]/+page.svelte index d9fb00d..da5d8a7 100644 --- a/src/routes/admin/posts/[id]/+page.svelte +++ b/src/routes/admin/posts/[id]/+page.svelte @@ -1,4 +1,7 @@

编辑

- - ← 返回 + + ← 返回
- + {#if form?.error}
@@ -275,43 +303,164 @@
- +
{#if mode === 'edit'} -
- - - - -
- - - - -
-
- {#if showAdmonitionMenu} - - - - {/if} - - {#if showAdmonitionMenu} -
- {#each admonitionTypes as item} - - {/each} +
+ + + + +
+ + + + +
+
+ {#if showAdmonitionMenu} + + + + {/if} + + {#if showAdmonitionMenu} +
+ {#each admonitionTypes as item (item.type)} + + {/each} +
+ {/if}
- {/if} +
- -
{/if}
- - + +
@@ -325,8 +474,7 @@ rows="16" style="min-height: 400px;" placeholder="支持 Markdown 格式..." - required - > + required> {:else}
@@ -362,7 +510,10 @@ ({ value: category.id, label: category.name }))} + options={data.categories.map((category) => ({ + value: category.id, + label: category.name + }))} bind:value={selectedCategoryId} />
@@ -389,12 +540,29 @@
@@ -554,7 +725,6 @@ overflow-y: auto; } - .form-grid { display: grid; grid-template-columns: 1fr 280px; @@ -580,7 +750,7 @@ .form-grid { grid-template-columns: 1fr; } - + .form-sidebar { position: static; flex-direction: row; @@ -604,9 +774,8 @@ text-transform: uppercase; } - .form-group input[type="text"], + .form-group input[type='text'], .form-group textarea { - width: 100%; padding: 12px 14px; border: 1px solid rgba(0, 0, 0, 0.12); @@ -616,7 +785,9 @@ font-size: 1.05rem; color: var(--text-color); box-sizing: border-box; - transition: border-color 0.3s ease, box-shadow 0.3s ease; + transition: + border-color 0.3s ease, + box-shadow 0.3s ease; } .form-group textarea { @@ -625,7 +796,7 @@ min-height: 120px; } - .form-group input[type="text"]:focus, + .form-group input[type='text']:focus, .form-group textarea:focus { outline: none; border-color: var(--accent-color, #419b38); @@ -704,7 +875,7 @@ opacity: 0.7; padding: 0 4px; } - + .remove-tag-btn:hover { opacity: 1; color: #d9534f; @@ -733,7 +904,7 @@ .tag-dropdown-item { background: transparent; border: none; - border-bottom: 1px solid rgba(0,0,0,0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding: 8px 12px; text-align: left; font-family: inherit; @@ -753,7 +924,6 @@ color: var(--bg-color); } - .editor-header-right { display: flex; align-items: center; @@ -785,7 +955,7 @@ .md-btn:hover { opacity: 1; color: var(--accent-color, #419b38); - background-color: rgba(0,0,0,0.03); + background-color: rgba(0, 0, 0, 0.03); } .md-btn svg { @@ -813,7 +983,10 @@ .dropdown-overlay { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; z-index: 99; } @@ -834,7 +1007,7 @@ .dropdown-item { background: transparent; border: none; - border-bottom: 1px solid rgba(0,0,0,0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding: 8px 12px; text-align: left; font-family: inherit; @@ -844,7 +1017,7 @@ font-weight: 600; transition: all 0.2s ease; } - + .dropdown-item:last-child { border-bottom: none; } @@ -853,5 +1026,4 @@ background: var(--text-color); color: var(--bg-color, #ffffff); } - diff --git a/src/routes/admin/posts/new/+page.server.ts b/src/routes/admin/posts/new/+page.server.ts index acef33c..e87c56e 100644 --- a/src/routes/admin/posts/new/+page.server.ts +++ b/src/routes/admin/posts/new/+page.server.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { @@ -15,14 +17,20 @@ export const load: PageServerLoad = async ({ platform }) => { 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(); - const { results: seriesResults } = await db.prepare('SELECT id, name, slug FROM series ORDER BY name COLLATE NOCASE ASC').all(); - + 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(); + const { results: seriesResults } = await db + .prepare('SELECT id, name, slug FROM series ORDER BY name COLLATE NOCASE ASC') + .all(); + tags = tagResults as unknown as { id: number; name: string }[]; categories = catResults as unknown as { id: number; name: string }[]; series = seriesResults as unknown as { id: number; name: string; slug: string }[]; - } catch (err) { + } catch (err: any) { console.error('Error fetching tags or categories:', err); } } @@ -40,7 +48,7 @@ export const actions: Actions = { const content = formData.get('content')?.toString() || ''; const cover = formData.get('cover')?.toString().trim() || null; const categoryId = formData.get('category_id') ? Number(formData.get('category_id')) : null; - const selectedTags = formData.getAll('tags').map(t => Number(t)); + const selectedTags = formData.getAll('tags').map((t) => Number(t)); const publishedAt = formData.get('published_at')?.toString().trim() || new Date().toISOString(); const updatedAt = formData.get('updated_at')?.toString().trim() || publishedAt; @@ -57,25 +65,45 @@ export const actions: Actions = { seriesId, parseSeriesWeight(formData.get('series_weight')) ); - } catch (err) { + } catch (err: any) { if (err instanceof SeriesValidationError) return fail(400, { error: err.message }); throw err; } try { - const res = await db.prepare( - `INSERT INTO posts (title, slug, content, cover, author_id, category_id, series_id, series_weight, published_at, updated_at) - VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?) - RETURNING id` - ).bind(title, slug, content, cover, categoryId, seriesId, seriesWeight, publishedAt, updatedAt).first<{ id: number }>(); + // 将创建新文章及所有绑定标签打包在 D1 原生 batch 中作为原子事务提交,杜绝不完整中间状态 + const stmts = [ + db + .prepare( + `INSERT INTO posts (title, slug, content, cover, author_id, category_id, series_id, series_weight, published_at, updated_at) + VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?)` + ) + .bind( + title, + slug, + content, + cover, + categoryId, + seriesId, + seriesWeight, + publishedAt, + updatedAt + ) + ]; - const postId = res?.id; - - if (postId && selectedTags.length > 0) { + 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(); + stmts.push( + db + .prepare( + 'INSERT INTO post_tags (post_id, tag_id) VALUES ((SELECT id FROM posts WHERE slug = ?), ?)' + ) + .bind(slug, tagId) + ); } } + + await db.batch(stmts); } catch (err: unknown) { console.error('Error creating post:', err); if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) { diff --git a/src/routes/admin/posts/new/+page.svelte b/src/routes/admin/posts/new/+page.svelte index 616a353..8c47d4d 100644 --- a/src/routes/admin/posts/new/+page.svelte +++ b/src/routes/admin/posts/new/+page.svelte @@ -1,4 +1,7 @@

新{$page.data.aliases?.post || '文章'}

- - ← 返回 + + ← 返回
- + {#if form?.error}
@@ -281,43 +309,164 @@
- +
{#if mode === 'edit'} -
- - - - -
- - - - -
-
- {#if showAdmonitionMenu} - - - - {/if} - - {#if showAdmonitionMenu} -
- {#each admonitionTypes as item} - - {/each} +
+ + + + +
+ + + + +
+
+ {#if showAdmonitionMenu} + + + + {/if} + + {#if showAdmonitionMenu} +
+ {#each admonitionTypes as item (item.type)} + + {/each} +
+ {/if}
- {/if} +
- -
{/if}
- - + +
@@ -331,8 +480,7 @@ rows="16" style="min-height: 400px;" placeholder="支持 Markdown 格式..." - required - > + required> {:else}
@@ -345,9 +493,11 @@
- + @@ -395,12 +548,29 @@
@@ -585,7 +758,7 @@ .form-grid { grid-template-columns: 1fr; } - + .form-sidebar { position: static; flex-direction: row; @@ -609,9 +782,8 @@ text-transform: uppercase; } - .form-group input[type="text"], + .form-group input[type='text'], .form-group textarea { - width: 100%; padding: 12px 14px; border: 1px solid rgba(0, 0, 0, 0.12); @@ -621,7 +793,9 @@ font-size: 1.05rem; color: var(--text-color); box-sizing: border-box; - transition: border-color 0.3s ease, box-shadow 0.3s ease; + transition: + border-color 0.3s ease, + box-shadow 0.3s ease; } .form-group textarea { @@ -630,7 +804,7 @@ min-height: 120px; } - .form-group input[type="text"]:focus, + .form-group input[type='text']:focus, .form-group textarea:focus { outline: none; border-color: var(--accent-color, #419b38); @@ -709,7 +883,7 @@ opacity: 0.7; padding: 0 4px; } - + .remove-tag-btn:hover { opacity: 1; color: #d9534f; @@ -738,7 +912,7 @@ .tag-dropdown-item { background: transparent; border: none; - border-bottom: 1px solid rgba(0,0,0,0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding: 8px 12px; text-align: left; font-family: inherit; @@ -758,7 +932,6 @@ color: var(--bg-color); } - .editor-header-right { display: flex; align-items: center; @@ -790,7 +963,7 @@ .md-btn:hover { opacity: 1; color: var(--accent-color, #419b38); - background-color: rgba(0,0,0,0.03); + background-color: rgba(0, 0, 0, 0.03); } .md-btn svg { @@ -818,7 +991,10 @@ .dropdown-overlay { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; z-index: 99; } @@ -839,7 +1015,7 @@ .dropdown-item { background: transparent; border: none; - border-bottom: 1px solid rgba(0,0,0,0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding: 8px 12px; text-align: left; font-family: inherit; @@ -849,7 +1025,7 @@ font-weight: 600; transition: all 0.2s ease; } - + .dropdown-item:last-child { border-bottom: none; } @@ -858,5 +1034,4 @@ background: var(--text-color); color: var(--bg-color, #ffffff); } - diff --git a/src/routes/admin/series/+page.server.ts b/src/routes/admin/series/+page.server.ts index 4acd0bd..7db5478 100644 --- a/src/routes/admin/series/+page.server.ts +++ b/src/routes/admin/series/+page.server.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { fail } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; @@ -38,7 +40,7 @@ export const load: PageServerLoad = async ({ platform }) => { post_count: number; }> }; - } catch (err) { + } catch (err: any) { console.error('Error fetching series:', err); return { series: [] }; } @@ -118,7 +120,7 @@ export const actions: Actions = { db.prepare('DELETE FROM series WHERE id = ?').bind(id) ]); return { success: true }; - } catch (err) { + } catch (err: any) { console.error('Error deleting series:', err); return fail(500, { error: '删除系列失败' }); } diff --git a/src/routes/admin/settings/+page.server.ts b/src/routes/admin/settings/+page.server.ts index 35c382d..2bf031b 100644 --- a/src/routes/admin/settings/+page.server.ts +++ b/src/routes/admin/settings/+page.server.ts @@ -1,24 +1,26 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { fail } from '@sveltejs/kit'; import type { PageServerLoad, Actions } from './$types'; export const load: PageServerLoad = async ({ locals, platform }) => { const db = platform?.env?.DB; - let settings: Record = {}; - + const settings: Record = {}; + if (db) { try { - const { results } = await db.prepare("SELECT key, value FROM settings").all(); + const { results } = await db.prepare('SELECT key, value FROM settings').all(); results.forEach((row: any) => { settings[row.key] = row.value; }); - } catch (e) { - console.error('Failed to load settings', e); + } catch (err: any) { + console.error('Failed to load settings', err); } } return { user: locals.user, - platformEnv: !!db ? 'Cloudflare D1 (Connected)' : 'Local Mock', + platformEnv: db ? 'Cloudflare D1 (Connected)' : 'Local Mock', settings }; }; @@ -31,21 +33,32 @@ export const actions: Actions = { const data = await request.formData(); const updates = [ { key: 'site.title', value: data.get('site_title')?.toString() || 'BlogFlare' }, - { key: 'site.description', value: data.get('site_description')?.toString() || 'A high-performance blog powered by Cloudflare D1, R2, and KV.' }, - { key: 'r2.public_domain', value: data.get('r2_public_domain')?.toString() || 'https://assets.yourdomain.com' } + { + key: 'site.description', + value: + data.get('site_description')?.toString() || + 'A high-performance blog powered by Cloudflare D1, R2, and KV.' + }, + { + key: 'r2.public_domain', + value: data.get('r2_public_domain')?.toString() || 'https://assets.yourdomain.com' + } ]; try { for (const { key, value } of updates) { - await db.prepare( - `INSERT INTO settings (key, value, updated_at) + await db + .prepare( + `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP` - ).bind(key, value).run(); + ) + .bind(key, value) + .run(); } return { success: true }; - } catch (e) { - console.error('Failed to update site info', e); + } catch (err: any) { + console.error('Failed to update site info', err); return fail(500, { error: '保存站点信息失败' }); } }, @@ -64,15 +77,18 @@ export const actions: Actions = { try { for (const { key, value } of updates) { - await db.prepare( - `INSERT INTO settings (key, value, updated_at) + await db + .prepare( + `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP` - ).bind(key, value).run(); + ) + .bind(key, value) + .run(); } return { success: true }; - } catch (e) { - console.error('Failed to update aliases', e); + } catch (err: any) { + console.error('Failed to update aliases', err); return fail(500, { error: '保存失败' }); } }, @@ -91,15 +107,18 @@ export const actions: Actions = { try { for (const { key, value } of updates) { - await db.prepare( - `INSERT INTO settings (key, value, updated_at) + await db + .prepare( + `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP` - ).bind(key, value).run(); + ) + .bind(key, value) + .run(); } return { success: true }; - } catch (e) { - console.error('Failed to update appearance', e); + } catch (err: any) { + console.error('Failed to update appearance', err); return fail(500, { error: '保存外观偏好失败' }); } } diff --git a/src/routes/admin/settings/+page.svelte b/src/routes/admin/settings/+page.svelte index 3c1296a..0bad080 100644 --- a/src/routes/admin/settings/+page.svelte +++ b/src/routes/admin/settings/+page.svelte @@ -4,12 +4,14 @@ import { addToast } from '$lib/stores/toast.svelte'; import Tabs from '$lib/components/admin/Tabs.svelte'; import ImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte'; + import { untrack } from 'svelte'; let { data, form }: { data: PageData; form: ActionData } = $props(); + const initialData = $state.snapshot(untrack(() => data)); let activeTab = $state('basic'); - - let avatarEnabled = $state(data.settings['site.avatar_enabled'] !== 'false'); - let avatarUrl = $state(data.settings['site.avatar_url'] || ''); + + let avatarEnabled = $state(initialData.settings['site.avatar_enabled'] !== 'false'); + let avatarUrl = $state(initialData.settings['site.avatar_url'] || ''); $effect(() => { if (form?.success) { @@ -24,146 +26,234 @@

系统设置

- -
{#if activeTab === 'basic'} -
-

站点基础信息

-

设置您的网站名称和一句话描述,这些信息将展示在首页和浏览器标题中。

- -
{ return async ({ update }) => { update({ reset: false }); }; }} class="alias-form"> -
-
- - +
+

站点基础信息

+

设置您的网站名称和一句话描述,这些信息将展示在首页和浏览器标题中。

+ + { + return async ({ update }) => { + update({ reset: false }); + }; + }} + class="alias-form" + > +
+
+ + +
+
+ + +
+
+ + + 请确保不要以斜杠 / 结尾 +
-
- - + +
+
-
- - - 请确保不要以斜杠 / 结尾 -
-
- -
- -
- -
+ +
{/if} {#if activeTab === 'appearance'} -
-

首页顶部主图与外观偏好

-

针对站点的主页展现风貌(如顶部站点头像/主图的开启或关闭、自定义挑选等)进行专属配置。

- -
{ return async ({ update }) => { update({ reset: false }); }; }} class="alias-form"> -
- -
+
+

首页顶部主图与外观偏好

+

+ 针对站点的主页展现风貌(如顶部站点头像/主图的开启或关闭、自定义挑选等)进行专属配置。 +

- {#if avatarEnabled} + { + return async ({ update }) => { + update({ reset: false }); + }; + }} + class="alias-form" + >
- -
- -
+
- {:else} - - {/if} -
- -
- -
+ {#if avatarEnabled} +
+ +
+ +
+
+ {:else} + + {/if} + +
+ +
+ +
{/if} {#if activeTab === 'system'} -
-

系统信息

-
- 主题风格 - Typecho Theme VOID +
+

系统信息

+
+ 主题风格 + Typecho Theme VOID +
+
+ 驱动框架 + SvelteKit 5 + Cloudflare +
-
- 驱动框架 - SvelteKit 5 + Cloudflare -
-
-
-

安全与环境

-
- 当前管理员 - {data.user?.email || '未知用户'} +
+

安全与环境

+
+ 当前管理员 + {data.user?.email || '未知用户'} +
+
+ 数据库绑定 + {data.platformEnv} +
+
+ 鉴权模式 + Cloudflare Access (Zero Trust) +
-
- 数据库绑定 - {data.platformEnv} -
-
- 鉴权模式 - Cloudflare Access (Zero Trust) -
-
{/if} {#if activeTab === 'terms'} -
-

个性化术语设定

-

您可以根据部署场景(如教学系统、文档库)自定义后台各模块的显示名称,例如将“文章”改为“实验手册”。

- -
{ return async ({ update }) => { update({ reset: false }); }; }} class="alias-form"> -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
+
+

个性化术语设定

+

+ 您可以根据部署场景(如教学系统、文档库)自定义后台各模块的显示名称,例如将“文章”改为“实验手册”。 +

-
- -
- -
+
{ + return async ({ update }) => { + update({ reset: false }); + }; + }} + class="alias-form" + > +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
{/if}
@@ -202,14 +292,14 @@ .settings-card h3 { margin: 0 0 12px 0; font-size: 1.2rem; - border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1)); + border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.1)); padding-bottom: 10px; color: var(--text-color); } .settings-card .desc { font-size: 0.9rem; - color: var(--text-muted, rgba(0,0,0,0.6)); + color: var(--text-muted, rgba(0, 0, 0, 0.6)); margin-bottom: 24px; line-height: 1.5; } @@ -219,7 +309,7 @@ justify-content: space-between; align-items: center; padding: 14px 0; - border-bottom: 1px dashed var(--border-color, rgba(0,0,0,0.08)); + border-bottom: 1px dashed var(--border-color, rgba(0, 0, 0, 0.08)); font-size: 0.95rem; gap: 15px; } @@ -229,7 +319,7 @@ } .info-row .label { - color: var(--text-muted, rgba(0,0,0,0.6)); + color: var(--text-muted, rgba(0, 0, 0, 0.6)); font-weight: 500; white-space: nowrap; flex-shrink: 0; @@ -252,7 +342,7 @@ display: flex; gap: 20px; } - + @media (max-width: 768px) { .form-row { flex-direction: column; @@ -331,7 +421,7 @@ color: var(--text-color); } - .toggle-label input[type="checkbox"] { + .toggle-label input[type='checkbox'] { width: 18px; height: 18px; cursor: pointer; diff --git a/src/routes/admin/tags/+page.server.ts b/src/routes/admin/tags/+page.server.ts index 6fcfea5..741247b 100644 --- a/src/routes/admin/tags/+page.server.ts +++ b/src/routes/admin/tags/+page.server.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { fail } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; @@ -10,18 +12,20 @@ export const load: PageServerLoad = async ({ platform }) => { try { // 查询所有标签以及对应的文章数量 - const { results } = await db.prepare( - `SELECT t.id, t.name, COUNT(pt.post_id) as post_count + const { results } = await db + .prepare( + `SELECT t.id, t.name, COUNT(pt.post_id) as post_count FROM tags t LEFT JOIN post_tags pt ON t.id = pt.tag_id GROUP BY t.id, t.name ORDER BY t.name ASC` - ).all(); + ) + .all(); return { tags: results as unknown as { id: number; name: string; post_count: number }[] }; - } catch (err) { + } catch (err: any) { console.error('Error fetching tags:', err); return { tags: [] }; } @@ -41,12 +45,15 @@ export const actions: Actions = { try { // 在 D1 中获取刚插入的 ID,可以通过 RETURNING id 语法 - const { results } = await db.prepare('INSERT INTO tags (name) VALUES (?) RETURNING id').bind(name).all(); + const { results } = await db + .prepare('INSERT INTO tags (name) VALUES (?) RETURNING id') + .bind(name) + .all(); const newId = results[0]?.id; return { success: true, tag: { id: newId, name } }; - } catch (err: any) { + } catch (err: unknown) { console.error('Error creating tag:', err); - if (err?.message?.includes('UNIQUE constraint failed')) { + if ((err as any)?.message?.includes('UNIQUE constraint failed')) { return fail(400, { error: '标签名称已存在' }); } return fail(500, { error: '创建标签失败' }); @@ -65,7 +72,7 @@ export const actions: Actions = { try { await db.prepare('DELETE FROM tags WHERE id = ?').bind(id).run(); return { success: true }; - } catch (err) { + } catch (err: any) { console.error('Error deleting tag:', err); return { success: false, error: '删除标签失败' }; } diff --git a/src/routes/admin/tags/+page.svelte b/src/routes/admin/tags/+page.svelte index bbf7982..a1a53b8 100644 --- a/src/routes/admin/tags/+page.svelte +++ b/src/routes/admin/tags/+page.svelte @@ -54,15 +54,23 @@ - {#each data.tags as tag} + {#each data.tags as tag (tag.id || tag)} {tag.id} {tag.name} {tag.post_count} 篇文章 -
{ - if(!confirm(`确定要删除${$page.data.aliases?.tag || '标签'} "${tag.name}" 吗?`)) e.preventDefault(); - }}> + { + if ( + !confirm(`确定要删除${$page.data.aliases?.tag || '标签'} "${tag.name}" 吗?`) + ) + e.preventDefault(); + }} + >
@@ -139,7 +147,7 @@ letter-spacing: 0.5px; } - .form-group input[type="text"] { + .form-group input[type='text'] { width: 100%; padding: 10px 0; border: none; @@ -152,7 +160,7 @@ transition: border-bottom-color 0.3s ease; } - .form-group input[type="text"]:focus { + .form-group input[type='text']:focus { outline: none; border-bottom-color: var(--accent-color, #419b38); } @@ -197,9 +205,10 @@ text-align: left; } - .data-table th, .data-table td { + .data-table th, + .data-table td { padding: 14px 18px; - border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.08)); + border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.08)); } .data-table th { @@ -215,10 +224,9 @@ } .data-table tbody tr:hover { - background-color: rgba(0,0,0,0.02); + background-color: rgba(0, 0, 0, 0.02); } - .inline-form { display: inline-block; margin: 0; @@ -240,7 +248,9 @@ color: var(--text-color); cursor: pointer; text-decoration: none; - transition: color 0.2s, text-decoration-color 0.2s; + transition: + color 0.2s, + text-decoration-color 0.2s; } .action-btn.delete:hover { @@ -252,6 +262,6 @@ .empty-state { padding: 50px 20px; text-align: center; - color: var(--text-muted, rgba(0,0,0,0.6)); + color: var(--text-muted, rgba(0, 0, 0, 0.6)); }