python
复制代码
import sqlite3
from datetime import datetime
class BlogDB:
def __init__(self, db_name='blog.db'):
self.db_name = db_name
self.init_db()
def init_db(self):
"""初始化数据库表"""
conn = sqlite3.connect(self.db_name)
cursor = conn.cursor()
# 创建分类表
cursor.execute('''
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建文章表
cursor.execute('''
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE SET NULL
)
''')
# 创建评论表
cursor.execute('''
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER,
author TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE
)
''')
# 添加一个默认分类
cursor.execute('''
INSERT OR IGNORE INTO categories (name, description) VALUES (?, ?)
''', ('未分类', '默认分类'))
conn.commit()
conn.close()
def get_connection(self):
"""获取数据库连接"""
conn = sqlite3.connect(self.db_name)
conn.row_factory = sqlite3.Row # 返回字典格式
return conn
# 文章相关操作
def create_post(self, title, content, category_id=None):
"""创建新文章"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO posts (title, content, category_id) VALUES (?, ?, ?)',
(title, content, category_id)
)
post_id = cursor.lastrowid
conn.commit()
conn.close()
return post_id
def get_all_posts(self, page=1, page_size=10):
"""获取所有文章,支持分页"""
conn = self.get_connection()
cursor = conn.cursor()
offset = (page - 1) * page_size
cursor.execute('SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?', (page_size, offset))
posts = cursor.fetchall()
conn.close()
return posts
def get_post_count(self):
"""获取文章总数"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM posts')
count = cursor.fetchone()[0]
conn.close()
return count
def get_post(self, post_id):
"""获取单篇文章"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM posts WHERE id = ?', (post_id,))
post = cursor.fetchone()
conn.close()
# 将sqlite3.Row对象转换为字典
return dict(post) if post else None
def update_post(self, post_id, title, content, category_id=None):
"""更新文章"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'UPDATE posts SET title = ?, content = ?, category_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
(title, content, category_id, post_id)
)
conn.commit()
conn.close()
return cursor.rowcount > 0
def delete_post(self, post_id):
"""删除文章"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM posts WHERE id = ?', (post_id,))
conn.commit()
conn.close()
return cursor.rowcount > 0
def search_posts(self, keyword, page=1, page_size=10):
"""根据关键词搜索文章,支持分页"""
conn = self.get_connection()
cursor = conn.cursor()
# 使用LIKE操作符搜索标题和内容
offset = (page - 1) * page_size
cursor.execute('''
SELECT * FROM posts
WHERE title LIKE ? OR content LIKE ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (f'%{keyword}%', f'%{keyword}%', page_size, offset))
posts = cursor.fetchall()
conn.close()
return posts
def get_search_count(self, keyword):
"""获取搜索结果的文章总数"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT COUNT(*) FROM posts
WHERE title LIKE ? OR content LIKE ?
''', (f'%{keyword}%', f'%{keyword}%'))
count = cursor.fetchone()[0]
conn.close()
return count
# 分类相关操作
def create_category(self, name, description=''):
"""创建分类"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO categories (name, description) VALUES (?, ?)',
(name, description)
)
category_id = cursor.lastrowid
conn.commit()
conn.close()
return category_id
def get_all_categories(self):
"""获取所有分类"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM categories ORDER BY created_at DESC')
categories = cursor.fetchall()
conn.close()
# 将sqlite3.Row对象转换为字典
return [dict(category) for category in categories]
def get_category(self, category_id):
"""获取单个分类"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT * FROM categories WHERE id = ?', (category_id,))
category = cursor.fetchone()
conn.close()
return category
def update_category(self, category_id, name, description=''):
"""更新分类"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'UPDATE categories SET name = ?, description = ? WHERE id = ?',
(name, description, category_id)
)
conn.commit()
conn.close()
return cursor.rowcount > 0
def delete_category(self, category_id):
"""删除分类"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('DELETE FROM categories WHERE id = ?', (category_id,))
conn.commit()
conn.close()
return cursor.rowcount > 0
def get_posts_by_category(self, category_id, page=1, page_size=10):
"""根据分类获取文章,支持分页"""
conn = self.get_connection()
cursor = conn.cursor()
offset = (page - 1) * page_size
cursor.execute('''
SELECT * FROM posts
WHERE category_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
''', (category_id, page_size, offset))
posts = cursor.fetchall()
conn.close()
return posts
def get_category_post_count(self, category_id):
"""获取分类下的文章总数"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM posts WHERE category_id = ?', (category_id,))
count = cursor.fetchone()[0]
conn.close()
return count
# 评论相关操作
def add_comment(self, post_id, author, content):
"""添加评论"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'INSERT INTO comments (post_id, author, content) VALUES (?, ?, ?)',
(post_id, author, content)
)
conn.commit()
conn.close()
def get_comments(self, post_id):
"""获取文章的评论"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC',
(post_id,)
)
comments = cursor.fetchall()
conn.close()
return comments
python
复制代码
from nicegui import ui
import database
# 初始化数据库
db = database.BlogDB()
def create_blog_ui():
"""创建博客界面"""
# 状态变量
current_post_id = None
edit_mode = False
search_keyword = ''
selected_category = None # 选中的分类ID,None表示不筛选
show_admin_buttons = False # 控制编辑/删除按钮的显示,默认为隐藏
current_page = 1
page_size = 5 # 每页显示5篇文章
total_posts = 0
# 设置全局样式
ui.query('body').style('background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;')
# 主容器 - 使用固定宽度居中布局
with ui.column().classes('w-full max-w-6xl mx-auto px-4 py-8'):
# 顶部标题栏
with ui.card().classes('w-full p-6 bg-gradient-to-r from-blue-600 to-indigo-700 shadow-lg mb-8 rounded-xl'):
with ui.row().classes('w-full justify-between items-center'):
ui.label('📝 我的博客系统').classes('text-3xl font-bold text-white')
with ui.row().classes('items-center gap-3'):
# 搜索和筛选区域
with ui.row().classes('items-center gap-2 bg-white bg-opacity-10 backdrop-blur-sm rounded-full px-3 py-1'):
# 搜索框
search_input = ui.input(placeholder='搜索文章...').classes('w-64 bg-transparent border-none text-white placeholder:text-white/70 focus:outline-none focus:ring-0')
# 分类筛选下拉框
categories = db.get_all_categories()
category_options = {None: '全部分类'}
if categories:
category_options.update({category['id']: category['name'] for category in categories})
category_filter = ui.select(category_options, value=None).classes('w-48 bg-transparent border-none text-white focus:outline-none focus:ring-0')
category_filter.props('hide-selected-icon hide-dropdown-icon')
def perform_search():
nonlocal search_keyword, selected_category, current_page
search_keyword = search_input.value.strip()
selected_category = category_filter.value
current_page = 1 # 搜索或筛选时重置到第一页
show_main_view()
# 搜索按钮
ui.button('🔍', on_click=perform_search).classes('bg-white text-blue-700 hover:bg-blue-100 px-3 py-1 rounded-full transition-all')
# 清空搜索
def clear_search():
nonlocal search_keyword, selected_category, current_page
search_keyword = ''
selected_category = None
search_input.value = ''
category_filter.value = None
current_page = 1 # 清空搜索时重置到第一页
show_main_view()
ui.button('🗑️', on_click=clear_search).classes('bg-white/20 text-white hover:bg-white/30 px-3 py-1 rounded-full transition-all')
# 操作按钮区域
with ui.row().classes('items-center gap-2'):
# 管理员控制按钮
def toggle_admin_buttons():
nonlocal show_admin_buttons
show_admin_buttons = not show_admin_buttons
show_main_view() # 刷新视图以更新按钮可见性
ui.button('⚙️ 管理模式', on_click=toggle_admin_buttons).classes(f'px-4 py-2 rounded-lg transition-all {"bg-purple-600 text-white hover:bg-purple-700 shadow-lg" if show_admin_buttons else "bg-purple-500 text-white hover:bg-purple-600"}')
# 刷新列表
ui.button('🔄', on_click=lambda: clear_search()).classes('bg-gray-500 text-white hover:bg-gray-600 px-4 py-2 rounded-lg transition-all')
# 分类管理按钮
def open_category_management():
category_dialog.open()
ui.button('🏷️', on_click=open_category_management).classes('bg-orange-500 text-white hover:bg-orange-600 px-4 py-2 rounded-lg transition-all')
# 写新文章
ui.button('✍️ 写新文章', on_click=lambda: show_create_post()).classes('bg-green-500 text-white hover:bg-green-600 px-4 py-2 rounded-lg font-semibold transition-all shadow-md hover:shadow-lg')
# 分类管理对话框
with ui.dialog() as category_dialog:
with ui.card().classes('w-full max-w-4xl p-8 rounded-2xl shadow-xl border border-gray-100'):
ui.label('🏷️ 分类管理').classes('text-3xl font-bold text-gradient-to-r from-orange-500 to-red-600 mb-8')
# 分类列表容器
categories_container = ui.column().classes('mb-8 gap-6')
# 添加分类表单
with ui.card().classes('w-full p-8 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl shadow-md border border-gray-100 mb-8'):
ui.label('添加新分类').classes('text-2xl font-bold text-gray-800 mb-6')
with ui.grid(columns=2).classes('gap-6 mb-6'):
name_input = ui.input(placeholder='分类名称').classes('w-full p-4 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all')
description_input = ui.textarea(placeholder='分类描述').classes('w-full p-4 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all resize-y')
def add_category():
name = name_input.value.strip()
description = description_input.value.strip()
if not name:
ui.notify('请输入分类名称', type='warning')
return
try:
db.create_category(name, description)
ui.notify(f'分类 "{name}" 创建成功!', type='positive')
name_input.value = ''
description_input.value = ''
load_categories()
except Exception as e:
ui.notify(f'创建分类失败: {str(e)}', type='negative')
ui.button('添加分类', on_click=add_category).classes('bg-gradient-to-r from-orange-500 to-red-600 text-white hover:from-orange-600 hover:to-red-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
# 加载分类列表
def load_categories():
categories_container.clear()
categories = db.get_all_categories()
if not categories:
with categories_container:
with ui.card().classes('text-center p-10 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl shadow-md border border-gray-100'):
ui.icon('tag', size='2xl').classes('text-orange-400 mb-4')
ui.label('暂无分类').classes('text-xl font-semibold text-gray-600 mb-2')
ui.label('请先添加分类').classes('text-gray-500')
return
with categories_container:
with ui.grid(columns=1).classes('gap-6'):
for category in categories:
with ui.card().classes('w-full p-6 rounded-xl shadow-md hover:shadow-xl transition-all duration-300 hover:-translate-y-1 border border-gray-100'):
with ui.row().classes('justify-between items-start'):
with ui.column().classes('flex-grow'):
ui.label(category['name']).classes('text-2xl font-bold text-gray-800')
if category['description']:
ui.label(category['description']).classes('text-gray-600 mt-2 text-lg leading-relaxed')
ui.label(f'文章数量: {db.get_category_post_count(category["id"])}').classes('text-sm text-gray-500 mt-3 bg-orange-100 px-3 py-1 rounded-full inline-block')
with ui.column(align_items='end'):
# 编辑按钮
def edit_category(category_id, category_name, category_desc):
# 创建编辑对话框
with ui.dialog() as edit_dialog:
with ui.card().classes('p-8 w-full max-w-md rounded-xl shadow-lg border border-gray-100'):
ui.label('编辑分类').classes('text-2xl font-bold text-gray-800 mb-6')
edit_name_input = ui.input(value=category_name, placeholder='分类名称').classes('w-full p-4 border-2 border-gray-200 rounded-xl mb-6 focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all')
edit_desc_input = ui.textarea(value=category_desc, placeholder='分类描述').classes('w-full p-4 border-2 border-gray-200 rounded-xl mb-6 focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all resize-y')
def save_edit():
new_name = edit_name_input.value.strip()
new_desc = edit_desc_input.value.strip()
if not new_name:
ui.notify('请输入分类名称', type='warning')
return
try:
db.update_category(category_id, new_name, new_desc)
ui.notify(f'分类更新成功!', type='positive')
edit_dialog.close()
load_categories()
except Exception as e:
ui.notify(f'更新分类失败: {str(e)}', type='negative')
with ui.row().classes('justify-end gap-4'):
ui.button('取消', on_click=edit_dialog.close).classes('bg-gradient-to-r from-gray-500 to-slate-600 text-white hover:from-gray-600 hover:to-slate-700 px-6 py-3 rounded-lg transition-all shadow-md hover:shadow-lg')
ui.button('保存修改', on_click=save_edit).classes('bg-gradient-to-r from-orange-500 to-red-600 text-white hover:from-orange-600 hover:to-red-700 px-6 py-3 rounded-lg transition-all shadow-md hover:shadow-lg')
edit_dialog.open()
# 删除按钮
def delete_category(category_id, category_name):
# 确认删除
def confirm_delete():
try:
# 检查是否有文章使用该分类
post_count = db.get_category_post_count(category_id)
if post_count > 0:
ui.notify(f'该分类下有 {post_count} 篇文章,无法删除', type='warning')
return
db.delete_category(category_id)
ui.notify(f'分类 "{category_name}" 删除成功!', type='positive')
load_categories()
except Exception as e:
ui.notify(f'删除分类失败: {str(e)}', type='negative')
with ui.dialog() as delete_dialog:
with ui.card().classes('p-8 w-96 rounded-xl shadow-lg border border-gray-100'):
ui.icon('warning', color='red', size='2xl').classes('mx-auto mb-6')
ui.label('确认删除').classes('text-2xl font-bold text-center mb-3')
ui.label(f'确定要删除分类 "{category_name}"吗?此操作不可恢复。').classes('text-gray-600 text-center mb-8')
with ui.row().classes('justify-center gap-4'):
ui.button('取消', on_click=delete_dialog.close).classes('bg-gradient-to-r from-gray-500 to-slate-600 text-white hover:from-gray-600 hover:to-slate-700 px-6 py-3 rounded-lg transition-all shadow-md hover:shadow-lg')
ui.button('确定删除', on_click=lambda: [delete_dialog.close(), confirm_delete()]).classes('bg-gradient-to-r from-red-500 to-rose-600 text-white hover:from-red-600 hover:to-rose-700 px-6 py-3 rounded-lg transition-all shadow-md hover:shadow-lg')
delete_dialog.open()
with ui.row().classes('gap-3 mt-2'):
ui.button('✏️ 编辑', on_click=lambda c=category: edit_category(c['id'], c['name'], c['description'])).classes('bg-gradient-to-r from-yellow-500 to-amber-600 text-white hover:from-yellow-600 hover:to-amber-700 px-5 py-2 rounded-lg transition-all shadow-sm hover:shadow-md')
ui.button('🗑️ 删除', on_click=lambda c=category: delete_category(c['id'], c['name'])).classes('bg-gradient-to-r from-red-500 to-rose-600 text-white hover:from-red-600 hover:to-rose-700 px-5 py-2 rounded-lg transition-all shadow-sm hover:shadow-md')
# 加载初始分类列表
load_categories()
# 主内容区域
main_container = ui.column().classes('w-full')
edit_container = ui.column().classes('w-full')
def show_main_view():
"""显示主视图(文章列表)"""
edit_container.visible = False
main_container.clear()
main_container.visible = True
nonlocal current_page, total_posts
with main_container:
# 页面标题和筛选条件
if search_keyword or selected_category is not None:
with ui.row().classes('items-center mb-6 max-w-6xl mx-auto'):
ui.label('📰 博客文章列表').classes('text-2xl font-bold text-gray-800 mr-4')
# 显示筛选条件
filters = []
if search_keyword:
filters.append(f'搜索:"{search_keyword}"')
if selected_category is not None:
category_name = db.get_category(selected_category)['name']
filters.append(f'分类:"{category_name}"')
if filters:
ui.label(' | '.join(filters)).classes('text-lg font-semibold text-gray-600')
else:
with ui.row().classes('items-center mb-6 max-w-6xl mx-auto'):
ui.label('📰 博客文章列表').classes('text-2xl font-bold text-gray-800 mb-6')
# 文章列表
if search_keyword:
# 获取搜索结果,支持分页
posts = db.search_posts(search_keyword, page=current_page, page_size=page_size)
total_posts = db.get_search_count(search_keyword)
elif selected_category is not None:
# 按分类筛选
posts = db.get_posts_by_category(selected_category, page=current_page, page_size=page_size)
total_posts = db.get_category_post_count(selected_category)
else:
# 获取所有文章,支持分页
posts = db.get_all_posts(page=current_page, page_size=page_size)
total_posts = db.get_post_count()
if not posts:
with ui.card().classes('w-full p-8 text-center'):
ui.icon('edit_note', size='xl').classes('text-gray-400 mb-4')
ui.label('暂无文章').classes('text-xl text-gray-600 mb-2')
ui.label('点击"写新文章"按钮开始您的创作之旅').classes('text-gray-500')
return
# 文章网格布局
with ui.grid(columns=1).classes('w-full gap-6 max-w-6xl mx-auto'):
for post in posts:
with ui.card().classes('w-full overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all duration-300 hover:-translate-y-1 border border-gray-100'):
# 卡片头部 - 背景色区分
with ui.row().classes('justify-between items-start p-5 bg-gradient-to-r from-blue-50 to-indigo-50'):
ui.label(post['title']).classes('text-2xl font-bold text-gray-800 truncate flex-grow')
ui.label(f'📅 {post["created_at"]}').classes('text-sm text-gray-500 ml-4 whitespace-nowrap')
# 卡片内容
with ui.column().classes('p-5'):
# 分类标签
category = db.get_category(post['category_id'])
with ui.row().classes('mb-4'):
ui.label(f'🏷️ {category["name"]}').classes('px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium')
# 文章内容预览
preview = post['content']
if len(preview) > 300:
preview = preview[:300] + '...'
ui.label(preview).classes('text-gray-700 mb-6 leading-relaxed line-clamp-3')
# 操作按钮区域
with ui.row().classes('justify-between items-center'):
with ui.row().classes('gap-2'):
ui.button('👁️ 查看详情', on_click=lambda p=post: show_post_detail(p['id'])).classes('bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700 px-4 py-2 rounded-lg transition-all shadow-sm hover:shadow-md')
# 编辑按钮 - 仅在管理模式下显示
if show_admin_buttons:
ui.button('✏️ 编辑', on_click=lambda p=post: show_edit_post(p['id'])).classes('bg-gradient-to-r from-yellow-500 to-amber-600 text-white hover:from-yellow-600 hover:to-amber-700 px-4 py-2 rounded-lg transition-all shadow-sm hover:shadow-md')
# 删除按钮 - 带确认,仅在管理模式下显示
def delete_with_confirm(p_id, p_title):
def confirm_delete():
db.delete_post(p_id)
ui.notify(f'文章"{p_title}"已删除', type='positive')
# 重新计算总文章数
nonlocal total_posts, current_page
if search_keyword:
total_posts = db.get_search_count(search_keyword)
elif selected_category is not None:
total_posts = db.get_category_post_count(selected_category)
else:
total_posts = db.get_post_count()
# 如果当前页没有文章了,返回上一页
if total_posts < (current_page - 1) * page_size + 1:
current_page = max(1, current_page - 1)
show_main_view()
# 使用对话框确认
with ui.dialog() as dialog:
with ui.card().classes('p-6 w-96 rounded-xl shadow-lg'):
ui.icon('warning', color='red', size='xl').classes('mx-auto mb-4')
ui.label('确认删除').classes('text-xl font-bold text-center mb-2')
ui.label(f'确定要删除文章"{p_title}"吗?此操作不可恢复。').classes('text-gray-600 text-center mb-6')
with ui.row().classes('justify-center gap-4'):
ui.button('确定删除', on_click=lambda: [dialog.close(), confirm_delete()]).classes('bg-red-500 text-white hover:bg-red-600 px-4 py-2 rounded-lg transition-all')
ui.button('取消', on_click=dialog.close).classes('bg-gray-200 text-gray-700 hover:bg-gray-300 px-4 py-2 rounded-lg transition-all')
dialog.open()
ui.button('🗑️ 删除', on_click=lambda p=post: delete_with_confirm(p['id'], p['title'])).classes('bg-gradient-to-r from-red-500 to-rose-600 text-white hover:from-red-600 hover:to-rose-700 px-4 py-2 rounded-lg transition-all shadow-sm hover:shadow-md')
# 分页控件
if total_posts > page_size:
total_pages = (total_posts + page_size - 1) // page_size
# 分页按钮点击处理函数
def go_to_previous_page():
nonlocal current_page
if current_page > 1:
current_page -= 1
show_main_view()
def go_to_next_page():
nonlocal current_page
if current_page < total_pages:
current_page += 1
show_main_view()
def go_to_first_page():
nonlocal current_page
current_page = 1
show_main_view()
def go_to_last_page():
nonlocal current_page
current_page = total_pages
show_main_view()
with ui.row().classes('justify-center items-center gap-2 mt-8 max-w-6xl mx-auto'):
# 首页按钮
ui.button('首页',
on_click=go_to_first_page).classes('bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700 px-4 py-2 rounded-lg text-sm transition-all shadow-sm hover:shadow-md')
# 上一页按钮
ui.button('◀️ 上一页',
on_click=go_to_previous_page).classes('bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700 px-4 py-2 rounded-lg transition-all shadow-sm hover:shadow-md')
# 页码显示
ui.label(f'第 {current_page} / {total_pages} 页').classes('text-gray-600 font-semibold px-4')
# 下一页按钮
ui.button('下一页 ▶️',
on_click=go_to_next_page).classes('bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700 px-4 py-2 rounded-lg transition-all shadow-sm hover:shadow-md')
# 末页按钮
ui.button('末页',
on_click=go_to_last_page).classes('bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700 px-4 py-2 rounded-lg text-sm transition-all shadow-sm hover:shadow-md')
def show_create_post():
"""显示创建文章界面"""
nonlocal current_post_id, edit_mode
current_post_id = None
edit_mode = True
main_container.visible = False
edit_container.clear()
edit_container.visible = True
with edit_container:
# 页面标题
with ui.row().classes('items-center mb-6 max-w-6xl mx-auto'):
ui.icon('add_circle', color='green', size='lg')
ui.label('创作新文章').classes('text-2xl font-bold text-gray-800 ml-2')
# 表单区域
with ui.card().classes('w-full p-8 rounded-xl shadow-md border border-gray-100 mb-8 max-w-6xl mx-auto'):
# 标题输入
ui.label('文章标题').classes('text-xl font-semibold text-gray-800 mb-3')
title_input = ui.input(placeholder='请输入文章标题...').classes('w-full p-4 border-2 border-gray-200 rounded-xl mb-6 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all')
# 分类选择
ui.label('文章分类').classes('text-xl font-semibold text-gray-800 mb-3')
categories = db.get_all_categories()
if not categories:
# 如果没有分类,创建一个默认分类
db.create_category('未分类', '默认分类')
categories = db.get_all_categories()
# 准备分类选项 (id: name)
category_options = {category['id']: category['name'] for category in categories}
# 强制转换为整数类型,确保与category_options的键类型一致
default_category_id = int(categories[0]['id'])
category_select = ui.select(category_options, value=default_category_id).classes('w-full p-4 border-2 border-gray-200 rounded-xl mb-6 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all')
category_select.props('hide-selected-icon')
# 内容输入
ui.label('文章内容').classes('text-xl font-semibold text-gray-800 mb-3')
content_input = ui.textarea(placeholder='开始创作您的内容...').classes('w-full h-96 p-4 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all resize-y')
# 操作按钮
with ui.row().classes('justify-end gap-4 mt-8'):
ui.button('保存文章', on_click=lambda: save_post(title_input, content_input, category_select)).classes('bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:from-green-600 hover:to-emerald-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
ui.button('取消', on_click=show_main_view).classes('bg-gradient-to-r from-gray-500 to-slate-600 text-white hover:from-gray-600 hover:to-slate-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
def show_edit_post(post_id):
"""显示编辑文章界面"""
nonlocal current_post_id, edit_mode
current_post_id = post_id
edit_mode = True
post = db.get_post(post_id)
if not post:
ui.notify('文章不存在', type='warning')
show_main_view()
return
main_container.visible = False
edit_container.clear()
edit_container.visible = True
with edit_container:
# 页面标题
with ui.row().classes('items-center mb-6 max-w-6xl mx-auto'):
ui.icon('edit', color='yellow', size='lg')
ui.label('编辑文章').classes('text-2xl font-bold text-gray-800 ml-2')
# 表单区域
with ui.card().classes('w-full p-8 rounded-xl shadow-md border border-gray-100 mb-8 max-w-6xl mx-auto'):
# 标题输入
ui.label('文章标题').classes('text-xl font-semibold text-gray-800 mb-3')
title_input = ui.input(placeholder='请输入文章标题...').classes('w-full p-4 border-2 border-gray-200 rounded-xl mb-6 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all')
title_input.value = post['title']
# 分类选择
ui.label('文章分类').classes('text-xl font-semibold text-gray-800 mb-3')
categories = db.get_all_categories()
if not categories:
# 如果没有分类,创建一个默认分类
db.create_category('未分类', '默认分类')
categories = db.get_all_categories()
# 准备分类选项 (id: name)
category_options = {category['id']: category['name'] for category in categories}
# 设置当前文章的分类
# 强制转换为整数类型,确保与category_options的键类型一致
if 'category_id' in post and post['category_id'] is not None:
current_category = int(post['category_id'])
else:
current_category = int(categories[0]['id']) if categories else None
category_select = ui.select(category_options, value=current_category).classes('w-full p-4 border-2 border-gray-200 rounded-xl mb-6 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all')
category_select.props('hide-selected-icon')
# 内容输入
ui.label('文章内容').classes('text-xl font-semibold text-gray-800 mb-3')
content_input = ui.textarea(placeholder='开始编辑您的内容...').classes('w-full h-96 p-4 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all resize-y')
content_input.value = post['content']
# 操作按钮
with ui.row().classes('justify-end gap-4 mt-8'):
ui.button('保存修改', on_click=lambda: save_post(title_input, content_input, category_select)).classes('bg-gradient-to-r from-yellow-500 to-amber-600 text-white hover:from-yellow-600 hover:to-amber-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
ui.button('取消', on_click=show_main_view).classes('bg-gradient-to-r from-gray-500 to-slate-600 text-white hover:from-gray-600 hover:to-slate-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
def save_post(title_input, content_input, category_select):
"""保存文章"""
nonlocal current_post_id
title = title_input.value.strip()
content = content_input.value.strip()
category_id = category_select.value
if not title:
ui.notify('请输入文章标题', type='warning')
return
if not content:
ui.notify('请输入文章内容', type='warning')
return
try:
if current_post_id is None:
# 创建新文章
db.create_post(title, content, category_id)
ui.notify('🎉 文章创建成功!', type='positive')
else:
# 更新文章
db.update_post(current_post_id, title, content, category_id)
ui.notify('✅ 文章更新成功!', type='positive')
show_main_view()
except Exception as e:
ui.notify(f'保存失败: {str(e)}', type='negative')
def show_post_detail(post_id):
"""显示文章详情"""
nonlocal current_post_id
current_post_id = post_id
post = db.get_post(post_id)
if not post:
ui.notify('文章不存在', type='warning')
show_main_view()
return
main_container.visible = False
edit_container.clear()
edit_container.visible = True
with edit_container:
# 返回按钮和标题
with ui.row().classes('justify-between items-center mb-8 max-w-6xl mx-auto'):
ui.button('← 返回列表', on_click=show_main_view).classes('bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700 px-6 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
ui.label('文章详情').classes('text-3xl font-bold text-gray-800')
# 文章内容卡片
with ui.card().classes('w-full p-10 bg-white rounded-2xl shadow-lg border border-gray-100 mb-8 max-w-6xl mx-auto'):
# 文章标题
ui.label(post['title']).classes('text-4xl font-bold text-gray-900 mb-6 leading-tight')
# 时间和分类信息
with ui.row().classes('items-center mb-8 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl'):
with ui.row().classes('items-center text-gray-600 mr-6'):
ui.icon('schedule', color='blue', size='md')
ui.label(f'发布时间: {post["created_at"]}').classes('ml-2 font-medium')
# 分类信息
with ui.row().classes('items-center'):
ui.icon('label', color='blue', size='md')
if 'category_id' in post and post['category_id'] is not None:
category = db.get_category(post['category_id'])
category_name = category['name'] if category else '未分类'
else:
category_name = '未分类'
ui.label(f'分类: {category_name}').classes('ml-2 font-medium text-gray-800')
# 文章内容
content_lines = post['content'].split('\n')
with ui.column().classes('gap-6'):
for line in content_lines:
if line.strip(): # 只显示非空行
ui.label(line).classes('text-gray-700 text-lg leading-relaxed')
# 操作按钮区域
with ui.card().classes('w-full p-8 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl shadow-md border border-gray-100 mb-8 max-w-6xl mx-auto'):
ui.label('文章操作').classes('text-2xl font-bold text-gray-800 mb-6')
with ui.row().classes('gap-4'):
# 编辑按钮 - 仅在管理模式下显示
if show_admin_buttons:
ui.button('✏️ 编辑文章', on_click=lambda: show_edit_post(post_id)).classes('bg-gradient-to-r from-yellow-500 to-amber-600 text-white hover:from-yellow-600 hover:to-amber-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
# 删除按钮 - 仅在管理模式下显示
def delete_post():
def confirm_delete():
db.delete_post(post_id)
ui.notify('文章已删除', type='positive')
show_main_view()
# 确认对话框
with ui.dialog() as dialog:
with ui.card().classes('p-8 w-96 rounded-xl shadow-lg border border-gray-100'):
ui.icon('warning', color='red', size='2xl').classes('mx-auto mb-6')
ui.label('确认删除').classes('text-2xl font-bold text-center mb-3')
ui.label('确定要删除这篇文章吗?此操作不可恢复。').classes('text-gray-600 text-center mb-8')
with ui.row().classes('justify-center gap-4'):
ui.button('确定删除', on_click=lambda: [dialog.close(), confirm_delete()]).classes('bg-gradient-to-r from-red-500 to-rose-600 text-white hover:from-red-600 hover:to-rose-700 px-6 py-3 rounded-lg transition-all shadow-md hover:shadow-lg')
ui.button('取消', on_click=dialog.close).classes('bg-gradient-to-r from-gray-500 to-slate-600 text-white hover:from-gray-600 hover:to-slate-700 px-6 py-3 rounded-lg transition-all shadow-md hover:shadow-lg')
dialog.open()
ui.button('🗑️ 删除文章', on_click=delete_post).classes('bg-gradient-to-r from-red-500 to-rose-600 text-white hover:from-red-600 hover:to-rose-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg')
# 评论区域
with ui.card().classes('w-full p-8 bg-white rounded-2xl shadow-lg border border-gray-100 mb-8 max-w-6xl mx-auto'):
ui.label('💬 评论').classes('text-3xl font-bold text-gray-900 mb-8')
# 添加评论表单
with ui.card().classes('w-full p-8 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl shadow-md border border-gray-100 mb-8'):
ui.label('添加新评论').classes('text-2xl font-bold text-gray-800 mb-6')
with ui.grid(columns=1).classes('gap-6'):
comment_author = ui.input(placeholder='请输入您的名字').classes('w-full p-4 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all')
comment_content = ui.textarea(placeholder='请输入评论内容...').classes('w-full h-40 p-4 border-2 border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all resize-y')
def add_comment():
author = comment_author.value.strip()
content = comment_content.value.strip()
if not author:
ui.notify('请输入名字', type='warning')
return
if not content:
ui.notify('请输入评论内容', type='warning')
return
try:
db.add_comment(post_id, author, content)
ui.notify('评论发表成功!', type='positive')
comment_author.value = ''
comment_content.value = ''
load_comments()
except Exception as e:
ui.notify(f'评论失败: {str(e)}', type='negative')
ui.button('发表评论', on_click=add_comment).classes('bg-gradient-to-r from-blue-500 to-indigo-600 text-white hover:from-blue-600 hover:to-indigo-700 px-8 py-3 rounded-xl text-lg font-semibold transition-all shadow-md hover:shadow-lg mt-6')
# 评论列表
comments_container = ui.column().classes('gap-4')
def load_comments():
comments_container.clear()
comments = db.get_comments(post_id)
if not comments:
with ui.card().classes('w-full text-center p-10 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl shadow-md border border-gray-100'):
ui.icon('chat', size='2xl').classes('text-blue-400 mb-4')
ui.label('暂无评论').classes('text-xl font-semibold text-gray-600 mb-2')
ui.label('快来发表第一条评论吧!').classes('text-gray-500')
return
for comment in comments:
with comments_container:
with ui.card().classes('w-full p-6 rounded-xl shadow-md hover:shadow-lg transition-all duration-300 border border-gray-100'):
# 评论头部
with ui.row().classes('justify-between items-center mb-4'):
with ui.row().classes('items-center'):
with ui.card().classes('w-10 h-10 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-full flex items-center justify-center shadow-md'):
ui.icon('person', color='white', size='md')
ui.label(comment['author']).classes('font-bold text-xl text-gray-800 ml-3')
ui.label(f'📅 {comment["created_at"]}').classes('text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full')
# 评论内容
ui.label(comment['content']).classes('text-gray-700 text-lg leading-relaxed mt-3 pl-13')
# 初始加载评论
load_comments()
# 初始显示主视图
show_main_view()
# 创建界面
create_blog_ui()
# 运行应用
ui.run(title='📝 我的博客系统', port=8080, favicon='📝')