upload project
This commit is contained in:
commit
06961cae04
422 changed files with 110626 additions and 0 deletions
903
subpackages/profile/avatar-edit/avatar-edit.js
Normal file
903
subpackages/profile/avatar-edit/avatar-edit.js
Normal file
|
|
@ -0,0 +1,903 @@
|
|||
// 头像编辑页面
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
9
subpackages/profile/avatar-edit/avatar-edit.json
Normal file
9
subpackages/profile/avatar-edit/avatar-edit.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"navigationBarTitleText": "编辑头像",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
|
||||
|
||||
|
||||
79
subpackages/profile/avatar-edit/avatar-edit.wxml
Normal file
79
subpackages/profile/avatar-edit/avatar-edit.wxml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<view class="avatar-edit-container">
|
||||
<!-- 图片编辑区域 -->
|
||||
<view class="edit-area">
|
||||
<view class="image-wrapper" style="transform: rotate({{rotateAngle}}deg);">
|
||||
<image
|
||||
class="edit-image"
|
||||
src="{{currentImagePath}}"
|
||||
mode="widthFix"
|
||||
bindload="onImageLoad"
|
||||
></image>
|
||||
|
||||
<!-- 裁剪框(圆形) -->
|
||||
<view
|
||||
wx:if="{{isCropping}}"
|
||||
class="crop-box"
|
||||
style="left: {{cropX}}px; top: {{cropY}}px; width: {{cropSize}}px; height: {{cropSize}}px;"
|
||||
catchtouchstart="onCropMoveStart"
|
||||
catchtouchmove="onCropMove"
|
||||
catchtouchend="onCropMoveEnd"
|
||||
>
|
||||
<view class="crop-circle"></view>
|
||||
<view class="crop-handle"></view>
|
||||
</view>
|
||||
|
||||
<!-- 模糊Canvas -->
|
||||
<canvas
|
||||
wx:if="{{isBlurring}}"
|
||||
id="blurCanvas"
|
||||
type="2d"
|
||||
class="blur-canvas"
|
||||
bindtouchstart="onBlurTouch"
|
||||
bindtouchmove="onBlurTouch"
|
||||
></canvas>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工具按钮栏 -->
|
||||
<view class="toolbar">
|
||||
<view class="tool-item {{isCropping ? 'active' : ''}}" bindtap="toggleCrop">
|
||||
<text class="tool-icon">✂️</text>
|
||||
<text class="tool-text">裁剪</text>
|
||||
</view>
|
||||
|
||||
<view class="tool-item" bindtap="rotateImage">
|
||||
<text class="tool-icon">🔄</text>
|
||||
<text class="tool-text">旋转</text>
|
||||
</view>
|
||||
|
||||
<view class="tool-item {{isBlurring ? 'active' : ''}}" bindtap="toggleBlur">
|
||||
<text class="tool-icon">🌫️</text>
|
||||
<text class="tool-text">模糊</text>
|
||||
</view>
|
||||
|
||||
<view class="tool-item {{editHistory.length > 0 ? '' : 'disabled'}}" bindtap="undoEdit">
|
||||
<text class="tool-icon">↩️</text>
|
||||
<text class="tool-text">撤回</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<button class="cancel-btn" bindtap="cancelEdit">取消</button>
|
||||
<button class="confirm-btn" bindtap="confirmEdit">完成</button>
|
||||
</view>
|
||||
|
||||
<!-- 隐藏的Canvas用于处理 -->
|
||||
<canvas
|
||||
id="cropCanvas"
|
||||
type="2d"
|
||||
style="position: fixed; left: -9999px; top: -9999px;"
|
||||
></canvas>
|
||||
|
||||
<canvas
|
||||
id="mergeCanvas"
|
||||
type="2d"
|
||||
style="position: fixed; left: -9999px; top: -9999px;"
|
||||
></canvas>
|
||||
</view>
|
||||
|
||||
135
subpackages/profile/avatar-edit/avatar-edit.wxss
Normal file
135
subpackages/profile/avatar-edit/avatar-edit.wxss
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
.avatar-edit-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-image {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
/* 裁剪框样式 */
|
||||
.crop-box {
|
||||
position: absolute;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.crop-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px dashed rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.crop-handle {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 模糊Canvas */
|
||||
.blur-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background: #1a1a1a;
|
||||
border-top: 1px solid #333333;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.tool-item.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.tool-item.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.tool-text {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
padding: 30rpx;
|
||||
background: #1a1a1a;
|
||||
border-top: 1px solid #333333;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 32rpx;
|
||||
margin: 0 15rpx;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: linear-gradient(135deg, #044db4 0%, #156301 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue