<template>
	<div>
		<el-dialog v-model="dialogVisible" title="剪裁图片" width="1000">
			<!-- 裁剪框 -->
			<div class="cropper">
				<img ref="cropimg" :src="imgSrc" alt />
			</div>
			<div class="cutter-tool">
				<el-button
					icon="ZoomIn"
					class="tool-but"
					style="border-radius: 5px 0 0 5px !important; border-right: none"
					@click="scale(1)" />
				<el-button icon="ZoomOut" class="tool-but" @click="scale(-1)" />
				<el-button icon="ArrowLeft" class="tool-but" @click="moveImg(0)" />
				<el-button icon="ArrowRight" class="tool-but" @click="moveImg(1)" />
				<el-button icon="ArrowUp" class="tool-but" @click="moveImg(2)" />
				<el-button icon="ArrowDown" class="tool-but" @click="moveImg(3)" />
				<el-button icon="Sort" class="tool-but rotate" @click="flipHorizontal" />
				<el-button icon="Sort" class="tool-but" @click="flipVertically" />
				<el-button icon="RefreshRight" class="tool-but" @click="turnImg(90)" />
				<el-button
					icon="RefreshLeft"
					style="border-radius: 0 !important; border-radius: 0 5px 5px 0 !important; margin-left: 0"
					@click="turnImg(-90)" />
			</div>
			<div style="width: calc(45px * 10); margin: 20px auto">
				<el-slider
					v-model="rotationAngle"
					show-input
					:min="-180"
					:max="180"
					:marks="marks"
					@input="sliderInput" />
			</div>
 
			<template #footer>
				<div class="dialog-footer">
					<el-tag type="primary">原图 {{ quality }}</el-tag>
					<div style="display: flex">
						<div style="display: flex; align-items: center">
							<span style="font-weight: bold">品质</span>
							<div style="cursor: pointer; margin: 0 5px; display: flex; align-items: center">
								<el-tooltip
									class="box-item"
									effect="dark"
									content="调低该值可缩小图片体积 调高该值可提升清晰度"
									placement="top">
									<el-icon><QuestionFilled /></el-icon>
								</el-tooltip>
							</div>
							<div style="width: 100px; margin: 0 20px">
								<el-slider v-model="imageQuality" :min="0" :max="10" :format-tooltip="formatTooltip" />
							</div>
						</div>
						<el-button type="info" @click="reset">重置</el-button>
						<el-button @click="close">取消</el-button>
						<el-button type="primary" @click="cropImage"> 确定 </el-button>
					</div>
				</div>
			</template>
		</el-dialog>
	</div>
</template>
 
<script setup>
import { ref, nextTick, watch } from 'vue';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
 
//给父组件传递方法
const emit = defineEmits(['cropImage', 'closeImage']);
 
const props = defineProps({
	corImg: {
		type: [Object, String],
		default: {},
	},
	// 裁剪框的宽高比
	aspectRatio: {
		type: String,
		default: '',
	},
	// 图片格式
	imgFormat: {
		type: String,
		default: 'jpeg',
	},
});
const imgSrc = ref('');
const aspectRatio = ref(NaN);
const imgFormat = ref('image/png');
watch(
	() => props.aspectRatio,
	(newVal) => {
		if (newVal == '') {
			aspectRatio.value = NaN;
		} else {
			aspectRatio.value = Number(newVal.split(':')[0]) / Number(newVal.split(':')[1]);
		}
		console.log(aspectRatio.value);
	},
	{
		immediate: true,
		deep: true,
	},
);
const imgFormatMap = {
	jpeg: 'image/jpeg',
	png: 'image/png',
	webp: 'image/webp',
	'': 'image/jpeg', // 默认值
};
watch(
	() => props.imgFormat,
	(newVal) => {
		imgFormat.value = imgFormatMap[newVal] || 'image/jpeg';
	},
	{
		immediate: true,
		deep: true,
	},
);
// 裁剪框是否显示
const dialogVisible = ref(false);
// 图片元素引用
const cropimg = ref(null);
// 裁剪实例引用
const cropper = ref(null);
const rotationAngle = ref(0);
const quality = ref('');
// 压缩图片的品质值
const imageQuality = ref(10);
const marks = ref({
	'-180': '-180°',
	0: '0°',
	180: '180°',
});
/**
 * 将传入的值除以10并返回结果
 *
 * @param val 需要被除的值
 * @returns 返回除以10后的结果
 */
const formatTooltip = (val) => {
	return val / 10;
};
 
/**
 * 关闭对话框
 *
 * @description 关闭对话框，并将对话框的显示状态设置为false，同时触发'closeImage'事件并传递null作为参数
 */
