初始化 EVUS 项目并统一首页及业务页面样式
This commit is contained in:
756
public/ocr.html
Normal file
756
public/ocr.html
Normal file
@@ -0,0 +1,756 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>H5 条形码扫描</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Arial, "Microsoft YaHei", sans-serif;
|
||||
background: #f4f6fb;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 430px;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
padding: 26px 22px;
|
||||
text-align: center;
|
||||
box-shadow: 0 12px 38px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
margin: 10px 0 22px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.camera-btn {
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: #1677ff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 12px 28px rgba(22, 119, 255, 0.35);
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.camera-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.camera-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.camera-btn svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
margin-top: 24px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result-box label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.result-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.result-input:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 0 14px;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: #f9fafb;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.scanner-layer {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
padding: env(safe-area-inset-top) 14px env(safe-area-inset-bottom);
|
||||
background: rgba(0, 0, 0, 0.86);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scanner-layer.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.scanner-panel {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
max-height: 96vh;
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.scanner-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.scanner-header strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
padding: 8px 12px;
|
||||
background: #374151;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: min(72vh, 520px);
|
||||
object-fit: cover;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.scan-frame {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 88%;
|
||||
height: 150px;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 2px solid #22c55e;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.28);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scan-frame::before,
|
||||
.scan-frame::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-top: 4px solid #22c55e;
|
||||
}
|
||||
|
||||
.scan-frame::before {
|
||||
left: -2px;
|
||||
border-left: 4px solid #22c55e;
|
||||
border-top-left-radius: 14px;
|
||||
}
|
||||
|
||||
.scan-frame::after {
|
||||
right: -2px;
|
||||
border-right: 4px solid #22c55e;
|
||||
border-top-right-radius: 14px;
|
||||
}
|
||||
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
left: 8%;
|
||||
right: 8%;
|
||||
top: 50%;
|
||||
height: 2px;
|
||||
background: #ef4444;
|
||||
transform: translateY(-50%);
|
||||
box-shadow: 0 0 12px rgba(239, 68, 68, 0.9);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.distance-tip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
width: calc(100% - 28px);
|
||||
max-width: 470px;
|
||||
transform: translateX(-50%);
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(17, 24, 39, 0.72);
|
||||
color: #f9fafb;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scanner-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 999px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.zoom-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 220px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
font-size: 13px;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.zoom-box input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status {
|
||||
min-height: 44px;
|
||||
padding: 12px 16px 16px;
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.status.ok {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 22px 18px;
|
||||
}
|
||||
|
||||
.scan-frame {
|
||||
width: 90%;
|
||||
height: 132px;
|
||||
}
|
||||
|
||||
video {
|
||||
height: 68vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="page">
|
||||
<section class="card">
|
||||
<h1 class="title">条形码扫描</h1>
|
||||
<p class="sub-title">点击相机图标,允许摄像头权限后,将一维条形码横向放入扫描框。</p>
|
||||
|
||||
<button id="openScannerBtn" class="camera-btn" type="button" aria-label="打开相机扫描条形码">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M9 3.5h6l1.45 2H20a2 2 0 0 1 2 2v10.8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7.5a2 2 0 0 1 2-2h3.55L9 3.5Zm3 5a4.7 4.7 0 1 0 0 9.4 4.7 4.7 0 0 0 0-9.4Zm0 2a2.7 2.7 0 1 1 0 5.4 2.7 2.7 0 0 1 0-5.4Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="result-box">
|
||||
<label for="barcodeInput">识别结果</label>
|
||||
<div class="result-row">
|
||||
<input id="barcodeInput" class="result-input" type="text" placeholder="扫码结果会显示在这里" autocomplete="off" />
|
||||
<button id="copyBtn" class="small-btn" type="button">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<strong>提示:</strong>条形码不要贴摄像头太近。建议距离 20-40cm,条形码完整进入框内,左右两边要留一点空白。
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section id="scannerLayer" class="scanner-layer" aria-hidden="true">
|
||||
<div class="scanner-panel" role="dialog" aria-modal="true" aria-label="条形码扫描窗口">
|
||||
<div class="scanner-header">
|
||||
<strong>扫描条形码</strong>
|
||||
<button id="closeScannerBtn" class="close-btn" type="button">关闭</button>
|
||||
</div>
|
||||
|
||||
<div class="video-wrap">
|
||||
<video
|
||||
id="previewVideo"
|
||||
muted
|
||||
autoplay
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
></video>
|
||||
|
||||
<div class="scan-frame"></div>
|
||||
<div class="scan-line"></div>
|
||||
<div class="distance-tip">不要贴太近;请拿远一点,让条形码清晰并完整横向进入框内</div>
|
||||
</div>
|
||||
|
||||
<div class="scanner-tools">
|
||||
<button id="torchBtn" class="tool-btn" type="button" hidden>打开闪光灯</button>
|
||||
|
||||
<label id="zoomBox" class="zoom-box" hidden>
|
||||
<span>变焦</span>
|
||||
<input id="zoomRange" type="range" min="1" max="1" step="0.1" value="1" />
|
||||
<span id="zoomValue">1.0x</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status">准备打开摄像头...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 条形码识别库:需要能访问外网 CDN;内网项目可把这个 JS 下载到本地再引用 -->
|
||||
<script src="https://unpkg.com/@zxing/browser@0.2.0"></script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const openScannerBtn = document.getElementById("openScannerBtn");
|
||||
const closeScannerBtn = document.getElementById("closeScannerBtn");
|
||||
const scannerLayer = document.getElementById("scannerLayer");
|
||||
const previewVideo = document.getElementById("previewVideo");
|
||||
const statusEl = document.getElementById("status");
|
||||
const barcodeInput = document.getElementById("barcodeInput");
|
||||
const copyBtn = document.getElementById("copyBtn");
|
||||
const torchBtn = document.getElementById("torchBtn");
|
||||
const zoomBox = document.getElementById("zoomBox");
|
||||
const zoomRange = document.getElementById("zoomRange");
|
||||
const zoomValue = document.getElementById("zoomValue");
|
||||
|
||||
let codeReader = null;
|
||||
let controls = null;
|
||||
let stream = null;
|
||||
let videoTrack = null;
|
||||
let scanning = false;
|
||||
let torchOn = false;
|
||||
|
||||
function setStatus(text, type) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = "status" + (type ? " " + type : "");
|
||||
}
|
||||
|
||||
function showScanner() {
|
||||
scannerLayer.classList.add("show");
|
||||
scannerLayer.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
function hideScanner() {
|
||||
scannerLayer.classList.remove("show");
|
||||
scannerLayer.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
function getReadableError(error) {
|
||||
if (!error) return "未知错误";
|
||||
|
||||
const name = error.name || "";
|
||||
|
||||
if (name === "NotAllowedError" || name === "PermissionDeniedError") {
|
||||
return "用户拒绝了摄像头权限,请在浏览器设置中允许摄像头。";
|
||||
}
|
||||
|
||||
if (name === "NotFoundError" || name === "DevicesNotFoundError") {
|
||||
return "没有找到可用摄像头。";
|
||||
}
|
||||
|
||||
if (name === "NotReadableError" || name === "TrackStartError") {
|
||||
return "摄像头被其他应用占用,关闭其他应用后再试。";
|
||||
}
|
||||
|
||||
if (name === "OverconstrainedError" || name === "ConstraintNotSatisfiedError") {
|
||||
return "当前摄像头不支持请求的分辨率或参数。";
|
||||
}
|
||||
|
||||
if (name === "SecurityError") {
|
||||
return "当前页面没有摄像头权限,请使用 HTTPS 页面。";
|
||||
}
|
||||
|
||||
return error.message || String(error);
|
||||
}
|
||||
|
||||
function createBarcodeReader() {
|
||||
if (!window.ZXingBrowser) {
|
||||
throw new Error("ZXingBrowser 加载失败,请检查网络或 CDN 地址。也可以把 JS 下载到本地引用。");
|
||||
}
|
||||
|
||||
// 一维条形码 Reader:EAN-13、EAN-8、UPC、Code128、Code39、ITF 等。
|
||||
if (ZXingBrowser.BrowserMultiFormatOneDReader) {
|
||||
return new ZXingBrowser.BrowserMultiFormatOneDReader();
|
||||
}
|
||||
|
||||
// 兜底:如果当前版本没有 OneDReader,就用 MultiFormatReader。
|
||||
if (ZXingBrowser.BrowserMultiFormatReader) {
|
||||
return new ZXingBrowser.BrowserMultiFormatReader();
|
||||
}
|
||||
|
||||
throw new Error("当前 ZXing 版本不支持浏览器条形码扫描 Reader。");
|
||||
}
|
||||
|
||||
async function openCameraStream() {
|
||||
const highQualityConstraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
facingMode: { ideal: "environment" },
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1080 },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
return await navigator.mediaDevices.getUserMedia(highQualityConstraints);
|
||||
} catch (error) {
|
||||
// 如果是用户拒绝权限,不要再弹第二次。
|
||||
if (
|
||||
error &&
|
||||
(error.name === "NotAllowedError" || error.name === "PermissionDeniedError" || error.name === "SecurityError")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 某些 iPhone / 浏览器对高分辨率约束比较敏感,失败后降级为默认摄像头。
|
||||
return await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function safeApplyConstraints(track, constraints) {
|
||||
if (!track || !track.applyConstraints) return false;
|
||||
|
||||
try {
|
||||
await track.applyConstraints(constraints);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("摄像头参数不支持:", constraints, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function enhanceCamera(track) {
|
||||
torchOn = false;
|
||||
torchBtn.hidden = true;
|
||||
zoomBox.hidden = true;
|
||||
|
||||
if (!track || !track.getCapabilities) {
|
||||
return;
|
||||
}
|
||||
|
||||
const capabilities = track.getCapabilities();
|
||||
console.log("摄像头能力:", capabilities);
|
||||
|
||||
// 自动/连续对焦:Android Chrome 支持较多,iPhone Safari 不一定支持。
|
||||
if (Array.isArray(capabilities.focusMode)) {
|
||||
if (capabilities.focusMode.includes("continuous")) {
|
||||
await safeApplyConstraints(track, { advanced: [{ focusMode: "continuous" }] });
|
||||
} else if (capabilities.focusMode.includes("single-shot")) {
|
||||
await safeApplyConstraints(track, { advanced: [{ focusMode: "single-shot" }] });
|
||||
}
|
||||
}
|
||||
|
||||
// 变焦:用户拿远一点,用 1.5x-2x 变焦,比贴近摄像头更容易清晰。
|
||||
if (capabilities.zoom) {
|
||||
const minZoom = Number(capabilities.zoom.min || 1);
|
||||
const maxZoom = Number(capabilities.zoom.max || minZoom);
|
||||
const stepZoom = Number(capabilities.zoom.step || 0.1);
|
||||
const targetZoom = Math.min(maxZoom, Math.max(minZoom, 1.6));
|
||||
|
||||
zoomRange.min = String(minZoom);
|
||||
zoomRange.max = String(maxZoom);
|
||||
zoomRange.step = String(stepZoom);
|
||||
zoomRange.value = String(targetZoom);
|
||||
zoomValue.textContent = targetZoom.toFixed(1) + "x";
|
||||
zoomBox.hidden = false;
|
||||
|
||||
if (targetZoom > minZoom) {
|
||||
await safeApplyConstraints(track, { advanced: [{ zoom: targetZoom }] });
|
||||
}
|
||||
}
|
||||
|
||||
// 闪光灯:很多 Android 支持,iPhone H5 不一定支持。
|
||||
if (capabilities.torch) {
|
||||
torchBtn.hidden = false;
|
||||
torchBtn.textContent = "打开闪光灯";
|
||||
}
|
||||
}
|
||||
|
||||
async function startScan() {
|
||||
if (scanning) return;
|
||||
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
alert("当前浏览器不支持摄像头调用,请使用 Chrome、Safari、Edge 等现代浏览器。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
alert("摄像头需要 HTTPS 或 localhost 环境。请把页面部署到 HTTPS 后再用手机访问。");
|
||||
return;
|
||||
}
|
||||
|
||||
showScanner();
|
||||
openScannerBtn.disabled = true;
|
||||
scanning = true;
|
||||
setStatus("正在打开摄像头,请允许浏览器使用摄像头...", "");
|
||||
|
||||
try {
|
||||
codeReader = createBarcodeReader();
|
||||
|
||||
stream = await openCameraStream();
|
||||
videoTrack = stream.getVideoTracks()[0] || null;
|
||||
|
||||
previewVideo.muted = true;
|
||||
previewVideo.autoplay = true;
|
||||
previewVideo.playsInline = true;
|
||||
previewVideo.setAttribute("playsinline", "true");
|
||||
previewVideo.setAttribute("webkit-playsinline", "true");
|
||||
previewVideo.srcObject = stream;
|
||||
|
||||
try {
|
||||
await previewVideo.play();
|
||||
} catch (playError) {
|
||||
console.warn("video.play() 失败,继续尝试解码:", playError);
|
||||
}
|
||||
|
||||
await enhanceCamera(videoTrack);
|
||||
|
||||
setStatus("扫描中:请距离条形码 20-40cm,横向放入框内,左右留出空白。", "ok");
|
||||
|
||||
controls = await codeReader.decodeFromStream(stream, previewVideo, function (result, error) {
|
||||
if (!scanning) return;
|
||||
|
||||
if (result) {
|
||||
const text = typeof result.getText === "function" ? result.getText() : String(result);
|
||||
onBarcodeFound(text);
|
||||
return;
|
||||
}
|
||||
|
||||
// 未识别到时 ZXing 会频繁抛 NotFoundException,这是正常现象,不要展示给用户。
|
||||
if (error && error.name && !["NotFoundException", "ChecksumException", "FormatException"].includes(error.name)) {
|
||||
console.debug("ZXing decode error:", error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
releaseCamera();
|
||||
scanning = false;
|
||||
openScannerBtn.disabled = false;
|
||||
setStatus("无法打开摄像头:" + getReadableError(error), "error");
|
||||
}
|
||||
}
|
||||
|
||||
function onBarcodeFound(text) {
|
||||
barcodeInput.value = text;
|
||||
setStatus("识别成功:" + text, "ok");
|
||||
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(120);
|
||||
}
|
||||
|
||||
// 这里写你的业务逻辑,例如提交到后端:
|
||||
// fetch("/api/barcode", {
|
||||
// method: "POST",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify({ barcode: text })
|
||||
// });
|
||||
|
||||
stopScan();
|
||||
}
|
||||
|
||||
function releaseCamera() {
|
||||
if (controls) {
|
||||
try {
|
||||
controls.stop();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
controls = null;
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
});
|
||||
stream = null;
|
||||
}
|
||||
|
||||
videoTrack = null;
|
||||
previewVideo.pause();
|
||||
previewVideo.srcObject = null;
|
||||
|
||||
torchOn = false;
|
||||
torchBtn.hidden = true;
|
||||
zoomBox.hidden = true;
|
||||
}
|
||||
|
||||
function stopScan() {
|
||||
scanning = false;
|
||||
openScannerBtn.disabled = false;
|
||||
releaseCamera();
|
||||
hideScanner();
|
||||
}
|
||||
|
||||
async function toggleTorch() {
|
||||
if (!videoTrack) return;
|
||||
|
||||
torchOn = !torchOn;
|
||||
const ok = await safeApplyConstraints(videoTrack, { advanced: [{ torch: torchOn }] });
|
||||
|
||||
if (!ok) {
|
||||
torchOn = !torchOn;
|
||||
setStatus("当前浏览器不支持控制闪光灯。", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
torchBtn.textContent = torchOn ? "关闭闪光灯" : "打开闪光灯";
|
||||
}
|
||||
|
||||
async function changeZoom() {
|
||||
if (!videoTrack) return;
|
||||
|
||||
const value = Number(zoomRange.value);
|
||||
zoomValue.textContent = value.toFixed(1) + "x";
|
||||
await safeApplyConstraints(videoTrack, { advanced: [{ zoom: value }] });
|
||||
}
|
||||
|
||||
async function copyResult() {
|
||||
const text = barcodeInput.value.trim();
|
||||
if (!text) {
|
||||
alert("还没有识别结果");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
alert("已复制:" + text);
|
||||
} catch (error) {
|
||||
barcodeInput.select();
|
||||
document.execCommand("copy");
|
||||
alert("已复制:" + text);
|
||||
}
|
||||
}
|
||||
|
||||
openScannerBtn.addEventListener("click", startScan);
|
||||
closeScannerBtn.addEventListener("click", stopScan);
|
||||
torchBtn.addEventListener("click", toggleTorch);
|
||||
zoomRange.addEventListener("input", changeZoom);
|
||||
copyBtn.addEventListener("click", copyResult);
|
||||
|
||||
// 点击黑色背景关闭扫描层。
|
||||
scannerLayer.addEventListener("click", function (event) {
|
||||
if (event.target === scannerLayer) {
|
||||
stopScan();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面切走或关闭时释放摄像头。
|
||||
window.addEventListener("pagehide", stopScan);
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.hidden && scanning) {
|
||||
stopScan();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user