NiceGUI 3.X FastAPI博客系统开发

28 阅读9分钟

图:

image.png

代码:

#!/usr/bin/env python3
from datetime import datetime
from typing import Optional, List
from fastapi import HTTPException
from pydantic import BaseModel
from nicegui import app, ui

# 数据模型
class BlogPost(BaseModel):
    id: Optional[int] = None
    title: str
    content: str
    author: str
    created_at: Optional[str] = None
    updated_at: Optional[str] = None

# 全局状态
class BlogState:
    def __init__(self):
        self.posts = []
        self.post_id_counter = 1
        self.init_sample_data()
    
    def init_sample_data(self):
        """初始化示例数据"""
        if not self.posts:
            self.posts.extend([
                {
                    "id": 1,
                    "title": "欢迎来到我的博客",
                    "content": "这是我的第一篇博客文章,欢迎阅读!",
                    "author": "管理员",
                    "created_at": "2024-01-01 10:00:00",
                    "updated_at": "2024-01-01 10:00:00"
                },
                {
                    "id": 2,
                    "title": "NiceGUI 3.X 新特性介绍",
                    "content": "NiceGUI 3.X 带来了很多令人兴奋的新特性...",
                    "author": "技术达人",
                    "created_at": "2024-01-02 14:30:00",
                    "updated_at": "2024-01-02 14:30:00"
                },
                {
                    "id": 3,
                    "title": "Python Web开发趋势",
                    "content": "近年来,Python在Web开发领域发展迅速...",
                    "author": "Python爱好者",
                    "created_at": "2024-01-03 09:15:00",
                    "updated_at": "2024-01-03 09:15:00"
                }
            ])
            self.post_id_counter = 4
    
    def get_all_posts(self):
        """获取所有文章"""
        return self.posts
    
    def get_post(self, post_id: int):
        """根据ID获取文章"""
        for post in self.posts:
            if post["id"] == post_id:
                return post
        return None
    
    def create_post(self, post: BlogPost):
        """创建新文章"""
        post_dict = post.dict()
        post_dict["id"] = self.post_id_counter
        post_dict["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        post_dict["updated_at"] = post_dict["created_at"]
        self.posts.append(post_dict)
        self.post_id_counter += 1
        return post_dict
    
    def update_post(self, post_id: int, post: BlogPost):
        """更新文章"""
        for i, existing_post in enumerate(self.posts):
            if existing_post["id"] == post_id:
                updated_post = post.dict()
                updated_post["id"] = post_id
                updated_post["created_at"] = existing_post["created_at"]
                updated_post["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                self.posts[i] = updated_post
                return updated_post
        return None
    
    def delete_post(self, post_id: int):
        """删除文章"""
        for i, post in enumerate(self.posts):
            if post["id"] == post_id:
                self.posts.pop(i)
                return True
        return False

# 全局状态实例
blog_state = BlogState()

# 将NiceGUI的app作为FastAPI应用使用
# NiceGUI的app对象已经是一个FastAPI应用

# 添加FastAPI路由到NiceGUI的app
@app.get('/api/posts')
def get_all_posts_api():
    """获取所有博客文章"""
    return blog_state.get_all_posts()

@app.get('/api/posts/{post_id}')
def get_post_api(post_id: int):
    """根据ID获取博客文章"""
    post = blog_state.get_post(post_id)
    if not post:
        raise HTTPException(status_code=404, detail="文章不存在")
    return post

@app.post('/api/posts')
def create_post_api(post: BlogPost):
    """创建新博客文章"""
    return blog_state.create_post(post)

@app.put('/api/posts/{post_id}')
def update_post_api(post_id: int, post: BlogPost):
    """更新博客文章"""
    updated = blog_state.update_post(post_id, post)
    if not updated:
        raise HTTPException(status_code=404, detail="文章不存在")
    return updated

@app.delete('/api/posts/{post_id}')
def delete_post_api(post_id: int):
    """删除博客文章"""
    if blog_state.delete_post(post_id):
        return {"message": "文章删除成功"}
    raise HTTPException(status_code=404, detail="文章不存在")

# NiceGUI 页面路由(与之前相同)
@ui.page('/')
def home_page():
    """博客首页"""
    with ui.header(elevated=True).classes('bg-primary text-white'):
        with ui.row().classes('w-full justify-between items-center'):
            ui.label('📝 NiceGUI 博客系统').classes('text-2xl font-bold')
            with ui.row().classes('gap-2'):
                ui.button('写文章', icon='add', 
                         on_click=lambda: ui.navigate.to('/create')).props('color=green')
                ui.button('管理文章', icon='manage_accounts',
                         on_click=lambda: ui.navigate.to('/manage'))
            
            # 主题切换
            dark = ui.dark_mode()
            ui.switch('深色模式', value=dark.value, on_change=dark.toggle)
    
    with ui.column().classes('w-full max-w-4xl mx-auto p-4'):
        ui.label('📚 最新文章').classes('text-3xl font-bold my-6')
        
        posts = blog_state.get_all_posts()
        if not posts:
            ui.label('暂无文章,点击"写文章"按钮开始创作吧!').classes('text-gray-500 text-center p-8')
            return
        
        # 显示所有文章
        for post in reversed(posts):  # 最新文章在前
            with ui.card().classes('w-full mb-6 shadow-lg hover:shadow-xl transition-shadow'):
                with ui.row().classes('w-full justify-between items-center'):
                    ui.label(post['title']).classes('text-xl font-bold')
                    ui.badge(post['author'], color='blue')
                
                ui.separator()
                content_preview = post['content'][:150] + "..." if len(post['content']) > 150 else post['content']
                ui.markdown(content_preview).classes('my-2')
                
                with ui.row().classes('w-full justify-between items-center mt-2'):
                    ui.label(f"📅 {post['created_at']}").classes('text-gray-500 text-sm')
                    ui.button('阅读全文', icon='visibility',
                             on_click=lambda p=post: ui.navigate.to(f"/post/{p['id']}")).props('outline')

@ui.page('/post/{post_id}')
def post_detail_page(post_id: int):
    """文章详情页面"""
    post = blog_state.get_post(post_id)
    
    if not post:
        with ui.column().classes('w-full h-full items-center justify-center'):
            ui.icon('error', size='4em').classes('text-red-500')
            ui.label('文章不存在').classes('text-2xl')
            ui.button('返回首页', icon='home',
                     on_click=lambda: ui.navigate.to('/')).props('color=primary')
        return
    
    with ui.header(elevated=True).classes('bg-primary text-white'):
        with ui.row().classes('w-full justify-between items-center'):
            ui.button('← 返回', icon='arrow_back',
                     on_click=lambda: ui.navigate.to('/')).props('flat')
            ui.label('文章详情').classes('text-xl')
            with ui.row().classes('gap-2'):
                ui.button('编辑', icon='edit',
                         on_click=lambda: ui.navigate.to(f'/edit/{post_id}')).props('color=secondary outline')
                ui.button('删除', icon='delete',
                         on_click=lambda: delete_post_dialog(post_id)).props('color=red outline')
    
    with ui.column().classes('w-full max-w-4xl mx-auto p-4'):
        ui.label(post['title']).classes('text-3xl font-bold my-4')
        
        with ui.row().classes('w-full justify-between text-gray-500 mb-6'):
            ui.label(f"👤 {post['author']}")
            ui.label(f"📅 {post['created_at']}")
            if post['updated_at'] != post['created_at']:
                ui.label(f"🔄 {post['updated_at']}")
        
        ui.separator()
        ui.markdown(post['content']).classes('text-lg leading-relaxed p-4 bg-gray-50 dark:bg-gray-800 rounded-lg')

@ui.page('/create')
def create_post_page():
    """创建新文章"""
    with ui.header(elevated=True).classes('bg-primary text-white'):
        with ui.row().classes('w-full justify-between items-center'):
            ui.button('← 返回', icon='arrow_back',
                     on_click=lambda: ui.navigate.to('/')).props('flat')
            ui.label('写新文章').classes('text-xl')
    
    with ui.column().classes('w-full max-w-4xl mx-auto p-4'):
        title = ui.input(label='标题', 
                        placeholder='输入文章标题').classes('w-full text-xl')
        
        author = ui.input(label='作者', 
                         placeholder='输入作者名称',
                         value='管理员').classes('w-full')
        
        content = ui.textarea(label='内容', 
                             placeholder='输入文章内容...').classes('w-full h-96')
        
        with ui.row().classes('w-full justify-end gap-4 mt-4'):
            ui.button('取消', icon='cancel',
                     on_click=lambda: ui.navigate.to('/')).props('color=grey')
            ui.button('发布', icon='publish',
                     on_click=lambda: save_new_post(title, author, content)).props('color=green')

def save_new_post(title_input, author_input, content_input):
    """保存新文章"""
    if not title_input.value or not author_input.value or not content_input.value:
        ui.notify('请填写所有必填字段', type='negative')
        return
    
    new_post = BlogPost(
        title=title_input.value,
        content=content_input.value,
        author=author_input.value
    )
    
    try:
        blog_state.create_post(new_post)
        ui.notify('文章发布成功!', type='positive')
        ui.navigate.to('/')
    except Exception as e:
        ui.notify(f'发布失败: {str(e)}', type='negative')

@ui.page('/edit/{post_id}')
def edit_post_page(post_id: int):
    """编辑文章"""
    post = blog_state.get_post(post_id)
    
    if not post:
        ui.notify('文章不存在', type='negative')
        ui.navigate.to('/')
        return
    
    with ui.header(elevated=True).classes('bg-primary text-white'):
        with ui.row().classes('w-full justify-between items-center'):
            ui.button('← 返回', icon='arrow_back',
                     on_click=lambda: ui.navigate.to(f'/post/{post_id}')).props('flat')
            ui.label('编辑文章').classes('text-xl')
    
    with ui.column().classes('w-full max-w-4xl mx-auto p-4'):
        title = ui.input(label='标题', 
                        value=post['title']).classes('w-full text-xl')
        
        author = ui.input(label='作者', 
                         value=post['author']).classes('w-full')
        
        content = ui.textarea(label='内容', 
                             value=post['content']).classes('w-full h-96')
        
        with ui.row().classes('w-full justify-end gap-4 mt-4'):
            ui.button('取消', icon='cancel',
                     on_click=lambda: ui.navigate.to(f'/post/{post_id}')).props('color=grey')
            ui.button('保存', icon='save',
                     on_click=lambda: save_edit(post_id, title, author, content)).props('color=green')

def save_edit(post_id, title_input, author_input, content_input):
    """保存编辑"""
    if not title_input.value or not author_input.value or not content_input.value:
        ui.notify('请填写所有必填字段', type='negative')
        return
    
    updated_post = BlogPost(
        title=title_input.value,
        content=content_input.value,
        author=author_input.value
    )
    
    try:
        result = blog_state.update_post(post_id, updated_post)
        if result:
            ui.notify('文章更新成功!', type='positive')
            ui.navigate.to(f'/post/{post_id}')
        else:
            ui.notify('文章更新失败', type='negative')
    except Exception as e:
        ui.notify(f'更新失败: {str(e)}', type='negative')

@ui.page('/manage')
def manage_posts_page():
    """文章管理页面"""
    with ui.header(elevated=True).classes('bg-primary text-white'):
        with ui.row().classes('w-full justify-between items-center'):
            ui.button('← 返回', icon='arrow_back',
                     on_click=lambda: ui.navigate.to('/')).props('flat')
            ui.label('📋 文章管理').classes('text-xl')
            ui.button('刷新', icon='refresh',
                     on_click=lambda: ui.navigate.reload()).props('color=secondary')
    
    with ui.column().classes('w-full max-w-6xl mx-auto p-4'):
        ui.label('所有文章').classes('text-2xl font-bold my-4')
        
        posts = blog_state.get_all_posts()
        if not posts:
            ui.label('暂无文章').classes('text-gray-500 text-center p-8')
            return
        
        # 创建表格
        table_container = ui.column().classes('w-full')
        
        for post in posts:
            with table_container:
                with ui.card().classes('w-full mb-2'):
                    with ui.row().classes('w-full items-center'):
                        ui.label(f"{post['id']}").classes('w-12 text-center')
                        ui.separator().props('vertical')
                        ui.label(post['title']).classes('flex-grow')
                        ui.separator().props('vertical')
                        ui.label(post['author']).classes('w-24 text-center')
                        ui.separator().props('vertical')
                        ui.label(post['created_at']).classes('w-40 text-center')
                        ui.separator().props('vertical')
                        with ui.row().classes('w-48 justify-end gap-2'):
                            ui.button('查看', icon='visibility',
                                     on_click=lambda pid=post['id']: ui.navigate.to(f'/post/{pid}')).props('size=sm outline')
                            ui.button('编辑', icon='edit',
                                     on_click=lambda pid=post['id']: ui.navigate.to(f'/edit/{pid}')).props('size=sm color=secondary outline')
                            ui.button('删除', icon='delete',
                                     on_click=lambda pid=post['id']: delete_post_dialog(pid)).props('size=sm color=red outline')

def delete_post_dialog(post_id: int):
    """删除文章确认对话框"""
    def delete_post():
        try:
            if blog_state.delete_post(post_id):
                ui.notify('文章删除成功!', type='positive')
                dialog.close()
                # 刷新当前页面
                ui.navigate.reload()
            else:
                ui.notify('文章删除失败', type='negative')
        except Exception as e:
            ui.notify(f'删除失败: {str(e)}', type='negative')
    
    with ui.dialog() as dialog, ui.card():
        ui.label('⚠️ 确认删除?').classes('text-xl font-bold text-red-500')
        ui.label('删除后无法恢复,确定要删除这篇文章吗?')
        with ui.row().classes('justify-end gap-4 mt-4'):
            ui.button('取消', icon='cancel',
                     on_click=dialog.close).props('color=grey')
            ui.button('确认删除', icon='delete_forever',
                     on_click=delete_post).props('color=red')

# 运行应用
if __name__ == '__main__':
    # 添加根路由
    @app.get('/')
    def root():
        return {"message": "欢迎访问博客系统!前端界面已就绪。"}
    
    # 使用NiceGUI运行
    ui.run(
        title='NiceGUI 博客系统',
        favicon='📝',
        dark=True,
        host='0.0.0.0',
        port=8000,
        reload=False
    )

项目概述

这是一个基于 NiceGUI 3.X 构建的现代化博客系统,集成了 FastAPI 作为后端框架,提供了完整的RESTful API和响应式Web界面。该系统允许用户创建、阅读、编辑和管理博客文章。

技术栈

  • 前端框架: NiceGUI 3.X(基于Vue.js的Python UI库)
  • 后端框架: FastAPI(高性能Python Web框架)
  • 数据模型: Pydantic(数据验证和设置管理)
  • 状态管理: 自定义状态类
  • UI组件: NiceGUI原生组件(卡片、表格、对话框等)

主要功能模块

1. 数据模型(BlogPost)

使用Pydantic定义博客文章的数据结构:

  • id: 文章唯一标识
  • title: 文章标题
  • content: 文章内容
  • author: 作者姓名
  • created_at: 创建时间
  • updated_at: 更新时间

2. 状态管理(BlogState)

内存中的状态管理类,提供文章数据的CRUD操作:

  • 初始化示例数据(3篇示例文章)
  • 文章列表管理
  • ID自动递增
  • 时间戳自动生成

3. RESTful API接口

通过FastAPI实现的API端点:

  • GET /api/posts - 获取所有文章
  • GET /api/posts/{id} - 获取指定文章
  • POST /api/posts - 创建新文章
  • PUT /api/posts/{id} - 更新文章
  • DELETE /api/posts/{id} - 删除文章

4. 前端页面

(1) 首页(/
  • 显示所有博客文章的预览
  • 最新文章置顶显示
  • 响应式卡片布局
  • 深色/浅色主题切换
  • 导航到创建和管理页面
(2) 文章详情页(/post/{id}
  • 显示完整文章内容
  • 作者和时间信息
  • 编辑和删除操作按钮
  • 返回首页导航
(3) 创建文章页(/create
  • 文章标题输入
  • 作者输入(默认"管理员")
  • Markdown内容编辑器
  • 发布和取消功能
(4) 编辑文章页(/edit/{id}
  • 预填充现有文章内容
  • 实时编辑和保存
  • 数据验证
(5) 管理页面(/manage
  • 表格形式展示所有文章
  • 每行提供查看、编辑、删除操作
  • 批量管理功能

5. 用户交互特性

  • 实时通知: 操作成功/失败提示
  • 确认对话框: 删除操作二次确认
  • 主题切换: 深色/浅色模式
  • 响应式设计: 适配不同屏幕尺寸
  • 加载状态: 页面切换和操作反馈

项目结构

博客系统/
├── 数据模型定义 (BlogPost)
├── 状态管理 (BlogState)
├── API路由 (FastAPI)
├── 页面路由 (NiceGUI)
│   ├── 首页 (/)
│   ├── 文章详情 (/post/{id})
│   ├── 创建文章 (/create)
│   ├── 编辑文章 (/edit/{id})
│   └── 文章管理 (/manage)
└── 工具函数和对话框

运行方式

# 直接运行
python blog_system.py

# 或使用模块方式
python -m uvicorn blog_system:app --reload

默认访问地址:http://localhost:8000

配置选项

启动时可以配置以下参数:

  • title: 应用标题
  • favicon: 网站图标
  • dark: 默认深色模式
  • host: 绑定主机(默认0.0.0.0)
  • port: 端口号(默认8000)
  • reload: 开发模式热重载