Browse Source

test 图片裁剪

icssoa 8 months ago
parent
commit
dfc0538537

+ 19 - 0
.cursor/rules/template.mdc

@@ -0,0 +1,19 @@
+---
+description: Code template
+globs: *.uvue
+alwaysApply: false
+---
+
+## 页面模板代码
+
+```uvue
+<template>
+    <cl-page>
+        <view class="p-3"></view>
+    </cl-page>
+</template>
+
+<script lang="ts" setup>
+
+</script>
+```

+ 6 - 0
pages.json

@@ -386,6 +386,12 @@
 					"style": {
 						"navigationBarTitleText": "Vibrate 震动"
 					}
+				},
+				{
+					"path": "other/cropper",
+					"style": {
+						"navigationBarTitleText": "Cropper 图片裁剪"
+					}
 				}
 			]
 		}

+ 84 - 0
pages/demo/other/cropper.uvue

@@ -0,0 +1,84 @@
+<template>
+	<cl-page>
+		<view class="p-4">
+			<cl-text size="lg" bold class="mb-4">图片裁剪器演示</cl-text>
+
+			<view class="mb-4">
+				<cl-text size="sm" color="info" class="mb-2">操作说明:</cl-text>
+				<cl-text size="xs" color="placeholder" class="block mb-1"
+					>• 拖拽图片进行移动</cl-text
+				>
+				<cl-text size="xs" color="placeholder" class="block mb-1"
+					>• 双指缩放图片(围绕双指中心缩放)</cl-text
+				>
+				<cl-text size="xs" color="placeholder" class="block mb-1"
+					>• 拖拽裁剪框进行移动</cl-text
+				>
+				<cl-text size="xs" color="placeholder" class="block mb-1"
+					>• 拖拽裁剪框四角进行缩放</cl-text
+				>
+				<cl-text size="xs" color="placeholder" class="block mb-1"
+					>• 缩放时会显示辅助线</cl-text
+				>
+				<cl-text size="xs" color="warning" class="block"
+					>• 图片会自动保持不小于裁剪框大小</cl-text
+				>
+			</view>
+
+			<view class="flex justify-center">
+				<cl-cropper
+					:src="imageSrc"
+					:width="320"
+					:height="320"
+					:crop-width="200"
+					:crop-height="200"
+					:max-scale="3"
+					:min-scale="0.5"
+					@crop="onCrop"
+					@load="onImageLoad"
+				></cl-cropper>
+			</view>
+
+			<view class="mt-4 space-y-2">
+				<cl-button @tap="selectImage" block>选择图片</cl-button>
+				<cl-button @tap="useDefaultImage" type="light" block>使用默认图片</cl-button>
+			</view>
+		</view>
+	</cl-page>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+
+const imageSrc = ref("/static/logo.png");
+
+function selectImage() {
+	// 选择图片
+	uni.chooseImage({
+		count: 1,
+		sizeType: ["original", "compressed"],
+		sourceType: ["album", "camera"],
+		success: (res) => {
+			if (res.tempFilePaths.length > 0) {
+				imageSrc.value = res.tempFilePaths[0];
+			}
+		}
+	});
+}
+
+function useDefaultImage() {
+	imageSrc.value = "/static/logo.png";
+}
+
+function onCrop(result: any) {
+	console.log("裁剪结果:", result);
+	uni.showToast({
+		title: "裁剪完成",
+		icon: "success"
+	});
+}
+
+function onImageLoad(e: any) {
+	console.log("图片加载完成:", e);
+}
+</script>

+ 727 - 0
uni_modules/cool-ui/components/cl-cropper/cl-cropper.uvue

