findme-miniprogram-frontend/subpackages/media/camera/camera.js

1568 lines
50 KiB
JavaScript
Raw Permalink Normal View History

2025-12-27 17:16:03 +08:00
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;
// 移除手动权限检查,让 <camera> 组件自动处理权限请求
// 这样可以避免权限弹窗弹出两次(组件初始化时会自动请求权限)
},
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);
}
}
});