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

@ -333,8 +333,14 @@
<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>
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile"/>
</el-card>
</div>
@ -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<HTMLInputElement | null>(null);
const cameraDialogVisible = ref(false);
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | 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); }
</style>
</style>