icssoa před 8 měsíci
rodič
revize
bad97a5a70

+ 2 - 2
cool/hooks/page.ts

@@ -99,11 +99,11 @@ class Page {
 	 * @returns 视图高度
 	 */
 	getViewHeight() {
-		// #ifdef H5
+		// #ifndef APP
 		return uni.getWindowInfo().windowHeight;
 		// #endif
 
-		// #ifndef H5
+		// #ifdef APP
 		const { screenHeight } = uni.getWindowInfo();
 
 		let h = screenHeight;

+ 1 - 1
package.json

@@ -13,7 +13,7 @@
 	"devDependencies": {
 		"@babel/parser": "^7.27.5",
 		"@babel/types": "^7.27.6",
-		"@cool-vue/ai": "^1.1.4",
+		"@cool-vue/ai": "^1.1.5",
 		"@cool-vue/vite-plugin": "^8.2.5",
 		"@dcloudio/types": "^3.4.16",
 		"@types/node": "^24.0.15",

+ 12 - 14
pages/demo/other/cropper.uvue

@@ -5,20 +5,20 @@
 				<cl-button @tap="chooseImage">{{ t("选择图片") }}</cl-button>
 
 				<cl-list border :pt="{ className: 'mt-5' }">
-					<cl-list-item :label="t('调节裁剪框大小')">
+					<cl-list-item :label="t('调节裁剪框大小')">
 						<cl-switch v-model="resizable"></cl-switch>
 					</cl-list-item>
 				</cl-list>
 			</demo-item>
 		</view>
-
-		<cl-cropper
-			ref="cropperRef"
-			:resizable="resizable"
-			@crop="onCrop"
-			@load="onImageLoad"
-		></cl-cropper>
 	</cl-page>
+
+	<cl-cropper
+		ref="cropperRef"
+		:resizable="resizable"
+		@crop="onCrop"
+		@load="onImageLoad"
+	></cl-cropper>
 </template>
 
 <script lang="ts" setup>
@@ -43,11 +43,9 @@ function chooseImage() {
 	});
 }
 
-function onCrop(result: any) {
-	console.log("裁剪结果:", result);
-	uni.showToast({
-		title: "裁剪完成",
-		icon: "success"
+function onCrop(url: string) {
+	uni.previewImage({
+		urls: [url]
 	});
 }
 
@@ -56,6 +54,6 @@ function onImageLoad(e: UniImageLoadEvent) {
 }
 
 onReady(() => {
-	cropperRef.value!.open('https://uni-docs.cool-js.com/demo/pages/demo/static/bg2.png');
+	cropperRef.value!.open("https://uni-docs.cool-js.com/demo/pages/demo/static/bg2.png");
 });
 </script>

+ 5 - 5
pnpm-lock.yaml

@@ -22,8 +22,8 @@ importers:
         specifier: ^7.27.6
         version: 7.28.1
       '@cool-vue/ai':
-        specifier: ^1.1.4
-        version: 1.1.4
+        specifier: ^1.1.5
+        version: 1.1.5
       '@cool-vue/vite-plugin':
         specifier: ^8.2.5
         version: 8.2.5
@@ -81,8 +81,8 @@ packages:
     resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==}
     engines: {node: '>=6.9.0'}
 
-  '@cool-vue/ai@1.1.4':
-    resolution: {integrity: sha512-1OKM1PnxMYzpzSTC7RjqnEcpwWKhAAns5/YJ5yi3RJY5vRRV6ZA0MbeMYvgNLLkSg5qEsUrC0lanKyX26B0R6g==}
+  '@cool-vue/ai@1.1.5':
+    resolution: {integrity: sha512-H3A9uml1uiux+g9UPcZT119W3WepvxTx5hs38chwnaj3/zBEF0J2pDI0HNq5FShoHZLQ6+Rq+R7Se0X+CmNU5Q==}
     hasBin: true
 
   '@cool-vue/vite-plugin@8.2.5':
@@ -1358,7 +1358,7 @@ snapshots:
       '@babel/helper-string-parser': 7.27.1
       '@babel/helper-validator-identifier': 7.27.1
 
-  '@cool-vue/ai@1.1.4':
+  '@cool-vue/ai@1.1.5':
     dependencies:
       axios: 1.10.0
       chalk: 4.1.2

+ 2 - 2
types/uni-app.d.ts

@@ -394,8 +394,8 @@ declare interface UniElement {
 	style: CSSStyleDeclaration;
 	classList: string[];
 	takeSnapshot(options: {
-		success: (res: { tempFilePath: string }) => void;
-		fail: (err: { errCode: number; errMsg: string }) => void;
+		success?: (res: { tempFilePath: string }) => void;
+		fail?: (err: { errCode: number; errMsg: string }) => void;
 	}): void;
 }
 

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

@@ -2,10 +2,10 @@
 	<view
 		class="cl-cropper"
 		:class="[pt.className]"
-		@touchstart="onImageTouchStart"
-		@touchmove.stop.prevent="onImageTouchMove"
-		@touchend="onImageTouchEnd"
-		@touchcancel="onImageTouchEnd"
+		@touchstart="onTouchStart"
+		@touchmove.stop.prevent="onTouchMove"
+		@touchend="onTouchEnd"
+		@touchcancel="onTouchEnd"
 		v-if="visible"
 	>
 		<!-- 图片容器 - 可拖拽和缩放的图片区域 -->
@@ -15,7 +15,7 @@
 				:class="[pt.image?.className]"
 				:src="imageUrl"
 				:style="imageStyle"
-				@load="onImageLoaded as any"
+				@load="onImageLoaded"
 			></image>
 		</view>
 
@@ -44,20 +44,42 @@
 					<view class="cl-cropper__guide-line cl-cropper__guide-line--h2"></view>
 					<view class="cl-cropper__guide-line cl-cropper__guide-line--v1"></view>
 					<view class="cl-cropper__guide-line cl-cropper__guide-line--v2"></view>
-				</view>
 
-				<template v-if="resizable">
-					<view
-						v-for="item in ['tl', 'tr', 'bl', 'br']"
-						:key="item"
-						class="cl-cropper__drag-point"
-						:class="[`cl-cropper__drag-point--${item}`]"
-						@touchstart="onResizeStart($event as TouchEvent, item)"
-					>
-						<view class="cl-cropper__corner-indicator"></view>
+					<view class="cl-cropper__guide-text">
+						<cl-text
+							:pt="{
+								className: '!text-xl'
+							}"
+							color="white"
+						>
+							{{ cropBox.width }}
+						</cl-text>
+
+						<cl-icon name="close-line" color="white"></cl-icon>
+
+						<cl-text
+							:pt="{
+								className: '!text-xl'
+							}"
+							color="white"
+						>
+							{{ cropBox.height }}
+						</cl-text>
 					</view>
-				</template>
+				</view>
 			</view>
+
+			<template v-if="resizable">
+				<view
+					v-for="item in ['tl', 'tr', 'bl', 'br']"
+					:key="item"
+					class="cl-cropper__drag-point"
+					:class="[`cl-cropper__drag-point--${item}`]"
+					@touchstart.stop="onResizeStart($event as TouchEvent, item)"
+				>
+					<view class="cl-cropper__corner-indicator"></view>
+				</view>
+			</template>
 		</view>
 
 		<!-- 底部按钮组 -->
@@ -94,16 +116,28 @@
 
 			<!-- 确定 -->
 			<view class="cl-cropper__actions-item">
-				<cl-icon name="check-line" color="white" :size="50" @tap="performCrop"></cl-icon>
+				<cl-icon name="check-line" color="white" :size="50" @tap="toPng"></cl-icon>
 			</view>
 		</view>
+
+		<!-- 裁剪用 -->
+		<view class="cl-cropper__canvas">
+			<canvas
+				ref="canvasRef"
+				:id="canvasId"
+				:style="{
+					height: `${cropBox.height}px`,
+					width: `${cropBox.width}px`
+				}"
+			></canvas>
+		</view>
 	</view>
 </template>
 
 <script setup lang="ts">
