// 头像编辑页面 const cosManager = require('../../../utils/cos-manager.js'); const apiClient = require('../../../utils/api-client.js'); Page({ data: { // 图片路径 originalImagePath: '', // 原始图片路径 currentImagePath: '', // 当前编辑的图片路径 imageInfo: null, // 图片信息 // 编辑状态 rotateAngle: 0, // 旋转角度 hasEdits: false, // 是否有编辑操作 editHistory: [], // 编辑历史(用于撤回) // 裁剪相关 isCropping: false, // 是否在裁剪模式 cropSize: 300, // 裁剪框大小(圆形) cropX: 0, // 裁剪框X坐标 cropY: 0, // 裁剪框Y坐标 // 模糊相关 isBlurring: false, // 是否在模糊模式 blurSize: 20, // 模糊块大小 blurData: [], // 模糊数据 // Canvas相关 canvasContext: null, canvas: null, canvasWidth: 0, canvasHeight: 0, // 系统信息 systemInfo: {} }, onLoad(options) { const imagePath = options.imagePath; if (!imagePath) { wx.showToast({ title: '图片路径无效', icon: 'none' }); setTimeout(() => { wx.navigateBack(); }, 1500); return; } // 获取系统信息 const systemInfo = wx.getSystemInfoSync(); this.setData({ systemInfo, originalImagePath: decodeURIComponent(imagePath), currentImagePath: decodeURIComponent(imagePath) }); }, // 加载图片信息(从image的bindload事件调用) onImageLoad(e) { const { width, height } = e.detail; const systemInfo = this.data.systemInfo || wx.getSystemInfoSync(); // 计算显示尺寸 const maxWidth = systemInfo.windowWidth; const maxHeight = systemInfo.windowHeight * 0.7; const scale = Math.min(maxWidth / width, maxHeight / height); const displayWidth = width * scale; const displayHeight = height * scale; // 获取图片容器的实际位置,用于计算裁剪框位置 const query = wx.createSelectorQuery().in(this); query.select('.image-wrapper').boundingClientRect((rect) => { if (rect) { // 缓存图片容器的位置信息 this._imageWrapperRect = rect; // 初始化裁剪框位置(居中,相对于图片容器) const cropSize = Math.min(rect.width, rect.height) * 0.8; // 计算相对于图片容器的坐标 const cropX = Math.max(0, (rect.width - cropSize) / 2); const cropY = Math.max(0, (rect.height - cropSize) / 2); this.setData({ imageInfo: { width, height, displayWidth, displayHeight }, cropSize: Math.round(cropSize), cropX: Math.round(cropX), cropY: Math.round(cropY), canvasWidth: Math.round(rect.width), // 使用容器的实际宽度 canvasHeight: Math.round(rect.height), // 使用容器的实际高度 systemInfo: systemInfo // 确保 systemInfo 已设置 }, () => { this.initCanvas(); }); } else { // 如果获取容器位置失败,使用默认值 const cropSize = Math.min(displayWidth, displayHeight) * 0.8; const cropX = Math.max(0, (displayWidth - cropSize) / 2); const cropY = Math.max(0, (displayHeight - cropSize) / 2); // 使用估算的容器位置 this._imageWrapperRect = { left: 0, top: 0, width: displayWidth, height: displayHeight }; this.setData({ imageInfo: { width, height, displayWidth, displayHeight }, cropSize: Math.round(cropSize), cropX: Math.round(cropX), cropY: Math.round(cropY), canvasWidth: Math.round(displayWidth), canvasHeight: Math.round(displayHeight), systemInfo: systemInfo // 确保 systemInfo 已设置 }, () => { this.initCanvas(); }); } }).exec(); }, // 初始化Canvas initCanvas() { const query = wx.createSelectorQuery().in(this); query.select('#blurCanvas') .fields({ node: true, size: true }) .exec((res) => { if (res && res[0] && res[0].node) { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const { canvasWidth, canvasHeight } = this.data; canvas.width = canvasWidth; canvas.height = canvasHeight; this.setData({ canvas: canvas, canvasContext: ctx }); } }); }, // 旋转图片 rotateImage() { const newAngle = (this.data.rotateAngle + 90) % 360; this.saveEditState(); this.setData({ rotateAngle: newAngle, hasEdits: true }); }, // 切换裁剪模式 toggleCrop() { this.setData({ isCropping: !this.data.isCropping, isBlurring: false }, () => { // 如果开启裁剪模式,重新获取图片容器位置 if (this.data.isCropping) { this._updateImageWrapperRect(); } }); }, // 裁剪框拖动开始 onCropMoveStart(e) { if (!this.data.isCropping) return; const touch = e.touches[0]; if (!touch) return; // 确保容器位置已获取 if (!this._imageWrapperRect) { this._updateImageWrapperRect(); } // 保存起始触摸位置 this._cropStartTouch = { clientX: touch.clientX, clientY: touch.clientY }; }, // 裁剪框拖动(优化版本,使用相对坐标) onCropMove(e) { if (!this.data.isCropping) return; const touch = e.touches[0]; if (!touch || !touch.clientX || !touch.clientY) return; const { cropSize, canvasWidth, canvasHeight } = this.data; // 保存触摸坐标,避免异步回调中失效 const touchX = touch.clientX; const touchY = touch.clientY; // 获取图片容器的实际位置(使用缓存的或重新获取) let imageWrapperRect = this._imageWrapperRect; if (!imageWrapperRect) { // 如果没有缓存的容器位置,同步获取(使用估算值作为兜底) const query = wx.createSelectorQuery().in(this); query.select('.image-wrapper').boundingClientRect((rect) => { if (rect) { this._imageWrapperRect = rect; // 使用保存的触摸坐标重新计算 const savedTouch = { clientX: touchX, clientY: touchY }; this._calculateCropPosition(savedTouch, cropSize, canvasWidth, canvasHeight, rect); } else { // 如果获取失败,使用系统信息估算(兜底方案) const systemInfo = this.data.systemInfo || wx.getSystemInfoSync(); const estimatedRect = { left: 0, top: 0, width: systemInfo.windowWidth || 375, height: systemInfo.windowHeight * 0.7 || 400 }; const savedTouch = { clientX: touchX, clientY: touchY }; this._calculateCropPosition(savedTouch, cropSize, canvasWidth, canvasHeight, estimatedRect); } }).exec(); return; } // 使用缓存的容器位置计算裁剪框位置 this._calculateCropPosition(touch, cropSize, canvasWidth, canvasHeight, imageWrapperRect); }, // 计算裁剪框位置(提取公共逻辑) _calculateCropPosition(touch, cropSize, canvasWidth, canvasHeight, imageWrapperRect) { // 确保 touch 对象有 clientX 和 clientY 属性 if (!touch || typeof touch.clientX === 'undefined' || typeof touch.clientY === 'undefined') { console.warn('触摸事件坐标无效:', touch); return; } // 计算相对于图片容器的坐标 const relativeX = touch.clientX - imageWrapperRect.left; const relativeY = touch.clientY - imageWrapperRect.top; // 计算裁剪框中心位置(相对于图片容器) let newX = relativeX - cropSize / 2; let newY = relativeY - cropSize / 2; // 限制裁剪框在图片范围内(确保左右和上下都能正常拖动) newX = Math.max(0, Math.min(newX, Math.max(0, canvasWidth - cropSize))); newY = Math.max(0, Math.min(newY, Math.max(0, canvasHeight - cropSize))); // 确保数值类型正确 this.setData({ cropX: Math.round(newX), cropY: Math.round(newY) }); }, // 更新图片容器位置(提取公共方法) _updateImageWrapperRect() { const query = wx.createSelectorQuery().in(this); query.select('.image-wrapper').boundingClientRect((rect) => { if (rect) { this._imageWrapperRect = rect; // 更新 canvasWidth 和 canvasHeight this.setData({ canvasWidth: rect.width, canvasHeight: rect.height }); } }).exec(); }, // 裁剪框拖动结束 onCropMoveEnd(e) { // 可以在这里添加拖动结束的逻辑,比如保存位置等 // 目前不需要特殊处理 }, // 切换模糊模式 toggleBlur() { this.setData({ isBlurring: !this.data.isBlurring, isCropping: false }); }, // 模糊触摸处理 onBlurTouch(e) { if (!this.data.isBlurring || !this.data.canvasContext) return; const touch = e.touches[0]; const { clientX, clientY } = touch; const { blurSize, canvasContext } = this.data; // 绘制模糊块 canvasContext.fillStyle = 'rgba(128, 128, 128, 0.6)'; canvasContext.beginPath(); canvasContext.arc(clientX, clientY, blurSize / 2, 0, 2 * Math.PI); canvasContext.fill(); canvasContext.draw(); // 保存模糊数据 const blurData = this.data.blurData || []; blurData.push({ x: clientX, y: clientY, size: blurSize }); this.saveEditState(); this.setData({ blurData: blurData, hasEdits: true }); }, // 保存编辑状态(用于撤回) saveEditState() { const history = this.data.editHistory || []; history.push({ imagePath: this.data.currentImagePath, rotateAngle: this.data.rotateAngle, blurData: JSON.parse(JSON.stringify(this.data.blurData || [])) }); // 限制历史记录数量 if (history.length > 10) { history.shift(); } this.setData({ editHistory: history }); }, // 撤回操作 undoEdit() { const history = this.data.editHistory || []; if (history.length === 0) { wx.showToast({ title: '没有可撤回的操作', icon: 'none' }); return; } const lastState = history.pop(); this.setData({ currentImagePath: lastState.imagePath, rotateAngle: lastState.rotateAngle, blurData: lastState.blurData, editHistory: history, hasEdits: history.length > 0 }); // 重新渲染模糊 if (this.data.canvasContext && lastState.blurData.length > 0) { this.reRenderBlur(); } }, // 重新渲染模糊 reRenderBlur() { const { canvasContext, blurData, canvasWidth, canvasHeight } = this.data; if (!canvasContext) return; canvasContext.clearRect(0, 0, canvasWidth, canvasHeight); blurData.forEach(point => { canvasContext.fillStyle = 'rgba(128, 128, 128, 0.6)'; canvasContext.beginPath(); canvasContext.arc(point.x, point.y, point.size / 2, 0, 2 * Math.PI); canvasContext.fill(); }); canvasContext.draw(); }, // 应用裁剪 async applyCrop() { if (!this.data.isCropping) { return Promise.resolve(); } wx.showLoading({ title: '裁剪中...' }); try { const { currentImagePath, cropX, cropY, cropSize, imageInfo, rotateAngle } = this.data; // 验证必要数据 if (!currentImagePath) { throw new Error('图片路径无效'); } if (!imageInfo || !imageInfo.width || !imageInfo.height) { throw new Error('图片信息不完整'); } if (!cropX || !cropY || !cropSize || cropSize <= 0) { throw new Error('裁剪参数无效'); } // 创建Canvas进行裁剪(使用 Promise 包装 exec) const canvasRes = await new Promise((resolve, reject) => { const query = wx.createSelectorQuery().in(this); query.select('#cropCanvas') .fields({ node: true, size: true }) .exec((res) => { if (!res || !res[0] || !res[0].node) { reject(new Error('Canvas初始化失败')); } else { resolve(res[0]); } }); }); const canvas = canvasRes.node; const ctx = canvas.getContext('2d'); // 设置Canvas尺寸为裁剪大小 const roundedCropSize = Math.round(cropSize); canvas.width = roundedCropSize; canvas.height = roundedCropSize; // 加载图片(添加超时处理) const img = canvas.createImage(); const imageLoadPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('图片加载超时')); }, 10000); // 10秒超时 img.onload = () => { clearTimeout(timeout); resolve(); }; img.onerror = (err) => { clearTimeout(timeout); reject(new Error('图片加载失败: ' + (err.message || '未知错误'))); }; }); img.src = currentImagePath; await imageLoadPromise; // 计算裁剪区域在原图中的比例 const scaleX = imageInfo.width / imageInfo.displayWidth; const scaleY = imageInfo.height / imageInfo.displayHeight; const sourceX = Math.max(0, Math.round(cropX * scaleX)); const sourceY = Math.max(0, Math.round(cropY * scaleY)); const sourceSize = Math.min( Math.round(cropSize * scaleX), imageInfo.width - sourceX, imageInfo.height - sourceY ); // 清空画布 ctx.clearRect(0, 0, roundedCropSize, roundedCropSize); // 绘制圆形裁剪 ctx.save(); ctx.beginPath(); ctx.arc(roundedCropSize / 2, roundedCropSize / 2, roundedCropSize / 2, 0, 2 * Math.PI); ctx.clip(); // 应用旋转 if (rotateAngle !== 0) { ctx.translate(roundedCropSize / 2, roundedCropSize / 2); ctx.rotate(rotateAngle * Math.PI / 180); ctx.translate(-roundedCropSize / 2, -roundedCropSize / 2); } // 绘制裁剪后的图片 ctx.drawImage(img, sourceX, sourceY, sourceSize, sourceSize, 0, 0, roundedCropSize, roundedCropSize); ctx.restore(); // 导出图片 const exportResult = await new Promise((resolve, reject) => { wx.canvasToTempFilePath({ canvas: canvas, success: resolve, fail: reject }, this); }); if (!exportResult || !exportResult.tempFilePath) { throw new Error('生成裁剪图片失败'); } // 验证生成的文件 const fileInfo = await new Promise((resolve, reject) => { wx.getFileInfo({ filePath: exportResult.tempFilePath, success: resolve, fail: reject }); }); if (!fileInfo || !fileInfo.size || fileInfo.size === 0) { throw new Error('裁剪后的文件无效'); } // 更新状态 this.saveEditState(); this.setData({ currentImagePath: exportResult.tempFilePath, isCropping: false, hasEdits: true }); wx.hideLoading(); console.log('裁剪成功,新图片路径:', exportResult.tempFilePath); } catch (error) { wx.hideLoading(); console.error('裁剪失败:', error); wx.showToast({ title: error.message || '裁剪失败,请重试', icon: 'none', duration: 2000 }); throw error; // 重新抛出错误,让调用者处理 } }, // 完成编辑并上传 async confirmEdit() { wx.showLoading({ title: '处理中...' }); try { let finalImagePath = this.data.currentImagePath; // 如果有裁剪,先应用裁剪 if (this.data.isCropping) { try { await this.applyCrop(); finalImagePath = this.data.currentImagePath; // 验证裁剪后的文件路径 if (!finalImagePath) { throw new Error('裁剪后未获取到图片路径'); } // 再次验证文件是否存在 const fileInfo = await new Promise((resolve, reject) => { wx.getFileInfo({ filePath: finalImagePath, success: resolve, fail: reject }); }); if (!fileInfo || !fileInfo.size || fileInfo.size === 0) { throw new Error('裁剪后的文件无效,请重试'); } console.log('裁剪完成,文件路径:', finalImagePath, '文件大小:', fileInfo.size); } catch (cropError) { wx.hideLoading(); console.error('裁剪失败:', cropError); wx.showToast({ title: cropError.message || '裁剪失败,请重试', icon: 'none', duration: 2000 }); return; // 裁剪失败,直接返回,不继续上传 } } // 如果有旋转或模糊,需要合并处理 if (this.data.rotateAngle !== 0 || (this.data.blurData && this.data.blurData.length > 0)) { finalImagePath = await this.mergeEdits(); } // 验证文件路径 if (!finalImagePath) { throw new Error('图片路径无效,请重新选择图片'); } // 检查文件是否存在 try { const fileInfo = await new Promise((resolve, reject) => { wx.getFileInfo({ filePath: finalImagePath, success: resolve, fail: reject }); }); console.log('文件信息:', fileInfo); } catch (fileError) { console.error('文件不存在或无法访问:', fileError); throw new Error('图片文件不存在或已损坏,请重新选择'); } // 上传头像 console.log('开始上传头像,文件路径:', finalImagePath); // 更新加载提示 wx.showLoading({ title: '上传中...', mask: true }); // 添加上传超时处理(60秒) const uploadPromise = cosManager.upload(finalImagePath, { fileType: 'avatar', enableDedup: true, enableCompress: false, compressOptions: { maxWidth: 400, maxHeight: 400, targetSize: 30 * 1024 }, onProgress: (percent) => { wx.showLoading({ title: `上传中 ${Math.round(percent)}%`, mask: true }); } }); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('上传超时,请检查网络后重试')); }, 60000); // 60秒超时 }); const uploadResult = await Promise.race([uploadPromise, timeoutPromise]); console.log('上传结果:', uploadResult); // 检查上传结果 if (!uploadResult) { throw new Error('上传失败:未获取到上传结果'); } // 检查 success 字段 if (uploadResult.success !== true) { const errorMsg = uploadResult?.error || uploadResult?.message || '上传失败'; console.error('上传失败:', errorMsg, uploadResult); throw new Error(errorMsg); } // 检查 fileUrl 字段 const avatarUrl = uploadResult.fileUrl || uploadResult.originalUrl; if (!avatarUrl) { console.error('上传结果中没有 fileUrl:', uploadResult); throw new Error('上传失败:未获取到文件URL'); } console.log('头像上传成功,URL:', avatarUrl); // 更新用户信息 await apiClient.updateUserProfile({ avatar: avatarUrl }); // 更新全局用户信息 const app = getApp(); if (app.globalData.userInfo && app.globalData.userInfo.user) { app.globalData.userInfo.user.avatar = avatarUrl; app.globalData.userInfo.user.avatarUrl = avatarUrl; } // 同步到 NIM try { const nimUserManager = require('../../../utils/nim-user-manager.js'); await nimUserManager.updateSelfUserInfo({ avatar: avatarUrl }); console.log('✅ 头像已同步到 NIM'); } catch (nimError) { console.error('⚠️ 同步头像到 NIM 失败:', nimError); } // 缓存头像 let cachedUrl = avatarUrl; try { const imageCacheManager = require('../../../utils/image-cache-manager.js'); cachedUrl = await imageCacheManager.cacheImage(avatarUrl, 'avatar'); console.log('✅ 头像已缓存:', cachedUrl); } catch (cacheError) { console.error('⚠️ 头像缓存失败:', cacheError); } wx.hideLoading(); wx.showToast({ title: '头像更新成功', icon: 'success' }); // 通知上一页更新头像 const pages = getCurrentPages(); console.log('当前页面栈:', pages.map(p => p.route)); // 查找 profile 页面 let profilePage = null; for (let i = pages.length - 2; i >= 0; i--) { if (pages[i].route === 'subpackages/profile/profile/profile') { profilePage = pages[i]; break; } } if (profilePage) { // 如果找到 profile 页面,更新头像 if (typeof profilePage.onAvatarUpdated === 'function') { profilePage.onAvatarUpdated({ avatarUrl: avatarUrl, cachedUrl: cachedUrl, serverUrl: avatarUrl, userInfo: { user: { ...app.globalData.userInfo?.user, avatar: avatarUrl, avatarUrl: avatarUrl } } }); } // 如果上一页有 syncGlobalUserInfo 方法,也调用它 if (typeof profilePage.syncGlobalUserInfo === 'function') { profilePage.syncGlobalUserInfo(cachedUrl || avatarUrl); } // 如果上一页有 loadUserData 方法,调用它刷新数据 if (typeof profilePage.loadUserData === 'function') { profilePage.loadUserData(); } } else { // 如果找不到 profile 页面,尝试从上一页更新 const prevPage = pages[pages.length - 2]; if (prevPage) { if (typeof prevPage.onAvatarUpdated === 'function') { prevPage.onAvatarUpdated({ avatarUrl: avatarUrl, cachedUrl: cachedUrl, serverUrl: avatarUrl, userInfo: { user: { ...app.globalData.userInfo?.user, avatar: avatarUrl, avatarUrl: avatarUrl } } }); } if (typeof prevPage.syncGlobalUserInfo === 'function') { prevPage.syncGlobalUserInfo(cachedUrl || avatarUrl); } if (typeof prevPage.loadUserData === 'function') { prevPage.loadUserData(); } } } // 返回上一页(确保返回到 profile 页面) setTimeout(() => { // 检查页面栈,确保返回到正确的页面 const currentPages = getCurrentPages(); console.log('返回前的页面栈:', currentPages.map(p => p.route)); // 检查上一页是否是 profile 页面 if (currentPages.length >= 2) { const prevPageRoute = currentPages[currentPages.length - 2].route; console.log('上一页路由:', prevPageRoute); // 如果上一页是 profile 页面,直接返回 if (prevPageRoute === 'subpackages/profile/profile/profile') { wx.navigateBack({ delta: 1, success: () => { console.log('成功返回到 profile 页面'); }, fail: (err) => { console.error('返回失败:', err); // 如果返回失败,直接跳转到 profile 页面 wx.redirectTo({ url: '/subpackages/profile/profile/profile', success: () => { console.log('使用 redirectTo 成功跳转到 profile 页面'); }, fail: (redirectErr) => { console.error('redirectTo 也失败:', redirectErr); } }); } }); } else { // 如果上一页不是 profile 页面,直接跳转到 profile 页面 console.log('上一页不是 profile 页面,直接跳转到 profile'); wx.redirectTo({ url: '/subpackages/profile/profile/profile', success: () => { console.log('成功跳转到 profile 页面'); }, fail: (err) => { console.error('跳转到 profile 失败:', err); } }); } } else { // 如果页面栈中只有当前页面,直接跳转到 profile console.log('页面栈中只有当前页面,直接跳转到 profile'); wx.redirectTo({ url: '/subpackages/profile/profile/profile', success: () => { console.log('成功跳转到 profile 页面'); }, fail: (err) => { console.error('跳转到 profile 失败:', err); } }); } }, 1000); } catch (error) { wx.hideLoading(); console.error('上传失败,详细错误:', error); console.error('错误堆栈:', error.stack); // 提供更详细的错误信息 let errorMessage = '上传失败'; if (error.message) { errorMessage = error.message; } else if (typeof error === 'string') { errorMessage = error; } else if (error.errMsg) { errorMessage = error.errMsg; } wx.showToast({ title: errorMessage, icon: 'none', duration: 3000 }); } }, // 合并所有编辑效果 async mergeEdits() { return new Promise((resolve, reject) => { const query = wx.createSelectorQuery().in(this); query.select('#mergeCanvas') .fields({ node: true, size: true }) .exec((res) => { if (!res || !res[0] || !res[0].node) { reject(new Error('Canvas初始化失败')); return; } const canvas = res[0].node; const ctx = canvas.getContext('2d'); const { imageInfo, rotateAngle, blurData } = this.data; // 设置Canvas尺寸 canvas.width = imageInfo.width; canvas.height = imageInfo.height; // 加载图片 const img = canvas.createImage(); img.src = this.data.currentImagePath; img.onload = () => { // 应用旋转 ctx.save(); ctx.translate(imageInfo.width / 2, imageInfo.height / 2); ctx.rotate(rotateAngle * Math.PI / 180); ctx.translate(-imageInfo.width / 2, -imageInfo.height / 2); // 绘制图片 ctx.drawImage(img, 0, 0, imageInfo.width, imageInfo.height); ctx.restore(); // 应用模糊(如果有) if (blurData && blurData.length > 0) { const scaleX = imageInfo.width / imageInfo.displayWidth; const scaleY = imageInfo.height / imageInfo.displayHeight; blurData.forEach(point => { ctx.fillStyle = 'rgba(128, 128, 128, 0.6)'; ctx.beginPath(); ctx.arc(point.x * scaleX, point.y * scaleY, point.size * scaleX / 2, 0, 2 * Math.PI); ctx.fill(); }); } // 导出图片 wx.canvasToTempFilePath({ canvas: canvas, success: (res) => { resolve(res.tempFilePath); }, fail: reject }, this); }; img.onerror = reject; }); }); }, // 取消编辑 cancelEdit() { wx.navigateBack(); } });