728 lines
18 KiB
Vue
728 lines
18 KiB
Vue
<template>
|
||
<view class="container">
|
||
<!-- 自定义扫码界面 -->
|
||
<view class="scanner-container" v-if="isScanning">
|
||
<!-- 顶部操作栏 -->
|
||
<view class="scanner-header">
|
||
<view class="back-btn" @click="cancelScan">返回</view>
|
||
<view class="title">扫码</view>
|
||
<view class="empty"></view>
|
||
</view>
|
||
|
||
<!-- 相机预览区域 -->
|
||
<view class="camera-preview">
|
||
<!-- 遮挡浏览器自带的相册按钮 -->
|
||
<view class="browser-button-cover"></view>
|
||
|
||
<video
|
||
v-if="showVideo"
|
||
:src="videoSrc"
|
||
autoplay
|
||
playsinline
|
||
muted
|
||
class="preview-video"
|
||
@error="handleVideoError"
|
||
></video>
|
||
|
||
<!-- 扫描框 -->
|
||
<view class="scan-frame">
|
||
<!-- 扫描线动画 -->
|
||
<view class="scan-line" :style="{ top: scanLinePosition + 'px' }"></view>
|
||
|
||
<!-- 扫描框边角 -->
|
||
<view class="corner top-left"></view>
|
||
<view class="corner top-right"></view>
|
||
<view class="corner bottom-left"></view>
|
||
<view class="corner bottom-right"></view>
|
||
</view>
|
||
|
||
<!-- 扫描提示文字 -->
|
||
<view class="scan-tip">{{cameraStatusText}}</view>
|
||
|
||
<!-- 浏览器不支持提示 -->
|
||
<view v-if="showBrowserNotSupported" class="browser-not-supported">
|
||
<view class="error-title">浏览器不支持</view>
|
||
<view class="error-desc">您的浏览器不支持直接访问摄像头</view>
|
||
<view class="suggestions">
|
||
<view>建议:</view>
|
||
<view>1. 使用Chrome、Firefox等现代浏览器</view>
|
||
<view>2. 确保网站使用HTTPS协议</view>
|
||
<view>3. 检查浏览器摄像头权限设置</view>
|
||
</view>
|
||
<button class="retry-btn" @click="retryCamera">重试</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 扫码结果展示 -->
|
||
<view v-if="qrCodeRes" class="result-container">
|
||
<view>扫码成功</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
let Qrcode = require('../../../../utils/reqrcode.js')
|
||
import {
|
||
getInspectionManage
|
||
} from '@/api/inspection/inspectionManage.js'
|
||
|
||
// 二维码优化处理器
|
||
const QROptimizer = {
|
||
// 图像预处理
|
||
preprocessImage: function(base64) {
|
||
return new Promise((resolve) => {
|
||
const img = new Image()
|
||
img.src = base64
|
||
img.onload = () => {
|
||
const canvas = document.createElement('canvas')
|
||
const ctx = canvas.getContext('2d')
|
||
canvas.width = img.width
|
||
canvas.height = img.height
|
||
|
||
// 增强对比度和亮度
|
||
ctx.filter = 'contrast(1.3) brightness(1.1)'
|
||
ctx.drawImage(img, 0, 0)
|
||
|
||
// 转换为灰度并二值化
|
||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||
this.binarize(imageData)
|
||
ctx.putImageData(imageData, 0, 0)
|
||
|
||
resolve(canvas.toDataURL('image/jpeg'))
|
||
}
|
||
})
|
||
},
|
||
|
||
// 图像二值化处理
|
||
binarize: function(imageData) {
|
||
const data = imageData.data
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2]
|
||
const value = gray > 128 ? 255 : 0
|
||
data[i] = data[i+1] = data[i+2] = value
|
||
}
|
||
},
|
||
|
||
// 增强版解码
|
||
enhancedDecode: function(base64) {
|
||
return new Promise((resolve) => {
|
||
this.preprocessImage(base64).then(processed => {
|
||
Qrcode.qrcode.decode(processed)
|
||
Qrcode.qrcode.callback = (res) => {
|
||
if (res.indexOf('error') === -1) {
|
||
resolve(res)
|
||
} else {
|
||
// 尝试原始图像解码
|
||
Qrcode.qrcode.decode(base64)
|
||
Qrcode.qrcode.callback = resolve
|
||
}
|
||
}
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
qrCodeRes: '',
|
||
videoSrc: '',
|
||
isScanning: false,
|
||
stream: null,
|
||
scanLinePosition: 0,
|
||
scanAnimation: null,
|
||
isCameraReady: false,
|
||
cameraStatusText: '将二维码/条形码放入框内,即可自动扫描',
|
||
permissionDenied: false,
|
||
showVideo: true,
|
||
showBrowserNotSupported: false,
|
||
scanInterval: null,
|
||
lastScanTime: 0,
|
||
scanFrameRect: null
|
||
}
|
||
},
|
||
onShow() {
|
||
this.scanCode();
|
||
},
|
||
mounted() {
|
||
this.initScanAnimation();
|
||
},
|
||
beforeDestroy() {
|
||
if (this.scanAnimation) {
|
||
cancelAnimationFrame(this.scanAnimation);
|
||
}
|
||
this.stopCamera();
|
||
this.stopAutoScan();
|
||
},
|
||
methods: {
|
||
scanCode() {
|
||
// #ifdef APP-PLUS
|
||
this.scanCodeAPP()
|
||
// #endif
|
||
|
||
// #ifdef H5
|
||
this.startCustomScanner()
|
||
// #endif
|
||
},
|
||
|
||
initScanAnimation() {
|
||
const scanFrameHeight = 210;
|
||
const speed = 2;
|
||
|
||
const animate = () => {
|
||
if (this.scanLinePosition >= scanFrameHeight) {
|
||
this.scanLinePosition = 0;
|
||
} else {
|
||
this.scanLinePosition += speed;
|
||
}
|
||
this.scanAnimation = requestAnimationFrame(animate);
|
||
};
|
||
|
||
animate();
|
||
},
|
||
|
||
startCustomScanner() {
|
||
this.isScanning = true;
|
||
this.qrCodeRes = '';
|
||
this.isCameraReady = false;
|
||
this.cameraStatusText = '正在请求摄像头权限...';
|
||
this.showBrowserNotSupported = false;
|
||
this.showVideo = true;
|
||
|
||
if (!this.checkBrowserSupport()) {
|
||
this.handleBrowserNotSupported();
|
||
return;
|
||
}
|
||
|
||
this.checkCameraPermission();
|
||
},
|
||
|
||
checkBrowserSupport() {
|
||
return !!(
|
||
navigator.mediaDevices &&
|
||
navigator.mediaDevices.getUserMedia &&
|
||
window.URL &&
|
||
window.URL.createObjectURL
|
||
);
|
||
},
|
||
|
||
handleBrowserNotSupported() {
|
||
this.showVideo = false;
|
||
this.showBrowserNotSupported = true;
|
||
this.cameraStatusText = '浏览器不支持摄像头访问';
|
||
this.isCameraReady = false;
|
||
},
|
||
|
||
checkCameraPermission() {
|
||
if (navigator.permissions && navigator.permissions.query) {
|
||
navigator.permissions.query({ name: 'camera' })
|
||
.then(permissionStatus => {
|
||
if (permissionStatus.state === 'denied') {
|
||
this.handleCameraError('摄像头权限已被拒绝,请在浏览器设置中启用');
|
||
this.permissionDenied = true;
|
||
return;
|
||
}
|
||
this.getUserMediaWithFallback();
|
||
})
|
||
.catch(() => {
|
||
this.getUserMediaWithFallback();
|
||
});
|
||
} else {
|
||
this.getUserMediaWithFallback();
|
||
}
|
||
},
|
||
|
||
getUserMediaWithFallback() {
|
||
const constraints = {
|
||
video: {
|
||
facingMode: { ideal: 'environment' },
|
||
width: { ideal: window.innerWidth },
|
||
height: { ideal: window.innerHeight }
|
||
}
|
||
};
|
||
|
||
navigator.mediaDevices.getUserMedia(constraints)
|
||
.then(stream => {
|
||
this.handleStreamSuccess(stream);
|
||
this.$nextTick(() => {
|
||
const scanFrame = document.querySelector('.scan-frame');
|
||
if (scanFrame) {
|
||
this.scanFrameRect = scanFrame.getBoundingClientRect();
|
||
}
|
||
this.startAutoScan();
|
||
});
|
||
})
|
||
.catch(err => {
|
||
console.warn('后置摄像头访问失败,尝试前置摄像头:', err);
|
||
|
||
const frontConstraints = {
|
||
video: {
|
||
facingMode: 'user',
|
||
width: { ideal: window.innerWidth },
|
||
height: { ideal: window.innerHeight }
|
||
}
|
||
};
|
||
|
||
navigator.mediaDevices.getUserMedia(frontConstraints)
|
||
.then(stream => {
|
||
this.handleStreamSuccess(stream);
|
||
this.$nextTick(() => {
|
||
const scanFrame = document.querySelector('.scan-frame');
|
||
if (scanFrame) {
|
||
this.scanFrameRect = scanFrame.getBoundingClientRect();
|
||
}
|
||
this.startAutoScan();
|
||
});
|
||
})
|
||
.catch(err2 => {
|
||
console.error('所有摄像头访问失败:', err2);
|
||
this.handleCameraError('无法访问摄像头,请检查设备是否有摄像头并授予权限');
|
||
});
|
||
});
|
||
},
|
||
|
||
handleStreamSuccess(stream) {
|
||
this.stream = stream;
|
||
this.videoSrc = URL.createObjectURL(stream);
|
||
this.isCameraReady = true;
|
||
|
||
if (this.stream.getVideoTracks()[0].label.toLowerCase().includes('front')) {
|
||
this.cameraStatusText = '正在使用前置摄像头,建议使用后置摄像头扫码';
|
||
} else {
|
||
this.cameraStatusText = '将二维码放入框内,自动扫描';
|
||
}
|
||
},
|
||
|
||
startAutoScan() {
|
||
this.stopAutoScan();
|
||
|
||
const settings = this.getVideoStreamSettings();
|
||
const interval = settings?.height > 720 ? 800 : 500;
|
||
|
||
this.scanInterval = setInterval(() => {
|
||
const now = Date.now();
|
||
if (now - this.lastScanTime > interval) {
|
||
this.captureAndDecode();
|
||
this.lastScanTime = now;
|
||
}
|
||
}, interval);
|
||
},
|
||
|
||
getVideoStreamSettings() {
|
||
if (this.stream) {
|
||
const track = this.stream.getVideoTracks()[0];
|
||
return track.getSettings();
|
||
}
|
||
return null;
|
||
},
|
||
|
||
stopAutoScan() {
|
||
if (this.scanInterval) {
|
||
clearInterval(this.scanInterval);
|
||
this.scanInterval = null;
|
||
}
|
||
},
|
||
|
||
async captureAndDecode() {
|
||
if (!this.isCameraReady || !this.scanFrameRect) return;
|
||
|
||
const canvas = document.createElement('canvas');
|
||
const video = document.querySelector('.preview-video');
|
||
|
||
if (!video || video.readyState !== 4) return;
|
||
|
||
const frameWidth = this.scanFrameRect.width;
|
||
const frameHeight = this.scanFrameRect.height;
|
||
canvas.width = frameWidth;
|
||
canvas.height = frameHeight;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
ctx.drawImage(
|
||
video,
|
||
this.scanFrameRect.left, this.scanFrameRect.top, frameWidth, frameHeight,
|
||
0, 0, frameWidth, frameHeight
|
||
);
|
||
|
||
const base64 = canvas.toDataURL('image/jpeg');
|
||
|
||
try {
|
||
const codeRes = await QROptimizer.enhancedDecode(base64);
|
||
|
||
if (codeRes.indexOf('error') === -1) {
|
||
this.stopAutoScan();
|
||
this.stopCamera();
|
||
this.isScanning = false;
|
||
|
||
let r = this.decodeStr(codeRes);
|
||
this.qrCodeRes = r;
|
||
this.handleScanResult(r);
|
||
} else {
|
||
const qrSize = await this.estimateQRCodeSize(base64);
|
||
if (qrSize < 0.3) {
|
||
this.cameraStatusText = '二维码太小,请靠近些';
|
||
} else if (qrSize > 0.9) {
|
||
this.cameraStatusText = '二维码太大,请离远些';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log("解析失败", error);
|
||
}
|
||
},
|
||
|
||
estimateQRCodeSize(base64) {
|
||
return new Promise(resolve => {
|
||
const img = new Image();
|
||
img.src = base64;
|
||
img.onload = () => {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||
const data = imageData.data;
|
||
|
||
let activePixels = 0;
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
if (data[i] < 128) activePixels++;
|
||
}
|
||
|
||
const ratio = activePixels / (canvas.width * canvas.height);
|
||
resolve(Math.min(ratio * 10, 1));
|
||
};
|
||
});
|
||
},
|
||
|
||
handleCameraError(message) {
|
||
this.cameraStatusText = message;
|
||
this.isCameraReady = false;
|
||
uni.showToast({
|
||
title: message,
|
||
icon: 'none',
|
||
duration: 3000
|
||
});
|
||
this.redirectToWorkbench(); // 摄像头错误跳回工作台
|
||
},
|
||
|
||
handleVideoError(error) {
|
||
console.error('视频元素错误:', error);
|
||
this.handleCameraError('视频加载失败,请重试');
|
||
},
|
||
|
||
retryCamera() {
|
||
this.stopCamera();
|
||
if (this.permissionDenied) {
|
||
uni.showModal({
|
||
title: '权限不足',
|
||
content: '请在浏览器设置中允许摄像头权限,然后重试',
|
||
showCancel: false
|
||
});
|
||
} else {
|
||
this.startCustomScanner();
|
||
}
|
||
},
|
||
|
||
stopCamera() {
|
||
this.stopAutoScan();
|
||
if (this.stream) {
|
||
this.stream.getTracks().forEach(track => {
|
||
track.stop();
|
||
});
|
||
this.stream = null;
|
||
}
|
||
this.videoSrc = '';
|
||
this.isCameraReady = false;
|
||
this.scanFrameRect = null;
|
||
},
|
||
|
||
cancelScan() {
|
||
this.isScanning = false;
|
||
this.stopCamera();
|
||
this.redirectToWorkbench(); // 取消扫码跳回工作台
|
||
},
|
||
|
||
scanCodeAPP() {
|
||
uni.scanCode({
|
||
scanType: ['qrCode'],
|
||
onlyFromCamera: true,
|
||
success: (res) => {
|
||
this.qrCodeRes = res.result
|
||
this.handleScanResult(res.result);
|
||
},
|
||
fail: (err) => {
|
||
console.log("扫码失败", err);
|
||
uni.showToast({
|
||
title: '扫码失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
this.redirectToWorkbench(); // APP扫码失败跳回工作台
|
||
}
|
||
})
|
||
},
|
||
|
||
handleScanResult(result) {
|
||
getInspectionManage(result)
|
||
.then(res => {
|
||
if (res.data.inspectionStatus === "1") {
|
||
let inspectionPoint = res.data.inspectionPoint;
|
||
let inspectionRequirements = res.data.inspectionRequirements;
|
||
let inspectionPointId = res.data.id
|
||
uni.reLaunch({
|
||
url: `/pages/work/inspection/scanSign/index?inspectionPoint=${inspectionPoint}&inspectionRequirements=${inspectionRequirements}&inspectionPointId=${inspectionPointId}`
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: '验证码已失效!',
|
||
icon: 'none'
|
||
});
|
||
this.redirectToWorkbench(); // 验证码失效跳回工作台
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.log("请求错误", err);
|
||
uni.showToast({
|
||
title: '服务器错误',
|
||
icon: 'none'
|
||
});
|
||
this.redirectToWorkbench(); // 请求错误跳回工作台
|
||
});
|
||
},
|
||
|
||
// 新增方法:跳转回工作台
|
||
redirectToWorkbench() {
|
||
setTimeout(() => {
|
||
uni.reLaunch({
|
||
url: '/pages/work/index'
|
||
});
|
||
}, 1500); // 1.5秒后跳转,让用户看到提示信息
|
||
},
|
||
|
||
decodeStr(str) {
|
||
try {
|
||
return decodeURIComponent(escape(str));
|
||
} catch (e) {
|
||
console.warn("解码失败,返回原字符串:", str);
|
||
return str;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.container {
|
||
padding: 0;
|
||
margin: 0;
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
overflow: hidden;
|
||
background-color: #000;
|
||
}
|
||
|
||
.scanner-container {
|
||
width: 100%;
|
||
height: 100vh;
|
||
position: relative;
|
||
background-color: #000;
|
||
}
|
||
|
||
.scanner-header {
|
||
height: 45px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 15px;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
color: #fff;
|
||
}
|
||
|
||
.back-btn {
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.title {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.empty {
|
||
width: 40px;
|
||
}
|
||
|
||
.camera-preview {
|
||
width: 100%;
|
||
height: calc(100vh - 45px);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.browser-button-cover {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
width: 45px;
|
||
height: 45px;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 999;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.preview-video {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
background-color: #000;
|
||
position: relative;
|
||
z-index: 0;
|
||
}
|
||
|
||
.camera-preview::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1;
|
||
clip-path: polygon(
|
||
0% 0%,
|
||
0% 100%,
|
||
calc(50% - 140px) 100%,
|
||
calc(50% - 140px) calc(50% - 105px),
|
||
calc(50% + 140px) calc(50% - 105px),
|
||
calc(50% + 140px) calc(50% + 105px),
|
||
calc(50% - 140px) calc(50% + 105px),
|
||
calc(50% - 140px) 100%,
|
||
100% 100%,
|
||
100% 0%
|
||
);
|
||
}
|
||
|
||
.scan-frame {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 280px;
|
||
height: 210px;
|
||
transform: translate(-50%, -50%);
|
||
border: 1px solid rgba(0, 255, 0, 0.5);
|
||
background-color: rgba(0, 0, 0, 0.3);
|
||
z-index: 10;
|
||
}
|
||
|
||
.scan-line {
|
||
position: absolute;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 2px;
|
||
background-color: #0f0;
|
||
transition: top 0.05s linear;
|
||
}
|
||
|
||
.corner {
|
||
position: absolute;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-color: #0f0;
|
||
border-style: solid;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.top-left {
|
||
top: -1px;
|
||
left: -1px;
|
||
border-width: 3px 0 0 3px;
|
||
}
|
||
|
||
.top-right {
|
||
top: -1px;
|
||
right: -1px;
|
||
border-width: 3px 3px 0 0;
|
||
}
|
||
|
||
.bottom-left {
|
||
bottom: -1px;
|
||
left: -1px;
|
||
border-width: 0 0 3px 3px;
|
||
}
|
||
|
||
.bottom-right {
|
||
bottom: -1px;
|
||
right: -1px;
|
||
border-width: 0 3px 3px 0;
|
||
}
|
||
|
||
.scan-tip {
|
||
position: absolute;
|
||
top: calc(50% + 120px);
|
||
left: 0;
|
||
width: 100%;
|
||
text-align: center;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
padding: 0 20px;
|
||
box-sizing: border-box;
|
||
z-index: 10;
|
||
}
|
||
|
||
.browser-not-supported {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
color: #fff;
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
text-align: center;
|
||
z-index: 20;
|
||
}
|
||
|
||
.error-title {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
margin-bottom: 15px;
|
||
color: #ff5252;
|
||
}
|
||
|
||
.error-desc {
|
||
font-size: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.suggestions {
|
||
text-align: left;
|
||
margin-bottom: 20px;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.retry-btn {
|
||
margin-top: 20px;
|
||
padding: 8px 20px;
|
||
background-color: #2196F3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.result-container {
|
||
padding: 15px;
|
||
background-color: #fff;
|
||
min-height: 100vh;
|
||
}
|
||
</style> |