Bladeren bron

feat: 新增文字错题录入功能

sequoia00 1 maand geleden
bovenliggende
commit
689ef28d56
9 gewijzigde bestanden met toevoegingen van 171 en 27 verwijderingen
  1. BIN
      __pycache__/app.cpython-313.pyc
  2. BIN
      __pycache__/crud.cpython-313.pyc
  3. BIN
      __pycache__/models.cpython-313.pyc
  4. 3 0
      add_text_fields.sql
  5. 15 0
      app.py
  6. 7 0
      crud.py
  7. 4 2
      models.py
  8. 26 0
      nohup.out
  9. 116 25
      templates/subject_detail.html

BIN
__pycache__/app.cpython-313.pyc


BIN
__pycache__/crud.cpython-313.pyc


BIN
__pycache__/models.cpython-313.pyc


+ 3 - 0
add_text_fields.sql

@@ -0,0 +1,3 @@
+ALTER TABLE error_image 
+ADD COLUMN question VARCHAR(500) NOT NULL DEFAULT '',
+ADD COLUMN answer VARCHAR(1000) NOT NULL DEFAULT '' AFTER original_name;

+ 15 - 0
app.py

@@ -115,6 +115,21 @@ async def upload_error_images(
     crud.add_error_images(session, subject_id, date, filenames, original_names)
     return RedirectResponse(url=f"/subject/{subject_id}?date={date}", status_code=303)
 
+# ── 录入文字错题 ────────────────────────────────────────
+@app.post("/subject/{subject_id}/text")
+async def add_error_text(
+    subject_id: int,
+    date: str = Form(...),
+    question: str = Form(...),
+    answer: str = Form(default=""),
+    session: Session = Depends(get_session)
+):
+    subject = session.get(Subject, subject_id)
+    if not subject:
+        raise HTTPException(404, "科目不存在")
+    crud.add_error_text(session, subject_id, date, question, answer)
+    return RedirectResponse(url=f"/subject/{subject_id}?date={date}", status_code=303)
+
 # ── 删除单张图片 ────────────────────────────────────────
 @app.post("/image/{image_id}/delete")
 async def delete_image(image_id: int, session: Session = Depends(get_session)):

+ 7 - 0
crud.py

@@ -40,6 +40,13 @@ def add_error_images(session: Session, subject_id: int, date: str, filenames: Li
         session.add(img)
     session.commit()
 
+def add_error_text(session: Session, subject_id: int, date: str, question: str, answer: str):
+    img = ErrorImage(subject_id=subject_id, date=date, question=question.strip(), answer=answer.strip())
+    session.add(img)
+    session.commit()
+    session.refresh(img)
+    return img
+
 def get_subject_images(session: Session, subject_id: int, date: Optional[str] = None) -> List[ErrorImage]:
     stmt = select(ErrorImage).where(ErrorImage.subject_id == subject_id)
     if date:

+ 4 - 2
models.py

@@ -17,8 +17,10 @@ class ErrorImage(SQLModel, table=True):
     id: Optional[int] = Field(default=None, primary_key=True)
     subject_id: int = Field(foreign_key="subject.id")
     date: str = Field(max_length=20, index=True)
-    filename: str = Field(max_length=255)
-    original_name: str = Field(max_length=255)
+    filename: str = Field(max_length=255, default="")
+    original_name: str = Field(max_length=255, default="")
+    question: str = Field(max_length=500, default="")
+    answer: str = Field(max_length=1000, default="")
     upload_time: datetime = Field(default_factory=datetime.now)
     subject: Subject = Relationship(back_populates="error_images")
 

+ 26 - 0
nohup.out

@@ -209,3 +209,29 @@ INFO:     Shutting down
 INFO:     Waiting for application shutdown.
 INFO:     Application shutdown complete.
 INFO:     Finished server process [2067450]
+INFO:     Started server process [2068284]
+INFO:     Waiting for application startup.
+INFO:     Application startup complete.
+INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+INFO:     219.146.255.202:63721 - "GET / HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63722 - "GET / HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63721 - "GET /static/uploads/e4e1cae0534f47f988212620b70e0c0e.png HTTP/1.1" 304 Not Modified
+INFO:     219.146.255.202:63721 - "GET /subjects HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63725 - "GET /subjects HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63721 - "GET /evaluations HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63728 - "GET /evaluations HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63738 - "GET /timeline HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63739 - "GET /timeline HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63743 - "GET / HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63744 - "GET / HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63743 - "GET /subject/1?date=2026-09-12 HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63757 - "GET /subject/1?date=2026%2D09%2D12 HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63809 - "GET /subject/2 HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63810 - "GET /subject/2 HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63809 - "GET /subjects HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63811 - "GET /subjects HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63809 - "GET /evaluations HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63812 - "GET /evaluations HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63821 - "GET /subject/1 HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63822 - "GET /subject/1 HTTP/1.1" 200 OK
+INFO:     219.146.255.202:63821 - "GET /static/uploads/e4e1cae0534f47f988212620b70e0c0e.png HTTP/1.1" 304 Not Modified

+ 116 - 25
templates/subject_detail.html

@@ -49,43 +49,80 @@
   </div>
 </div>
 
-<!-- Upload form -->
-<div class="bg-white rounded-2xl border border-dashed border-gray-200 p-5 mb-6">
-  <h4 class="font-semibold text-gray-700 text-sm mb-3"><i class="fa-solid fa-cloud-arrow-up mr-2 text-primary"></i>上传错题图片</h4>
-  <form method="POST" action="/subject/{{ subject.id }}/upload" enctype="multipart/form-data" id="uploadForm">
-    <input type="hidden" name="date" value="{{ current_date }}">
-    <div class="flex items-start gap-4">
-      <label class="flex-1 cursor-pointer">
-        <div class="border-2 border-dashed border-gray-200 rounded-xl p-6 text-center hover:border-primary/50 hover:bg-indigo-50/30 transition" id="dropZone">
-          <i class="fa-solid fa-images text-2xl text-gray-300 mb-2 block"></i>
-          <p class="text-sm text-gray-500">点击选择或拖拽图片到此</p>
-          <p class="text-xs text-gray-300 mt-1">支持 JPG、PNG、WEBP,可多选</p>
-          <input type="file" name="files" multiple accept="image/*" class="hidden" id="fileInput" onchange="updateFileLabel()">
+<!-- Input tabs -->
+<div class="bg-white rounded-2xl border border-gray-100 p-5 mb-6">
+  <div class="flex gap-2 mb-5">
+    <button id="tabImage" onclick="switchTab('image')" class="tab-btn flex-1 py-2.5 rounded-xl text-sm font-medium transition bg-primary text-white shadow-sm">
+      <i class="fa-solid fa-image mr-1.5"></i>上传错题图片
+    </button>
+    <button id="tabText" onclick="switchTab('text')" class="tab-btn flex-1 py-2.5 rounded-xl text-sm font-medium transition bg-gray-100 text-gray-500 hover:bg-gray-50">
+      <i class="fa-solid fa-keyboard mr-1.5"></i>文字录���错题
+    </button>
+  </div>
+
+  <!-- Image upload panel -->
+  <div id="panelImage">
+    <form method="POST" action="/subject/{{ subject.id }}/upload" enctype="multipart/form-data" id="uploadForm">
+      <input type="hidden" name="date" value="{{ current_date }}">
+      <div class="flex items-start gap-4">
+        <label class="flex-1 cursor-pointer">
+          <div class="border-2 border-dashed border-gray-200 rounded-xl p-6 text-center hover:border-primary/50 hover:bg-indigo-50/30 transition" id="dropZone">
+            <i class="fa-solid fa-images text-2xl text-gray-300 mb-2 block"></i>
+            <p class="text-sm text-gray-500">点击选择或拖拽图片到此</p>
+            <p class="text-xs text-gray-300 mt-1">支持 JPG、PNG、WEBP,可多选</p>
+            <input type="file" name="files" multiple accept="image/*" class="hidden" id="fileInput" onchange="updateFileLabel()">
+          </div>
+        </label>
+        <button type="submit" class="flex-shrink-0 bg-primary text-white px-5 py-2.5 rounded-xl text-sm font-medium hover:bg-indigo-600 transition shadow-sm">
+          <i class="fa-solid fa-upload mr-1.5"></i>上传
+        </button>
+      </div>
+      <p id="fileLabel" class="text-xs text-primary mt-2 min-h-[18px]"></p>
+    </form>
+  </div>
+
+  <!-- Text input panel -->
+  <div id="panelText" class="hidden">
+    <form method="POST" action="/subject/{{ subject.id }}/text">
+      <input type="hidden" name="date" value="{{ current_date }}">
+      <div class="space-y-4">
+        <div>
+          <label class="block text-sm font-medium text-gray-600 mb-1.5">错题题目 <span class="text-red-400">*</span></label>
+          <textarea name="question" required rows="3" placeholder="请输入错题的题目内容..."
+            class="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition resize-none"></textarea>
         </div>
-      </label>
-      <button type="submit" class="flex-shrink-0 bg-primary text-white px-5 py-2.5 rounded-xl text-sm font-medium hover:bg-indigo-600 transition shadow-sm">
-        <i class="fa-solid fa-upload mr-1.5"></i>上传
-      </button>
-    </div>
-    <p id="fileLabel" class="text-xs text-primary mt-2 min-h-[18px]"></p>
-  </form>
+        <div>
+          <label class="block text-sm font-medium text-gray-600 mb-1.5">正确答案 / 解析</label>
+          <textarea name="answer" rows="2" placeholder="请输入正确答案或解题思路..."
+            class="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition resize-none"></textarea>
+        </div>
+        <div class="flex justify-end">
+          <button type="submit" class="bg-primary text-white px-6 py-2.5 rounded-xl text-sm font-medium hover:bg-indigo-600 transition shadow-sm">
+            <i class="fa-solid fa-check mr-1.5"></i>保存错题
+          </button>
+        </div>
+      </div>
+    </form>
+  </div>
 </div>
 
-<!-- Image grid -->
-{% if images %}
+<!-- Content grid -->
+{% set text_errors = images | selectattr('filename', 'equalto', '') | list %}
+{% set img_errors = images | selectattr('filename', 'notequalto', '') | list %}
+{% if img_errors or text_errors %}
 <div class="mb-4 flex items-center justify-between">
-  <h3 class="font-semibold text-gray-700 text-sm">{{ current_date }} 的错题({{ images|length }} 张)</h3>
-  <span class="text-xs text-gray-400">点击可放大查看</span>
+  <h3 class="font-semibold text-gray-700 text-sm">{{ current_date }} 的错题({{ images|length }} 条)</h3>
+  <span class="text-xs text-gray-400">{% if img_errors %}{{ img_errors|length }} 张图片{% if text_errors %} · {{ text_errors|length }} 条文字{% endif %}{% else %}{{ text_errors|length }} 条文字记录{% endif %}</span>
 </div>
 <div class="grid grid-cols-4 gap-4">
-  {% for img in images %}
+  {% for img in img_errors %}
   <div class="image-card group relative aspect-[3/4] rounded-2xl overflow-hidden bg-gray-100 shadow-sm cursor-pointer" onclick="openLightbox('/static/uploads/{{ img.filename }}','{{ img.original_name }}',lbAllImages,{{ loop.index0 }})">
     <img src="/static/uploads/{{ img.filename }}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" loading="lazy">
     <div class="image-actions absolute inset-0 bg-black/30 opacity-0 transition flex items-center justify-center gap-2">
       <button onclick="event.stopPropagation();openLightbox('/static/uploads/{{ img.filename }}','{{ img.original_name }}',lbAllImages,{{ loop.index0 }})" class="w-9 h-9 rounded-full bg-white/90 flex items-center justify-center text-gray-700 hover:bg-white transition" title="查看">
         <i class="fa-solid fa-magnifying-glass-plus text-sm"></i>
       </button>
-      <form method="POST" action="/image/{{ img.id }}/delete" onsubmit="return confirm('确定删除这张错题图片?')" onclick="event.stopPropagation()">
+      <form method="POST" action="/image/{{ img.id }}/delete" onsubmit="return confirm('确定删除这条错题记录?')" onclick="event.stopPropagation()">
         <button type="submit" class="w-9 h-9 rounded-full bg-white/90 flex items-center justify-center text-red-500 hover:bg-red-50 transition" title="删除">
           <i class="fa-solid fa-trash-can text-sm"></i>
         </button>
@@ -96,6 +133,35 @@
     </div>
   </div>
   {% endfor %}
+  {% for txt in text_errors %}
+  <div class="image-card group relative rounded-2xl overflow-hidden bg-white shadow-sm border border-gray-100 cursor-pointer hover:shadow-md transition" onclick="toggleAnswer({{ txt.id }})">
+    <div class="p-4 flex flex-col h-full">
+      <div class="flex items-center gap-2 mb-2">
+        <span class="w-2 h-2 rounded-full bg-violet-400 flex-shrink-0"></span>
+        <span class="text-[10px] text-gray-400 flex-shrink-0">文字记录</span>
+        <div class="ml-auto flex gap-1 image-actions opacity-0 transition" onclick="event.stopPropagation()">
+          <form method="POST" action="/image/{{ txt.id }}/delete" onsubmit="return confirm('确定删除这条错题记录?')">
+            <button type="submit" class="w-7 h-7 rounded-full bg-gray-100 hover:bg-red-50 flex items-center justify-center text-gray-400 hover:text-red-500 transition" title="删除">
+              <i class="fa-solid fa-trash-can text-xs"></i>
+            </button>
+          </form>
+        </div>
+      </div>
+      <p class="text-sm text-gray-800 font-medium leading-relaxed flex-1 {{ 'line-clamp-4' if not txt.answer else 'line-clamp-3' }}">{{ txt.question }}</p>
+      {% if txt.answer %}
+      <div id="answer-{{ txt.id }}" class="hidden mt-3 pt-3 border-t border-gray-100">
+        <p class="text-xs text-gray-500 leading-relaxed">{{ txt.answer }}</p>
+      </div>
+      {% endif %}
+    </div>
+    <div class="px-4 py-2.5 bg-gray-50 border-t border-gray-100 flex items-center justify-between">
+      <span class="text-[10px] text-gray-400">点击{%'展开解析{% if txt.answer %} / 收起解析{% endif %}'%}</span>
+      {% if txt.answer %}
+      <span class="text-[10px] text-primary flex items-center gap-1"><i class="fa-solid fa-lightbulb"></i>有解析</span>
+      {% endif %}
+    </div>
+  </div>
+  {% endfor %}
 </div>
 {% else %}
 <div class="bg-white rounded-2xl border border-gray-100 p-12 text-center">
@@ -107,6 +173,31 @@
 
 {% block extra_js %}
 <script>
+function switchTab(tab) {
+  var pi = document.getElementById('panelImage');
+  var pt = document.getElementById('panelText');
+  var bi = document.getElementById('tabImage');
+  var bt = document.getElementById('tabText');
+  if (tab === 'text') {
+    pi.classList.add('hidden');
+    pt.classList.remove('hidden');
+    bi.classList.remove('bg-primary','text-white','shadow-sm');
+    bi.classList.add('bg-gray-100','text-gray-500','hover:bg-gray-50');
+    bt.classList.remove('bg-gray-100','text-gray-500','hover:bg-gray-50');
+    bt.classList.add('bg-primary','text-white','shadow-sm');
+  } else {
+    pt.classList.add('hidden');
+    pi.classList.remove('hidden');
+    bt.classList.remove('bg-primary','text-white','shadow-sm');
+    bt.classList.add('bg-gray-100','text-gray-500','hover:bg-gray-50');
+    bi.classList.remove('bg-gray-100','text-gray-500','hover:bg-gray-50');
+    bi.classList.add('bg-primary','text-white','shadow-sm');
+  }
+}
+function toggleAnswer(id) {
+  var el = document.getElementById('answer-' + id);
+  if (el) el.classList.toggle('hidden');
+}
 const lbAllImages = [
   {% for img in images %}
   "/static/uploads/{{ img.filename }}",