3v324v23 commited on
Commit
7df15d6
·
1 Parent(s): fbc2b0a

Fix double submission bug and upgrade features: category, search, and edit modal

Browse files
Files changed (2) hide show
  1. README.md +22 -29
  2. app.py +241 -67
README.md CHANGED
@@ -1,64 +1,57 @@
1
  ---
2
- title: 经典 Python 后端项目 (FastAPI + SQLite)
3
  emoji: 🚀
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
- short_description: 经典 Python 后端项目 (FastAPI + SQLite)
9
  ---
10
 
11
- # 🚀 经典 Python 后端项目 (FastAPI + SQLite)
12
 
13
- 这是一个功能完备的经典后端项目示例,展示了如何使用 FastAPI 构建具持久化存储、CRUD 逻辑和现代化前端界面 Web 应用。
14
 
15
- ## 🌟 项目功能
16
 
17
- - **持久化存储**:集成 SQLAlchemy ORM,使用 SQLite 数据库保存数据
18
- - **完整 CRUD**:支持笔记/任务的创建读取状态切换(完成/重做及删除
19
- - **现代化 UI**:使用 Tailwind CSS 构建的响应式前端界面,适配 PC 移动端
20
- - **自动化文档**:支持 Swagger UI 交互式 API 文档
21
- - **容器化部署**:专为 Hugging Face Spaces 优化Docker 配置
 
 
22
 
23
  ## 🛠️ 技术栈
24
 
25
- - **后端**: FastAPI (Python)
26
  - **数据库**: SQLite + SQLAlchemy
27
- - **前端**: HTML5 + Tailwind CSS + FontAwesome
28
- - **部署**: Docker
29
 
30
  ## 🚀 快速开始
31
 
32
- ### 本地开发
33
 
34
  1. 安装依赖:
35
  ```bash
36
  pip install -r requirements.txt
37
  ```
38
 
39
- 2. 运行应用:
40
  ```bash
41
  python app.py
42
  ```
43
 
44
  3. 访问:
45
  - 首页:`http://localhost:7860`
46
- - API 文档:`http://localhost:7860/docs`
47
 
48
- ### 部署到 Hugging Face
49
 
50
- 1. 建一个 Docker 类型Space
51
- 2. 将代码推送至 Space 仓库:
52
- ```bash
53
- git push origin main
54
- ```
55
-
56
- ## 📝 目录结构
57
-
58
- - `app.py`: 核心逻辑,包含数据库模型、API 路由及 HTML 模板。
59
- - `requirements.txt`: 项目依赖列表。
60
- - `Dockerfile`: 镜像构建脚本。
61
- - `notes.db`: 自动生成的 SQLite 数据库文件。
62
 
63
  ## 📄 协议
64
 
 
1
  ---
2
+ title: 高级 Python 后端项目 (FastAPI + SQLite)
3
  emoji: 🚀
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ short_description: 高级 Python 后端项目 (FastAPI + SQLite)
9
  ---
10
 
11
+ # 🚀 高级 Python 后端项目 (FastAPI + SQLite)
12
 
13
+ 这是一个功能强大的经典后端项目,展示了如何使用 FastAPI 构建一个持久化存储、CRUD、分类管理、搜索及响应式前端的完整应用。
14
 
15
+ ## 🌟 核心功能
16
 
17
+ - **持久化存储**:使用 SQLAlchemy ORM + SQLite 数据库。
18
+ - **分类管理**:支持笔记进行分类(工作生活灵感、学习等)。
19
+ - **全局搜索**:支持对标题内容的关键词搜索
20
+ - **实时编辑**:通过 Modal 弹窗实现无需刷新的在线编辑功能
21
+ - **防重复提交**:前后端双重校验,彻底修复重复保存Bug
22
+ - **现代化 UI**:基于 Tailwind CSS 的精致侧边栏布局,支持交互动画。
23
+ - **自动化文档**:内置 Swagger UI (`/docs`)。
24
 
25
  ## 🛠️ 技术栈
26
 
27
+ - **后端**: FastAPI (Python 3.10+)
28
  - **数据库**: SQLite + SQLAlchemy
29
+ - **前端**: HTML5 + Tailwind CSS + FontAwesome + Vanilla JS
30
+ - **部署**: Docker (Hugging Face Spaces 适配)
31
 
32
  ## 🚀 快速开始
33
 
34
+ ### 本地运行
35
 
36
  1. 安装依赖:
37
  ```bash
38
  pip install -r requirements.txt
39
  ```
40
 
41
+ 2. 启动应用:
42
  ```bash
43
  python app.py
44
  ```
45
 
46
  3. 访问:
47
  - 首页:`http://localhost:7860`
48
+ - 文档:`http://localhost:7860/docs`
49
 
50
+ ## 📝 修复说明 (v3.0)
51
 
52
+ - **Bug 修复**:解决了用户反馈的“新建一条保存两条”问题。通过前端禁用按钮和后端 1s 内内容去重逻辑彻底解决
53
+ - **功能升级**:新增了搜索、分类筛选和 Modal 编辑功能。
54
+ - **视觉优化**:重构了布局,采用侧边栏设计,增加了列表加载动画和按钮交互。
 
 
 
 
 
 
 
 
 
55
 
56
  ## 📄 协议
57
 
app.py CHANGED
@@ -1,10 +1,9 @@
1
  from fastapi import FastAPI, Request, Depends, HTTPException, Form
2
  from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
3
- from fastapi.staticfiles import StaticFiles
4
- from fastapi.templating import Jinja2Templates
5
  from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean
6
  from sqlalchemy.ext.declarative import declarative_base
7
  from sqlalchemy.orm import sessionmaker, Session
 
8
  from datetime import datetime
9
  import uvicorn
10
  import os
@@ -23,7 +22,9 @@ class Note(Base):
23
  id = Column(Integer, primary_key=True, index=True)
24
  title = Column(String(100), index=True)
25
  content = Column(Text)
26
- created_at = Column(DateTime, default=datetime.utcnow)
 
 
27
  is_done = Column(Boolean, default=False)
28
 
29
  # 创建表
@@ -39,25 +40,52 @@ def get_db():
39
 
40
  # --- FastAPI 应用初始化 ---
41
  app = FastAPI(
42
- title="经典 Python 后端项目 (FastAPI + SQLite)",
43
- description="一个功能完备的经典笔记/任务管理系统,展示了 CRUD 逻辑数据库持久化及现代前端交互。",
44
- version="2.0.0"
45
  )
46
 
47
- # --- API 路由 ---
48
- @app.get("/api/notes", response_model=List[dict])
49
- def read_notes(db: Session = Depends(get_db)):
50
- notes = db.query(Note).order_by(Note.created_at.desc()).all()
51
- return [{"id": n.id, "title": n.title, "content": n.content, "created_at": n.created_at.isoformat(), "is_done": n.is_done} for n in notes]
 
 
 
 
 
 
 
52
 
53
  @app.post("/api/notes")
54
- def create_note(title: str = Form(...), content: str = Form(...), db: Session = Depends(get_db)):
55
- new_note = Note(title=title, content=content)
 
 
 
 
 
 
 
 
 
56
  db.add(new_note)
57
  db.commit()
58
  db.refresh(new_note)
59
  return RedirectResponse(url="/", status_code=303)
60
 
 
 
 
 
 
 
 
 
 
 
 
61
  @app.post("/api/notes/{note_id}/toggle")
62
  def toggle_note(note_id: int, db: Session = Depends(get_db)):
63
  note = db.query(Note).filter(Note.id == note_id).first()
@@ -78,29 +106,52 @@ def delete_note(note_id: int, db: Session = Depends(get_db)):
78
 
79
  # --- 页面路由 (HTML) ---
80
  @app.get("/", response_class=HTMLResponse)
81
- async def home(db: Session = Depends(get_db)):
82
- notes = db.query(Note).order_by(Note.created_at.desc()).all()
 
 
 
 
 
 
 
 
83
 
84
  notes_html = ""
85
  for note in notes:
86
- status_class = "line-through text-gray-400" if note.is_done else "text-gray-800"
87
- btn_text = "重���" if note.is_done else "完成"
88
- btn_color = "bg-yellow-500" if note.is_done else "bg-green-500"
89
 
90
  notes_html += f"""
91
- <div class="bg-white p-6 rounded-lg shadow-md mb-4 border-l-4 {'border-green-500' if note.is_done else 'border-blue-500'}">
 
92
  <div class="flex justify-between items-start">
93
  <div class="flex-1">
94
- <h3 class="text-xl font-bold {status_class}">{note.title}</h3>
95
- <p class="mt-2 text-gray-600 whitespace-pre-wrap {status_class}">{note.content}</p>
96
- <span class="text-xs text-gray-400 mt-2 block">{note.created_at.strftime('%Y-%m-%d %H:%M:%S')}</span>
 
 
 
 
 
 
97
  </div>
98
- <div class="flex space-x-2 ml-4">
 
 
 
 
99
  <form action="/api/notes/{note.id}/toggle" method="post" class="inline">
100
- <button type="submit" class="{btn_color} hover:opacity-90 text-white px-3 py-1 rounded text-sm transition-colors">{btn_text}</button>
 
 
101
  </form>
102
- <form action="/api/notes/{note.id}/delete" method="post" class="inline">
103
- <button type="submit" class="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm transition-colors">删除</button>
 
 
104
  </form>
105
  </div>
106
  </div>
@@ -108,7 +159,13 @@ async def home(db: Session = Depends(get_db)):
108
  """
109
 
110
  if not notes:
111
- notes_html = '<div class="text-center py-10 text-gray-500 italic">暂无记录,快去创建一个吧!</div>'
 
 
 
 
 
 
112
 
113
  html_content = f"""
114
  <!DOCTYPE html>
@@ -116,61 +173,178 @@ async def home(db: Session = Depends(get_db)):
116
  <head>
117
  <meta charset="UTF-8">
118
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
119
- <title>经典 Python 后端项目</title>
120
  <script src="https://cdn.tailwindcss.com"></script>
121
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 
 
 
 
 
 
 
 
 
 
 
122
  </head>
123
- <body class="bg-gray-100 min-h-screen font-sans">
124
- <nav class="bg-blue-600 text-white shadow-lg p-4">
 
125
  <div class="container mx-auto flex justify-between items-center">
126
- <h1 class="text-2xl font-bold flex items-center">
127
- <i class="fas fa-rocket mr-2"></i> Python 后端框架项目
128
- </h1>
129
- <div class="space-x-4">
130
- <a href="/docs" class="hover:underline">API 文档</a>
131
- <a href="https://huggingface.co/spaces" target="_blank" class="hover:underline">Hugging Face</a>
 
 
 
 
132
  </div>
133
  </div>
134
  </nav>
135
 
136
- <main class="container mx-auto px-4 py-8 max-w-4xl">
137
- <!-- 新建笔记 -->
138
- <section class="bg-white p-8 rounded-xl shadow-lg mb-10">
139
- <h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center">
140
- <i class="fas fa-plus-circle mr-2 text-blue-500"></i> 新建笔记/任务
141
- </h2>
142
- <form action="/api/notes" method="post" class="space-y-4">
143
- <div>
144
- <label class="block text-sm font-medium text-gray-700 mb-1">标题</label>
145
- <input type="text" name="title" required placeholder="输入简短标题..."
146
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all">
147
- </div>
148
- <div>
149
- <label class="block text-sm font-medium text-gray-700 mb-1">内容</label>
150
- <textarea name="content" rows="3" required placeholder="输入详细内容..."
151
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  </div>
153
- <button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-md transition-colors shadow-md">
154
- 立即保存
155
- </button>
156
- </form>
157
- </section>
158
 
159
- <!-- 列表展示 -->
160
- <section>
161
- <h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center">
162
- <i class="fas fa-list-ul mr-2 text-blue-500"></i> 我的记录
163
- </h2>
164
- <div id="notes-list">
 
 
 
 
165
  {notes_html}
166
  </div>
167
  </section>
168
  </main>
169
 
170
- <footer class="text-center py-10 text-gray-400 text-sm">
171
- <p>© 2026 基于 FastAPI 构建的经典项目示例</p>
172
- <p class="mt-2">运行在 Hugging Face Spaces (Docker)</p>
173
- </footer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </body>
175
  </html>
176
  """
 
1
  from fastapi import FastAPI, Request, Depends, HTTPException, Form
2
  from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
 
 
3
  from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean
4
  from sqlalchemy.ext.declarative import declarative_base
5
  from sqlalchemy.orm import sessionmaker, Session
6
+ from sqlalchemy import or_
7
  from datetime import datetime
8
  import uvicorn
9
  import os
 
22
  id = Column(Integer, primary_key=True, index=True)
23
  title = Column(String(100), index=True)
24
  content = Column(Text)
25
+ category = Column(String(50), default="默认")
26
+ created_at = Column(DateTime, default=datetime.now)
27
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
28
  is_done = Column(Boolean, default=False)
29
 
30
  # 创建表
 
40
 
41
  # --- FastAPI 应用初始化 ---
42
  app = FastAPI(
43
+ title="高级 Python 后端项目 (FastAPI + SQLite)",
44
+ description="一个功能完备的笔记/任务管理系统,支持 CRUD、分类、搜索、实时编辑及自动持久化。",
45
+ version="3.0.0"
46
  )
47
 
48
+ # --- API 路由 (CRUD) ---
49
+
50
+ @app.get("/api/notes")
51
+ def read_notes(q: Optional[str] = None, category: Optional[str] = None, db: Session = Depends(get_db)):
52
+ query = db.query(Note)
53
+ if q:
54
+ query = query.filter(or_(Note.title.contains(q), Note.content.contains(q)))
55
+ if category and category != "全部":
56
+ query = query.filter(Note.category == category)
57
+ notes = query.order_by(Note.created_at.desc()).all()
58
+ return [{"id": n.id, "title": n.title, "content": n.content, "category": n.category,
59
+ "created_at": n.created_at.strftime("%Y-%m-%d %H:%M:%S"), "is_done": n.is_done} for n in notes]
60
 
61
  @app.post("/api/notes")
62
+ def create_note(title: str = Form(...), content: str = Form(...), category: str = Form("默认"), db: Session = Depends(get_db)):
63
+ # 后端防重逻辑:检查是否在 1 秒内创建了内容完全一样的记录
64
+ existing = db.query(Note).filter(
65
+ Note.title == title,
66
+ Note.content == content
67
+ ).order_by(Note.created_at.desc()).first()
68
+
69
+ if existing and (datetime.now() - existing.created_at).total_seconds() < 1:
70
+ return RedirectResponse(url="/", status_code=303)
71
+
72
+ new_note = Note(title=title, content=content, category=category)
73
  db.add(new_note)
74
  db.commit()
75
  db.refresh(new_note)
76
  return RedirectResponse(url="/", status_code=303)
77
 
78
+ @app.post("/api/notes/{note_id}/edit")
79
+ def edit_note(note_id: int, title: str = Form(...), content: str = Form(...), category: str = Form(...), db: Session = Depends(get_db)):
80
+ note = db.query(Note).filter(Note.id == note_id).first()
81
+ if not note:
82
+ raise HTTPException(status_code=404, detail="Note not found")
83
+ note.title = title
84
+ note.content = content
85
+ note.category = category
86
+ db.commit()
87
+ return RedirectResponse(url="/", status_code=303)
88
+
89
  @app.post("/api/notes/{note_id}/toggle")
90
  def toggle_note(note_id: int, db: Session = Depends(get_db)):
91
  note = db.query(Note).filter(Note.id == note_id).first()
 
106
 
107
  # --- 页面路由 (HTML) ---
108
  @app.get("/", response_class=HTMLResponse)
109
+ async def home(request: Request, q: Optional[str] = None, cat: Optional[str] = None, db: Session = Depends(get_db)):
110
+ # 获取所有分类供筛选
111
+ categories = [c[0] for c in db.query(Note.category).distinct().all()]
112
+ if "默认" not in categories: categories.append("默认")
113
+
114
+ # 构造列表 HTML (服务器端渲染以保证首屏速度)
115
+ query = db.query(Note)
116
+ if q: query = query.filter(or_(Note.title.contains(q), Note.content.contains(q)))
117
+ if cat and cat != "全部": query = query.filter(Note.category == cat)
118
+ notes = query.order_by(Note.created_at.desc()).all()
119
 
120
  notes_html = ""
121
  for note in notes:
122
+ status_class = "opacity-50 line-through" if note.is_done else ""
123
+ btn_text = '<i class="fas fa-undo"></i> 重做' if note.is_done else '<i class="fas fa-check"></i> 完成'
124
+ btn_color = "bg-yellow-500 hover:bg-yellow-600" if note.is_done else "bg-green-500 hover:bg-green-600"
125
 
126
  notes_html += f"""
127
+ <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"
128
+ data-id="{note.id}" data-title="{note.title}" data-content="{note.content}" data-category="{note.category}">
129
  <div class="flex justify-between items-start">
130
  <div class="flex-1">
131
+ <div class="flex items-center space-x-2 mb-1">
132
+ <span class="px-2 py-0.5 bg-gray-100 text-gray-500 text-xs rounded-full">{note.category}</span>
133
+ <h3 class="text-lg font-bold text-gray-800 {status_class}">{note.title}</h3>
134
+ </div>
135
+ <p class="text-gray-600 whitespace-pre-wrap text-sm leading-relaxed {status_class}">{note.content}</p>
136
+ <div class="flex items-center mt-3 text-xs text-gray-400 space-x-3">
137
+ <span><i class="far fa-calendar-alt mr-1"></i> {note.created_at.strftime('%Y-%m-%d %H:%M')}</span>
138
+ {f'<span class="text-green-500"><i class="fas fa-check-circle"></i> 已完成</span>' if note.is_done else ''}
139
+ </div>
140
  </div>
141
+ <div class="flex flex-col space-y-2 ml-4 opacity-0 group-hover:opacity-100 transition-opacity">
142
+ <button onclick='openEditModal({note.id}, "{note.title}", `{note.content}`, "{note.category}")'
143
+ class="text-blue-500 hover:text-blue-700 p-2 bg-blue-50 rounded-lg transition-colors" title="编辑">
144
+ <i class="fas fa-edit"></i>
145
+ </button>
146
  <form action="/api/notes/{note.id}/toggle" method="post" class="inline">
147
+ <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 '完成' }">
148
+ {btn_text.split(' ')[0]}
149
+ </button>
150
  </form>
151
+ <form action="/api/notes/{note.id}/delete" method="post" class="inline" onsubmit="return confirm('确定要删除吗?')">
152
+ <button type="submit" class="text-red-500 hover:text-red-700 p-2 bg-red-50 rounded-lg transition-colors" title="删除">
153
+ <i class="fas fa-trash-alt"></i>
154
+ </button>
155
  </form>
156
  </div>
157
  </div>
 
159
  """
160
 
161
  if not notes:
162
+ notes_html = """
163
+ <div class="text-center py-20 bg-white rounded-2xl border-2 border-dashed border-gray-200">
164
+ <div class="text-gray-300 text-6xl mb-4"><i class="fas fa-folder-open"></i></div>
165
+ <p class="text-gray-500">没找到相关记录哦</p>
166
+ <a href="/" class="text-blue-500 hover:underline text-sm mt-2 inline-block">清除搜索条件</a>
167
+ </div>
168
+ """
169
 
170
  html_content = f"""
171
  <!DOCTYPE html>
 
173
  <head>
174
  <meta charset="UTF-8">
175
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
176
+ <title>我的记录 - 高级 Python 后端项目</title>
177
  <script src="https://cdn.tailwindcss.com"></script>
178
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
179
+ <style>
180
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
181
+ body {{ font-family: 'Noto Sans SC', sans-serif; }}
182
+ .note-card {{ animation: slideIn 0.3s ease-out; }}
183
+ @keyframes slideIn {{
184
+ from {{ opacity: 0; transform: translateY(10px); }}
185
+ to {{ opacity: 1; transform: translateY(0); }}
186
+ }}
187
+ .modal {{ display: none; }}
188
+ .modal.active {{ display: flex; }}
189
+ </style>
190
  </head>
191
+ <body class="bg-[#f8fafc] min-h-screen">
192
+ <!-- 顶部导航 -->
193
+ <nav class="bg-white border-b border-gray-200 sticky top-0 z-50 px-4 py-3">
194
  <div class="container mx-auto flex justify-between items-center">
195
+ <div class="flex items-center space-x-3">
196
+ <div class="bg-blue-600 p-2 rounded-lg text-white">
197
+ <i class="fas fa-book-open"></i>
198
+ </div>
199
+ <h1 class="text-xl font-bold text-gray-800 tracking-tight">我的记录</h1>
200
+ </div>
201
+ <div class="hidden md:flex items-center space-x-6">
202
+ <a href="/docs" class="text-sm text-gray-500 hover:text-blue-600 transition-colors">API 开发文档</a>
203
+ <div class="h-4 w-px bg-gray-200"></div>
204
+ <span class="text-xs text-gray-400">Powered by FastAPI</span>
205
  </div>
206
  </div>
207
  </nav>
208
 
209
+ <main class="container mx-auto px-4 py-8 flex flex-col lg:flex-row gap-8">
210
+ <!-- 左侧:新建 & 筛选 -->
211
+ <aside class="w-full lg:w-1/3 space-y-6">
212
+ <!-- 搜索框 -->
213
+ <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
214
+ <form action="/" method="get" class="relative">
215
+ <input type="text" name="q" value="{q or ''}" placeholder="搜索内容或标题..."
216
+ 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">
217
+ <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
218
+ </form>
219
+ </div>
220
+
221
+ <!-- 新建笔记卡片 -->
222
+ <div class="bg-white p-8 rounded-2xl shadow-sm border border-gray-100">
223
+ <h2 class="text-lg font-bold mb-5 text-gray-800 flex items-center">
224
+ <i class="fas fa-pencil-alt mr-2 text-blue-500"></i> 新建笔记
225
+ </h2>
226
+ <form id="createForm" action="/api/notes" method="post" class="space-y-4" onsubmit="handleSubmit(this)">
227
+ <div>
228
+ <input type="text" name="title" required placeholder="标题"
229
+ 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">
230
+ </div>
231
+ <div>
232
+ <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">
233
+ <option value="默认">默认分类</option>
234
+ <option value="工作">工作</option>
235
+ <option value="生活">生活</option>
236
+ <option value="灵感">灵感</option>
237
+ <option value="学习">学习</option>
238
+ </select>
239
+ </div>
240
+ <div>
241
+ <textarea name="content" rows="4" required placeholder="今天想记录点什么?"
242
+ 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>
243
+ </div>
244
+ <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">
245
+ 保存记录
246
+ </button>
247
+ </form>
248
+ </div>
249
+
250
+ <!-- 分类筛选 -->
251
+ <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
252
+ <h3 class="text-sm font-bold text-gray-400 uppercase tracking-wider mb-4">分类筛选</h3>
253
+ <div class="flex flex-wrap gap-2">
254
+ <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>
255
+ {"".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])}
256
  </div>
257
+ </div>
258
+ </aside>
 
 
 
259
 
260
+ <!-- 右侧:列表展示 -->
261
+ <section class="flex-1">
262
+ <div class="flex items-center justify-between mb-6">
263
+ <h2 class="text-2xl font-bold text-gray-800">
264
+ { f'“{q}” 的搜索结果' if q else f'{cat or "全部"} 记录' }
265
+ </h2>
266
+ <span class="text-sm text-gray-400">共 {len(notes)} 条</span>
267
+ </div>
268
+
269
+ <div id="notes-list" class="space-y-4">
270
  {notes_html}
271
  </div>
272
  </section>
273
  </main>
274
 
275
+ <!-- 编辑弹窗 (Modal) -->
276
+ <div id="editModal" class="modal fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm items-center justify-center p-4">
277
+ <div class="bg-white w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden">
278
+ <div class="px-6 py-4 bg-gray-50 border-b border-gray-100 flex justify-between items-center">
279
+ <h3 class="font-bold text-gray-800">编辑记录</h3>
280
+ <button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
281
+ </div>
282
+ <form id="editForm" method="post" class="p-6 space-y-4" onsubmit="handleSubmit(this)">
283
+ <div>
284
+ <label class="block text-xs font-bold text-gray-400 mb-1">标题</label>
285
+ <input type="text" id="editTitle" name="title" required
286
+ class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
287
+ </div>
288
+ <div>
289
+ <label class="block text-xs font-bold text-gray-400 mb-1">分类</label>
290
+ <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">
291
+ <option value="默认">默认分类</option>
292
+ <option value="工作">工作</option>
293
+ <option value="生活">生活</option>
294
+ <option value="灵感">灵感</option>
295
+ <option value="学习">学习</option>
296
+ </select>
297
+ </div>
298
+ <div>
299
+ <label class="block text-xs font-bold text-gray-400 mb-1">内容</label>
300
+ <textarea id="editContent" name="content" rows="5" required
301
+ class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none"></textarea>
302
+ </div>
303
+ <div class="flex space-x-3 pt-2">
304
+ <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>
305
+ <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>
306
+ </div>
307
+ </form>
308
+ </div>
309
+ </div>
310
+
311
+ <script>
312
+ // 处理提交:防止双击
313
+ function handleSubmit(form) {{
314
+ const btn = form.querySelector('button[type="submit"]');
315
+ if (btn) {{
316
+ btn.disabled = true;
317
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...';
318
+ }}
319
+ return true;
320
+ }}
321
+
322
+ // 打开编辑弹窗
323
+ function openEditModal(id, title, content, category) {{
324
+ const modal = document.getElementById('editModal');
325
+ const form = document.getElementById('editForm');
326
+
327
+ form.action = `/api/notes/${{id}}/edit`;
328
+ document.getElementById('editTitle').value = title;
329
+ document.getElementById('editContent').value = content;
330
+ document.getElementById('editCategory').value = category;
331
+
332
+ modal.classList.add('active');
333
+ document.body.style.overflow = 'hidden';
334
+ }}
335
+
336
+ // 关闭弹窗
337
+ function closeModal() {{
338
+ const modal = document.getElementById('editModal');
339
+ modal.classList.remove('active');
340
+ document.body.style.overflow = 'auto';
341
+ }}
342
+
343
+ // 点击遮罩层关闭
344
+ document.getElementById('editModal').addEventListener('click', function(e) {{
345
+ if (e.target === this) closeModal();
346
+ }});
347
+ </script>
348
  </body>
349
  </html>
350
  """