const close = () => {
	dialogVisible.value = false;
	emit('closeImage', props.corImg);
};
watch(
	() => props.corImg,
	(newVal) => {
		if (newVal) {
			dialogVisible.value = true;
			setTimeout(() => {
				// 判断newVal是字符串还是对象，如果是字符串则直接赋值给imgSrc.value
				if (typeof newVal === 'string' && cropper.value) {
					cropper.value.replace(newVal);
				} else {
					if (cropper.value && newVal.url) {
						cropper.value.replace(newVal.url);
						getQuality(newVal.size);
					}
				}
			}, 500);
		}
	},
	{
		immediate: true,
	},
);
 
watch(
	() => dialogVisible.value,
	(newVal) => {
		if (!newVal) {
			cropper.value?.destroy();
			imageQuality.value = 10;
			rotationAngle.value = 0;
		} else {
			nextTick(() => {
				getCropper();
			});
		}
	},
	{
		immediate: true,
	},
);
 
/**
 * 根据文件大小计算文件质量，并以合适的单位显示
 *
 * @param {number} size - 文件大小（以字节为单位）
 * @returns {void}
 */
const getQuality = (size) => {
	quality.value = size / 1024;
	if (quality.value >= 1024) {
		// 超过1M
		quality.value = (quality.value / 1024).toFixed(2) + 'M';
	} else if (quality.value >= 1) {
		// 1K到1M之间
		quality.value = quality.value.toFixed(2) + 'K';
	} else {
		// 小于1K
		quality.value = (quality.value * 1024).toFixed(2) + 'B'; // 可以选择转换为字节，或者保持小数形式
	}
};
 
/**
 * 获取裁剪器实例
 *
 * @returns 裁剪器实例
 */
const getCropper = () => {
	cropper.value = new Cropper(cropimg.value, {
		/*
			定义裁剪器的拖动模式
				move: 移动图片
				crop: 创建一个新的裁剪框
				none: 什么也不做
		*/
		dragMode: 'move',
		aspectRatio: aspectRatio.value, // 设置裁剪框的宽高比。默认值是 NaN，意味着比例自由
		initialAspectRatio: NaN, // 定义裁剪框的初始宽高比。默认和图片容器的宽高比相同。只有在 aspectRatio 的值为 NaN时才可以设置
		data: null, // 之前存储的裁剪后的数据，将在初始化时传递给setData方法 只有在 autoCrop 的值为 true时可用
		preview: '', // 添加额外的元素(容器)以供预览
		responsive: true, // 在窗口大小变化后，重新渲染裁剪器
		restore: false, // 在窗口大小变化后，恢复被裁剪的区域
		checkCrossOrigin: false, // 检查当前图片是否为跨域图片
		checkOrientation: true, // 检查当前图片的 EXIF 信息，并根据相关信息旋转图片
		modal: true, // 是否在图片和裁剪框之间显示黑色蒙版。
		guides: true, // 是否显示裁剪框的辅助线
		center: true, // 是否显示裁剪框中心的指示器。
		highlight: true, // 是否显示裁剪框上面的白色蒙版(突出显示裁剪框）。
		background: true, // 是否显示裁剪框的背景。
		autoCrop: true, // 是否在初始化时自动裁剪图片。默认值为 true
		autoCropArea: 1, // 设置裁剪框的初始大小。默认值为 auto，即自动调整为图片的 80%
		movable: true, // 是否可以移动图片。默认为 true，即允许移动
		rotatable: true, // 是否可以旋转图片。默认为 true，即允许旋转
		scalable: true, // 是否可以缩放图片（以图片中心点为原点进行缩放）。
		zoomable: true, // 是否可以缩放图片（以图片左上角为原点进行缩放）。
		zoomOnTouch: true, // 是否允许通过拖动来缩放图片。
		zoomOnWheel: true, // 是否可以通过鼠标滚轮来缩放图片。默认为 true，即允许通过鼠标滚轮进行缩放
		wheelZoomRatio: 0.1, // 鼠标滚轮缩放图片的比例。默认为 0.1，即每次滚动时缩小或放大 10%
		cropBoxMovable: true, // 是否可以移动裁剪框。默认为 true，即允许移动
		cropBoxResizable: true, // 是否可以改变裁剪框的大小。默认为 true，即允许改变大小
		toggleDragModeOnDblclick: false, // 当点击两次时可以在“crop”和“move”之间切换拖拽模式 需要浏览器支持 dblclick 事件。
		/*
			裁剪框的视图模式:
				0 无限制
				1 限制裁剪框不能超出图片的范围
				2 限制裁剪框不能超出图片的范围且图片填充模式为 cover 最长边填充
				3 限制裁剪框不能超出图片的范围 且图片填充模式为 contain 最短边填充
		*/
		viewMode: 0,
		minContainerWidth: 200, // 设置裁剪器容器的最小宽度。默认为 200px
		minContainerHeight: 100, // 设置裁剪器容器的最小高度。默认为 150px
		minCanvasWidth: 0, // 设置图片容器的最小宽度。默认为 0，即不限制
		minCanvasHeight: 0, // 设置图片容器的最小高度。默认为 0，即不限制
		minCropBoxWidth: 0, // 设置裁剪框的最小宽度。默认为 0  注: 这个尺寸是相对于页面的，而不是图片。
		minCropBoxHeight: 0, // 设置裁剪框的最小高度。默认为 0  注: 这个尺寸是相对于页面的，而不是图片。
		ready: ready(), // ready 事件的快捷写法。 在初始化完成后触发。
		cropstart: cropstart(), // cropstart 事件的快捷写法。 在开始裁剪时触发。
		cropmove: null, // cropmove 事件的快捷写法。 在裁剪时触发。
		cropend: null, // cropend 事件的快捷写法。 在结束裁剪时触发。
		crop: null, // crop 事件的快捷写法。 在完成裁剪后触发。
		zoom: null, // zoom 事件的快捷写法。 当图片缩放时触发。
	});
};
 
