findme-miniprogram-frontend/subpackages/media/camera/camera.js
2025-12-27 17:16:03 +08:00

1567 lines
50 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
});