Files
pasd_app/pages/work/inspection/scanSign/scanSign2.vue
2025-08-07 13:01:29 +08:00

728 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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. 使用ChromeFirefox等现代浏览器</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>