图:
代码:
#!/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: 开发模式热重载