const config = require('../../../config/config.js'); const apiClient = require('../../../utils/api-client.js'); const cosManager = require('../../../utils/cos-manager.js'); Page({ data: { currentTool: '', isCanvasInit: false, canvasContext: null, canvas: null, systemInfo: {}, isRotated: false, // 相机状态 cameraPosition: 'back', // 默认后置摄像头 flashState: 'off', // 默认关闭闪光灯 hasTakenPhoto: false, // 是否已拍摄照片 // 图片状态 originalPhotoPath: '', // 原始照片路径 currentPhotoPath: '', // 当前编辑的照片路径 imageInfo: null, // 图片信息 imageScale: 1, // 图片缩放比例 imagePosition: { // 图片在屏幕上的位置 left: 0, top: 0, width: 0, height: 0 }, // 裁剪相关 isCropping: false, cropBox: { left: 100, top: 200, width: 400, height: 400, right: 500, bottom: 600 }, cropStartPos: null, draggingHandle: null, isMovingCropBox: false, containerRect: null, // 图片容器的位置信息 // 旋转相关 rotateAngle: 0, // 马赛克相关 mosaicData: [], // 存储马赛克数据 hasMosaic: false, // 是否有马赛克 mosaicSize: 25, // 初始马赛克大小 sizePercent: 50, // 滑块初始位置百分比 dragStartX: 0, // 触摸起始 X 坐标 canvasWidth: 0, // 画布宽度 canvasHeight: 0, // 画布高度 // 编辑状态 hasEdits: false, // 是否有编辑操作 imageRect: { left: 0, top: 0, width: 0, height: 0 } }, // 获取容器位置(在开启裁剪时调用) updateContainerRect() { return new Promise((resolve) => { const query = wx.createSelectorQuery().in(this); query.select('.image-container').boundingClientRect((rect) => { if (rect) { this.setData({ containerRect: rect }); resolve(rect); } else { resolve(null); } }).exec(); }); }, // 拖动整个裁剪框(优化版本,使用缓存的容器位置) startCropMove(e) { if (!this.data.isCropping) return; const touch = e.touches[0]; const { cropBox, imagePosition, containerRect } = this.data; if (!cropBox || !imagePosition) return; // 保存触摸坐标,避免异步回调中失效 const touchX = touch.clientX; const touchY = touch.clientY; // 优先使用缓存的容器位置,避免异步延迟 if (containerRect) { const relativeX = touchX - containerRect.left; const relativeY = touchY - containerRect.top; // 检查是否点击在裁剪框内 const isInsideCropBox = relativeX >= cropBox.left && relativeX <= cropBox.right && relativeY >= cropBox.top && relativeY <= cropBox.bottom; if (isInsideCropBox) { // 记录起始位置 this.setData({ cropStartPos: { x: touchX, y: touchY, relativeX: relativeX, relativeY: relativeY, cropX: cropBox.left, cropY: cropBox.top }, draggingHandle: null, isMovingCropBox: true }); } return; } // 如果没有缓存的容器位置,异步获取(兼容旧代码) const query = wx.createSelectorQuery().in(this); query.select('.image-container').boundingClientRect((rect) => { if (!rect) return; const relativeX = touchX - rect.left; const relativeY = touchY - rect.top; // 检查是否点击在裁剪框内 const isInsideCropBox = relativeX >= cropBox.left && relativeX <= cropBox.right && relativeY >= cropBox.top && relativeY <= cropBox.bottom; if (isInsideCropBox) { // 记录起始位置,并缓存容器位置 this.setData({ cropStartPos: { x: touchX, y: touchY, relativeX: relativeX, relativeY: relativeY, cropX: cropBox.left, cropY: cropBox.top }, draggingHandle: null, isMovingCropBox: true, containerRect: rect }); } }).exec(); }, // 滑动 slider 时触发 onSliderChange(e) { const percent = e.detail.value; const newSize = 5 + (percent / 100) * 25; this.setData({ sizePercent: percent, mosaicSize: newSize }); }, onLoad() { try { const systemInfo = wx.getSystemInfoSync(); this.setData({ systemInfo }); this.systemInfo = systemInfo; } catch (err) { console.error('获取系统信息失败:', err); } // 初始化双击检测变量(不使用 setData,避免不必要的渲染) this.lastTapTime = 0; this.tapTimer = null; // 移除手动权限检查,让 组件自动处理权限请求 // 这样可以避免权限弹窗弹出两次(组件初始化时会自动请求权限) }, toggleFlash() { const newState = this.data.flashState === 'off' ? 'torch' : 'off'; this.setData({ flashState: newState }); }, switchCamera() { const newPosition = this.data.cameraPosition === 'back' ? 'front' : 'back'; this.setData({ cameraPosition: newPosition }); }, // 双击切换摄像头(优化版本,使用局部变量,响应更快) onCameraDoubleTap(e) { const currentTime = Date.now(); const timeDiff = currentTime - this.lastTapTime; // 清除之前的定时器 if (this.tapTimer) { clearTimeout(this.tapTimer); this.tapTimer = null; } // 如果两次点击间隔小于 250ms,认为是双击 if (timeDiff < 250 && this.lastTapTime !== 0) { // 立即执行切换摄像头 this.switchCamera(); // 重置时间,防止连续触发 this.lastTapTime = 0; } else { // 记录本次点击时间(使用实例变量,不触发 setData) this.lastTapTime = currentTime; // 250ms 后重置,防止误触发 this.tapTimer = setTimeout(() => { this.lastTapTime = 0; this.tapTimer = null; }, 250); } }, chooseFromAlbum() { wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album'], success: (res) => { const tempImagePath = res.tempFiles[0].tempFilePath; wx.getImageInfo({ src: tempImagePath, success: (info) => { this.setData({ originalPhotoPath: tempImagePath, currentPhotoPath: tempImagePath, imageInfo: info, hasTakenPhoto: true, hasEdits: false, rotateAngle: 0, mosaicData: [], hasMosaic: false, // 选择图片后初始化裁剪框 cropBox: this.calculateInitialCropBox(info) }, () => { this.initMosaicCanvas(); }); } }); }, fail: (err) => { console.error('选择图片失败:', err); wx.showToast({ title: '选择图片失败', icon: 'none' }); } }); }, takePhoto() { const ctx = wx.createCameraContext(); ctx.takePhoto({ quality: 'high', success: (res) => { wx.getImageInfo({ src: res.tempImagePath, success: (info) => { this.setData({ originalPhotoPath: res.tempImagePath, currentPhotoPath: res.tempImagePath, imageInfo: info, hasTakenPhoto: true, hasEdits: false, rotateAngle: 0, mosaicData: [], hasMosaic: false, // 拍照后初始化裁剪框 cropBox: this.calculateInitialCropBox() }, () => { this.initMosaicCanvas(); }); } }); }, fail: (err) => { console.error('拍照失败:', err); wx.showToast({ title: '拍照失败', icon: 'none' }); } }); }, // 计算初始裁剪框(确保在可视区域内) calculateInitialCropBox(imageInfo) { const targetImageInfo = imageInfo || this.data.imageInfo; const imagePosition = this.data.imagePosition || { width: 0, height: 0 }; if (!targetImageInfo || !imagePosition.width || !imagePosition.height) { return { left: 0, top: 0, width: 100, height: 100, right: 100, bottom: 100 }; } // 使用图片显示尺寸(相对于容器) const displayWidth = imagePosition.width; const displayHeight = imagePosition.height; const minCropSize = 80; // 最小裁剪尺寸 // 初始裁剪框大小为图片的70%,但确保不小于最小尺寸 const cropSize = Math.max( minCropSize, Math.min(displayWidth, displayHeight) * 0.7 ); // 居中显示,确保在图片范围内 let cropX = (displayWidth - cropSize) / 2; let cropY = (displayHeight - cropSize) / 2; // 确保裁剪框完全在图片可视区域内 cropX = Math.max(0, Math.min(cropX, displayWidth - cropSize)); cropY = Math.max(0, Math.min(cropY, displayHeight - cropSize)); return { left: cropX, top: cropY, width: cropSize, height: cropSize, right: cropX + cropSize, bottom: cropY + cropSize }; }, onImageLoaded(e) { const { width, height } = e.detail; const { systemInfo, imageScale } = this.data; const scale = imageScale || Math.min( systemInfo.windowWidth / width, systemInfo.windowHeight * 0.8 / height ); const displayWidth = width * scale; const displayHeight = height * scale; const imageLeft = (systemInfo.windowWidth - displayWidth) / 2; const imageTop = (systemInfo.windowHeight - displayHeight) / 2; // 获取图片容器的实际尺寸(用于裁剪框计算) const query = wx.createSelectorQuery().in(this); query.select('.image-container').boundingClientRect((rect) => { // 容器尺寸(用于裁剪框计算,裁剪框相对于容器定位) const containerWidth = rect ? rect.width : displayWidth; const containerHeight = rect ? rect.height : displayHeight; // 更新图片位置信息 // imageRect: 绝对位置(用于显示) // imagePosition: 相对于容器的位置(用于裁剪框计算) this.setData({ imageRect: { left: imageLeft, top: imageTop, width: displayWidth, height: displayHeight }, imagePosition: { left: 0, // 相对于容器 top: 0, // 相对于容器 width: containerWidth, height: containerHeight }, imageInfo: { width, height }, originalImageWidth: width, originalImageHeight: height, rotateAngle: 0, imageScale: 1, containerRect: rect || { left: 0, top: 0, width: containerWidth, height: containerHeight } }, () => { // 图片加载后若处于裁剪模式,重新初始化裁剪框 if (this.data.isCropping) { const newCropBox = this.calculateInitialCropBox(); if (newCropBox.width > 0 && newCropBox.height > 0) { this.setData({ cropBox: newCropBox }); } } }); }).exec(); // 初始化马赛克画布 if (this.data.currentTool === 'mosaic' || this.data.currentTool === 'eraser') { this.initMosaicCanvas(); } }, initMosaicCanvas() { return new Promise((resolve, reject) => { const query = wx.createSelectorQuery().in(this); query.select('#mosaicCanvas') .fields({ node: true, size: true }) .exec((res) => { if (!res || res.length === 0 || !res[0]) { reject('未找到 Canvas 元素'); return; } const canvasNode = res[0].node; const ctx = canvasNode.getContext('2d'); const { width, height } = this.data.imagePosition; canvasNode.width = width; canvasNode.height = height; this.setData({ canvasContext: ctx, canvas: canvasNode, canvasWidth: width, canvasHeight: height, isCanvasInit: true }, () => { // 初始化后重新渲染马赛克 this.reRenderMosaic(); resolve(); }); }); }); }, toggleCropMode() { this.setData({ isCropping: !this.data.isCropping, currentTool: '' }, () => { // 切换裁剪模式后,若开启裁剪则重新初始化裁剪框 if (this.data.isCropping) { // 先获取容器位置,确保 imagePosition 已更新 this.updateContainerRect().then(() => { const newCropBox = this.calculateInitialCropBox(); if (newCropBox.width > 0 && newCropBox.height > 0) { this.setData({ cropBox: newCropBox }); } }); } }); }, // 开始拖动角点(优化版本,使用缓存的容器位置) startCropDrag(e) { if (!this.data.isCropping) return; const handle = e.currentTarget.dataset.handle; if (!handle) { console.warn('未找到 handle 属性:', e.currentTarget); return; } const touch = e.touches[0]; if (!touch || !touch.clientX || !touch.clientY) { console.warn('触摸事件无效:', touch); return; } const { containerRect, cropBox } = this.data; // 保存触摸坐标,避免异步回调中失效 const touchX = touch.clientX; const touchY = touch.clientY; // 优先使用缓存的容器位置,避免异步延迟 if (containerRect && containerRect.left !== undefined && containerRect.top !== undefined) { const relativeX = touchX - containerRect.left; const relativeY = touchY - containerRect.top; this.setData({ cropStartPos: { x: touchX, y: touchY, relativeX: relativeX, relativeY: relativeY, cropX: cropBox?.left || 0, cropY: cropBox?.top || 0, cropRight: cropBox?.right || 0, cropBottom: cropBox?.bottom || 0 }, draggingHandle: handle, isMovingCropBox: false }); console.log(`开始拖动角点: ${handle}`, { touchX, touchY, relativeX, relativeY, cropBox }); return; } // 如果没有缓存的容器位置,使用估算值并立即响应,同时异步更新 const systemInfo = this.data.systemInfo || wx.getSystemInfoSync(); const estimatedRect = { left: 0, top: 0, width: systemInfo.windowWidth || 375, height: systemInfo.windowHeight || 667 }; const relativeX = touchX - estimatedRect.left; const relativeY = touchY - estimatedRect.top; // 立即设置拖动状态,使用估算的容器位置 this.setData({ cropStartPos: { x: touchX, y: touchY, relativeX: relativeX, relativeY: relativeY, cropX: cropBox?.left || 0, cropY: cropBox?.top || 0, cropRight: cropBox?.right || 0, cropBottom: cropBox?.bottom || 0 }, draggingHandle: handle, isMovingCropBox: false, containerRect: estimatedRect // 临时使用估算值 }); console.log(`开始拖动角点: ${handle} (使用估算位置)`, { touchX, touchY, relativeX, relativeY, cropBox }); // 异步获取准确的容器位置,用于后续拖动 const query = wx.createSelectorQuery().in(this); query.select('.image-container').boundingClientRect((rect) => { if (rect) { this.setData({ containerRect: rect }); console.log('容器位置已更新:', rect); } }).exec(); }, // 处理裁剪框拖动(优化版本,确保容器位置可用) onCropTouchMove(e) { if (!this.data.isCropping || !this.data.cropStartPos) return; const touch = e.touches[0]; const { cropBox, imagePosition, containerRect } = this.data; if (!cropBox || !imagePosition) return; // 获取容器位置(优先使用缓存的) let currentRect = containerRect; if (!currentRect) { // 如果没有缓存的,使用默认值(基于系统信息估算) const systemInfo = this.data.systemInfo || wx.getSystemInfoSync(); currentRect = { left: 0, top: 0, width: systemInfo.windowWidth || 375, height: systemInfo.windowHeight || 667 }; // 异步更新容器位置(用于下次使用) const query = wx.createSelectorQuery().in(this); query.select('.image-container').boundingClientRect((rect) => { if (rect) { this.setData({ containerRect: rect }); } }).exec(); } // 直接使用clientX/clientY计算相对坐标 const relativeX = touch.clientX - currentRect.left; const relativeY = touch.clientY - currentRect.top; const maxWidth = imagePosition.width; const maxHeight = imagePosition.height; const minCropSize = 60; // 最小裁剪尺寸 let newCropBox = { ...cropBox }; // 拖动整个裁剪框(参考avatar-edit: touch.clientX - cropSize / 2) if (this.data.isMovingCropBox) { // 参考avatar-edit的简单计算方式 const deltaX = relativeX - (this.data.cropStartPos.relativeX || 0); const deltaY = relativeY - (this.data.cropStartPos.relativeY || 0); let newX = (this.data.cropStartPos.cropX || cropBox.left) + deltaX; let newY = (this.data.cropStartPos.cropY || cropBox.top) + deltaY; // 参考avatar-edit的边界检查:Math.max(0, Math.min(newX, canvasWidth - cropSize)) newX = Math.max(0, Math.min(newX, maxWidth - cropBox.width)); newY = Math.max(0, Math.min(newY, maxHeight - cropBox.height)); newCropBox.left = newX; newCropBox.top = newY; newCropBox.right = newX + cropBox.width; newCropBox.bottom = newY + cropBox.height; // 调整裁剪框大小(4个角点自由拖动) } else if (this.data.draggingHandle) { const handle = this.data.draggingHandle; // 验证 handle 是否有效 if (!handle || !['top-left', 'top-right', 'bottom-left', 'bottom-right'].includes(handle)) { console.warn('无效的拖动角点:', handle); return; } // 获取初始裁剪框位置(从 cropStartPos 中获取,确保使用拖动开始时的位置) const startCropX = this.data.cropStartPos?.cropX ?? cropBox.left; const startCropY = this.data.cropStartPos?.cropY ?? cropBox.top; const startCropRight = this.data.cropStartPos?.cropRight ?? cropBox.right; const startCropBottom = this.data.cropStartPos?.cropBottom ?? cropBox.bottom; // 获取初始触摸位置 const startRelativeX = this.data.cropStartPos?.relativeX ?? relativeX; const startRelativeY = this.data.cropStartPos?.relativeY ?? relativeY; // 计算相对于初始触摸点的偏移量 const deltaX = relativeX - startRelativeX; const deltaY = relativeY - startRelativeY; // 根据拖动的角点调整裁剪框 switch (handle) { case 'top-left': // 左上角:调整 left 和 top,保持右下角不变 newCropBox.left = Math.max(0, Math.min(startCropX + deltaX, startCropRight - minCropSize)); newCropBox.top = Math.max(0, Math.min(startCropY + deltaY, startCropBottom - minCropSize)); newCropBox.right = startCropRight; newCropBox.bottom = startCropBottom; break; case 'top-right': // 右上角:调整 right 和 top,保持左下角不变 newCropBox.right = Math.max(startCropX + minCropSize, Math.min(startCropRight + deltaX, maxWidth)); newCropBox.top = Math.max(0, Math.min(startCropY + deltaY, startCropBottom - minCropSize)); newCropBox.left = startCropX; newCropBox.bottom = startCropBottom; break; case 'bottom-left': // 左下角:调整 left 和 bottom,保持右上角不变 newCropBox.left = Math.max(0, Math.min(startCropX + deltaX, startCropRight - minCropSize)); newCropBox.bottom = Math.max(startCropY + minCropSize, Math.min(startCropBottom + deltaY, maxHeight)); newCropBox.right = startCropRight; newCropBox.top = startCropY; break; case 'bottom-right': // 右下角:调整 right 和 bottom,保持左上角不变 newCropBox.right = Math.max(startCropX + minCropSize, Math.min(startCropRight + deltaX, maxWidth)); newCropBox.bottom = Math.max(startCropY + minCropSize, Math.min(startCropBottom + deltaY, maxHeight)); newCropBox.left = startCropX; newCropBox.top = startCropY; break; default: console.warn('未知的拖动角点:', handle); return; } // 重新计算宽高 newCropBox.width = newCropBox.right - newCropBox.left; newCropBox.height = newCropBox.bottom - newCropBox.top; // 确保最小尺寸 if (newCropBox.width < minCropSize) { if (handle.includes('left')) { newCropBox.left = newCropBox.right - minCropSize; } else { newCropBox.right = newCropBox.left + minCropSize; } newCropBox.width = minCropSize; } if (newCropBox.height < minCropSize) { if (handle.includes('top')) { newCropBox.top = newCropBox.bottom - minCropSize; } else { newCropBox.bottom = newCropBox.top + minCropSize; } newCropBox.height = minCropSize; } // 最终边界检查:确保裁剪框完全在图片可视区域内 // 如果超出左边界 if (newCropBox.left < 0) { const offset = -newCropBox.left; newCropBox.left = 0; if (handle.includes('right')) { newCropBox.right = Math.min(newCropBox.right + offset, maxWidth); } else { newCropBox.right = Math.min(newCropBox.right - offset, maxWidth); } } // 如果超出上边界 if (newCropBox.top < 0) { const offset = -newCropBox.top; newCropBox.top = 0; if (handle.includes('bottom')) { newCropBox.bottom = Math.min(newCropBox.bottom + offset, maxHeight); } else { newCropBox.bottom = Math.min(newCropBox.bottom - offset, maxHeight); } } // 如果超出右边界 if (newCropBox.right > maxWidth) { const offset = newCropBox.right - maxWidth; newCropBox.right = maxWidth; if (handle.includes('left')) { newCropBox.left = Math.max(0, newCropBox.left - offset); } else { newCropBox.left = Math.max(0, newCropBox.left + offset); } } // 如果超出下边界 if (newCropBox.bottom > maxHeight) { const offset = newCropBox.bottom - maxHeight; newCropBox.bottom = maxHeight; if (handle.includes('top')) { newCropBox.top = Math.max(0, newCropBox.top - offset); } else { newCropBox.top = Math.max(0, newCropBox.top + offset); } } // 重新计算宽高(边界调整后) newCropBox.width = newCropBox.right - newCropBox.left; newCropBox.height = newCropBox.bottom - newCropBox.top; } else { return; } // 更新裁剪框 this.setData({ cropBox: newCropBox }); }, // 裁剪框拖动结束(参考avatar-edit的简单实现) onCropTouchEnd(e) { this.setData({ cropStartPos: null, draggingHandle: null, isMovingCropBox: false }); }, // 应用裁剪(裁剪完成后直接上传,不预览) async applyCrop() { const { cropBox, currentPhotoPath, imageInfo, imagePosition } = this.data; if (!imagePosition || !imagePosition.width || !imagePosition.height || !imageInfo) { wx.showToast({ title: '图片信息不完整', icon: 'none' }); return; } if (!cropBox || !cropBox.width || !cropBox.height) { wx.showToast({ title: '裁剪框无效', icon: 'none' }); return; } wx.showLoading({ title: '裁剪中...', mask: true }); // 参考avatar-edit:使用页面中的canvas进行裁剪 const query = wx.createSelectorQuery().in(this); query.select('#mergeCanvas') .fields({ node: true, size: true }) .exec((res) => { if (!res || !res[0] || !res[0].node) { wx.hideLoading(); wx.showToast({ title: 'Canvas初始化失败', icon: 'none' }); return; } const canvas = res[0].node; const ctx = canvas.getContext('2d'); // 参考avatar-edit:计算裁剪区域在原图中的比例 const scaleX = imageInfo.width / imagePosition.width; const scaleY = imageInfo.height / imagePosition.height; // 参考avatar-edit:转换为原始图片的实际像素坐标 const sourceX = Math.max(0, cropBox.left * scaleX); const sourceY = Math.max(0, cropBox.top * scaleY); const sourceWidth = Math.max(1, Math.min(cropBox.width * scaleX, imageInfo.width - sourceX)); const sourceHeight = Math.max(1, Math.min(cropBox.height * scaleY, imageInfo.height - sourceY)); // 设置Canvas尺寸为裁剪大小 const canvasWidth = Math.ceil(sourceWidth); const canvasHeight = Math.ceil(sourceHeight); canvas.width = canvasWidth; canvas.height = canvasHeight; console.log('裁剪参数:', { cropBox, imageInfo, imagePosition, scaleX, scaleY, sourceX, sourceY, sourceWidth, sourceHeight, canvasWidth, canvasHeight }); // 加载图片 const img = canvas.createImage(); img.onerror = (err) => { wx.hideLoading(); console.error('图片加载失败:', err); wx.showToast({ title: '图片加载失败', icon: 'none' }); }; img.onload = () => { try { // 参考avatar-edit:仅绘制裁剪选中的区域 // drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) ctx.drawImage( img, sourceX, // 原始图片中裁剪区域的起点X sourceY, // 原始图片中裁剪区域的起点Y sourceWidth, // 原始图片中裁剪区域的宽度 sourceHeight, // 原始图片中裁剪区域的高度 0, 0, // 画布上的起点坐标 canvasWidth, // 画布上的宽度 canvasHeight // 画布上的高度 ); // 导出图片并直接上传 wx.canvasToTempFilePath({ canvas: canvas, success: async (res) => { try { // 验证文件 const fileInfo = await new Promise((resolve, reject) => { wx.getFileInfo({ filePath: res.tempFilePath, success: resolve, fail: reject }); }); if (!fileInfo || !fileInfo.size || fileInfo.size === 0) { throw new Error('裁剪后的文件无效'); } // 裁剪完成,直接上传(不预览) wx.hideLoading(); wx.showLoading({ title: '上传中...', mask: true }); // 调用原有的上传方法 const uploadResult = await cosManager.upload(res.tempFilePath, { fileType: 'image', enableDedup: false, onProgress: (percent) => { wx.showLoading({ title: `上传中 ${Math.round(percent)}%`, mask: true }); }, enableCompress: false, compressOptions: { maxWidth: 400, maxHeight: 400, targetSize: 20 * 1024 } }); if (!uploadResult || !uploadResult.success || !uploadResult.fileUrl) { throw new Error(uploadResult?.error || uploadResult?.message || '上传失败'); } wx.hideLoading(); // 跳转到edits页面 wx.redirectTo({ url: `/subpackages/media/edits/edits?imagePath=${encodeURIComponent(uploadResult.fileUrl)}`, fail: (err) => { console.error('跳转发布页失败:', err); wx.showToast({ title: '跳转失败,请重试', icon: 'none', duration: 2000 }); } }); } catch (error) { wx.hideLoading(); console.error('裁剪上传失败:', error); wx.showToast({ title: error.message || '裁剪上传失败,请重试', icon: 'none', duration: 2000 }); } }, fail: (err) => { wx.hideLoading(); console.error('生成裁剪图片失败:', err); wx.showToast({ title: '生成图片失败', icon: 'none' }); } }, this); } catch (error) { wx.hideLoading(); console.error('绘制裁剪图片失败:', error, error.stack); wx.showToast({ title: '绘制失败: ' + (error.message || '未知错误'), icon: 'none', duration: 3000 }); } }; img.src = currentPhotoPath; }); }, // 旋转 rotateImage() { // 如果正在裁剪,先关闭裁剪模式 if (this.data.isCropping) { this.setData({ isCropping: false, currentTool: '' }); } let { rotateAngle, cropBox, imageRect, mosaicData, imagePosition } = this.data; // 计算新旋转角度 const newRotateAngle = (rotateAngle - 90) % 360; const widthRatio = cropBox.width / imageRect.width; const heightRatio = cropBox.height / imageRect.height; const newWidth = cropBox.height; const newHeight = cropBox.width; const newLeft = imageRect.left + (imageRect.width - newWidth) / 2; const newTop = imageRect.top + (imageRect.height - newHeight) / 2; // 旋转历史马赛克坐标 const centerX = imagePosition.width / 2; const centerY = imagePosition.height / 2; const rad = -90 * Math.PI / 180; const cos = Math.cos(rad); const sin = Math.sin(rad); const rotatedMosaicData = mosaicData.map(point => { // 转换为相对图片中心的坐标 const x = point.x - centerX; const y = point.y - centerY; // 旋转坐标 const rotatedX = x * cos - y * sin + centerX; const rotatedY = x * sin + y * cos + centerY; return { ...point, x: rotatedX, y: rotatedY }; }); // 更新数据 this.setData({ rotateAngle: newRotateAngle, isRotated: true, hasEdits: true, cropBox: { left: newLeft, top: newTop, width: newWidth, height: newHeight, right: newLeft + newWidth, bottom: newTop + newHeight }, mosaicData: rotatedMosaicData }, () => { // 重新渲染马赛克 this.reRenderMosaic(); }); }, // 重新渲染马赛克 reRenderMosaic() { const { canvasContext, mosaicData, canvasWidth, canvasHeight } = this.data; if (!canvasContext) return; // 清空 Canvas 原有内容 canvasContext.clearRect(0, 0, canvasWidth, canvasHeight); // 重新绘制所有马赛克 mosaicData.forEach(point => { canvasContext.save(); canvasContext.fillStyle = 'rgba(128, 128, 128, 0.8)'; canvasContext.beginPath(); canvasContext.arc(point.x, point.y, point.size / 2, 0, 2 * Math.PI); canvasContext.fill(); canvasContext.restore(); }); }, // 合并图片(支持裁剪后的图片) async mergeImages() { return new Promise((resolve, reject) => { try { const { currentPhotoPath, imageInfo, rotateAngle, mosaicData, systemInfo } = this.data; // 验证必要数据 if (!currentPhotoPath) { reject(new Error('图片路径不存在')); return; } if (!imageInfo || !imageInfo.width || !imageInfo.height) { reject(new Error('图片信息不完整')); return; } if (!systemInfo || !systemInfo.windowWidth || !systemInfo.windowHeight) { reject(new Error('系统信息不完整')); return; } // 获取页面内的辅助 Canvas 节点 const query = wx.createSelectorQuery().in(this); query.select('#mergeCanvas') .fields({ node: true, size: true }) .exec((res) => { if (!res || res.length === 0 || !res[0] || !res[0].node) { reject(new Error('未找到辅助 Canvas 节点')); return; } const mergeCanvas = res[0].node; const mergeCtx = mergeCanvas.getContext('2d'); // 直接使用当前图片的原始尺寸(裁剪后已经是最终尺寸) const canvasWidth = imageInfo.width; const canvasHeight = imageInfo.height; // 设置 Canvas 尺寸为图片原始尺寸 mergeCanvas.width = canvasWidth; mergeCanvas.height = canvasHeight; console.log('合并图片参数:', { currentPhotoPath, imageInfo, rotateAngle, mosaicDataLength: mosaicData?.length || 0, canvasWidth, canvasHeight }); // 加载图片并绘制 const img = mergeCanvas.createImage(); // 设置超时,避免图片加载卡死 const timeoutId = setTimeout(() => { reject(new Error('图片加载超时')); }, 10000); // 10秒超时 img.onload = () => { clearTimeout(timeoutId); try { // 清空画布 mergeCtx.clearRect(0, 0, canvasWidth, canvasHeight); // 绘制旋转后的图片 mergeCtx.save(); mergeCtx.translate(canvasWidth / 2, canvasHeight / 2); mergeCtx.rotate(rotateAngle * Math.PI / 180); mergeCtx.drawImage( img, -canvasWidth / 2, -canvasHeight / 2, canvasWidth, canvasHeight ); mergeCtx.restore(); // 如果有马赛克,绘制马赛克(需要根据图片尺寸比例调整坐标) if (mosaicData && mosaicData.length > 0) { // 计算缩放比例(从显示尺寸到原始尺寸) const imagePosition = this.data.imagePosition || {}; let scaleX = 1; let scaleY = 1; if (imagePosition.width && imagePosition.height) { scaleX = canvasWidth / imagePosition.width; scaleY = canvasHeight / imagePosition.height; } mosaicData.forEach(point => { mergeCtx.fillStyle = 'rgba(128, 128, 128, 0.8)'; mergeCtx.beginPath(); mergeCtx.arc( point.x * scaleX, point.y * scaleY, (point.size / 2) * scaleX, 0, 2 * Math.PI ); mergeCtx.fill(); }); } // 导出图片 wx.canvasToTempFilePath({ canvas: mergeCanvas, x: 0, y: 0, width: canvasWidth, height: canvasHeight, destWidth: canvasWidth, destHeight: canvasHeight, success: (res) => { console.log('合并图片成功:', res.tempFilePath); resolve(res.tempFilePath); }, fail: (err) => { console.error('Canvas 导出临时路径失败:', err); reject(new Error('导出图片失败: ' + (err.errMsg || '未知错误'))); } }, this); } catch (error) { clearTimeout(timeoutId); console.error('绘制图片失败:', error); reject(new Error('绘制图片失败: ' + (error.message || '未知错误'))); } }; img.onerror = (err) => { clearTimeout(timeoutId); console.error('图片加载失败:', err, currentPhotoPath); reject(new Error('图片加载失败: ' + (err.errMsg || '未知错误'))); }; img.src = currentPhotoPath; }); } catch (error) { console.error('合并图片异常:', error); reject(new Error('合并图片失败: ' + (error.message || '未知错误'))); } }); }, cancelCrop() { this.setData({ isCropping: false, currentTool: '' }); }, selectTool(e) { // 获取点击的工具 const tool = e.currentTarget.dataset.tool; const currentActiveTool = this.data.currentTool; // 判断是否为"第二次点击" if (currentActiveTool === tool) { // 第二次点击:取消激活,清空工具状态 this.setData({ // 清空当前工具 currentTool: '', // 同步关闭裁剪 isCropping: false, }); return; } // 裁剪工具特殊处理 if (tool === 'crop') { this.setData({ currentTool: 'crop', isCropping: !this.data.isCropping }, () => { // 切换裁剪模式后,若开启裁剪则重新初始化裁剪框 if (this.data.isCropping) { // 先获取容器位置 this.updateContainerRect().then(() => { // 在 setData 回调中初始化,确保 imagePosition 已更新 setTimeout(() => { const newCropBox = this.calculateInitialCropBox(); if (newCropBox.width > 0 && newCropBox.height > 0) { this.setData({ cropBox: newCropBox }); } }, 50); }); } }); return; } // 第一次点击:激活工具,初始化 Canvas this.setData({ currentTool: tool, isCropping: false }, async () => { if (tool === 'mosaic' || tool === 'eraser') { await this.initMosaicCanvas(); // 重新绘制所有马赛克 const { canvasContext, mosaicData } = this.data; if (canvasContext && mosaicData.length > 0) { canvasContext.clearRect(0, 0, this.data.canvasWidth, this.data.canvasHeight); mosaicData.forEach(point => { canvasContext.fillStyle = 'rgba(128, 128, 128, 0.8)'; canvasContext.beginPath(); canvasContext.arc(point.x, point.y, point.size / 2, 0, 2 * Math.PI); canvasContext.fill(); }); } } }); }, onSizeTouchStart(e) { this.setData({ dragStartX: e.touches[0].clientX }); }, onSizeTouchMove(e) { const query = wx.createSelectorQuery().in(this); query.select('.mosaic-size-drag') .boundingClientRect((rect) => { if (!rect) return; const containerWidth = rect.width; const touchX = e.touches[0].clientX; const startX = rect.left; const endX = rect.right; // 计算滑块在容器内的百分比 let percent = ((touchX - startX) / containerWidth) * 100; percent = Math.max(0, Math.min(100, percent)); // 限制在0-100 // 计算马赛克大小 const newSize = 5 + (percent / 100) * 25; this.setData({ mosaicSize: newSize, thumbStyle: { left: `${percent}%` } }); }) .exec(); }, onCanvasTouchStart(e) { const { currentTool } = this.data; if (currentTool === 'mosaic' || currentTool === 'eraser') { this.handleCanvasDrawing(e); } }, onCanvasTouchMove(e) { const { currentTool } = this.data; if (currentTool === 'mosaic' || currentTool === 'eraser') { this.handleCanvasDrawing(e); } }, onCanvasTouchEnd() { // 触摸结束无需处理 }, // 画布绘制逻辑 handleCanvasDrawing(e) { const { currentTool, canvasContext, mosaicSize, mosaicData, imagePosition } = this.data; if (!canvasContext || !imagePosition) return; // 获取触摸点原始坐标 const touch = e.touches[0]; const rawX = touch.clientX; const rawY = touch.clientY; // Canvas在页面中的实际位置 const query = wx.createSelectorQuery().in(this); query.select('#mosaicCanvas') .boundingClientRect() .exec((res) => { if (res && res[0]) { const canvasRect = res[0]; // 计算相对于Canvas的触摸坐标 const canvasX = rawX - canvasRect.left; const canvasY = rawY - canvasRect.top; // 计算Canvas的逻辑尺寸与物理尺寸的比例 const realWidth = this.data.canvasWidth || imagePosition.width; const realHeight = this.data.canvasHeight || imagePosition.height; const scaleX = realWidth / canvasRect.width; const scaleY = realHeight / canvasRect.height; // 计算最终绘制坐标 const drawX = canvasX * scaleX; const drawY = canvasY * scaleY; // 绘制马赛克或橡皮擦效果 if (currentTool === 'mosaic') { const newMosaicData = [...mosaicData, { x: drawX, y: drawY, size: mosaicSize }]; canvasContext.save(); canvasContext.fillStyle = 'rgba(128, 128, 128, 0.8)'; canvasContext.beginPath(); canvasContext.arc(drawX, drawY, mosaicSize / 2, 0, 2 * Math.PI); canvasContext.fill(); canvasContext.restore(); this.setData({ mosaicData: newMosaicData, hasMosaic: true, hasEdits: true }); } else if (currentTool === 'eraser') { const newMosaicData = mosaicData.filter(point => { const distance = Math.hypot(drawX - point.x, drawY - point.y); return distance > (point.size + mosaicSize) / 2; }); canvasContext.clearRect(0, 0, realWidth, realHeight); newMosaicData.forEach(point => { canvasContext.save(); canvasContext.fillStyle = 'rgba(128, 128, 128, 0.8)'; canvasContext.beginPath(); canvasContext.arc(point.x, point.y, point.size / 2, 0, 2 * Math.PI); canvasContext.fill(); canvasContext.restore(); }); this.setData({ mosaicData: newMosaicData, hasMosaic: newMosaicData.length > 0, hasEdits: true }); } } }); }, clearMosaic() { this.initMosaicCanvas(); this.setData({ mosaicData: [], hasMosaic: false, hasEdits: this.data.rotateAngle !== 0 || this.data.cropBox.width !== this.calculateInitialCropBox(this.data.imageInfo).width }); }, restoreOriginal() { // 如果正在裁剪,先关闭裁剪模式 if (this.data.isCropping) { this.setData({ isCropping: false, currentTool: '' }); } const currentScale = this.data.imageScale; wx.getImageInfo({ src: this.data.originalPhotoPath, success: (info) => { this.setData({ currentPhotoPath: this.data.originalPhotoPath, imageInfo: info, rotateAngle: 0, imageScale: currentScale, mosaicData: [], hasMosaic: false, hasEdits: false, isCropping: false, currentTool: '', cropBox: this.calculateInitialCropBox(info), isRotated: false }, () => { this.initMosaicCanvas(); }); } }); }, retryPhoto() { this.setData({ hasTakenPhoto: false, currentTool: '', isCropping: false }); }, // 添加裁剪逻辑 async confirmPhoto() { // 显示加载提示 wx.showLoading({ title: '处理中...', mask: true }); try { // 合并图片 let tempFilePath; try { tempFilePath = await this.mergeImages(); } catch (mergeError) { wx.hideLoading(); console.error('合并图片失败:', mergeError); wx.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 }); return; } if (!tempFilePath) { wx.hideLoading(); wx.showToast({ title: '图片处理失败', icon: 'none', duration: 2000 }); return; } // 上传图片 try { const uploadResult = await cosManager.upload(tempFilePath, { fileType: 'image', enableDedup: false, onProgress: (percent) => { console.log('上传进度:', percent + '%'); // 更新加载提示 wx.showLoading({ title: `上传中 ${Math.round(percent)}%`, mask: true }); }, enableCompress: false, compressOptions: { maxWidth: 400, maxHeight: 400, targetSize: 20 * 1024 } }); // 检查上传结果 if (!uploadResult || !uploadResult.success) { throw new Error(uploadResult?.error || uploadResult?.message || '上传失败'); } if (!uploadResult.fileUrl) { throw new Error('上传失败:未获取到文件URL'); } // 获取图片路径 const imagePath = uploadResult.fileUrl; // 确保隐藏loading,避免页面切换时残留 wx.hideLoading(); // 跳转发布页并传递图片数据(不显示toast,避免与页面切换冲突) wx.redirectTo({ url: `/subpackages/media/edits/edits?imagePath=${encodeURIComponent(imagePath)}`, fail: (err) => { console.error('跳转发布页失败:', err); wx.showToast({ title: '跳转失败,请重试', icon: 'none', duration: 2000 }); } }); } catch (uploadError) { wx.hideLoading(); console.error('上传失败:', uploadError); // 判断错误类型,提供更友好的提示 let errorMessage = '上传失败,请检查网络后重试'; if (uploadError && uploadError.message) { if (uploadError.message.includes('Failed to fetch') || uploadError.message.includes('网络') || uploadError.message.includes('timeout')) { errorMessage = '网络连接失败,请检查网络后重试'; } else if (uploadError.message.includes('权限') || uploadError.message.includes('AccessDenied')) { errorMessage = '上传权限不足,请重新登录'; } else { errorMessage = uploadError.message || errorMessage; } } wx.showModal({ title: '上传失败', content: errorMessage, showCancel: true, cancelText: '取消', confirmText: '重试', success: (res) => { if (res.confirm) { // 用户选择重试 this.confirmPhoto(); } } }); return; // 确保不再执行后续代码 } } catch (err) { wx.hideLoading(); console.error('图片处理失败:', err); let errorMessage = '图片处理失败,请重试'; if (err && err.message) { errorMessage = err.message; } wx.showToast({ title: errorMessage, icon: 'none', duration: 2000 }); } }, navigateBack() { wx.navigateBack({ // 返回上一页 delta: 1 }); }, saveAndReturn(imagePath) { const pages = getCurrentPages(); const prevPage = pages[pages.length - 2]; if (prevPage && prevPage.uploadImage) { prevPage.uploadImage(imagePath); } this.navigateBack(); }, goBackToShoot() { wx.redirectTo({ url: '/subpackages/media/camera/camera', success: () => { }, fail: (err) => { console.error("返回拍摄页面失败:", err); wx.showToast({ title: '返回失败,请重试', icon: 'none' }); } }); }, handleCameraError(e) { console.error('相机错误:', e.detail); const error = e.detail; // 处理权限相关的错误 if (error.errMsg && (error.errMsg.includes('permission') || error.errMsg.includes('权限'))) { wx.showModal({ title: '权限不足', content: '请授予相机权限以使用拍摄功能', confirmText: '去设置', cancelText: '取消', success: (modalRes) => { if (modalRes.confirm) { wx.openSetting({ success: (settingRes) => { // 如果用户在设置中授权了,可以尝试重新加载页面 if (settingRes.authSetting['scope.camera']) { // 权限已授予,可以继续使用相机 console.log('用户已授予相机权限'); } else { // 用户仍未授权,返回上一页 this.navigateBack(); } }, fail: () => { this.navigateBack(); } }); } else { // 用户取消,返回上一页 this.navigateBack(); } } }); } else { // 其他相机错误 wx.showToast({ title: '相机初始化失败', icon: 'none' }); setTimeout(() => { this.navigateBack(); }, 1500); } } });