This commit is contained in:
2025-08-07 13:01:29 +08:00
parent 8fcffec73d
commit 4c918f9c3b
7 changed files with 958 additions and 285 deletions

View File

@@ -1,145 +1,728 @@
<template>
<view class="container">
<button type="default" @click="scanCode">扫码</button>
<!-- <view>
扫码结果{{qrCodeRes}}
</view>
<image :src="qc"></image> -->
</view>
<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'
export default {
data() {
return {
qrCodeRes: '',
qc: ''
}
},
methods: {
// 扫码
scanCode() {
// #ifdef APP-PLUS
this.scanCodeAPP()
// #endif
let Qrcode = require('../../../../utils/reqrcode.js')
import {
getInspectionManage
} from '@/api/inspection/inspectionManage.js'
// #ifdef H5
this.scanCodeH5()
// #endif
},
// APP直接调用 uni.scanCode 接口
scanCodeAPP() {
uni.scanCode({
scanType: ['qrCode'],
success: (res) => {
this.qrCodeRes = res.result
}
})
},
// H5通过拉起相机拍照来识别二维码
scanCodeH5() {
let that = this;
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: imgRes => {
that.qc = imgRes.tempFiles[0].path; // 预览图片
const tempFile = imgRes.tempFiles[0];
const reader = new FileReader();
// 二维码优化处理器
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
}
}
})
})
}
}
reader.onload = function(event) {
const base64 = event.target.result;
try {
Qrcode.qrcode.decode(base64);
Qrcode.qrcode.callback = (codeRes) => {
if (codeRes.indexOf('error') >= 0) {
that.qrCodeRes = '不合法二维码:' + codeRes;
uni.showToast({
title: '二维码识别失败!',
icon: 'none'
});
} else {
let r = that.decodeStr(codeRes);
that.qrCodeRes = r;
getInspectionManage(that.qrCodeRes)
.then(res => {
if (res.data.inspectionStatus === "1") {
console.log(res)
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'
});
uni.reLaunch({
url: '/pages/work/index'
});
}
})
.catch(err => {
console.log("请求错误", err);
uni.showToast({
title: '服务器错误',
icon: 'none'
});
});
}
};
} catch (error) {
console.log("解析失败", error);
uni.showToast({
title: '二维码解析失败',
icon: 'none'
});
}
};
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
reader.readAsDataURL(tempFile);
},
fail: (err) => {
console.log("图片选择失败", err);
uni.showToast({
title: '拍照失败,请重试',
icon: 'none'
});
}
});
},
// 获取文件地址函数
getObjectURL(file) {
if (window.URL && window.URL.createObjectURL) {
return window.URL.createObjectURL(file);
} else {
console.warn("当前浏览器不支持 createObjectURL 方法");
return null;
}
},
// 解码,输出:中文
decodeStr(str) {
try {
return decodeURIComponent(escape(str));
} catch (e) {
console.warn("解码失败,返回原字符串:", str);
return str;
}
},
}
}
// #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: 10px;
}
.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>