364 lines
7.9 KiB
Vue
364 lines
7.9 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="camera-container">
|
|||
|
|
<!-- 相机预览层(由插件控制) -->
|
|||
|
|
|
|||
|
|
<!-- 自定义OCR框 -->
|
|||
|
|
<view class="preview-overlay">
|
|||
|
|
<view class="ocr-frame">
|
|||
|
|
<view class="frame-label">请将编码置于框内拍摄</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 预览结果 -->
|
|||
|
|
<view class="preview-result" v-if="previewImage">
|
|||
|
|
<image :src="previewImage" alt="预览" class="result-image"/>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="status">OCR相机模式</view>
|
|||
|
|
|
|||
|
|
<!-- 控制按钮 -->
|
|||
|
|
<view class="controls">
|
|||
|
|
<view class="close-btn" @click="closeCamera">×</view>
|
|||
|
|
<view class="capture-btn" @click="captureImage"></view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 加载指示器 -->
|
|||
|
|
<view class="loading" v-if="isLoading">
|
|||
|
|
<view class="loading-spinner"></view>
|
|||
|
|
<text class="loading-text">处理中...</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
previewImage: null,
|
|||
|
|
frameRect: {
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
width: 0,
|
|||
|
|
height: 0
|
|||
|
|
},
|
|||
|
|
isLoading: false
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
mounted() {
|
|||
|
|
this.initCamera();
|
|||
|
|
// 初始计算框位置
|
|||
|
|
this.calculateFrameRect();
|
|||
|
|
// 监听窗口变化
|
|||
|
|
uni.onWindowResize(() => {
|
|||
|
|
this.calculateFrameRect();
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
beforeUnmount() {
|
|||
|
|
this.stopCamera();
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
// 计算OCR框的位置和尺寸
|
|||
|
|
calculateFrameRect() {
|
|||
|
|
const query = uni.createSelectorQuery().in(this);
|
|||
|
|
query.select('.ocr-frame').boundingClientRect(data => {
|
|||
|
|
if (data) {
|
|||
|
|
this.frameRect = {
|
|||
|
|
x: data.left,
|
|||
|
|
y: data.top,
|
|||
|
|
width: data.width,
|
|||
|
|
height: data.height
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}).exec();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 初始化相机
|
|||
|
|
initCamera() {
|
|||
|
|
if (typeof cordova === 'undefined') {
|
|||
|
|
console.warn('Cordova环境未加载');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.addEventListener('deviceready', () => {
|
|||
|
|
if (cordova.plugins && cordova.plugins.camerapreview) {
|
|||
|
|
window.CameraPreview = cordova.plugins.camerapreview;
|
|||
|
|
this.startCamera();
|
|||
|
|
} else {
|
|||
|
|
console.error('CameraPreview插件未加载');
|
|||
|
|
}
|
|||
|
|
}, false);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 启动相机预览
|
|||
|
|
startCamera() {
|
|||
|
|
if (!window.CameraPreview) return;
|
|||
|
|
|
|||
|
|
const screenWidth = uni.getSystemInfoSync().screenWidth;
|
|||
|
|
const screenHeight = uni.getSystemInfoSync().screenHeight;
|
|||
|
|
|
|||
|
|
window.CameraPreview.startCamera({
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
width: screenWidth,
|
|||
|
|
height: screenHeight,
|
|||
|
|
camera: 'back',
|
|||
|
|
toBack: true,
|
|||
|
|
tapPhoto: false,
|
|||
|
|
previewDrag: false,
|
|||
|
|
disableExifHeader: true
|
|||
|
|
},
|
|||
|
|
() => console.log('相机启动成功'),
|
|||
|
|
(err) => console.error('相机启动失败', err));
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 停止相机
|
|||
|
|
stopCamera() {
|
|||
|
|
if (window.CameraPreview) {
|
|||
|
|
window.CameraPreview.stopCamera();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 拍照并裁剪
|
|||
|
|
captureImage() {
|
|||
|
|
if (!window.CameraPreview) return;
|
|||
|
|
|
|||
|
|
this.isLoading = true;
|
|||
|
|
|
|||
|
|
window.CameraPreview.takePicture({quality: 85}, (imageData) => {
|
|||
|
|
const fullBase64 = 'data:image/jpeg;base64,' + imageData;
|
|||
|
|
this.cropToOcrFrame(fullBase64);
|
|||
|
|
}, (error) => {
|
|||
|
|
console.error('拍照失败', error);
|
|||
|
|
this.isLoading = false;
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '拍照失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 裁剪到OCR框区域
|
|||
|
|
cropToOcrFrame(base64) {
|
|||
|
|
const img = new Image();
|
|||
|
|
img.onload = () => {
|
|||
|
|
const canvas = document.createElement('canvas');
|
|||
|
|
const ctx = canvas.getContext('2d');
|
|||
|
|
|
|||
|
|
// 计算裁剪区域(考虑设备像素比)
|
|||
|
|
const dpr = window.devicePixelRatio || 1;
|
|||
|
|
const cropX = this.frameRect.x * dpr;
|
|||
|
|
const cropY = this.frameRect.y * dpr;
|
|||
|
|
const cropWidth = this.frameRect.width * dpr;
|
|||
|
|
const cropHeight = this.frameRect.height * dpr;
|
|||
|
|
|
|||
|
|
// 设置画布尺寸
|
|||
|
|
canvas.width = cropWidth;
|
|||
|
|
canvas.height = cropHeight;
|
|||
|
|
|
|||
|
|
// 裁剪图像
|
|||
|
|
ctx.drawImage(
|
|||
|
|
img,
|
|||
|
|
cropX, cropY, cropWidth, cropHeight,
|
|||
|
|
0, 0, cropWidth, cropHeight
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 获取裁剪后的base64
|
|||
|
|
const croppedBase64 = canvas.toDataURL('image/jpeg', 0.9);
|
|||
|
|
this.previewImage = croppedBase64;
|
|||
|
|
this.isLoading = false;
|
|||
|
|
|
|||
|
|
// 这里可以添加OCR识别逻辑
|
|||
|
|
console.log('裁剪后的Base64:', croppedBase64.substring(0, 100) + '...');
|
|||
|
|
|
|||
|
|
// 在实际应用中,这里可以触发OCR识别
|
|||
|
|
// this.recognizeOcr(croppedBase64);
|
|||
|
|
};
|
|||
|
|
img.src = base64;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// OCR识别方法(示例)
|
|||
|
|
recognizeOcr(base64Image) {
|
|||
|
|
// 在实际应用中,这里调用OCR识别API
|
|||
|
|
console.log('调用OCR识别...');
|
|||
|
|
// uni.request({
|
|||
|
|
// url: 'https://api.ocr.example.com/recognize',
|
|||
|
|
// method: 'POST',
|
|||
|
|
// data: { image: base64Image.split(',')[1] },
|
|||
|
|
// success: (res) => {
|
|||
|
|
// console.log('OCR识别结果:', res.data);
|
|||
|
|
// }
|
|||
|
|
// });
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 关闭相机
|
|||
|
|
closeCamera() {
|
|||
|
|
this.stopCamera();
|
|||
|
|
// 返回上一页或执行其他关闭逻辑
|
|||
|
|
uni.navigateBack();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.camera-container {
|
|||
|
|
position: relative;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background-color: transparent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-overlay {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
z-index: 10;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ocr-frame {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 50%;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
width: 80%;
|
|||
|
|
height: 30%;
|
|||
|
|
border: 4px solid #00ff00;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
box-shadow: 0 0 0 10000px rgba(0, 0, 0, 0.6);
|
|||
|
|
pointer-events: none;
|
|||
|
|
z-index: 11;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.frame-label {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: -40px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
color: white;
|
|||
|
|
font-size: 16px;
|
|||
|
|
text-align: center;
|
|||
|
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.controls {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 40px;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
z-index: 20;
|
|||
|
|
gap: 30px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capture-btn {
|
|||
|
|
width: 70px;
|
|||
|
|
height: 70px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: white;
|
|||
|
|
border: 4px solid #f0f0f0;
|
|||
|
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
|
|||
|
|
cursor: pointer;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capture-btn::after {
|
|||
|
|
content: '';
|
|||
|
|
width: 50px;
|
|||
|
|
height: 50px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) inset;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.close-btn {
|
|||
|
|
width: 50px;
|
|||
|
|
height: 50px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: rgba(255, 255, 255, 0.2);
|
|||
|
|
border: 2px solid white;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 30px;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-result {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 20px;
|
|||
|
|
right: 20px;
|
|||
|
|
width: 120px;
|
|||
|
|
height: 160px;
|
|||
|
|
border: 2px solid white;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
|
|||
|
|
z-index: 15;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-image {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
object-fit: cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 20px;
|
|||
|
|
left: 20px;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 14px;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
padding: 5px 12px;
|
|||
|
|
border-radius: 20px;
|
|||
|
|
z-index: 15;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background: rgba(0, 0, 0, 0.7);
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-spinner {
|
|||
|
|
width: 50px;
|
|||
|
|
height: 50px;
|
|||
|
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
border-top: 4px solid #00ff00;
|
|||
|
|
animation: spin 1s linear infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-text {
|
|||
|
|
margin-top: 15px;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes spin {
|
|||
|
|
0% {
|
|||
|
|
transform: rotate(0deg);
|
|||
|
|
}
|
|||
|
|
100% {
|
|||
|
|
transform: rotate(360deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|