From b4e1774feb16f57efb4c426ccfad772de5e8f60f Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 18 Apr 2026 20:25:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BC=98=E5=8C=96=E8=AF=95=E9=A2=98?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E5=92=8C=E6=A0=B7=E5=BC=8F=E6=8E=92=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 138 +++++++++++++++++++++++++++++++++++++++- frontend/src/styles.css | 26 ++++++++ 2 files changed, 162 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8434bb0..97629ca 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,6 +4,7 @@ import { Line, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContai import { BookOpen, Bot, + Camera, ChartNoAxesCombined, FileDown, FolderOpen, @@ -553,7 +554,7 @@ function ResourceModule() { ); } -function MistakeModule() { +function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) { const [items, setItems] = useState([]); const [selectedId, setSelectedId] = useState(null); const [selectedExportIds, setSelectedExportIds] = useState([]); @@ -726,6 +727,14 @@ function MistakeModule() { } }; + const uploadImageFileOnly = async (file) => { + if (!file) return ""; + const fd = new FormData(); + fd.append("file", file); + const res = await api.post("/api/upload", fd, { headers: { "Content-Type": "multipart/form-data" } }); + return res.data.url; + }; + const rerunOcrForEditing = async () => { const imageUrl = String(editingItem?.image_url || "").trim(); if (!imageUrl) { @@ -900,6 +909,59 @@ function MistakeModule() { show("已清空勾选"); }; + useEffect(() => { + if (!quickCaptureTask?.id) return; + let cancelled = false; + + const run = async () => { + try { + const files = Array.isArray(quickCaptureTask.files) ? quickCaptureTask.files : []; + if (!files.length) return; + + if (quickCaptureTask.mode === "single") { + setShowAdd(true); + await uploadImage(files[0], true); + return; + } + + let ok = 0; + let fail = 0; + for (const file of files) { + try { + const url = await uploadImageFileOnly(file); + await api.post("/api/mistakes", { + title: "待补录图片错题", + image_url: url, + category: "其他", + difficulty: "medium", + question_content: "", + answer: "", + explanation: "", + note: "", + wrong_count: 1 + }); + ok += 1; + } catch { + fail += 1; + } + } + if (!cancelled) { + await load(); + show(`连拍导入完成:成功 ${ok},失败 ${fail}`); + } + } finally { + if (!cancelled) { + onQuickCaptureHandled?.(quickCaptureTask.id); + } + } + }; + + run(); + return () => { + cancelled = true; + }; + }, [quickCaptureTask?.id]); + const diffLabel = { easy: "易", medium: "中", hard: "难" }; return ( @@ -1680,6 +1742,29 @@ function AiModule() { export default function App() { const [mainTab, setMainTab] = useState("mistake"); const [extraTab, setExtraTab] = useState("resource"); + const [showQuickCamera, setShowQuickCamera] = useState(false); + const [quickCaptureMode, setQuickCaptureMode] = useState("single"); + const [quickCaptureTask, setQuickCaptureTask] = useState(null); + const quickCaptureInputRef = useRef(null); + + const triggerQuickCapture = () => { + quickCaptureInputRef.current?.click(); + }; + + const onQuickCapturePicked = (filesLike) => { + const files = Array.from(filesLike || []); + if (!files.length) return; + setMainTab("mistake"); + setQuickCaptureTask({ + id: Date.now(), + mode: quickCaptureMode, + files + }); + setShowQuickCamera(false); + if (quickCaptureInputRef.current) { + quickCaptureInputRef.current.value = ""; + } + }; return (
@@ -1748,7 +1833,14 @@ export default function App() { )}
- {mainTab === "mistake" && } + {mainTab === "mistake" && ( + { + setQuickCaptureTask((prev) => (prev?.id === taskId ? null : prev)); + }} + /> + )} {mainTab === "score" && } {mainTab === "more" && ( <> @@ -1757,6 +1849,48 @@ export default function App() { )}
+ + onQuickCapturePicked(e.target.files)} + /> + + + + {showQuickCamera && ( + setShowQuickCamera(false)}> +
+ +
+ + +
+
+ 单拍:拍一张后自动打开新增错题并识别。连拍:可一次选择多张,批量生成待补录错题。 +
+
+
+ + +
+
+ )}
); } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 62c35d2..30bffe1 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -824,6 +824,32 @@ textarea { width: auto; } +.quick-camera-fab { + position: fixed; + right: 18px; + bottom: calc(18px + env(safe-area-inset-bottom)); + z-index: 90; + display: inline-flex; + align-items: center; + gap: 8px; + border: none; + border-radius: 999px; + padding: 12px 16px; + background: linear-gradient(135deg, #0ea5e9, #2563eb); + color: #fff; + font-weight: 700; + box-shadow: 0 10px 24px rgba(37, 99, 235, 0.32); + cursor: pointer; +} + +@media (max-width: 860px) { + .quick-camera-fab { + right: 14px; + bottom: calc(76px + env(safe-area-inset-bottom)); + padding: 11px 14px; + } +} + .toast { position: fixed; left: 50%;