|
|
@@ -7,20 +7,21 @@
|
|
|
style="display: none"
|
|
|
@change="handleFileSelect"
|
|
|
/>
|
|
|
- <button
|
|
|
- class="upload-btn"
|
|
|
- @click="triggerFileSelect"
|
|
|
- :disabled="isUploading"
|
|
|
- title="上传参考图"
|
|
|
- >
|
|
|
- 上传参考图
|
|
|
- <el-icon><Picture /></el-icon>
|
|
|
- </button>
|
|
|
+
|
|
|
+ <div class="combined-upload-btn">
|
|
|
+ <span class="upload-part" @click="triggerFileSelect" :title="'上传参考图'">
|
|
|
+ 上传参考图
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
+ </span>
|
|
|
+ <span class="separator">|</span>
|
|
|
+ <span class="camera-part" @click="openCamera" :title="'使用相机拍照'">
|
|
|
+ <el-icon><Camera /></el-icon>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
|
|
|
<!-- 预览图显示区域 - 使用el-image组件 -->
|
|
|
<div v-if="previewImage && previewImage !== '/src/assets/images/default-preview.png'" class="preview-container">
|
|
|
<div class="image-wrapper">
|
|
|
- <!-- 删除按钮 - 确保显示 -->
|
|
|
<button class="delete-btn" @click="removeImage">×</button>
|
|
|
<!-- 使用el-image组件实现预览功能 -->
|
|
|
<el-image
|
|
|
@@ -49,12 +50,37 @@
|
|
|
</el-image>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 相机弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="cameraVisible"
|
|
|
+ title="相机拍照"
|
|
|
+ width="700px"
|
|
|
+ @close="closeCamera"
|
|
|
+ >
|
|
|
+ <div class="camera-container">
|
|
|
+ <div v-if="!isCapturing" class="camera-view">
|
|
|
+ <video ref="videoRef" autoplay muted playsinline></video>
|
|
|
+ </div>
|
|
|
+ <div v-else class="captured-image-view">
|
|
|
+ <img :src="capturedImage" alt="已拍摄照片" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <div class="camera-actions">
|
|
|
+ <el-button @click="closeCamera">关闭</el-button>
|
|
|
+ <el-button v-if="!isCapturing" @click="takePhoto">拍照</el-button>
|
|
|
+ <el-button v-else @click="retakePhoto">重拍</el-button>
|
|
|
+ <el-button v-if="isCapturing" type="primary" @click="confirmAndUpload">确认并上传</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref } from 'vue';
|
|
|
-import { Picture, ZoomOut, ZoomIn, RefreshRight, RefreshLeft, Refresh, Download } from '@element-plus/icons-vue';
|
|
|
+import { ref, watch, nextTick } from 'vue';
|
|
|
+import { Picture, ZoomOut, ZoomIn, RefreshRight, RefreshLeft, Refresh, Download, Camera } from '@element-plus/icons-vue';
|
|
|
import { Message } from '@/utils/message/Message.js';
|
|
|
// 导入axios用于文件上传
|
|
|
import axios from 'axios';
|
|
|
@@ -77,6 +103,20 @@ const isUploading = ref(false);
|
|
|
// 预览图片URL
|
|
|
const previewImage = ref('');
|
|
|
|
|
|
+// 相机相关状态变量
|
|
|
+const cameraVisible = ref(false); // 相机弹窗显示状态
|
|
|
+const stream = ref(null); // 媒体流
|
|
|
+const capturedImage = ref(''); // 拍摄的照片
|
|
|
+const isCapturing = ref(false); // 是否正在拍摄中
|
|
|
+const videoRef = ref(null); // 视频元素引用
|
|
|
+
|
|
|
+// 监听modelValue变化,更新预览图
|
|
|
+watch(() => props.modelValue, (newValue) => {
|
|
|
+ if (newValue && newValue !== previewImage.value) {
|
|
|
+ previewImage.value = newValue;
|
|
|
+ }
|
|
|
+}, { immediate: true });
|
|
|
+
|
|
|
// 触发文件选择
|
|
|
const triggerFileSelect = () => {
|
|
|
fileInput.value?.click();
|
|
|
@@ -100,13 +140,17 @@ const handleFileSelect = async (event) => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ await uploadFile(file);
|
|
|
+};
|
|
|
+
|
|
|
+// 抽取文件上传逻辑为单独的函数,供文件选择和相机拍摄共用
|
|
|
+const uploadFile = async (file) => {
|
|
|
try {
|
|
|
isUploading.value = true;
|
|
|
// 创建FormData对象
|
|
|
const formData = new FormData();
|
|
|
formData.append('file', file);
|
|
|
|
|
|
-
|
|
|
// 构建上传地址
|
|
|
const uploadUrl = import.meta.env.VITE_BASE_URL + '/infra/file/upload';
|
|
|
|
|
|
@@ -119,7 +163,6 @@ const handleFileSelect = async (event) => {
|
|
|
timeout: 60000
|
|
|
});
|
|
|
|
|
|
-
|
|
|
let imageUrl = '';
|
|
|
if (response.data?.data) {
|
|
|
// 去除反引号和可能的空格
|
|
|
@@ -138,8 +181,8 @@ const handleFileSelect = async (event) => {
|
|
|
isUploading.value = false;
|
|
|
} finally {
|
|
|
// 清空input值,允许重复选择同一文件
|
|
|
- if (event.target) {
|
|
|
- event.target.value = '';
|
|
|
+ if (fileInput.value) {
|
|
|
+ fileInput.value.value = '';
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
@@ -152,7 +195,8 @@ const clearPreview = () => {
|
|
|
|
|
|
// 暴露方法给父组件
|
|
|
defineExpose({
|
|
|
- clearPreview
|
|
|
+ clearPreview,
|
|
|
+ previewImage
|
|
|
});
|
|
|
|
|
|
// 移除图片
|
|
|
@@ -171,6 +215,96 @@ const download = (index) => {
|
|
|
link.click();
|
|
|
document.body.removeChild(link);
|
|
|
};
|
|
|
+
|
|
|
+// 打开相机弹窗
|
|
|
+const openCamera = async () => {
|
|
|
+ try {
|
|
|
+ // 获取用户媒体设备权限
|
|
|
+ stream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: true
|
|
|
+ });
|
|
|
+ // 先显示弹窗,让DOM渲染完成
|
|
|
+ cameraVisible.value = true;
|
|
|
+ isCapturing.value = false;
|
|
|
+
|
|
|
+ // 使用nextTick确保DOM已经更新,再设置视频源
|
|
|
+ nextTick(() => {
|
|
|
+ if (videoRef.value) {
|
|
|
+ videoRef.value.srcObject = stream.value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取相机权限失败:', error);
|
|
|
+ Message().error('无法访问相机,请确保已授予相机权限!', true);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 关闭相机弹窗
|
|
|
+const closeCamera = () => {
|
|
|
+ if (stream.value) {
|
|
|
+ stream.value.getTracks().forEach(track => track.stop());
|
|
|
+ stream.value = null;
|
|
|
+ }
|
|
|
+ cameraVisible.value = false;
|
|
|
+ capturedImage.value = '';
|
|
|
+ isCapturing.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 拍照功能
|
|
|
+const takePhoto = () => {
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ const video = videoRef.value;
|
|
|
+ canvas.width = video.videoWidth;
|
|
|
+ canvas.height = video.videoHeight;
|
|
|
+ const context = canvas.getContext('2d');
|
|
|
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
+ capturedImage.value = canvas.toDataURL('image/jpeg');
|
|
|
+ isCapturing.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 重拍功能 - 修复不显示相机实况的问题
|
|
|
+const retakePhoto = async () => {
|
|
|
+ capturedImage.value = '';
|
|
|
+ isCapturing.value = false;
|
|
|
+
|
|
|
+ // 确保视频元素已经渲染且媒体流存在
|
|
|
+ await nextTick();
|
|
|
+
|
|
|
+ if (videoRef.value && stream.value) {
|
|
|
+ // 确保视频元素的srcObject正确设置为媒体流
|
|
|
+ if (videoRef.value.srcObject !== stream.value) {
|
|
|
+ videoRef.value.srcObject = stream.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试重新播放视频(在某些浏览器中可能需要)
|
|
|
+ try {
|
|
|
+ await videoRef.value.play();
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('自动播放视频失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 确认并上传照片
|
|
|
+const confirmAndUpload = async () => {
|
|
|
+ if (capturedImage.value) {
|
|
|
+ try {
|
|
|
+ // 创建Blob对象
|
|
|
+ const response = await fetch(capturedImage.value);
|
|
|
+ const blob = await response.blob();
|
|
|
+ const file = new File([blob], `camera_photo_${Date.now()}.jpg`, { type: 'image/jpeg' });
|
|
|
+
|
|
|
+ // 调用共用的上传函数
|
|
|
+ await uploadFile(file);
|
|
|
+
|
|
|
+ // 关闭相机弹窗
|
|
|
+ closeCamera();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('相机拍摄图片上传失败:', error);
|
|
|
+ Message().error('相机拍摄图片上传失败,请重试!', true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@@ -185,29 +319,59 @@ const download = (index) => {
|
|
|
display: inline-block;
|
|
|
}
|
|
|
|
|
|
-.upload-btn {
|
|
|
- padding: rpx(5) rpx(10);
|
|
|
+// 合并按钮样式
|
|
|
+.combined-upload-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
background: #fff;
|
|
|
border: 1px solid #ffce1b;
|
|
|
box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
|
|
|
border-radius: rpx(5);
|
|
|
- cursor: pointer;
|
|
|
+ overflow: hidden;
|
|
|
+ font-size: rpx(6);
|
|
|
+ cursor: default;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-part {
|
|
|
+ padding: rpx(5) rpx(10);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: rpx(3);
|
|
|
- font-size: rpx(6);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.2s;
|
|
|
|
|
|
- .el-icon {
|
|
|
- font-size: rpx(8);
|
|
|
- color: #666;
|
|
|
+ &:hover {
|
|
|
+ background-color: rgba(255, 206, 27, 0.1);
|
|
|
}
|
|
|
|
|
|
- &:disabled {
|
|
|
+ &.disabled {
|
|
|
opacity: 0.6;
|
|
|
cursor: not-allowed;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+.separator {
|
|
|
+ color: #ccc;
|
|
|
+ font-size: rpx(8);
|
|
|
+}
|
|
|
+
|
|
|
+.camera-part {
|
|
|
+ padding: rpx(5) rpx(10);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: rgba(255, 206, 27, 0.1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.el-icon {
|
|
|
+ font-size: rpx(8);
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
// 预览图样式
|
|
|
.preview-container {
|
|
|
position: absolute;
|
|
|
@@ -233,7 +397,6 @@ const download = (index) => {
|
|
|
border-radius: rpx(3);
|
|
|
}
|
|
|
|
|
|
-// 删除按钮样式 - 确保正确显示
|
|
|
.delete-btn {
|
|
|
position: absolute;
|
|
|
top: rpx(3);
|
|
|
@@ -266,4 +429,26 @@ const download = (index) => {
|
|
|
.delete-btn:hover {
|
|
|
opacity: 1;
|
|
|
}
|
|
|
+
|
|
|
+// 相机弹窗样式
|
|
|
+.camera-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-view video {
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.captured-image-view img {
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 5px;
|
|
|
+}
|
|
|
</style>
|