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:
dxc
2026-02-09 16:57:47 +08:00
parent 5c8cefdb69
commit 107c311391
5 changed files with 303 additions and 84 deletions

View 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>