756 lines
24 KiB
HTML
756 lines
24 KiB
HTML
<!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> |