Преглед на файлове

添加图片裁剪组件 cl-cropper

icssoa преди 8 месеца
родител
ревизия
ba78d54582
променени са 2 файла, в които са добавени 109 реда и са изтрити 55 реда
  1. 1 5
      pages/demo/other/cropper.uvue
  2. 108 50
      uni_modules/cool-ui/components/cl-cropper/cl-cropper.uvue

+ 1 - 5
pages/demo/other/cropper.uvue

@@ -50,10 +50,6 @@ function onCrop(url: string) {
 }
 
 function onImageLoad(e: UniImageLoadEvent) {
-	console.log("图片加载完成", e);
+	console.log("onImageLoad", e);
 }
-
-onReady(() => {
-	cropperRef.value!.open("https://uni-docs.cool-js.com/demo/pages/demo/static/bg2.png");
-});
 </script>

+ 108 - 50
uni_modules/cool-ui/components/cl-cropper/cl-cropper.uvue

@@ -83,14 +83,14 @@
 		</view>
 
 		<!-- 底部按钮组 -->
-		<view class="cl-cropper__actions" :class="[pt.actions?.className]">
+		<view class="cl-cropper__op" :class="[pt.op?.className]" :style="opStyle" @touchmove.stop>
 			<!-- 关闭 -->
-			<view class="cl-cropper__actions-item">
+			<view class="cl-cropper__op-item">
 				<cl-icon name="close-line" color="white" :size="50" @tap="close"></cl-icon>
 			</view>
 
 			<!-- 旋转 -->
-			<view class="cl-cropper__actions-item">
+			<view class="cl-cropper__op-item">
 				<cl-icon
 					name="anticlockwise-line"
 					color="white"
@@ -100,22 +100,17 @@
 			</view>
 
 			<!-- 重置 -->
-			<view class="cl-cropper__actions-item">
-				<cl-icon
-					name="reset-right-line"
-					color="white"
-					:size="40"
-					@tap="resetCropper"
-				></cl-icon>
+			<view class="cl-cropper__op-item">
+				<cl-icon name="reset-right-line" color="white" :size="40" @tap="reset"></cl-icon>
 			</view>
 
 			<!-- 重新选择 -->
-			<view class="cl-cropper__actions-item">
+			<view class="cl-cropper__op-item">
 				<cl-icon name="image-line" color="white" :size="40" @tap="chooseImage"></cl-icon>
 			</view>
 
 			<!-- 确定 -->
-			<view class="cl-cropper__actions-item">
+			<view class="cl-cropper__op-item">
 				<cl-icon name="check-line" color="white" :size="50" @tap="toPng"></cl-icon>
 			</view>
 		</view>