const ready = (env) => {
	// console.log('初始化', env);
};
const cropstart = (env) => {
	// console.log('开始裁剪', env);
};
 
/**
 * 根据传入的参数调整图片缩放比例
 *
 * @param env 缩放方向参数，大于0表示放大，小于等于0表示缩小
 */
const scale = (env) => {
	if (env > 0) {
		// 放大图片
		cropper.value.zoom(0.1);
	} else {
		// 缩小图片
		cropper.value.zoom(-0.1);
	}
};
 
/**
 * 移动图片位置
 *
 * @param env 环境变量，决定图片移动的方向
 *             0: 向左移动
 *             1: 向右移动
 *             2: 向上移动
 *             3: 向下移动
 */
const moveImg = (env) => {
	if (env == 0) {
		// 向左移动图片
		cropper.value.move(-10, 0);
	} else if (env == 1) {
		// 向右移动图片
		cropper.value.move(10, 0);
	} else if (env == 2) {
		// 向上移动图片
		cropper.value.move(0, -10);
	} else if (env == 3) {
		// 向下移动图片
		cropper.value.move(0, 10);
	}
};
 
const scaleX = ref(1); // 水平翻转的缩放值
const scaleY = ref(1); // 垂直翻转的缩放值
/**
 * 水平翻转图片
 */
const flipHorizontal = () => {
	if (scaleX.value == 1) {
		scaleX.value = -1;
	} else {
		scaleX.value = 1;
	}
	cropper.value.scale(scaleX.value, scaleY.value);
};
 
/**
 * 垂直翻转函数
 *
 */
const flipVertically = () => {
	if (scaleY.value == 1) {
		scaleY.value = -1;
	} else {
		scaleY.value = 1;
	}
	cropper.value.scale(scaleX.value, scaleY.value);
};
 
/**
 * 根据环境变量旋转图片
 *
 * @param {Object} env - 包含旋转角度的环境变量对象
 * @returns {void}
 */
const turnImg = (env) => {
	cropper.value.rotate(env);
};
 
const sliderInput = (val) => {
	cropper.value.rotateTo(Number(val));
};
const reset = () => {
	cropper.value.reset();
};
 
const cropImage = () => {
	const canvas = cropper.value.getCroppedCanvas(); // 获取裁剪后的canvas元素
	// 减少图片体积，压缩图片 0-1之间，1为原始大小
	canvas.toBlob(
		(blob) => {
			const url = URL.createObjectURL(blob); // 创建临时url地址
			const file = new File([blob], props.corImg.name || 'croppedImage.jpg', {
				type: blob.type,
				lastModified: Date.now(),
			});
			const data = {
				name: file.name,
				size: file.size,
				raw: file,
				url: url,
				uid: props.corImg.uid,
			};
			dialogVisible.value = false;
			emit('cropImage', data);
		},
		imgFormat.value,
		imageQuality.value / 10, // 动态调整压缩质量
	);
};
 
/**
 * 将Data URL转换为Blob对象
 *
 * @param dataurl Data URL字符串
 * @returns 返回转换后的Blob对象
 */
const dataURLtoBlob = (dataurl) => {
	var arr = dataurl.split(','),
		mime = arr[0].match(/:(.*?);/)[1],
		bstr = atob(arr[1]),
		n = bstr.length,
		u8arr = new Uint8Array(n);
	while (n--) {
		u8arr[n] = bstr.charCodeAt(n);
	}
	return new Blob([u8arr], { type: mime });
};
</script>
 
<style lang="scss" scoped>
.cropper {
	width: 100%;
	height: 40vh;
 
	img {
		max-height: 100%;
	}
}
 
::v-deep .i-dialog-footer {
	display: none !important;
}
 
.cutter-tool {
	display: flex;
	justify-content: center;
	margin-top: 30px;
	.tool-but {
		margin-left: 0;
		border-radius: 0 !important;
		border-right: none;
	}
	::v-deep .rotate {
		.el-icon {
			transform: rotate(90deg);
		}
	}
}
.dialog-footer {
	width: 100%;
	display: flex;
	justify-content: space-between;
	align-items: center;
}
</style>