|
|
@@ -19,15 +19,35 @@
|
|
|
class="camera"
|
|
|
@error="handleCameraError"
|
|
|
>
|
|
|
- <!-- 扫描框 -->
|
|
|
- <view class="scan-frame">
|
|
|
- <view class="corner corner-tl"></view>
|
|
|
- <view class="corner corner-tr"></view>
|
|
|
- <view class="corner corner-bl"></view>
|
|
|
- <view class="corner corner-br"></view>
|
|
|
- </view>
|
|
|
+ <!-- 检测边框覆盖层 -->
|
|
|
+ <canvas
|
|
|
+ v-if="detectedBorders.length > 0"
|
|
|
+ canvas-id="borderCanvas"
|
|
|
+ class="border-canvas"
|
|
|
+ :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|
|
+ ></canvas>
|
|
|
</camera>
|
|
|
|
|
|
+ <!-- 模式切换按钮 - 在底部操作栏上方 -->
|
|
|
+ <view class="mode-switch-wrapper">
|
|
|
+ <view class="mode-switch">
|
|
|
+ <view
|
|
|
+ class="mode-btn"
|
|
|
+ :class="{ active: scanMode === 'single' }"
|
|
|
+ @click="switchToSingle"
|
|
|
+ >
|
|
|
+ <text class="mode-text">单页</text>
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ class="mode-btn"
|
|
|
+ :class="{ active: scanMode === 'multiple' }"
|
|
|
+ @click="switchToMultiple"
|
|
|
+ >
|
|
|
+ <text class="mode-text">多页</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 底部操作按钮 -->
|
|
|
<view class="action-buttons">
|
|
|
<view class="action-btn" @click="handleSelectImage">
|
|
|
@@ -49,7 +69,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
+import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
@@ -57,20 +77,92 @@ const { t } = useI18n()
|
|
|
// 状态栏高度
|
|
|
const statusBarHeight = ref(0)
|
|
|
|
|
|
+// 扫描模式:single-单页,multiple-多页
|
|
|
+const scanMode = ref('single')
|
|
|
+
|
|
|
+// 检测到的边框
|
|
|
+const detectedBorders = ref([])
|
|
|
+
|
|
|
+// Canvas尺寸
|
|
|
+const canvasWidth = ref(0)
|
|
|
+const canvasHeight = ref(0)
|
|
|
+
|
|
|
+// 相机上下文
|
|
|
+let cameraContext = null
|
|
|
+
|
|
|
+// Canvas上下文
|
|
|
+let canvasContext = null
|
|
|
+
|
|
|
+// 扫描定时器
|
|
|
+let scanTimer = null
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
- // 获取系统信息 - 使用新API
|
|
|
+ // 获取系统信息
|
|
|
const windowInfo = uni.getWindowInfo()
|
|
|
statusBarHeight.value = windowInfo.statusBarHeight || 0
|
|
|
+
|
|
|
+ // 获取屏幕尺寸
|
|
|
+ const systemInfo = uni.getSystemInfoSync()
|
|
|
+ canvasWidth.value = systemInfo.windowWidth
|
|
|
+ canvasHeight.value = systemInfo.windowHeight - statusBarHeight.value - 88 - 200 // 减去头部和底部按钮高度
|
|
|
+
|
|
|
+ // 初始化相机上下文
|
|
|
+ cameraContext = uni.createCameraContext()
|
|
|
+
|
|
|
+ // 初始化Canvas上下文
|
|
|
+ canvasContext = uni.createCanvasContext('borderCanvas')
|
|
|
+
|
|
|
+ // 开始实物检测
|
|
|
+ startDetection()
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ // 停止检测
|
|
|
+ stopDetection()
|
|
|
})
|
|
|
|
|
|
// 返回上一页
|
|
|
const handleBack = () => {
|
|
|
- // 返回到home页面
|
|
|
uni.reLaunch({
|
|
|
url: '/pages/home/index'
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+// 切换扫描模式
|
|
|
+const toggleScanMode = () => {
|
|
|
+ scanMode.value = scanMode.value === 'single' ? 'multiple' : 'single'
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: scanMode.value === 'single' ? '已切换到单页模式' : '已切换到多页模式',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 切换到单页模式
|
|
|
+const switchToSingle = () => {
|
|
|
+ if (scanMode.value === 'single') return
|
|
|
+ scanMode.value = 'single'
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '已切换到单页模式',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 切换到多页模式
|
|
|
+const switchToMultiple = () => {
|
|
|
+ if (scanMode.value === 'multiple') return
|
|
|
+ scanMode.value = 'multiple'
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '已切换到多页模式',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
// 相机错误处理
|
|
|
const handleCameraError = (e) => {
|
|
|
console.error('相机错误:', e)
|
|
|
@@ -80,18 +172,199 @@ const handleCameraError = (e) => {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+// 开始实物检测
|
|
|
+const startDetection = () => {
|
|
|
+ // 每隔一段时间进行一次实物检测
|
|
|
+ scanTimer = setInterval(() => {
|
|
|
+ detectObject()
|
|
|
+ }, 300) // 每300ms检测一次
|
|
|
+}
|
|
|
+
|
|
|
+// 停止检测
|
|
|
+const stopDetection = () => {
|
|
|
+ if (scanTimer) {
|
|
|
+ clearInterval(scanTimer)
|
|
|
+ scanTimer = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 检测实物
|
|
|
+const detectObject = () => {
|
|
|
+ if (!cameraContext) return
|
|
|
+
|
|
|
+ // 拍摄临时照片用于检测
|
|
|
+ cameraContext.takePhoto({
|
|
|
+ quality: 'low', // 使用低质量以提高检测速度
|
|
|
+ success: (res) => {
|
|
|
+ // 分析图片检测实物
|
|
|
+ analyzeImage(res.tempImagePath)
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ // 静默失败,继续下一次检测
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 分析图片中的实物
|
|
|
+const analyzeImage = (imagePath) => {
|
|
|
+ // TODO: 调用后端API或图像识别服务检测实物
|
|
|
+ // 这里需要接入实际的物体检测API,例如:
|
|
|
+ // - 百度AI文字识别OCR
|
|
|
+ // - 腾讯云文档识别
|
|
|
+ // - 阿里云文档检测
|
|
|
+
|
|
|
+ // 模拟检测结果 - 检测到1-3个实物
|
|
|
+ const hasObject = Math.random() > 0.3 // 70%概率检测到实物
|
|
|
+
|
|
|
+ if (hasObject) {
|
|
|
+ const objectCount = Math.floor(Math.random() * 3) + 1 // 1-3个实物
|
|
|
+ const borders = []
|
|
|
+
|
|
|
+ for (let i = 0; i < objectCount; i++) {
|
|
|
+ // 生成随机位置的实物边框(模拟检测结果)
|
|
|
+ const x = Math.random() * (canvasWidth.value - 200) + 50
|
|
|
+ const y = Math.random() * (canvasHeight.value - 300) + 50
|
|
|
+ const width = Math.random() * 200 + 150
|
|
|
+ const height = Math.random() * 250 + 200
|
|
|
+
|
|
|
+ borders.push({
|
|
|
+ x,
|
|
|
+ y,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ // 四个角点坐标(用于绘制更精确的边框)
|
|
|
+ points: [
|
|
|
+ { x, y }, // 左上
|
|
|
+ { x: x + width, y }, // 右上
|
|
|
+ { x: x + width, y: y + height }, // 右下
|
|
|
+ { x, y: y + height } // 左下
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ detectedBorders.value = borders
|
|
|
+
|
|
|
+ // 绘制边框
|
|
|
+ drawBorders(borders)
|
|
|
+
|
|
|
+ // 自动对焦到第一个检测到的实物
|
|
|
+ if (borders.length > 0) {
|
|
|
+ autoFocus(borders[0])
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 没有检测到实物,清空边框
|
|
|
+ detectedBorders.value = []
|
|
|
+ clearCanvas()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 绘制检测到的边框
|
|
|
+const drawBorders = (borders) => {
|
|
|
+ if (!canvasContext || !borders || borders.length === 0) return
|
|
|
+
|
|
|
+ // 清空画布
|
|
|
+ canvasContext.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
|
|
|
+
|
|
|
+ // 绘制每个检测到的边框
|
|
|
+ borders.forEach((border, index) => {
|
|
|
+ // 设置边框样式
|
|
|
+ canvasContext.setStrokeStyle('#1ec9c9')
|
|
|
+ canvasContext.setLineWidth(3)
|
|
|
+ canvasContext.setLineDash([10, 5], 0) // 虚线效果
|
|
|
+
|
|
|
+ // 绘制四边形边框
|
|
|
+ canvasContext.beginPath()
|
|
|
+ canvasContext.moveTo(border.points[0].x, border.points[0].y)
|
|
|
+ border.points.forEach((point, i) => {
|
|
|
+ if (i > 0) {
|
|
|
+ canvasContext.lineTo(point.x, point.y)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ canvasContext.closePath()
|
|
|
+ canvasContext.stroke()
|
|
|
+
|
|
|
+ // 绘制四个角点
|
|
|
+ border.points.forEach(point => {
|
|
|
+ canvasContext.beginPath()
|
|
|
+ canvasContext.arc(point.x, point.y, 8, 0, 2 * Math.PI)
|
|
|
+ canvasContext.setFillStyle('#1ec9c9')
|
|
|
+ canvasContext.fill()
|
|
|
+
|
|
|
+ // 角点外圈
|
|
|
+ canvasContext.beginPath()
|
|
|
+ canvasContext.arc(point.x, point.y, 12, 0, 2 * Math.PI)
|
|
|
+ canvasContext.setStrokeStyle('#ffffff')
|
|
|
+ canvasContext.setLineWidth(2)
|
|
|
+ canvasContext.stroke()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 添加半透明填充
|
|
|
+ canvasContext.setFillStyle('rgba(30, 201, 201, 0.1)')
|
|
|
+ canvasContext.beginPath()
|
|
|
+ canvasContext.moveTo(border.points[0].x, border.points[0].y)
|
|
|
+ border.points.forEach((point, i) => {
|
|
|
+ if (i > 0) {
|
|
|
+ canvasContext.lineTo(point.x, point.y)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ canvasContext.closePath()
|
|
|
+ canvasContext.fill()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 绘制到屏幕
|
|
|
+ canvasContext.draw()
|
|
|
+}
|
|
|
+
|
|
|
+// 清空画布
|
|
|
+const clearCanvas = () => {
|
|
|
+ if (!canvasContext) return
|
|
|
+ canvasContext.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
|
|
|
+ canvasContext.draw()
|
|
|
+}
|
|
|
+
|
|
|
+// 自动对焦到实物
|
|
|
+const autoFocus = (position) => {
|
|
|
+ if (!cameraContext || !position) return
|
|
|
+
|
|
|
+ // 计算实物中心点(相对于屏幕的比例)
|
|
|
+ const centerX = (position.x + position.width / 2) / 750
|
|
|
+ const centerY = (position.y + position.height / 2) / 1334
|
|
|
+
|
|
|
+ // 设置对焦点
|
|
|
+ console.log('自动对焦到实物:', centerX, centerY)
|
|
|
+
|
|
|
+ // 注意:实际对焦效果取决于设备和平台支持
|
|
|
+}
|
|
|
+
|
|
|
// 拍照
|
|
|
const handleCapture = () => {
|
|
|
- const ctx = uni.createCameraContext()
|
|
|
- ctx.takePhoto({
|
|
|
+ if (!cameraContext) return
|
|
|
+
|
|
|
+ // 暂停检测
|
|
|
+ stopDetection()
|
|
|
+
|
|
|
+ cameraContext.takePhoto({
|
|
|
quality: 'high',
|
|
|
success: (res) => {
|
|
|
console.log('拍照成功:', res.tempImagePath)
|
|
|
- uni.showToast({
|
|
|
- title: '拍照成功',
|
|
|
- icon: 'success'
|
|
|
+
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...',
|
|
|
+ mask: true
|
|
|
})
|
|
|
- // TODO: 处理拍照后的图片
|
|
|
+
|
|
|
+ // TODO: 上传图片到服务器
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '拍照成功',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+
|
|
|
+ // 恢复检测
|
|
|
+ startDetection()
|
|
|
+ }, 1000)
|
|
|
},
|
|
|
fail: (err) => {
|
|
|
console.error('拍照失败:', err)
|
|
|
@@ -99,45 +372,76 @@ const handleCapture = () => {
|
|
|
title: '拍照失败',
|
|
|
icon: 'none'
|
|
|
})
|
|
|
+
|
|
|
+ // 恢复检测
|
|
|
+ startDetection()
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 导入图片
|
|
|
const handleSelectImage = () => {
|
|
|
+ stopDetection()
|
|
|
+
|
|
|
uni.chooseImage({
|
|
|
count: 1,
|
|
|
sizeType: ['original', 'compressed'],
|
|
|
sourceType: ['album'],
|
|
|
success: (res) => {
|
|
|
console.log('选择图片成功:', res.tempFilePaths)
|
|
|
- uni.showToast({
|
|
|
- title: '图片导入成功',
|
|
|
- icon: 'success'
|
|
|
+
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...',
|
|
|
+ mask: true
|
|
|
})
|
|
|
- // TODO: 处理选择的图片
|
|
|
+
|
|
|
+ // TODO: 上传图片到服务器
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '图片导入成功',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+
|
|
|
+ startDetection()
|
|
|
+ }, 1000)
|
|
|
},
|
|
|
- fail: (err) => {
|
|
|
- console.error('选择图片失败:', err)
|
|
|
+ fail: () => {
|
|
|
+ startDetection()
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 导入文档
|
|
|
const handleSelectFile = () => {
|
|
|
+ stopDetection()
|
|
|
+
|
|
|
uni.chooseMessageFile({
|
|
|
count: 1,
|
|
|
type: 'file',
|
|
|
success: (res) => {
|
|
|
console.log('选择文件成功:', res.tempFiles)
|
|
|
- uni.showToast({
|
|
|
- title: '文档导入成功',
|
|
|
- icon: 'success'
|
|
|
+
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...',
|
|
|
+ mask: true
|
|
|
})
|
|
|
- // TODO: 处理选择的文件
|
|
|
+
|
|
|
+ // TODO: 上传文件到服务器
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '文档导入成功',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+
|
|
|
+ startDetection()
|
|
|
+ }, 1000)
|
|
|
},
|
|
|
- fail: (err) => {
|
|
|
- console.error('选择文件失败:', err)
|
|
|
+ fail: () => {
|
|
|
+ startDetection()
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
@@ -204,48 +508,54 @@ const handleSelectFile = () => {
|
|
|
width: 100%;
|
|
|
position: relative;
|
|
|
|
|
|
- // 扫描框
|
|
|
- .scan-frame {
|
|
|
+ // 边框检测画布
|
|
|
+ .border-canvas {
|
|
|
position: absolute;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- width: 500rpx;
|
|
|
- height: 500rpx;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ z-index: 10;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 模式切换按钮 - 在底部操作栏上方
|
|
|
+ .mode-switch-wrapper {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 280rpx;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ z-index: 10;
|
|
|
+
|
|
|
+ .mode-switch {
|
|
|
+ display: flex;
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
+ border-radius: 40rpx;
|
|
|
+ padding: 6rpx;
|
|
|
+ backdrop-filter: blur(10rpx);
|
|
|
|
|
|
- .corner {
|
|
|
- position: absolute;
|
|
|
- width: 60rpx;
|
|
|
- height: 60rpx;
|
|
|
- border-color: #1ec9c9;
|
|
|
- border-style: solid;
|
|
|
-
|
|
|
- &.corner-tl {
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- border-width: 6rpx 0 0 6rpx;
|
|
|
- border-radius: 12rpx 0 0 0;
|
|
|
- }
|
|
|
+ .mode-btn {
|
|
|
+ padding: 12rpx 32rpx;
|
|
|
+ border-radius: 34rpx;
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
|
- &.corner-tr {
|
|
|
- top: 0;
|
|
|
- right: 0;
|
|
|
- border-width: 6rpx 6rpx 0 0;
|
|
|
- border-radius: 0 12rpx 0 0;
|
|
|
+ .mode-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.6);
|
|
|
+ font-weight: 500;
|
|
|
+ transition: all 0.3s;
|
|
|
}
|
|
|
|
|
|
- &.corner-bl {
|
|
|
- bottom: 0;
|
|
|
- left: 0;
|
|
|
- border-width: 0 0 6rpx 6rpx;
|
|
|
- border-radius: 0 0 0 12rpx;
|
|
|
+ &.active {
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+
|
|
|
+ .mode-text {
|
|
|
+ color: #333333;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- &.corner-br {
|
|
|
- bottom: 0;
|
|
|
- right: 0;
|
|
|
- border-width: 0 6rpx 6rpx 0;
|
|
|
- border-radius: 0 0 12rpx 0;
|
|
|
+ &:active {
|
|
|
+ transform: scale(0.95);
|
|
|
}
|
|
|
}
|
|
|
}
|