feat: add WebRTC camera component for in-app photo capture
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
134
inventory-web/src/components/Camera/WebRtcCamera.vue
Normal file
134
inventory-web/src/components/Camera/WebRtcCamera.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="camera-container">
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<video ref="videoRef" autoplay playsinline class="video-preview"></video>
|
||||
<canvas ref="canvasRef" style="display: none;"></canvas>
|
||||
</div>
|
||||
<div class="camera-actions">
|
||||
<el-button @click="handleCancel" size="large">取消</el-button>
|
||||
<el-button type="primary" @click="capture" size="large" :disabled="!isCameraReady">拍照</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
visible?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [file: File]
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const videoRef = ref<HTMLVideoElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const mediaStream = ref<MediaStream>()
|
||||
const error = ref('')
|
||||
const isCameraReady = ref(false)
|
||||
|
||||
const startCamera = async () => {
|
||||
stopCamera()
|
||||
error.value = ''
|
||||
try {
|
||||
const constraints = {
|
||||
video: { facingMode: 'environment' }
|
||||
}
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints)
|
||||
mediaStream.value = stream
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = stream
|
||||
await videoRef.value.play()
|
||||
isCameraReady.value = true
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = '无法访问摄像头: ' + (err.message || '请检查权限或连接')
|
||||
ElMessage.error(error.value)
|
||||
emit('cancel')
|
||||
}
|
||||
}
|
||||
|
||||
const stopCamera = () => {
|
||||
if (mediaStream.value) {
|
||||
mediaStream.value.getTracks().forEach(track => track.stop())
|
||||
mediaStream.value = undefined
|
||||
}
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = null
|
||||
}
|
||||
isCameraReady.value = false
|
||||
}
|
||||
|
||||
const capture = () => {
|
||||
const video = videoRef.value
|
||||
const canvas = canvasRef.value
|
||||
if (!video || !canvas) return
|
||||
|
||||
const width = video.videoWidth
|
||||
const height = video.videoHeight
|
||||
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
ctx.drawImage(video, 0, 0, width, height)
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return
|
||||
const file = new File([blob], 'camera.jpg', { type: 'image/jpeg' })
|
||||
emit('confirm', file)
|
||||
stopCamera()
|
||||
}, 'image/jpeg', 0.95)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
stopCamera()
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startCamera()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopCamera()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
startCamera,
|
||||
stopCamera
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.camera-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.video-preview {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 360px;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.error-message {
|
||||
color: #f56c6c;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.camera-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user