优化连拍功能
This commit is contained in:
File diff suppressed because one or more lines are too long
204
frontend/dist/assets/index-Dw7fpRnj.js
vendored
204
frontend/dist/assets/index-Dw7fpRnj.js
vendored
File diff suppressed because one or more lines are too long
204
frontend/dist/assets/index-YakFBlkm.js
vendored
Normal file
204
frontend/dist/assets/index-YakFBlkm.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-Dw7fpRnj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CoOKrmS9.css">
|
||||
<script type="module" crossorigin src="/assets/index-YakFBlkm.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B2Duzhn9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1948,10 +1948,16 @@ export default function App() {
|
||||
const [mainTab, setMainTab] = useState("mistake");
|
||||
const [extraTab, setExtraTab] = useState("resource");
|
||||
const [showQuickCamera, setShowQuickCamera] = useState(false);
|
||||
const [showBurstCamera, setShowBurstCamera] = useState(false);
|
||||
const [quickCameraFabCompact, setQuickCameraFabCompact] = useState(() => readQuickCameraFabCompact());
|
||||
const [quickCaptureMode, setQuickCaptureMode] = useState("single");
|
||||
const [quickCaptureTask, setQuickCaptureTask] = useState(null);
|
||||
const [burstShots, setBurstShots] = useState([]);
|
||||
const [burstCameraError, setBurstCameraError] = useState("");
|
||||
const [burstCameraReady, setBurstCameraReady] = useState(false);
|
||||
const quickCaptureInputRef = useRef(null);
|
||||
const burstVideoRef = useRef(null);
|
||||
const burstStreamRef = useRef(null);
|
||||
|
||||
const triggerQuickCapture = () => {
|
||||
quickCaptureInputRef.current?.click();
|
||||
@@ -1977,6 +1983,101 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const pushQuickCaptureTaskWithFiles = (files, mode = quickCaptureMode) => {
|
||||
if (!files?.length) return;
|
||||
setMainTab("mistake");
|
||||
setQuickCaptureTask({
|
||||
id: Date.now(),
|
||||
mode,
|
||||
files
|
||||
});
|
||||
};
|
||||
|
||||
const stopBurstStream = () => {
|
||||
if (burstStreamRef.current) {
|
||||
burstStreamRef.current.getTracks().forEach((t) => t.stop());
|
||||
burstStreamRef.current = null;
|
||||
}
|
||||
if (burstVideoRef.current) {
|
||||
burstVideoRef.current.srcObject = null;
|
||||
}
|
||||
setBurstCameraReady(false);
|
||||
};
|
||||
|
||||
const startBurstCamera = async () => {
|
||||
setBurstCameraError("");
|
||||
setBurstShots([]);
|
||||
setShowQuickCamera(false);
|
||||
setShowBurstCamera(true);
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: { ideal: "environment" },
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1080 }
|
||||
},
|
||||
audio: false
|
||||
});
|
||||
burstStreamRef.current = stream;
|
||||
if (burstVideoRef.current) {
|
||||
burstVideoRef.current.srcObject = stream;
|
||||
await burstVideoRef.current.play().catch(() => {});
|
||||
}
|
||||
setBurstCameraReady(true);
|
||||
} catch {
|
||||
setBurstCameraError("无法开启相机,请检查浏览器权限。你也可以改用系统相机上传。");
|
||||
setBurstCameraReady(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeBurstCamera = () => {
|
||||
stopBurstStream();
|
||||
setShowBurstCamera(false);
|
||||
};
|
||||
|
||||
const captureBurstFrame = async () => {
|
||||
const video = burstVideoRef.current;
|
||||
if (!video || !video.videoWidth || !video.videoHeight) return;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
const blob = await new Promise((resolve) =>
|
||||
canvas.toBlob((b) => resolve(b), "image/jpeg", 0.9)
|
||||
);
|
||||
if (!blob) return;
|
||||
const file = new File([blob], `burst-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`, { type: "image/jpeg" });
|
||||
setBurstShots((prev) => [...prev, file]);
|
||||
};
|
||||
|
||||
const finishBurstCapture = () => {
|
||||
if (!burstShots.length) return;
|
||||
pushQuickCaptureTaskWithFiles(burstShots, "burst");
|
||||
closeBurstCamera();
|
||||
};
|
||||
|
||||
const startQuickCaptureFlow = () => {
|
||||
if (quickCaptureMode === "single") {
|
||||
triggerQuickCapture();
|
||||
return;
|
||||
}
|
||||
const mediaSupported = !!navigator?.mediaDevices?.getUserMedia;
|
||||
if (mediaSupported) {
|
||||
startBurstCamera();
|
||||
return;
|
||||
}
|
||||
// Fallback for old browsers/devices.
|
||||
triggerQuickCapture();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopBurstStream();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<header className="app-hero">
|
||||
@@ -2129,7 +2230,7 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="btn-row" style={{ marginTop: 14 }}>
|
||||
<button type="button" className="btn btn-primary" onClick={triggerQuickCapture}>
|
||||
<button type="button" className="btn btn-primary" onClick={startQuickCaptureFlow}>
|
||||
开始拍照
|
||||
</button>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => setShowQuickCamera(false)}>
|
||||
@@ -2138,6 +2239,35 @@ export default function App() {
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showBurstCamera && (
|
||||
<div className="burst-camera-overlay" role="dialog" aria-modal="true">
|
||||
<div className="burst-camera-panel">
|
||||
<div className="burst-camera-head">
|
||||
<strong>连拍录题</strong>
|
||||
<span className="text-muted small">已拍 {burstShots.length} 张</span>
|
||||
</div>
|
||||
<div className="burst-camera-preview">
|
||||
<video ref={burstVideoRef} playsInline muted autoPlay />
|
||||
{!burstCameraReady && (
|
||||
<div className="burst-camera-mask">{burstCameraError || "正在启动相机..."}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="burst-camera-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={closeBurstCamera}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary burst-shutter-btn" onClick={captureBurstFrame} disabled={!burstCameraReady}>
|
||||
拍一张
|
||||
</button>
|
||||
<button type="button" className="btn btn-success" onClick={finishBurstCapture} disabled={burstShots.length === 0}>
|
||||
完成并导入
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-muted small">连拍模式下不会退出相机,连续点击“拍一张”即可。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -959,6 +959,72 @@ textarea {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.burst-camera-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 220;
|
||||
background: rgba(2, 6, 23, 0.82);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.burst-camera-panel {
|
||||
width: min(820px, 100%);
|
||||
background: #0b1222;
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.burst-camera-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.burst-camera-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 4;
|
||||
background: #000;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.burst-camera-preview video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.burst-camera-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
color: #cbd5e1;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.burst-camera-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.burst-shutter-btn {
|
||||
min-width: 112px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.quick-camera-fab-shell {
|
||||
right: 14px;
|
||||
@@ -988,6 +1054,27 @@ textarea {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
}
|
||||
|
||||
.burst-camera-overlay {
|
||||
padding: 0;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.burst-camera-panel {
|
||||
width: 100%;
|
||||
border-radius: 14px 14px 0 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.burst-camera-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.burst-camera-actions .btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
|
||||
Reference in New Issue
Block a user