|
|
@@ -10,8 +10,26 @@
|
|
|
<!-- 原图显示区域 -->
|
|
|
<image class="original-image" :src="imageSrc" mode="aspectFit"></image>
|
|
|
|
|
|
- <!-- 裁剪框遮罩 -->
|
|
|
- <view class="crop-mask"></view>
|
|
|
+ <!-- 用于裁剪的 canvas(隐藏)- 仅在小程序环境中渲染 -->
|
|
|
+ <canvas v-if="!isH5" canvas-id="cropCanvas" id="cropCanvas" class="crop-canvas" width="200" height="200"></canvas>
|
|
|
+
|
|
|
+ <!-- 四块镂空遮罩,实现框外变暗、框内高亮 -->
|
|
|
+ <view class="mask-top" :style="{ height: cropBox.y + 'px' }"></view>
|
|
|
+ <view class="mask-left" :style="{
|
|
|
+ top: cropBox.y + 'px',
|
|
|
+ width: cropBox.x + 'px',
|
|
|
+ height: cropBox.size + 'px'
|
|
|
+ }"></view>
|
|
|
+ <view class="mask-right" :style="{
|
|
|
+ top: cropBox.y + 'px',
|
|
|
+ left: cropBox.x + cropBox.size + 'px',
|
|
|
+ width: containerSize.width - cropBox.x - cropBox.size + 'px',
|
|
|
+ height: cropBox.size + 'px'
|
|
|
+ }"></view>
|
|
|
+ <view class="mask-bottom" :style="{
|
|
|
+ top: cropBox.y + cropBox.size + 'px',
|
|
|
+ height: containerSize.height - cropBox.y - cropBox.size + 'px'
|
|
|
+ }"></view>
|
|
|
|
|
|
<!-- 裁剪框 -->
|
|
|
<view class="crop-box" :style="{
|
|
|
@@ -43,6 +61,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+import { baseUrl } from '@/common/config.js'
|
|
|
export default {
|
|
|
data() {
|
|
|
return {
|
|
|
@@ -68,16 +87,26 @@ export default {
|
|
|
touchStart: {},
|
|
|
resizeStart: {},
|
|
|
isDragging: false,
|
|
|
- isResizing: false
|
|
|
+ isResizing: false,
|
|
|
+ isH5: false,
|
|
|
+ tempBlobUrl: ''
|
|
|
}
|
|
|
},
|
|
|
onLoad(options) {
|
|
|
this.imageSrc = decodeURIComponent(options.imageSrc)
|
|
|
this.initCropBox()
|
|
|
+ const sysInfo = uni.getSystemInfoSync()
|
|
|
+ this.isH5 = sysInfo.platform === 'h5'
|
|
|
},
|
|
|
onReady() {
|
|
|
this.getImageInfo()
|
|
|
},
|
|
|
+ onUnload() {
|
|
|
+ // 销毁blob内存
|
|
|
+ if (this.isH5 && this.tempBlobUrl) {
|
|
|
+ URL.revokeObjectURL(this.tempBlobUrl)
|
|
|
+ }
|
|
|
+ },
|
|
|
methods: {
|
|
|
initCropBox() {
|
|
|
this.cropBox.size = 200
|
|
|
@@ -91,7 +120,6 @@ export default {
|
|
|
this.calculateCropBox()
|
|
|
},
|
|
|
fail: () => {
|
|
|
- // 如果获取图片信息失败,使用默认尺寸
|
|
|
this.imageSize.width = 1000
|
|
|
this.imageSize.height = 1000
|
|
|
this.calculateCropBox()
|
|
|
@@ -100,45 +128,23 @@ export default {
|
|
|
},
|
|
|
calculateCropBox() {
|
|
|
const sysInfo = uni.getSystemInfoSync()
|
|
|
-
|
|
|
- // header高度约为 88rpx = 44px
|
|
|
const headerHeight = 44
|
|
|
-
|
|
|
this.containerSize.width = sysInfo.windowWidth
|
|
|
this.containerSize.height = sysInfo.windowHeight - headerHeight
|
|
|
-
|
|
|
- // 计算图片缩放比例(aspectFit模式)
|
|
|
this.scale = Math.min(
|
|
|
this.containerSize.width / this.imageSize.width,
|
|
|
this.containerSize.height / this.imageSize.height
|
|
|
)
|
|
|
-
|
|
|
- // 图片实际显示尺寸
|
|
|
this.displayWidth = this.imageSize.width * this.scale
|
|
|
this.displayHeight = this.imageSize.height * this.scale
|
|
|
-
|
|
|
- // 图片居中偏移(考虑状态栏和header)
|
|
|
this.imageOffsetX = (this.containerSize.width - this.displayWidth) / 2
|
|
|
this.imageOffsetY = (this.containerSize.height - this.displayHeight) / 2
|
|
|
-
|
|
|
- // 如果图片比容器小,确保偏移量不为负
|
|
|
this.imageOffsetX = Math.max(0, this.imageOffsetX)
|
|
|
this.imageOffsetY = Math.max(0, this.imageOffsetY)
|
|
|
-
|
|
|
- // 裁剪框最大尺寸(不超过图片显示尺寸)
|
|
|
const maxBoxSize = Math.min(200, Math.min(this.displayWidth, this.displayHeight))
|
|
|
this.cropBox.size = Math.max(80, maxBoxSize)
|
|
|
-
|
|
|
- // 裁剪框初始位置(居中在图片上)
|
|
|
this.cropBox.x = this.imageOffsetX + (this.displayWidth - this.cropBox.size) / 2
|
|
|
this.cropBox.y = this.imageOffsetY + (this.displayHeight - this.cropBox.size) / 2
|
|
|
-
|
|
|
- // 调试信息
|
|
|
- console.log('容器高度:', this.containerSize.height)
|
|
|
- console.log('图片显示高度:', this.displayHeight)
|
|
|
- console.log('图片Y偏移:', this.imageOffsetY)
|
|
|
- console.log('裁剪框初始Y:', this.cropBox.y)
|
|
|
- console.log('裁剪框最大Y:', this.imageOffsetY + this.displayHeight - this.cropBox.size)
|
|
|
},
|
|
|
onTouchStart(e) {
|
|
|
const touch = e.touches[0]
|
|
|
@@ -153,27 +159,19 @@ export default {
|
|
|
onTouchMove(e) {
|
|
|
if (!this.isDragging) return
|
|
|
e.preventDefault()
|
|
|
-
|
|
|
const touch = e.touches[0]
|
|
|
const deltaX = touch.clientX - this.touchStart.x
|
|
|
const deltaY = touch.clientY - this.touchStart.y
|
|
|
-
|
|
|
let newX = this.touchStart.boxX + deltaX
|
|
|
let newY = this.touchStart.boxY + deltaY
|
|
|
-
|
|
|
- // 限制裁剪框在图片显示区域内
|
|
|
const maxX = this.imageOffsetX + this.displayWidth - this.cropBox.size
|
|
|
const maxY = this.imageOffsetY + this.displayHeight - this.cropBox.size
|
|
|
-
|
|
|
- // 确保边界值有效
|
|
|
const effectiveMinX = Math.min(this.imageOffsetX, maxX)
|
|
|
const effectiveMaxX = Math.max(this.imageOffsetX, maxX)
|
|
|
const effectiveMinY = Math.min(this.imageOffsetY, maxY)
|
|
|
const effectiveMaxY = Math.max(this.imageOffsetY, maxY)
|
|
|
-
|
|
|
newX = Math.max(effectiveMinX, Math.min(newX, effectiveMaxX))
|
|
|
newY = Math.max(effectiveMinY, Math.min(newY, effectiveMaxY))
|
|
|
-
|
|
|
this.cropBox.x = newX
|
|
|
this.cropBox.y = newY
|
|
|
},
|
|
|
@@ -190,33 +188,24 @@ export default {
|
|
|
}
|
|
|
this.isDragging = true
|
|
|
this.isResizing = false
|
|
|
-
|
|
|
document.addEventListener('mousemove', this.onMouseMove)
|
|
|
document.addEventListener('mouseup', this.onMouseUp)
|
|
|
},
|
|
|
onMouseMove(e) {
|
|
|
if (!this.isDragging && !this.isResizing) return
|
|
|
-
|
|
|
if (this.isDragging && !this.isResizing) {
|
|
|
const deltaX = e.clientX - this.touchStart.x
|
|
|
const deltaY = e.clientY - this.touchStart.y
|
|
|
-
|
|
|
let newX = this.touchStart.boxX + deltaX
|
|
|
let newY = this.touchStart.boxY + deltaY
|
|
|
-
|
|
|
- // 限制裁剪框在图片显示区域内
|
|
|
const maxX = this.imageOffsetX + this.displayWidth - this.cropBox.size
|
|
|
const maxY = this.imageOffsetY + this.displayHeight - this.cropBox.size
|
|
|
-
|
|
|
- // 确保边界值有效
|
|
|
const effectiveMinX = Math.min(this.imageOffsetX, maxX)
|
|
|
const effectiveMaxX = Math.max(this.imageOffsetX, maxX)
|
|
|
const effectiveMinY = Math.min(this.imageOffsetY, maxY)
|
|
|
const effectiveMaxY = Math.max(this.imageOffsetY, maxY)
|
|
|
-
|
|
|
newX = Math.max(effectiveMinX, Math.min(newX, effectiveMaxX))
|
|
|
newY = Math.max(effectiveMinY, Math.min(newY, effectiveMaxY))
|
|
|
-
|
|
|
this.cropBox.x = newX
|
|
|
this.cropBox.y = newY
|
|
|
} else if (this.isResizing) {
|
|
|
@@ -241,7 +230,6 @@ export default {
|
|
|
}
|
|
|
this.isResizing = true
|
|
|
this.isDragging = false
|
|
|
-
|
|
|
document.addEventListener('touchmove', this.onCornerTouchMove)
|
|
|
document.addEventListener('touchend', this.onCornerTouchEnd)
|
|
|
},
|
|
|
@@ -267,7 +255,6 @@ export default {
|
|
|
}
|
|
|
this.isResizing = true
|
|
|
this.isDragging = false
|
|
|
-
|
|
|
document.addEventListener('mousemove', this.onCornerMouseMove)
|
|
|
document.addEventListener('mouseup', this.onCornerMouseUp)
|
|
|
},
|
|
|
@@ -284,42 +271,32 @@ export default {
|
|
|
const deltaX = clientX - this.resizeStart.x
|
|
|
const deltaY = clientY - this.resizeStart.y
|
|
|
const corner = this.resizeStart.corner
|
|
|
-
|
|
|
let newSize = this.resizeStart.boxSize
|
|
|
let newX = this.resizeStart.boxX
|
|
|
let newY = this.resizeStart.boxY
|
|
|
-
|
|
|
const minSize = 80
|
|
|
const maxSize = Math.min(this.displayWidth, this.displayHeight)
|
|
|
-
|
|
|
switch (corner) {
|
|
|
case 'br':
|
|
|
- // 右下角:向右下方拖动放大
|
|
|
newSize = Math.max(minSize, Math.min(maxSize, this.resizeStart.boxSize + deltaX))
|
|
|
break
|
|
|
case 'bl':
|
|
|
- // 左下角:向左拖动放大,X位置右移
|
|
|
newSize = Math.max(minSize, Math.min(maxSize, this.resizeStart.boxSize - deltaX))
|
|
|
newX = this.resizeStart.boxX + deltaX
|
|
|
break
|
|
|
case 'tr':
|
|
|
- // 右上角:向上拖动放大,Y位置下移
|
|
|
newSize = Math.max(minSize, Math.min(maxSize, this.resizeStart.boxSize - deltaY))
|
|
|
newY = this.resizeStart.boxY + deltaY
|
|
|
break
|
|
|
case 'tl':
|
|
|
- // 左上角:向左上方拖动放大,X右移,Y下移
|
|
|
const deltaTL = Math.max(-deltaX, -deltaY)
|
|
|
newSize = Math.max(minSize, Math.min(maxSize, this.resizeStart.boxSize + deltaTL))
|
|
|
newX = this.resizeStart.boxX + deltaX
|
|
|
newY = this.resizeStart.boxY + deltaY
|
|
|
break
|
|
|
}
|
|
|
-
|
|
|
- // 确保位置在图片边界内
|
|
|
newX = Math.max(this.imageOffsetX, Math.min(newX, this.imageOffsetX + this.displayWidth - newSize))
|
|
|
newY = Math.max(this.imageOffsetY, Math.min(newY, this.imageOffsetY + this.displayHeight - newSize))
|
|
|
-
|
|
|
this.cropBox.size = newSize
|
|
|
this.cropBox.x = newX
|
|
|
this.cropBox.y = newY
|
|
|
@@ -329,54 +306,266 @@ export default {
|
|
|
},
|
|
|
confirm() {
|
|
|
uni.showLoading({ title: '裁剪中...' })
|
|
|
-
|
|
|
+ const sysInfo = uni.getSystemInfoSync()
|
|
|
+ const isH5 = sysInfo.platform === 'h5'
|
|
|
+ if (isH5) {
|
|
|
+ this.cropImageH5()
|
|
|
+ } else {
|
|
|
+ this.cropImageMiniProgram()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ cropImageH5() {
|
|
|
+ // 销毁上一次blob,释放内存
|
|
|
+ if (this.tempBlobUrl) {
|
|
|
+ URL.revokeObjectURL(this.tempBlobUrl)
|
|
|
+ this.tempBlobUrl = ''
|
|
|
+ }
|
|
|
const canvas = document.createElement('canvas')
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
- canvas.width = this.cropBox.size
|
|
|
- canvas.height = this.cropBox.size
|
|
|
-
|
|
|
+ const outputSize = 150
|
|
|
+ canvas.width = outputSize
|
|
|
+ canvas.height = outputSize
|
|
|
const img = new Image()
|
|
|
img.crossOrigin = 'anonymous'
|
|
|
img.src = this.imageSrc
|
|
|
-
|
|
|
img.onload = () => {
|
|
|
- // 计算裁剪区域在原图中的坐标
|
|
|
const boxInImageX = this.cropBox.x - this.imageOffsetX
|
|
|
const boxInImageY = this.cropBox.y - this.imageOffsetY
|
|
|
-
|
|
|
const cropX = boxInImageX / this.scale
|
|
|
const cropY = boxInImageY / this.scale
|
|
|
const cropSize = this.cropBox.size / this.scale
|
|
|
-
|
|
|
- // 确保裁剪区域在原图范围内
|
|
|
const actualCropX = Math.max(0, cropX)
|
|
|
const actualCropY = Math.max(0, cropY)
|
|
|
const actualCropSize = Math.min(cropSize, img.width - actualCropX, img.height - actualCropY)
|
|
|
-
|
|
|
- ctx.drawImage(img, actualCropX, actualCropY, actualCropSize, actualCropSize, 0, 0, canvas.width, canvas.height)
|
|
|
-
|
|
|
- const croppedImage = canvas.toDataURL('image/png')
|
|
|
-
|
|
|
+ ctx.clearRect(0, 0, outputSize, outputSize)
|
|
|
+ ctx.drawImage(img, actualCropX, actualCropY, actualCropSize, actualCropSize, 0, 0, outputSize, outputSize)
|
|
|
+
|
|
|
+ const quality = 0.65 // 进一步降低压缩质量
|
|
|
+ if (canvas.toBlob) {
|
|
|
+ canvas.toBlob((blob) => {
|
|
|
+ this.tempBlobUrl = URL.createObjectURL(blob)
|
|
|
+ // 清空原图img内存缓冲区
|
|
|
+ img.src = ''
|
|
|
+ this.handleCropResult(this.tempBlobUrl)
|
|
|
+ }, 'image/jpeg', quality)
|
|
|
+ } else {
|
|
|
+ const jpgBase64 = canvas.toDataURL('image/jpeg', quality)
|
|
|
+ const arr = jpgBase64.split(',')
|
|
|
+ const byteStr = atob(arr[1])
|
|
|
+ const byteArr = new Uint8Array([...byteStr].map(c => c.charCodeAt(0)))
|
|
|
+ const blob = new Blob([byteArr], { type: 'image/jpeg' })
|
|
|
+ this.tempBlobUrl = URL.createObjectURL(blob)
|
|
|
+ img.src = ''
|
|
|
+ this.handleCropResult(this.tempBlobUrl)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ img.onerror = () => {
|
|
|
uni.hideLoading()
|
|
|
-
|
|
|
+ uni.showToast({ title: '裁剪失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ //
|
|
|
+ cropImageMiniProgram() {
|
|
|
+ uni.getImageInfo({
|
|
|
+ src: this.imageSrc,
|
|
|
+ success: (imgInfo) => {
|
|
|
+ const boxInImageX = this.cropBox.x - this.imageOffsetX
|
|
|
+ const boxInImageY = this.cropBox.y - this.imageOffsetY
|
|
|
+ const cropX = boxInImageX / this.scale
|
|
|
+ const cropY = boxInImageY / this.scale
|
|
|
+ const cropSize = this.cropBox.size / this.scale
|
|
|
+
|
|
|
+ // 确保裁剪区域不超出原图边界
|
|
|
+ const actualCropX = Math.max(0, Math.min(cropX, imgInfo.width - 1))
|
|
|
+ const actualCropY = Math.max(0, Math.min(cropY, imgInfo.height - 1))
|
|
|
+ const maxCropSize = Math.min(
|
|
|
+ cropSize,
|
|
|
+ imgInfo.width - actualCropX,
|
|
|
+ imgInfo.height - actualCropY
|
|
|
+ )
|
|
|
+ const actualCropSize = Math.max(1, maxCropSize)
|
|
|
+
|
|
|
+ // 输出尺寸等于实际裁剪区域大小,不填充,保持正方形
|
|
|
+ const outputSize = Math.min(Math.floor(cropSize), 200)
|
|
|
+
|
|
|
+ const ctx = uni.createCanvasContext('cropCanvas')
|
|
|
+ ctx.clearRect(0, 0, 200, 200)
|
|
|
+ ctx.drawImage(
|
|
|
+ this.imageSrc,
|
|
|
+ cropX, // 直接使用,不再调整
|
|
|
+ cropY, // 直接使用,不再调整
|
|
|
+ cropSize,
|
|
|
+ cropSize,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ outputSize,
|
|
|
+ outputSize
|
|
|
+ )
|
|
|
+ ctx.draw(false, () => {
|
|
|
+ uni.canvasToTempFilePath({
|
|
|
+ canvasId: 'cropCanvas',
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ width: outputSize,
|
|
|
+ height: outputSize,
|
|
|
+ fileType: 'jpeg',
|
|
|
+ quality: 0.8,
|
|
|
+ success: (res) => {
|
|
|
+ this.handleCropResult(res.tempFilePath)
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ uni.hideLoading()
|
|
|
+ console.error('canvas导出失败', err)
|
|
|
+ uni.showToast({ title: '裁剪导出图片失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '读取原图失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 裁剪完成统一入口:先上传,成功再回传页面
|
|
|
+ async handleCropResult(tempFilePath) {
|
|
|
+ try {
|
|
|
+ // 执行上传
|
|
|
+ const obj = await this.uploadCropImage(tempFilePath)
|
|
|
+ console.log('obj', obj)
|
|
|
// 更新上一页数据
|
|
|
const pages = getCurrentPages()
|
|
|
if (pages.length >= 2) {
|
|
|
const prevPage = pages[pages.length - 2]
|
|
|
if (prevPage && prevPage.$data && prevPage.$data.baseInfo) {
|
|
|
- prevPage.$data.baseInfo.portraitPhoto = croppedImage
|
|
|
+ prevPage.$data.baseInfo.portraitPhotoList = [{
|
|
|
+ url: obj.fullImgUrl,
|
|
|
+ //fileUrl: obj.fileUrl,
|
|
|
+ contentType: 'image/jpeg',
|
|
|
+ type: 'jpg',
|
|
|
+ size: obj.size
|
|
|
+ }]
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- uni.$emit('cropSuccess', croppedImage)
|
|
|
uni.navigateBack()
|
|
|
+ // 上传成功再返回线上地址给上一页
|
|
|
+ uni.$emit('cropSuccess', uploadUrl)
|
|
|
+ uni.navigateBack()
|
|
|
+ } catch (err) {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: err.message || '上传失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async getImageFileInfo(tempPath) {
|
|
|
+ const sysInfo = uni.getSystemInfoSync()
|
|
|
+ const isH5 = sysInfo.platform === 'h5'
|
|
|
+
|
|
|
+ if (isH5 && tempPath.startsWith('blob:')) {
|
|
|
+ // H5 微信服务号:从blob读取
|
|
|
+ const res = await fetch(tempPath)
|
|
|
+ const blob = await res.blob()
|
|
|
+ const size = blob.size
|
|
|
+ const type = blob.type || 'image/png'
|
|
|
+ const format = type.split('/')[1]
|
|
|
+ return {
|
|
|
+ size,
|
|
|
+ kb: (size / 1024).toFixed(2),
|
|
|
+ mb: (size / 1024 / 1024).toFixed(3),
|
|
|
+ type,
|
|
|
+ format
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 小程序:uni.getFileInfo 读取本地临时文件
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.getFileInfo({
|
|
|
+ filePath: tempPath,
|
|
|
+ success: async (res) => {
|
|
|
+ // 小程序无直接type,通过图片后缀判断
|
|
|
+ let format = 'png'
|
|
|
+ if (tempPath.includes('.jpg') || tempPath.includes('.jpeg')) format = 'jpeg'
|
|
|
+ if (tempPath.includes('.webp')) format = 'webp'
|
|
|
+ const type = `image/${format}`
|
|
|
+ const size = res.size
|
|
|
+ resolve({
|
|
|
+ size,
|
|
|
+ kb: (size / 1024).toFixed(2),
|
|
|
+ mb: (size / 1024 / 1024).toFixed(3),
|
|
|
+ type,
|
|
|
+ format
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fail: reject
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 格式化文件大小
|
|
|
+ formatFileSize(bytes) {
|
|
|
+ if (bytes === 0) return '0 B'
|
|
|
+ const k = 1024
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB']
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
|
+ },
|
|
|
+ // 【核心修复】完整上传函数,返回线上图片地址
|
|
|
+ async uploadCropImage(tempFilePath) {
|
|
|
+ const token = uni.getStorageSync('access-token')
|
|
|
+ if (!token) throw new Error('登录失效,请重新登录')
|
|
|
+ const headers = { Authorization: `tf: ${token}` }
|
|
|
+ const sysInfo = uni.getSystemInfoSync()
|
|
|
+ const isH5 = sysInfo.platform === 'h5'
|
|
|
+ let uploadResult = null
|
|
|
+
|
|
|
+ const imgInfo = await this.getImageFileInfo(tempFilePath)
|
|
|
+ console.log('裁剪图片完整信息:', imgInfo)
|
|
|
+ // 弹窗展示大小、格式
|
|
|
+ // uni.showToast({
|
|
|
+ // title: `大小${imgInfo.kb}KB ${imgInfo.format}`,
|
|
|
+ // icon: 'none',
|
|
|
+ // duration: 1800
|
|
|
+ // })
|
|
|
+
|
|
|
+ // 分支1:H5 微信服务号 blob 上传
|
|
|
+ if (isH5 && tempFilePath.startsWith('blob:')) {
|
|
|
+ const resBlob = await fetch(tempFilePath)
|
|
|
+ const blob = await resBlob.blob()
|
|
|
+ const size = blob.size
|
|
|
+ console.log('裁剪图片大小', size, this.formatFileSize(size))
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append('file', blob, 'crop_avatar.jpg')
|
|
|
+ formData.append('biz', 'temp')
|
|
|
+ const resp = await fetch(`${baseUrl}/common/uploads`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers,
|
|
|
+ body: formData
|
|
|
+ })
|
|
|
+ console.log('formData', formData)
|
|
|
+ uploadResult = await resp.json()
|
|
|
+
|
|
|
+ } else {
|
|
|
+ // 分支2:微信小程序 补充完整uni.uploadFile逻辑(原代码缺失)
|
|
|
+ uploadResult = await new Promise((resolve, reject) => {
|
|
|
+ uni.uploadFile({
|
|
|
+ url: `${baseUrl}/common/uploads`,
|
|
|
+ filePath: tempFilePath,
|
|
|
+ name: 'file',
|
|
|
+ header: headers,
|
|
|
+ formData: { biz: 'temp' },
|
|
|
+ success: (res) => resolve(JSON.parse(res.data)),
|
|
|
+ fail: reject
|
|
|
+ })
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- img.onerror = () => {
|
|
|
- uni.hideLoading()
|
|
|
- uni.showToast({ title: '裁剪失败', icon: 'none' })
|
|
|
- uni.navigateBack()
|
|
|
+ //console.log('上传结果', uploadResult)
|
|
|
+
|
|
|
+ // 校验后端返回
|
|
|
+ if (!uploadResult || uploadResult.code !== 200) {
|
|
|
+ throw new Error(uploadResult?.msg || '接口返回失败')
|
|
|
}
|
|
|
+ // 拼接完整线上图片地址
|
|
|
+ const fullImgUrl = `${baseUrl}/${uploadResult.url.replace(/^\//, '')}`
|
|
|
+ return { fileUrl: uploadResult.url, size: imgInfo.size, fullImgUrl: fullImgUrl }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -403,8 +592,14 @@ export default {
|
|
|
font-size: 32rpx;
|
|
|
padding: 10rpx 20rpx;
|
|
|
|
|
|
- &.cancel { color: #999; }
|
|
|
- &.confirm { color: #4CAF50; font-weight: bold; }
|
|
|
+ &.cancel {
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.confirm {
|
|
|
+ color: #4CAF50;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.header-title {
|
|
|
@@ -417,64 +612,134 @@ export default {
|
|
|
flex: 1;
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
-}
|
|
|
|
|
|
-.original-image {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
-}
|
|
|
+ .original-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
|
|
|
-.crop-mask {
|
|
|
- position: absolute;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- bottom: 0;
|
|
|
- background: rgba(0, 0, 0, 0.7);
|
|
|
-}
|
|
|
+ // 裁剪用canvas(隐藏)
|
|
|
+ .crop-canvas {
|
|
|
+ position: absolute;
|
|
|
+ width: 200px;
|
|
|
+ height: 200px;
|
|
|
+ opacity: 0;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
|
|
|
-.crop-box {
|
|
|
- position: absolute;
|
|
|
- border: 2px solid #fff;
|
|
|
- box-sizing: border-box;
|
|
|
- z-index: 10;
|
|
|
- cursor: move;
|
|
|
- touch-action: none;
|
|
|
- user-select: none;
|
|
|
+ // 四块半透明遮罩,框外变暗
|
|
|
+ .mask-top,
|
|
|
+ .mask-bottom,
|
|
|
+ .mask-left,
|
|
|
+ .mask-right {
|
|
|
+ position: absolute;
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ z-index: 5;
|
|
|
+ }
|
|
|
|
|
|
- .grid-lines {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- position: relative;
|
|
|
+ .mask-top {
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ top: 0;
|
|
|
+ }
|
|
|
|
|
|
- .grid-line {
|
|
|
- position: absolute;
|
|
|
- background: rgba(255, 255, 255, 0.5);
|
|
|
+ .mask-bottom {
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ }
|
|
|
|
|
|
- &.horizontal {
|
|
|
- width: 100%; height: 1px; top: 33.33%;
|
|
|
- &:nth-child(2) { top: 66.66%; }
|
|
|
- }
|
|
|
+ .mask-left {
|
|
|
+ left: 0;
|
|
|
+ }
|
|
|
|
|
|
- &.vertical {
|
|
|
- width: 1px; height: 100%; left: 33.33%;
|
|
|
- &:nth-child(4) { left: 66.66%; }
|
|
|
- }
|
|
|
- }
|
|
|
+ .mask-right {
|
|
|
+ right: 0;
|
|
|
}
|
|
|
|
|
|
- .corner-tl, .corner-tr, .corner-bl, .corner-br {
|
|
|
+ .crop-box {
|
|
|
position: absolute;
|
|
|
- width: 30rpx;
|
|
|
- height: 30rpx;
|
|
|
- border: 3px solid #fff;
|
|
|
- background: #333;
|
|
|
- cursor: nwse-resize;
|
|
|
- }
|
|
|
+ border: 2px solid #fff;
|
|
|
+ box-sizing: border-box;
|
|
|
+ z-index: 10;
|
|
|
+ cursor: move;
|
|
|
+ touch-action: none;
|
|
|
+ user-select: none;
|
|
|
+ background: transparent;
|
|
|
+ filter: brightness(1.08); // 轻微提亮框内图片,增强高亮对比
|
|
|
+
|
|
|
+ .grid-lines {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ .grid-line {
|
|
|
+ position: absolute;
|
|
|
+ background: rgba(255, 255, 255, 0.5);
|
|
|
+
|
|
|
+ &.horizontal {
|
|
|
+ width: 100%;
|
|
|
+ height: 1px;
|
|
|
+ top: 33.33%;
|
|
|
+
|
|
|
+ &:nth-child(2) {
|
|
|
+ top: 66.66%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.vertical {
|
|
|
+ width: 1px;
|
|
|
+ height: 100%;
|
|
|
+ left: 33.33%;
|
|
|
|
|
|
- .corner-tl { top: -15rpx; left: -15rpx; border-right: none; border-bottom: none; }
|
|
|
- .corner-tr { top: -15rpx; right: -15rpx; border-left: none; border-bottom: none; cursor: nesw-resize; }
|
|
|
- .corner-bl { bottom: -15rpx; left: -15rpx; border-right: none; border-top: none; cursor: nesw-resize; }
|
|
|
- .corner-br { bottom: -15rpx; right: -15rpx; border-left: none; border-top: none; }
|
|
|
+ &:nth-child(4) {
|
|
|
+ left: 66.66%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .corner-tl,
|
|
|
+ .corner-tr,
|
|
|
+ .corner-bl,
|
|
|
+ .corner-br {
|
|
|
+ position: absolute;
|
|
|
+ width: 30rpx;
|
|
|
+ height: 30rpx;
|
|
|
+ border: 3px solid #fff;
|
|
|
+ background: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .corner-tl {
|
|
|
+ top: -15rpx;
|
|
|
+ left: -15rpx;
|
|
|
+ border-right: none;
|
|
|
+ border-bottom: none;
|
|
|
+ cursor: nwse-resize;
|
|
|
+ }
|
|
|
+
|
|
|
+ .corner-tr {
|
|
|
+ top: -15rpx;
|
|
|
+ right: -15rpx;
|
|
|
+ border-left: none;
|
|
|
+ border-bottom: none;
|
|
|
+ cursor: nesw-resize;
|
|
|
+ }
|
|
|
+
|
|
|
+ .corner-bl {
|
|
|
+ bottom: -15rpx;
|
|
|
+ left: -15rpx;
|
|
|
+ border-right: none;
|
|
|
+ border-top: none;
|
|
|
+ cursor: nesw-resize;
|
|
|
+ }
|
|
|
+
|
|
|
+ .corner-br {
|
|
|
+ bottom: -15rpx;
|
|
|
+ right: -15rpx;
|
|
|
+ border-left: none;
|
|
|
+ border-top: none;
|
|
|
+ cursor: nwse-resize;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|