| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969 |
- <template>
- <view
- class="cl-cropper"
- :class="[
- {
- 'is-disabled': disabled
- },
- pt.className
- ]"
- >
- <view class="cl-cropper__container" :class="[pt.inner?.className]">
- <!-- 图片容器 -->
- <view
- class="cl-cropper__image-container"
- @touchstart="onImageTouchStart"
- @touchmove="onImageTouchMove"
- @touchend="onImageTouchEnd"
- >
- <image
- class="cl-cropper__image"
- :class="[pt.image?.className]"
- :src="src"
- :style="imageStyle"
- @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="onCropAreaTouchStart"
- @touchmove="onCropAreaTouchMove"
- @touchend="onCropAreaTouchEnd"
- >
- <!-- 辅助线 -->
- <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";
- // 类型定义
- type RectType = {
- height: number;
- width: number;
- };
- type ImageInfoType = {
- width: number;
- height: number;
- loaded: boolean;
- };
- type ImageTransformType = {
- translateX: number;
- translateY: number;
- };
- type ImageDisplayType = {
- width: number;
- height: number;
- };
- type CropBoxType = {
- x: number;
- y: number;
- width: number;
- height: number;
- };
- type TouchStateType = {
- startX: number;
- startY: number;
- startDistance: number;
- startWidth: number;
- startHeight: number;
- startTranslateX: number;
- startTranslateY: number;
- touching: boolean;
- mode: string; // image, crop, resize
- resizeDirection: string; // tl, tr, bl, br
- };
- defineOptions({
- name: "cl-cropper"
- });
- const props = defineProps({
- // 透传样式
- pt: {
- type: Object,
- default: () => ({})
- },
- // 图片源
- src: {
- type: String,
- default: ""
- },
- // 裁剪框宽度
- cropWidth: {
- type: Number,
- default: 300
- },
- // 裁剪框高度
- cropHeight: {
- type: Number,
- default: 300
- },
- // 最大缩放比例
- 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 { windowHeight, windowWidth } = uni.getWindowInfo();
- const rect = reactive<RectType>({
- height: windowHeight,
- width: windowWidth
- });
- // 图片信息
- const imageInfo = reactive<ImageInfoType>({
- width: 0,
- height: 0,
- loaded: false
- });
- // 图片变换状态
- const imageTransform = reactive<ImageTransformType>({
- translateX: 0,
- translateY: 0
- });
- // 图片显示尺寸
- const imageDisplay = reactive<ImageDisplayType>({
- width: 0,
- height: 0
- });
- // 裁剪框状态
- const cropBox = reactive<CropBoxType>({
- x: 0,
- y: 0,
- width: props.cropWidth,
- height: props.cropHeight
- });
- // 触摸状态
- const touchState = reactive<TouchStateType>({
- startX: 0,
- startY: 0,
- startDistance: 0,
- startWidth: 0,
- startHeight: 0,
- startTranslateX: 0,
- startTranslateY: 0,
- touching: false,
- mode: "", // image, crop, resize
- resizeDirection: "" // tl, tr, bl, br
- });
- // 缩放状态
- const isResizing = ref(false);
- const showGuideLines = ref(false);
- // 计算图片样式
- const imageStyle = computed(() => {
- if (touchState.touching) {
- return {
- transform: `translate(${imageTransform.translateX}px, ${imageTransform.translateY}px)`,
- transitionProperty: "none",
- height: imageDisplay.height + "px",
- width: imageDisplay.width + "px"
- };
- } else {
- return {
- transform: `translate(${imageTransform.translateX}px, ${imageTransform.translateY}px)`,
- transitionProperty: "transform, width, height",
- transitionDuration: "0.3s",
- height: imageDisplay.height + "px",
- width: imageDisplay.width + "px"
- };
- }
- });
- // 计算裁剪框样式
- 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();
- // 设置初始图片尺寸,确保图片按比例适配到裁剪框
- setInitialSize();
- // 检查边界,确保图片覆盖裁剪框
- adjustImageBounds();
- emit("load", e);
- }
- // 设置初始图片尺寸,确保图片按比例适配到裁剪框
- function setInitialSize() {
- if (!imageInfo.loaded || !imageInfo.width || !imageInfo.height) {
- return;
- }
- console.log(imageInfo.height, imageInfo.width);
- // 计算图片在容器中的基础显示尺寸
- const containerWidth = rect.width;
- const containerHeight = rect.height;
- const imageAspectRatio = imageInfo.width / imageInfo.height;
- const containerAspectRatio = containerWidth / containerHeight;
- let baseDisplayWidth: number;
- let baseDisplayHeight: number;
- if (imageAspectRatio > containerAspectRatio) {
- // 图片较宽,以容器宽度为准
- baseDisplayWidth = containerWidth;
- baseDisplayHeight = containerWidth / imageAspectRatio;
- } else {
- // 图片较高,以容器高度为准
- baseDisplayHeight = containerHeight;
- baseDisplayWidth = containerHeight * imageAspectRatio;
- }
- // 计算确保能覆盖裁剪框的最小尺寸
- const minScaleForWidth = cropBox.width / baseDisplayWidth;
- const minScaleForHeight = cropBox.height / baseDisplayHeight;
- const minScale = Math.max(minScaleForWidth, minScaleForHeight);
- // 设置图片显示尺寸
- imageDisplay.width = baseDisplayWidth * minScale;
- imageDisplay.height = baseDisplayHeight * minScale;
- }
- // 计算图片最小显示尺寸(确保图片尺寸不小于裁剪框)
- function calculateMinSize() {
- if (!imageInfo.loaded || !imageInfo.width || !imageInfo.height) {
- return { width: 0, height: 0 };
- }
- const containerWidth = rect.width;
- const containerHeight = rect.height;
- // 计算图片在容器中的基础显示尺寸(mode="aspectFit"的显示尺寸)
- const imageAspectRatio = imageInfo.width / imageInfo.height;
- const containerAspectRatio = containerWidth / containerHeight;
- let baseDisplayWidth: number;
- let baseDisplayHeight: number;
- if (imageAspectRatio > containerAspectRatio) {
- // 图片较宽,以容器宽度为准
- baseDisplayWidth = containerWidth;
- baseDisplayHeight = containerWidth / imageAspectRatio;
- } else {
- // 图片较高,以容器高度为准
- baseDisplayHeight = containerHeight;
- baseDisplayWidth = containerHeight * imageAspectRatio;
- }
- // 计算使图片尺寸能够完全覆盖裁剪框的最小尺寸
- const minScaleForWidth = cropBox.width / baseDisplayWidth;
- const minScaleForHeight = cropBox.height / baseDisplayHeight;
- const minScale = Math.max(minScaleForWidth, minScaleForHeight);
- // 应用用户设置的最小缩放限制
- const finalScale = Math.max(props.minScale, minScale * 1.01); // 增加1%的缓冲
- return {
- width: baseDisplayWidth * finalScale,
- height: baseDisplayHeight * finalScale
- };
- }
- // 初始化裁剪框
- function initCropBox() {
- const containerWidth = rect.width;
- const containerHeight = rect.height;
- cropBox.width = props.cropWidth;
- cropBox.height = props.cropHeight;
- cropBox.x = (containerWidth - cropBox.width) / 2;
- cropBox.y = (containerHeight - cropBox.height) / 2;
- // 图片加载完成后,确保当前图片尺寸不小于最小要求
- if (imageInfo.loaded) {
- const minSize = calculateMinSize();
- if (imageDisplay.width < minSize.width || imageDisplay.height < minSize.height) {
- imageDisplay.width = minSize.width;
- imageDisplay.height = minSize.height;
- }
- }
- }
- // 图片触摸开始
- 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.startWidth = imageDisplay.width;
- touchState.startHeight = imageDisplay.height;
- // 记录缩放中心点(两指中心)
- 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;
- // 计算新位置
- const newTranslateX = touchState.startTranslateX + deltaX;
- const newTranslateY = touchState.startTranslateY + deltaY;
- // 应用新位置(在移动过程中不做边界检查,等移动结束后再检查)
- imageTransform.translateX = newTranslateX;
- imageTransform.translateY = newTranslateY;
- } 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 scaleFactor = distance / touchState.startDistance;
- // 计算新的图片尺寸
- const newWidth = touchState.startWidth * scaleFactor;
- const newHeight = touchState.startHeight * scaleFactor;
- // 检查尺寸限制
- const minSize = calculateMinSize();
- const containerWidth = rect.width;
- const containerHeight = rect.height;
- const maxWidth = containerWidth * props.maxScale;
- const maxHeight = containerHeight * props.maxScale;
- // 应用尺寸限制
- const finalWidth = Math.max(minSize.width, Math.min(maxWidth, newWidth));
- const finalHeight = Math.max(minSize.height, Math.min(maxHeight, newHeight));
- // 计算当前缩放中心点
- const centerX = (touch1.clientX + touch2.clientX) / 2;
- const centerY = (touch1.clientY + touch2.clientY) / 2;
- // 计算缩放中心相对于容器的偏移
- const containerCenterX = containerWidth / 2;
- const containerCenterY = containerHeight / 2;
- // 计算尺寸变化引起的位移调整
- const widthDelta = finalWidth - touchState.startWidth;
- const heightDelta = finalHeight - touchState.startHeight;
- // 根据缩放中心调整位移
- const offsetX = ((centerX - containerCenterX) * widthDelta) / (2 * touchState.startWidth);
- const offsetY = ((centerY - containerCenterY) * heightDelta) / (2 * touchState.startHeight);
- imageDisplay.width = finalWidth;
- imageDisplay.height = finalHeight;
- imageTransform.translateX = touchState.startTranslateX - offsetX;
- imageTransform.translateY = touchState.startTranslateY - offsetY;
- }
- }
- // 图片触摸结束
- function onImageTouchEnd(e: TouchEvent) {
- touchState.touching = false;
- touchState.mode = "";
- // 检查并调整图片边界,确保覆盖整个裁剪框
- adjustImageBounds();
- }
- // 裁剪区域触摸开始 - 用于在裁剪框内拖拽图片
- function onCropAreaTouchStart(e: TouchEvent) {
- // 如果触摸点在拖拽点上,不处理图片拖拽
- const target = e.target as HTMLElement;
- if (target.classList.contains("cl-cropper__drag-point")) {
- return;
- }
- // 调用图片拖拽逻辑
- onImageTouchStart(e);
- }
- // 裁剪区域触摸移动 - 用于在裁剪框内拖拽图片
- function onCropAreaTouchMove(e: TouchEvent) {
- // 如果不是图片拖拽模式,不处理
- if (touchState.mode != "image") {
- return;
- }
- // 调用图片拖拽逻辑
- onImageTouchMove(e);
- }
- // 裁剪区域触摸结束 - 用于在裁剪框内拖拽图片
- function onCropAreaTouchEnd(e: TouchEvent) {
- // 如果不是图片拖拽模式,不处理
- if (touchState.mode != "image") {
- return;
- }
- // 调用图片拖拽逻辑
- onImageTouchEnd(e);
- }
- // 裁剪框缩放开始
- 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 = rect.width;
- const containerHeight = rect.height;
- // 最小裁剪框尺寸
- 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;
- // 更新起始位置
- 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;
- // 拖拽结束后,自动调整裁剪框尺寸和图片缩放
- adjustCropBoxToDefault();
- // 延迟隐藏辅助线,给用户一点时间看到最终位置
- setTimeout(() => {
- showGuideLines.value = false;
- }, 500);
- }
- // 重置
- function reset() {
- // 重置裁剪框
- initCropBox();
- // 设置初始图片尺寸,确保图片按比例适配到裁剪框
- if (imageInfo.loaded) {
- setInitialSize();
- // 检查边界
- adjustImageBounds();
- } else {
- imageDisplay.width = 0;
- imageDisplay.height = 0;
- imageTransform.translateX = 0;
- imageTransform.translateY = 0;
- }
- }
- // 执行裁剪
- function crop() {
- if (!imageInfo.loaded) {
- emit("error", "图片未加载完成");
- return;
- }
- }
- // 调整裁剪框到默认尺寸(宽度恢复默认,高度按比例计算)
- function adjustCropBoxToDefault() {
- // 记录调整前的状态
- const oldWidth = cropBox.width;
- const oldHeight = cropBox.height;
- const oldImageWidth = imageDisplay.width;
- const oldImageHeight = imageDisplay.height;
- // 计算当前裁剪框的宽高比
- const currentRatio = cropBox.width / cropBox.height;
- // 设置宽度为默认值
- const newWidth = props.cropWidth;
- // 按当前比例计算新高度
- const newHeight = newWidth / currentRatio;
- const containerWidth = rect.width;
- const containerHeight = rect.height;
- // 确保新尺寸不超出容器
- if (newHeight <= containerHeight) {
- cropBox.width = newWidth;
- cropBox.height = newHeight;
- // 重新居中
- cropBox.x = (containerWidth - newWidth) / 2;
- cropBox.y = (containerHeight - newHeight) / 2;
- } else {
- // 如果高度超出,则以高度为准计算
- cropBox.height = Math.min(newHeight, containerHeight - 40); // 留一20px边距
- cropBox.width = cropBox.height * currentRatio;
- cropBox.x = (containerWidth - cropBox.width) / 2;
- cropBox.y = (containerHeight - cropBox.height) / 2;
- }
- // 裁剪框调整完成后,再次调整图片尺寸
- adjustImageSizeAfterCropResize(oldWidth, oldHeight, oldImageWidth, oldImageHeight);
- }
- // 裁剪框调整后的图片尺寸调整
- function adjustImageSizeAfterCropResize(
- oldWidth: number,
- oldHeight: number,
- oldImageWidth: number,
- oldImageHeight: number
- ) {
- if (!imageInfo.loaded) return;
- // 计算裁剪框尺寸变化比例
- const widthRatio = cropBox.width / oldWidth;
- const heightRatio = cropBox.height / oldHeight;
- const avgRatio = (widthRatio + heightRatio) / 2;
- // 根据裁剪框尺寸变化调整图片尺寸
- const adjustedWidth = oldImageWidth * avgRatio;
- const adjustedHeight = oldImageHeight * avgRatio;
- // 计算最终尺寸(强制确保能覆盖新的裁剪框)
- const minSize = calculateMinSize();
- const containerWidth = rect.width;
- const containerHeight = rect.height;
- const maxWidth = containerWidth * props.maxScale;
- const maxHeight = containerHeight * props.maxScale;
- // 强制应用新的尺寸,不允许小于最小值或大于最大值
- imageDisplay.width = Math.max(minSize.width, Math.min(maxWidth, adjustedWidth));
- imageDisplay.height = Math.max(minSize.height, Math.min(maxHeight, adjustedHeight));
- // 图片重新居中
- imageTransform.translateX = 0;
- imageTransform.translateY = 0;
- // 检查并调整图片边界
- adjustImageBounds();
- }
- // 检查并调整图片边界,确保图片完全覆盖裁剪框
- function adjustImageBounds() {
- if (!imageInfo.loaded) return;
- const containerWidth = rect.width;
- const containerHeight = rect.height;
- // 计算图片中心点在容器中的位置
- const imageCenterX = containerWidth / 2 + imageTransform.translateX;
- const imageCenterY = containerHeight / 2 + imageTransform.translateY;
- // 计算图片的边界
- const imageLeft = imageCenterX - imageDisplay.width / 2;
- const imageRight = imageCenterX + imageDisplay.width / 2;
- const imageTop = imageCenterY - imageDisplay.height / 2;
- const imageBottom = imageCenterY + imageDisplay.height / 2;
- // 裁剪框的边界
- const cropLeft = cropBox.x;
- const cropRight = cropBox.x + cropBox.width;
- const cropTop = cropBox.y;
- const cropBottom = cropBox.y + cropBox.height;
- // 检查图片是否完全覆盖裁剪框,如果不覆盖则调整位置
- let newTranslateX = imageTransform.translateX;
- let newTranslateY = imageTransform.translateY;
- // 检查水平方向
- if (imageLeft > cropLeft) {
- // 图片左边界在裁剪框左边界右侧,需要向左移动
- newTranslateX = imageTransform.translateX - (imageLeft - cropLeft);
- } else if (imageRight < cropRight) {
- // 图片右边界在裁剪框右边界左侧,需要向右移动
- newTranslateX = imageTransform.translateX + (cropRight - imageRight);
- }
- // 检查垂直方向
- if (imageTop > cropTop) {
- // 图片上边界在裁剪框上边界下方,需要向上移动
- newTranslateY = imageTransform.translateY - (imageTop - cropTop);
- } else if (imageBottom < cropBottom) {
- // 图片下边界在裁剪框下边界上方,需要向下移动
- newTranslateY = imageTransform.translateY + (cropBottom - imageBottom);
- }
- // 应用调整后的位置
- imageTransform.translateX = newTranslateX;
- imageTransform.translateY = newTranslateY;
- }
- // 初始化
- onMounted(() => {
- initCropBox();
- });
- </script>
- <style lang="scss" scoped>
- .cl-cropper {
- @apply bg-black absolute left-0 top-0 w-full h-full;
- z-index: 100;
- &.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 w-full h-full;
- z-index: 1;
- }
- &__image {
- @apply max-w-full max-h-full;
- }
- &__crop-box {
- @apply absolute;
- z-index: 2;
- pointer-events: none; // 让裁剪框本身不阻挡触摸事件
- }
- &__crop-area {
- @apply relative w-full h-full border border-white border-solid transition-all duration-300;
- pointer-events: auto; // 恢复裁剪区域的触摸事件
- &.is-resizing {
- @apply border-primary-500;
- }
- }
- &__crop-mask {
- @apply absolute bg-black opacity-50;
- &--top {
- @apply top-0 left-0 w-full;
- }
- &--right {
- @apply top-0 right-0 h-full;
- }
- &--bottom {
- @apply bottom-0 left-0 w-full;
- }
- &--left {
- @apply top-0 left-0 h-full;
- }
- }
- &__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;
- height: 0.5px;
- }
- &--h2 {
- @apply top-2/3 left-0 w-full;
- height: 0.5px;
- }
- &--v1 {
- @apply left-1/3 top-0 h-full;
- width: 0.5px;
- }
- &--v2 {
- @apply left-2/3 top-0 h-full;
- width: 0.5px;
- }
- }
- &__drag-point {
- @apply absolute transition-all duration-200;
- touch-action: none;
- // 触发区域为40px
- width: 40px;
- height: 40px;
- // 使用粗线样式代替伪元素
- border: 3px solid white;
- background: transparent;
- &--tl {
- top: -20px;
- left: -20px;
- border-right: none;
- border-bottom: none;
- }
- &--tr {
- top: -20px;
- right: -20px;
- border-left: none;
- border-bottom: none;
- }
- &--bl {
- bottom: -20px;
- left: -20px;
- border-right: none;
- border-top: none;
- }
- &--br {
- bottom: -20px;
- right: -20px;
- border-left: none;
- border-top: none;
- }
- // 缩放时高亮显示
- .is-resizing & {
- @apply scale-110;
- border-color: #3b82f6;
- }
- }
- &__buttons {
- @apply absolute bottom-4 left-0 right-0 flex flex-row justify-center;
- }
- }
- </style>
|