Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, Request, Depends, HTTPException, Form | |
| from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse | |
| from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean | |
| from sqlalchemy.ext.declarative import declarative_base | |
| from sqlalchemy.orm import sessionmaker, Session | |
| from sqlalchemy import or_ | |
| from datetime import datetime | |
| import uvicorn | |
| import os | |
| from pydantic import BaseModel | |
| from typing import List, Optional | |
| # --- 数据库配置 --- | |
| DATABASE_URL = "sqlite:///./notes.db" | |
| engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) | |
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | |
| Base = declarative_base() | |
| # --- 数据库模型 --- | |
| class Note(Base): | |
| __tablename__ = "notes" | |
| id = Column(Integer, primary_key=True, index=True) | |
| title = Column(String(100), index=True) | |
| content = Column(Text) | |
| category = Column(String(50), default="默认") | |
| created_at = Column(DateTime, default=datetime.now) | |
| updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) | |
| is_done = Column(Boolean, default=False) | |
| # 创建表 | |
| Base.metadata.create_all(bind=engine) | |
| # --- 依赖项 --- | |
| def get_db(): | |
| db = SessionLocal() | |
| try: | |
| yield db | |
| finally: | |
| db.close() | |
| # --- FastAPI 应用初始化 --- | |
| app = FastAPI( | |
| title="高级 Python 后端项目 (FastAPI + SQLite)", | |
| description="一个功能完备的笔记/任务管理系统,支持 CRUD、分类、搜索、实时编辑及自动持久化。", | |
| version="3.0.0" | |
| ) | |
| # --- API 路由 (CRUD) --- | |
| def read_notes(q: Optional[str] = None, category: Optional[str] = None, db: Session = Depends(get_db)): | |
| query = db.query(Note) | |
| if q: | |
| query = query.filter(or_(Note.title.contains(q), Note.content.contains(q))) | |
| if category and category != "全部": | |
| query = query.filter(Note.category == category) | |
| notes = query.order_by(Note.created_at.desc()).all() | |
| return [{"id": n.id, "title": n.title, "content": n.content, "category": n.category, | |
| "created_at": n.created_at.strftime("%Y-%m-%d %H:%M:%S"), "is_done": n.is_done} for n in notes] | |
| def create_note(title: str = Form(...), content: str = Form(...), category: str = Form("默认"), db: Session = Depends(get_db)): | |
| # 后端防重逻辑:检查是否在 1 秒内创建了内容完全一样的记录 | |
| existing = db.query(Note).filter( | |
| Note.title == title, | |
| Note.content == content | |
| ).order_by(Note.created_at.desc()).first() | |
| if existing and (datetime.now() - existing.created_at).total_seconds() < 1: | |
| return RedirectResponse(url="/", status_code=303) | |
| new_note = Note(title=title, content=content, category=category) | |
| db.add(new_note) | |
| db.commit() | |
| db.refresh(new_note) | |
| return RedirectResponse(url="/", status_code=303) | |
| def edit_note(note_id: int, title: str = Form(...), content: str = Form(...), category: str = Form(...), db: Session = Depends(get_db)): | |
| note = db.query(Note).filter(Note.id == note_id).first() | |
| if not note: | |
| raise HTTPException(status_code=404, detail="Note not found") | |
| note.title = title | |
| note.content = content | |
| note.category = category | |
| db.commit() | |
| return RedirectResponse(url="/", status_code=303) | |
| def toggle_note(note_id: int, db: Session = Depends(get_db)): | |
| note = db.query(Note).filter(Note.id == note_id).first() | |
| if not note: | |
| raise HTTPException(status_code=404, detail="Note not found") | |
| note.is_done = not note.is_done | |
| db.commit() | |
| return RedirectResponse(url="/", status_code=303) | |
| def delete_note(note_id: int, db: Session = Depends(get_db)): | |
| note = db.query(Note).filter(Note.id == note_id).first() | |
| if not note: | |
| raise HTTPException(status_code=404, detail="Note not found") | |
| db.delete(note) | |
| db.commit() | |
| return RedirectResponse(url="/", status_code=303) | |
| # --- 页面路由 (HTML) --- | |
| async def home(request: Request, q: Optional[str] = None, cat: Optional[str] = None, db: Session = Depends(get_db)): | |
| # 获取所有分类供筛选 | |
| categories = [c[0] for c in db.query(Note.category).distinct().all()] | |
| if "默认" not in categories: categories.append("默认") | |
| # 构造列表 HTML (服务器端渲染以保证首屏速度) | |
| query = db.query(Note) | |
| if q: query = query.filter(or_(Note.title.contains(q), Note.content.contains(q))) | |
| if cat and cat != "全部": query = query.filter(Note.category == cat) | |
| notes = query.order_by(Note.created_at.desc()).all() | |
| notes_html = "" | |
| for note in notes: | |
| status_class = "opacity-50 line-through" if note.is_done else "" | |
| btn_text = '<i class="fas fa-undo"></i> 重做' if note.is_done else '<i class="fas fa-check"></i> 完成' | |
| btn_color = "bg-yellow-500 hover:bg-yellow-600" if note.is_done else "bg-green-500 hover:bg-green-600" | |
| notes_html += f""" | |
| <div class="note-card bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border-l-4 {'border-green-400' if note.is_done else 'border-blue-400'} mb-4 group" | |
| data-id="{note.id}" data-title="{note.title}" data-content="{note.content}" data-category="{note.category}"> | |
| <div class="flex justify-between items-start"> | |
| <div class="flex-1"> | |
| <div class="flex items-center space-x-2 mb-1"> | |
| <span class="px-2 py-0.5 bg-gray-100 text-gray-500 text-xs rounded-full">{note.category}</span> | |
| <h3 class="text-lg font-bold text-gray-800 {status_class}">{note.title}</h3> | |
| </div> | |
| <p class="text-gray-600 whitespace-pre-wrap text-sm leading-relaxed {status_class}">{note.content}</p> | |
| <div class="flex items-center mt-3 text-xs text-gray-400 space-x-3"> | |
| <span><i class="far fa-calendar-alt mr-1"></i> {note.created_at.strftime('%Y-%m-%d %H:%M')}</span> | |
| {f'<span class="text-green-500"><i class="fas fa-check-circle"></i> 已完成</span>' if note.is_done else ''} | |
| </div> | |
| </div> | |
| <div class="flex flex-col space-y-2 ml-4 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <button onclick='openEditModal({note.id}, "{note.title}", `{note.content}`, "{note.category}")' | |
| class="text-blue-500 hover:text-blue-700 p-2 bg-blue-50 rounded-lg transition-colors" title="编辑"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <form action="/api/notes/{note.id}/toggle" method="post" class="inline"> | |
| <button type="submit" class="text-white {btn_color} w-8 h-8 rounded-lg flex items-center justify-center transition-colors" title="{ '重做' if note.is_done else '完成' }"> | |
| {btn_text.split(' ')[0]} | |
| </button> | |
| </form> | |
| <form action="/api/notes/{note.id}/delete" method="post" class="inline" onsubmit="return confirm('确定要删除吗?')"> | |
| <button type="submit" class="text-red-500 hover:text-red-700 p-2 bg-red-50 rounded-lg transition-colors" title="删除"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| if not notes: | |
| notes_html = """ | |
| <div class="text-center py-20 bg-white rounded-2xl border-2 border-dashed border-gray-200"> | |
| <div class="text-gray-300 text-6xl mb-4"><i class="fas fa-folder-open"></i></div> | |
| <p class="text-gray-500">没找到相关记录哦</p> | |
| <a href="/" class="text-blue-500 hover:underline text-sm mt-2 inline-block">清除搜索条件</a> | |
| </div> | |
| """ | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>我的记录 - 高级 Python 后端项目</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap'); | |
| body {{ font-family: 'Noto Sans SC', sans-serif; }} | |
| .note-card {{ animation: slideIn 0.3s ease-out; }} | |
| @keyframes slideIn {{ | |
| from {{ opacity: 0; transform: translateY(10px); }} | |
| to {{ opacity: 1; transform: translateY(0); }} | |
| }} | |
| .modal {{ display: none; }} | |
| .modal.active {{ display: flex; }} | |
| </style> | |
| </head> | |
| <body class="bg-[#f8fafc] min-h-screen"> | |
| <!-- 顶部导航 --> | |
| <nav class="bg-white border-b border-gray-200 sticky top-0 z-50 px-4 py-3"> | |
| <div class="container mx-auto flex justify-between items-center"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="bg-blue-600 p-2 rounded-lg text-white"> | |
| <i class="fas fa-book-open"></i> | |
| </div> | |
| <h1 class="text-xl font-bold text-gray-800 tracking-tight">我的记录</h1> | |
| </div> | |
| <div class="hidden md:flex items-center space-x-6"> | |
| <a href="/docs" class="text-sm text-gray-500 hover:text-blue-600 transition-colors">API 开发文档</a> | |
| <div class="h-4 w-px bg-gray-200"></div> | |
| <span class="text-xs text-gray-400">Powered by FastAPI</span> | |
| </div> | |
| </div> | |
| </nav> | |
| <main class="container mx-auto px-4 py-8 flex flex-col lg:flex-row gap-8"> | |
| <!-- 左侧:新建 & 筛选 --> | |
| <aside class="w-full lg:w-1/3 space-y-6"> | |
| <!-- 搜索框 --> | |
| <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> | |
| <form action="/" method="get" class="relative"> | |
| <input type="text" name="q" value="{q or ''}" placeholder="搜索内容或标题..." | |
| class="w-full pl-10 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </form> | |
| </div> | |
| <!-- 新建笔记卡片 --> | |
| <div class="bg-white p-8 rounded-2xl shadow-sm border border-gray-100"> | |
| <h2 class="text-lg font-bold mb-5 text-gray-800 flex items-center"> | |
| <i class="fas fa-pencil-alt mr-2 text-blue-500"></i> 新建笔记 | |
| </h2> | |
| <form id="createForm" action="/api/notes" method="post" class="space-y-4" onsubmit="handleSubmit(this)"> | |
| <div> | |
| <input type="text" name="title" required placeholder="标题" | |
| class="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all"> | |
| </div> | |
| <div> | |
| <select name="category" class="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all"> | |
| <option value="默认">默认分类</option> | |
| <option value="工作">工作</option> | |
| <option value="生活">生活</option> | |
| <option value="灵感">灵感</option> | |
| <option value="学习">学习</option> | |
| </select> | |
| </div> | |
| <div> | |
| <textarea name="content" rows="4" required placeholder="今天想记录点什么?" | |
| class="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all"></textarea> | |
| </div> | |
| <button type="submit" id="submitBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-xl shadow-lg shadow-blue-200 transition-all active:scale-95"> | |
| 保存记录 | |
| </button> | |
| </form> | |
| </div> | |
| <!-- 分类筛选 --> | |
| <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100"> | |
| <h3 class="text-sm font-bold text-gray-400 uppercase tracking-wider mb-4">分类筛选</h3> | |
| <div class="flex flex-wrap gap-2"> | |
| <a href="/?cat=全部" class="px-4 py-2 rounded-lg text-sm transition-all {'bg-blue-600 text-white shadow-md' if cat=='全部' or not cat else 'bg-gray-50 text-gray-600 hover:bg-gray-100'}">全部</a> | |
| {"".join([f'<a href="/?cat={c}" class="px-4 py-2 rounded-lg text-sm transition-all {"bg-blue-600 text-white shadow-md" if cat==c else "bg-gray-50 text-gray-600 hover:bg-gray-100"}">{c}</a>' for c in categories])} | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- 右侧:列表展示 --> | |
| <section class="flex-1"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800"> | |
| { f'“{q}” 的搜索结果' if q else f'{cat or "全部"} 记录' } | |
| </h2> | |
| <span class="text-sm text-gray-400">共 {len(notes)} 条</span> | |
| </div> | |
| <div id="notes-list" class="space-y-4"> | |
| {notes_html} | |
| </div> | |
| </section> | |
| </main> | |
| <!-- 编辑弹窗 (Modal) --> | |
| <div id="editModal" class="modal fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm items-center justify-center p-4"> | |
| <div class="bg-white w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden"> | |
| <div class="px-6 py-4 bg-gray-50 border-b border-gray-100 flex justify-between items-center"> | |
| <h3 class="font-bold text-gray-800">编辑记录</h3> | |
| <button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button> | |
| </div> | |
| <form id="editForm" method="post" class="p-6 space-y-4" onsubmit="handleSubmit(this)"> | |
| <div> | |
| <label class="block text-xs font-bold text-gray-400 mb-1">标题</label> | |
| <input type="text" id="editTitle" name="title" required | |
| class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-bold text-gray-400 mb-1">分类</label> | |
| <select id="editCategory" name="category" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"> | |
| <option value="默认">默认分类</option> | |
| <option value="工作">工作</option> | |
| <option value="生活">生活</option> | |
| <option value="灵感">灵感</option> | |
| <option value="学习">学习</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-bold text-gray-400 mb-1">内容</label> | |
| <textarea id="editContent" name="content" rows="5" required | |
| class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"></textarea> | |
| </div> | |
| <div class="flex space-x-3 pt-2"> | |
| <button type="button" onclick="closeModal()" class="flex-1 py-3 bg-gray-100 text-gray-600 font-bold rounded-xl hover:bg-gray-200 transition-colors">取消</button> | |
| <button type="submit" class="flex-1 py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 shadow-lg shadow-blue-200 transition-colors">保存修改</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <script> | |
| // 处理提交:防止双击 | |
| function handleSubmit(form) {{ | |
| const btn = form.querySelector('button[type="submit"]'); | |
| if (btn) {{ | |
| btn.disabled = true; | |
| btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...'; | |
| }} | |
| return true; | |
| }} | |
| // 打开编辑弹窗 | |
| function openEditModal(id, title, content, category) {{ | |
| const modal = document.getElementById('editModal'); | |
| const form = document.getElementById('editForm'); | |
| form.action = `/api/notes/${{id}}/edit`; | |
| document.getElementById('editTitle').value = title; | |
| document.getElementById('editContent').value = content; | |
| document.getElementById('editCategory').value = category; | |
| modal.classList.add('active'); | |
| document.body.style.overflow = 'hidden'; | |
| }} | |
| // 关闭弹窗 | |
| function closeModal() {{ | |
| const modal = document.getElementById('editModal'); | |
| modal.classList.remove('active'); | |
| document.body.style.overflow = 'auto'; | |
| }} | |
| // 点击遮罩层关闭 | |
| document.getElementById('editModal').addEventListener('click', function(e) {{ | |
| if (e.target === this) closeModal(); | |
| }}); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return HTMLResponse(content=html_content) | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |