From 9de389a1aa30935a2af9d92b224e0fb022959257 Mon Sep 17 00:00:00 2001 From: seaHi Date: Wed, 29 Jul 2026 10:49:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E3=80=81=E5=88=86=E7=B1=BB=E4=B8=8E=E6=A0=87=E7=AD=BE=E7=9A=84?= =?UTF-8?q?=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5=E5=8F=8A=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=A4=84=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/categories/+page.server.ts | 3 + src/routes/admin/categories/+page.svelte | 21 +- src/routes/admin/posts/+page.server.ts | 21 +- src/routes/admin/posts/+page.svelte | 179 ++++-- src/routes/admin/posts/[id]/+page.server.ts | 11 +- src/routes/admin/posts/[id]/+page.svelte | 598 ++++++++++++++++---- src/routes/admin/posts/new/+page.server.ts | 9 +- src/routes/admin/posts/new/+page.svelte | 593 +++++++++++++++---- src/routes/admin/tags/+page.server.ts | 6 +- src/routes/admin/tags/+page.svelte | 21 +- 10 files changed, 1141 insertions(+), 321 deletions(-) diff --git a/src/routes/admin/categories/+page.server.ts b/src/routes/admin/categories/+page.server.ts index f496d57..d1ee115 100644 --- a/src/routes/admin/categories/+page.server.ts +++ b/src/routes/admin/categories/+page.server.ts @@ -1,3 +1,6 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + async function ensureCategoriesTable(db: any) { try { await db.prepare(` diff --git a/src/routes/admin/categories/+page.svelte b/src/routes/admin/categories/+page.svelte index 896d4d6..af0019d 100644 --- a/src/routes/admin/categories/+page.svelte +++ b/src/routes/admin/categories/+page.svelte @@ -1,4 +1,5 @@
-

分类管理

+

{$page.data.aliases?.category || '分类'}管理

{#if form?.error} @@ -24,9 +25,9 @@ {/if}
- +
-

新分类

+

新{$page.data.aliases?.category || '分类'}

@@ -54,18 +55,18 @@
- +
{#if data.categories.length === 0}
-

暂无任何分类,在左侧添加一个吧!

+

暂无任何{$page.data.aliases?.category || '分类'},在左侧添加一个吧!

{:else} - + @@ -80,7 +81,7 @@
ID分类名称{$page.data.aliases?.category || '分类'}名称 别名 (Slug) 关联文章数 操作{category.post_count} 篇文章
{ - if(!confirm(`确定要删除分类 "${category.name}" 吗?删除后相关文章将归为未分类。`)) e.preventDefault(); + if(!confirm(`确定要删除${$page.data.aliases?.category || '分类'} "${category.name}" 吗?删除后相关文章将归为未${$page.data.aliases?.category || '分类'}。`)) e.preventDefault(); }}> @@ -173,7 +174,7 @@ .form-group input[type="text"]:focus { outline: none; - border-bottom-color: var(--accent-color, #bd6262); + border-bottom-color: var(--accent-color, #419b38); } .btn { @@ -199,8 +200,8 @@ } .btn-primary:hover { - background-color: var(--accent-color, #bd6262); - border-color: var(--accent-color, #bd6262); + background-color: var(--accent-color, #419b38); + border-color: var(--accent-color, #419b38); color: white; } diff --git a/src/routes/admin/posts/+page.server.ts b/src/routes/admin/posts/+page.server.ts index ee073ff..192b038 100644 --- a/src/routes/admin/posts/+page.server.ts +++ b/src/routes/admin/posts/+page.server.ts @@ -1,5 +1,13 @@ import type { PageServerLoad } from './$types'; +interface PostListRow { + id: number; + title: string; + slug: string; + date: string; + author: string | null; + category?: string; +} export const load: PageServerLoad = async ({ platform }) => { const db = platform?.env?.DB; @@ -10,7 +18,7 @@ export const load: PageServerLoad = async ({ platform }) => { } try { - let results; + let results: PostListRow[]; 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 @@ -18,7 +26,7 @@ export const load: PageServerLoad = async ({ platform }) => { 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(); + ).all(); results = queryRes.results; } catch (e) { // Fallback query if categories table or category_id column is missing @@ -27,7 +35,7 @@ export const load: PageServerLoad = async ({ platform }) => { FROM posts p LEFT JOIN users u ON p.author_id = u.id ORDER BY p.published_at DESC` - ).all(); + ).all(); results = queryRes.results.map(r => ({ ...r, category: '未分类' })); } @@ -59,12 +67,13 @@ export const actions = { if (!db) return { success: false, error: 'Database not found' }; const data = await request.formData(); - const id = data.get('id'); + const ids = data.getAll('ids'); - if (!id) return { success: false, error: 'Missing ID' }; + if (!ids || ids.length === 0) return { success: false, error: 'Missing IDs' }; try { - await db.prepare('DELETE FROM posts WHERE id = ?').bind(id).run(); + const placeholders = ids.map(() => '?').join(','); + await db.prepare(`DELETE FROM posts WHERE id IN (${placeholders})`).bind(...ids).run(); return { success: true }; } catch (err) { console.error('Error deleting post:', err); diff --git a/src/routes/admin/posts/+page.svelte b/src/routes/admin/posts/+page.svelte index 780e39f..70c39ca 100644 --- a/src/routes/admin/posts/+page.svelte +++ b/src/routes/admin/posts/+page.svelte @@ -1,54 +1,87 @@
-

文章管理

- 发布新文章 +

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

+ 发布新{$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} +
{#if data.posts.length === 0}
-

还没有发布任何文章,现在就开始写作吧!

+

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

{:else} + - + {#each data.posts as post} + - + {/each} @@ -92,8 +125,8 @@ } .btn-primary:hover { - background-color: var(--accent-color, #bd6262); - border-color: var(--accent-color, #bd6262); + background-color: var(--accent-color, #419b38); + border-color: var(--accent-color, #419b38); color: white; } @@ -110,7 +143,7 @@ } .data-table th, .data-table td { - padding: 14px 18px; + padding: 10px 16px; border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.08)); } @@ -132,63 +165,85 @@ .preview-link { text-decoration: none; - font-size: 0.85rem; margin-left: 6px; opacity: 0.5; transition: opacity 0.2s; + display: inline-block; + vertical-align: middle; } .preview-link:hover { opacity: 1; } - .actions { - white-space: nowrap; - } - - .inline-form { - display: inline-block; - margin: 0; - padding: 0; - vertical-align: baseline; - } - - .action-btn { - display: inline-block; - background: transparent; - border: none; - padding: 0; - margin: 0; - font-family: inherit; - font-size: 0.9rem; - font-weight: 500; - line-height: 1; - vertical-align: baseline; - color: var(--text-color); - cursor: pointer; - text-decoration: none; - transition: color 0.2s, text-decoration-color 0.2s; - } - - .action-btn.edit { - margin-right: 14px; - } - - .action-btn.edit:hover { - color: var(--accent-color, #bd6262); - text-decoration: underline; - text-underline-offset: 4px; - } - - .action-btn.delete:hover { - color: #d9534f; - text-decoration: underline; - text-underline-offset: 4px; - } + .empty-state { padding: 50px 20px; text-align: center; color: var(--text-muted, rgba(0,0,0,0.6)); } + + .batch-actions { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; + padding: 10px 15px; + background: rgba(0,0,0,0.02); + height: 54px; + box-sizing: border-box; + } + + .selected-count { + font-size: 0.9rem; + font-weight: 600; + color: var(--text-color); + } + + .selected-count.empty { + color: var(--text-muted, rgba(0,0,0,0.4)); + font-weight: normal; + } + + .checkbox-col { + width: 40px; + text-align: center; + } + + .checkbox-col input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--accent-color, #419b38); + cursor: pointer; + } + + .title-link { + color: var(--text-color); + text-decoration: none; + transition: color 0.2s; + } + + .title-link:hover { + color: var(--accent-color, #419b38); + text-decoration: underline; + text-underline-offset: 4px; + } + + .btn-danger { + background-color: transparent; + color: #d9534f; + border-color: #d9534f; + } + + .btn-danger:hover { + 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 95c97bf..88c5136 100644 --- a/src/routes/admin/posts/[id]/+page.server.ts +++ b/src/routes/admin/posts/[id]/+page.server.ts @@ -11,8 +11,8 @@ export const load: PageServerLoad = async ({ params, platform }) => { 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 }>(); + 'SELECT id, title, slug, content, category_id FROM posts WHERE id = ?' + ).bind(postId).first<{ id: number; title: string; slug: string; content: string; category_id: number | null }>(); if (!post) { throw error(404, '文章不存在'); @@ -46,8 +46,7 @@ export const actions: Actions = { 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 slug = formData.get('slug')?.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)); @@ -60,9 +59,9 @@ export const actions: Actions = { // 更新文章表 await db.prepare( `UPDATE posts - SET title = ?, slug = ?, excerpt = ?, content = ?, category_id = ?, updated_at = CURRENT_TIMESTAMP + SET title = ?, slug = ?, content = ?, category_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?` - ).bind(title, slug, excerpt, content, categoryId, postId).run(); + ).bind(title, slug, content, categoryId, postId).run(); // 清除原有标签并重新绑定 await db.prepare('DELETE FROM post_tags WHERE post_id = ?').bind(postId).run(); diff --git a/src/routes/admin/posts/[id]/+page.svelte b/src/routes/admin/posts/[id]/+page.svelte index 72fbd61..ff751a6 100644 --- a/src/routes/admin/posts/[id]/+page.svelte +++ b/src/routes/admin/posts/[id]/+page.svelte @@ -1,13 +1,17 @@
@@ -34,17 +150,10 @@
{/if} -{#if form?.success} -
- 文章更新成功! - 预览文章 ↗ -
-{/if} -
-
+
- - -
+
+ + +
+ {#if mode === 'edit'} +
+ + + + +
+ + + + +
+
+ {#if showAdmonitionMenu} + + + + {/if} + + {#if showAdmonitionMenu} +
+ {#each admonitionTypes as item} + + {/each} +
+ {/if} +
+ +
+ {/if} +
+ + +
+
+
-
- - +
+ {#if mode === 'edit'} + + {:else} +
+ +
+ {/if} +
@@ -93,7 +242,7 @@ type="text" id="slug" name="slug" - class="select-input" + class="sidebar-input" bind:value={slug} placeholder="hello-world" required @@ -110,25 +259,51 @@
- {#if data.tags.length > 0} -
@@ -173,14 +348,14 @@ } .btn-primary:hover { - background-color: var(--accent-color, #bd6262); - border-color: var(--accent-color, #bd6262); + background-color: var(--accent-color, #419b38); + border-color: var(--accent-color, #419b38); color: white; } .btn-secondary:hover { - border-color: var(--accent-color, #bd6262); - color: var(--accent-color, #bd6262); + border-color: var(--accent-color, #419b38); + color: var(--accent-color, #419b38); } .error-alert { @@ -192,90 +367,105 @@ font-weight: 600; } - .form-grid { + .editor-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + } + + .editor-tabs { + display: flex; + gap: 8px; + } + + .tab-btn { + background: none; + border: none; + padding: 4px 12px; + font-size: 0.85rem; + font-weight: 600; + color: rgba(0, 0, 0, 0.4); + cursor: pointer; + border-radius: 4px; + transition: all 0.2s ease; + } + + .tab-btn:hover { + background: rgba(0, 0, 0, 0.05); + } + + .tab-btn.active { + color: var(--accent-color, #419b38); + background: rgba(65, 155, 56, 0.1); + } + + .preview-area { + padding: 20px; + border: 1px solid rgba(0, 0, 0, 0.12); + background: rgba(0, 0, 0, 0.015); + min-height: 400px; + max-height: 600px; + overflow-y: auto; + } + + .editor-container { display: block; } + .form-grid { + display: grid; + grid-template-columns: 1fr 280px; + gap: 40px; + align-items: start; + } + .form-main { - max-width: 800px; - margin: 0 auto; + width: 100%; } .form-sidebar { - position: fixed; - top: 120px; - right: calc((100vw - 1024px) / 2 - 280px); - width: 260px; + position: sticky; + top: 80px; + width: 100%; display: flex; flex-direction: column; gap: 30px; z-index: 10; } - @media (max-width: 1580px) { - .form-sidebar { - right: 20px; + @media (max-width: 900px) { + .form-grid { + grid-template-columns: 1fr; } - } - - @media (max-width: 1200px) { + .form-sidebar { position: static; - width: 100%; - margin-top: 40px; flex-direction: row; flex-wrap: wrap; - gap: 40px; + gap: 20px; + margin-top: 20px; } } .form-group { - margin-bottom: 30px; - } - - .form-group.inline-group { - display: flex; - align-items: center; - gap: 20px; + margin-bottom: 20px; } .form-group label { display: block; font-weight: 700; - margin-bottom: 12px; + margin-bottom: 6px; color: var(--text-color); font-size: 0.9rem; letter-spacing: 0.5px; } - .form-group.inline-group label { - margin-bottom: 0; - width: 55px; /* Fixed width for alignment */ - flex-shrink: 0; - } - - .form-group input[type="text"] { - width: 100%; - padding: 10px 0; - border: none; - border-bottom: 2px solid rgba(0, 0, 0, 0.1); - background: transparent; - font-family: inherit; - font-size: 1.05rem; - color: var(--text-color); - box-sizing: border-box; - transition: border-bottom-color 0.3s ease; - } - - .form-group input[type="text"]:focus, - .select-input:focus { - outline: none; - border-bottom-color: var(--accent-color, #bd6262); - } - + .form-group input[type="text"], .form-group textarea { + width: 100%; - padding: 16px; + padding: 12px 14px; border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 0; background: rgba(0, 0, 0, 0.015); @@ -284,12 +474,29 @@ color: var(--text-color); box-sizing: border-box; transition: border-color 0.3s ease, box-shadow 0.3s ease; + } + + .form-group textarea { resize: vertical; line-height: 1.8; min-height: 120px; } - .select-input { + .form-group input[type="text"]:focus, + .form-group textarea:focus { + outline: none; + border-color: var(--accent-color, #419b38); + box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08); + } + + .select-input:focus, + .sidebar-input:focus { + outline: none; + border-bottom-color: var(--accent-color, #419b38); + } + + .select-input, + .sidebar-input { width: 100%; padding: 10px 0; border: none; @@ -300,13 +507,16 @@ color: var(--text-color); box-sizing: border-box; transition: border-bottom-color 0.3s ease; + } + + .select-input { cursor: pointer; } .form-group textarea:focus { outline: none; - border-color: var(--accent-color, #bd6262); - box-shadow: 0 0 0 3px rgba(189, 98, 98, 0.08); + border-color: var(--accent-color, #419b38); + box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08); } .sidebar-box { @@ -326,25 +536,181 @@ border-bottom: none; } - .tags-list { + .selected-tags-vertical { display: flex; flex-direction: column; - gap: 12px; + gap: 6px; + margin-bottom: 15px; } - .tag-item { + .selected-tag-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 10px; + border: 1px solid var(--text-color); + background: transparent; + font-weight: 600; + font-size: 0.9rem; + } + + .remove-tag-btn { + background: transparent; + border: none; + color: var(--text-color); + font-size: 1.1rem; + cursor: pointer; + opacity: 0.7; + padding: 0 4px; + } + + .remove-tag-btn:hover { + opacity: 1; + color: #d9534f; + } + + .tag-input-container { + position: relative; + } + + .tag-autocomplete-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-color); + border: 1px solid var(--text-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15); + z-index: 50; + max-height: 200px; + overflow-y: auto; + display: flex; + flex-direction: column; + margin-top: 4px; + } + + .tag-dropdown-item { + background: transparent; + border: none; + border-bottom: 1px solid rgba(0,0,0,0.1); + padding: 8px 12px; + text-align: left; + font-family: inherit; + font-size: 0.85rem; + color: var(--text-color); + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + } + + .tag-dropdown-item:last-child { + border-bottom: none; + } + + .tag-dropdown-item:hover { + background: var(--text-color); + color: var(--bg-color); + } + + + .editor-header-right { display: flex; align-items: center; - gap: 10px; - cursor: pointer; - font-size: 0.95rem; + gap: 15px; } - .tag-item input[type="checkbox"] { - accent-color: var(--accent-color, #bd6262); - width: 16px; - height: 16px; - margin: 0; - cursor: pointer; + .md-toolbar { + display: flex; + align-items: center; + gap: 4px; } + + .md-btn { + background: transparent; + border: none; + padding: 4px; + cursor: pointer; + color: var(--text-color); + opacity: 0.65; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 2px; + } + + .md-btn:hover { + opacity: 1; + color: var(--accent-color, #419b38); + background-color: rgba(0,0,0,0.03); + } + + .md-btn svg { + width: 18px; + height: 18px; + stroke: currentColor; + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; + } + + .md-divider { + width: 2px; + height: 16px; + background: var(--text-color); + opacity: 0.15; + margin: 0 4px; + } + + .toolbar-dropdown-wrapper { + position: relative; + display: inline-block; + } + + .dropdown-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 99; + } + + .toolbar-dropdown { + position: absolute; + top: 100%; + left: 0; + margin-top: 4px; + background: var(--bg-color, #ffffff); + border: 1px solid var(--text-color); + z-index: 100; + width: 140px; + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + } + + .dropdown-item { + background: transparent; + border: none; + border-bottom: 1px solid rgba(0,0,0,0.1); + padding: 8px 12px; + text-align: left; + font-family: inherit; + font-size: 0.85rem; + color: var(--text-color); + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + } + + .dropdown-item:last-child { + border-bottom: none; + } + + .dropdown-item:hover { + 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 053e181..a75b4e0 100644 --- a/src/routes/admin/posts/new/+page.server.ts +++ b/src/routes/admin/posts/new/+page.server.ts @@ -27,8 +27,7 @@ export const actions: Actions = { 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 slug = formData.get('slug')?.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)); @@ -39,10 +38,10 @@ export const actions: Actions = { 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) + `INSERT INTO posts (title, slug, 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 }>(); + ).bind(title, slug, content, categoryId).first<{ id: number }>(); const postId = res?.id; diff --git a/src/routes/admin/posts/new/+page.svelte b/src/routes/admin/posts/new/+page.svelte index 628304f..0f4d30b 100644 --- a/src/routes/admin/posts/new/+page.svelte +++ b/src/routes/admin/posts/new/+page.svelte @@ -1,12 +1,17 @@
-

新文章

+

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

← 返回
@@ -33,7 +159,7 @@
-
+
- - -
+
+ + +
+ {#if mode === 'edit'} +
+ + + + +
+ + + + +
+
+ {#if showAdmonitionMenu} + + + + {/if} + + {#if showAdmonitionMenu} +
+ {#each admonitionTypes as item} + + {/each} +
+ {/if} +
+ +
+ {/if} +
+ + +
+
+
-
- - +
+ {#if mode === 'edit'} + + {:else} +
+ +
+ {/if} +
@@ -82,7 +248,7 @@ type="text" id="slug" name="slug" - class="select-input" + class="sidebar-input" bind:value={slug} placeholder="hello-world" required @@ -99,19 +265,51 @@
- {#if data.tags.length > 0} -
@@ -156,14 +354,14 @@ } .btn-primary:hover { - background-color: var(--accent-color, #bd6262); - border-color: var(--accent-color, #bd6262); + background-color: var(--accent-color, #419b38); + border-color: var(--accent-color, #419b38); color: white; } .btn-secondary:hover { - border-color: var(--accent-color, #bd6262); - color: var(--accent-color, #bd6262); + border-color: var(--accent-color, #419b38); + color: var(--accent-color, #419b38); } .error-alert { @@ -175,90 +373,101 @@ font-weight: 600; } + .editor-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + } + + .editor-tabs { + display: flex; + gap: 8px; + } + + .tab-btn { + background: none; + border: none; + padding: 4px 12px; + font-size: 0.85rem; + font-weight: 600; + color: rgba(0, 0, 0, 0.4); + cursor: pointer; + border-radius: 4px; + transition: all 0.2s ease; + } + + .tab-btn:hover { + background: rgba(0, 0, 0, 0.05); + } + + .tab-btn.active { + color: var(--accent-color, #419b38); + background: rgba(65, 155, 56, 0.1); + } + + .preview-area { + padding: 20px; + border: 1px solid rgba(0, 0, 0, 0.12); + background: rgba(0, 0, 0, 0.015); + min-height: 400px; + max-height: 600px; + overflow-y: auto; + } + .form-grid { - display: block; + display: grid; + grid-template-columns: 1fr 280px; + gap: 40px; + align-items: start; } .form-main { - max-width: 800px; - margin: 0 auto; + width: 100%; } .form-sidebar { - position: fixed; - top: 120px; - right: calc((100vw - 1024px) / 2 - 280px); - width: 260px; + position: sticky; + top: 80px; + width: 100%; display: flex; flex-direction: column; gap: 30px; z-index: 10; } - @media (max-width: 1580px) { - .form-sidebar { - right: 20px; + @media (max-width: 900px) { + .form-grid { + grid-template-columns: 1fr; } - } - - @media (max-width: 1200px) { + .form-sidebar { position: static; - width: 100%; - margin-top: 40px; flex-direction: row; flex-wrap: wrap; - gap: 40px; + gap: 20px; + margin-top: 20px; } } .form-group { - margin-bottom: 30px; - } - - .form-group.inline-group { - display: flex; - align-items: center; - gap: 20px; + margin-bottom: 20px; } .form-group label { display: block; font-weight: 700; - margin-bottom: 12px; + margin-bottom: 6px; color: var(--text-color); font-size: 0.9rem; letter-spacing: 0.5px; } - .form-group.inline-group label { - margin-bottom: 0; - width: 55px; /* Fixed width for alignment */ - flex-shrink: 0; - } - - .form-group input[type="text"] { - width: 100%; - padding: 10px 0; - border: none; - border-bottom: 2px solid rgba(0, 0, 0, 0.1); - background: transparent; - font-family: inherit; - font-size: 1.05rem; - color: var(--text-color); - box-sizing: border-box; - transition: border-bottom-color 0.3s ease; - } - - .form-group input[type="text"]:focus, - .select-input:focus { - outline: none; - border-bottom-color: var(--accent-color, #bd6262); - } - + .form-group input[type="text"], .form-group textarea { + width: 100%; - padding: 16px; + padding: 12px 14px; border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 0; background: rgba(0, 0, 0, 0.015); @@ -267,12 +476,29 @@ color: var(--text-color); box-sizing: border-box; transition: border-color 0.3s ease, box-shadow 0.3s ease; + } + + .form-group textarea { resize: vertical; line-height: 1.8; min-height: 120px; } - .select-input { + .form-group input[type="text"]:focus, + .form-group textarea:focus { + outline: none; + border-color: var(--accent-color, #419b38); + box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08); + } + + .select-input:focus, + .sidebar-input:focus { + outline: none; + border-bottom-color: var(--accent-color, #419b38); + } + + .select-input, + .sidebar-input { width: 100%; padding: 10px 0; border: none; @@ -283,13 +509,16 @@ color: var(--text-color); box-sizing: border-box; transition: border-bottom-color 0.3s ease; + } + + .select-input { cursor: pointer; } .form-group textarea:focus { outline: none; - border-color: var(--accent-color, #bd6262); - box-shadow: 0 0 0 3px rgba(189, 98, 98, 0.08); + border-color: var(--accent-color, #419b38); + box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08); } .sidebar-box { @@ -309,25 +538,181 @@ border-bottom: none; } - .tags-list { + .selected-tags-vertical { display: flex; flex-direction: column; - gap: 12px; + gap: 6px; + margin-bottom: 15px; } - .tag-item { + .selected-tag-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 10px; + border: 1px solid var(--text-color); + background: transparent; + font-weight: 600; + font-size: 0.9rem; + } + + .remove-tag-btn { + background: transparent; + border: none; + color: var(--text-color); + font-size: 1.1rem; + cursor: pointer; + opacity: 0.7; + padding: 0 4px; + } + + .remove-tag-btn:hover { + opacity: 1; + color: #d9534f; + } + + .tag-input-container { + position: relative; + } + + .tag-autocomplete-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-color); + border: 1px solid var(--text-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15); + z-index: 50; + max-height: 200px; + overflow-y: auto; + display: flex; + flex-direction: column; + margin-top: 4px; + } + + .tag-dropdown-item { + background: transparent; + border: none; + border-bottom: 1px solid rgba(0,0,0,0.1); + padding: 8px 12px; + text-align: left; + font-family: inherit; + font-size: 0.85rem; + color: var(--text-color); + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + } + + .tag-dropdown-item:last-child { + border-bottom: none; + } + + .tag-dropdown-item:hover { + background: var(--text-color); + color: var(--bg-color); + } + + + .editor-header-right { display: flex; align-items: center; - gap: 10px; - cursor: pointer; - font-size: 0.95rem; + gap: 15px; } - .tag-item input[type="checkbox"] { - accent-color: var(--accent-color, #bd6262); - width: 16px; - height: 16px; - margin: 0; - cursor: pointer; + .md-toolbar { + display: flex; + align-items: center; + gap: 4px; } + + .md-btn { + background: transparent; + border: none; + padding: 4px; + cursor: pointer; + color: var(--text-color); + opacity: 0.65; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 2px; + } + + .md-btn:hover { + opacity: 1; + color: var(--accent-color, #419b38); + background-color: rgba(0,0,0,0.03); + } + + .md-btn svg { + width: 18px; + height: 18px; + stroke: currentColor; + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; + } + + .md-divider { + width: 2px; + height: 16px; + background: var(--text-color); + opacity: 0.15; + margin: 0 4px; + } + + .toolbar-dropdown-wrapper { + position: relative; + display: inline-block; + } + + .dropdown-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 99; + } + + .toolbar-dropdown { + position: absolute; + top: 100%; + left: 0; + margin-top: 4px; + background: var(--bg-color, #ffffff); + border: 1px solid var(--text-color); + z-index: 100; + width: 140px; + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + } + + .dropdown-item { + background: transparent; + border: none; + border-bottom: 1px solid rgba(0,0,0,0.1); + padding: 8px 12px; + text-align: left; + font-family: inherit; + font-size: 0.85rem; + color: var(--text-color); + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + } + + .dropdown-item:last-child { + border-bottom: none; + } + + .dropdown-item:hover { + background: var(--text-color); + color: var(--bg-color, #ffffff); + } + diff --git a/src/routes/admin/tags/+page.server.ts b/src/routes/admin/tags/+page.server.ts index a02ff97..6fcfea5 100644 --- a/src/routes/admin/tags/+page.server.ts +++ b/src/routes/admin/tags/+page.server.ts @@ -40,8 +40,10 @@ export const actions: Actions = { } try { - await db.prepare('INSERT INTO tags (name) VALUES (?)').bind(name).run(); - return { success: true }; + // 在 D1 中获取刚插入的 ID,可以通过 RETURNING id 语法 + 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) { console.error('Error creating tag:', err); if (err?.message?.includes('UNIQUE constraint failed')) { diff --git a/src/routes/admin/tags/+page.svelte b/src/routes/admin/tags/+page.svelte index 8049796..231cf1f 100644 --- a/src/routes/admin/tags/+page.svelte +++ b/src/routes/admin/tags/+page.svelte @@ -1,4 +1,5 @@
-

标签管理

+

{$page.data.aliases?.tag || '标签'}管理

{#if form?.error} @@ -17,9 +18,9 @@ {/if}
- +
-

新标签

+

新{$page.data.aliases?.tag || '标签'}

@@ -36,18 +37,18 @@
- +
{#if data.tags.length === 0}
-

暂无任何标签,在左侧添加一个吧!

+

暂无任何{$page.data.aliases?.tag || '标签'},在左侧添加一个吧!

{:else}
+ + ID 标题 分类 别名 (Slug) 作者 发布时间操作
+ + {post.id} - {post.title} + {post.title} - + {post.category || '未分类'} {post.slug} {post.author || '未知'} {post.dateStr} - 编辑 -
{ - if(confirm('确定要删除这篇文章吗?操作不可恢复。')) e.currentTarget.submit(); - }}> - - -
-
- + @@ -60,7 +61,7 @@
ID标签名称{$page.data.aliases?.tag || '标签'}名称 关联文章数 操作
{tag.post_count} 篇文章
{ - if(!confirm(`确定要删除标签 "${tag.name}" 吗?`)) e.preventDefault(); + if(!confirm(`确定要删除${$page.data.aliases?.tag || '标签'} "${tag.name}" 吗?`)) e.preventDefault(); }}> @@ -153,7 +154,7 @@ .form-group input[type="text"]:focus { outline: none; - border-bottom-color: var(--accent-color, #bd6262); + border-bottom-color: var(--accent-color, #419b38); } .btn { @@ -179,8 +180,8 @@ } .btn-primary:hover { - background-color: var(--accent-color, #bd6262); - border-color: var(--accent-color, #bd6262); + background-color: var(--accent-color, #419b38); + border-color: var(--accent-color, #419b38); color: white; }