-import { computed, ref, reactive, nextTick } from "vue";
+import { computed, ref, reactive, nextTick, getCurrentInstance } from "vue";
 import type { PassThroughProps } from "../../types";
-import { parsePt, usePage } from "@/cool";
+import { canvasToPng, parsePt, usePage, uuid } from "@/cool";
 
 // 定义遮罩层样式类型
 type MaskStyle = {
@@ -193,9 +227,18 @@ const props = defineProps({
 // 定义事件发射器
 const emit = defineEmits(["crop", "load", "error"]);
 
+// 获取当前实例
+const { proxy } = getCurrentInstance()!;
+
 // 获取页面实例,用于获取视图尺寸
 const page = usePage();
 
+// 创建唯一的canvas ID
+const canvasId = `cl-cropper__${uuid()}`;
+
+// 创建canvas实例
+const canvasRef = ref<UniElement | null>(null);
+
 // 像素取整工具函数 - 避免小数点造成的样式兼容问题
 function toPixel(value: number): number {
 	return Math.round(value); // 四舍五入取整
@@ -363,7 +406,7 @@ function getRotatedImageSize(): Size {
 	};
 }
 
-// 计算双指缩放时的最小图片尺寸(简化版本,确保能覆盖裁剪框)
+// 计算双指缩放时的最小图片尺寸
 function getMinImageSizeForPinch(): Size {
 	// 如果图片未加载,返回零尺寸
 	if (!imageInfo.isLoaded) {
@@ -372,14 +415,14 @@ function getMinImageSizeForPinch(): Size {
 
 	// 计算图片原始宽高比
 	const originalRatio = imageInfo.width / imageInfo.height;
-	
+
 	// 获取旋转角度
 	const angle = ((rotate.value % 360) + 360) % 360;
-	
+
 	// 获取裁剪框需要的最小覆盖尺寸
 	let requiredW: number; // 旋转后需要覆盖裁剪框宽度的图片实际尺寸
 	let requiredH: number; // 旋转后需要覆盖裁剪框高度的图片实际尺寸
-	
+
 	if (angle == 90 || angle == 270) {
 		// 旋转90度/270度时,图片的宽变成高,高变成宽
 		// 所以图片实际宽度需要覆盖裁剪框高度,实际高度需要覆盖裁剪框宽度
@@ -390,11 +433,11 @@ function getMinImageSizeForPinch(): Size {
 		requiredW = cropBox.width;
 		requiredH = cropBox.height;
 	}
-	
+
 	// 根据图片原始比例,计算能满足覆盖要求的最小尺寸
 	let minW: number;
 	let minH: number;
-	
+
 	// 比较哪个约束更严格
 	if (requiredW / originalRatio > requiredH) {
 		// 宽度约束更严格
@@ -405,7 +448,7 @@ function getMinImageSizeForPinch(): Size {
 		minH = requiredH;
 		minW = requiredH * originalRatio;
 	}
-	
+
 	return {
 		width: toPixel(minW),
 		height: toPixel(minH)
@@ -423,7 +466,7 @@ function getMinImageSize(): Size {
 	const angle = ((rotate.value % 360) + 360) % 360;
 	let effectiveWidth = imageInfo.width;
 	let effectiveHeight = imageInfo.height;
-	
+
 	// 如果旋转90度或270度,宽高交换
 	if (angle == 90 || angle == 270) {
 		effectiveWidth = imageInfo.height;
@@ -583,9 +626,6 @@ function onImageLoaded(e: UniImageLoadEvent) {
 
 // 开始调整裁剪框尺寸的函数
 function onResizeStart(e: TouchEvent, direction: string) {
-	// 阻止事件冒泡到图片容器
-	e.stopPropagation(); // 避免触发图片的触摸事件
-
 	// 设置调整状态
 	touch.isTouching = true; // 标记正在触摸
 	touch.mode = "resizing"; // 设置为调整尺寸模式
@@ -759,7 +799,7 @@ function centerAndAdjust() {
 	// 应用 maxScale 限制,保持图片比例
 	const maxW = container.width * props.maxScale; // 最大宽度限制
 	const maxH = container.height * props.maxScale; // 最大高度限制
-	
+
 	// 计算统一的最大缩放约束
 	const maxScaleW = maxW / newW; // 最大宽度缩放比例
 	const maxScaleH = maxH / newH; // 最大高度缩放比例
@@ -803,7 +843,7 @@ function onResizeEnd() {
 }
 
 // 处理图片触摸开始事件的函数
-function onImageTouchStart(e: TouchEvent) {
+function onTouchStart(e: TouchEvent) {
 	// 如果组件图片未加载,直接返回
 	if (!imageInfo.isLoaded) return;
 
@@ -842,19 +882,14 @@ function onImageTouchStart(e: TouchEvent) {
 }
 
 // 处理图片触摸移动事件的函数
-function onImageTouchMove(e: TouchEvent) {
+function onTouchMove(e: TouchEvent) {
+	if (!touch.isTouching) return;
+
 	if (touch.mode == "resizing") {
 		onResizeMove(e);
 		return;
 	}
 
-	// 如果组件不在触摸状态或不是图片操作模式,直接返回
-	if (!touch.isTouching || touch.mode != "image") return;
-
-	// 阻止默认行为和事件冒泡
-	e.preventDefault(); // 阻止页面滚动等默认行为
-	e.stopPropagation(); // 阻止事件向上冒泡
-
 	// 根据触摸点数量判断操作类型
 	if (e.touches.length == 1) {
 		// 单指拖拽模式
@@ -891,7 +926,7 @@ function onImageTouchMove(e: TouchEvent) {
 		const minScaleH = minSize.height / newH; // 最小高度缩放比例
 		const maxScaleW = maxW / newW; // 最大宽度缩放比例
 		const maxScaleH = maxH / newH; // 最大高度缩放比例
-		
+
 		// 取最严格的约束条件,确保图片不变形
 		const minScale = Math.max(minScaleW, minScaleH); // 最小缩放约束
 		const maxScale = Math.min(maxScaleW, maxScaleH); // 最大缩放约束
@@ -922,7 +957,7 @@ function onImageTouchMove(e: TouchEvent) {
 }
 
 // 处理图片触摸结束事件的函数
-function onImageTouchEnd() {
+function onTouchEnd() {
 	if (touch.mode == "resizing") {
 		onResizeEnd();
 		return;
@@ -958,51 +993,6 @@ function resetCropper() {
 	}
 }
 
-// 切换水平翻转状态的函数
-function toggleHorizontalFlip() {
-	flipHorizontal.value = !flipHorizontal.value; // 切换水平翻转状态
-}
-
-// 切换垂直翻转状态的函数
-function toggleVerticalFlip() {
-	flipVertical.value = !flipVertical.value; // 切换垂直翻转状态
-}
-
-// 90度旋转
-function rotate90() {
-	rotate.value -= 90; // 旋转90度(逆时针)
-	
-	// 如果图片已加载,检查旋转后是否还能覆盖裁剪框
-	if (imageInfo.isLoaded) {
-		// 获取旋转后的有效尺寸
-		const rotatedSize = getRotatedImageSize();
-		
-		// 检查旋转后的有效尺寸是否能完全覆盖裁剪框
-		const scaleW = cropBox.width / rotatedSize.width;   // 宽度需要的缩放比例
-		const scaleH = cropBox.height / rotatedSize.height; // 高度需要的缩放比例
-		const requiredScale = Math.max(scaleW, scaleH);     // 取最大比例确保完全覆盖
-		
-		// 如果需要放大图片(旋转后尺寸不够覆盖裁剪框)
-		if (requiredScale > 1) {
-			// 同比例放大图片尺寸
-			imageSize.width = toPixel(imageSize.width * requiredScale);
-			imageSize.height = toPixel(imageSize.height * requiredScale);
-		}
-		
-		// 调整边界确保图片完全覆盖裁剪框
-		adjustBounds();
-	}
-}
-
-// 执行裁剪操作的函数
-function performCrop() {
-	// 检查图片是否已加载
-	if (!imageInfo.isLoaded) {
-		emit("error", "图片尚未加载完成,无法执行裁剪操作"); // 发送错误事件
-		return; // 提前退出
-	}
-}
-
 // 是否显示
 const visible = ref(false);
 
@@ -1037,10 +1027,112 @@ function chooseImage() {
 	});
 }
 
+// 切换水平翻转状态的函数
+function toggleHorizontalFlip() {
+	flipHorizontal.value = !flipHorizontal.value; // 切换水平翻转状态
+}
+
+// 切换垂直翻转状态的函数
+function toggleVerticalFlip() {
+	flipVertical.value = !flipVertical.value; // 切换垂直翻转状态
+}
+
+// 90度旋转
+function rotate90() {
+	rotate.value -= 90; // 旋转90度(逆时针)
+
+	// 如果图片已加载,检查旋转后是否还能覆盖裁剪框
+	if (imageInfo.isLoaded) {
+		// 获取旋转后的有效尺寸
+		const rotatedSize = getRotatedImageSize();
+
+		// 检查旋转后的有效尺寸是否能完全覆盖裁剪框
+		const scaleW = cropBox.width / rotatedSize.width; // 宽度需要的缩放比例
+		const scaleH = cropBox.height / rotatedSize.height; // 高度需要的缩放比例
+		const requiredScale = Math.max(scaleW, scaleH); // 取最大比例确保完全覆盖
+
+		// 如果需要放大图片(旋转后尺寸不够覆盖裁剪框)
+		if (requiredScale > 1) {
+			// 同比例放大图片尺寸
+			imageSize.width = toPixel(imageSize.width * requiredScale);
+			imageSize.height = toPixel(imageSize.height * requiredScale);
+		}
+
+		// 调整边界确保图片完全覆盖裁剪框
+		adjustBounds();
+	}
+}
+
+// 执行裁剪转图片
+async function toPng(): Promise<string> {
+	return new Promise((resolve) => {
+		uni.createCanvasContextAsync({
+			id: canvasId,
+			component: proxy,
+			success: (context: CanvasContext) => {
+				// 获取绘图上下文
+				const ctx = context.getContext("2d")!;
+
+				// #ifdef APP
+				ctx!.reset();
+				// #endif
+
+				// #ifndef APP
+				ctx!.clearRect(0, 0, cropBox.width, cropBox.height);
+				// #endif
+
+				// 获取设备像素比
+				const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
+
+				// #ifndef H5
+				// 设置缩放比例
+				ctx!.scale(dpr, dpr);
+				// #endif
+
+				// 设置宽高
+				ctx!.canvas.width = cropBox.width;
+				ctx!.canvas.height = cropBox.height;
+
+				let img: Image;
+
+				// 微信小程序环境创建图片
+				// #ifdef MP-WEIXIN || APP-HARMONY
+				img = context.createImage();
+				// #endif
+
+				// 其他环境创建图片
+				// #ifndef MP-WEIXIN || APP-HARMONY
+				img = new Image(cropBox.width, cropBox.height);
+				// #endif
+
+				// 设置图片源并在加载完成后绘制
+				img.src = imageUrl.value;
+				img.onload = () => {
+					ctx!.drawImage(img, cropBox.x, cropBox.y, cropBox.width, cropBox.height);
+
+					setTimeout(() => {
+						canvasToPng({
+							proxy,
+							canvasId,
+							canvasRef: canvasRef.value!
+						})
+							.then((url) => {
+								emit("crop", url);
+								resolve(url);
+							})
+							.catch(() => {});
+					}, 10);
+				};
+			}
+		});
+	});
+}
+
 defineExpose({
 	open,
 	close,
-	chooseImage
+	chooseImage,
+	toPng
 });
 </script>
 
@@ -1050,7 +1142,7 @@ defineExpose({
 	z-index: 510;
 
 	&__image {
-		@apply absolute top-0 left-0 flex items-center justify-center w-full h-full;
+		@apply absolute top-0 left-0 flex items-center justify-center w-full h-full pointer-events-none;
 	}
 
 	&__mask {
@@ -1068,7 +1160,7 @@ defineExpose({
 	}
 
 	&__crop-area {
-		@apply relative w-full h-full overflow-visible duration-200;
+		@apply relative w-full h-full overflow-visible duration-200 pointer-events-none;
 		@apply border border-solid;
 		border-color: rgba(255, 255, 255, 0.5);
 
@@ -1078,6 +1170,7 @@ defineExpose({
 	}
 
 	&__guide-lines {
+		@apply flex justify-center items-center;
 		@apply absolute top-0 left-0 w-full h-full pointer-events-none opacity-0 duration-200;
 
 		&.is-show {
@@ -1109,59 +1202,63 @@ defineExpose({
 		}
 	}
 
+	&__guide-text {
+		@apply absolute flex flex-row items-center justify-center;
+	}
+
 	&__corner-indicator {
-		@apply border-white border-solid border-b-transparent border-l-transparent absolute duration-200 pointer-events-auto;
+		@apply border-white border-solid border-b-transparent border-l-transparent absolute duration-200;
 		width: 20px;
 		height: 20px;
 		border-width: 1px;
 	}
 
 	&__drag-point {
-		@apply absolute duration-200 flex items-center justify-center pointer-events-auto;
+		@apply absolute duration-200 flex items-center justify-center overflow-visible;
 		width: 40px;
 		height: 40px;
 
 		&--tl {
-			top: -20px;
-			left: -20px;
+			top: 0;
+			left: 0;
 
 			.cl-cropper__corner-indicator {
 				transform: rotate(-90deg);
-				left: 20px;
-				top: 20px;
+				left: -1px;
+				top: -1px;
 			}
 		}
 
 		&--tr {
-			top: -20px;
-			right: -20px;
+			top: 0;
+			right: 0;
 
 			.cl-cropper__corner-indicator {
 				transform: rotate(0deg);
-				right: 20px;
-				top: 20px;
+				right: -1px;
+				top: -1px;
 			}
 		}
 
 		&--bl {
-			bottom: -20px;
-			left: -20px;
+			bottom: 0;
+			left: 0;
 
 			.cl-cropper__corner-indicator {
 				transform: rotate(180deg);
-				bottom: 20px;
-				left: 20px;
+				bottom: -1px;
+				left: -1px;
 			}
 		}
 
 		&--br {
-			bottom: -20px;
-			right: -20px;
+			bottom: 0;
+			right: 0;
 
 			.cl-cropper__corner-indicator {
 				transform: rotate(90deg);
-				bottom: 20px;
-				right: 20px;
+				bottom: -1px;
+				right: 0-1px;
 			}
 		}
 	}
@@ -1176,5 +1273,10 @@ defineExpose({
 			@apply flex flex-row justify-center items-center flex-1;
 		}
 	}
+
+	&__canvas {
+		@apply absolute top-0;
+		left: -10000px;
+	}
 }
 </style>

+ 3 - 0
uni_modules/cool-ui/components/cl-sign/cl-sign.uvue

@@ -23,6 +23,7 @@ defineOptions({
 	name: "cl-sign"
 });
 
+// 定义组件属性
 const props = defineProps({
 	pt: {
 		type: Object,
@@ -75,8 +76,10 @@ const props = defineProps({
 	}
 });
 
+// 定义事件发射器
 const emit = defineEmits(["change"]);
 
+// 获取当前实例
 const { proxy } = getCurrentInstance()!;
 
 // 触摸点类型

+ 1 - 0
uni_modules/cool-ui/types/component.d.ts

@@ -157,4 +157,5 @@ declare type ClCropperComponentPublicInstance = {
 	open: (url: string) => void;
 	close: () => void;
 	chooseImage: () => void;
+	toPng: () => Promise<string>;
 };