@@ -0,0 +1,727 @@
+<template>
+	<view
+		class="cl-cropper"
+		:class="[
+			{
+				'is-disabled': disabled
+			},
+			pt.className
+		]"
+		:style="{
+			width: width + 'px',
+			height: height + 'px'
+		}"
+	>
+		<view class="cl-cropper__container" :class="[pt.inner?.className]">
+			<!-- 图片容器 -->
+			<view
+				class="cl-cropper__image-container"
+				:style="imageContainerStyle"
+				@touchstart="onImageTouchStart"
+				@touchmove="onImageTouchMove"
+				@touchend="onImageTouchEnd"
+			>
+				<image
+					class="cl-cropper__image"
+					:class="[pt.image?.className]"
+					:src="src"
+					:style="imageStyle"
+					mode="aspectFit"
+					@load="onImageLoad"
+				></image>
+			</view>
+
+			<!-- 裁剪框 -->
+			<view
+				class="cl-cropper__crop-box"
+				:class="[pt.cropBox?.className]"
+				:style="cropBoxStyle"
+			>
+				<view class="cl-cropper__crop-mask cl-cropper__crop-mask--top"></view>
+				<view class="cl-cropper__crop-mask cl-cropper__crop-mask--right"></view>
+				<view class="cl-cropper__crop-mask cl-cropper__crop-mask--bottom"></view>
+				<view class="cl-cropper__crop-mask cl-cropper__crop-mask--left"></view>
+
+				<!-- 裁剪区域 -->
+				<view
+					class="cl-cropper__crop-area"
+					:class="{ 'is-resizing': isResizing }"
+					@touchstart="onCropTouchStart"
+					@touchmove="onCropTouchMove"
+					@touchend="onCropTouchEnd"
+				>
+					<!-- 辅助线 -->
+					<view class="cl-cropper__guide-lines" v-if="showGuideLines">
+						<view class="cl-cropper__guide-line cl-cropper__guide-line--h1"></view>
+						<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>
+					
+					<!-- 拖拽点 -->
+					<view 
+						class="cl-cropper__drag-point cl-cropper__drag-point--tl"
+						@touchstart="onResizeTouchStart"
+						@touchmove="onResizeTouchMove" 
+						@touchend="onResizeTouchEnd"
+						data-direction="tl"
+					></view>
+					<view 
+						class="cl-cropper__drag-point cl-cropper__drag-point--tr"
+						@touchstart="onResizeTouchStart"
+						@touchmove="onResizeTouchMove"
+						@touchend="onResizeTouchEnd"
+						data-direction="tr"
+					></view>
+					<view 
+						class="cl-cropper__drag-point cl-cropper__drag-point--bl"
+						@touchstart="onResizeTouchStart"
+						@touchmove="onResizeTouchMove"
+						@touchend="onResizeTouchEnd"
+						data-direction="bl"
+					></view>
+					<view 
+						class="cl-cropper__drag-point cl-cropper__drag-point--br"
+						@touchstart="onResizeTouchStart"
+						@touchmove="onResizeTouchMove"
+						@touchend="onResizeTouchEnd"
+						data-direction="br"
+					></view>
+				</view>
+			</view>
+
+			<!-- 操作按钮 -->
+			<view class="cl-cropper__buttons" v-if="showButtons">
+				<cl-button
+					type="light"
+					size="small"
+					:pt="{ className: pt.button?.className }"
+					@tap="reset"
+				>
+					重置
+				</cl-button>
+				<cl-button
+					type="primary"
+					size="small"
+					:pt="{ className: pt.button?.className }"
+					@tap="crop"
+				>
+					裁剪
+				</cl-button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, reactive, onMounted, type PropType } from "vue";
+import type { PassThroughProps } from "../../types";
+import { parsePt, parseRpx } from "@/cool";
+
+defineOptions({
+	name: "cl-cropper"
+});
+
+const props = defineProps({
+	// 透传样式
+	pt: {
+		type: Object,
+		default: () => ({})
+	},
+	// 图片源
+	src: {
+		type: String,
+		default: ""
+	},
+	// 容器宽度
+	width: {
+		type: [String, Number] as PropType<string | number>,
+		default: 375
+	},
+	// 容器高度
+	height: {
+		type: [String, Number] as PropType<string | number>,
+		default: 375
+	},
+	// 裁剪框宽度
+	cropWidth: {
+		type: [String, Number] as PropType<string | number>,
+		default: 200
+	},
+	// 裁剪框高度
+	cropHeight: {
+		type: [String, Number] as PropType<string | number>,
+		default: 200
+	},
+	// 最大缩放比例
+	maxScale: {
+		type: Number,
+		default: 3
+	},
+	// 最小缩放比例
+	minScale: {
+		type: Number,
+		default: 0.5
+	},
+	// 是否显示操作按钮
+	showButtons: {
+		type: Boolean,
+		default: true
+	},
+	// 输出图片质量
+	quality: {
+		type: Number,
+		default: 0.9
+	},
+	// 输出图片格式
+	format: {
+		type: String as PropType<"jpg" | "png">,
+		default: "jpg"
+	},
+	// 是否禁用
+	disabled: {
+		type: Boolean,
+		default: false
+	}
+});
+
+// 事件定义
+const emit = defineEmits(["crop", "load", "error"]);
+
+// 透传样式类型
+type PassThrough = {
+	className?: string;
+	inner?: PassThroughProps;
+	image?: PassThroughProps;
+	cropBox?: PassThroughProps;
+	button?: PassThroughProps;
+};
+
+// 解析透传样式
+const pt = computed(() => parsePt<PassThrough>(props.pt));
+
+// 图片信息
+const imageInfo = reactive({
+	width: 0,
+	height: 0,
+	loaded: false
+});
+
+// 图片变换状态
+const imageTransform = reactive({
+	scale: 1,
+	translateX: 0,
+	translateY: 0
+});
+
+// 裁剪框状态
+const cropBox = reactive({
+	x: 0,
+	y: 0,
+	width: 200,
+	height: 200
+});
+
+// 触摸状态
+const touchState = reactive({
+	startX: 0,
+	startY: 0,
+	startDistance: 0,
+	startScale: 1,
+	startTranslateX: 0,
+	startTranslateY: 0,
+	touching: false,
+	mode: "", // image, crop, resize
+	resizeDirection: "" // tl, tr, bl, br
+});
+
+// 缩放状态
+const isResizing = ref(false);
+const showGuideLines = ref(false);
+
+// 计算图片容器样式
+const imageContainerStyle = computed(() => {
+	return {
+		width: "100%",
+		height: "100%",
+		overflow: "hidden"
+	};
+});
+
+// 计算图片样式
+const imageStyle = computed(() => {
+	return {
+		transform: `translate3d(${imageTransform.translateX}px, ${imageTransform.translateY}px, 0) scale(${imageTransform.scale})`,
+		transformOrigin: "center center",
+		transition: touchState.touching ? "none" : "transform 0.3s ease"
+	};
+});
+
+// 计算裁剪框样式
+const cropBoxStyle = computed(() => {
+	return {
+		left: `${cropBox.x}px`,
+		top: `${cropBox.y}px`,
+		width: `${cropBox.width}px`,
+		height: `${cropBox.height}px`
+	};
+});
+
+// 图片加载完成
+function onImageLoad(e: any) {
+	imageInfo.width = e.detail.width;
+	imageInfo.height = e.detail.height;
+	imageInfo.loaded = true;
+
+	// 初始化裁剪框位置
+	initCropBox();
+
+	emit("load", e);
+}
+
+// 计算图片最小缩放比例(确保图片不小于裁剪框)
+function calculateMinScale() {
+	if (!imageInfo.loaded || !imageInfo.width || !imageInfo.height) {
+		return props.minScale;
+	}
+
+	const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
+	const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
+
+	// 计算图片在容器中的显示尺寸(保持宽高比)
+	const imageAspectRatio = imageInfo.width / imageInfo.height;
+	const containerAspectRatio = containerWidth / containerHeight;
+
+	let displayWidth: number;
+	let displayHeight: number;
+
+	if (imageAspectRatio > containerAspectRatio) {
+		// 图片较宽,以容器宽度为准
+		displayWidth = containerWidth;
+		displayHeight = containerWidth / imageAspectRatio;
+	} else {
+		// 图片较高,以容器高度为准
+		displayHeight = containerHeight;
+		displayWidth = containerHeight * imageAspectRatio;
+	}
+
+	// 计算使图片能够覆盖裁剪框的最小缩放比例
+	const minScaleX = cropBox.width / displayWidth;
+	const minScaleY = cropBox.height / displayHeight;
+	const calculatedMinScale = Math.max(minScaleX, minScaleY);
+
+	// 确保不低于用户设置的最小缩放比例
+	return Math.max(props.minScale, calculatedMinScale);
+}
+
+// 初始化裁剪框
+function initCropBox() {
+	const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
+	const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
+
+	cropBox.width = parseFloat(parseRpx(props.cropWidth!).replace("px", ""));
+	cropBox.height = parseFloat(parseRpx(props.cropHeight!).replace("px", ""));
+	cropBox.x = (containerWidth - cropBox.width) / 2;
+	cropBox.y = (containerHeight - cropBox.height) / 2;
+
+	// 图片加载完成后,确保当前缩放比例不小于最小要求
+	if (imageInfo.loaded) {
+		const minScale = calculateMinScale();
+		if (imageTransform.scale < minScale) {
+			imageTransform.scale = minScale;
+		}
+	}
+}
+
+// 图片触摸开始
+function onImageTouchStart(e: TouchEvent) {
+	if (props.disabled || !imageInfo.loaded) return;
+
+	touchState.touching = true;
+	touchState.mode = "image";
+
+	if (e.touches != null && e.touches.length == 1) {
+		// 单指拖拽
+		touchState.startX = e.touches[0].clientX;
+		touchState.startY = e.touches[0].clientY;
+		touchState.startTranslateX = imageTransform.translateX;
+		touchState.startTranslateY = imageTransform.translateY;
+	} else if (e.touches != null && e.touches.length == 2) {
+		// 双指缩放
+		const touch1 = e.touches[0];
+		const touch2 = e.touches[1];
+		
+		// 计算两指间距离
+		touchState.startDistance = Math.sqrt(
+			Math.pow(touch2.clientX - touch1.clientX, 2) +
+				Math.pow(touch2.clientY - touch1.clientY, 2)
+		);
+		touchState.startScale = imageTransform.scale;
+		
+		// 记录缩放中心点(两指中心)
+		touchState.startX = (touch1.clientX + touch2.clientX) / 2;
+		touchState.startY = (touch1.clientY + touch2.clientY) / 2;
+		touchState.startTranslateX = imageTransform.translateX;
+		touchState.startTranslateY = imageTransform.translateY;
+	}
+}
+
+// 图片触摸移动
+function onImageTouchMove(e: TouchEvent) {
+	if (props.disabled || !touchState.touching || touchState.mode != "image") return;
+
+	e.preventDefault();
+
+	if (e.touches != null && e.touches.length == 1) {
+		// 单指拖拽
+		const deltaX = e.touches[0].clientX - touchState.startX;
+		const deltaY = e.touches[0].clientY - touchState.startY;
+
+		imageTransform.translateX = touchState.startTranslateX + deltaX;
+		imageTransform.translateY = touchState.startTranslateY + deltaY;
+	} else if (e.touches != null && e.touches.length == 2) {
+		// 双指缩放
+		const touch1 = e.touches[0];
+		const touch2 = e.touches[1];
+		
+		// 计算当前两指间距离
+		const distance = Math.sqrt(
+			Math.pow(touch2.clientX - touch1.clientX, 2) +
+				Math.pow(touch2.clientY - touch1.clientY, 2)
+		);
+
+		// 计算缩放比例
+		const scale = (distance / touchState.startDistance) * touchState.startScale;
+		
+		// 使用动态计算的最小缩放比例
+		const minScale = calculateMinScale();
+		const newScale = Math.max(minScale, Math.min(props.maxScale, scale));
+		
+		// 计算当前缩放中心点
+		const centerX = (touch1.clientX + touch2.clientX) / 2;
+		const centerY = (touch1.clientY + touch2.clientY) / 2;
+		
+		// 计算缩放中心相对于容器的偏移
+		const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
+		const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
+		const containerCenterX = containerWidth / 2;
+		const containerCenterY = containerHeight / 2;
+		
+		// 缩放时调整位移,使缩放围绕双指中心进行
+		const scaleDelta = newScale - touchState.startScale;
+		const offsetX = (centerX - containerCenterX - touchState.startX + containerCenterX) * scaleDelta / touchState.startScale;
+		const offsetY = (centerY - containerCenterY - touchState.startY + containerCenterY) * scaleDelta / touchState.startScale;
+		
+		imageTransform.scale = newScale;
+		imageTransform.translateX = touchState.startTranslateX - offsetX;
+		imageTransform.translateY = touchState.startTranslateY - offsetY;
+	}
+}
+
+// 图片触摸结束
+function onImageTouchEnd(e: TouchEvent) {
+	touchState.touching = false;
+	touchState.mode = "";
+}
+
+// 裁剪框触摸开始
+function onCropTouchStart(e: TouchEvent) {
+	if (props.disabled) return;
+
+	e.stopPropagation();
+	touchState.touching = true;
+	touchState.mode = "crop";
+
+	if (e.touches != null && e.touches.length == 1) {
+		touchState.startX = e.touches[0].clientX;
+		touchState.startY = e.touches[0].clientY;
+	}
+}
+
+// 裁剪框触摸移动
+function onCropTouchMove(e: TouchEvent) {
+	if (props.disabled || !touchState.touching || touchState.mode != "crop") return;
+
+	e.preventDefault();
+	e.stopPropagation();
+
+	if (e.touches != null && e.touches.length == 1) {
+		const deltaX = e.touches[0].clientX - touchState.startX;
+		const deltaY = e.touches[0].clientY - touchState.startY;
+
+		const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
+		const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
+
+		// 限制裁剪框在容器内
+		cropBox.x = Math.max(0, Math.min(containerWidth - cropBox.width, cropBox.x + deltaX));
+		cropBox.y = Math.max(0, Math.min(containerHeight - cropBox.height, cropBox.y + deltaY));
+
+		touchState.startX = e.touches[0].clientX;
+		touchState.startY = e.touches[0].clientY;
+	}
+}
+
+// 裁剪框触摸结束
+function onCropTouchEnd(e: TouchEvent) {
+	touchState.touching = false;
+	touchState.mode = "";
+}
+
+// 裁剪框缩放开始
+function onResizeTouchStart(e: TouchEvent) {
+	if (props.disabled) return;
+
+	e.stopPropagation();
+	
+	touchState.touching = true;
+	touchState.mode = "resize";
+	isResizing.value = true;
+	showGuideLines.value = true;
+
+	// 从 data-direction 属性获取缩放方向
+	const target = e.target as HTMLElement;
+	touchState.resizeDirection = target.getAttribute("data-direction") || "";
+
+	if (e.touches != null && e.touches.length == 1) {
+		touchState.startX = e.touches[0].clientX;
+		touchState.startY = e.touches[0].clientY;
+	}
+}
+
+// 裁剪框缩放移动
+function onResizeTouchMove(e: TouchEvent) {
+	if (props.disabled || !touchState.touching || touchState.mode != "resize") return;
+
+	e.preventDefault();
+	e.stopPropagation();
+
+	if (e.touches != null && e.touches.length == 1) {
+		const deltaX = e.touches[0].clientX - touchState.startX;
+		const deltaY = e.touches[0].clientY - touchState.startY;
+
+		const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
+		const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
+
+		// 最小裁剪框尺寸
+		const minSize = 50;
+		
+		let newX = cropBox.x;
+		let newY = cropBox.y;
+		let newWidth = cropBox.width;
+		let newHeight = cropBox.height;
+
+		// 根据拖拽方向调整裁剪框
+		switch (touchState.resizeDirection) {
+			case "tl": // 左上角
+				newX = Math.max(0, cropBox.x + deltaX);
+				newY = Math.max(0, cropBox.y + deltaY);
+				newWidth = cropBox.width - deltaX;
+				newHeight = cropBox.height - deltaY;
+				break;
+			case "tr": // 右上角
+				newY = Math.max(0, cropBox.y + deltaY);
+				newWidth = cropBox.width + deltaX;
+				newHeight = cropBox.height - deltaY;
+				break;
+			case "bl": // 左下角
+				newX = Math.max(0, cropBox.x + deltaX);
+				newWidth = cropBox.width - deltaX;
+				newHeight = cropBox.height + deltaY;
+				break;
+			case "br": // 右下角
+				newWidth = cropBox.width + deltaX;
+				newHeight = cropBox.height + deltaY;
+				break;
+		}
+
+		// 确保尺寸不小于最小值且不超出容器
+		if (newWidth >= minSize && newHeight >= minSize) {
+			// 检查是否超出容器边界
+			if (newX >= 0 && newY >= 0 && 
+				newX + newWidth <= containerWidth && 
+				newY + newHeight <= containerHeight) {
+				
+				cropBox.x = newX;
+				cropBox.y = newY;
+				cropBox.width = newWidth;
+				cropBox.height = newHeight;
+
+				// 当裁剪框大小改变时,检查图片是否需要放大
+				const minScale = calculateMinScale();
+				if (imageTransform.scale < minScale) {
+					imageTransform.scale = minScale;
+				}
+
+				// 更新起始位置
+				touchState.startX = e.touches[0].clientX;
+				touchState.startY = e.touches[0].clientY;
+			}
+		}
+	}
+}
+
+// 裁剪框缩放结束
+function onResizeTouchEnd(e: TouchEvent) {
+	touchState.touching = false;
+	touchState.mode = "";
+	touchState.resizeDirection = "";
+	isResizing.value = false;
+	
+	// 延迟隐藏辅助线,给用户一点时间看到最终位置
+	setTimeout(() => {
+		showGuideLines.value = false;
+	}, 500);
+}
+
+// 重置
+function reset() {
+	// 重置位移
+	imageTransform.translateX = 0;
+	imageTransform.translateY = 0;
+	
+	// 重置裁剪框
+	initCropBox();
+	
+	// 设置合适的初始缩放比例(确保图片能覆盖裁剪框)
+	if (imageInfo.loaded) {
+		const minScale = calculateMinScale();
+		imageTransform.scale = Math.max(1, minScale);
+	} else {
+		imageTransform.scale = 1;
+	}
+}
+
+// 执行裁剪
+function crop() {
+	if (!imageInfo.loaded) {
+		emit("error", "图片未加载完成");
+		return;
+	}
+}
+
+// 初始化
+onMounted(() => {
+	if (props.src != "") {
+		initCropBox();
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.cl-cropper {
+	@apply relative overflow-hidden bg-surface-100 rounded-xl;
+
+	&.is-disabled {
+		@apply opacity-50;
+	}
+
+	&__container {
+		@apply relative w-full h-full;
+	}
+
+	&__image-container {
+		@apply absolute top-0 left-0 flex items-center justify-center;
+	}
+
+	&__image {
+		@apply max-w-full max-h-full;
+	}
+
+	&__crop-box {
+		@apply absolute;
+	}
+
+	&__crop-mask {
+		@apply absolute bg-black opacity-50;
+
+		&--top {
+			@apply top-0 left-0 w-full;
+			height: var(--crop-top);
+		}
+
+		&--right {
+			@apply top-0 right-0 h-full;
+			width: var(--crop-right);
+		}
+
+		&--bottom {
+			@apply bottom-0 left-0 w-full;
+			height: var(--crop-bottom);
+		}
+
+		&--left {
+			@apply top-0 left-0 h-full;
+			width: var(--crop-left);
+		}
+	}
+
+	&__crop-area {
+		@apply relative w-full h-full border-2 border-white border-solid transition-all duration-300;
+
+		&.is-resizing {
+			@apply border-primary-500;
+		}
+	}
+
+	&__guide-lines {
+		@apply absolute top-0 left-0 w-full h-full pointer-events-none;
+	}
+
+	&__guide-line {
+		@apply absolute bg-white opacity-70;
+
+		&--h1 {
+			@apply top-1/3 left-0 w-full h-px;
+		}
+
+		&--h2 {
+			@apply top-2/3 left-0 w-full h-px;
+		}
+
+		&--v1 {
+			@apply left-1/3 top-0 w-px h-full;
+		}
+
+		&--v2 {
+			@apply left-2/3 top-0 w-px h-full;
+		}
+	}
+
+	&__drag-point {
+		@apply absolute w-3 h-3 bg-white border border-surface-300 border-solid cursor-move transition-all duration-200;
+
+		&:hover {
+			@apply scale-125 border-primary-500;
+		}
+
+		&--tl {
+			@apply -top-1 -left-1;
+			cursor: nw-resize;
+		}
+
+		&--tr {
+			@apply -top-1 -right-1;
+			cursor: ne-resize;
+		}
+
+		&--bl {
+			@apply -bottom-1 -left-1;
+			cursor: sw-resize;
+		}
+
+		&--br {
+			@apply -bottom-1 -right-1;
+			cursor: se-resize;
+		}
+		
+		// 缩放时高亮显示
+		.is-resizing & {
+			@apply scale-125 border-primary-500 shadow-md;
+		}
+	}
+
+	&__buttons {
+		@apply absolute bottom-4 left-0 right-0 flex justify-center space-x-4;
+	}
+}
+</style>

+ 25 - 0
uni_modules/cool-ui/components/cl-cropper/props.ts

@@ -0,0 +1,25 @@
+import type { PassThroughProps } from "../../types";
+
+export type ClCropperPassThrough = {
+	className?: string;
+	inner?: PassThroughProps;
+	image?: PassThroughProps;
+	cropBox?: PassThroughProps;
+	button?: PassThroughProps;
+};
+
+export type ClCropperProps = {
+	className?: string;
+	pt?: ClCropperPassThrough;
+	src?: string;
+	width?: string | number;
+	height?: string | number;
+	cropWidth?: string | number;
+	cropHeight?: string | number;
+	maxScale?: number;
+	minScale?: number;
+	showButtons?: boolean;
+	quality?: number;
+	format?: "jpg" | "png";
+	disabled?: boolean;
+};