|
|
@@ -1,13 +1,13 @@
|
|
|
<template>
|
|
|
<div class="image-upload-container">
|
|
|
<input
|
|
|
- type="file"
|
|
|
- ref="fileInput"
|
|
|
- accept="image/*"
|
|
|
- style="display: none"
|
|
|
- @change="handleFileSelect"
|
|
|
+ type="file"
|
|
|
+ ref="fileInput"
|
|
|
+ accept="image/*"
|
|
|
+ style="display: none"
|
|
|
+ @change="handleFileSelect"
|
|
|
/>
|
|
|
-
|
|
|
+
|
|
|
<div class="combined-upload-btn">
|
|
|
<span class="upload-part" @click="triggerFileSelect" :title="'上传参考图'">
|
|
|
上传参考图
|
|
|
@@ -25,22 +25,22 @@
|
|
|
<button class="delete-btn" @click="removeImage">×</button>
|
|
|
<!-- 使用el-image组件实现预览功能 -->
|
|
|
<el-image
|
|
|
- :src="previewImage"
|
|
|
- :preview-src-list="[previewImage]"
|
|
|
- fit="cover"
|
|
|
- show-progress
|
|
|
- class="preview-image"
|
|
|
+ :src="previewImage"
|
|
|
+ :preview-src-list="[previewImage]"
|
|
|
+ fit="cover"
|
|
|
+ show-progress
|
|
|
+ class="preview-image"
|
|
|
>
|
|
|
<template
|
|
|
- #toolbar="{ actions, reset, activeIndex }"
|
|
|
+ #toolbar="{ actions, reset, activeIndex }"
|
|
|
>
|
|
|
<el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
|
|
|
<el-icon
|
|
|
- @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
|
|
|
+ @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
|
|
|
<ZoomIn />
|
|
|
</el-icon>
|
|
|
<el-icon
|
|
|
- @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
|
|
|
+ @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
|
|
|
<RefreshRight />
|
|
|
</el-icon>
|
|
|
<el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
|
|
|
@@ -53,10 +53,12 @@
|
|
|
|
|
|
<!-- 相机弹窗 -->
|
|
|
<el-dialog
|
|
|
- v-model="cameraVisible"
|
|
|
- title="相机拍照"
|
|
|
- width="700px"
|
|
|
- @close="closeCamera"
|
|
|
+ v-model="cameraVisible"
|
|
|
+ title="相机拍照"
|
|
|
+ :width="isSmallScreen ? '100%' : '90vw'"
|
|
|
+ :fullscreen="isSmallScreen"
|
|
|
+ @close="closeCamera"
|
|
|
+ class="camera-dialog"
|
|
|
>
|
|
|
<div class="camera-container">
|
|
|
<div v-if="!isCapturing" class="camera-view">
|
|
|
@@ -79,7 +81,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, watch, nextTick } from 'vue';
|
|
|
+import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
|
|
import { Picture, ZoomOut, ZoomIn, RefreshRight, RefreshLeft, Refresh, Download, Camera } from '@element-plus/icons-vue';
|
|
|
import { Message } from '@/utils/message/Message.js';
|
|
|
// 导入axios用于文件上传
|
|
|
@@ -109,6 +111,21 @@ const stream = ref(null); // 媒体流
|
|
|
const capturedImage = ref(''); // 拍摄的照片
|
|
|
const isCapturing = ref(false); // 是否正在拍摄中
|
|
|
const videoRef = ref(null); // 视频元素引用
|
|
|
+const isSmallScreen = ref(false); // 是否为小屏设备
|
|
|
+
|
|
|
+// 监听窗口大小变化
|
|
|
+const handleResize = () => {
|
|
|
+ isSmallScreen.value = window.innerWidth < 768;
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ handleResize();
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', handleResize);
|
|
|
+});
|
|
|
|
|
|
// 监听modelValue变化,更新预览图
|
|
|
watch(() => props.modelValue, (newValue) => {
|
|
|
@@ -150,10 +167,10 @@ const uploadFile = async (file) => {
|
|
|
// 创建FormData对象
|
|
|
const formData = new FormData();
|
|
|
formData.append('file', file);
|
|
|
-
|
|
|
+
|
|
|
// 构建上传地址
|
|
|
const uploadUrl = import.meta.env.VITE_BASE_URL + '/infra/file/upload';
|
|
|
-
|
|
|
+
|
|
|
// 发送文件上传请求
|
|
|
const response = await axios.post(uploadUrl, formData, {
|
|
|
headers: {
|
|
|
@@ -162,13 +179,13 @@ const uploadFile = async (file) => {
|
|
|
},
|
|
|
timeout: 60000
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
let imageUrl = '';
|
|
|
if (response.data?.data) {
|
|
|
// 去除反引号和可能的空格
|
|
|
imageUrl = response.data.data.replace(/[`\s]/g, '');
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 更新预览图
|
|
|
previewImage.value = imageUrl;
|
|
|
// 触发更新事件
|
|
|
@@ -219,10 +236,38 @@ const download = (index) => {
|
|
|
// 打开相机弹窗
|
|
|
const openCamera = async () => {
|
|
|
try {
|
|
|
- // 获取用户媒体设备权限
|
|
|
- stream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
- video: true
|
|
|
- });
|
|
|
+ // 获取相机容器的尺寸
|
|
|
+ const containerWidth = window.innerWidth * (isSmallScreen.value ? 0.9 : 0.8);
|
|
|
+ const containerHeight = window.innerHeight * (isSmallScreen.value ? 0.7 : 0.6);
|
|
|
+
|
|
|
+ // 定义相机视频的最大分辨率
|
|
|
+ const videoConstraints = {
|
|
|
+ width: { ideal: containerWidth, max: containerWidth * 1.2 },
|
|
|
+ height: { ideal: containerHeight, max: containerHeight * 1.2 },
|
|
|
+ frameRate: { ideal: 30, max: 60 }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 尝试优先使用后摄像头(不带exact,提高兼容性)
|
|
|
+ try {
|
|
|
+ stream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: { ...videoConstraints, facingMode: 'environment' }
|
|
|
+ });
|
|
|
+ } catch (backCameraError) {
|
|
|
+ console.log('后摄像头不可用,尝试使用前摄像头:', backCameraError);
|
|
|
+ // 后摄像头不可用,尝试使用前摄像头
|
|
|
+ try {
|
|
|
+ stream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: { ...videoConstraints, facingMode: 'user' }
|
|
|
+ });
|
|
|
+ } catch (frontCameraError) {
|
|
|
+ console.log('前摄像头也不可用,尝试默认设置:', frontCameraError);
|
|
|
+ // 前摄像头也不可用,使用默认设置
|
|
|
+ stream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: videoConstraints
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 先显示弹窗,让DOM渲染完成
|
|
|
cameraVisible.value = true;
|
|
|
isCapturing.value = false;
|
|
|
@@ -257,14 +302,20 @@ const takePhoto = () => {
|
|
|
canvas.width = video.videoWidth;
|
|
|
canvas.height = video.videoHeight;
|
|
|
const context = canvas.getContext('2d');
|
|
|
-
|
|
|
- // 应用镜像效果
|
|
|
- context.save();
|
|
|
- context.translate(canvas.width, 0); // 平移到右侧
|
|
|
- context.scale(-1, 1); // 水平翻转
|
|
|
- context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
- context.restore();
|
|
|
-
|
|
|
+
|
|
|
+ // 应用镜像效果(仅对前摄像头)
|
|
|
+ const isFrontCamera = stream.value?.getVideoTracks()[0]?.getSettings().facingMode === 'user';
|
|
|
+
|
|
|
+ if (isFrontCamera) {
|
|
|
+ context.save();
|
|
|
+ context.translate(canvas.width, 0); // 平移到右侧
|
|
|
+ context.scale(-1, 1); // 水平翻转
|
|
|
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
+ context.restore();
|
|
|
+ } else {
|
|
|
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
+ }
|
|
|
+
|
|
|
capturedImage.value = canvas.toDataURL('image/jpeg');
|
|
|
isCapturing.value = true;
|
|
|
};
|
|
|
@@ -273,16 +324,16 @@ const takePhoto = () => {
|
|
|
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();
|
|
|
@@ -303,7 +354,7 @@ const confirmAndUpload = async () => {
|
|
|
|
|
|
// 调用共用的上传函数
|
|
|
await uploadFile(file);
|
|
|
-
|
|
|
+
|
|
|
// 关闭相机弹窗
|
|
|
closeCamera();
|
|
|
} catch (error) {
|
|
|
@@ -399,7 +450,7 @@ const confirmAndUpload = async () => {
|
|
|
|
|
|
.preview-image {
|
|
|
width: rpx(90);
|
|
|
- max-height: rpx(150);
|
|
|
+ max-height: rpx(150);
|
|
|
object-fit: contain;
|
|
|
border-radius: rpx(3);
|
|
|
}
|
|
|
@@ -438,26 +489,80 @@ const confirmAndUpload = async () => {
|
|
|
}
|
|
|
|
|
|
// 相机弹窗样式
|
|
|
+.camera-dialog .el-dialog__body {
|
|
|
+ padding: 0;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
.camera-container {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
- padding: 20px 0;
|
|
|
+ padding: 0;
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ height: calc(100vh - 150px);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-view {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
.camera-view video {
|
|
|
max-width: 100%;
|
|
|
- height: auto;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
border: 1px solid #ddd;
|
|
|
border-radius: 5px;
|
|
|
- transform: scaleX(-1);
|
|
|
+}
|
|
|
+
|
|
|
+.captured-image-view {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
.captured-image-view img {
|
|
|
max-width: 100%;
|
|
|
- height: auto;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
border: 1px solid #ddd;
|
|
|
border-radius: 5px;
|
|
|
- transform: scaleX(-1);
|
|
|
+}
|
|
|
+
|
|
|
+// 小屏设备样式
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .camera-container {
|
|
|
+ padding: 10px 0;
|
|
|
+ height: calc(100vh - 150px);
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .camera-view video {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ max-width: none;
|
|
|
+ max-height: none;
|
|
|
+ object-fit: contain;
|
|
|
+ border-radius: 0;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .captured-image-view img {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ border-radius: 0;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|