@@ -248,20 +243,19 @@ function toPixel(value: number): number {
 type PassThrough = {
 	className?: string; // 组件根元素类名
 	image?: PassThroughProps; // 图片元素透传属性
-	op?: PassThroughProps; // 操作按钮组透传属性
-	actions?: PassThroughProps; // 底部按钮组透传属性
+	op?: PassThroughProps; // 底部按钮组透传属性
 	mask?: PassThroughProps; // 遮罩层透传属性
 	cropBox?: PassThroughProps; // 裁剪框透传属性
 	button?: PassThroughProps; // 按钮透传属性
 };
 
-// 解析透传样式配置的计算属性
+// 解析透传样式配置
 const pt = computed(() => parsePt<PassThrough>(props.pt));
 
 // 创建容器尺寸响应式对象
 const container = reactive<Size>({
-	height: page.getViewHeight(), // 获取视图高度
-	width: page.getViewWidth() // 获取视图宽度
+	height: 0, // 获取视图高度
+	width: 0 // 获取视图宽度
 });
 
 // 创建图片信息响应式对象
@@ -320,7 +314,7 @@ const flipVertical = ref(false); // 垂直翻转状态
 // 图片旋转状态
 const rotate = ref(0); // 旋转状态
 
-// 计算图片样式的计算属性
+// 计算图片样式
 const imageStyle = computed(() => {
 	// 构建翻转变换
 	const flipX = flipHorizontal.value ? "scaleX(-1)" : "scaleX(1)";
@@ -342,7 +336,7 @@ const imageStyle = computed(() => {
 	return style;
 });
 
-// 计算裁剪框样式的计算属性
+// 计算裁剪框样式
 const cropBoxStyle = computed(() => {
 	// 返回裁剪框定位和尺寸样式
 	return {
@@ -353,7 +347,7 @@ const cropBoxStyle = computed(() => {
 	};
 });
 
-// 计算遮罩层样式的计算属性
+// 计算遮罩层样式
 const maskStyle = computed<MaskStyle>(() => {
 	// 返回四个方向的遮罩样式
 	return {
@@ -386,6 +380,19 @@ const maskStyle = computed<MaskStyle>(() => {
 	};
 });
 
+// 底部按钮组样式
+const opStyle = computed(() => {
+	let bottom = page.getSafeAreaHeight("bottom");
+
+	if (bottom == 0) {
+		bottom = 10;
+	}
+
+	return {
+		bottom: bottom + "px"
+	};
+});
+
 // 计算旋转后图片的有效尺寸的函数
 function getRotatedImageSize(): Size {
 	// 获取旋转角度(转换为0-360度范围内的正值)
@@ -508,9 +515,18 @@ function getMinImageSize(): Size {
 
 // 初始化裁剪框的函数
 function initCrop() {
+	const { windowHeight, windowWidth } = uni.getWindowInfo();
+
+	// 设置容器尺寸为视口尺寸
+	container.height = windowHeight;
+	container.width = windowWidth;
+
+	console.log(container);
+
 	// 设置裁剪框尺寸为传入的初始值
 	cropBox.width = props.cropWidth; // 设置裁剪框宽度
 	cropBox.height = props.cropHeight; // 设置裁剪框高度
+
 	// 计算裁剪框居中位置
 	cropBox.x = toPixel((container.width - cropBox.width) / 2); // 水平居中
 	cropBox.y = toPixel((container.height - cropBox.height) / 2); // 垂直居中
@@ -615,10 +631,12 @@ function onImageLoaded(e: UniImageLoadEvent) {
 	imageInfo.height = e.detail.height; // 保存图片原始高度
 	imageInfo.isLoaded = true; // 标记图片已加载
 
-	// 执行初始化流程
-	initCrop(); // 初始化裁剪框位置和尺寸
-	setInitialImageSize(); // 设置图片初始显示尺寸
-	adjustBounds(); // 调整图片边界确保覆盖裁剪框
+	nextTick(() => {
+		// 执行初始化流程
+		initCrop(); // 初始化裁剪框位置和尺寸
+		setInitialImageSize(); // 设置图片初始显示尺寸
+		adjustBounds(); // 调整图片边界确保覆盖裁剪框
+	});
 
 	// 触发加载完成事件
 	emit("load", e); // 向父组件发送加载事件
@@ -971,7 +989,7 @@ function onTouchEnd() {
 }
 
 // 重置裁剪器到初始状态的函数
-function resetCropper() {
+function reset() {
 	// 重新初始化裁剪框
 	initCrop(); // 恢复裁剪框到初始位置和尺寸
 
@@ -980,6 +998,10 @@ function resetCropper() {
 	flipVertical.value = false; // 重置垂直翻转状态
 	rotate.value = 0; // 重置旋转角度
 
+	// 重置图片位移
+	transform.translateX = 0;
+	transform.translateY = 0;
+
 	// 根据图片加载状态进行不同处理
 	if (imageInfo.isLoaded) {
 		setInitialImageSize(); // 重新设置图片初始尺寸
@@ -1084,14 +1106,9 @@ async function toPng(): Promise<string> {
 				// 获取设备像素比
 				const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
 
-				// #ifndef H5
-				// 设置缩放比例
-				ctx!.scale(dpr, dpr);
-				// #endif
-
 				// 设置宽高
-				ctx!.canvas.width = cropBox.width;
-				ctx!.canvas.height = cropBox.height;
+				ctx!.canvas.width = cropBox.width * dpr;
+				ctx!.canvas.height = cropBox.height * dpr;
 
 				let img: Image;
 
@@ -1102,25 +1119,67 @@ async function toPng(): Promise<string> {
 
 				// 其他环境创建图片
 				// #ifndef MP-WEIXIN || APP-HARMONY
-				img = new Image(cropBox.width, cropBox.height);
+				img = new Image();
 				// #endif
 
 				// 设置图片源并在加载完成后绘制
 				img.src = imageUrl.value;
 				img.onload = () => {
-					ctx!.drawImage(img, cropBox.x, cropBox.y, cropBox.width, cropBox.height);
+					let x: number;
+					let y: number;
+
+					// 根据旋转角度计算裁剪位置
+					switch (Math.abs(rotate.value) % 360) {
+						case 270:
+							// 旋转270度时的位置计算
+							x = (imageSize.width - cropBox.height) / 2 - transform.translateY;
+							y = (imageSize.height + cropBox.width) / 2 + transform.translateX;
+							break;
+
+						case 180:
+							// 旋转180度时的位置计算
+							x = (imageSize.width + cropBox.width) / 2 + transform.translateX;
+							y = (imageSize.height + cropBox.height) / 2 + transform.translateY;
+							break;
+
+						case 90:
+							// 旋转90度时的位置计算
+							x = (imageSize.width + cropBox.height) / 2 + transform.translateY;
+							y = (imageSize.height - cropBox.width) / 2 - transform.translateX;
+							break;
+
+						default:
+							// 不旋转时的位置计算
+							x = (imageSize.width - cropBox.width) / 2 - transform.translateX;
+							y = (imageSize.height - cropBox.height) / 2 - transform.translateY;
+							break;
+					}
+
+					if (x < 0) {
+						x = 0;
+					}
+
+					if (y < 0) {
+						y = 0;
+					}
+
+					// 图片旋转
+					ctx!.rotate((rotate.value * Math.PI) / 180);
+
+					// 绘制图片
+					ctx!.drawImage(
+						img,
+						-x * dpr,
+						-y * dpr,
+						imageSize.width * dpr,
+						imageSize.height * dpr
+					);
 
 					setTimeout(() => {
-						canvasToPng({
-							proxy,
-							canvasId,
-							canvasRef: canvasRef.value!
-						})
-							.then((url) => {
-								emit("crop", url);
-								resolve(url);
-							})
-							.catch(() => {});
+						canvasToPng(canvasRef.value!).then((url) => {
+							emit("crop", url);
+							resolve(url);
+						});
 					}, 10);
 				};
 			}
@@ -1263,14 +1322,13 @@ defineExpose({
 		}
 	}
 
-	&__actions {
-		@apply absolute left-0 w-full flex flex-row items-center justify-between;
-		z-index: 10;
-		height: 50px;
-		bottom: env(safe-area-inset-bottom);
+	&__op {
+		@apply absolute left-0 bottom-0 w-full flex flex-row justify-between;
+		z-index: 30;
+		height: 40px;
 
 		&-item {
-			@apply flex flex-row justify-center items-center flex-1;
+			@apply flex flex-row justify-center items-center flex-1 h-full;
 		}
 	}