Compare commits

4 Commits

Author SHA1 Message Date
dxc
a0ed92319c 修改拍照上传逻辑,避免平板不可以调用照相机 2026-02-10 09:27:52 +08:00
dxc
d4b23790a1 fix: only close camera dialog on successful upload
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-09 17:15:50 +08:00
dxc
aee0fc4380 inventory-web/src/views/stock/inbound/buy.vue
```python
<<<<<<< SEARCH
const cameraInputRef = ref<HTMLInputElement | null>(null)
const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
=======
const cameraDialogVisible = ref(false)
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null)
const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
>>>>>>> REPLACE
```

inventory-web/src/views/stock/inbound/buy.vue
```python
<<<<<<< SEARCH
const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() }
=======
const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => {
  currentCameraField.value = field;
  cameraDialogVisible.value = true;
}
>>>>>>> REPLACE
```

Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-09 17:02:02 +08:00
dxc
107c311391 feat: add WebRTC camera component for in-app photo capture
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-09 16:57:47 +08:00
5 changed files with 506 additions and 150 deletions

View File

@ -0,0 +1,240 @@
<template>
<div class="camera-container">
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-else class="media-box">
<video
v-show="!imgSrc"
ref="videoRef"
autoplay
playsinline
class="media-content"
></video>
<img
v-if="imgSrc"
:src="imgSrc"
class="media-content preview-img"
alt="Photo Preview"
/>
<canvas ref="canvasRef" style="display: none;"></canvas>
</div>
<div class="camera-actions">
<template v-if="!imgSrc">
<el-button @click="handleCancel" size="large">取消</el-button>
<el-button
type="primary"
@click="capture"
size="large"
:disabled="!isCameraReady"
>
<el-icon class="el-icon--left"><Camera /></el-icon>
拍照
</el-button>
</template>
<template v-else>
<el-button @click="retake" size="large" type="warning" plain>
<el-icon class="el-icon--left"><RefreshLeft /></el-icon>
重拍
</el-button>
<el-button
type="success"
@click="confirmUse"
size="large"
>
<el-icon class="el-icon--left"><Check /></el-icon>
确认使用
</el-button>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { Camera, RefreshLeft, Check } from '@element-plus/icons-vue'
const emit = defineEmits(['photo-submit', 'cancel'])
const videoRef = ref<HTMLVideoElement>()
const canvasRef = ref<HTMLCanvasElement>()
const mediaStream = ref<MediaStream>()
const error = ref('')
const isCameraReady = ref(false)
const imgSrc = ref('')
const currentFile = ref<File | null>(null)
const startCamera = async () => {
stopCamera()
error.value = ''
imgSrc.value = ''
currentFile.value = null
try {
const constraints = {
video: {
facingMode: 'environment',
width: { ideal: 1920 },
height: { ideal: 1080 }
}
}
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) {
console.error(err)
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
error.value = '无法访问摄像头: 浏览器限制非 HTTPS 环境调用摄像头,请使用 localhost 或 配置 HTTPS。'
} else {
error.value = '无法访问摄像头: ' + (err.message || '请检查权限')
}
ElMessage.error(error.value)
}
}
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
if (width === 0 || height === 0) return
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) {
ElMessage.error('照片生成失败,请重试')
return
}
const timestamp = new Date().getTime()
const filename = `photo_${timestamp}.jpg`
const file = new File([blob], filename, { type: 'image/jpeg' })
currentFile.value = file
imgSrc.value = URL.createObjectURL(blob)
console.log('✅ 照片已捕获:', filename, (file.size / 1024).toFixed(2) + 'KB')
}, 'image/jpeg', 0.95)
}
const retake = () => {
if (imgSrc.value) {
URL.revokeObjectURL(imgSrc.value)
}
imgSrc.value = ''
currentFile.value = null
if (!mediaStream.value || !mediaStream.value.active) {
startCamera()
}
}
const confirmUse = () => {
console.log('👆 点击了确认使用')
if (currentFile.value) {
try {
console.log('📤 发送 photo-submit 事件', currentFile.value)
emit('photo-submit', currentFile.value)
} catch (err) {
console.error('父组件处理事件失败:', err)
}
} else {
ElMessage.warning('照片数据未就绪,请稍后或重拍')
}
}
const handleCancel = () => {
stopCamera()
emit('cancel')
}
onMounted(() => {
startCamera()
})
onBeforeUnmount(() => {
stopCamera()
if (imgSrc.value) {
URL.revokeObjectURL(imgSrc.value)
}
})
defineExpose({
startCamera,
stopCamera
})
</script>
<style scoped>
.camera-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
}
.media-box {
width: 100%;
max-width: 640px;
height: 400px;
background: #000;
border-radius: 8px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.media-content {
width: 100%;
height: 100%;
object-fit: contain;
}
.error-message {
color: #f56c6c;
padding: 40px 20px;
text-align: center;
background: #fef0f0;
border-radius: 8px;
width: 100%;
}
.camera-actions {
margin-top: 20px;
display: flex;
gap: 20px;
justify-content: center;
width: 100%;
}
</style>

View File

@ -148,12 +148,14 @@
<el-link v-if="isExternalLink(link)" :href="link" target="_blank" type="primary" :underline="false"> <el-link v-if="isExternalLink(link)" :href="link" target="_blank" type="primary" :underline="false">
说明书 {{idx+1}} <el-icon><Link /></el-icon> 说明书 {{idx+1}} <el-icon><Link /></el-icon>
</el-link> </el-link>
<el-image v-else <el-image v-else-if="isImageFile(link)"
style="width: 100px; height: 100px" style="width: 100px; height: 100px"
:src="getImageUrl(link)" :src="getImageUrl(link)"
:preview-src-list="[getImageUrl(link)]" :preview-src-list="[getImageUrl(link)]"
fit="cover" fit="cover"
preview-teleported
/> />
<el-link v-else :href="getImageUrl(link)" target="_blank" type="info">PDF 文件 {{idx+1}}</el-link>
</div> </div>
</div> </div>
</el-popover> </el-popover>
@ -333,9 +335,13 @@
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%"> <el-dialog v-model="dialogVisibleImage" append-to-body width="50%">
<img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /> <img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" />
</el-dialog> </el-dialog>
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close :close-on-click-modal="false">
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile"/> <WebRtcCamera
ref="cameraRef"
@photo-submit="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
</el-card> </el-card>
</div> </div>
</template> </template>
@ -343,7 +349,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'; import { ref, reactive, onMounted, nextTick } from 'vue';
import { Plus, Picture, Document, Refresh, Setting, Rank, Camera, Link } from '@element-plus/icons-vue'; import { Plus, Picture, Document, Refresh, Setting, Rank, Camera, Link } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus'; // 修复:引入 ElLoading
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { import {
@ -352,7 +359,8 @@ import {
updateMaterialBase, updateMaterialBase,
delMaterialBase delMaterialBase
} from '@/api/material_base'; } from '@/api/material_base';
import { uploadFile, deleteFile } from '@/api/common/upload'; // 假设通用上传接口在此 import { uploadFile, deleteFile } from '@/api/common/upload';
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
// --- 类型定义 --- // --- 类型定义 ---
interface MaterialBaseVO { interface MaterialBaseVO {
@ -364,8 +372,8 @@ interface MaterialBaseVO {
spec: string; spec: string;
unit: string; unit: string;
visibilityLevel: number; visibilityLevel: number;
generalManual: string[]; // 修改为数组 generalManual: string[];
generalImage: string[]; // 修改为数组 generalImage: string[];
isEnabled: number; isEnabled: number;
statusLoading?: boolean; statusLoading?: boolean;
} }
@ -393,10 +401,10 @@ const imageExternalUrl = ref('');
const manualExternalUrl = ref(''); const manualExternalUrl = ref('');
const dialogVisibleImage = ref(false); const dialogVisibleImage = ref(false);
const dialogImageUrl = ref(''); 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'); const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage');
const columns = reactive({ const columns = reactive({
id: { visible: true }, id: { visible: true },
name: { visible: true }, name: { visible: true },
@ -439,8 +447,8 @@ const initForm = {
spec: '', spec: '',
unit: '', unit: '',
visibilityLevel: 0, visibilityLevel: 0,
generalManual: [] as string[], // 初始化为数组 generalManual: [] as string[],
generalImage: [] as string[], // 初始化为数组 generalImage: [] as string[],
isEnabled: 1 isEnabled: 1
}; };
@ -544,8 +552,8 @@ const handleEdit = (row: MaterialBaseVO) => {
dialog.visible = true; dialog.visible = true;
nextTick(() => { nextTick(() => {
// 基础字段赋值 // 基础字段赋值 - 深拷贝防止引用
Object.assign(form.value, row); Object.assign(form.value, JSON.parse(JSON.stringify(row)));
// 初始化文件列表 // 初始化文件列表
const images = row.generalImage || []; const images = row.generalImage || [];
@ -569,13 +577,13 @@ const handleEdit = (row: MaterialBaseVO) => {
const checkDuplicate = async (name: string, spec: string): Promise<boolean> => { const checkDuplicate = async (name: string, spec: string): Promise<boolean> => {
try { try {
const nameRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: name }); const nameRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: name });
if (nameRes.data?.items?.some((item: MaterialBaseVO) => item.name === name)) { if (nameRes.data?.items?.some((item: MaterialBaseVO) => item.name === name && item.id !== form.value.id)) {
ElMessage.error(`添加失败:已存在名称为 "${name}" 的基础信息!`); ElMessage.error(`已存在名称为 "${name}" 的基础信息!`);
return true; return true;
} }
const specRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: spec }); const specRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: spec });
if (specRes.data?.items?.some((item: MaterialBaseVO) => item.spec === spec)) { if (specRes.data?.items?.some((item: MaterialBaseVO) => item.spec === spec && item.id !== form.value.id)) {
ElMessage.error(`添加失败:已存在规格/编号为 "${spec}" 的基础信息!`); ElMessage.error(`已存在规格/编号为 "${spec}" 的基础信息!`);
return true; return true;
} }
} catch (e) { } catch (e) {
@ -591,36 +599,25 @@ const submitForm = async () => {
if (valid) { if (valid) {
submitLoading.value = true; submitLoading.value = true;
try { try {
if (!form.value.id) { // 重复校验
const isDuplicate = await checkDuplicate(form.value.name, form.value.spec); const isDuplicate = await checkDuplicate(form.value.name, form.value.spec);
if (isDuplicate) { if (isDuplicate) {
submitLoading.value = false; submitLoading.value = false;
return; return;
} }
}
// 整理文件数据 // 整理文件数据:本地上传的路径已经在 form.value 中
const finalImageList = [...form.value.generalImage]; // 我们需要重新合并:已有的非外链 + 新的输入框外链
// 如果输入了外部链接且不在列表中,则加入 const finalImageList = form.value.generalImage.filter(item => !isExternalLink(item));
if (imageExternalUrl.value && !finalImageList.includes(imageExternalUrl.value)) { if (imageExternalUrl.value) finalImageList.push(imageExternalUrl.value);
finalImageList.push(imageExternalUrl.value);
}
// 过滤:只保留上传的(已经在customUpload处理) 和 外部链接
const cleanImages = finalImageList.filter(item => !isExternalLink(item)); // 这里的逻辑需要修正应基于form.value.generalImage已经包含的内容
if (imageExternalUrl.value) cleanImages.push(imageExternalUrl.value);
const finalManualList = form.value.generalManual.filter(item => !isExternalLink(item));
const finalManualList = [...form.value.generalManual]; if (manualExternalUrl.value) finalManualList.push(manualExternalUrl.value);
if (manualExternalUrl.value && !finalManualList.includes(manualExternalUrl.value)) {
finalManualList.push(manualExternalUrl.value);
}
const cleanManuals = finalManualList.filter(item => !isExternalLink(item));
if (manualExternalUrl.value) cleanManuals.push(manualExternalUrl.value);
const payload = { const payload = {
...form.value, ...form.value,
generalImage: cleanImages, generalImage: finalImageList,
generalManual: cleanManuals generalManual: finalManualList
}; };
const requestApi = form.value.id ? updateMaterialBase : addMaterialBase; const requestApi = form.value.id ? updateMaterialBase : addMaterialBase;
@ -630,8 +627,8 @@ const submitForm = async () => {
ElMessage.success(`${actionText}成功`); ElMessage.success(`${actionText}成功`);
dialog.visible = false; dialog.visible = false;
getList(); getList();
} catch (error) { } catch (error: any) {
console.error(error); ElMessage.error(error.msg || '保存失败');
} finally { } finally {
submitLoading.value = false; submitLoading.value = false;
} }
@ -645,7 +642,7 @@ const cancel = () => {
}; };
const resetForm = () => { const resetForm = () => {
form.value = {...initForm}; form.value = JSON.parse(JSON.stringify(initForm));
fileListImage.value = []; fileListImage.value = [];
fileListManual.value = []; fileListManual.value = [];
imageExternalUrl.value = ''; imageExternalUrl.value = '';
@ -677,26 +674,24 @@ const handleDelete = (row: MaterialBaseVO) => {
}).catch(() => {}); }).catch(() => {});
}; };
const openLink = (url: string) => {
if (!url) return;
window.open(url, '_blank');
}
// --- 文件上传辅助函数 --- // --- 文件上传辅助函数 ---
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) } const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') } const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) } const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp)$/i.test(url) }
const beforeAvatarUpload = (rawFile: any) => { const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png' && rawFile.type !== 'application/pdf') { const isTypeValid = ['image/jpeg', 'image/png', 'application/pdf'].includes(rawFile.type);
// 允许PDF用于说明书 if (!isTypeValid) {
if (rawFile.type === 'application/pdf') return true; ElMessage.error('仅支持 JPG/PNG/PDF');
ElMessage.error('支持 JPG/PNG/PDF'); return false;
return false
} }
if (rawFile.size / 1024 / 1024 > 10) { ElMessage.error('文件不能超过 10MB'); return false } if (rawFile.size / 1024 / 1024 > 10) {
return true ElMessage.error('文件不能超过 10MB');
return false;
}
return true;
} }
const customUpload = async (options: any, targetField: 'generalImage' | 'generalManual') => { const customUpload = async (options: any, targetField: 'generalImage' | 'generalManual') => {
@ -728,7 +723,7 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' |
const filename = urlToRemove.split('/').pop(); const filename = urlToRemove.split('/').pop();
if (filename) await deleteFile(filename) if (filename) await deleteFile(filename)
} }
ElMessage.success('已删除') ElMessage.success('已从列表移除')
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
} }
@ -739,29 +734,46 @@ const handlePreviewPicture = (uploadFile: any) => {
const triggerCamera = (field: 'generalImage' | 'generalManual') => { const triggerCamera = (field: 'generalImage' | 'generalManual') => {
currentCameraField.value = field; currentCameraField.value = field;
if (cameraInputRef.value) cameraInputRef.value.click() cameraDialogVisible.value = true;
} }
const handleCameraFile = async (event: Event) => { const handleCameraConfirm = async (file: File) => {
const input = event.target as HTMLInputElement console.log('✅ 父组件收到照片:', file.name)
if (input.files && input.files[0]) { if (!beforeAvatarUpload(file)) {
const file = input.files[0] cameraDialogVisible.value = false;
if (!beforeAvatarUpload(file)) { input.value = ''; return } return;
const formData = new FormData(); formData.append('file', file) }
const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }) const formData = new FormData();
formData.append('file', file);
// 修复点:使用 ElLoading
const loadingInstance = ElLoading.service({ text: '照片上传中...', background: 'rgba(0, 0, 0, 0.7)' });
try { try {
const res: any = await uploadFile(formData) const res: any = await uploadFile(formData);
if (res.code === 200) { if (res.code === 200) {
const newUrl = res.data.url; const newUrl = res.data.url;
const field = currentCameraField.value const field = currentCameraField.value;
form.value[field].push(newUrl) 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) }) const fileObj = { name: newUrl.split('/').pop(), url: getImageUrl(newUrl) };
ElMessage.success('拍照上传成功') if (field === 'generalImage') {
} else { ElMessage.error(res.msg || '上传失败') } fileListImage.value.push(fileObj);
} catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' } } else {
fileListManual.value.push(fileObj);
} }
ElMessage.success('拍照上传成功');
cameraDialogVisible.value = false;
} else {
ElMessage.error(res.msg || '上传失败');
} }
} catch (e) {
ElMessage.error('上传过程中发生异常');
} finally {
loadingInstance.close();
}
};
onMounted(() => { onMounted(() => {
getList(); getList();

View File

@ -374,9 +374,15 @@
</template> </template>
</el-dialog> </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="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 :close-on-click-modal="false">
<WebRtcCamera
ref="cameraRef"
@photo-submit="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body> <el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
<div style="text-align: center;"> <div style="text-align: center;">
@ -396,7 +402,8 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, onMounted, watch} from 'vue' import {ref, reactive, onMounted, watch} from 'vue'
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Delete, Picture} from '@element-plus/icons-vue' import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Delete, Picture} from '@element-plus/icons-vue'
import {ElMessage, ElMessageBox} from 'element-plus' // 修改:引入 ElLoading
import {ElMessage, ElMessageBox, ElLoading} from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
getBuyList, getBuyList,
@ -408,6 +415,7 @@ import {
deleteFile deleteFile
} from '@/api/inbound/buy' } from '@/api/inbound/buy'
import {getLabelPreview, executePrint} from '@/api/common/print' import {getLabelPreview, executePrint} from '@/api/common/print'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
// ------------------------------------ // ------------------------------------
// 状态与变量 // 状态与变量
@ -440,7 +448,8 @@ const dialogImageUrl = ref('')
const dialogVisibleImage = ref(false) const dialogVisibleImage = ref(false)
const arrivalFileList = ref<any[]>([]) const arrivalFileList = ref<any[]>([])
const reportFileList = 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' | 'inspection_report'>('arrival_photo') const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
const inspection_report_url = ref('') const inspection_report_url = ref('')
@ -778,26 +787,63 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' |
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
} }
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true } 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 triggerCamera = (field: 'arrival_photo' | 'inspection_report') => {
const handleCameraFile = async (event: Event) => { currentCameraField.value = field;
const input = event.target as HTMLInputElement cameraDialogVisible.value = true;
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 }) // ----------------------------------------------------
const handleCameraConfirm = async (file: File) => {
console.log('✅ 父组件收到照片:', file.name, file.size)
if (!beforeAvatarUpload(file)) {
return
}
// 修复点:使用 ElLoading.service 替代报错的 ElMessage.loading
const loadingInstance = ElLoading.service({
lock: true,
text: '照片上传中,请稍候...',
background: 'rgba(0, 0, 0, 0.7)',
})
try { try {
const formData = new FormData()
formData.append('file', file)
console.log('🚀 开始上传...')
const res: any = await uploadFile(formData) const res: any = await uploadFile(formData)
console.log('📡 上传结果:', res)
if (res.code === 200) { if (res.code === 200) {
const newUrl = res.data.url; const field = currentCameraField.value const newUrl = res.data.url
const field = currentCameraField.value // 'arrival_photo' 或 'inspection_report'
// 更新表单数据
form[field].push(newUrl) 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) }) // 更新文件展示列表
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('拍照上传成功') ElMessage.success('拍照上传成功')
} else { ElMessage.error(res.msg || '上传失败') } cameraDialogVisible.value = false // 成功才关闭弹窗
} catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' } } else {
ElMessage.error(res.msg || '上传失败')
}
} catch (e: any) {
console.error('上传异常:', e)
ElMessage.error('网络错误,上传失败')
} finally {
loadingInstance.close() // 关闭加载状态
} }
} }
const handleDelete = async (row: any) => { try { await deleteBuyInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } } const handleDelete = async (row: any) => { try { await deleteBuyInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } }
const handlePrint = async (row: any) => { const handlePrint = async (row: any) => {
printVisible.value = true; printLoading.value = true; previewUrl.value = '' printVisible.value = true; printLoading.value = true; previewUrl.value = ''
@ -817,7 +863,6 @@ onMounted(() => fetchData())
</script> </script>
<style scoped> <style scoped>
/* 样式部分保持不变,直接复用原有代码 */
.buy-module { background: #f5f7fa; padding: 20px; min-height: 100vh; } .buy-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); } .header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; } .left-tools { display: flex; gap: 10px; align-items: center; flex: 1; }

View File

@ -317,11 +317,17 @@
</template> </template>
</el-dialog> </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%"> <el-dialog v-model="dialogVisibleImage" append-to-body width="50%">
<img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /> <img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" />
</el-dialog> </el-dialog>
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close :close-on-click-modal="false">
<WebRtcCamera
ref="cameraRef"
@photo-submit="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body> <el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
<div style="text-align: center;"> <div style="text-align: center;">
@ -347,10 +353,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue' import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' // 修复:引入 ElLoading
import { ElMessage, ElLoading } from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { getProductList, createProductInbound, updateProductInbound, deleteProductInbound, searchMaterialBase } from '@/api/inbound/product' import { getProductList, createProductInbound, updateProductInbound, deleteProductInbound, searchMaterialBase } from '@/api/inbound/product'
import { uploadFile, deleteFile } from '@/api/inbound/buy' import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import { getLabelPreview, executePrint } from '@/api/common/print' import { getLabelPreview, executePrint } from '@/api/common/print'
const loading = ref(false) const loading = ref(false)
@ -379,7 +387,8 @@ const productPhotoList = ref<any[]>([]) // 成品实拍
const qualityFileList = ref<any[]>([]) // 质量报告 const qualityFileList = ref<any[]>([]) // 质量报告
const inspectionFileList = 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 currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspection_report_link'>('product_photo')
const quality_url = ref('') const quality_url = ref('')
const inspection_url = ref('') const inspection_url = ref('')
@ -558,25 +567,52 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'product_photo' |
ElMessage.success('已删除') ElMessage.success('已删除')
} catch (e) { console.error(e) } } 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 handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true } const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
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);
// 修复点:使用 ElLoading
const loadingMsg = ElLoading.service({ text: '照片上传中...', background: 'rgba(0, 0, 0, 0.7)' });
let success = false;
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('拍照上传成功');
success = true;
} else {
ElMessage.error(res.msg || '上传失败');
}
} catch (e) {
ElMessage.error('网络错误,上传失败');
} finally {
loadingMsg.close();
if (success) {
cameraDialogVisible.value = false;
}
}
};
const submitForm = async () => { const submitForm = async () => {
await formRef.value.validate(async (valid: boolean) => { await formRef.value.validate(async (valid: boolean) => {
if(valid) { if(valid) {

View File

@ -136,10 +136,7 @@
<template #default="scope" v-else-if="['detail_link'].includes(col.prop)"> <template #default="scope" v-else-if="['detail_link'].includes(col.prop)">
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" <el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank"
:underline="false"> :underline="false">
<el-icon> <el-icon><Link/></el-icon> 查看
<Link/>
</el-icon>
查看
</el-link> </el-link>
</template> </template>
@ -152,10 +149,7 @@
<el-table-column label="操作" width="220" fixed="right" align="center"> <el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="warning" size="default" @click="handlePrint(row)"> <el-button link type="warning" size="default" @click="handlePrint(row)">
<el-icon> <el-icon><Printer/></el-icon> 打印
<Printer/>
</el-icon>
打印
</el-button> </el-button>
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button> <el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
<el-popconfirm title="确定删除该条记录吗不可恢复" @confirm="handleDelete(row)" width="220"> <el-popconfirm title="确定删除该条记录吗不可恢复" @confirm="handleDelete(row)" width="220">
@ -409,8 +403,14 @@
</template> </template>
</el-dialog> </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="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 :close-on-click-modal="false">
<WebRtcCamera
ref="cameraRef"
@photo-submit="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body> <el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
<div style="text-align: center;"> <div style="text-align: center;">
<div v-loading="printLoading" class="preview-box"> <div v-loading="printLoading" class="preview-box">
@ -429,7 +429,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, onMounted, watch} from 'vue' import {ref, reactive, onMounted, watch} from 'vue'
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Picture} from '@element-plus/icons-vue' import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Picture} from '@element-plus/icons-vue'
import {ElMessage} from 'element-plus' import {ElMessage, ElLoading} from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { import {
getSemiList, getSemiList,
@ -439,6 +439,7 @@ import {
searchMaterialBase searchMaterialBase
} from '@/api/inbound/semi' } from '@/api/inbound/semi'
import { uploadFile, deleteFile } from '@/api/inbound/buy' import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import {getLabelPreview, executePrint} from '@/api/common/print' import {getLabelPreview, executePrint} from '@/api/common/print'
// ------------------------------------ // ------------------------------------
@ -467,7 +468,8 @@ const dialogImageUrl = ref('')
const dialogVisibleImage = ref(false) const dialogVisibleImage = ref(false)
const arrivalFileList = ref<any[]>([]) const arrivalFileList = ref<any[]>([])
const reportFileList = 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 currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo')
const quality_report_url = ref('') const quality_report_url = ref('')
@ -734,26 +736,47 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' |
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
} }
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true } 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 triggerCamera = (field: 'arrival_photo' | 'quality_report_link') => {
const handleCameraFile = async (event: Event) => { currentCameraField.value = field;
const input = event.target as HTMLInputElement cameraDialogVisible.value = true;
if (input.files && input.files[0]) { }
const file = input.files[0] const handleCameraConfirm = async (file: File) => {
if (!beforeAvatarUpload(file)) { input.value = ''; return } if (!beforeAvatarUpload(file)) {
const formData = new FormData(); formData.append('file', file) cameraDialogVisible.value = false;
const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }) return;
}
const formData = new FormData();
formData.append('file', file);
// 修复点:使用 ElLoading
const loadingMsg = ElLoading.service({ text: '照片上传中...', background: 'rgba(0, 0, 0, 0.7)' });
let success = false;
try { try {
const res: any = await uploadFile(formData) const res: any = await uploadFile(formData);
if (res.code === 200) { if (res.code === 200) {
const newUrl = res.data.url; const field = currentCameraField.value const newUrl = res.data.url;
form[field].push(newUrl) const field = currentCameraField.value;
if (field === 'arrival_photo') arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) form[field].push(newUrl);
else reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) if (field === 'arrival_photo') {
ElMessage.success('拍照上传成功') arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) });
} else { ElMessage.error(res.msg || '上传失败') } } else if (field === 'quality_report_link') {
} catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' } reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) });
}
ElMessage.success('拍照上传成功');
success = true;
} else {
ElMessage.error(res.msg || '上传失败');
}
} catch (e) {
ElMessage.error('网络错误,上传失败');
} finally {
loadingMsg.close();
if (success) {
cameraDialogVisible.value = false;
} }
} }
};
const submitForm = async () => { const submitForm = async () => {
if (!formRef.value) return if (!formRef.value) return