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

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
"* text=auto eol=lf"

View File

@@ -3,7 +3,7 @@ module.exports = {
// baseUrl: 'http://172.16.129.101:8080/dev-api/',
//baseUrl: 'http://pasd.gxsdxy.cn/prod-api/',
// baseUrl: 'http://172.16.129.101:8080',//172.16.129.101
//baseUrl: 'http://localhost:8080',
baseUrl: 'http://192.168.100.106:8080',
// 应用信息
appInfo: {
// 应用名称

View File

@@ -1,23 +1,31 @@
{
"name" : "平安水电移动端",
"appid" : "__UNI__3CA192A",
"description" : "",
"versionName" : "1.1.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueCompiler" : "uni-app",
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
"name": "平安水电移动端",
"appid": "__UNI__3CA192A",
"description": "",
"versionName": "1.1.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueCompiler": "uni-app",
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"modules": {},
"permission": {
"Camera": {
"description": "需要访问相机以实现扫码功能"
},
"Flashlight": {
"description": "需要访问闪光灯以支持扫码补光"
}
},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
@@ -32,38 +40,54 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
]
},
"ios" : {},
"sdkConfigs" : {}
"ios": {
"permissions": {
"Camera": {
"description": "需要访问相机以实现扫码功能"
},
"PhotoLibraryAdd": {
"description": "需要访问相册以保存图片"
}
}
},
"sdkConfigs": {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wx6caa681653417faa",
"setting" : {
"urlCheck" : false,
"es6" : false,
"minified" : true,
"postcss" : true
"quickapp": {},
"mp-weixin": {
"appid": "wx6caa681653417faa",
"setting": {
"urlCheck": false,
"es6": false,
"minified": true,
"postcss": true
},
"optimization" : {
"subPackages" : true
"optimization": {
"subPackages": true
},
"usingComponents" : true
"permission": {
"scope.camera": {
"desc": "需要访问相机以实现扫码功能"
}
},
"usingComponents": true
},
"vueVersion" : "2",
"h5" : {
"template" : "static/index.html",
"devServer" : {
"port" : 9090,
"https" : false
"vueVersion": "2",
"h5": {
"template": "static/index.html",
"devServer": {
"port": 9090,
"https": false
},
"title" : "RuoYi-App",
"router" : {
"mode" : "hash",
"base" : "./"
"title": "RuoYi-App",
"router": {
"mode": "hash",
"base": "./"
}
}
}
}

View File

@@ -14,6 +14,8 @@
]
},
"dependencies": {
"echarts": "^5.5.1"
"@zxing/library": "^0.21.3",
"echarts": "^5.5.1",
"jsqr": "^1.4.0"
}
}

View File

@@ -20,7 +20,7 @@
</uni-forms>
<view class="button-group">
<button type="primary" size="mini" @click="cancel()">取消</button>
<button type="primary" size="mini" @click="submit()">提交</button>
<button type="primary" size="mini" @click="submit()">提交1</button>
</view>
</view>
</view>

View File

@@ -16,7 +16,7 @@
</view>
</uni-forms-item>
<!-- 备注 -->
<uni-forms-item label="备注" name="remark">
<uni-forms-item label="备注1" name="remark">
<uni-easyinput type="textarea" v-model="form.remark" placeholder="请输入备注"></uni-easyinput>
</uni-forms-item>
</uni-forms>
@@ -47,149 +47,204 @@
addWatermarkToImage
} from "@/utils/watermark.js"
export default {
// vue
data() {
return {
form: {
inspectionType: 0, //默认常规巡检
inspectorId: this.$store.state.user.nickName, //巡检人
// 巡检点
inspectionType: 0,
inspectorId: this.$store.state.user.nickName,
inspectionPoint: "",
// 巡检要求
inspectionRequirements: "",
inspectionImg: "",
ImgUrl: []
ImgUrl: [],
remark: ""
},
img: [],
sourceType: ['camera'],
//存图片文件
imgFiles: "",
// 消息提示框
msgType: '',
messageText: '',
isUploading: false, // 上传中标志
isUploading: false,
isMobileBrowser: false // 改为更通用的移动浏览器检测标志
}
},
methods: {
// 提交
submit() {
if (this.isUploading) {
this.msgType = 'warning'
this.messageText = '图片正在上传中,请稍等'
this.$refs.alertDialog.open()
this.showMessage('warning', '图片正在上传中,请稍等');
return;
}
if (!Array.isArray(this.form.ImgUrl) || this.form.ImgUrl.length === 0) {
this.msgType = 'error'
this.messageText = `请选择要上传的图片`
this.$refs.alertDialog.open()
this.showMessage('error', '请选择要上传的图片');
return;
}
this.form.inspectionImg = this.joinList()
this.form.inspectionImg = this.joinList();
addRecord(this.form).then(res => {
if (res.code === 200) {
// console.log("sdfsadfsdfsdfsdfsdf");
this.msgType = 'success'
this.messageText = `打卡成功`
this.$refs.alertDialog.open()
this.showMessage('success', '打卡成功');
} else {
this.msgType = 'error'
this.messageText = `打卡失败`
this.$refs.alertDialog.open()
this.showMessage('error', '打卡失败');
}
})
}).catch(err => {
this.showMessage('error', `提交失败: ${err.message}`);
});
},
// 图片删除
deleteImg(e) {
//const index = this.img.findIndex(f => f.uuid === e.tempFile.uuid); // 假设文件通过 url 唯一标识
const index = this.img.findIndex(f => f.path === e.tempFile.path); // 假设文件通过 url 唯一标识
const index = this.img.findIndex(f => f.path === e.tempFile.path);
if (index !== -1) {
this.form.ImgUrl.splice(index, 1)
this.img.splice(index, 1)
this.form.ImgUrl.splice(index, 1);
this.img.splice(index, 1);
}
},
// 上传图片
upload(e) {
console.log(e);
// this.img.push(e.tempFiles[0])
// e.tempFilePaths.forEach(item => {
// uploadImg({
// filePath: item
// }).then(res => {
// this.form.ImgUrl.push(res.fileName)
// })
// })
this.handleImageUpload(e.tempFiles[0]);
if (e.tempFiles && e.tempFiles.length > 0) {
this.handleImageUpload(e.tempFiles[0]);
}
},
// 取消
cancel() {
uni.reLaunch({
url: '/pages/work/index'
})
uni.showToast({
title: '打卡失败',
icon: 'error', // 成功图标
duration: 1000 // 持续时间为2000ms
});
},
// 定义一个方法用于将list拼接成字符串
joinList() {
// 使用数组的join方法以逗号分隔元素
return this.form.ImgUrl.join(',');
},
/** 处理图片上传 --知无涯 */
async handleImageUpload(file) {
this.isUploading = true; // 开始上传
const that = this;
this.isUploading = true;
try {
if (!file) {
if (!file || !file.file) {
throw new Error('无效的文件对象');
}
// 生成水印文字:巡检地点 + 巡检时间
const times = this.getCurrentDate();
// 生成水印文字
const watermarkText = `${this.form.inspectionPoint || '未命名地点'} \n${this.getCurrentDate()}`;
const watermarkText = `${this.form.inspectionPoint || '未命名地点'} \n${times || '未设置时间'}`;
// 添加水印 - 水印配置
// const watermarkedFile = await addWatermarkToImage(file.file, watermarkText, {
// font: 'bold 20px Arial', // 加大字号并加粗
// color: 'rgba(0, 0, 0, 0.7)' // 黑色70%不透明度
// });
const watermarkedFile = await addWatermarkToImage(file.file, watermarkText);
// 创建预览URL
const previewUrl = URL.createObjectURL(watermarkedFile);
// 更新表单数据
that.imgFiles = {
raw: watermarkedFile,
name: watermarkedFile.name,
url: previewUrl
};
//上传图片,获取图片路径
that.img.push(that.imgFiles)
await uploadImg({
filePath: previewUrl
}).then(res => {
that.form.ImgUrl.push(res.fileName)
// console.log(res);
})
// 添加水印
let processedFile = await addWatermarkToImage(file.file, watermarkText);
// 如果是移动浏览器且文件大于300KB则进行压缩
if (this.isMobileBrowser && processedFile.size > 300 * 1024) {
console.log('原始文件大小:', (processedFile.size / 1024).toFixed(2), 'KB');
processedFile = await this.compressImageForMobile(processedFile);
console.log('压缩后文件大小:', (processedFile.size / 1024).toFixed(2), 'KB');
}
// 上传处理后的文件
const fileUrl = await this.uploadFile(processedFile);
// 更新UI显示
this.img.push({
raw: processedFile,
name: processedFile.name,
url: fileUrl
});
} catch (error) {
console.error("水印处理失败:", error);
proxy.$modal.msgError("图片处理失败: " + error.message);
console.error("图片处理失败:", error);
this.showMessage('error', `图片处理失败: ${error.message}`);
} finally {
this.isUploading = false; // 上传完毕
this.isUploading = false;
}
},
// 获取当前时间
// 专门为移动浏览器优化的压缩方法
compressImageForMobile(file, quality = 0.6, maxWidth = 1200, maxHeight = 1200) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (event) => {
img.onload = () => {
// 计算压缩比例
let width = img.width;
let height = img.height;
// 限制最大尺寸
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width = Math.floor(width * ratio);
height = Math.floor(height * ratio);
}
// 创建canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// 设置更高的压缩质量
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, width, height);
// 使用toBlob的polyfill保证兼容性
if (canvas.toBlob) {
canvas.toBlob(
(blob) => {
if (!blob) {
console.warn('压缩失败,返回原始文件');
resolve(file);
return;
}
// 创建压缩后的文件对象
const compressedFile = new File([blob], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
});
resolve(compressedFile);
},
'image/jpeg',
quality
);
} else {
// 不支持toBlob的浏览器直接返回原文件
console.warn('浏览器不支持toBlob方法无法压缩');
resolve(file);
}
};
img.onerror = () => {
console.warn('图片加载失败,返回原始文件');
resolve(file);
};
img.src = event.target.result;
};
reader.onerror = () => {
console.warn('文件读取失败,返回原始文件');
resolve(file);
};
reader.readAsDataURL(file);
});
},
async uploadFile(file) {
return new Promise((resolve, reject) => {
// 创建预览URL用于显示
const previewUrl = URL.createObjectURL(file);
// 实际上传
uploadImg({
filePath: previewUrl
}).then(res => {
this.form.ImgUrl.push(res.fileName);
resolve(previewUrl);
}).catch(err => {
reject(err);
});
});
},
getCurrentDate() {
const now = new Date();
const year = now.getFullYear();
@@ -202,34 +257,42 @@
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
//对话框确定按钮
dialogConfirm() {
if (this.msgType == 'success') {
uni.reLaunch({
url: '/pages/work/index'
})
return;
} else {
return;
});
}
},
showMessage(type, text) {
this.msgType = type;
this.messageText = text;
this.$refs.alertDialog.open();
},
// 检测移动浏览器环境
checkMobileBrowser() {
// 更全面的移动设备检测
const userAgent = navigator.userAgent.toLowerCase();
const isMobile = /iphone|ipod|android|windows phone|mobile|blackberry/.test(userAgent);
// 如果是通过uni-app的web-view打开也认为是移动环境
this.isMobileBrowser = isMobile || window.__uniAppWebview;
}
},
// uniapp
//uniapp方法在最开始的时候拿到传值过来的值
onLoad(option) {
const _this = this
// console.log('传递的值',option)
_this.form.inspectionPoint = option.inspectionPoint
_this.form.inspectionRequirements = option.inspectionRequirements
_this.form.inspectionPointId = option.inspectionPointId
this.form.inspectionPoint = option.inspectionPoint || '';
this.form.inspectionRequirements = option.inspectionRequirements || '';
this.form.inspectionPointId = option.inspectionPointId || '';
},
created() {
// console.log("this.$store.state.user",this.$store.state.user);
mounted() {
this.checkMobileBrowser();
}
}
</script>
<style lang="scss">
page {
display: flex;

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>