修改拍照的大小以及增加放大缩小编辑等功能
This commit is contained in:
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.13.3",
|
"axios": "^1.13.3",
|
||||||
|
"cropperjs": "^1.6.2",
|
||||||
"element-plus": "^2.13.1",
|
"element-plus": "^2.13.1",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="camera-container">
|
<div class="camera-container is-fullscreen">
|
||||||
<div v-if="error" class="error-message">
|
<div v-if="error" class="error-message">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
|
<el-button style="margin-top: 20px" @click="handleCancel">关闭</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="media-box">
|
<div v-else class="media-box">
|
||||||
@ -10,60 +11,134 @@
|
|||||||
ref="videoRef"
|
ref="videoRef"
|
||||||
autoplay
|
autoplay
|
||||||
playsinline
|
playsinline
|
||||||
class="media-content"
|
class="media-content video-feed"
|
||||||
|
:style="{ transform: `scale(${cameraZoom})` }"
|
||||||
></video>
|
></video>
|
||||||
|
|
||||||
<img
|
<div v-show="imgSrc" class="editor-container">
|
||||||
v-if="imgSrc"
|
<img
|
||||||
:src="imgSrc"
|
ref="previewImgRef"
|
||||||
class="media-content preview-img"
|
:src="imgSrc"
|
||||||
alt="Photo Preview"
|
class="media-content preview-img"
|
||||||
/>
|
alt="Photo Preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<canvas ref="canvasRef" style="display: none;"></canvas>
|
<canvas ref="canvasRef" style="display: none;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!imgSrc && isCameraReady" class="zoom-slider-container">
|
||||||
|
<el-icon class="zoom-icon"><Remove /></el-icon>
|
||||||
|
<el-slider
|
||||||
|
v-model="cameraZoom"
|
||||||
|
:min="1"
|
||||||
|
:max="3"
|
||||||
|
:step="0.1"
|
||||||
|
:show-tooltip="false"
|
||||||
|
class="custom-slider"
|
||||||
|
/>
|
||||||
|
<el-icon class="zoom-icon"><CirclePlus /></el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="camera-actions">
|
<div class="camera-actions">
|
||||||
|
|
||||||
<template v-if="!imgSrc">
|
<template v-if="!imgSrc">
|
||||||
<el-button @click="handleCancel" size="large">取消</el-button>
|
<el-button circle size="large" @click="handleCancel" class="action-btn icon-btn">
|
||||||
<el-button
|
<el-icon><Close /></el-icon>
|
||||||
type="primary"
|
|
||||||
@click="capture"
|
|
||||||
size="large"
|
|
||||||
:disabled="!isCameraReady"
|
|
||||||
>
|
|
||||||
<el-icon class="el-icon--left"><Camera /></el-icon>
|
|
||||||
拍照
|
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
circle
|
||||||
|
type="danger"
|
||||||
|
size="large"
|
||||||
|
@click="capture"
|
||||||
|
:disabled="!isCameraReady"
|
||||||
|
class="shutter-btn"
|
||||||
|
>
|
||||||
|
<div class="shutter-inner"></div>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<div class="placeholder-btn"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-button @click="retake" size="large" type="warning" plain>
|
|
||||||
<el-icon class="el-icon--left"><RefreshLeft /></el-icon>
|
<div v-if="isEditing" class="edit-mode-bar">
|
||||||
重拍
|
<div class="edit-tools">
|
||||||
</el-button>
|
<el-tooltip content="切换移动图片/调整裁剪框" placement="top" :show-after="1000">
|
||||||
<el-button
|
<el-button
|
||||||
type="success"
|
circle
|
||||||
@click="confirmUse"
|
@click="toggleDragMode"
|
||||||
size="large"
|
class="tool-btn"
|
||||||
>
|
:class="{ 'is-active': isMoveMode }"
|
||||||
<el-icon class="el-icon--left"><Check /></el-icon>
|
>
|
||||||
确认使用
|
<el-icon><Rank /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
|
||||||
|
<el-button circle @click="zoomCropper(0.1)" class="tool-btn"><el-icon><ZoomIn /></el-icon></el-button>
|
||||||
|
<el-button circle @click="zoomCropper(-0.1)" class="tool-btn"><el-icon><ZoomOut /></el-icon></el-button>
|
||||||
|
|
||||||
|
<el-button circle @click="rotateLeft" class="tool-btn"><el-icon><RefreshLeft /></el-icon></el-button>
|
||||||
|
<el-button circle @click="rotateRight" class="tool-btn"><el-icon><RefreshRight /></el-icon></el-button>
|
||||||
|
<el-button circle @click="resetCrop" class="tool-btn"><el-icon><Refresh /></el-icon></el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-confirm">
|
||||||
|
<el-button circle size="large" @click="handleCancel" class="action-btn icon-btn">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button @click="stopEdit" class="text-btn">取消编辑</el-button>
|
||||||
|
<el-button type="success" @click="confirmUse" class="confirm-btn">
|
||||||
|
完成并上传
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="preview-mode-bar">
|
||||||
|
<el-button circle size="large" @click="handleCancel" class="action-btn icon-btn">
|
||||||
|
<el-icon><Close /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button @click="retake" size="large" class="text-btn" style="min-width: 80px;">重拍</el-button>
|
||||||
|
|
||||||
|
<el-button @click="startEdit" size="large" class="text-btn" style="min-width: 80px;">
|
||||||
|
<el-icon style="margin-right: 4px"><Edit /></el-icon>编辑
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
@click="confirmUse"
|
||||||
|
size="large"
|
||||||
|
class="confirm-btn"
|
||||||
|
>
|
||||||
|
确认使用 <el-icon class="el-icon--right"><Check /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Camera, RefreshLeft, Check } from '@element-plus/icons-vue'
|
import {
|
||||||
|
Camera, RefreshLeft, RefreshRight, Check, Close, Refresh, Edit,
|
||||||
|
ZoomIn, ZoomOut, Remove, CirclePlus, Rank
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
|
||||||
const emit = defineEmits(['photo-submit', 'cancel'])
|
const emit = defineEmits(['photo-submit', 'cancel'])
|
||||||
|
|
||||||
const videoRef = ref<HTMLVideoElement>()
|
const videoRef = ref<HTMLVideoElement>()
|
||||||
const canvasRef = ref<HTMLCanvasElement>()
|
const canvasRef = ref<HTMLCanvasElement>()
|
||||||
|
const previewImgRef = ref<HTMLImageElement>()
|
||||||
const mediaStream = ref<MediaStream>()
|
const mediaStream = ref<MediaStream>()
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const isCameraReady = ref(false)
|
const isCameraReady = ref(false)
|
||||||
@ -71,11 +146,18 @@ const isCameraReady = ref(false)
|
|||||||
const imgSrc = ref('')
|
const imgSrc = ref('')
|
||||||
const currentFile = ref<File | null>(null)
|
const currentFile = ref<File | null>(null)
|
||||||
|
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const isMoveMode = ref(false)
|
||||||
|
const cameraZoom = ref(1) // 控制拍摄时的变焦倍数
|
||||||
|
let cropper: Cropper | null = null
|
||||||
|
|
||||||
const startCamera = async () => {
|
const startCamera = async () => {
|
||||||
stopCamera()
|
stopCamera()
|
||||||
error.value = ''
|
error.value = ''
|
||||||
imgSrc.value = ''
|
imgSrc.value = ''
|
||||||
|
isEditing.value = false
|
||||||
currentFile.value = null
|
currentFile.value = null
|
||||||
|
cameraZoom.value = 1
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const constraints = {
|
const constraints = {
|
||||||
@ -96,7 +178,7 @@ const startCamera = async () => {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||||
error.value = '无法访问摄像头: 浏览器限制非 HTTPS 环境调用摄像头,请使用 localhost 或 配置 HTTPS。'
|
error.value = '无法访问摄像头: 请使用 HTTPS 环境。'
|
||||||
} else {
|
} else {
|
||||||
error.value = '无法访问摄像头: ' + (err.message || '请检查权限')
|
error.value = '无法访问摄像头: ' + (err.message || '请检查权限')
|
||||||
}
|
}
|
||||||
@ -115,126 +197,357 @@ const stopCamera = () => {
|
|||||||
isCameraReady.value = false
|
isCameraReady.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 核心修复:拍照时应用数码变焦(裁剪+拉伸)
|
||||||
|
// ----------------------------------------------------
|
||||||
const capture = () => {
|
const capture = () => {
|
||||||
const video = videoRef.value
|
const video = videoRef.value
|
||||||
const canvas = canvasRef.value
|
const canvas = canvasRef.value
|
||||||
if (!video || !canvas) return
|
if (!video || !canvas) return
|
||||||
|
|
||||||
const width = video.videoWidth
|
// 1. 获取视频原始尺寸
|
||||||
const height = video.videoHeight
|
const vW = video.videoWidth
|
||||||
|
const vH = video.videoHeight
|
||||||
|
if (vW === 0 || vH === 0) return
|
||||||
|
|
||||||
if (width === 0 || height === 0) return
|
// 2. 设置画布为全尺寸(保持清晰度)
|
||||||
|
canvas.width = vW
|
||||||
canvas.width = width
|
canvas.height = vH
|
||||||
canvas.height = height
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
|
|
||||||
ctx.drawImage(video, 0, 0, width, height)
|
// 3. 计算基于 zoomLevel 的裁剪区域
|
||||||
|
// zoom = 1: 裁剪宽 = vW
|
||||||
|
// zoom = 2: 裁剪宽 = vW / 2
|
||||||
|
const zoom = cameraZoom.value
|
||||||
|
const cropW = vW / zoom
|
||||||
|
const cropH = vH / zoom
|
||||||
|
|
||||||
|
// 4. 计算裁剪的起始点 (居中裁剪)
|
||||||
|
const cropX = (vW - cropW) / 2
|
||||||
|
const cropY = (vH - cropH) / 2
|
||||||
|
|
||||||
|
// 5. 将裁剪区域绘制到全尺寸画布上 (drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh))
|
||||||
|
ctx.drawImage(
|
||||||
|
video,
|
||||||
|
cropX, cropY, cropW, cropH, // 源:截取中心部分
|
||||||
|
0, 0, vW, vH // 目标:铺满整个画布
|
||||||
|
)
|
||||||
|
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
ElMessage.error('照片生成失败,请重试')
|
ElMessage.error('拍照失败,请重试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = new Date().getTime()
|
const timestamp = new Date().getTime()
|
||||||
const filename = `photo_${timestamp}.jpg`
|
const filename = `photo_${timestamp}.jpg`
|
||||||
const file = new File([blob], filename, { type: 'image/jpeg' })
|
currentFile.value = new File([blob], filename, { type: 'image/jpeg' })
|
||||||
|
|
||||||
currentFile.value = file
|
|
||||||
imgSrc.value = URL.createObjectURL(blob)
|
imgSrc.value = URL.createObjectURL(blob)
|
||||||
console.log('✅ 照片已捕获:', filename, (file.size / 1024).toFixed(2) + 'KB')
|
stopCamera()
|
||||||
}, 'image/jpeg', 0.95)
|
}, 'image/jpeg', 0.95)
|
||||||
}
|
}
|
||||||
|
|
||||||
const retake = () => {
|
const retake = () => {
|
||||||
if (imgSrc.value) {
|
destroyCropper()
|
||||||
URL.revokeObjectURL(imgSrc.value)
|
isEditing.value = false
|
||||||
}
|
if (imgSrc.value) URL.revokeObjectURL(imgSrc.value)
|
||||||
imgSrc.value = ''
|
imgSrc.value = ''
|
||||||
currentFile.value = null
|
currentFile.value = null
|
||||||
|
startCamera()
|
||||||
|
}
|
||||||
|
|
||||||
if (!mediaStream.value || !mediaStream.value.active) {
|
const startEdit = () => {
|
||||||
startCamera()
|
if (!imgSrc.value || !previewImgRef.value) return
|
||||||
|
isEditing.value = true
|
||||||
|
isMoveMode.value = false
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (cropper) cropper.destroy()
|
||||||
|
|
||||||
|
cropper = new Cropper(previewImgRef.value!, {
|
||||||
|
viewMode: 1,
|
||||||
|
dragMode: 'none',
|
||||||
|
autoCropArea: 0.8,
|
||||||
|
background: false,
|
||||||
|
modal: true,
|
||||||
|
guides: true,
|
||||||
|
highlight: false,
|
||||||
|
cropBoxMovable: true,
|
||||||
|
cropBoxResizable: true,
|
||||||
|
toggleDragModeOnDblclick: false,
|
||||||
|
movable: true,
|
||||||
|
zoomable: true,
|
||||||
|
rotatable: true,
|
||||||
|
scalable: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDragMode = () => {
|
||||||
|
if (!cropper) return
|
||||||
|
isMoveMode.value = !isMoveMode.value
|
||||||
|
cropper.setDragMode(isMoveMode.value ? 'move' : 'none')
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopEdit = () => {
|
||||||
|
destroyCropper()
|
||||||
|
isEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const destroyCropper = () => {
|
||||||
|
if (cropper) {
|
||||||
|
cropper.destroy()
|
||||||
|
cropper = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rotateLeft = () => cropper?.rotate(-90)
|
||||||
|
const rotateRight = () => cropper?.rotate(90)
|
||||||
|
const resetCrop = () => {
|
||||||
|
cropper?.reset()
|
||||||
|
isMoveMode.value = false
|
||||||
|
cropper?.setDragMode('none')
|
||||||
|
}
|
||||||
|
const zoomCropper = (ratio: number) => cropper?.zoom(ratio)
|
||||||
|
|
||||||
const confirmUse = () => {
|
const confirmUse = () => {
|
||||||
console.log('👆 点击了确认使用')
|
console.log('👆 确认使用')
|
||||||
if (currentFile.value) {
|
|
||||||
try {
|
if (isEditing.value && cropper) {
|
||||||
console.log('📤 发送 photo-submit 事件', currentFile.value)
|
const croppedCanvas = cropper.getCroppedCanvas({
|
||||||
emit('photo-submit', currentFile.value)
|
imageSmoothingQuality: 'high'
|
||||||
} catch (err) {
|
})
|
||||||
console.error('父组件处理事件失败:', err)
|
|
||||||
|
if (!croppedCanvas) {
|
||||||
|
ElMessage.error('图片处理失败')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ElMessage.warning('照片数据未就绪,请稍后或重拍')
|
croppedCanvas.toBlob((blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
ElMessage.error('文件生成失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const timestamp = new Date().getTime()
|
||||||
|
const filename = `photo_crop_${timestamp}.jpg`
|
||||||
|
const file = new File([blob], filename, { type: 'image/jpeg' })
|
||||||
|
|
||||||
|
emitFile(file)
|
||||||
|
}, 'image/jpeg', 0.9)
|
||||||
|
}
|
||||||
|
else if (currentFile.value) {
|
||||||
|
emitFile(currentFile.value)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ElMessage.warning('没有可用的照片')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitFile = (file: File) => {
|
||||||
|
try {
|
||||||
|
console.log('📤 提交文件:', file.name, (file.size/1024).toFixed(1)+'KB')
|
||||||
|
emit('photo-submit', file)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('父组件处理事件失败:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
destroyCropper()
|
||||||
stopCamera()
|
stopCamera()
|
||||||
emit('cancel')
|
emit('cancel')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => startCamera())
|
||||||
startCamera()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
destroyCropper()
|
||||||
stopCamera()
|
stopCamera()
|
||||||
if (imgSrc.value) {
|
if (imgSrc.value) URL.revokeObjectURL(imgSrc.value)
|
||||||
URL.revokeObjectURL(imgSrc.value)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({ startCamera, stopCamera })
|
||||||
startCamera,
|
|
||||||
stopCamera
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.camera-container {
|
.camera-container.is-fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; width: 100vw; height: 100vh;
|
||||||
|
background-color: #000;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #fff;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 媒体显示区 */
|
||||||
.media-box {
|
.media-box {
|
||||||
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 640px;
|
position: relative;
|
||||||
height: 400px;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-content {
|
.media-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
|
transition: transform 0.2s ease-out; /* 变焦平滑动画 */
|
||||||
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
.error-message {
|
|
||||||
color: #f56c6c;
|
.editor-container { width: 100%; height: 100%; }
|
||||||
padding: 40px 20px;
|
.preview-img { display: block; max-width: 100%; max-height: 100%; }
|
||||||
text-align: center;
|
|
||||||
background: #fef0f0;
|
/* 变焦滑块 */
|
||||||
border-radius: 8px;
|
.zoom-slider-container {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
}
|
bottom: 150px;
|
||||||
.camera-actions {
|
left: 50%;
|
||||||
margin-top: 20px;
|
transform: translateX(-50%);
|
||||||
|
width: 70%;
|
||||||
|
max-width: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 10px;
|
||||||
|
z-index: 20;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-icon { color: #fff; font-size: 18px; }
|
||||||
|
.custom-slider { flex: 1; }
|
||||||
|
:deep(.el-slider__runway) { background-color: #555; }
|
||||||
|
:deep(.el-slider__bar) { background-color: #fff; }
|
||||||
|
:deep(.el-slider__button) { border-color: #fff; }
|
||||||
|
|
||||||
|
/* 底部操作栏 */
|
||||||
|
.camera-actions {
|
||||||
|
height: 140px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拍照按钮布局 */
|
||||||
|
.camera-actions:has(.shutter-btn) {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-btn { width: 40px; }
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.action-btn { background: rgba(255, 255, 255, 0.15); border: none; color: #fff; }
|
||||||
|
|
||||||
|
.text-btn {
|
||||||
|
color: #fff;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.confirm-btn { min-width: 120px; font-weight: bold; }
|
||||||
|
|
||||||
|
/* 快门按钮 */
|
||||||
|
.shutter-btn {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border: 4px solid #fff;
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.shutter-inner {
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
.shutter-btn:active .shutter-inner {
|
||||||
|
transform: scale(0.9);
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览模式操作栏 */
|
||||||
|
.preview-mode-bar {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编辑模式操作栏 */
|
||||||
|
.edit-mode-bar {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.edit-tools {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.tool-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.tool-btn.is-active {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-confirm {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cropper 样式定制 */
|
||||||
|
:deep(.cropper-view-box) {
|
||||||
|
outline: 3px solid #409EFF;
|
||||||
|
outline-color: #409EFF;
|
||||||
|
}
|
||||||
|
:deep(.cropper-point) {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #409EFF;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
:deep(.cropper-line) {
|
||||||
|
background-color: rgba(64, 158, 255, 0.5);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -353,7 +353,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, 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'
|
||||||
// 修复:引入 ElLoading
|
// 修复点:引入 ElLoading
|
||||||
import { ElMessage, ElLoading } from 'element-plus'
|
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'
|
||||||
@ -389,6 +389,7 @@ const inspectionFileList = ref<any[]>([]) // 检测报告
|
|||||||
|
|
||||||
const cameraDialogVisible = ref(false)
|
const cameraDialogVisible = ref(false)
|
||||||
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null)
|
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('')
|
||||||
@ -573,6 +574,10 @@ const triggerCamera = (field: any) => {
|
|||||||
currentCameraField.value = field;
|
currentCameraField.value = field;
|
||||||
cameraDialogVisible.value = true;
|
cameraDialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// 修复核心:拍照上传回调逻辑
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
const handleCameraConfirm = async (file: File) => {
|
const handleCameraConfirm = async (file: File) => {
|
||||||
if (!beforeAvatarUpload(file)) {
|
if (!beforeAvatarUpload(file)) {
|
||||||
cameraDialogVisible.value = false;
|
cameraDialogVisible.value = false;
|
||||||
@ -581,23 +586,33 @@ const handleCameraConfirm = async (file: File) => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
// 修复点:使用 ElLoading
|
// 使用 ElLoading.service 替代报错的 ElMessage.loading
|
||||||
const loadingMsg = ElLoading.service({ text: '照片上传中...', background: 'rgba(0, 0, 0, 0.7)' });
|
const loadingMsg = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '照片处理中...',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
});
|
||||||
|
|
||||||
let success = false;
|
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 newUrl = res.data.url;
|
||||||
const field = currentCameraField.value;
|
const field = currentCameraField.value; // 根据触发时记录的字段
|
||||||
|
|
||||||
|
// 添加到表单数据
|
||||||
form[field].push(newUrl);
|
form[field].push(newUrl);
|
||||||
|
|
||||||
|
// 更新对应的显示列表
|
||||||
|
const previewItem = { name: newUrl.split('/').pop(), url: getImageUrl(newUrl) };
|
||||||
if (field === 'product_photo') {
|
if (field === 'product_photo') {
|
||||||
productPhotoList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) });
|
productPhotoList.value.push(previewItem);
|
||||||
} else if (field === 'quality_report_link') {
|
} else if (field === 'quality_report_link') {
|
||||||
qualityFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) });
|
qualityFileList.value.push(previewItem);
|
||||||
} else if (field === 'inspection_report_link') {
|
} else if (field === 'inspection_report_link') {
|
||||||
inspectionFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) });
|
inspectionFileList.value.push(previewItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success('拍照上传成功');
|
ElMessage.success('拍照上传成功');
|
||||||
success = true;
|
success = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user