fix: 优化整个项目内容

This commit is contained in:
Daniel
2026-04-19 15:09:22 +08:00
parent 019963abd6
commit 06af48f560
9 changed files with 299 additions and 257 deletions

File diff suppressed because one or more lines are too long

192
frontend/dist/assets/index-DyP_J9zM.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>公考助手</title>
<script type="module" crossorigin src="/assets/index-B6wMcdCx.js"></script>
<title>学习伙伴</title>
<script type="module" crossorigin src="/assets/index-DyP_J9zM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C-dSv0ph.css">
</head>
<body>

View File

@@ -1,7 +1,7 @@
{
"name": "公考助手",
"short_name": "公考助手",
"description": "公考学习资源、错题与过程管理",
"name": "学习伙伴",
"short_name": "学习伙伴",
"description": "学习资源、错题与过程管理",
"display": "standalone",
"background_color": "#e8ecf1",
"theme_color": "#2563eb",

View File

@@ -8,7 +8,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>公考助手</title>
<title>学习伙伴</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,7 +1,7 @@
{
"name": "公考助手",
"short_name": "公考助手",
"description": "公考学习资源、错题与过程管理",
"name": "学习伙伴",
"short_name": "学习伙伴",
"description": "学习资源、错题与过程管理",
"display": "standalone",
"background_color": "#e8ecf1",
"theme_color": "#2563eb",

View File

@@ -614,9 +614,7 @@ function ResourceModule() {
function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
const [items, setItems] = useState([]);
const [selectedId, setSelectedId] = useState(null);
const [selectedExportIds, setSelectedExportIds] = useState([]);
const [analysis, setAnalysis] = useState("");
const [categoryFilter, setCategoryFilter] = useState("");
const [keyword, setKeyword] = useState("");
const [sortKey, setSortKey] = useState("time_desc");
@@ -688,9 +686,7 @@ 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)));
if (selectedId && !res.data.some((x) => x.id === selectedId)) {
setSelectedId(null);
}
setDetailItem((prev) => (prev && !res.data.some((x) => x.id === prev.id) ? null : prev));
} catch (error) {
show(getApiErrorMessage(error, "加载错题失败"));
}
@@ -734,6 +730,56 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
runOcr(imageUrl).catch(() => {});
};
const normalizeCaptureFile = async (file) => {
if (!file) return file;
const type = String(file.type || "").toLowerCase();
const isAlreadySupported = ["image/jpeg", "image/png", "image/webp"].includes(type);
const hasKnownExt = /\.(jpe?g|png|webp)$/i.test(file.name || "");
if (isAlreadySupported && hasKnownExt) return file;
if (!type.startsWith("image/")) return file;
try {
const dataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(new Error("读取图片失败"));
reader.readAsDataURL(file);
});
const img = await new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error("图片解码失败"));
image.src = dataUrl;
});
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
if (!ctx) return file;
ctx.drawImage(img, 0, 0);
const blob = await new Promise((resolve, reject) => {
canvas.toBlob(
(b) => {
if (!b) reject(new Error("图片转换失败"));
else resolve(b);
},
"image/jpeg",
0.92
);
});
const baseName = String(file.name || "capture").replace(/\.[^.]+$/, "");
return new File([blob], `${baseName || "capture"}-${Date.now()}.jpg`, { type: "image/jpeg" });
} catch {
// If conversion fails, fallback to original file; backend still has suffix+mime fallback.
return file;
}
};
const uploadImageBlob = async (blob, fileName = `scan-${Date.now()}.jpg`, autoParse = true) => {
const fd = new FormData();
fd.append("file", new File([blob], fileName, { type: blob.type || "image/jpeg" }));
@@ -753,8 +799,9 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
const uploadImage = async (file, autoParse = true) => {
if (!file) return;
const normalized = await normalizeCaptureFile(file);
const fd = new FormData();
fd.append("file", file);
fd.append("file", normalized);
setUploading(true);
try {
const res = await api.post("/api/upload", fd, { headers: { "Content-Type": "multipart/form-data" } });
@@ -771,8 +818,9 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
const replaceEditingImage = async (file) => {
if (!file) return;
const normalized = await normalizeCaptureFile(file);
const fd = new FormData();
fd.append("file", file);
fd.append("file", normalized);
setUploading(true);
try {
const res = await api.post("/api/upload", fd, { headers: { "Content-Type": "multipart/form-data" } });
@@ -787,8 +835,9 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
const uploadImageFileOnly = async (file) => {
if (!file) return "";
const normalized = await normalizeCaptureFile(file);
const fd = new FormData();
fd.append("file", file);
fd.append("file", normalized);
const res = await api.post("/api/upload", fd, { headers: { "Content-Type": "multipart/form-data" } });
return res.data.url;
};
@@ -891,7 +940,7 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
const remove = async (id) => {
if (!window.confirm("确认删除该错题?")) return;
await api.delete(`/api/mistakes/${id}`);
if (selectedId === id) setSelectedId(null);
setDetailItem((prev) => (prev?.id === id ? null : prev));
show("错题已删除");
load();
};
@@ -934,17 +983,6 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
show("已开始下载");
};
const askAi = async () => {
if (!selectedId) return show("请先点击列表中的一条错题");
try {
const res = await api.post(`/api/ai/mistakes/${selectedId}/analyze`);
setAnalysis(res.data.analysis);
show("解析完成");
} catch (error) {
show(getApiErrorMessage(error, "AI 错题解析失败"));
}
};
const toggleExportSelected = (id) => {
setSelectedExportIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
};
@@ -978,7 +1016,11 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
if (quickCaptureTask.mode === "single") {
setShowAdd(true);
await uploadImage(files[0], true);
try {
await uploadImage(files[0], true);
} catch (error) {
show(getApiErrorMessage(error, "快速拍题上传失败,请重试"));
}
return;
}
@@ -1040,9 +1082,6 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
>
<FileDown size={18} /> 导出错题
</button>
<button type="button" className="btn btn-outline btn-pill" onClick={askAi} disabled={!selectedId}>
AI 解析
</button>
</div>
<div className="toolbar-right">
<select className="select-min" value={categoryFilter} onChange={(e) => setCategoryFilter(e.target.value)} aria-label="分类">
@@ -1093,17 +1132,11 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
<div
role="button"
tabIndex={0}
className={`mistake-card ${selectedId === item.id ? "is-selected" : ""}`}
onClick={() => {
setSelectedId(item.id);
setAnalysis("");
setDetailItem(item);
}}
className={`mistake-card ${detailItem?.id === item.id ? "is-selected" : ""}`}
onClick={() => setDetailItem(item)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setSelectedId(item.id);
setAnalysis("");
setDetailItem(item);
}
}}
@@ -1144,13 +1177,6 @@ function MistakeModule({ quickCaptureTask, onQuickCaptureHandled }) {
)}
</div>
{analysis && (
<div className="panel ai-result">
<h4 className="panel-subtitle">AI 解析</h4>
<pre className="pre-wrap">{analysis}</pre>
</div>
)}
{showAdd && (
<Modal title="添加错题" onClose={() => setShowAdd(false)}>
<form onSubmit={submit}>
@@ -1753,7 +1779,7 @@ function ScoreModule() {
function AiModule() {
const { message, show } = useToast();
const [form, setForm] = useState({ goal: "30天内行测稳定到70分以上", days_left: 30, daily_hours: 2 });
const [form, setForm] = useState({ goal: "30天内模考成绩稳定达到目标分", days_left: 30, daily_hours: 2 });
const [plan, setPlan] = useState("");
const submit = async (e) => {
@@ -1839,7 +1865,7 @@ export default function App() {
<GraduationCap size={28} strokeWidth={2} />
</span>
<div>
<h1 className="brand-title">公考助手</h1>
<h1 className="brand-title">学习伙伴</h1>
<p className="brand-sub">智能错题整理 · 科学分数管理</p>
</div>
</div>