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

@ -374,9 +374,15 @@
</template>
</el-dialog>
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile"/>
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%"><img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /></el-dialog>
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close>
<WebRtcCamera
ref="cameraRef"
@confirm="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
<div style="text-align: center;">
@ -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; }
</style>
</style>

View File

@ -317,11 +317,17 @@
</template>
</el-dialog>
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile" />
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%">
<img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close>
<WebRtcCamera
ref="cameraRef"
@confirm="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
<div style="text-align: center;">
@ -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<any[]>([]) // 成品实拍
const qualityFileList = ref<any[]>([]) // 质量报告
const inspectionFileList = ref<any[]>([]) // 检测报告
const cameraInputRef = ref<HTMLInputElement | null>(null)
const cameraDialogVisible = ref(false)
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | 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; }
</style>
</style>

View File

@ -409,8 +409,14 @@
</template>
</el-dialog>
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile"/>
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%"><img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /></el-dialog>
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close>
<WebRtcCamera
ref="cameraRef"
@confirm="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
<div style="text-align: center;">
<div v-loading="printLoading" class="preview-box">
@ -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<any[]>([])
const reportFileList = ref<any[]>([])
const cameraInputRef = ref<HTMLInputElement | null>(null)
const cameraDialogVisible = ref(false)
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | 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; }
</style>
</style>