|
|
@@ -19,13 +19,6 @@
|
|
|
class="camera"
|
|
|
@error="handleCameraError"
|
|
|
>
|
|
|
- <!-- 检测边框覆盖层 -->
|
|
|
- <canvas
|
|
|
- v-if="detectedBorders.length > 0"
|
|
|
- canvas-id="borderCanvas"
|
|
|
- class="border-canvas"
|
|
|
- :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|
|
- ></canvas>
|
|
|
</camera>
|
|
|
|
|
|
<!-- 模式切换按钮 - 在底部操作栏上方 -->
|
|
|
@@ -48,6 +41,46 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 多页模式缩略图列表 -->
|
|
|
+ <view v-if="scanMode === 'multiple' && pdfDataList.length > 0" class="thumbnail-list">
|
|
|
+ <scroll-view scroll-x class="thumbnail-scroll">
|
|
|
+ <view class="thumbnail-wrapper">
|
|
|
+ <view
|
|
|
+ v-for="(item, index) in pdfDataList"
|
|
|
+ :key="index"
|
|
|
+ class="thumbnail-item"
|
|
|
+ @click="handleViewAllPdf"
|
|
|
+ >
|
|
|
+ <view class="pdf-thumbnail-icon">
|
|
|
+ <image src="/static/icon/pdf.svg" mode="aspectFit" class="pdf-icon-img" />
|
|
|
+ </view>
|
|
|
+ <view class="thumbnail-delete" @click.stop="handleDeletePdf(index)">
|
|
|
+ <text class="delete-icon">×</text>
|
|
|
+ </view>
|
|
|
+ <view class="thumbnail-index">{{ index + 1 }}</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 上传按钮(多页模式且有图片时显示) -->
|
|
|
+ <view
|
|
|
+ v-if="scanMode === 'multiple' && imageBase64List.length > 0"
|
|
|
+ class="upload-btn"
|
|
|
+ @click="handleMultipleUpload"
|
|
|
+ >
|
|
|
+ <text class="upload-text">上传</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- PDF缩略图预览(右下角) -->
|
|
|
+ <view v-if="showPdfThumbnail" class="pdf-thumbnail" @click="handleOpenPdf">
|
|
|
+ <view class="pdf-close" @click.stop="handleClosePdf">
|
|
|
+ <text class="close-icon">×</text>
|
|
|
+ </view>
|
|
|
+ <image class="pdf-icon" src="/static/icon/pdf.svg" mode="aspectFit" />
|
|
|
+ <text class="pdf-text">PDF</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 底部操作按钮 -->
|
|
|
<view class="action-buttons">
|
|
|
<view class="action-btn" @click="handleSelectImage">
|
|
|
@@ -71,6 +104,7 @@
|
|
|
<script setup>
|
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
+import { scanUpload } from '@/apis/scan'
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
@@ -80,45 +114,38 @@ const statusBarHeight = ref(0)
|
|
|
// 扫描模式:single-单页,multiple-多页
|
|
|
const scanMode = ref('single')
|
|
|
|
|
|
-// 检测到的边框
|
|
|
-const detectedBorders = ref([])
|
|
|
+// 图片base64数组(用于多页模式)
|
|
|
+const imageBase64List = ref([])
|
|
|
|
|
|
-// Canvas尺寸
|
|
|
-const canvasWidth = ref(0)
|
|
|
-const canvasHeight = ref(0)
|
|
|
+// 图片临时路径数组(用于显示缩略图)
|
|
|
+const imageTempPaths = ref([])
|
|
|
|
|
|
-// 相机上下文
|
|
|
-let cameraContext = null
|
|
|
+// PDF数据数组(多页模式,包含路径和base64)
|
|
|
+const pdfDataList = ref([])
|
|
|
+
|
|
|
+// 缓存的ossId
|
|
|
+const cachedOssId = ref(null)
|
|
|
|
|
|
-// Canvas上下文
|
|
|
-let canvasContext = null
|
|
|
+// PDF文件路径(用于预览)
|
|
|
+const pdfFilePath = ref('')
|
|
|
|
|
|
-// 扫描定时器
|
|
|
-let scanTimer = null
|
|
|
+// 是否显示PDF缩略图
|
|
|
+const showPdfThumbnail = ref(false)
|
|
|
+
|
|
|
+// 相机上下文
|
|
|
+let cameraContext = null
|
|
|
|
|
|
onMounted(() => {
|
|
|
// 获取系统信息
|
|
|
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()
|
|
|
+ // 清理资源
|
|
|
})
|
|
|
|
|
|
// 返回上一页
|
|
|
@@ -172,199 +199,37 @@ 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 = () => {
|
|
|
if (!cameraContext) return
|
|
|
|
|
|
- // 暂停检测
|
|
|
- stopDetection()
|
|
|
-
|
|
|
cameraContext.takePhoto({
|
|
|
quality: 'high',
|
|
|
- success: (res) => {
|
|
|
- console.log('拍照成功:', res.tempImagePath)
|
|
|
-
|
|
|
- uni.showLoading({
|
|
|
- title: '处理中...',
|
|
|
- mask: true
|
|
|
- })
|
|
|
-
|
|
|
- // TODO: 上传图片到服务器
|
|
|
- setTimeout(() => {
|
|
|
- uni.hideLoading()
|
|
|
+ success: async (res) => {
|
|
|
+ try {
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...',
|
|
|
+ mask: true
|
|
|
+ })
|
|
|
|
|
|
+ // 将图片转换为base64
|
|
|
+ const base64Data = await imageToBase64(res.tempImagePath)
|
|
|
+
|
|
|
+ if (scanMode.value === 'single') {
|
|
|
+ // 单页模式:直接上传
|
|
|
+ await uploadSingleImage(base64Data)
|
|
|
+ } else {
|
|
|
+ // 多页模式:立即上传并扫描
|
|
|
+ await uploadAndScanImage(base64Data)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('处理图片失败:', error)
|
|
|
uni.showToast({
|
|
|
- title: '拍照成功',
|
|
|
- icon: 'success'
|
|
|
+ title: '处理失败',
|
|
|
+ icon: 'none'
|
|
|
})
|
|
|
-
|
|
|
- // 恢复检测
|
|
|
- startDetection()
|
|
|
- }, 1000)
|
|
|
+ }
|
|
|
},
|
|
|
fail: (err) => {
|
|
|
console.error('拍照失败:', err)
|
|
|
@@ -372,57 +237,51 @@ const handleCapture = () => {
|
|
|
title: '拍照失败',
|
|
|
icon: 'none'
|
|
|
})
|
|
|
-
|
|
|
- // 恢复检测
|
|
|
- startDetection()
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 导入图片
|
|
|
-const handleSelectImage = () => {
|
|
|
- stopDetection()
|
|
|
-
|
|
|
+const handleSelectImage = async () => {
|
|
|
uni.chooseImage({
|
|
|
count: 1,
|
|
|
sizeType: ['original', 'compressed'],
|
|
|
sourceType: ['album'],
|
|
|
- success: (res) => {
|
|
|
- console.log('选择图片成功:', res.tempFilePaths)
|
|
|
-
|
|
|
- uni.showLoading({
|
|
|
- title: '处理中...',
|
|
|
- mask: true
|
|
|
- })
|
|
|
-
|
|
|
- // TODO: 上传图片到服务器
|
|
|
- setTimeout(() => {
|
|
|
- uni.hideLoading()
|
|
|
+ success: async (res) => {
|
|
|
+ try {
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...',
|
|
|
+ mask: true
|
|
|
+ })
|
|
|
+
|
|
|
+ // 将图片转换为base64
|
|
|
+ const base64Data = await imageToBase64(res.tempFilePaths[0])
|
|
|
|
|
|
+ if (scanMode.value === 'single') {
|
|
|
+ // 单页模式:直接上传
|
|
|
+ await uploadSingleImage(base64Data)
|
|
|
+ } else {
|
|
|
+ // 多页模式:立即上传并扫描
|
|
|
+ await uploadAndScanImage(base64Data)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('处理图片失败:', error)
|
|
|
uni.showToast({
|
|
|
- title: '图片导入成功',
|
|
|
- icon: 'success'
|
|
|
+ title: '处理失败',
|
|
|
+ icon: 'none'
|
|
|
})
|
|
|
-
|
|
|
- startDetection()
|
|
|
- }, 1000)
|
|
|
- },
|
|
|
- fail: () => {
|
|
|
- startDetection()
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 导入文档
|
|
|
const handleSelectFile = () => {
|
|
|
- stopDetection()
|
|
|
-
|
|
|
uni.chooseMessageFile({
|
|
|
count: 1,
|
|
|
type: 'file',
|
|
|
success: (res) => {
|
|
|
- console.log('选择文件成功:', res.tempFiles)
|
|
|
-
|
|
|
uni.showLoading({
|
|
|
title: '处理中...',
|
|
|
mask: true
|
|
|
@@ -436,15 +295,266 @@ const handleSelectFile = () => {
|
|
|
title: '文档导入成功',
|
|
|
icon: 'success'
|
|
|
})
|
|
|
-
|
|
|
- startDetection()
|
|
|
}, 1000)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 将图片转换为base64
|
|
|
+const imageToBase64 = (filePath) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.getFileSystemManager().readFile({
|
|
|
+ filePath: filePath,
|
|
|
+ encoding: 'base64',
|
|
|
+ success: (res) => {
|
|
|
+ resolve(res.data)
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 单页模式上传
|
|
|
+const uploadSingleImage = async (base64Data) => {
|
|
|
+ try {
|
|
|
+ // 调用上传接口(单个文件)
|
|
|
+ const response = await scanUpload({
|
|
|
+ file: base64Data
|
|
|
+ })
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ // 缓存ossId
|
|
|
+ cachedOssId.value = response.data.ossId
|
|
|
+
|
|
|
+ // 处理PDF预览
|
|
|
+ if (response.data.fileBase64) {
|
|
|
+ await handlePdfPreview(response.data.fileBase64)
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '上传成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error(response.msg || '上传失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('上传失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: error.message || '上传失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 多页模式批量上传
|
|
|
+const handleMultipleUpload = async () => {
|
|
|
+ if (imageBase64List.value.length === 0) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '请先添加图片',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uni.showLoading({
|
|
|
+ title: '上传中...',
|
|
|
+ mask: true
|
|
|
+ })
|
|
|
+
|
|
|
+ // 调用上传接口
|
|
|
+ const response = await scanUpload({
|
|
|
+ files: imageBase64List.value
|
|
|
+ })
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ if (response.code === 200 && response.data) {
|
|
|
+ // 缓存ossId
|
|
|
+ cachedOssId.value = response.data.ossId
|
|
|
+
|
|
|
+ // 清空数组
|
|
|
+ imageBase64List.value = []
|
|
|
+ imageTempPaths.value = []
|
|
|
+
|
|
|
+ // 处理PDF预览
|
|
|
+ if (response.data.fileBase64) {
|
|
|
+ await handlePdfPreview(response.data.fileBase64)
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '上传成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error(response.msg || '上传失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('上传失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: error.message || '上传失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 多页模式:上传并扫描单张图片
|
|
|
+const uploadAndScanImage = async (base64Data) => {
|
|
|
+ try {
|
|
|
+ // 调用上传接口(单个文件)
|
|
|
+ const response = await scanUpload({
|
|
|
+ file: base64Data
|
|
|
+ })
|
|
|
+
|
|
|
+ uni.hideLoading()
|
|
|
+
|
|
|
+ if (response.code === 200 && response.data && response.data.fileBase64) {
|
|
|
+ // 将PDF转换为临时文件
|
|
|
+ const pdfPath = await base64ToTempFile(response.data.fileBase64)
|
|
|
+
|
|
|
+ // 添加到PDF数据列表(包含路径和base64)
|
|
|
+ pdfDataList.value.push({
|
|
|
+ path: pdfPath,
|
|
|
+ base64: response.data.fileBase64
|
|
|
+ })
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: `已扫描第${pdfDataList.value.length}页`,
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1500
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ throw new Error(response.msg || '扫描失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('扫描失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: error.message || '扫描失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 删除指定图片
|
|
|
+const handleDeleteImage = (index) => {
|
|
|
+ imageBase64List.value.splice(index, 1)
|
|
|
+ imageTempPaths.value.splice(index, 1)
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '已删除',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1000
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 删除指定PDF
|
|
|
+const handleDeletePdf = (index) => {
|
|
|
+ pdfDataList.value.splice(index, 1)
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '已删除',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 1000
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 查看所有PDF
|
|
|
+const handleViewAllPdf = () => {
|
|
|
+ if (pdfDataList.value.length === 0) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '暂无PDF',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用全局数据存储PDF数据(包含路径和base64)
|
|
|
+ getApp().globalData.pdfData = pdfDataList.value
|
|
|
+
|
|
|
+ uni.navigateTo({
|
|
|
+ url: '/pages/scan/pdfViewer/index'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 处理PDF预览
|
|
|
+const handlePdfPreview = async (base64Data) => {
|
|
|
+ try {
|
|
|
+ // 将base64转换为临时文件
|
|
|
+ const filePath = await base64ToTempFile(base64Data)
|
|
|
+ pdfFilePath.value = filePath
|
|
|
+ showPdfThumbnail.value = true
|
|
|
+
|
|
|
+ uni.showToast({
|
|
|
+ title: '上传成功',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理PDF失败:', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: '处理PDF失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 将base64转换为临时文件
|
|
|
+const base64ToTempFile = (base64Data) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const fs = uni.getFileSystemManager()
|
|
|
+ const fileName = `scan_${Date.now()}.pdf`
|
|
|
+ const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`
|
|
|
+
|
|
|
+ fs.writeFile({
|
|
|
+ filePath: filePath,
|
|
|
+ data: base64Data,
|
|
|
+ encoding: 'base64',
|
|
|
+ success: () => {
|
|
|
+ resolve(filePath)
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ reject(err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 打开PDF预览
|
|
|
+const handleOpenPdf = () => {
|
|
|
+ if (!pdfFilePath.value) return
|
|
|
+
|
|
|
+ uni.openDocument({
|
|
|
+ filePath: pdfFilePath.value,
|
|
|
+ fileType: 'pdf',
|
|
|
+ showMenu: true,
|
|
|
+ success: () => {
|
|
|
+ // 预览成功
|
|
|
},
|
|
|
- fail: () => {
|
|
|
- startDetection()
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('打开文档失败:', err)
|
|
|
+ uni.showToast({
|
|
|
+ title: '打开文档失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
+// 关闭PDF缩略图
|
|
|
+const handleClosePdf = () => {
|
|
|
+ showPdfThumbnail.value = false
|
|
|
+ pdfFilePath.value = ''
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@@ -507,15 +617,6 @@ const handleSelectFile = () => {
|
|
|
flex: 1;
|
|
|
width: 100%;
|
|
|
position: relative;
|
|
|
-
|
|
|
- // 边框检测画布
|
|
|
- .border-canvas {
|
|
|
- position: absolute;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- z-index: 10;
|
|
|
- pointer-events: none;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
// 模式切换按钮 - 在底部操作栏上方
|
|
|
@@ -561,6 +662,167 @@ const handleSelectFile = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 多页模式缩略图列表
|
|
|
+ .thumbnail-list {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 220rpx;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ height: 160rpx;
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
+ backdrop-filter: blur(10rpx);
|
|
|
+
|
|
|
+ .thumbnail-scroll {
|
|
|
+ height: 100%;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ .thumbnail-wrapper {
|
|
|
+ display: inline-flex;
|
|
|
+ padding: 20rpx;
|
|
|
+ gap: 16rpx;
|
|
|
+
|
|
|
+ .thumbnail-item {
|
|
|
+ position: relative;
|
|
|
+ width: 120rpx;
|
|
|
+ height: 120rpx;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 2rpx solid #e0e0e0;
|
|
|
+
|
|
|
+ .thumbnail-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pdf-thumbnail-icon {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #f5f5f5;
|
|
|
+
|
|
|
+ .pdf-icon-img {
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumbnail-delete {
|
|
|
+ position: absolute;
|
|
|
+ top: 4rpx;
|
|
|
+ right: 4rpx;
|
|
|
+ width: 36rpx;
|
|
|
+ height: 36rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ .delete-icon {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .thumbnail-index {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 4rpx;
|
|
|
+ left: 4rpx;
|
|
|
+ padding: 2rpx 8rpx;
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ border-radius: 8rpx;
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传按钮
|
|
|
+ .upload-btn {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 240rpx;
|
|
|
+ right: 32rpx;
|
|
|
+ width: 120rpx;
|
|
|
+ height: 80rpx;
|
|
|
+ background: #1ec9c9;
|
|
|
+ border-radius: 40rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-shadow: 0 4rpx 16rpx rgba(30, 201, 201, 0.4);
|
|
|
+ z-index: 20;
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ transform: scale(0.95);
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #ffffff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // PDF缩略图
|
|
|
+ .pdf-thumbnail {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 240rpx;
|
|
|
+ right: 32rpx;
|
|
|
+ width: 140rpx;
|
|
|
+ height: 140rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 20;
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ transform: scale(0.95);
|
|
|
+ }
|
|
|
+
|
|
|
+ .pdf-close {
|
|
|
+ position: absolute;
|
|
|
+ top: -8rpx;
|
|
|
+ right: -8rpx;
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #ff4444;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(255, 68, 68, 0.3);
|
|
|
+
|
|
|
+ .close-icon {
|
|
|
+ font-size: 36rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ line-height: 1;
|
|
|
+ font-weight: 300;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pdf-icon {
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pdf-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #666666;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 底部操作按钮
|
|
|
.action-buttons {
|
|
|
position: absolute;
|