1
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* 图片裁剪核心库 - View显示版本
|
||||
* 使用view和image显示界面,只在导出时使用canvas
|
||||
* 支持H5、App、微信小程序、支付宝小程序、抖音小程序
|
||||
*/
|
||||
|
||||
export default class ImageCropper {
|
||||
constructor(context,options) {
|
||||
|
||||
this.context = context;
|
||||
this.options = options || {};
|
||||
|
||||
this.canvasId = options.canvasId;
|
||||
this.fileType = options.fileType || 'jpg';
|
||||
this.quality = options.quality || 0.8;
|
||||
|
||||
const { windowWidth, windowHeight } = uni.$u.window();
|
||||
|
||||
// 画布尺寸(显示区域)
|
||||
this.canvasWidth = options.canvasWidth || windowWidth;
|
||||
this.canvasHeight = options.canvasHeight || windowHeight;
|
||||
|
||||
// 输出图片配置
|
||||
this.width = options.width || 400;
|
||||
this.height = options.height || 400;
|
||||
|
||||
// 裁剪框配置
|
||||
this.rectWidth = options.rectWidth || 400;
|
||||
this.rectHeight = options.rectHeight || 400;
|
||||
|
||||
// 图片数据
|
||||
this.imgData = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scale: 1,
|
||||
angle: 0,
|
||||
originalWidth: 0,
|
||||
originalHeight: 0,
|
||||
src: ''
|
||||
};
|
||||
|
||||
// 触摸数据
|
||||
this.touch = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
isTouch: false,
|
||||
isMove: false
|
||||
};
|
||||
|
||||
// 图片加载状态
|
||||
this.imageLoaded = false;
|
||||
|
||||
// 回调函数
|
||||
this.onUpdate = options.onUpdate || (() => {});
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
// 设置图片源
|
||||
setImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!src) {
|
||||
reject(new Error('图片路径不能为空'));
|
||||
return;
|
||||
}
|
||||
|
||||
uni.getImageInfo({
|
||||
src: src,
|
||||
success: (res) => {
|
||||
this.imgData.src = src;
|
||||
this.imgData.originalWidth = res.width;
|
||||
this.imgData.originalHeight = res.height;
|
||||
this.imageLoaded = true;
|
||||
this.resetImageData();
|
||||
this.triggerUpdate();
|
||||
resolve(res);
|
||||
},
|
||||
fail: () => reject(new Error('图片信息获取失败'))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 重置图片数据
|
||||
resetImageData() {
|
||||
if (!this.imageLoaded) return;
|
||||
|
||||
const imgWidth = this.imgData.originalWidth;
|
||||
const imgHeight = this.imgData.originalHeight;
|
||||
|
||||
// 计算让图片宽度或高度等于裁剪框尺寸的缩放比例
|
||||
const scaleX = this.rectWidth / imgWidth;
|
||||
const scaleY = this.rectHeight / imgHeight;
|
||||
// 使用较大的缩放比例,确保图片至少有一边等于裁剪框尺寸
|
||||
const scale = Math.max(scaleX, scaleY);
|
||||
|
||||
// 计算裁剪框在画布中的居中位置
|
||||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||||
|
||||
// 保持当前的旋转角度
|
||||
const currentAngle = this.imgData.angle || 0;
|
||||
|
||||
this.imgData = {
|
||||
...this.imgData,
|
||||
x: rectX + (this.rectWidth - imgWidth * scale) / 2,
|
||||
y: rectY + (this.rectHeight - imgHeight * scale) / 2,
|
||||
width: imgWidth * scale,
|
||||
height: imgHeight * scale,
|
||||
scale: scale,
|
||||
angle: currentAngle
|
||||
};
|
||||
|
||||
// 如果图片有旋转,需要重新计算边界限制
|
||||
if (currentAngle !== 0) {
|
||||
this.applyBoundaryConstraints();
|
||||
}
|
||||
}
|
||||
|
||||
// 应用边界约束
|
||||
applyBoundaryConstraints() {
|
||||
// 计算裁剪框在画布中的居中位置
|
||||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||||
|
||||
// 获取旋转后的边界框
|
||||
const rotatedBounds = this.getRotatedBounds();
|
||||
|
||||
// 计算图片中心点相对于裁剪框的偏移范围
|
||||
const maxOffsetX = Math.max(0, (rotatedBounds.width - this.rectWidth) / 2);
|
||||
const maxOffsetY = Math.max(0, (rotatedBounds.height - this.rectHeight) / 2);
|
||||
|
||||
// 裁剪框中心点
|
||||
const rectCenterX = rectX + this.rectWidth / 2;
|
||||
const rectCenterY = rectY + this.rectHeight / 2;
|
||||
|
||||
// 当前图片中心点
|
||||
const imgCenterX = this.imgData.x + this.imgData.width / 2;
|
||||
const imgCenterY = this.imgData.y + this.imgData.height / 2;
|
||||
|
||||
// 限制图片中心点的移动范围
|
||||
const limitedCenterX = Math.max(rectCenterX - maxOffsetX, Math.min(rectCenterX + maxOffsetX, imgCenterX));
|
||||
const limitedCenterY = Math.max(rectCenterY - maxOffsetY, Math.min(rectCenterY + maxOffsetY, imgCenterY));
|
||||
|
||||
// 重新计算图片位置
|
||||
this.imgData.x = limitedCenterX - this.imgData.width / 2;
|
||||
this.imgData.y = limitedCenterY - this.imgData.height / 2;
|
||||
}
|
||||
|
||||
// 计算旋转后图片的边界框
|
||||
getRotatedBounds() {
|
||||
const { width, height, angle } = this.imgData;
|
||||
|
||||
if (angle === 0 || angle % 360 === 0) {
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
// 将角度转换为弧度
|
||||
const rad = (angle * Math.PI) / 180;
|
||||
|
||||
// 计算旋转后的边界框
|
||||
const cos = Math.abs(Math.cos(rad));
|
||||
const sin = Math.abs(Math.sin(rad));
|
||||
|
||||
const rotatedWidth = width * cos + height * sin;
|
||||
const rotatedHeight = width * sin + height * cos;
|
||||
|
||||
return {
|
||||
width: rotatedWidth,
|
||||
height: rotatedHeight
|
||||
};
|
||||
}
|
||||
|
||||
// 触摸开始
|
||||
touchStart(e) {
|
||||
if (!this.imageLoaded) return;
|
||||
|
||||
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const y = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
this.touch.startX = x;
|
||||
this.touch.startY = y;
|
||||
this.touch.isTouch = true;
|
||||
this.touch.isMove = false;
|
||||
}
|
||||
|
||||
// 触摸移动
|
||||
touchMove(e) {
|
||||
if (!this.touch.isTouch || !this.imageLoaded) return;
|
||||
|
||||
e.preventDefault && e.preventDefault();
|
||||
|
||||
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const y = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
// 单点拖拽
|
||||
const deltaX = x - this.touch.startX;
|
||||
const deltaY = y - this.touch.startY;
|
||||
|
||||
// 计算新的图片位置
|
||||
let newX = this.imgData.x + deltaX;
|
||||
let newY = this.imgData.y + deltaY;
|
||||
|
||||
// 计算裁剪框在画布中的居中位置
|
||||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||||
|
||||
// 获取旋转后的边界框
|
||||
const rotatedBounds = this.getRotatedBounds();
|
||||
|
||||
// 计算图片中心点相对于裁剪框的偏移范围
|
||||
const maxOffsetX = Math.max(0, (rotatedBounds.width - this.rectWidth) / 2);
|
||||
const maxOffsetY = Math.max(0, (rotatedBounds.height - this.rectHeight) / 2);
|
||||
|
||||
// 裁剪框中心点
|
||||
const rectCenterX = rectX + this.rectWidth / 2;
|
||||
const rectCenterY = rectY + this.rectHeight / 2;
|
||||
|
||||
// 限制图片中心点的移动范围
|
||||
const imgCenterX = newX + this.imgData.width / 2;
|
||||
const imgCenterY = newY + this.imgData.height / 2;
|
||||
|
||||
const limitedCenterX = Math.max(rectCenterX - maxOffsetX, Math.min(rectCenterX + maxOffsetX, imgCenterX));
|
||||
const limitedCenterY = Math.max(rectCenterY - maxOffsetY, Math.min(rectCenterY + maxOffsetY, imgCenterY));
|
||||
|
||||
// 重新计算图片位置
|
||||
newX = limitedCenterX - this.imgData.width / 2;
|
||||
newY = limitedCenterY - this.imgData.height / 2;
|
||||
|
||||
this.imgData.x = newX;
|
||||
this.imgData.y = newY;
|
||||
|
||||
this.touch.startX = x;
|
||||
this.touch.startY = y;
|
||||
this.touch.isMove = true;
|
||||
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
// 触摸结束
|
||||
touchEnd(e) {
|
||||
this.touch.isTouch = false;
|
||||
this.touch.isMove = false;
|
||||
}
|
||||
|
||||
// 缩放图片
|
||||
scaleImage(ratio) {
|
||||
if (!this.imageLoaded) return;
|
||||
|
||||
const newScale = this.imgData.scale * ratio;
|
||||
|
||||
// 计算新的尺寸
|
||||
const newWidth = this.imgData.originalWidth * newScale;
|
||||
const newHeight = this.imgData.originalHeight * newScale;
|
||||
|
||||
// 限制最小缩放,确保图片至少有一边等于或大于裁剪框
|
||||
const minScaleX = this.rectWidth / this.imgData.originalWidth;
|
||||
const minScaleY = this.rectHeight / this.imgData.originalHeight;
|
||||
const minScale = Math.max(minScaleX, minScaleY);
|
||||
|
||||
// 限制缩放范围
|
||||
if (newScale < minScale || newScale > 3) return;
|
||||
|
||||
// 保存当前中心点
|
||||
const centerX = this.imgData.x + this.imgData.width / 2;
|
||||
const centerY = this.imgData.y + this.imgData.height / 2;
|
||||
|
||||
// 更新图片数据
|
||||
this.imgData.scale = newScale;
|
||||
this.imgData.width = newWidth;
|
||||
this.imgData.height = newHeight;
|
||||
|
||||
// 重新计算位置,保持中心点不变
|
||||
this.imgData.x = centerX - this.imgData.width / 2;
|
||||
this.imgData.y = centerY - this.imgData.height / 2;
|
||||
|
||||
// 应用边界约束
|
||||
this.applyBoundaryConstraints();
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
// 旋转图片
|
||||
rotate(angle = 90) {
|
||||
if (!this.imageLoaded) return;
|
||||
|
||||
this.imgData.angle += angle;
|
||||
this.imgData.angle = this.imgData.angle % 360;
|
||||
|
||||
// 旋转后应用边界约束
|
||||
this.applyBoundaryConstraints();
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
// 重置图片
|
||||
reset() {
|
||||
this.resetImageData();
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
loadImage(canvas,src) {
|
||||
return new Promise( (resolve, reject) => {
|
||||
if (this.options.type == '2d') {
|
||||
var img = canvas.createImage();
|
||||
img.onload = () => {
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
img.src = src;
|
||||
} else {
|
||||
resolve(src);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
canvasToTempFilePath(resolve, reject, canvas) {
|
||||
let params = {
|
||||
canvas: canvas,
|
||||
canvasId: this.canvasId,
|
||||
fileType: this.fileType,
|
||||
quality: this.quality,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('导出图片失败:', err);
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
// #ifdef MP-ALIPAY
|
||||
uni.canvasToTempFilePath(params);
|
||||
// #endif
|
||||
// #ifndef MP-ALIPAY
|
||||
uni.canvasToTempFilePath(params, this.context);
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 导出裁剪图片
|
||||
getCropperImage() {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!this.imageLoaded) {
|
||||
reject(new Error('图片未加载'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算裁剪框在画布中的居中位置
|
||||
const rectX = (this.canvasWidth - this.rectWidth) / 2;
|
||||
const rectY = (this.canvasHeight - this.rectHeight) / 2;
|
||||
|
||||
// 计算图片在裁剪框中的相对位置
|
||||
const imgX = this.imgData.x - rectX;
|
||||
const imgY = this.imgData.y - rectY;
|
||||
|
||||
// 计算缩放比例 - 导出图片尺寸 vs 裁剪框尺寸
|
||||
const scaleX = this.width / this.rectWidth;
|
||||
const scaleY = this.height / this.rectHeight;
|
||||
|
||||
let canvas = null;
|
||||
// #ifdef MP-ALIPAY
|
||||
let ctx = uni.createCanvasContext(this.canvasId);
|
||||
// #endif
|
||||
// #ifndef MP-ALIPAY
|
||||
let ctx = uni.createCanvasContext(this.canvasId, this.context);
|
||||
// #endif
|
||||
|
||||
if(this.options.type == '2d'){
|
||||
canvas = await new Promise(resolve => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this.context)
|
||||
.select(`#${this.canvasId}`)
|
||||
.fields({
|
||||
node: true,
|
||||
size: true
|
||||
})
|
||||
.exec(res => {
|
||||
resolve(res[0].node);
|
||||
});
|
||||
});
|
||||
|
||||
canvas.width = this.width;
|
||||
canvas.height = this.height;
|
||||
|
||||
ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(0, 0, this.width, this.height);
|
||||
|
||||
}else{
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, this.width, this.height);
|
||||
}
|
||||
|
||||
const image = await this.loadImage(canvas,this.imgData.src);
|
||||
|
||||
// 计算缩放后的图片位置和尺寸
|
||||
const scaledImgX = imgX * scaleX;
|
||||
const scaledImgY = imgY * scaleY;
|
||||
const scaledImgWidth = this.imgData.width * scaleX;
|
||||
const scaledImgHeight = this.imgData.height * scaleY;
|
||||
|
||||
// 绘制图片
|
||||
if (this.imgData.angle == 0) {
|
||||
ctx.drawImage(image, scaledImgX, scaledImgY, scaledImgWidth, scaledImgHeight);
|
||||
} else {
|
||||
ctx.save();
|
||||
ctx.translate(scaledImgX + scaledImgWidth / 2, scaledImgY + scaledImgHeight / 2);
|
||||
ctx.rotate((this.imgData.angle * Math.PI) / 180);
|
||||
ctx.drawImage(image, -scaledImgWidth / 2, -scaledImgHeight / 2, scaledImgWidth, scaledImgHeight);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if(this.options.type == '2d') {
|
||||
this.canvasToTempFilePath(resolve, reject, canvas);
|
||||
}else{
|
||||
ctx.draw(false, ()=>{
|
||||
this.canvasToTempFilePath(resolve, reject);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 触发更新回调
|
||||
triggerUpdate() {
|
||||
this.onUpdate({
|
||||
imageLoaded: this.imageLoaded,
|
||||
imageSrc: this.imgData.src,
|
||||
imageData: { ...this.imgData },
|
||||
canvasWidth: this.canvasWidth,
|
||||
canvasHeight: this.canvasHeight,
|
||||
rectWidth: this.rectWidth,
|
||||
rectHeight: this.rectHeight
|
||||
});
|
||||
}
|
||||
|
||||
// 销毁实例
|
||||
destroy() {
|
||||
this.imageLoaded = false;
|
||||
this.imgData = {};
|
||||
this.touch = {};
|
||||
this.options = null;
|
||||
this.onUpdate = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export default {
|
||||
props: {
|
||||
// 是否显示
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cropper.show
|
||||
},
|
||||
// 裁剪框形状,circle-圆形,square-方形
|
||||
shape: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cropper.shape
|
||||
},
|
||||
// 每次显示时是否自动打开选择图片
|
||||
autoChoose: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cropper.autoChoose
|
||||
},
|
||||
// 裁剪框宽度,单位px
|
||||
rectWidth: {
|
||||
type: [String, Number],
|
||||
default: ()=> uni.$u.props.cropper.rectWidth
|
||||
},
|
||||
// 裁剪框高度,单位px
|
||||
rectHeight: {
|
||||
type: [String, Number],
|
||||
default: ()=> uni.$u.props.cropper.rectHeight
|
||||
},
|
||||
// 输出图片宽度,单位px
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: ()=> uni.$u.props.cropper.width
|
||||
},
|
||||
// 输出图片高度,单位px
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: ()=> uni.$u.props.cropper.height
|
||||
},
|
||||
// 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
|
||||
fileType: {
|
||||
type: String,
|
||||
default: ()=> uni.$u.props.cropper.fileType
|
||||
},
|
||||
// 是否显示网格线
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
default: ()=> uni.$u.props.cropper.showGrid
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<view class="u-cropper">
|
||||
<view class="u-cropper__container"
|
||||
@touchstart.prevent="handleTouchStart"
|
||||
@touchmove.prevent="handleTouchMove"
|
||||
@touchend.prevent="handleTouchEnd">
|
||||
<!-- 遮罩层和裁剪框 -->
|
||||
<view class="u-cropper__mask">
|
||||
<!-- 裁剪框边框 -->
|
||||
<view class="u-cropper__cropbox" :class="[showGrid && 'u-cropper__cropbox_grid', `u-cropper__cropbox_${shape}`]" :style="cropBoxStyle">
|
||||
<view v-if="shape === 'square'" class="u-cropper__edge u-cropper__edge--1"></view>
|
||||
<view v-if="shape === 'square'" class="u-cropper__edge u-cropper__edge--2"></view>
|
||||
<view v-if="shape === 'square'" class="u-cropper__edge u-cropper__edge--3"></view>
|
||||
<view v-if="shape === 'square'" class="u-cropper__edge u-cropper__edge--4"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片层 -->
|
||||
<image
|
||||
v-if="viewData.imageLoaded && viewData.imageSrc"
|
||||
class="u-cropper__image"
|
||||
:src="viewData.imageSrc"
|
||||
:style="imageStyle"
|
||||
mode="aspectFit"
|
||||
@error="onImageError"
|
||||
/>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<view class="u-cropper__tools">
|
||||
<view class="u-cropper__button__cancel" @click="close">
|
||||
<text>取消</text>
|
||||
</view>
|
||||
<view class="cropper__button__choose" @click="chooseImage">
|
||||
<u-icon name="photo" size="36" color="#fff"/>
|
||||
</view>
|
||||
<view class="cropper__button__rotate" @click="rotateImage">
|
||||
<u-icon name="rotate" size="36" color="#fff"/>
|
||||
</view>
|
||||
<view class="u-cropper__button__confirm" @click="confirmCrop(false)">
|
||||
<text>确定</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- Canvas层 -->
|
||||
<canvas
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
type="2d"
|
||||
<!-- #endif -->
|
||||
canvas-id="cropper"
|
||||
id="cropper"
|
||||
class="u-cropper__canvas" :style="canvasStyle"></canvas>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import props from './props.js';
|
||||
import mixin from '../../libs/mixin/mixin'
|
||||
import mpMixin from '../../libs/mixin/mpMixin';
|
||||
import ImageCropper from './cropper.js';
|
||||
|
||||
/**
|
||||
* Cropper 图片裁剪组件
|
||||
* @description 封装的图片裁剪组件,支持图片选择、裁剪、旋转等功能,适用于头像上传、图片编辑等场景
|
||||
* @tutorial https://uview.d3u.cn/components/cropper.html
|
||||
*
|
||||
* @property {Boolean} autoChoose 每次显示时是否自动打开选择图片(默认 false )
|
||||
* @property {String | Number} rectWidth 裁剪框宽度,单位px(默认 200 )
|
||||
* @property {String | Number} rectHeight 裁剪框高度,单位px(默认 200 )
|
||||
* @property {String | Number} width 输出图片宽度,单位px(默认 200 )
|
||||
* @property {String | Number} height 输出图片高度,单位px(默认 200 )
|
||||
* @property {String} fileType 输出图片格式,支持jpg/png,建议使用jpg减小文件大小(默认 'jpg' )
|
||||
* @property {Boolean} showGrid 是否显示网格线(默认 true )
|
||||
* @property {String} shape 输入框形状,circle-圆形,square-方形 ( 默认 'square' )
|
||||
* @event {Function} change 选择图片时触发,返回原始图片路径
|
||||
* @event {Function} open 打开裁剪弹窗时触发
|
||||
* @event {Function} close 关闭裁剪弹窗时触发
|
||||
* @event {Function} confirm 确认裁剪时触发,返回裁剪后的图片临时路径
|
||||
* @example <u-cropper @confirm="onCropperConfirm"></u-cropper>
|
||||
*/
|
||||
export default {
|
||||
name: "u-cropper",
|
||||
mixins: [mpMixin, mixin, props],
|
||||
data() {
|
||||
return {
|
||||
cropper: null,
|
||||
canvasId: uni.$u.guid(),
|
||||
viewData: {
|
||||
imageLoaded: false,
|
||||
imageSrc: '',
|
||||
imageData: {},
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
rectWidth: 0,
|
||||
rectHeight: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
// 图片样式 - 从原始数据计算
|
||||
imageStyle() {
|
||||
const imageData = this.viewData.imageData;
|
||||
if (!this.viewData.imageLoaded || !imageData || !imageData.width) return {};
|
||||
|
||||
let duration = '0s';
|
||||
let transform = `translate3d(${imageData.x}px, ${imageData.y}px,0)`;
|
||||
if (imageData.angle && imageData.angle !== 0) {
|
||||
duration = '0.3s';
|
||||
transform += ` rotate(${imageData.angle}deg)`;
|
||||
}
|
||||
|
||||
return {
|
||||
width: uni.$u.addUnit(imageData.width),
|
||||
height: uni.$u.addUnit(imageData.height),
|
||||
transform: transform,
|
||||
transitionDuration: duration
|
||||
};
|
||||
},
|
||||
|
||||
// 裁剪框样式 - 从原始数据计算
|
||||
cropBoxStyle() {
|
||||
if (!this.viewData.canvasWidth) return {};
|
||||
|
||||
const rectX = (this.viewData.canvasWidth - this.viewData.rectWidth) / 2;
|
||||
const rectY = (this.viewData.canvasHeight - this.viewData.rectHeight) / 2;
|
||||
|
||||
return {
|
||||
boxShadow: '0 0 0 '+ this.viewData.canvasWidth +'px rgba(0, 0, 0, 0.5)',
|
||||
left: uni.$u.addUnit(rectX),
|
||||
top: uni.$u.addUnit(rectY),
|
||||
width: uni.$u.addUnit(this.viewData.rectWidth),
|
||||
height: uni.$u.addUnit(this.viewData.rectHeight)
|
||||
};
|
||||
},
|
||||
|
||||
canvasStyle() {
|
||||
return {
|
||||
width: uni.$u.addUnit(this.width),
|
||||
height: uni.$u.addUnit(this.height)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(async ()=>{
|
||||
await uni.$u.sleep(30)
|
||||
this.initCropper();
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
// #ifdef VUE3
|
||||
emits: ['change', 'open', 'close','confirm'],
|
||||
// #endif
|
||||
methods: {
|
||||
// 初始化裁剪器
|
||||
async initCropper() {
|
||||
|
||||
const options = {
|
||||
canvasId: 'cropper',
|
||||
width: uni.$u.getPx(this.width),
|
||||
height: uni.$u.getPx(this.height),
|
||||
rectWidth: uni.$u.getPx(this.rectWidth),
|
||||
rectHeight: uni.$u.getPx(this.rectHeight),
|
||||
fileType: 'jpg',
|
||||
quality: 0.8,
|
||||
onUpdate: (viewData) => {
|
||||
this.viewData = viewData;
|
||||
}
|
||||
};
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
options.type = '2d';
|
||||
// #endif
|
||||
|
||||
this.cropper = new ImageCropper(this, options);
|
||||
|
||||
this.$emit('open');
|
||||
if(this.autoChoose) {
|
||||
this.chooseImage();
|
||||
}
|
||||
},
|
||||
|
||||
// 加载图片
|
||||
loadImage(src) {
|
||||
if (!this.cropper) return;
|
||||
this.cropper.setImage(src).catch(err => {
|
||||
console.error('图片加载失败:', err);
|
||||
uni.showToast({
|
||||
title: '图片加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 选择图片
|
||||
chooseImage() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const imagePath = res.tempFilePaths[0];
|
||||
this.$emit('change', imagePath);
|
||||
this.loadImage(imagePath);
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 图片加载失败
|
||||
onImageError(e) {
|
||||
console.error('图片加载失败', e);
|
||||
uni.showToast({
|
||||
title: '图片加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
// 旋转图片
|
||||
rotateImage() {
|
||||
if (this.cropper) {
|
||||
this.cropper.rotate(90);
|
||||
}
|
||||
},
|
||||
|
||||
// 重置图片
|
||||
resetImage() {
|
||||
if (this.cropper) {
|
||||
this.cropper.reset();
|
||||
}
|
||||
},
|
||||
|
||||
// 确认裁剪
|
||||
async confirmCrop() {
|
||||
if (!this.cropper || !this.viewData.imageLoaded) {
|
||||
uni.showToast({
|
||||
title: '请先选择图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showLoading({
|
||||
title: '裁剪中...',
|
||||
mask: true
|
||||
});
|
||||
|
||||
this.cropper.getCropperImage().then((res)=>{
|
||||
this.$emit('confirm',res);
|
||||
this.close();
|
||||
}).catch(err => {
|
||||
console.error('裁剪失败:', err);
|
||||
uni.showToast({
|
||||
title: '裁剪失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}).finally(()=>{
|
||||
uni.hideLoading();
|
||||
});
|
||||
},
|
||||
|
||||
// 关闭裁剪器
|
||||
close() {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy();
|
||||
this.cropper = null;
|
||||
}
|
||||
this.viewData = {};
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
// 处理触摸开始 - 兼容各平台
|
||||
handleTouchStart(e) {
|
||||
if (!this.cropper) return;
|
||||
|
||||
// 阻止默认行为和事件冒泡
|
||||
e.preventDefault && e.preventDefault();
|
||||
e.stopPropagation && e.stopPropagation();
|
||||
|
||||
// 兼容不同平台的触摸事件
|
||||
let touch;
|
||||
// #ifdef H5
|
||||
touch = e.touches ? e.touches[0] : e;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
touch = e.touches && e.touches[0] ? e.touches[0] : e.changedTouches && e.changedTouches[0] ? e.changedTouches[0] : e;
|
||||
// #endif
|
||||
|
||||
const event = {
|
||||
touches: [{
|
||||
clientX: touch.clientX || touch.x || touch.pageX,
|
||||
clientY: touch.clientY || touch.y || touch.pageY
|
||||
}]
|
||||
};
|
||||
|
||||
this.cropper.touchStart(event);
|
||||
},
|
||||
|
||||
// 处理触摸移动 - 兼容各平台
|
||||
handleTouchMove(e) {
|
||||
if (!this.cropper) return;
|
||||
|
||||
// 阻止默认行为和事件冒泡
|
||||
e.preventDefault && e.preventDefault();
|
||||
e.stopPropagation && e.stopPropagation();
|
||||
|
||||
// 兼容不同平台的触摸事件
|
||||
let touch;
|
||||
// #ifdef H5
|
||||
touch = e.touches ? e.touches[0] : e;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
touch = e.touches && e.touches[0] ? e.touches[0] : e.changedTouches && e.changedTouches[0] ? e.changedTouches[0] : e;
|
||||
// #endif
|
||||
|
||||
const event = {
|
||||
touches: [{
|
||||
clientX: touch.clientX || touch.x || touch.pageX,
|
||||
clientY: touch.clientY || touch.y || touch.pageY
|
||||
}],
|
||||
preventDefault: () => {}
|
||||
};
|
||||
|
||||
this.cropper.touchMove(event);
|
||||
},
|
||||
|
||||
// 处理触摸结束 - 兼容各平台
|
||||
handleTouchEnd(e) {
|
||||
if (!this.cropper) return;
|
||||
|
||||
// 阻止默认行为和事件冒泡
|
||||
e.preventDefault && e.preventDefault();
|
||||
e.stopPropagation && e.stopPropagation();
|
||||
|
||||
this.cropper.touchEnd(e);
|
||||
}
|
||||
},
|
||||
|
||||
// #ifdef VUE2
|
||||
beforeDestroy() {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy();
|
||||
this.cropper = null;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
beforeUnmount() {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy();
|
||||
this.cropper = null;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.u-cropper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
background: #000;
|
||||
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&__cropbox {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 255, 255, .5);
|
||||
|
||||
&_grid {
|
||||
&::before{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 33.3333%;
|
||||
height: 33.3333%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border: 1px dashed rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
&::after{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 33.3333%;
|
||||
width: 33.3333%;
|
||||
height: 100%;
|
||||
border: 1px dashed rgba(255, 255, 255, .5);
|
||||
}
|
||||
}
|
||||
|
||||
&_circle{
|
||||
border-radius: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&_square{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
&__edge {
|
||||
position: absolute;
|
||||
border:3px solid #ffffff;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
&--1 {
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&--2 {
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
border-left: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&--3 {
|
||||
bottom: -3px;
|
||||
left: -3px;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
&--4 {
|
||||
bottom: -3px;
|
||||
right: -3px;
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
// &__grid {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// bottom: 0;
|
||||
|
||||
// &-line {
|
||||
// position: absolute;
|
||||
// background: rgba(255, 255, 255, 0.3);
|
||||
|
||||
// &--v {
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// width: 1px;
|
||||
|
||||
// &1 {
|
||||
// left: 33.33%;
|
||||
// }
|
||||
|
||||
// &2 {
|
||||
// left: 66.67%;
|
||||
// }
|
||||
// }
|
||||
|
||||
// &--h {
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// height: 1px;
|
||||
|
||||
// &1 {
|
||||
// top: 33.33%;
|
||||
// }
|
||||
|
||||
// &2 {
|
||||
// top: 66.67%;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
&__tools {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__button{
|
||||
|
||||
&__cancel{
|
||||
color:#fff;
|
||||
font-weight: 700;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__confirm {
|
||||
color:#fff;
|
||||
font-weight: 700;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
background-color: $u-primary;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__canvas {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user