daShangDao_scanBook/pages/upload/camera_capture.vue

559 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="cc-page">
<!-- 摄像头预览区 -->
<view class="cc-camera-wrap">
<!-- lime-camera 预览Android nvue / uni-app x -->
<l-camera
v-if="useLimeCamera"
ref="lCamera"
class="cc-lcamera"
mode="normal"
:device-position="cameraPosition"
flash="off"
:focus="true"
@initdone="onCameraInitDone"
@error="onCameraError"
style="width:100%;height:100%;"
></l-camera>
<!-- WebRTC 预览H5 -->
<video
v-else-if="useWebRTC"
id="ccVideo"
ref="ccVideo"
class="cc-video"
autoplay
playsinline
muted
></video>
<!-- 无预览时占位 -->
<view v-else class="cc-camera-hint">
<text class="cc-hint-text">相机加载中...</text>
</view>
<canvas id="ccCanvas" ref="ccCanvas" class="cc-canvas" style="display:none;"></canvas>
<!-- 连拍模式提示遮罩 -->
<view class="cc-burst-overlay" v-if="isBurstMode">
<text class="cc-burst-tip">连拍中⋯</text>
<text class="cc-burst-count">{{ capturedList.length }} 张</text>
<text class="cc-burst-dots">
<text class="cc-dot" v-for="n in 3" :key="n">.</text>
</text>
<text class="cc-burst-hint">点击红色按钮停止</text>
</view>
</view>
<!-- 已拍照九宫格 -->
<view class="cc-thumb-bar">
<view class="cc-thumb-count" v-if="capturedList.length > 0">已拍 {{ capturedList.length }} 张</view>
<view class="cc-thumb-list">
<view class="cc-thumb-item" v-for="(img, idx) in capturedList" :key="idx">
<image class="cc-thumb-img" :src="img" mode="aspectFill"></image>
<view class="cc-thumb-del" @click.stop="deletePhoto(idx)">
<text class="cc-del-icon"></text>
</view>
</view>
<view class="cc-thumb-item cc-thumb-placeholder" v-for="n in (9 - capturedList.length)" :key="'p'+n"></view>
</view>
</view>
<!-- 底部操作区 -->
<view class="cc-footer">
<!-- 连拍模式 - 停止按钮 -->
<view class="cc-stop-btn" @click="stopBurst" v-if="isBurstMode">
<view class="cc-stop-inner">■</view>
</view>
<!-- 普通模式 - 拍照按钮 -->
<view class="cc-capture-btn" @click="capturePhoto" v-else :class="{ disabled: capturedList.length >= 9 }">
<view class="cc-capture-inner"></view>
</view>
<view class="cc-right-actions">
<view class="cc-burst-toggle" @click="toggleBurstMode" v-if="!isBurstMode && capturedList.length < 9">
<text class="cc-burst-toggle-text">⚡连拍</text>
</view>
<view class="cc-confirm-btn" @click="confirmCapture" :class="{ disabled: capturedList.length === 0 }">
<text class="cc-confirm-text">确认 ({{ capturedList.length }})</text>
</view>
</view>
</view>
</view>
</template>
<script>
// 尝试导入 lime-cameraAndroid nvue 环境使用)
var limeCameraReady = false
var createCameraContext = null
try {
var limeModule = require('@/uni_modules/lime-camera')
createCameraContext = limeModule.createCameraContext
limeCameraReady = true
} catch (e) {
console.log('lime-camera 插件未安装,使用备用方案')
limeCameraReady = false
}
export default {
data() {
return {
capturedList: [],
mediaStream: null,
ctxReady: false,
useWebRTC: false,
useLimeCamera: false,
cameraContext: null,
cameraPosition: 'back',
isBurstMode: false,
burstTimer: null
}
},
onReady() {
this.checkEnvironment()
},
onUnload() {
this.stopCamera()
this.clearBurstTimer()
},
methods: {
// 检测运行环境
checkEnvironment() {
// 1. 优先尝试 lime-cameraAndroid nvue
if (limeCameraReady) {
this.useLimeCamera = true
this.ctxReady = true
console.log('使用 lime-camera 相机')
return
}
// 2. WebRTCH5
try {
if (typeof document !== 'undefined' && typeof navigator !== 'undefined' && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
this.useWebRTC = true
this.startWebRTC()
return
}
} catch (e) {
// fall through
}
// 3. 无可用环境
this.useWebRTC = false
this.ctxReady = false
uni.showToast({ title: '当前环境不支持相机预览', icon: 'none' })
},
// lime-camera 初始化完成
onCameraInitDone(e) {
console.log('lime-camera 初始化完成,最大缩放:', e.detail.maxZoom)
this.cameraContext = createCameraContext()
this.ctxReady = true
},
// lime-camera 错误
onCameraError(err) {
console.error('lime-camera 错误:', err)
uni.showToast({ title: '相机启动失败: ' + (err.detail ? JSON.stringify(err.detail) : '未知错误'), icon: 'none' })
this.useLimeCamera = false
this.checkEnvironment()
},
// WebRTC 启动摄像头H5 环境)
startWebRTC() {
var that = this
try {
if (typeof document === 'undefined') {
this.ctxReady = true
return
}
var video = document.getElementById('ccVideo')
if (!video) {
this.ctxReady = true
return
}
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: 1280, height: 720 } })
.then(function(stream) {
that.mediaStream = stream
video.srcObject = stream
video.play()
that.ctxReady = true
console.log('WebRTC摄像头已就绪')
})
.catch(function(err) {
console.error('getUserMedia失败:', err)
that.ctxReady = true
})
} catch (e) {
console.error('摄像头启动异常:', e)
this.ctxReady = true
}
},
stopCamera() {
this.clearBurstTimer()
if (this.mediaStream) {
var tracks = this.mediaStream.getTracks()
for (var i = 0; i < tracks.length; i++) {
tracks[i].stop()
}
this.mediaStream = null
}
},
toggleBurstMode() {
if (this.capturedList.length >= 9) {
uni.showToast({ title: '最多拍9张', icon: 'none' })
return
}
this.startBurst()
},
startBurst() {
this.isBurstMode = true
this.doBurstCapture()
},
doBurstCapture() {
if (!this.isBurstMode || this.capturedList.length >= 9) {
this.isBurstMode = false
return
}
var that = this
this.captureSinglePhoto(function() {
// 间隔 1.5 秒继续
that.burstTimer = setTimeout(function() {
that.doBurstCapture()
}, 1500)
})
},
stopBurst() {
this.isBurstMode = false
this.clearBurstTimer()
},
clearBurstTimer() {
if (this.burstTimer) {
clearTimeout(this.burstTimer)
this.burstTimer = null
}
},
// 拍摄单张照片
capturePhoto() {
if (this.capturedList.length >= 9) {
uni.showToast({ title: '最多拍9张', icon: 'none' })
return
}
this.captureSinglePhoto()
},
// 底层拍照逻辑(带可选回调)
captureSinglePhoto(callback) {
// 1. lime-camera 拍照Android 内联相机,带预览)
if (this.useLimeCamera && this.cameraContext) {
this.limeCapture(callback)
return
}
// 2. WebRTC 截图H5
if (this.useWebRTC && this.mediaStream && this.ctxReady) {
this.webRTCCapture(callback)
return
}
// 3. 系统相机(无预览,备用)
this.systemCapture(callback)
},
// lime-camera 拍照
limeCapture(callback) {
var that = this
try {
this.cameraContext.takePhoto({
quality: 'high',
success: function(res) {
that.capturedList.push(res.tempImagePath)
if (callback) { callback() }
},
fail: function(err) {
console.error('lime拍照失败:', err)
if (that.isBurstMode) {
that.isBurstMode = false
}
uni.showToast({ title: '拍照失败', icon: 'none' })
if (callback) { callback() }
}
})
} catch (e) {
console.error('lime拍照异常:', e)
if (callback) { callback() }
}
},
// WebRTC 截图
webRTCCapture(callback) {
if (typeof document === 'undefined') {
this.systemCapture(callback)
return
}
var canvas = document.getElementById('ccCanvas')
var video = document.getElementById('ccVideo')
if (!canvas || !video) {
this.systemCapture(callback)
return
}
canvas.width = video.videoWidth || 1280
canvas.height = video.videoHeight || 720
var ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
var dataUrl = canvas.toDataURL('image/jpeg', 0.85)
this.capturedList.push(dataUrl)
if (callback) { callback() }
},
// 系统相机拍照(无预览,备用)
systemCapture(callback) {
var that = this
uni.chooseImage({
count: 1,
sourceType: ['camera'],
sizeType: ['original'],
success: function(res) {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
that.capturedList.push(res.tempFilePaths[0])
}
if (callback) { callback() }
},
fail: function() {
if (that.isBurstMode) {
that.isBurstMode = false
}
if (callback) { callback() }
}
})
},
deletePhoto(idx) {
this.capturedList.splice(idx, 1)
},
confirmCapture() {
if (this.capturedList.length === 0) {
uni.showToast({ title: '请先拍照', icon: 'none' })
return
}
this.stopCamera()
var pages = getCurrentPages()
var prevPage = pages[pages.length - 2]
if (prevPage) {
prevPage.$vm.capturedPhotoList = this.capturedList
}
uni.navigateBack()
}
}
}
</script>
<style>
.cc-page {
display: flex;
flex-direction: column;
height: 100vh;
background: #000;
overflow: hidden;
}
.cc-camera-wrap {
flex: 1;
position: relative;
overflow: hidden;
background: #111;
display: flex;
align-items: center;
justify-content: center;
}
.cc-lcamera {
width: 100%;
height: 100%;
}
.cc-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.cc-canvas {
display: none;
}
.cc-camera-hint {
text-align: center;
padding: 40rpx;
}
.cc-hint-text {
color: #999;
font-size: 28rpx;
}
/* 连拍模式遮罩 */
.cc-burst-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.25);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 5;
pointer-events: none;
}
.cc-burst-tip {
color: #fff;
font-size: 40rpx;
font-weight: bold;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
}
.cc-burst-count {
color: rgba(255,255,255,0.9);
font-size: 32rpx;
margin-top: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
}
.cc-burst-dots {
color: #409eff;
font-size: 60rpx;
margin-top: 10rpx;
animation: burstBlink 0.8s ease-in-out infinite;
}
@keyframes burstBlink {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.cc-burst-hint {
color: rgba(255,255,255,0.6);
font-size: 24rpx;
margin-top: 20rpx;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
}
.cc-dot {
margin: 0 4rpx;
}
.cc-thumb-bar {
background: #1a1a1a;
padding: 16rpx 20rpx;
}
.cc-thumb-count {
color: #999;
font-size: 22rpx;
margin-bottom: 12rpx;
}
.cc-thumb-list {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.cc-thumb-item {
width: 100rpx;
height: 100rpx;
border-radius: 10rpx;
overflow: hidden;
position: relative;
background: #333;
}
.cc-thumb-img {
width: 100%;
height: 100%;
}
.cc-thumb-del {
position: absolute;
top: -6rpx;
right: -6rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
}
.cc-del-icon {
color: #fff;
font-size: 18rpx;
}
.cc-thumb-placeholder {
border: 2rpx dashed #444;
box-sizing: border-box;
}
.cc-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 40rpx;
padding-bottom: calc(30rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
background: #1a1a1a;
}
.cc-capture-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 6rpx solid #fff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.cc-capture-inner {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #fff;
}
.cc-capture-btn:active .cc-capture-inner {
background: #ccc;
}
.cc-capture-btn.disabled {
opacity: 0.4;
}
/* 连拍停止按钮 */
.cc-stop-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 6rpx solid #ff4444;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: rgba(255,68,68,0.15);
}
.cc-stop-inner {
color: #ff4444;
font-size: 32rpx;
font-weight: bold;
}
.cc-right-actions {
display: flex;
align-items: center;
gap: 20rpx;
}
/* 连拍模式切换 */
.cc-burst-toggle {
padding: 16rpx 24rpx;
border-radius: 32rpx;
border: 2rpx solid #409eff;
background: transparent;
}
.cc-burst-toggle-text {
color: #409eff;
font-size: 24rpx;
font-weight: bold;
}
.cc-confirm-btn {
padding: 18rpx 40rpx;
border-radius: 40rpx;
background: #409eff;
}
.cc-confirm-btn.disabled {
background: #555;
}
.cc-confirm-text {
color: #fff;
font-size: 28rpx;
font-weight: bold;
}
</style>