904 lines
28 KiB
JavaScript
904 lines
28 KiB
JavaScript
|
|
// 头像编辑页面
|
|||
|
|
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();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|