| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956 |
- <script setup lang="ts">
- import { onMounted, onUnmounted, ref, watch } from 'vue';
- // 定义组件参数
- const props = defineProps<{
- /** 裁剪比例 格式如 '1:1', '16:9', '3:4' 等(非必填) */
- aspectRatio?: string;
- /** 容器高度(默认400) */
- height?: number;
- /** 图片地址 */
- img: string;
- /** 容器宽度(默认500) */
- width?: number;
- }>();
- const CROPPER_CONSTANTS = {
- MIN_WIDTH: 60 as const,
- MIN_HEIGHT: 60 as const,
- DEFAULT_WIDTH: 500 as const,
- DEFAULT_HEIGHT: 400 as const,
- PADDING_RATIO: 0.1 as const,
- MAX_PADDING: 50 as const,
- } as const;
- type Point = [number, number]; // [clientX, clientY]
- type Dimension = [number, number, number, number]; // [top, right, bottom, left]
- // 拖拽点类型
- type DragAction =
- | 'bottom'
- | 'bottom-left'
- | 'bottom-right'
- | 'left'
- | 'move'
- | 'right'
- | 'top'
- | 'top-left'
- | 'top-right';
- // DOM 引用
- const containerRef = ref<HTMLDivElement | null>(null);
- const bgImageRef = ref<HTMLImageElement | null>(null);
- const maskRef = ref<HTMLDivElement | null>(null);
- const maskViewRef = ref<HTMLDivElement | null>(null);
- const cropperRef = ref<HTMLDivElement | null>(null);
- const cropperViewRef = ref<HTMLDivElement | null>(null);
- // 响应式数据
- const isCropperVisible = ref<boolean>(false);
- const validAspectRatio = ref<null | number>(null); // 有效比例值(null表示无固定比例)
- const containerWidth = ref<number>(
- props.width ?? CROPPER_CONSTANTS.DEFAULT_WIDTH,
- );
- const containerHeight = ref<number>(
- props.height ?? CROPPER_CONSTANTS.DEFAULT_HEIGHT,
- );
- // 裁剪区域尺寸(top, right, bottom, left)
- const currentDimension = ref<Dimension>([50, 50, 50, 50]);
- const initDimension = ref<Dimension>([50, 50, 50, 50]);
- // 拖拽状态
- const dragging = ref<boolean>(false);
- const startPoint = ref<Point>([0, 0]);
- const startDimension = ref<Dimension>([0, 0, 0, 0]);
- const direction = ref<Dimension>([0, 0, 0, 0]);
- const moving = ref<boolean>(false);
- /**
- * 计算图片的适配尺寸,保证完整显示且不超过最大宽高限制
- */
- const calculateImageFitSize = () => {
- if (!bgImageRef.value) return;
- // 获取图片原始尺寸
- const imgWidth = bgImageRef.value.naturalWidth;
- const imgHeight = bgImageRef.value.naturalHeight;
- if (imgWidth === 0 || imgHeight === 0) return;
- // 计算缩放比例(使用传入的width/height,默认500/400)
- const widthRatio =
- (props.width ?? CROPPER_CONSTANTS.DEFAULT_WIDTH) / imgWidth;
- const heightRatio =
- (props.height ?? CROPPER_CONSTANTS.DEFAULT_HEIGHT) / imgHeight;
- const scaleRatio = Math.min(widthRatio, heightRatio, 1); // 不放大图片,只缩小
- // 计算适配后的容器尺寸
- const fitWidth = Math.floor(imgWidth * scaleRatio);
- const fitHeight = Math.floor(imgHeight * scaleRatio);
- containerWidth.value = fitWidth;
- containerHeight.value = fitHeight;
- // 重置裁剪框初始尺寸(基于新的容器尺寸)
- const padding = Math.min(
- CROPPER_CONSTANTS.MAX_PADDING,
- Math.floor(fitWidth * CROPPER_CONSTANTS.PADDING_RATIO),
- Math.floor(fitHeight * CROPPER_CONSTANTS.PADDING_RATIO),
- );
- initDimension.value = [padding, padding, padding, padding];
- currentDimension.value = [padding, padding, padding, padding];
- };
- /**
- * 验证并解析比例字符串
- * @returns {number|null} 比例值 (width/height),解析失败返回null
- */
- const parseAndValidateAspectRatio = (): null | number => {
- // 如果未传入比例参数,直接返回null
- if (!props.aspectRatio) {
- return null;
- }
- // 验证比例格式
- const ratioRegex = /^[1-9]\d*:[1-9]\d*$/;
- if (!ratioRegex.test(props.aspectRatio)) {
- console.warn('裁剪比例格式错误,应为 "数字:数字" 格式,如 "16:9"');
- return null;
- }
- // 解析比例
- const [width, height] = props.aspectRatio.split(':').map(Number);
- // 验证解析结果有效性
- if (Number.isNaN(width) || Number.isNaN(height) || !width || !height) {
- console.warn('裁剪比例解析失败,宽高必须为正整数');
- return null;
- }
- return width / height;
- };
- /**
- * 设置裁剪区域尺寸
- * @param {Dimension} dimension - [top, right, bottom, left]
- */
- const setDimension = (dimension: Dimension) => {
- currentDimension.value = [...dimension];
- if (maskViewRef.value) {
- maskViewRef.value.style.clipPath = `inset(${dimension[0]}px ${dimension[1]}px ${dimension[2]}px ${dimension[3]}px)`;
- }
- };
- /**
- * 调整裁剪区域至指定比例
- */
- const adjustCropperToAspectRatio = () => {
- if (!cropperRef.value) return;
- // 验证并解析比例
- validAspectRatio.value = parseAndValidateAspectRatio();
- // 如果无有效比例,使用初始尺寸,不强制固定比例
- if (validAspectRatio.value === null) {
- setDimension(initDimension.value);
- return;
- }
- // 有有效比例,按比例调整裁剪框
- const ratio = validAspectRatio.value;
- const containerWidthVal = containerWidth.value;
- const containerHeightVal = containerHeight.value;
- // 根据比例计算裁剪框尺寸
- let newHeight: number, newWidth: number;
- // 先按宽度优先计算
- newWidth = containerWidthVal;
- newHeight = newWidth / ratio;
- // 如果高度超出容器,按高度优先计算
- if (newHeight > containerHeightVal) {
- newHeight = containerHeightVal;
- newWidth = newHeight * ratio;
- }
- // 居中显示
- const leftRight = (containerWidthVal - newWidth) / 2;
- const topBottom = (containerHeightVal - newHeight) / 2;
- const newDimension: Dimension = [topBottom, leftRight, topBottom, leftRight];
- setDimension(newDimension);
- };
- /**
- * 创建裁剪器
- */
- const createCropper = () => {
- // 计算图片适配尺寸
- calculateImageFitSize();
- isCropperVisible.value = true;
- adjustCropperToAspectRatio();
- };
- /**
- * 处理鼠标按下事件
- * @param {MouseEvent} e - 鼠标事件
- * @param {DragAction} action - 操作类型
- */
- const handleMouseDown = (e: MouseEvent, action: DragAction) => {
- dragging.value = true;
- startPoint.value = [e.clientX, e.clientY];
- startDimension.value = [...currentDimension.value];
- direction.value = [0, 0, 0, 0];
- moving.value = false;
- // 处理移动
- if (action === 'move') {
- direction.value[0] = 1;
- direction.value[2] = -1;
- direction.value[3] = 1;
- direction.value[1] = -1;
- moving.value = true;
- return;
- }
- // 处理拖拽方向
- switch (action) {
- case 'bottom': {
- direction.value[2] = -1;
- break;
- }
- case 'bottom-left': {
- direction.value[2] = -1;
- direction.value[3] = 1;
- break;
- }
- case 'bottom-right': {
- direction.value[2] = -1;
- direction.value[1] = -1;
- break;
- }
- case 'left': {
- direction.value[3] = 1;
- break;
- }
- case 'right': {
- direction.value[1] = -1;
- break;
- }
- case 'top': {
- direction.value[0] = 1;
- break;
- }
- case 'top-left': {
- direction.value[0] = 1;
- direction.value[3] = 1;
- break;
- }
- case 'top-right': {
- direction.value[0] = 1;
- direction.value[1] = -1;
- break;
- }
- }
- };
- /**
- * 处理鼠标移动事件
- * @param {MouseEvent} e - 鼠标事件
- */
- const handleMouseMove = (e: MouseEvent) => {
- if (!dragging.value || !cropperRef.value) return;
- const { clientX, clientY } = e;
- const diffX = clientX - startPoint.value[0];
- const diffY = clientY - startPoint.value[1];
- // 处理移动裁剪框
- if (moving.value) {
- handleMoveCropBox(diffX, diffY);
- return;
- }
- // 无有效比例
- if (validAspectRatio.value === null) {
- handleFreeAspectResize(diffX, diffY);
- } else {
- handleFixedAspectResize(diffX, diffY);
- }
- };
- const handleMoveCropBox = (diffX: number, diffY: number) => {
- const newDimension = [...startDimension.value] as Dimension;
- // 计算临时偏移后的位置
- const tempTop = startDimension.value[0] + diffY;
- const tempLeft = startDimension.value[3] + diffX;
- // 计算裁剪框的固定尺寸
- const cropWidth =
- containerWidth.value - startDimension.value[3] - startDimension.value[1];
- const cropHeight =
- containerHeight.value - startDimension.value[0] - startDimension.value[2];
- // 边界限制:确保裁剪框完全在容器内,且尺寸不变
- // 顶部边界:top >= 0,且 bottom = 容器高度 - top - 裁剪高度 >= 0
- newDimension[0] = Math.max(
- 0,
- Math.min(tempTop, containerHeight.value - cropHeight),
- );
- // 底部边界:bottom = 容器高度 - top - 裁剪高度(由top推导,无需额外计算)
- newDimension[2] = containerHeight.value - newDimension[0] - cropHeight;
- // 左侧边界:left >= 0,且 right = 容器宽度 - left - 裁剪宽度 >= 0
- newDimension[3] = Math.max(
- 0,
- Math.min(tempLeft, containerWidth.value - cropWidth),
- );
- // 右侧边界:right = 容器宽度 - left - 裁剪宽度(由left推导,无需额外计算)
- newDimension[1] = containerWidth.value - newDimension[3] - cropWidth;
- // 强制保证尺寸不变(兜底)
- const finalWidth = containerWidth.value - newDimension[3] - newDimension[1];
- const finalHeight = containerHeight.value - newDimension[0] - newDimension[2];
- if (finalWidth !== cropWidth) {
- newDimension[1] = containerWidth.value - newDimension[3] - cropWidth;
- }
- if (finalHeight !== cropHeight) {
- newDimension[2] = containerHeight.value - newDimension[0] - cropHeight;
- }
- // 更新裁剪区域(仅位置变化,尺寸/比例完全不变)
- setDimension(newDimension);
- };
- const handleFreeAspectResize = (diffX: number, diffY: number) => {
- const cropperWidth = containerWidth.value;
- const cropperHeight = containerHeight.value;
- const currentDimensionNew: Dimension = [0, 0, 0, 0];
- // 计算新的尺寸,确保不小于最小值
- currentDimensionNew[0] = Math.min(
- Math.max(startDimension.value[0] + direction.value[0] * diffY, 0),
- cropperHeight - CROPPER_CONSTANTS.MIN_HEIGHT,
- );
- currentDimensionNew[1] = Math.min(
- Math.max(startDimension.value[1] + direction.value[1] * diffX, 0),
- cropperWidth - CROPPER_CONSTANTS.MIN_WIDTH,
- );
- currentDimensionNew[2] = Math.min(
- Math.max(startDimension.value[2] + direction.value[2] * diffY, 0),
- cropperHeight - CROPPER_CONSTANTS.MIN_HEIGHT,
- );
- currentDimensionNew[3] = Math.min(
- Math.max(startDimension.value[3] + direction.value[3] * diffX, 0),
- cropperWidth - CROPPER_CONSTANTS.MIN_WIDTH,
- );
- // 确保裁剪区域宽度和高度不小于最小值
- const newWidth =
- cropperWidth - currentDimensionNew[3] - currentDimensionNew[1];
- const newHeight =
- cropperHeight - currentDimensionNew[0] - currentDimensionNew[2];
- if (newWidth < CROPPER_CONSTANTS.MIN_WIDTH) {
- if (direction.value[3] === 1) {
- currentDimensionNew[3] =
- cropperWidth - currentDimensionNew[1] - CROPPER_CONSTANTS.MIN_WIDTH;
- } else {
- currentDimensionNew[1] =
- cropperWidth - currentDimensionNew[3] - CROPPER_CONSTANTS.MIN_WIDTH;
- }
- }
- if (newHeight < CROPPER_CONSTANTS.MIN_HEIGHT) {
- if (direction.value[0] === 1) {
- currentDimensionNew[0] =
- cropperHeight - currentDimensionNew[2] - CROPPER_CONSTANTS.MIN_HEIGHT;
- } else {
- currentDimensionNew[2] =
- cropperHeight - currentDimensionNew[0] - CROPPER_CONSTANTS.MIN_HEIGHT;
- }
- }
- setDimension(currentDimensionNew);
- };
- const handleFixedAspectResize = (diffX: number, diffY: number) => {
- if (validAspectRatio.value === null) return;
- const cropperWidth = containerWidth.value;
- const cropperHeight = containerHeight.value;
- // 有有效比例 - 固定比例裁剪
- const ratio = validAspectRatio.value;
- const currentWidth =
- cropperWidth - startDimension.value[3] - startDimension.value[1];
- const currentHeight =
- cropperHeight - startDimension.value[0] - startDimension.value[2];
- let newHeight: number, newWidth: number;
- let widthChange = 0;
- let heightChange = 0;
- // 计算宽度/高度变化量
- if (direction.value[3] === 1) widthChange = -diffX;
- else if (direction.value[1] === -1) widthChange = diffX;
- if (direction.value[0] === 1) heightChange = -diffY;
- else if (direction.value[2] === -1) heightChange = diffY;
- const isCornerDrag =
- (direction.value[3] === 1 || direction.value[1] === -1) &&
- (direction.value[0] === 1 || direction.value[2] === -1);
- // 计算新尺寸
- if (isCornerDrag) {
- if (Math.abs(widthChange) > Math.abs(heightChange)) {
- newWidth = Math.max(
- CROPPER_CONSTANTS.MIN_WIDTH,
- currentWidth + widthChange,
- );
- newHeight = newWidth / ratio;
- } else {
- newHeight = Math.max(
- CROPPER_CONSTANTS.MIN_HEIGHT,
- currentHeight + heightChange,
- );
- newWidth = newHeight * ratio;
- }
- } else {
- if (direction.value[3] === 1 || direction.value[1] === -1) {
- newWidth = Math.max(
- CROPPER_CONSTANTS.MIN_WIDTH,
- currentWidth + widthChange,
- );
- newHeight = newWidth / ratio;
- } else {
- newHeight = Math.max(
- CROPPER_CONSTANTS.MIN_HEIGHT,
- currentHeight + heightChange,
- );
- newWidth = newHeight * ratio;
- }
- }
- // 限制最大尺寸
- const maxWidth = cropperWidth;
- const maxHeight = cropperHeight;
- if (newWidth > maxWidth) {
- newWidth = maxWidth;
- newHeight = newWidth / ratio;
- }
- if (newHeight > maxHeight) {
- newHeight = maxHeight;
- newWidth = newHeight * ratio;
- }
- // 计算新的位置
- let newLeft = startDimension.value[3];
- let newTop = startDimension.value[0];
- let newRight = startDimension.value[1];
- let newBottom = startDimension.value[2];
- // 根据拖拽方向调整位置
- if (direction.value[3] === 1) {
- newLeft = cropperWidth - newWidth - startDimension.value[1];
- } else if (direction.value[1] === -1) {
- newRight = cropperWidth - newWidth - startDimension.value[3];
- } else if (!isCornerDrag) {
- // 居中调整
- const currentHorizontalCenter = startDimension.value[3] + currentWidth / 2;
- newLeft = Math.max(
- 0,
- Math.min(cropperWidth - newWidth, currentHorizontalCenter - newWidth / 2),
- );
- newRight = cropperWidth - newWidth - newLeft;
- }
- if (direction.value[0] === 1) {
- newTop = cropperHeight - newHeight - startDimension.value[2];
- } else if (direction.value[2] === -1) {
- newBottom = cropperHeight - newHeight - startDimension.value[0];
- } else if (!isCornerDrag) {
- // 居中调整
- const currentVerticalCenter = startDimension.value[0] + currentHeight / 2;
- newTop = Math.max(
- 0,
- Math.min(
- cropperHeight - newHeight,
- currentVerticalCenter - newHeight / 2,
- ),
- );
- newBottom = cropperHeight - newHeight - newTop;
- }
- // 边界检查
- newLeft = Math.max(0, newLeft);
- newTop = Math.max(0, newTop);
- newRight = Math.max(0, newRight);
- newBottom = Math.max(0, newBottom);
- const newDimension: Dimension = [newTop, newRight, newBottom, newLeft];
- setDimension(newDimension);
- };
- /**
- * 处理鼠标抬起事件
- */
- const handleMouseUp = () => {
- dragging.value = false;
- moving.value = false;
- direction.value = [0, 0, 0, 0];
- };
- /**
- * 处理图片加载完成
- */
- const handleImageLoad = () => {
- createCropper();
- };
- /**
- * 裁剪图片
- * @param {'image/jpeg' | 'image/png'} format - 输出图片格式
- * @param {number} quality - 压缩质量(0-1)
- * @param {number} targetWidth - 目标宽度(可选,不传则为原始裁剪宽度)
- * @param {number} targetHeight - 目标高度(可选,不传则为原始裁剪高度)
- */
- const getCropImage = async (
- format: 'image/jpeg' | 'image/png' = 'image/jpeg',
- quality: number = 0.92,
- targetWidth?: number,
- targetHeight?: number,
- ): Promise<string | undefined> => {
- if (!props.img || !bgImageRef.value || !containerRef.value) return;
- // 创建临时图片对象获取原始尺寸
- const tempImg = new Image();
- // Only set crossOrigin for cross-origin URLs that need CORS
- if (props.img.startsWith('http://') || props.img.startsWith('https://')) {
- try {
- const url = new URL(props.img);
- if (url.origin !== location.origin) {
- tempImg.crossOrigin = 'anonymous';
- }
- } catch {
- // Invalid URL, proceed without crossOrigin
- }
- }
- // 等待临时图片加载完成
- await new Promise<void>((resolve, reject) => {
- const timeout = setTimeout(() => {
- tempImg.removeEventListener('load', handleLoad);
- tempImg.removeEventListener('error', handleError);
- reject(new Error('图片加载超时'));
- }, 10_000);
- const handleLoad = () => {
- clearTimeout(timeout);
- tempImg.removeEventListener('load', handleLoad);
- tempImg.removeEventListener('error', handleError);
- resolve();
- };
- const handleError = (err: ErrorEvent) => {
- clearTimeout(timeout);
- tempImg.removeEventListener('load', handleLoad);
- tempImg.removeEventListener('error', handleError);
- reject(new Error(`图片加载失败: ${err.message}`));
- };
- tempImg.addEventListener('load', handleLoad);
- tempImg.addEventListener('error', handleError);
- tempImg.src = props.img;
- });
- const containerRect = containerRef.value.getBoundingClientRect();
- const imgRect = bgImageRef.value.getBoundingClientRect();
- // 1. 计算图片在容器内的渲染参数
- const containerWidth = containerRect.width;
- const containerHeight = containerRect.height;
- const renderedImgWidth = imgRect.width;
- const renderedImgHeight = imgRect.height;
- const imgOffsetX = (containerWidth - renderedImgWidth) / 2;
- const imgOffsetY = (containerHeight - renderedImgHeight) / 2;
- // 2. 计算裁剪框在容器内的实际坐标
- const [cropTop, cropRight, cropBottom, cropLeft] = currentDimension.value;
- const cropBoxWidth = containerWidth - cropLeft - cropRight;
- const cropBoxHeight = containerHeight - cropTop - cropBottom;
- // 3. 将裁剪框坐标转换为图片上的坐标(考虑图片偏移)
- const cropOnImgX = cropLeft - imgOffsetX;
- const cropOnImgY = cropTop - imgOffsetY;
- // 4. 计算渲染图片到原始图片的缩放比例(关键:保留原始像素)
- const scaleX = tempImg.width / renderedImgWidth;
- const scaleY = tempImg.height / renderedImgHeight;
- // 5. 映射到原始图片的裁剪区域(精确到原始像素)
- const originalCropX = Math.max(0, Math.floor(cropOnImgX * scaleX));
- const originalCropY = Math.max(0, Math.floor(cropOnImgY * scaleY));
- const originalCropWidth = Math.min(
- Math.floor(cropBoxWidth * scaleX),
- tempImg.width - originalCropX,
- );
- const originalCropHeight = Math.min(
- Math.floor(cropBoxHeight * scaleY),
- tempImg.height - originalCropY,
- );
- // 6. 处理高清屏适配(关键:解决Retina屏模糊)
- const dpr = window.devicePixelRatio || 1;
- // 最终画布尺寸(优先使用原始裁剪尺寸,或目标尺寸)
- const finalWidth = targetWidth || originalCropWidth;
- const finalHeight = targetHeight || originalCropHeight;
- // 创建画布(乘以设备像素比,保证高清)
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
- if (!ctx) return;
- // 画布物理尺寸(适配高清屏)
- canvas.width = finalWidth * dpr;
- canvas.height = finalHeight * dpr;
- // 画布显示尺寸(视觉尺寸)
- canvas.style.width = `${finalWidth}px`;
- canvas.style.height = `${finalHeight}px`;
- // 缩放上下文(适配DPR)
- ctx.scale(dpr, dpr);
- // 7. 绘制裁剪后的图片(使用原始像素绘制,保证清晰度)
- ctx.drawImage(
- tempImg,
- originalCropX, // 原始图片裁剪起始X(精确像素)
- originalCropY, // 原始图片裁剪起始Y(精确像素)
- originalCropWidth, // 原始图片裁剪宽度(精确像素)
- originalCropHeight, // 原始图片裁剪高度(精确像素)
- 0, // 画布绘制起始X
- 0, // 画布绘制起始Y
- finalWidth, // 画布绘制宽度(目标尺寸)
- finalHeight, // 画布绘制高度(目标尺寸)
- );
- // 8. 导出图片(指定质量,平衡清晰度和体积)
- return canvas.toDataURL(format, quality);
- };
- // 监听比例变化,重新调整裁剪框
- watch(() => props.aspectRatio, adjustCropperToAspectRatio);
- // 监听width/height变化,重新计算尺寸
- watch([() => props.width, () => props.height], () => {
- calculateImageFitSize();
- adjustCropperToAspectRatio();
- });
- // 组件挂载时注册全局事件
- onMounted(() => {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- // 如果图片已经加载完成,手动触发创建裁剪器
- if (
- bgImageRef.value &&
- bgImageRef.value.complete &&
- bgImageRef.value.naturalWidth > 0
- ) {
- createCropper();
- }
- });
- // 组件卸载时清理
- onUnmounted(() => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- });
- defineExpose({ getCropImage });
- </script>
- <template>
- <div
- :style="{
- width: `${width || CROPPER_CONSTANTS.DEFAULT_WIDTH}px`,
- height: `${height || CROPPER_CONSTANTS.DEFAULT_HEIGHT}px`,
- }"
- class="cropper-action-wrapper"
- >
- <div
- ref="containerRef"
- class="cropper-container"
- :style="{
- width: `${containerWidth}px`,
- height: `${containerHeight}px`,
- }"
- >
- <!-- 原图展示 - 自适应尺寸 -->
- <img
- ref="bgImageRef"
- class="cropper-image"
- :src="img"
- @load="handleImageLoad"
- :style="{
- maxWidth: '100%',
- maxHeight: '100%',
- objectFit: 'contain',
- }"
- alt="裁剪原图"
- />
- <!-- 遮罩层 -->
- <div
- ref="maskRef"
- class="cropper-mask"
- :style="{
- display: isCropperVisible ? 'block' : 'none',
- width: '100%',
- height: '100%',
- }"
- >
- <div
- ref="maskViewRef"
- class="cropper-mask-view"
- :style="{
- backgroundImage: `url(${img})`,
- backgroundSize: 'contain',
- backgroundPosition: 'center',
- backgroundRepeat: 'no-repeat',
- clipPath: `inset(${currentDimension[0]}px ${currentDimension[1]}px ${currentDimension[2]}px ${currentDimension[3]}px)`,
- width: '100%',
- height: '100%',
- }"
- ></div>
- </div>
- <!-- 裁剪框 -->
- <div
- ref="cropperRef"
- class="cropper-box"
- :style="{
- display: isCropperVisible ? 'block' : 'none',
- width: '100%',
- height: '100%',
- }"
- >
- <div
- ref="cropperViewRef"
- class="cropper-view"
- :style="{
- inset: `${currentDimension[0]}px ${currentDimension[1]}px ${currentDimension[2]}px ${currentDimension[3]}px`,
- }"
- >
- <!-- 裁剪框辅助线-->
- <span class="cropper-dashed-h"></span>
- <span class="cropper-dashed-v"></span>
- <!-- 裁剪框拖拽区域 -->
- <span
- class="cropper-move-area"
- @mousedown="handleMouseDown($event, 'move')"
- ></span>
- <!-- 边框线 -->
- <span class="cropper-line-e"></span>
- <span class="cropper-line-n"></span>
- <span class="cropper-line-w"></span>
- <span class="cropper-line-s"></span>
- <!-- 边角拖拽点 -->
- <span
- class="cropper-point cropper-point-ne"
- @mousedown="handleMouseDown($event, 'top-right')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- <span
- class="cropper-point cropper-point-nw"
- @mousedown="handleMouseDown($event, 'top-left')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- <span
- class="cropper-point cropper-point-sw"
- @mousedown="handleMouseDown($event, 'bottom-left')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- <span
- class="cropper-point cropper-point-se"
- @mousedown="handleMouseDown($event, 'bottom-right')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- <!-- 边中点拖拽点 -->
- <span
- class="cropper-point cropper-point-e"
- @mousedown="handleMouseDown($event, 'right')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- <span
- class="cropper-point cropper-point-n"
- @mousedown="handleMouseDown($event, 'top')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- <span
- class="cropper-point cropper-point-w"
- @mousedown="handleMouseDown($event, 'left')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- <span
- class="cropper-point cropper-point-s"
- @mousedown="handleMouseDown($event, 'bottom')"
- >
- <span class="cropper-point-inner"></span>
- </span>
- </div>
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- .cropper-action-wrapper {
- @apply box-border flex items-center justify-center;
- /* 马赛克背景 */
- background-image:
- linear-gradient(45deg, #ccc 25%, transparent 25%),
- linear-gradient(-45deg, #ccc 25%, transparent 25%),
- linear-gradient(45deg, transparent 75%, #ccc 75%),
- linear-gradient(-45deg, transparent 75%, #ccc 75%);
- background-size: 20px 20px;
- background-position:
- 0 0,
- 0 10px,
- 10px -10px,
- -10px 0;
- background-color: transparent;
- }
- .cropper-container {
- @apply relative;
- }
- .cropper-image {
- @apply block;
- }
- /* 遮罩层 */
- .cropper-mask {
- @apply absolute left-0 top-0 bg-black/50;
- }
- .cropper-mask-view {
- @apply absolute left-0 top-0;
- }
- /* 裁剪框 */
- .cropper-box {
- @apply absolute left-0 top-0 z-10;
- }
- .cropper-view {
- @apply absolute bottom-0 left-0 right-0 top-0 select-none outline outline-1 outline-blue-500;
- }
- /* 裁剪框辅助线 */
- .cropper-dashed-h {
- @apply absolute left-0 top-1/3 block h-1/3 w-full border-b border-t border-dashed border-gray-200/50;
- }
- .cropper-dashed-v {
- @apply absolute left-1/3 top-0 block h-full w-1/3 border-l border-r border-dashed border-gray-200/50;
- }
- /* 裁剪框拖拽区域 */
- .cropper-move-area {
- @apply absolute left-0 top-0 block h-full w-full cursor-move bg-white/10;
- }
- /* 边框拖拽线 */
- .cropper-line-e,
- .cropper-line-n,
- .cropper-line-w,
- .cropper-line-s {
- @apply absolute block bg-blue-500/10;
- }
- .cropper-line-e {
- @apply right-[-3px] top-0 h-full w-1;
- }
- .cropper-line-n {
- @apply left-0 top-[-3px] h-1 w-full;
- }
- .cropper-line-w {
- @apply left-[-3px] top-0 h-full w-1;
- }
- .cropper-line-s {
- @apply bottom-[-3px] left-0 h-1 w-full;
- }
- /* 拖拽点 */
- .cropper-point {
- @apply absolute flex h-2 w-2 items-center justify-center bg-blue-500;
- }
- .cropper-point-inner {
- @apply block h-1.5 w-1.5 bg-white;
- }
- /* 边角拖拽点位置和光标 */
- .cropper-point-ne {
- @apply right-[-5px] top-[-5px] cursor-ne-resize;
- }
- .cropper-point-nw {
- @apply left-[-5px] top-[-5px] cursor-nw-resize;
- }
- .cropper-point-sw {
- @apply bottom-[-5px] left-[-5px] cursor-sw-resize;
- }
- .cropper-point-se {
- @apply bottom-[-5px] right-[-5px] cursor-se-resize;
- }
- /* 边中点拖拽点位置和光标 */
- .cropper-point-e {
- @apply right-[-5px] top-1/2 -mt-1 cursor-e-resize;
- }
- .cropper-point-n {
- @apply left-1/2 top-[-5px] -ml-1 cursor-n-resize;
- }
- .cropper-point-w {
- @apply left-[-5px] top-1/2 -mt-1 cursor-w-resize;
- }
- .cropper-point-s {
- @apply bottom-[-5px] left-1/2 -ml-1 cursor-s-resize;
- }
- </style>
|