From 107c3113914f5b2f228cde8e7a8322cd9b2134b5 Mon Sep 17 00:00:00 2001 From: dxc Date: Mon, 9 Feb 2026 16:57:47 +0800 Subject: [PATCH] feat: add WebRTC camera component for in-app photo capture Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) --- .../src/components/Camera/WebRtcCamera.vue | 134 ++++++++++++++++++ inventory-web/src/views/material/list.vue | 64 ++++++--- inventory-web/src/views/stock/inbound/buy.vue | 58 +++++--- .../src/views/stock/inbound/product.vue | 65 ++++++--- .../src/views/stock/inbound/semi.vue | 66 ++++++--- 5 files changed, 303 insertions(+), 84 deletions(-) create mode 100644 inventory-web/src/components/Camera/WebRtcCamera.vue diff --git a/inventory-web/src/components/Camera/WebRtcCamera.vue b/inventory-web/src/components/Camera/WebRtcCamera.vue new file mode 100644 index 0000000..3d7d77c --- /dev/null +++ b/inventory-web/src/components/Camera/WebRtcCamera.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/inventory-web/src/views/material/list.vue b/inventory-web/src/views/material/list.vue index 1ac4173..1e93160 100644 --- a/inventory-web/src/views/material/list.vue +++ b/inventory-web/src/views/material/list.vue @@ -333,8 +333,14 @@ Preview Image + + + - @@ -353,6 +359,7 @@ import { delMaterialBase } from '@/api/material_base'; import { uploadFile, deleteFile } from '@/api/common/upload'; // 假设通用上传接口在此 +import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'; // --- 类型定义 --- interface MaterialBaseVO { @@ -393,7 +400,8 @@ const imageExternalUrl = ref(''); const manualExternalUrl = ref(''); const dialogVisibleImage = ref(false); const dialogImageUrl = ref(''); -const cameraInputRef = ref(null); +const cameraDialogVisible = ref(false); +const cameraRef = ref | null>(null); const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage'); @@ -739,29 +747,39 @@ const handlePreviewPicture = (uploadFile: any) => { const triggerCamera = (field: 'generalImage' | 'generalManual') => { currentCameraField.value = field; - if (cameraInputRef.value) cameraInputRef.value.click() + cameraDialogVisible.value = true; } -const handleCameraFile = async (event: Event) => { - const input = event.target as HTMLInputElement - if (input.files && input.files[0]) { - const file = input.files[0] - if (!beforeAvatarUpload(file)) { input.value = ''; return } - const formData = new FormData(); formData.append('file', file) - const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }) - try { - const res: any = await uploadFile(formData) - if (res.code === 200) { - const newUrl = res.data.url; - const field = currentCameraField.value - form.value[field].push(newUrl) - if (field === 'generalImage') fileListImage.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - else fileListManual.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - ElMessage.success('拍照上传成功') - } else { ElMessage.error(res.msg || '上传失败') } - } catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' } +const handleCameraConfirm = async (file: File) => { + if (!beforeAvatarUpload(file)) { + cameraDialogVisible.value = false; + return; } -} + const formData = new FormData(); + formData.append('file', file); + const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }); + try { + const res: any = await uploadFile(formData); + if (res.code === 200) { + const newUrl = res.data.url; + const field = currentCameraField.value; + form.value[field].push(newUrl); + if (field === 'generalImage') { + fileListImage.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } else if (field === 'generalManual') { + fileListManual.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } + ElMessage.success('拍照上传成功'); + } else { + ElMessage.error(res.msg || '上传失败'); + } + } catch (e) { + ElMessage.error('网络错误,上传失败'); + } finally { + loadingMsg.close(); + cameraDialogVisible.value = false; + } +}; onMounted(() => { getList(); @@ -806,4 +824,4 @@ onMounted(() => { /* 表格缩略图样式 */ .file-preview-cell { display: flex; align-items: center; justify-content: center; position: relative; } .more-badge { position: absolute; top: -5px; right: -5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 4px; font-size: 10px; transform: scale(0.9); } - \ No newline at end of file + diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index 3fa248d..a26622c 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -374,9 +374,15 @@ - Preview Image + + +
@@ -408,6 +414,7 @@ import { deleteFile } from '@/api/inbound/buy' import {getLabelPreview, executePrint} from '@/api/common/print' +import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' // ------------------------------------ // 状态与变量 @@ -779,25 +786,36 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | } const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true } const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() } -const handleCameraFile = async (event: Event) => { - const input = event.target as HTMLInputElement - if (input.files && input.files[0]) { - const file = input.files[0] - if (!beforeAvatarUpload(file)) { input.value = ''; return } - const formData = new FormData(); formData.append('file', file) - const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }) - try { - const res: any = await uploadFile(formData) - if (res.code === 200) { - const newUrl = res.data.url; const field = currentCameraField.value - form[field].push(newUrl) - if (field === 'arrival_photo') arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - else reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - ElMessage.success('拍照上传成功') - } else { ElMessage.error(res.msg || '上传失败') } - } catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' } +const handleCameraConfirm = async (file: File) => { + if (!beforeAvatarUpload(file)) { + cameraDialogVisible.value = false; + return; } -} + const formData = new FormData(); + formData.append('file', file); + const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }); + try { + const res: any = await uploadFile(formData); + if (res.code === 200) { + const newUrl = res.data.url; + const field = currentCameraField.value; + form[field].push(newUrl); + if (field === 'arrival_photo') { + arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } else if (field === 'inspection_report') { + reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } + ElMessage.success('拍照上传成功'); + } else { + ElMessage.error(res.msg || '上传失败'); + } + } catch (e) { + ElMessage.error('网络错误,上传失败'); + } finally { + loadingMsg.close(); + cameraDialogVisible.value = false; + } +}; const handleDelete = async (row: any) => { try { await deleteBuyInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } } const handlePrint = async (row: any) => { printVisible.value = true; printLoading.value = true; previewUrl.value = '' @@ -872,4 +890,4 @@ onMounted(() => fetchData()) .camera-card:hover { border-color: #409EFF; color: #409EFF; } .camera-card .text { font-size: 12px; margin-top: 5px; } .camera-card .el-icon { font-size: 24px; } - \ No newline at end of file + diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index 0a69a7d..5d580e9 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -317,11 +317,17 @@ - Preview Image + + +
@@ -351,6 +357,7 @@ import { ElMessage } from 'element-plus' import dayjs from 'dayjs' import { getProductList, createProductInbound, updateProductInbound, deleteProductInbound, searchMaterialBase } from '@/api/inbound/product' import { uploadFile, deleteFile } from '@/api/inbound/buy' +import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' import { getLabelPreview, executePrint } from '@/api/common/print' const loading = ref(false) @@ -379,7 +386,8 @@ const productPhotoList = ref([]) // 成品实拍 const qualityFileList = ref([]) // 质量报告 const inspectionFileList = ref([]) // 检测报告 -const cameraInputRef = ref(null) +const cameraDialogVisible = ref(false) +const cameraRef = ref | null>(null) const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspection_report_link'>('product_photo') const quality_url = ref('') const inspection_url = ref('') @@ -558,23 +566,42 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'product_photo' | ElMessage.success('已删除') } catch (e) { console.error(e) } } -const triggerCamera = (field: any) => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() } -const handleCameraFile = async (event: Event) => { - const input = event.target as HTMLInputElement; if (input.files && input.files[0]) { - const file = input.files[0]; if (!beforeAvatarUpload(file)) { input.value = ''; return } - const formData = new FormData(); formData.append('file', file); const loadingMsg = ElMessage.loading({ message: '上传中...', duration: 0 }) - try { - const res: any = await uploadFile(formData) - if (res.code === 200) { - const newUrl = res.data.url; const field = currentCameraField.value; form[field].push(newUrl) - if (field === 'product_photo') productPhotoList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - else if (field === 'quality_report_link') qualityFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - else inspectionFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - ElMessage.success('拍照上传成功') - } else { ElMessage.error(res.msg || '上传失败') } - } catch (e) { ElMessage.error('网络错误') } finally { loadingMsg.close(); input.value = '' } - } +const triggerCamera = (field: any) => { + currentCameraField.value = field; + cameraDialogVisible.value = true; } +const handleCameraConfirm = async (file: File) => { + if (!beforeAvatarUpload(file)) { + cameraDialogVisible.value = false; + return; + } + const formData = new FormData(); + formData.append('file', file); + const loadingMsg = ElMessage.loading({ message: '上传中...', duration: 0 }); + try { + const res: any = await uploadFile(formData); + if (res.code === 200) { + const newUrl = res.data.url; + const field = currentCameraField.value; + form[field].push(newUrl); + if (field === 'product_photo') { + productPhotoList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } else if (field === 'quality_report_link') { + qualityFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } else if (field === 'inspection_report_link') { + inspectionFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } + ElMessage.success('拍照上传成功'); + } else { + ElMessage.error(res.msg || '上传失败'); + } + } catch (e) { + ElMessage.error('网络错误'); + } finally { + loadingMsg.close(); + cameraDialogVisible.value = false; + } +}; const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true } const submitForm = async () => { @@ -664,4 +691,4 @@ onMounted(() => fetchData()) .camera-card:hover { border-color: #409EFF; color: #409EFF; } .camera-card .text { font-size: 12px; margin-top: 5px; } .camera-card .el-icon { font-size: 24px; } - \ No newline at end of file + diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue index c9e205f..27069ee 100644 --- a/inventory-web/src/views/stock/inbound/semi.vue +++ b/inventory-web/src/views/stock/inbound/semi.vue @@ -409,8 +409,14 @@ - Preview Image + + +
@@ -439,6 +445,7 @@ import { searchMaterialBase } from '@/api/inbound/semi' import { uploadFile, deleteFile } from '@/api/inbound/buy' +import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' import {getLabelPreview, executePrint} from '@/api/common/print' // ------------------------------------ @@ -467,7 +474,8 @@ const dialogImageUrl = ref('') const dialogVisibleImage = ref(false) const arrivalFileList = ref([]) const reportFileList = ref([]) -const cameraInputRef = ref(null) +const cameraDialogVisible = ref(false) +const cameraRef = ref | null>(null) const currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo') const quality_report_url = ref('') @@ -734,26 +742,40 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | } catch (e) { console.error(e) } } const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true } -const triggerCamera = (field: 'arrival_photo' | 'quality_report_link') => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() } -const handleCameraFile = async (event: Event) => { - const input = event.target as HTMLInputElement - if (input.files && input.files[0]) { - const file = input.files[0] - if (!beforeAvatarUpload(file)) { input.value = ''; return } - const formData = new FormData(); formData.append('file', file) - const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }) - try { - const res: any = await uploadFile(formData) - if (res.code === 200) { - const newUrl = res.data.url; const field = currentCameraField.value - form[field].push(newUrl) - if (field === 'arrival_photo') arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - else reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - ElMessage.success('拍照上传成功') - } else { ElMessage.error(res.msg || '上传失败') } - } catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' } - } +const triggerCamera = (field: 'arrival_photo' | 'quality_report_link') => { + currentCameraField.value = field; + cameraDialogVisible.value = true; } +const handleCameraConfirm = async (file: File) => { + if (!beforeAvatarUpload(file)) { + cameraDialogVisible.value = false; + return; + } + const formData = new FormData(); + formData.append('file', file); + const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }); + try { + const res: any = await uploadFile(formData); + if (res.code === 200) { + const newUrl = res.data.url; + const field = currentCameraField.value; + form[field].push(newUrl); + if (field === 'arrival_photo') { + arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } else if (field === 'quality_report_link') { + reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }); + } + ElMessage.success('拍照上传成功'); + } else { + ElMessage.error(res.msg || '上传失败'); + } + } catch (e) { + ElMessage.error('网络错误,上传失败'); + } finally { + loadingMsg.close(); + cameraDialogVisible.value = false; + } +}; const submitForm = async () => { if (!formRef.value) return @@ -853,4 +875,4 @@ onMounted(() => fetchData()) .camera-card:hover { border-color: #409EFF; color: #409EFF; } .camera-card .text { font-size: 12px; margin-top: 5px; } .camera-card .el-icon { font-size: 24px; } - \ No newline at end of file +