feat: 实现文章管理功能,支持发布、编辑与删除

This commit is contained in:
seahi committed 2026-07-26 17:52:39 +08:00
1 parent b3456b8916
commit b4b2907d77
6 files changed
+1100

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ platform }) => {
const db = platform?.env?.DB;
if (!db) {
return {
posts: []
};
}
try {
let results;
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
FROM posts p
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();
results = queryRes.results;
} catch (e) {
// Fallback query if categories table or category_id column is missing
const queryRes = await db.prepare(
`SELECT p.id, p.title, p.slug, p.published_at as date, u.username as author
FROM posts p
LEFT JOIN users u ON p.author_id = u.id
ORDER BY p.published_at DESC`
).all();
results = queryRes.results.map(r => ({ ...r, category: '未分类' }));
}
// Format date for display
const formattedPosts = results.map((post) => {
const d = new Date(post.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 {
...post,
dateStr: `${year}-${month}-${day}`
};
});
return {
posts: formattedPosts
};
} catch (err) {
console.error('Error fetching admin posts list:', err);
return { posts: [] };
}
};
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 id = data.get('id');
if (!id) return { success: false, error: 'Missing ID' };
try {
await db.prepare('DELETE FROM posts WHERE id = ?').bind(id).run();
return { success: true };
} catch (err) {
console.error('Error deleting post:', err);
return { success: false, error: 'Failed to delete post' };
}
}
};
+194
View File
@@ -0,0 +1,194 @@
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<div class="admin-page-header">
<h2>文章管理</h2>
<a href="/admin/posts/new" class="btn btn-primary">发布新文章</a>
</div>
<div class="table-container">
{#if data.posts.length === 0}
<div class="empty-state">
<p>还没有发布任何文章,现在就开始写作吧!</p>
</div>
{:else}
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>分类</th>
<th>别名 (Slug)</th>
<th>作者</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{#each data.posts as post}
<tr>
<td>{post.id}</td>
<td>
<strong>{post.title}</strong>
<a href="/posts/{post.slug}" target="_blank" class="preview-link" title="前台预览">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
</a>
</td>
<td><span class="badge">{post.category || '未分类'}</span></td>
<td><span class="badge">{post.slug}</span></td>
<td>{post.author || '未知'}</td>
<td>{post.dateStr}</td>
<td class="actions">
<a href="/admin/posts/{post.id}" class="action-btn edit">编辑</a>
<form action="?/delete" method="POST" class="inline-form" on:submit|preventDefault={(e) => {
if(confirm('确定要删除这篇文章吗?操作不可恢复。')) e.currentTarget.submit();
}}>
<input type="hidden" name="id" value={post.id} />
<button type="submit" class="action-btn delete">删除</button>
</form>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<style>
.admin-page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
border-bottom: 2px solid var(--text-color);
padding-bottom: 12px;
}
.admin-page-header h2 {
margin: 0;
font-size: 2rem;
color: var(--text-color);
}
.btn {
display: inline-block;
padding: 8px 18px;
text-decoration: none;
font-size: 0.95rem;
font-weight: 600;
transition: all 0.2s;
cursor: pointer;
border: 2px solid var(--text-color);
background-color: transparent;
color: var(--text-color);
font-family: inherit;
}
.btn-primary {
background-color: var(--text-color);
color: var(--bg-color, #ffffff);
}
.btn-primary:hover {
background-color: var(--accent-color, #bd6262);
border-color: var(--accent-color, #bd6262);
color: white;
}
.table-container {
background: transparent;
border: 2px solid var(--text-color);
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.data-table th, .data-table td {
padding: 14px 18px;
border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.08));
}
.data-table th {
background-color: transparent;
color: var(--text-color);
font-weight: 700;
font-size: 0.95rem;
border-bottom: 2px solid var(--text-color);
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tbody tr:hover {
background-color: rgba(0,0,0,0.02);
}
.preview-link {
text-decoration: none;
font-size: 0.85rem;
margin-left: 6px;
opacity: 0.5;
transition: opacity 0.2s;
}
.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));
}
</style>
@@ -0,0 +1,85 @@
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 postId = params.id;
if (!db) {
throw error(500, '数据库未连接');
}
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 }>();
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: selectedTagRows } = await db.prepare('SELECT tag_id FROM post_tags WHERE post_id = ?').bind(postId).all();
const selectedTagIds = selectedTagRows.map((r: any) => r.tag_id as number);
return {
post,
tags: allTags as unknown as { id: number; name: string }[],
categories: allCategories as unknown as { id: number; name: string }[],
selectedTagIds
};
} catch (err: any) {
if (err?.status === 404) throw err;
console.error('Error loading post for edit:', err);
throw error(500, '读取文章详情失败');
}
};
export const actions: Actions = {
update: async ({ params, request, platform }) => {
const db = platform?.env?.DB;
const postId = 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 excerpt = formData.get('excerpt')?.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));
if (!title || !slug || !content) {
return fail(400, { error: '标题、URL 别名和文章正文为必填项' });
}
try {
// 更新文章表
await db.prepare(
`UPDATE posts
SET title = ?, slug = ?, excerpt = ?, content = ?, category_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`
).bind(title, slug, excerpt, content, categoryId, postId).run();
// 清除原有标签并重新绑定
await db.prepare('DELETE FROM post_tags WHERE post_id = ?').bind(postId).run();
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();
}
}
} catch (err: any) {
console.error('Error updating post:', err);
if (err?.message?.includes('UNIQUE constraint failed')) {
return fail(400, { error: 'URL 别名 (Slug) 已被其他文章占用' });
}
return fail(500, { error: '保存修改失败' });
}
throw redirect(303, '/admin/posts');
}
};
+350
View File
@@ -0,0 +1,350 @@
<script lang="ts">
import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props();
let title = $state(data.post.title);
let slug = $state(data.post.slug);
let excerpt = $state(data.post.excerpt || '');
let content = $state(data.post.content || '');
let selectedTags = $state<number[]>(data.selectedTagIds || []);
function toggleTag(tagId: number) {
if (selectedTags.includes(tagId)) {
selectedTags = selectedTags.filter(id => id !== tagId);
} else {
selectedTags = [...selectedTags, tagId];
}
}
function handleTitleInput(e: Event) {
const target = e.target as HTMLInputElement;
title = target.value;
}
</script>
<div class="admin-page-header">
<h2>编辑</h2>
<a href="/admin/posts" class="btn btn-secondary">&larr; 返回</a>
</div>
{#if form?.error}
<div class="error-alert">
{form.error}
</div>
{/if}
{#if form?.success}
<div class="success-alert">
文章更新成功!
<a href="/{data.post.slug}" target="_blank" class="preview-link">预览文章 &#8599;</a>
</div>
{/if}
<form method="POST" action="?/update" class="post-form">
<div class="form-grid">
<div class="form-main">
<div class="form-group inline-group">
<label for="title">标题 *</label>
<input
type="text"
id="title"
name="title"
bind:value={title}
oninput={handleTitleInput}
placeholder="输入标题..."
required
/>
</div>
<div class="form-group">
<label for="excerpt">摘要</label>
<textarea
id="excerpt"
name="excerpt"
bind:value={excerpt}
rows="3"
placeholder="简短介绍..."
></textarea>
</div>
<div class="form-group">
<label for="content">正文 *</label>
<textarea
id="content"
name="content"
bind:value={content}
rows="16"
placeholder="使用 Markdown 撰写..."
required
></textarea>
</div>
</div>
<div class="form-sidebar">
<div class="sidebar-box">
<h3>保存</h3>
<button type="submit" class="btn btn-primary submit-btn">保存</button>
</div>
<div class="sidebar-box">
<h3>别名 *</h3>
<input
type="text"
id="slug"
name="slug"
class="select-input"
bind:value={slug}
placeholder="hello-world"
required
/>
</div>
<div class="sidebar-box">
<h3>分类</h3>
<select name="category_id" class="select-input">
<option value="" selected={!data.post.category_id}>-- 无 --</option>
{#each data.categories as category}
<option value={category.id} selected={data.post.category_id === category.id}>{category.name}</option>
{/each}
</select>
</div>
{#if data.tags.length > 0}
<div class="sidebar-box">
<h3>标签</h3>
<div class="tags-list">
{#each data.tags as tag}
<label class="tag-item">
<input
type="checkbox"
name="tags"
value={tag.id}
checked={selectedTags.includes(tag.id)}
onchange={() => toggleTag(tag.id)}
/>
<span>{tag.name}</span>
</label>
{/each}
</div>
</div>
{/if}
</div>
</div>
</form>
<style>
.admin-page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
border-bottom: 2px solid var(--text-color);
padding-bottom: 12px;
}
.admin-page-header h2 {
margin: 0;
font-size: 2rem;
color: var(--text-color);
}
.btn {
display: inline-block;
padding: 10px 20px;
text-decoration: none;
font-size: 0.95rem;
font-weight: 600;
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid var(--text-color);
background-color: transparent;
color: var(--text-color);
font-family: inherit;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary {
background-color: var(--text-color);
color: var(--bg-color, #ffffff);
width: 100%;
text-align: center;
}
.btn-primary:hover {
background-color: var(--accent-color, #bd6262);
border-color: var(--accent-color, #bd6262);
color: white;
}
.btn-secondary:hover {
border-color: var(--accent-color, #bd6262);
color: var(--accent-color, #bd6262);
}
.error-alert {
padding: 12px 16px;
background-color: transparent;
border-left: 4px solid #d9534f;
color: #d9534f;
margin-bottom: 30px;
font-weight: 600;
}
.form-grid {
display: block;
}
.form-main {
max-width: 800px;
margin: 0 auto;
}
.form-sidebar {
position: fixed;
top: 120px;
right: calc((100vw - 1024px) / 2 - 280px);
width: 260px;
display: flex;
flex-direction: column;
gap: 30px;
z-index: 10;
}
@media (max-width: 1580px) {
.form-sidebar {
right: 20px;
}
}
@media (max-width: 1200px) {
.form-sidebar {
position: static;
width: 100%;
margin-top: 40px;
flex-direction: row;
flex-wrap: wrap;
gap: 40px;
}
}
.form-group {
margin-bottom: 30px;
}
.form-group.inline-group {
display: flex;
align-items: center;
gap: 20px;
}
.form-group label {
display: block;
font-weight: 700;
margin-bottom: 12px;
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 textarea {
width: 100%;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 0;
background: rgba(0, 0, 0, 0.015);
font-family: inherit;
font-size: 1.05rem;
color: var(--text-color);
box-sizing: border-box;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
resize: vertical;
line-height: 1.8;
min-height: 120px;
}
.select-input {
width: 100%;
padding: 10px 0;
border: none;
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
background: transparent;
font-family: inherit;
font-size: 1rem;
color: var(--text-color);
box-sizing: border-box;
transition: border-bottom-color 0.3s ease;
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);
}
.sidebar-box {
background: transparent;
border: none;
border-radius: 0;
padding: 0;
}
.sidebar-box h3 {
margin: 0 0 20px 0;
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text-color);
border-bottom: none;
}
.tags-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.tag-item {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 0.95rem;
}
.tag-item input[type="checkbox"] {
accent-color: var(--accent-color, #bd6262);
width: 16px;
height: 16px;
margin: 0;
cursor: pointer;
}
</style>
@@ -0,0 +1,64 @@
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ platform }) => {
const db = platform?.env?.DB;
let tags: { id: number; name: string }[] = [];
let categories: { id: number; name: string }[] = [];
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();
tags = tagResults as unknown as { id: number; name: string }[];
categories = catResults as unknown as { id: number; name: string }[];
} catch (err) {
console.error('Error fetching tags or categories:', err);
}
}
return { tags, categories };
};
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 excerpt = formData.get('excerpt')?.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));
if (!title || !slug || !content) {
return fail(400, { error: '标题、URL 别名和文章正文为必填项' });
}
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)
RETURNING id`
).bind(title, slug, excerpt, content, categoryId).first<{ id: number }>();
const postId = res?.id;
if (postId && selectedTags.length > 0) {
for (const tagId of selectedTags) {
await db.prepare('INSERT INTO post_tags (post_id, tag_id) VALUES (?, ?)').bind(postId, tagId).run();
}
}
} catch (err: any) {
console.error('Error creating post:', err);
if (err?.message?.includes('UNIQUE constraint failed')) {
return fail(400, { error: 'URL 别名 (Slug) 已被占用,请使用其他别名' });
}
return fail(500, { error: '保存文章失败' });
}
throw redirect(303, '/admin/posts');
}
};
+333
View File
@@ -0,0 +1,333 @@
<script lang="ts">
import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props();
let title = $state('');
let slug = $state('');
let excerpt = $state('');
let content = $state('');
// 自动从标题生成 Slug 简易逻辑
function handleTitleInput() {
if (!slug) {
slug = title
.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
}
}
</script>
<div class="admin-page-header">
<h2>新文章</h2>
<a href="/admin/posts" class="btn btn-secondary">&larr; 返回</a>
</div>
{#if form?.error}
<div class="error-alert">
{form.error}
</div>
{/if}
<form method="POST" class="post-form">
<div class="form-grid">
<div class="form-main">
<div class="form-group inline-group">
<label for="title">标题 *</label>
<input
type="text"
id="title"
name="title"
bind:value={title}
oninput={handleTitleInput}
placeholder="输入标题..."
required
/>
</div>
<div class="form-group">
<label for="excerpt">摘要</label>
<textarea
id="excerpt"
name="excerpt"
bind:value={excerpt}
rows="3"
placeholder="简短介绍..."
></textarea>
</div>
<div class="form-group">
<label for="content">正文 *</label>
<textarea
id="content"
name="content"
bind:value={content}
rows="16"
placeholder="使用 Markdown 撰写..."
required
></textarea>
</div>
</div>
<div class="form-sidebar">
<div class="sidebar-box">
<h3>发布</h3>
<button type="submit" class="btn btn-primary submit-btn">发布</button>
</div>
<div class="sidebar-box">
<h3>别名 *</h3>
<input
type="text"
id="slug"
name="slug"
class="select-input"
bind:value={slug}
placeholder="hello-world"
required
/>
</div>
<div class="sidebar-box">
<h3>分类</h3>
<select name="category_id" class="select-input">
<option value="">-- 无 --</option>
{#each data.categories as category}
<option value={category.id}>{category.name}</option>
{/each}
</select>
</div>
{#if data.tags.length > 0}
<div class="sidebar-box">
<h3>标签</h3>
<div class="tags-list">
{#each data.tags as tag}
<label class="tag-item">
<input type="checkbox" name="tags" value={tag.id} />
<span>{tag.name}</span>
</label>
{/each}
</div>
</div>
{/if}
</div>
</div>
</form>
<style>
.admin-page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
border-bottom: 2px solid var(--text-color);
padding-bottom: 12px;
}
.admin-page-header h2 {
margin: 0;
font-size: 2rem;
color: var(--text-color);
}
.btn {
display: inline-block;
padding: 10px 20px;
text-decoration: none;
font-size: 0.95rem;
font-weight: 600;
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid var(--text-color);
background-color: transparent;
color: var(--text-color);
font-family: inherit;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary {
background-color: var(--text-color);
color: var(--bg-color, #ffffff);
width: 100%;
text-align: center;
}
.btn-primary:hover {
background-color: var(--accent-color, #bd6262);
border-color: var(--accent-color, #bd6262);
color: white;
}
.btn-secondary:hover {
border-color: var(--accent-color, #bd6262);
color: var(--accent-color, #bd6262);
}
.error-alert {
padding: 12px 16px;
background-color: transparent;
border-left: 4px solid #d9534f;
color: #d9534f;
margin-bottom: 30px;
font-weight: 600;
}
.form-grid {
display: block;
}
.form-main {
max-width: 800px;
margin: 0 auto;
}
.form-sidebar {
position: fixed;
top: 120px;
right: calc((100vw - 1024px) / 2 - 280px);
width: 260px;
display: flex;
flex-direction: column;
gap: 30px;
z-index: 10;
}
@media (max-width: 1580px) {
.form-sidebar {
right: 20px;
}
}
@media (max-width: 1200px) {
.form-sidebar {
position: static;
width: 100%;
margin-top: 40px;
flex-direction: row;
flex-wrap: wrap;
gap: 40px;
}
}
.form-group {
margin-bottom: 30px;
}
.form-group.inline-group {
display: flex;
align-items: center;
gap: 20px;
}
.form-group label {
display: block;
font-weight: 700;
margin-bottom: 12px;
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 textarea {
width: 100%;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 0;
background: rgba(0, 0, 0, 0.015);
font-family: inherit;
font-size: 1.05rem;
color: var(--text-color);
box-sizing: border-box;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
resize: vertical;
line-height: 1.8;
min-height: 120px;
}
.select-input {
width: 100%;
padding: 10px 0;
border: none;
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
background: transparent;
font-family: inherit;
font-size: 1rem;
color: var(--text-color);
box-sizing: border-box;
transition: border-bottom-color 0.3s ease;
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);
}
.sidebar-box {
background: transparent;
border: none;
border-radius: 0;
padding: 0;
}
.sidebar-box h3 {
margin: 0 0 20px 0;
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text-color);
border-bottom: none;
}
.tags-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.tag-item {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 0.95rem;
}
.tag-item input[type="checkbox"] {
accent-color: var(--accent-color, #bd6262);
width: 16px;
height: 16px;
margin: 0;
cursor: pointer;
}
</style>