fix: 优化个相机功能与样式
This commit is contained in:
192
frontend/dist/assets/index-9iYNjrsg.js
vendored
192
frontend/dist/assets/index-9iYNjrsg.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
204
frontend/dist/assets/index-Dw7fpRnj.js
vendored
Normal file
204
frontend/dist/assets/index-Dw7fpRnj.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@@ -9,8 +9,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<title>学习伙伴</title>
|
||||
<script type="module" crossorigin src="/assets/index-9iYNjrsg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C-dSv0ph.css">
|
||||
<script type="module" crossorigin src="/assets/index-Dw7fpRnj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CoOKrmS9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
FileDown,
|
||||
FolderOpen,
|
||||
GraduationCap,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
MoreHorizontal,
|
||||
NotebookPen,
|
||||
Plus,
|
||||
@@ -17,6 +19,24 @@ import {
|
||||
|
||||
const api = axios.create({ baseURL: "/" });
|
||||
|
||||
const QUICK_CAMERA_FAB_COMPACT_KEY = "studyBuddy_quickCameraFabCompact";
|
||||
|
||||
function readQuickCameraFabCompact() {
|
||||
try {
|
||||
return localStorage.getItem(QUICK_CAMERA_FAB_COMPACT_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writeQuickCameraFabCompact(value) {
|
||||
try {
|
||||
localStorage.setItem(QUICK_CAMERA_FAB_COMPACT_KEY, value ? "1" : "0");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
const CATEGORIES = ["常识", "数量关系", "言语理解", "判断推理", "资料分析"];
|
||||
const MISTAKE_CATEGORIES = ["常识", "言语", "数量", "判断", "资料", "科学", "其他"];
|
||||
|
||||
@@ -616,6 +636,7 @@ function ResourceModule() {
|
||||
}
|
||||
|
||||
function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
const PENDING_IMAGE_TITLE = "待补录图片错题";
|
||||
const [items, setItems] = useState([]);
|
||||
const [selectedExportIds, setSelectedExportIds] = useState([]);
|
||||
const [categoryFilter, setCategoryFilter] = useState("");
|
||||
@@ -632,6 +653,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
const [exportScope, setExportScope] = useState("all");
|
||||
const [exportContentMode, setExportContentMode] = useState("full");
|
||||
const [exportRange, setExportRange] = useState({ start_date: "", end_date: "" });
|
||||
const [batchRecognizeScope, setBatchRecognizeScope] = useState("all_pending");
|
||||
|
||||
const uploadInputRef = useRef(null);
|
||||
const captureInputRef = useRef(null);
|
||||
@@ -651,6 +673,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
});
|
||||
const [ocrText, setOcrText] = useState("");
|
||||
const [ocrLoading, setOcrLoading] = useState(false);
|
||||
const [batchRecognizing, setBatchRecognizing] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [detailItem, setDetailItem] = useState(null);
|
||||
|
||||
@@ -660,6 +683,15 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
return source.slice(0, 40);
|
||||
};
|
||||
|
||||
const isPendingImageTitle = (title) => String(title || "").trim() === PENDING_IMAGE_TITLE;
|
||||
|
||||
const resolveTitleAfterOcr = (currentTitle, data) => {
|
||||
const recognizedTitle = data?.title_suggestion || buildTitleFromQuestion(data?.question_content || data?.text);
|
||||
const current = String(currentTitle || "").trim();
|
||||
if (!current || isPendingImageTitle(current)) return recognizedTitle || current || PENDING_IMAGE_TITLE;
|
||||
return current;
|
||||
};
|
||||
|
||||
/** 模型常把全文放在 text,question_content 只有短问句;题目内容应取更完整的一份 */
|
||||
const mergeQuestionContent = (questionContent, fullText) => {
|
||||
const q = String(questionContent || "").trim();
|
||||
@@ -689,7 +721,11 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
const res = await api.get("/api/mistakes", { params: apiFilters });
|
||||
setItems(res.data);
|
||||
setSelectedExportIds((prev) => prev.filter((id) => res.data.some((x) => x.id === id)));
|
||||
setDetailItem((prev) => (prev && !res.data.some((x) => x.id === prev.id) ? null : prev));
|
||||
setDetailItem((prev) => {
|
||||
if (!prev) return prev;
|
||||
const latest = res.data.find((x) => x.id === prev.id);
|
||||
return latest || null;
|
||||
});
|
||||
} catch (error) {
|
||||
show(getApiErrorMessage(error, "加载错题失败"));
|
||||
}
|
||||
@@ -708,7 +744,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
setOcrText(data.text || "");
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
title: prev.title || data.title_suggestion || buildTitleFromQuestion(data.question_content || data.text),
|
||||
title: resolveTitleAfterOcr(prev.title, data),
|
||||
category: MISTAKE_CATEGORIES.includes(data.category_suggestion) ? data.category_suggestion : prev.category,
|
||||
difficulty: ["easy", "medium", "hard"].includes(data.difficulty_suggestion) ? data.difficulty_suggestion : prev.difficulty,
|
||||
question_content: (
|
||||
@@ -868,7 +904,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
title: prev.title || data.title_suggestion || buildTitleFromQuestion(data.question_content || data.text),
|
||||
title: resolveTitleAfterOcr(prev.title, data),
|
||||
category: MISTAKE_CATEGORIES.includes(data.category_suggestion) ? data.category_suggestion : prev.category,
|
||||
difficulty: ["easy", "medium", "hard"].includes(data.difficulty_suggestion) ? data.difficulty_suggestion : prev.difficulty,
|
||||
question_content: (mergeQuestionContent(data.question_content, data.text) || prev.question_content || "").slice(0, 8000),
|
||||
@@ -885,6 +921,60 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
}
|
||||
};
|
||||
|
||||
const batchRecognizePending = async () => {
|
||||
setBatchRecognizing(true);
|
||||
try {
|
||||
let candidates = [];
|
||||
if (batchRecognizeScope === "current_filtered") {
|
||||
candidates = (items || []).filter((item) => isPendingImageTitle(item.title) && String(item.image_url || "").trim());
|
||||
} else {
|
||||
const res = await api.get("/api/mistakes", {
|
||||
params: {
|
||||
keyword: PENDING_IMAGE_TITLE,
|
||||
sort_by: "created_at",
|
||||
order: "desc"
|
||||
}
|
||||
});
|
||||
candidates = (res.data || []).filter((item) => isPendingImageTitle(item.title) && String(item.image_url || "").trim());
|
||||
}
|
||||
if (!candidates.length) {
|
||||
show(batchRecognizeScope === "current_filtered" ? "当前筛选结果没有可批量识别的待补录图片错题" : "没有可批量识别的待补录图片错题");
|
||||
return;
|
||||
}
|
||||
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
|
||||
for (const item of candidates) {
|
||||
try {
|
||||
const parsed = await api.post("/api/ocr/parse", { image_url: item.image_url });
|
||||
const data = parsed.data;
|
||||
await api.put(`/api/mistakes/${item.id}`, {
|
||||
...item,
|
||||
title: resolveTitleAfterOcr(item.title, data),
|
||||
category: MISTAKE_CATEGORIES.includes(data.category_suggestion) ? data.category_suggestion : item.category,
|
||||
difficulty: ["easy", "medium", "hard"].includes(data.difficulty_suggestion) ? data.difficulty_suggestion : item.difficulty || "medium",
|
||||
question_content: (mergeQuestionContent(data.question_content, data.text) || item.question_content || "").slice(0, 8000),
|
||||
answer: (data.answer || item.answer || "").slice(0, 4000),
|
||||
explanation: (data.explanation || item.explanation || "").slice(0, 8000),
|
||||
note: [item.note, data.text].filter(Boolean).join("\n\n").slice(0, 4000),
|
||||
wrong_count: Number(item.wrong_count || 1)
|
||||
});
|
||||
ok += 1;
|
||||
} catch {
|
||||
fail += 1;
|
||||
}
|
||||
}
|
||||
|
||||
await load();
|
||||
show(`批量识别完成:成功 ${ok},失败 ${fail}`);
|
||||
} catch (error) {
|
||||
show(getApiErrorMessage(error, "批量识别失败"));
|
||||
} finally {
|
||||
setBatchRecognizing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!form.category) return show("请选择分类");
|
||||
@@ -897,7 +987,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
title:
|
||||
form.title ||
|
||||
buildTitleFromQuestion(form.question_content) ||
|
||||
(hasImage ? "待补录图片错题" : "") ||
|
||||
(hasImage ? PENDING_IMAGE_TITLE : "") ||
|
||||
`错题-${Date.now()}`
|
||||
};
|
||||
try {
|
||||
@@ -937,7 +1027,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
title:
|
||||
editingItem.title ||
|
||||
buildTitleFromQuestion(editingItem.question_content) ||
|
||||
(hasImage ? "待补录图片错题" : "") ||
|
||||
(hasImage ? PENDING_IMAGE_TITLE : "") ||
|
||||
`错题-${editingItem.id}`,
|
||||
wrong_count: Number(editingItem.wrong_count || 1)
|
||||
});
|
||||
@@ -1042,7 +1132,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
try {
|
||||
const url = await uploadImageFileOnly(file);
|
||||
await api.post("/api/mistakes", {
|
||||
title: "待补录图片错题",
|
||||
title: PENDING_IMAGE_TITLE,
|
||||
image_url: url,
|
||||
category: "其他",
|
||||
difficulty: "medium",
|
||||
@@ -1094,6 +1184,18 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
|
||||
>
|
||||
<FileDown size={18} /> 导出错题
|
||||
</button>
|
||||
<select
|
||||
className="select-min"
|
||||
value={batchRecognizeScope}
|
||||
onChange={(e) => setBatchRecognizeScope(e.target.value)}
|
||||
aria-label="批量识别范围"
|
||||
>
|
||||
<option value="all_pending">识别范围:全部待补录</option>
|
||||
<option value="current_filtered">识别范围:当前筛选</option>
|
||||
</select>
|
||||
<button type="button" className="btn btn-outline btn-pill" onClick={batchRecognizePending} disabled={batchRecognizing}>
|
||||
{batchRecognizing ? "批量识别中…" : "批量识别待补录"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="toolbar-right">
|
||||
<select className="select-min" value={categoryFilter} onChange={(e) => setCategoryFilter(e.target.value)} aria-label="分类">
|
||||
@@ -1846,6 +1948,7 @@ export default function App() {
|
||||
const [mainTab, setMainTab] = useState("mistake");
|
||||
const [extraTab, setExtraTab] = useState("resource");
|
||||
const [showQuickCamera, setShowQuickCamera] = useState(false);
|
||||
const [quickCameraFabCompact, setQuickCameraFabCompact] = useState(() => readQuickCameraFabCompact());
|
||||
const [quickCaptureMode, setQuickCaptureMode] = useState("single");
|
||||
const [quickCaptureTask, setQuickCaptureTask] = useState(null);
|
||||
const quickCaptureInputRef = useRef(null);
|
||||
@@ -1854,6 +1957,11 @@ export default function App() {
|
||||
quickCaptureInputRef.current?.click();
|
||||
};
|
||||
|
||||
const setQuickCameraFabCompactPersist = (value) => {
|
||||
setQuickCameraFabCompact(value);
|
||||
writeQuickCameraFabCompact(value);
|
||||
};
|
||||
|
||||
const onQuickCapturePicked = (filesLike) => {
|
||||
const files = Array.from(filesLike || []);
|
||||
if (!files.length) return;
|
||||
@@ -1963,10 +2071,46 @@ export default function App() {
|
||||
onChange={(e) => onQuickCapturePicked(e.target.files)}
|
||||
/>
|
||||
|
||||
<button type="button" className="quick-camera-fab" onClick={() => setShowQuickCamera(true)} aria-label="快速拍照录题">
|
||||
<Camera size={20} />
|
||||
快速拍题
|
||||
<div className="quick-camera-fab-shell">
|
||||
{quickCameraFabCompact ? (
|
||||
<div className="quick-camera-fab-cluster">
|
||||
<button
|
||||
type="button"
|
||||
className="quick-camera-fab-expand"
|
||||
onClick={() => setQuickCameraFabCompactPersist(false)}
|
||||
aria-label="展开快速拍题标签"
|
||||
title="展开标签"
|
||||
>
|
||||
<Maximize2 size={13} strokeWidth={2.25} aria-hidden />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="quick-camera-fab quick-camera-fab--compact"
|
||||
onClick={() => setShowQuickCamera(true)}
|
||||
aria-label="快速拍照录题(已收纳为图标)"
|
||||
title="快速拍题"
|
||||
>
|
||||
<Camera size={20} strokeWidth={2} aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="quick-camera-fab quick-camera-fab--split" role="group" aria-label="快速拍题">
|
||||
<button type="button" className="quick-camera-fab__open" onClick={() => setShowQuickCamera(true)}>
|
||||
<Camera size={20} strokeWidth={2} aria-hidden />
|
||||
<span>快速拍题</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="quick-camera-fab__collapse"
|
||||
onClick={() => setQuickCameraFabCompactPersist(true)}
|
||||
aria-label="收纳为图标,减少遮挡"
|
||||
title="收纳为图标"
|
||||
>
|
||||
<Minimize2 size={14} strokeWidth={2.25} aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showQuickCamera && (
|
||||
<Modal title="快速拍照录题" onClose={() => setShowQuickCamera(false)}>
|
||||
|
||||
@@ -846,14 +846,18 @@ textarea {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.quick-camera-fab {
|
||||
.quick-camera-fab-shell {
|
||||
position: fixed;
|
||||
right: 18px;
|
||||
bottom: calc(18px + env(safe-area-inset-bottom));
|
||||
z-index: 90;
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.quick-camera-fab {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 12px 16px;
|
||||
@@ -864,11 +868,125 @@ textarea {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quick-camera-fab--split {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-camera-fab__open {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 4px 12px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quick-camera-fab__collapse {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 14px 12px 10px;
|
||||
border: none;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.28);
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quick-camera-fab__collapse:hover,
|
||||
.quick-camera-fab__collapse:focus-visible {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.quick-camera-fab__open:focus-visible,
|
||||
.quick-camera-fab__collapse:focus-visible,
|
||||
.quick-camera-fab--compact:focus-visible,
|
||||
.quick-camera-fab-expand:focus-visible {
|
||||
outline: 2px solid #93c5fd;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 收纳态:展开键 + 相机融为一条胶囊,单阴影 */
|
||||
.quick-camera-fab-cluster {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
height: 42px;
|
||||
border-radius: 21px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.26);
|
||||
background: linear-gradient(135deg, #0ea5e9, #2563eb);
|
||||
}
|
||||
|
||||
.quick-camera-fab-cluster .quick-camera-fab-expand {
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0;
|
||||
background: rgba(0, 0, 0, 0.07);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-camera-fab-cluster .quick-camera-fab-expand:hover,
|
||||
.quick-camera-fab-cluster .quick-camera-fab-expand:focus-visible {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.quick-camera-fab-cluster .quick-camera-fab--compact {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.quick-camera-fab {
|
||||
.quick-camera-fab-shell {
|
||||
right: 14px;
|
||||
bottom: calc(76px + env(safe-area-inset-bottom));
|
||||
padding: 11px 14px;
|
||||
}
|
||||
|
||||
.quick-camera-fab__open {
|
||||
padding: 11px 4px 11px 14px;
|
||||
}
|
||||
|
||||
.quick-camera-fab__collapse {
|
||||
padding: 11px 12px 11px 8px;
|
||||
}
|
||||
|
||||
.quick-camera-fab-cluster {
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.quick-camera-fab-cluster .quick-camera-fab-expand {
|
||||
flex-basis: 22px;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
}
|
||||
|
||||
.quick-camera-fab-cluster .quick-camera-fab--compact {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user