Browse Source

no message

DaMowang 2 years ago
parent
commit
fd7132407c

+ 1 - 0
src/app.scss

@@ -1,6 +1,7 @@
 /**app.wxss**/
 page {
     min-height: 100%;
+    background: #f5f5f5;
 }
 
 view,

+ 628 - 0
src/components/image-cropper/image-cropper.js

@@ -0,0 +1,628 @@
+/**
+ * 图片编辑器-手势监听
+ * 1. 支持编译到app-vue(uni-app 2.5.5及以上版本)、H5上
+ */
+/** 图片偏移量 */
+var offset = { x: 0, y: 0 };
+/** 图片缩放比例 */
+var scale = 1;
+/** 图片最小缩放比例 */
+var minScale = 1;
+/** 图片旋转角度 */
+var rotate = 0;
+/** 触摸点 */
+var touches = [];
+/** 图片布局信息 */
+var img = {};
+/** 系统信息 */
+var sys = {};
+/** 裁剪区域布局信息 */
+var area = {};
+/** 触摸行为类型 */
+var touchType = '';
+/** 操作角的位置 */
+var activeAngle = 0;
+/** 裁剪区域布局信息偏移量 */
+var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
+/** 元素ID */
+var elIds = {
+	'imageStyles': 'crop-image',
+	'maskStylesList': 'crop-mask-block',
+	'borderStyles': 'crop-border',
+	'circleBoxStyles': 'crop-circle-box',
+	'circleStyles': 'crop-circle',
+	'gridStylesList': 'crop-grid',
+	'angleStylesList': 'crop-angle',
+}
+/** 记录上次初始化时间戳,排除APP重复更新 */
+var timestamp = 0;
+/**
+ * 样式对象转字符串
+ * @param {Object} style 样式对象
+ */
+function styleToString(style) {
+	if(typeof style === 'string') return style;
+	var str = '';
+	for (let k in style) {
+		str += k + ':' + style[k] + ';';
+	}
+	return str;
+}
+/**
+ * 
+ * @param {Object} instance 页面实例对象
+ * @param {Object} key 要修改样式的key
+ * @param {Object|Array} style 样式
+ */
+function setStyle(instance, key, style) {
+	// console.log('setStyle', instance, key, JSON.stringify(style))
+	// #ifdef APP-PLUS
+	if(Object.prototype.toString.call(style) === '[object Array]') {
+		for (var i = 0, len = style.length; i < len; i++) {
+			var el = window.document.getElementById(elIds[key] + '-' + (i + 1));
+			el && (el.style = styleToString(style[i]));
+		}
+	} else {
+		var el = window.document.getElementById(elIds[key]);
+		el && (el.style = styleToString(style));
+	}
+	// #endif
+	// #ifdef H5
+	instance[key] = style;
+	// #endif
+}
+/**
+ * 触发页面实例指定方法
+ * @param {Object} instance 页面实例对象
+ * @param {Object} name 方法名称
+ * @param {Object} obj 传递参数
+ */
+function callMethod(instance, name, obj) {
+	// #ifdef APP-PLUS
+	instance.callMethod(name, obj);
+	// #endif
+	// #ifdef H5
+	instance[name](obj);
+	// #endif
+}
+/**
+ * 计算两点间距
+ * @param {Object} touches 触摸点信息
+ */
+function getDistanceByTouches(touches) {
+	// 根据勾股定理求两点间距离
+	var a = touches[1].pageX - touches[0].pageX;
+	var b = touches[1].pageY - touches[0].pageY;
+	var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
+	// 求两点间的中点坐标
+	// 1. a、b可能为负值
+	// 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
+	// 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
+	var x = touches[1].pageX - a / 2;
+	var y = touches[1].pageY - b / 2;
+	return { c, x, y };
+};
+/**
+ * 检查边界:限制 x、y 拖动范围,禁止滑出边界
+ * @param {Object} e 点坐标
+ */
+function checkRange(e) {
+	var r = rotate / 90 % 2;
+	if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
+		var o = (img.height - img.width) / 2; // 宽高差值一半
+		return {
+			x: Math.min(Math.max(e.x, -img.height + o + area.width + area.left), area.left + o),
+			y: Math.min(Math.max(e.y, -img.width - o + area.height + area.top), area.top - o)
+		}
+	}
+	return {
+		x: Math.min(Math.max(e.x, -img.width + area.width + area.left), area.left),
+		y: Math.min(Math.max(e.y, -img.height + area.height + area.top), area.top)
+	}
+};
+/**
+ * 变更图片布局信息
+ * @param {Object} e 布局信息
+ */
+function changeImageRect(e) {
+	// console.log('changeImageRect', e)
+	offset.x += e.x || 0;
+	offset.y += e.y || 0;
+	if(e.check) { // 检查边界
+		var point = checkRange(offset);
+		if(offset.x !== point.x || offset.y !== point.y) {
+			offset = point;
+		}
+	}
+	
+	// 因频繁修改 width/height 会造成大量的内存消耗,改为scale
+	// e.instance.imageStyles = {
+	// 	width: img.width + 'px',
+	// 	height: img.height + 'px',
+	// 	transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + ox) + 'px) rotate(' + rotate +'deg)'
+	// };
+	var ox = (img.width - img.oldWidth) / 2;
+	var oy = (img.height - img.oldHeight) / 2;
+	// e.instance.imageStyles = {
+	// 	width: img.oldWidth + 'px',
+	// 	height: img.oldHeight + 'px',
+	// 	transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
+	// };
+	setStyle(e.instance, 'imageStyles', {
+		width: img.oldWidth + 'px',
+		height: img.oldHeight + 'px',
+		transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
+	});
+	callMethod(e.instance, 'dataChange', {
+		width: img.width,
+		height: img.height,
+		x: offset.x,
+		y: offset.y,
+		rotate: rotate
+	});
+};
+/**
+ * 变更裁剪区域布局信息
+ * @param {Object} e 布局信息
+ */
+function changeAreaRect(e) {
+	// console.log('changeAreaRect', e)
+	// 变更蒙版样式
+	setStyle(e.instance, 'maskStylesList', [
+		{
+			left: 0,
+			width: (area.left + areaOffset.left) + 'px',
+			top: 0,
+			bottom: 0,
+		},
+		{
+			left: (area.right + areaOffset.right) + 'px',
+			right: 0,
+			top: 0,
+			bottom: 0,
+		},
+		{
+			left: (area.left + areaOffset.left) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			top: 0,
+			height: (area.top + areaOffset.top) + 'px',
+		},
+		{
+			left: (area.left + areaOffset.left) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			top: (area.bottom + areaOffset.bottom) + 'px',
+			// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
+			bottom: 0,
+		}
+	]);
+	// 变更边框样式
+	if(area.showBorder) {
+		setStyle(e.instance, 'borderStyles', {
+			left: (area.left + areaOffset.left) + 'px',
+			top: (area.top + areaOffset.top) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
+		});
+	}
+	
+	// 变更参考线样式
+	if(area.showGrid) {
+		setStyle(e.instance, 'gridStylesList', [
+			{
+				'border-width': '1px 0 0 0',
+				left: (area.left + areaOffset.left) + 'px',
+				right: (area.right + areaOffset.right) + 'px',
+				top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
+				width: (area.width + areaOffset.right - areaOffset.left) + 'px'
+			},
+			{
+				'border-width': '1px 0 0 0',
+				left: (area.left + areaOffset.left) + 'px',
+				right: (area.right + areaOffset.right) + 'px',
+				top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
+				width: (area.width + areaOffset.right - areaOffset.left) + 'px'
+			},
+			{
+				'border-width': '0 1px 0 0',
+				top: (area.top + areaOffset.top) + 'px',
+				bottom: (area.bottom + areaOffset.bottom) + 'px',
+				left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
+				height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
+			},
+			{
+				'border-width': '0 1px 0 0',
+				top: (area.top + areaOffset.top) + 'px',
+				bottom: (area.bottom + areaOffset.bottom) + 'px',
+				left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
+				height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
+			}
+		]);
+	}
+	
+	// 变更四个伸缩角样式
+	if(area.showAngle) {
+		setStyle(e.instance, 'angleStylesList', [
+			{
+				'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
+				left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
+				top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
+			},
+			{
+				'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
+				left: (area.right + areaOffset.right - area.angleSize) + 'px',
+				top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
+			},
+			{
+				'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
+				left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
+				top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
+			},
+			{
+				'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
+				left: (area.right + areaOffset.right - area.angleSize) + 'px',
+				top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
+			}
+		]);
+	}
+	
+	// 变更圆角样式
+	if(area.radius > 0) {
+		var radius = area.radius;
+		if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
+			radius = (area.width / 2);
+		} else { // 圆角矩形
+			if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
+				radius = Math.min(area.width / 2, area.height / 2, radius);
+			}
+		}
+		setStyle(e.instance, 'circleBoxStyles', {
+			left: (area.left + areaOffset.left) + 'px',
+			top: (area.top + areaOffset.top) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
+		});
+		setStyle(e.instance, 'circleStyles', {
+			'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
+			'border-radius': radius + 'px'
+		});
+	}
+};
+/**
+ * 缩放图片
+ * @param {Object} e 布局信息
+ */
+function scaleImage(e) {
+	// console.log('scaleImage', e)
+	var last = scale;
+	scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
+	if(last !== scale) {
+		img.width = img.oldWidth * scale;
+		img.height = img.oldHeight * scale;
+		// 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
+		// 			该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
+		// 			新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
+		// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
+		// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
+		e.x = (e.x - offset.x) * (1 - scale / last);
+		e.y = (e.y - offset.y) * (1 - scale / last);
+		changeImageRect(e);
+		return true;
+	}
+	return false;
+};
+/**
+ * 获取触摸点在哪个角
+ * @param {number} x 触摸点x轴坐标
+ * @param {number} y 触摸点y轴坐标
+ * @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
+ */
+function getToucheAngle(x, y) {
+	// console.log('getToucheAngle', x, y, JSON.stringify(area))
+	var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
+	var oy = sys.navigation ? 0 : sys.windowTop;
+	if(y >= area.top - o + oy && y <= area.top + area.angleSize + o + oy) {
+		if(x >= area.left - o && x <= area.left + area.angleSize + o) {
+			return 1; // 左上角
+		} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
+			return 2; // 右上角
+		}
+	} else if(y >= area.bottom - area.angleSize - o + oy && y <= area.bottom + o + oy) {
+		if(x >= area.left - o && x <= area.left + area.angleSize + o) {
+			return 3; // 左下角
+		} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
+			return 4; // 右下角
+		}
+	}
+	return 0; // 无触摸到角
+};
+/**
+ * 重置数据
+ */
+function resetData() {
+	offset = { x: 0, y: 0 };
+	scale = 1;
+	minScale = 1;
+	rotate = 0;
+};
+function getTouchs(touches) {
+	var result = [];
+	var len = touches ? touches.length : 0
+	for (var i = 0; i < len; i++) {
+		result[i] = {
+			pageX: touches[i].pageX,
+			// h5无标题栏时,窗口顶部距离仍为标题栏高度,且触摸点y轴坐标还是有标题栏的值,即减去标题栏高度的值
+			pageY: touches[i].pageY + sys.windowTop
+		};
+	}
+	return result;
+};
+export default {
+	data() {
+		return {
+			imageStyles: {},
+			maskStylesList: [{}, {}, {}, {}],
+			borderStyles: {},
+			gridStylesList: [{}, {}, {}, {}],
+			angleStylesList: [{}, {}, {}, {}],
+			circleBoxStyles: {},
+			circleStyles: {}
+		}
+	},
+	created() {
+		// 监听 PC 端鼠标滚轮
+		// #ifdef H5
+		window.addEventListener('mousewheel', (e) => {
+			var touchs = getTouchs([e])
+			img.src && scaleImage({
+				instance: this.getInstance(),
+				check: true,
+				// 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
+				scale: e.deltaY > 0 ? -0.05 : 0.05,
+				x: touchs[0].pageX,
+				y: touchs[0].pageY
+			});
+		});
+		// #endif
+	},
+	methods: {
+		getInstance() {
+			// #ifdef APP-PLUS
+			return this.$ownerInstance;
+			// #endif
+			// #ifdef H5
+			return this;
+			// #endif
+		},
+		/**
+		 * 初始化:观察数据变更
+		 * @param {Object} newVal 新数据
+		 * @param {Object} oldVal 旧数据
+		 * @param {Object} o 组件实例对象
+		 */
+		initObserver: function(newVal, oldVal, o, i) {
+			console.log('initObserver', newVal, oldVal, o, i)
+			if(newVal && (!img.src || timestamp !== newVal.timestamp)) {
+				timestamp = newVal.timestamp;
+				img = newVal.img;
+				sys = newVal.sys;
+				area = newVal.area;
+				resetData();
+				img.src && changeImageRect({
+					instance: this.getInstance(),
+					x: (sys.windowWidth - img.width) / 2,
+					y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
+				});
+				changeAreaRect({
+					instance: this.getInstance()
+				});
+			}
+		},
+		/**
+		 * 鼠标滚轮滚动
+		 * @param {Object} e 事件对象
+		 * @param {Object} o 组件实例对象
+		 */
+		mousewheel: function(e, o) {
+			// h5平台 wheel 事件无法判断滚轮滑动方向,需使用 mousewheel 
+		},
+		/**
+		 * 触摸开始
+		 * @param {Object} e 事件对象
+		 * @param {Object} o 组件实例对象
+		 */
+		touchstart: function(e, o) {
+			if(!img.src) return;
+			touches = getTouchs(e.touches);
+			activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
+			if(touches.length === 1 && activeAngle !== 0) {
+				touchType = 'stretch'; // 伸缩裁剪区域
+			} else {
+				touchType = '';
+			}
+			// console.log('touchstart', e, activeAngle)
+		},
+		/**
+		 * 触摸移动
+		 * @param {Object} e 事件对象
+		 * @param {Object} o 组件实例对象
+		 */
+		touchmove: function(e, o) {
+			if(!img.src) return;
+			// console.log('touchmove', e, o)
+			e.touches = getTouchs(e.touches);
+			if(touchType === 'stretch') { // 触摸四个角进行拉伸
+				var point = e.touches[0];
+				var start = touches[0];
+				var x = point.pageX - start.pageX;
+				var y = point.pageY - start.pageY;
+				if(x !== 0 || y !== 0) {
+					var maxX = area.width * (1 - area.minScale);
+					var maxY = area.height * (1 - area.minScale);
+					// console.log(x, y, maxX, maxY)
+					touches[0] = point;
+					switch(activeAngle) {
+						case 1: // 左上角
+							x += areaOffset.left;
+							y += areaOffset.top;
+							if(x >= 0 && y >= 0) { // 有效滑动
+								if(x > y) { // 以x轴滑动距离为缩放基准
+									if(x > maxX) x = maxX;
+									y = x * area.height / area.width;
+								} else { // 以y轴滑动距离为缩放基准
+									if(y > maxY) y = maxY;
+									x = y * area.width / area.height;
+								}
+								areaOffset.left = x;
+								areaOffset.top = y;
+							}
+							break;
+						case 2: // 右上角
+							x += areaOffset.right;
+							y += areaOffset.top;
+							if(x <= 0 && y >= 0) { // 有效滑动
+								if(-x > y) { // 以x轴滑动距离为缩放基准
+									if(-x > maxX) x = -maxX;
+									y = -x * area.height / area.width;
+								} else { // 以y轴滑动距离为缩放基准
+									if(y > maxY) y = maxY;
+									x = -y * area.width / area.height;
+								}
+								areaOffset.right = x;
+								areaOffset.top = y;
+							}
+							break;
+						case 3: // 左下角
+							x += areaOffset.left;
+							y += areaOffset.bottom;
+							if(x >= 0 && y <= 0) { // 有效滑动
+								if(x > -y) { // 以x轴滑动距离为缩放基准
+									if(x > maxX) x = maxX;
+									y = -x * area.height / area.width;
+								} else { // 以y轴滑动距离为缩放基准
+									if(-y > maxY) y = -maxY;
+									x = -y * area.width / area.height;
+								}
+								areaOffset.left = x;
+								areaOffset.bottom = y;
+							}
+							break;
+						case 4: // 右下角
+							x += areaOffset.right;
+							y += areaOffset.bottom;
+							if(x <= 0 && y <= 0) { // 有效滑动
+								if(-x > -y) { // 以x轴滑动距离为缩放基准
+									if(-x > maxX) x = -maxX;
+									y = x * area.height / area.width;
+								} else { // 以y轴滑动距离为缩放基准
+									if(-y > maxY) y = -maxY;
+									x = y * area.width / area.height;
+								}
+								areaOffset.right = x;
+								areaOffset.bottom = y;
+							}
+							break;
+					}
+					// console.log(x, y, JSON.stringify(areaOffset))
+					changeAreaRect({
+						instance: this.getInstance(),
+					});
+					// this.draw();
+				}
+			} else if (e.touches.length == 2) { // 双点触摸缩放
+				var start = getDistanceByTouches(touches);
+				var end = getDistanceByTouches(e.touches);
+				scaleImage({
+					instance: this.getInstance(),
+					check: !area.bounce,
+					scale: (end.c - start.c) / 100,
+					x: end.x,
+					y: end.y
+				});
+				touchType = 'scale';
+			} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
+				touchType = 'move';
+			} else {
+				changeImageRect({
+					instance: this.getInstance(),
+					check: !area.bounce,
+					x: e.touches[0].pageX - touches[0].pageX,
+					y: e.touches[0].pageY - touches[0].pageY
+				});
+				touchType = 'move';
+			}
+			touches = e.touches;
+		},
+		/**
+		 * 触摸结束
+		 * @param {Object} e 事件对象
+		 * @param {Object} o 组件实例对象
+		 */
+		touchend: function(e, o) {
+			if(!img.src) return;
+			if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
+				// 裁剪区域宽度被缩放到多少
+				var left = areaOffset.left;
+				var right = areaOffset.right;
+				var top = areaOffset.top;
+				var bottom = areaOffset.bottom;
+				var w = area.width + right - left;
+				var h = area.height + bottom - top;
+				// 图像放大倍数
+				var p = scale * (area.width / w) - scale;
+				// 复原裁剪区域
+				areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
+				changeAreaRect({
+					instance: this.getInstance(),
+				});
+				scaleImage({
+					instance: this.getInstance(),
+					scale: p,
+					x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
+					y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
+				});
+			} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
+				changeImageRect({
+					instance: this.getInstance(),
+					check: true
+				});
+			}
+		},
+		/**
+		 * 顺时针翻转图片90°
+		 * @param {Object} e 事件对象
+		 * @param {Object} o 组件实例对象
+		 */
+		rotateImage: function(e, o) {
+			rotate = (rotate + 90) % 360;
+			
+			// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
+			var r = rotate / 90 % 2;
+			minScale = 1;
+			if(img.width < area.height) {
+				minScale = area.height / img.oldWidth;
+			} else if(img.height < area.width) {
+				minScale = (area.width / img.oldHeight)
+			}
+			if(minScale !== 1) {
+				scaleImage({
+					instance: this.getInstance(),
+					scale: minScale - scale,
+					x: sys.windowWidth / 2,
+					y: (sys.windowHeight - sys.offsetBottom) / 2
+				});
+			}
+			
+			// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
+			// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
+			// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
+			var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
+			var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
+			changeImageRect({
+				instance: this.getInstance(),
+				check: true,
+				x: -ox - oy,
+				y: -oy + ox
+			});
+		}
+	}
+}

+ 688 - 0
src/components/image-cropper/image-cropper.vue

@@ -0,0 +1,688 @@
+<template>
+    <view class="image-cropper" @wheel="cropper.mousewheel">
+        <canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
+			width: `${canvansWidth}px`,
+			height: `${canvansHeight}px`
+		}"></canvas>
+        <canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
+			width: `${canvansWidth}px`,
+			height: `${canvansHeight}px`
+		}"></canvas>
+        <view class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
+            <image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
+            <view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
+            <view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
+            <view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
+                <view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
+            </view>
+            <block v-if="showGrid">
+                <view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
+            </block>
+            <block v-if="showAngle">
+                <view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
+                    <view :style="[{
+						width: `${angleSize}px`,
+						height: `${angleSize}px`
+					}]"></view>
+                </view>
+            </block>
+        </view>
+        <view class="fixed-bottom safe-area-inset-bottom">
+            <view v-if="rotatable && !!imgSrc" class="rotate-icon" @click="cropper.rotateImage"></view>
+            <view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
+            <block v-else-if="!!imgSrc">
+                <view class="rechoose" @click="chooseImage">重选</view>
+                <button class="button" size="mini" @click="cropClick">确定</button>
+            </block>
+            <view v-else class="choose-btn" @click="chooseImage">选择图片</view>
+        </view>
+    </view>
+</template>
+<!-- #ifdef APP-VUE || H5 -->
+<script module="cropper" lang="renderjs">
+import cropper from './image-cropper.js';
+export default {
+    mixins: [cropper]
+}
+</script>
+<!-- #endif -->
+<!-- #ifdef MP-WEIXIN || MP-QQ -->
+<script module="cropper" lang="wxs" src="./image-cropper.wxs"></script>
+<!-- #endif -->
+<script>
+/** 裁剪区域最大宽高所占屏幕宽度百分比 */
+const AREA_SIZE = 75;
+/** 图片默认宽高 */
+const IMG_SIZE = 300;
+
+export default {
+    name: "image-cropper",
+    // #ifdef MP-WEIXIN
+    options: {
+        // 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
+        styleIsolation: "isolated"
+    },
+    // #endif
+    props: {
+        /** 图片资源地址 */
+        src: {
+            type: String,
+            default: ''
+        },
+        /** 裁剪宽度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
+        width: {
+            type: Number,
+            default: IMG_SIZE
+        },
+        /** 裁剪高度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
+        height: {
+            type: Number,
+            default: IMG_SIZE
+        },
+        /** 是否绘制裁剪区域边框 */
+        showBorder: {
+            type: Boolean,
+            default: true
+        },
+        /** 是否绘制裁剪区域网格参考线 */
+        showGrid: {
+            type: Boolean,
+            default: true
+        },
+        /** 是否展示四个支持伸缩的角 */
+        showAngle: {
+            type: Boolean,
+            default: true
+        },
+        /** 裁剪区域最小缩放倍数 */
+        areaScale: {
+            type: Number,
+            default: 0.3
+        },
+        /** 图片最大缩放倍数 */
+        maxScale: {
+            type: Number,
+            default: 5
+        },
+        /** 是否有回弹效果:拖动时可以拖出边界,释放时会弹回边界 */
+        bounce: {
+            type: Boolean,
+            default: true
+        },
+        /** 是否支持翻转 */
+        rotatable: {
+            type: Boolean,
+            default: true
+        },
+        /** 是否支持从本地选择素材 */
+        choosable: {
+            type: Boolean,
+            default: true
+        },
+        /** 四个角尺寸,单位px */
+        angleSize: {
+            type: Number,
+            default: 20
+        },
+        /** 四个角边框宽度,单位px */
+        angleBorderWidth: {
+            type: Number,
+            default: 2
+        },
+        /** 裁剪图片圆角半径,单位px */
+        radius: {
+            type: Number,
+            default: 0
+        },
+        /** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
+        fileType: {
+            type: String,
+            default: 'png'
+        },
+        /**
+         * 图片从绘制到生成所需时间,单位ms
+         * 微信小程序平台使用 `Canvas 2D` 绘制时有效
+         * 如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间
+         */
+        delay: {
+            type: Number,
+            default: 1000
+        },
+        // #ifdef H5
+        /** 
+         * 页面是否是原生标题栏
+         * H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。
+         * 注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的
+         */
+        navigation: {
+            type: Boolean,
+            default: true
+        }
+        // #endif
+    },
+    emits: ["crop"],
+    data() {
+        return {
+            // 用不同 id 使 v-for key 不重复
+            maskList: [
+                { id: 'crop-mask-block-1' },
+                { id: 'crop-mask-block-2' },
+                { id: 'crop-mask-block-3' },
+                { id: 'crop-mask-block-4' },
+            ],
+            gridList: [
+                { id: 'crop-grid-1' },
+                { id: 'crop-grid-2' },
+                { id: 'crop-grid-3' },
+                { id: 'crop-grid-4' },
+            ],
+            angleList: [
+                { id: 'crop-angle-1' },
+                { id: 'crop-angle-2' },
+                { id: 'crop-angle-3' },
+                { id: 'crop-angle-4' },
+            ],
+            /** 本地缓存的图片路径 */
+            imgSrc: '',
+            /** 图片的裁剪宽度 */
+            imgWidth: IMG_SIZE,
+            /** 图片的裁剪高度 */
+            imgHeight: IMG_SIZE,
+            /** 裁剪区域最大宽度所占屏幕宽度百分比 */
+            widthPercent: AREA_SIZE,
+            /** 裁剪区域最大高度所占屏幕宽度百分比 */
+            heightPercent: AREA_SIZE,
+            /** 裁剪区域布局信息 */
+            area: {},
+            /** 未被缩放过的图片宽 */
+            oldWidth: 0,
+            /** 未被缩放过的图片高 */
+            oldHeight: 0,
+            /** 系统信息 */
+            sys: uni.getSystemInfoSync(),
+            scaleWidth: 0,
+            scaleHeight: 0,
+            rotate: 0,
+            offsetX: 0,
+            offsetY: 0,
+            use2d: false,
+            canvansWidth: 0,
+            canvansHeight: 0,
+            // imageStyles: {},
+            // maskStylesList: [{}, {}, {}, {}],
+            // borderStyles: {},
+            // gridStylesList: [{}, {}, {}, {}],
+            // angleStylesList: [{}, {}, {}, {}],
+            // circleBoxStyles: {},
+            // circleStyles: {},
+        }
+    },
+    computed: {
+        initData() {
+            return {
+                timestamp: new Date().getTime(),
+                area: {
+                    ...this.area,
+                    bounce: this.bounce,
+                    showBorder: this.showBorder,
+                    showGrid: this.showGrid,
+                    showAngle: this.showAngle,
+                    angleSize: this.angleSize,
+                    angleBorderWidth: this.angleBorderWidth,
+                    minScale: this.areaScale,
+                    widthPercent: this.widthPercent,
+                    heightPercent: this.heightPercent,
+                    radius: this.radius
+                },
+                sys: this.sys,
+                img: {
+                    maxScale: this.maxScale,
+                    src: this.imgSrc,
+                    width: this.oldWidth,
+                    height: this.oldHeight,
+                    oldWidth: this.oldWidth,
+                    oldHeight: this.oldHeight,
+                }
+            }
+        },
+        imgProps() {
+            return {
+                width: this.width,
+                height: this.height,
+                src: this.src,
+            }
+        }
+    },
+    watch: {
+        imgProps: {
+            handler(val) {
+                // 自定义裁剪尺,示例如下:
+                this.imgWidth = Number(val.width) || IMG_SIZE;
+                this.imgHeight = Number(val.height) || IMG_SIZE;
+                let use2d = true;
+                // #ifndef MP-WEIXIN
+                use2d = false;
+                // #endif
+                // if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
+                // 	use2d = false;
+                // }
+                let canvansWidth = this.imgWidth;
+                let canvansHeight = this.imgHeight;
+                let size = Math.max(canvansWidth, canvansHeight)
+                let scalc = 1;
+                if (size > 1365) {
+                    scalc = 1365 / size;
+                }
+                this.canvansWidth = canvansWidth * scalc;
+                this.canvansHeight = canvansHeight * scalc;
+                this.use2d = use2d;
+                this.initArea();
+                val.src && this.initImage(val.src);
+            },
+            immediate: true
+        },
+    },
+    methods: {
+        /** 提供给wxs调用,用来接收图片变更数据 */
+        dataChange(e) {
+            // console.log('dataChange', e)
+            this.scaleWidth = e.width;
+            this.scaleHeight = e.height;
+            this.rotate = e.rotate;
+            this.offsetX = e.x;
+            this.offsetY = e.y;
+        },
+        /** 初始化裁剪区域布局信息 */
+        initArea() {
+            // 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
+            this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
+            // #ifndef H5
+            this.sys.windowTop = 0;
+            this.sys.navigation = true;
+            // #endif
+            // #ifdef H5
+            // h5平台的窗口高度是包含标题栏的
+            this.sys.windowTop = this.sys.windowTop || 44;
+            this.sys.navigation = this.navigation;
+            // #endif
+            let wp = this.widthPercent;
+            let hp = this.heightPercent;
+            if (this.imgWidth > this.imgHeight) {
+                hp = hp * this.imgHeight / this.imgWidth;
+            } else if (this.imgWidth < this.imgHeight) {
+                wp = wp * this.imgWidth / this.imgHeight;
+            }
+            const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
+            const width = size * wp / 100;
+            const height = size * hp / 100;
+            const left = (this.sys.windowWidth - width) / 2;
+            const right = left + width;
+            const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
+            const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
+            this.area = { width, height, left, right, top, bottom };
+            this.scaleWidth = width;
+            this.scaleHeight = height;
+        },
+        /** 从本地选取图片 */
+        chooseImage() {
+            // #ifdef MP-WEIXIN || MP-JD
+            if (uni.chooseMedia) {
+                uni.chooseMedia({
+                    count: 1,
+                    mediaType: ['image'],
+                    success: (res) => {
+                        this.resetData();
+                        this.initImage(res.tempFiles[0].tempFilePath);
+                    }
+                });
+                return;
+            }
+            // #endif
+            uni.chooseImage({
+                count: 1,
+                success: (res) => {
+                    this.resetData();
+                    this.initImage(res.tempFiles[0].path);
+                }
+            });
+        },
+        /** 重置数据 */
+        resetData() {
+            this.imgSrc = '';
+            this.rotate = 0;
+            this.offsetX = 0;
+            this.offsetY = 0;
+            this.initArea();
+        },
+        /**
+         * 初始化图片信息
+         * @param {String} url 图片链接
+         */
+        initImage(url) {
+            uni.getImageInfo({
+                src: url,
+                success: (res) => {
+                    this.imgSrc = res.path;
+                    let scale = res.width / res.height;
+                    let areaScale = this.area.width / this.area.height;
+                    if (scale > 1) { // 横向图片
+                        if (scale >= areaScale) { // 图片宽不小于目标宽,则高固定,宽自适应
+                            this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
+                        } else { // 否则宽固定、高自适应
+                            this.scaleHeight = res.height * this.scaleWidth / res.width;
+                        }
+                    } else { // 纵向图片
+                        if (scale <= areaScale) { // 图片高不小于目标高,宽固定,高自适应
+                            this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
+                        } else { // 否则高固定,宽自适应
+                            this.scaleWidth = res.width * this.scaleHeight / res.height;
+                        }
+                    }
+                    // 记录原始宽高,为缩放比列做限制
+                    this.oldWidth = this.scaleWidth;
+                    this.oldHeight = this.scaleHeight;
+                },
+                fail: (err) => {
+                    console.error(err)
+                }
+            });
+        },
+        /**
+         * 剪切图片圆角
+         * @param {Object} ctx canvas 的绘图上下文对象
+         * @param {Number} radius 圆角半径
+         * @param {Number} scale 生成图片的实际尺寸与截取区域比
+         * @param {Function} drawImage 执行剪切时所调用的绘图方法,入参为是否执行了剪切
+         */
+        drawClipImage(ctx, radius, scale, drawImage) {
+            if (radius > 0) {
+                ctx.save();
+                ctx.beginPath();
+                const w = this.canvansWidth;
+                const h = this.canvansHeight;
+                if (w === h && radius >= w / 2) { // 圆形
+                    ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
+                } else { // 圆角矩形
+                    if (w !== h) { // 限制圆角半径不能超过短边的一半
+                        radius = Math.min(w / 2, h / 2, radius);
+                        // radius = Math.min(Math.max(w, h) / 2, radius);
+                    }
+                    ctx.moveTo(radius, 0);
+                    ctx.arcTo(w, 0, w, h, radius);
+                    ctx.arcTo(w, h, 0, h, radius);
+                    ctx.arcTo(0, h, 0, 0, radius);
+                    ctx.arcTo(0, 0, w, 0, radius);
+                    ctx.closePath();
+                }
+                ctx.clip();
+                drawImage && drawImage(true);
+                ctx.restore();
+            } else {
+                drawImage && drawImage(false);
+            }
+        },
+        /**
+         * 旋转图片
+         * @param {Object} ctx canvas 的绘图上下文对象
+         * @param {Number} rotate 旋转角度
+         * @param {Number} scale 生成图片的实际尺寸与截取区域比
+         */
+        drawRotateImage(ctx, rotate, scale) {
+            if (rotate !== 0) {
+                // 1. 以图片中心点为旋转中心点
+                const x = this.scaleWidth * scale / 2;
+                const y = this.scaleHeight * scale / 2;
+                ctx.translate(x, y);
+                // 2. 旋转画布
+                ctx.rotate(rotate * Math.PI / 180);
+                // 3. 旋转完画布后恢复设置旋转中心时所做的偏移
+                ctx.translate(-x, -y);
+            }
+        },
+        drawImage(ctx, image, callback) {
+            // 生成图片的实际尺寸与截取区域比
+            const scale = this.canvansWidth / this.area.width;
+            this.drawClipImage(ctx, this.radius, scale, () => {
+                this.drawRotateImage(ctx, this.rotate, scale);
+                const r = this.rotate / 90;
+                ctx.drawImage(
+                    image,
+                    [
+                        (this.offsetX - this.area.left),
+                        (this.offsetY - this.area.top),
+                        -(this.offsetX - this.area.left),
+                        -(this.offsetY - this.area.top)
+                    ][r] * scale,
+                    [
+                        (this.offsetY - this.area.top),
+                        -(this.offsetX - this.area.left),
+                        -(this.offsetY - this.area.top),
+                        (this.offsetX - this.area.left)
+                    ][r] * scale,
+                    this.scaleWidth * scale,
+                    this.scaleHeight * scale
+                );
+            });
+        },
+        /**
+         * 绘图
+         * @param {Object} canvas 
+         * @param {Object} ctx canvas 的绘图上下文对象
+         * @param {String} src 图片路径
+         * @param {Function} callback 开始绘制时回调
+         */
+        draw2DImage(canvas, ctx, src, callback) {
+            // console.log('draw2DImage', canvas, ctx, src, callback)
+            if (canvas) {
+                const image = canvas.createImage();
+                image.onload = () => {
+                    this.drawImage(ctx, image);
+                    // 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
+                    callback && setTimeout(callback, this.delay);
+                };
+                image.onerror = (err) => {
+                    console.error(err)
+                    uni.hideLoading();
+                };
+                image.src = src;
+            } else {
+                this.drawImage(ctx, src);
+                setTimeout(() => {
+                    ctx.draw(false, callback);
+                }, 200);
+            }
+        },
+        /**
+         * 画布转图片到本地缓存
+         * @param {Object} canvas 
+         * @param {String} canvasId 
+         */
+        canvasToTempFilePath(canvas, canvasId) {
+            // console.log('canvasToTempFilePath', canvas, canvasId)
+            uni.canvasToTempFilePath({
+                canvas,
+                canvasId,
+                x: 0,
+                y: 0,
+                width: this.canvansWidth,
+                height: this.canvansHeight,
+                destWidth: this.imgWidth, // 必要,保证生成图片宽度不受设备分辨率影响
+                destHeight: this.imgHeight, // 必要,保证生成图片高度不受设备分辨率影响
+                fileType: this.fileType, // 目标文件的类型,默认png
+                success: (res) => {
+                    // 生成的图片临时文件路径
+                    this.handleImage(res.tempFilePath);
+                },
+                fail: (err) => {
+                    uni.hideLoading();
+                    uni.showToast({ title: '裁剪失败,生成图片异常!', icon: 'none' });
+                }
+            }, this);
+        },
+        /** 确认裁剪 */
+        cropClick() {
+            uni.showLoading({ title: '裁剪中...', mask: true });
+            if (!this.use2d) {
+                const ctx = uni.createCanvasContext('imgCanvas', this);
+                ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
+                this.draw2DImage(null, ctx, this.imgSrc, () => {
+                    this.canvasToTempFilePath(null, 'imgCanvas');
+                });
+                return;
+            }
+            // #ifdef MP-WEIXIN
+            const query = uni.createSelectorQuery().in(this);
+            query.select('#imgCanvas')
+                .fields({ node: true, size: true })
+                .exec((res) => {
+                    const canvas = res[0].node;
+
+                    const dpr = uni.getSystemInfoSync().pixelRatio;
+                    canvas.width = res[0].width * dpr;
+                    canvas.height = res[0].height * dpr;
+                    const ctx = canvas.getContext('2d');
+                    ctx.scale(dpr, dpr);
+                    ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
+
+                    this.draw2DImage(canvas, ctx, this.imgSrc, () => {
+                        this.canvasToTempFilePath(canvas);
+                    });
+                });
+            // #endif
+        },
+        handleImage(tempFilePath) {
+            // 在H5平台下,tempFilePath 为 base64
+            // console.log(tempFilePath)
+            uni.hideLoading();
+            this.$emit('crop', { tempFilePath });
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+.image-cropper {
+    position: fixed;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    background-color: #000;
+
+    .img-canvas {
+        position: absolute !important;
+        transform: translateX(-100%);
+    }
+
+    .pic-preview {
+        width: 100%;
+        flex: 1;
+        position: relative;
+
+        .crop-mask-block {
+            background-color: rgba(51, 51, 51, 0.8);
+            z-index: 2;
+            position: fixed;
+            box-sizing: border-box;
+            pointer-events: none;
+        }
+
+        .crop-circle-box {
+            position: fixed;
+            box-sizing: border-box;
+            z-index: 2;
+            pointer-events: none;
+            overflow: hidden;
+
+            .crop-circle {
+                width: 100%;
+                height: 100%;
+            }
+        }
+
+        .crop-image {
+            padding: 0 !important;
+            margin: 0 !important;
+            border-radius: 0 !important;
+            display: block !important;
+        }
+
+        .crop-border {
+            position: fixed;
+            border: 1px solid #fff;
+            box-sizing: border-box;
+            z-index: 3;
+            pointer-events: none;
+        }
+
+        .crop-grid {
+            position: fixed;
+            z-index: 3;
+            border-style: dashed;
+            border-color: #fff;
+            pointer-events: none;
+            opacity: 0.5;
+        }
+
+        .crop-angle {
+            position: fixed;
+            z-index: 3;
+            border-style: solid;
+            border-color: #fff;
+            pointer-events: none;
+        }
+    }
+
+    .fixed-bottom {
+        position: fixed;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        z-index: 99;
+        display: flex;
+        flex-direction: row;
+        background-color: $uni-bg-color-grey;
+
+        .rotate-icon {
+            background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=');
+            background-size: 60% 60%;
+            background-repeat: no-repeat;
+            background-position: center;
+            width: 80rpx;
+            height: 80rpx;
+            position: absolute;
+            top: -90rpx;
+            left: 10rpx;
+            transform: rotateY(180deg);
+        }
+
+        .rechoose {
+            color: $uni-color-primary;
+            padding: 0 $uni-spacing-row-lg;
+            line-height: 100rpx;
+        }
+
+        .choose-btn {
+            color: $uni-color-primary;
+            text-align: center;
+            line-height: 100rpx;
+            flex: 1;
+        }
+
+        .button {
+            margin: auto $uni-spacing-row-lg auto auto;
+            background-color: $uni-color-primary;
+            color: #fff;
+        }
+    }
+
+    .safe-area-inset-bottom {
+        padding-bottom: 0;
+        padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
+        padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
+    }
+
+}
+</style>

+ 543 - 0
src/components/image-cropper/image-cropper.wxs

@@ -0,0 +1,543 @@
+/**
+ * 图片编辑器-手势监听
+ * 1. wxs 暂不支持 es6 语法
+ * 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上(uni-app 2.2.5及以上版本)
+ */
+/** 图片偏移量 */
+var offset = { x: 0, y: 0 };
+/** 图片缩放比例 */
+var scale = 1;
+/** 图片最小缩放比例 */
+var minScale = 1;
+/** 图片旋转角度 */
+var rotate = 0;
+/** 触摸点 */
+var touches = [];
+/** 图片布局信息 */
+var img = {};
+/** 系统信息 */
+var sys = {};
+/** 裁剪区域布局信息 */
+var area = {};
+/** 触摸行为类型 */
+var touchType = '';
+/** 操作角的位置 */
+var activeAngle = 0;
+/** 裁剪区域布局信息偏移量 */
+var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
+/**
+ * 计算两点间距
+ * @param {Object} touches 触摸点信息
+ */
+function getDistanceByTouches(touches) {
+	// 根据勾股定理求两点间距离
+	var a = touches[1].pageX - touches[0].pageX;
+	var b = touches[1].pageY - touches[0].pageY;
+	var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
+	// 求两点间的中点坐标
+	// 1. a、b可能为负值
+	// 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
+	// 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
+	var x = touches[1].pageX - a / 2;
+	var y = touches[1].pageY - b / 2;
+	return { c, x, y };
+};
+/**
+ * 检查边界:限制 x、y 拖动范围,禁止滑出边界
+ * @param {Object} e 点坐标
+ */
+function checkRange(e) {
+	var r = rotate / 90 % 2;
+	if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
+		var o = (img.height - img.width) / 2; // 宽高差值一半
+		return {
+			x: Math.min(Math.max(e.x, -img.height + o + area.width + area.left), area.left + o),
+			y: Math.min(Math.max(e.y, -img.width - o + area.height + area.top), area.top - o)
+		}
+	}
+	return {
+		x: Math.min(Math.max(e.x, -img.width + area.width + area.left), area.left),
+		y: Math.min(Math.max(e.y, -img.height + area.height + area.top), area.top)
+	}
+};
+/**
+ * 变更图片布局信息
+ * @param {Object} e 布局信息
+ */
+function changeImageRect(e) {
+	offset.x += e.x || 0;
+	offset.y += e.y || 0;
+	var image = e.instance.selectComponent('.crop-image');
+	if(e.check) { // 检查边界
+		var point = checkRange(offset);
+		if(offset.x !== point.x || offset.y !== point.y) {
+			offset = point;
+		}
+	}
+	// image.setStyle({
+	// 	width: img.width + 'px',
+	// 	height: img.height + 'px',
+	// 	transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
+	// });
+	var ox = (img.width - img.oldWidth) / 2;
+	var oy = (img.height - img.oldHeight) / 2;
+	image.setStyle({
+		width: img.oldWidth + 'px',
+		height: img.oldHeight + 'px',
+		transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
+	});
+	
+	e.instance.callMethod('dataChange', {
+		width: img.width,
+		height: img.height,
+		x: offset.x,
+		y: offset.y,
+		rotate: rotate
+	});
+};
+/**
+ * 变更裁剪区域布局信息
+ * @param {Object} e 布局信息
+ */
+function changeAreaRect(e) {
+	// 变更蒙版样式
+	var masks = e.instance.selectAllComponents('.crop-mask-block');
+	var maskStyles = [
+		{
+			left: 0,
+			width: (area.left + areaOffset.left) + 'px',
+			top: 0,
+			bottom: 0,
+		},
+		{
+			left: (area.right + areaOffset.right) + 'px',
+			right: 0,
+			top: 0,
+			bottom: 0,
+		},
+		{
+			left: (area.left + areaOffset.left) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			top: 0,
+			height: (area.top + areaOffset.top) + 'px',
+		},
+		{
+			left: (area.left + areaOffset.left) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			top: (area.bottom + areaOffset.bottom) + 'px',
+			// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
+			bottom: 0,
+		}
+	];
+	var len = masks.length;
+	for (var i = 0; i < len; i++) {
+		masks[i].setStyle(maskStyles[i]);
+	}
+	
+	// 变更边框样式
+	if(area.showBorder) {
+		var border = e.instance.selectComponent('.crop-border');
+		border.setStyle({
+			left: (area.left + areaOffset.left) + 'px',
+			top: (area.top + areaOffset.top) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
+		});
+	}
+	
+	// 变更参考线样式
+	if(area.showGrid) {
+		var grids = e.instance.selectAllComponents('.crop-grid');
+		var gridStyles = [
+			{
+				'border-width': '1px 0 0 0',
+				left: (area.left + areaOffset.left) + 'px',
+				right: (area.right + areaOffset.right) + 'px',
+				top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
+				width: (area.width + areaOffset.right - areaOffset.left) + 'px'
+			},
+			{
+				'border-width': '1px 0 0 0',
+				left: (area.left + areaOffset.left) + 'px',
+				right: (area.right + areaOffset.right) + 'px',
+				top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
+				width: (area.width + areaOffset.right - areaOffset.left) + 'px'
+			},
+			{
+				'border-width': '0 1px 0 0',
+				top: (area.top + areaOffset.top) + 'px',
+				bottom: (area.bottom + areaOffset.bottom) + 'px',
+				left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
+				height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
+			},
+			{
+				'border-width': '0 1px 0 0',
+				top: (area.top + areaOffset.top) + 'px',
+				bottom: (area.bottom + areaOffset.bottom) + 'px',
+				left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
+				height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
+			}
+		];
+		var len = grids.length;
+		for (var i = 0; i < len; i++) {
+			grids[i].setStyle(gridStyles[i]);
+		}
+	}
+	
+	// 变更四个伸缩角样式
+	if(area.showAngle) {
+		var angles = e.instance.selectAllComponents('.crop-angle');
+		var angleStyles = [
+			{
+				'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
+				left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
+				top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
+			},
+			{
+				'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
+				left: (area.right + areaOffset.right - area.angleSize) + 'px',
+				top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
+			},
+			{
+				'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
+				left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
+				top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
+			},
+			{
+				'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
+				left: (area.right + areaOffset.right - area.angleSize) + 'px',
+				top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
+			}
+		];
+		var len = angles.length;
+		for (var i = 0; i < len; i++) {
+			angles[i].setStyle(angleStyles[i]);
+		}
+	}
+	
+	// 变更圆角样式
+	if(area.radius > 0) {
+		var circleBox = e.instance.selectComponent('.crop-circle-box');
+		var circle = e.instance.selectComponent('.crop-circle');
+		var radius = area.radius;
+		if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
+			radius = (area.width / 2);
+		} else { // 圆角矩形
+			if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
+				radius = Math.min(area.width / 2, area.height / 2, radius);
+			}
+		}
+		circleBox.setStyle({
+			left: (area.left + areaOffset.left) + 'px',
+			top: (area.top + areaOffset.top) + 'px',
+			width: (area.width + areaOffset.right - areaOffset.left) + 'px',
+			height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
+		});
+		circle.setStyle({
+			'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
+			'border-radius': radius + 'px'
+		});
+	}
+};
+/**
+ * 缩放图片
+ * @param {Object} e 布局信息
+ */
+function scaleImage(e) {
+	var last = scale;
+	scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
+	if(last !== scale) {
+		img.width = img.oldWidth * scale;
+		img.height = img.oldHeight * scale;
+		// 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
+		// 			该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
+		// 			新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
+		// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
+		// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
+		e.x = (e.x - offset.x) * (1 - scale / last);
+		e.y = (e.y - offset.y) * (1 - scale / last);
+		changeImageRect(e);
+		return true;
+	}
+	return false;
+};
+/**
+ * 获取触摸点在哪个角
+ * @param {number} x 触摸点x轴坐标
+ * @param {number} y 触摸点y轴坐标
+ * @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
+ */
+function getToucheAngle(x, y) {
+	// console.log('getToucheAngle', x, y, JSON.stringify(area))
+	var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
+	if(y >= area.top - o && y <= area.top + area.angleSize + o) {
+		if(x >= area.left - o && x <= area.left + area.angleSize + o) {
+			return 1; // 左上角
+		} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
+			return 2; // 右上角
+		}
+	} else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
+		if(x >= area.left - o && x <= area.left + area.angleSize + o) {
+			return 3; // 左下角
+		} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
+			return 4; // 右下角
+		}
+	}
+	return 0; // 无触摸到角
+};
+/**
+ * 重置数据
+ */
+function resetData() {
+	offset = { x: 0, y: 0 };
+	scale = 1;
+	minScale = 1;
+	rotate = 0;
+};
+module.exports = {
+	/**
+	 * 初始化:观察数据变更
+	 * @param {Object} newVal 新数据
+	 * @param {Object} oldVal 旧数据
+	 * @param {Object} o 组件实例对象
+	 */
+	initObserver: function(newVal, oldVal, o, i) {
+		if(newVal) {
+			img = newVal.img;
+			sys = newVal.sys;
+			area = newVal.area;
+			resetData();
+			img.src && changeImageRect({
+				instance: o,
+				x: (sys.windowWidth - img.width) / 2,
+				y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
+			});
+			changeAreaRect({
+				instance: o
+			});
+			// console.log('initRect', JSON.stringify(newVal))
+		}
+	},
+	/**
+	 * 鼠标滚轮滚动
+	 * @param {Object} e 事件对象
+	 * @param {Object} o 组件实例对象
+	 */
+	mousewheel: function(e, o) {
+		if(!img.src) return;
+		scaleImage({
+			instance: o,
+			check: true,
+			// 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
+			scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
+			x: e.touches[0].pageX,
+			y: e.touches[0].pageY
+		});
+	},
+	/**
+	 * 触摸开始
+	 * @param {Object} e 事件对象
+	 * @param {Object} o 组件实例对象
+	 */
+	touchstart: function(e, o) {
+		if(!img.src) return;
+		touches = e.touches;
+		activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
+		if(touches.length === 1 && activeAngle !== 0) {
+			touchType = 'stretch'; // 伸缩裁剪区域
+		} else {
+			touchType = '';
+		}
+		// console.log('touchstart', JSON.stringify(e), activeAngle)
+	},
+	/**
+	 * 触摸移动
+	 * @param {Object} e 事件对象
+	 * @param {Object} o 组件实例对象
+	 */
+	touchmove: function(e, o) {
+		if(!img.src) return;
+		// console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
+		if(touchType === 'stretch') { // 触摸四个角进行拉伸
+			var point = e.touches[0];
+			var start = touches[0];
+			var x = point.pageX - start.pageX;
+			var y = point.pageY - start.pageY;
+			if(x !== 0 || y !== 0) {
+				var maxX = area.width * (1 - area.minScale);
+				var maxY = area.height * (1 - area.minScale);
+				// console.log(x, y, maxX, maxY)
+				touches[0] = point;
+				switch(activeAngle) {
+					case 1: // 左上角
+						x += areaOffset.left;
+						y += areaOffset.top;
+						if(x >= 0 && y >= 0) { // 有效滑动
+							if(x > y) { // 以x轴滑动距离为缩放基准
+								if(x > maxX) x = maxX;
+								y = x * area.height / area.width;
+							} else { // 以y轴滑动距离为缩放基准
+								if(y > maxY) y = maxY;
+								x = y * area.width / area.height;
+							}
+							areaOffset.left = x;
+							areaOffset.top = y;
+						}
+						break;
+					case 2: // 右上角
+						x += areaOffset.right;
+						y += areaOffset.top;
+						if(x <= 0 && y >= 0) { // 有效滑动
+							if(-x > y) { // 以x轴滑动距离为缩放基准
+								if(-x > maxX) x = -maxX;
+								y = -x * area.height / area.width;
+							} else { // 以y轴滑动距离为缩放基准
+								if(y > maxY) y = maxY;
+								x = -y * area.width / area.height;
+							}
+							areaOffset.right = x;
+							areaOffset.top = y;
+						}
+						break;
+					case 3: // 左下角
+						x += areaOffset.left;
+						y += areaOffset.bottom;
+						if(x >= 0 && y <= 0) { // 有效滑动
+							if(x > -y) { // 以x轴滑动距离为缩放基准
+								if(x > maxX) x = maxX;
+								y = -x * area.height / area.width;
+							} else { // 以y轴滑动距离为缩放基准
+								if(-y > maxY) y = -maxY;
+								x = -y * area.width / area.height;
+							}
+							areaOffset.left = x;
+							areaOffset.bottom = y;
+						}
+						break;
+					case 4: // 右下角
+						x += areaOffset.right;
+						y += areaOffset.bottom;
+						if(x <= 0 && y <= 0) { // 有效滑动
+							if(-x > -y) { // 以x轴滑动距离为缩放基准
+								if(-x > maxX) x = -maxX;
+								y = x * area.height / area.width;
+							} else { // 以y轴滑动距离为缩放基准
+								if(-y > maxY) y = -maxY;
+								x = y * area.width / area.height;
+							}
+							areaOffset.right = x;
+							areaOffset.bottom = y;
+						}
+						break;
+				}
+				// console.log(x, y, JSON.stringify(areaOffset))
+				changeAreaRect({
+					instance: o,
+				});
+				// this.draw();
+			}
+		} else if (e.touches.length == 2) { // 双点触摸缩放
+			var start = getDistanceByTouches(touches);
+			var end = getDistanceByTouches(e.touches);
+			scaleImage({
+				instance: o,
+				check: !area.bounce,
+				scale: (end.c - start.c) / 100,
+				x: end.x,
+				y: end.y
+			});
+			touchType = 'scale';
+		} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
+			touchType = 'move';
+		} else {
+			changeImageRect({
+				instance: o,
+				check: !area.bounce,
+				x: e.touches[0].pageX - touches[0].pageX,
+				y: e.touches[0].pageY - touches[0].pageY
+			});
+			touchType = 'move';
+		}
+		touches = e.touches;
+	},
+	/**
+	 * 触摸结束
+	 * @param {Object} e 事件对象
+	 * @param {Object} o 组件实例对象
+	 */
+	touchend: function(e, o) {
+		if(!img.src) return;
+		if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
+			// 裁剪区域宽度被缩放到多少
+			var left = areaOffset.left;
+			var right = areaOffset.right;
+			var top = areaOffset.top;
+			var bottom = areaOffset.bottom;
+			var w = area.width + right - left;
+			var h = area.height + bottom - top;
+			// 图像放大倍数
+			var p = scale * (area.width / w) - scale;
+			// 复原裁剪区域
+			areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
+			changeAreaRect({
+				instance: o,
+			});
+			scaleImage({
+				instance: o,
+				scale: p,
+				x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
+				y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
+			});
+		} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
+			changeImageRect({
+				instance: o,
+				check: true
+			});
+		}
+	},
+	/**
+	 * 顺时针翻转图片90°
+	 * @param {Object} e 事件对象
+	 * @param {Object} o 组件实例对象
+	 */
+	rotateImage: function(e, o) {
+		rotate = (rotate + 90) % 360;
+		
+		// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
+		var r = rotate / 90 % 2;
+		minScale = 1;
+		if(img.width < area.height) {
+			minScale = area.height / img.oldWidth;
+		} else if(img.height < area.width) {
+			minScale = (area.width / img.oldHeight)
+		}
+		if(minScale !== 1) {
+			scaleImage({
+				instance: o,
+				scale: minScale - scale,
+				x: sys.windowWidth / 2,
+				y: (sys.windowHeight - sys.offsetBottom) / 2
+			});
+		}
+		
+		// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
+		// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
+		// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
+		var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
+		var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
+		changeImageRect({
+			instance: o,
+			check: true,
+			x: -ox - oy,
+			y: -oy + ox
+		});
+	},
+	// 此处只用于对齐其他平台端的样式参数,防止异常,无作用
+	imageStyles: {},
+	maskStylesList: [{}, {}, {}, {}],
+	borderStyles: {},
+	gridStylesList: [{}, {}, {}, {}],
+	angleStylesList: [{}, {}, {}, {}],
+	circleBoxStyles: {},
+	circleStyles: {},
+}

+ 83 - 0
src/components/image-cropper/readme.md

@@ -0,0 +1,83 @@
+# qf-image-cropper
+## 图片裁剪插件
+uniapp微信小程序图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
+
+### 平台支持:
+1. 支持微信小程序:移动端、PC端、开发者工具
+2. 支持H5平台(2.1.0版本起)
+3. 支持APP平台(2.1.5版本起):Android、IOS
+4. 其他平台暂未测试兼容性未知
+
+### 支持功能:
+1. 自定义裁剪尺寸
+2. 定点等比例缩放:移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
+3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
+4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
+5. 裁剪生成新图片
+6. 本地选择图片
+7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
+8. 裁剪圆角图片:圆形、圆角矩形
+
+### 属性说明
+| 属性名 | 类型 | 默认值 | 说明 |
+|:---|:---|:---|:---|
+| src              | String        |         | 图片资源地址 |
+| width            | Number        | 300     | 裁剪宽度 |
+| height           | Number        | 300     | 裁剪高度 |
+| showBorder       | Boolean       | true    | 是否绘制裁剪区域边框 |
+| showGrid         | Boolean       | true    | 是否绘制裁剪区域网格参考线 |
+| showAngle        | Boolean       | true    | 是否展示四个支持伸缩的角 |
+| areaScale        | Number        | 0.3     | 裁剪区域最小缩放倍数 |
+| maxScale         | Number        | 5       | 图片最大缩放倍数 |
+| bounce           | Boolean       | true    | 是否有回弹效果:拖动时可以拖出边界,释放时会弹回边界 |
+| rotatable        | Boolean       | true    | 是否支持翻转 |
+| choosable        | Boolean       | true    | 是否支持从本地选择素材 |
+| angleSize        | Number        | 20      | 四个角尺寸,单位px |
+| angleBorderWidth | Number        | 2       | 四个角边框宽度,单位px |
+| radius           | Number        |         | 裁剪图片圆角半径,单位px |
+| fileType         | String        | png     | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
+| delay            | Number        | 1000    | 图片从绘制到生成所需时间,单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
+| navigation       | Boolean       | true    | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的 |
+| @crop    	       | EventHandle   |         | 剪裁完成后触发,event = { tempFilePath }。在H5平台下,tempFilePath 为 base64 |
+
+### 基本用法
+```
+<template>
+	<div>
+		<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
+	</div>
+</template>
+
+<script>
+	import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
+	export default {
+		components: {
+			QfImageCropper
+		},
+		methods: {
+			handleCrop(e) {
+				uni.previewImage({
+					urls: [e.tempFilePath],
+					current: 0
+				});
+			}
+ 		}
+	}
+</script>
+```
+### 使用说明
+1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
+```
+{
+	"enablePullDownRefresh": false,
+	"disableScroll": true
+}
+```
+2.建议使用本插件不要设置过大宽高的目标图片尺寸,建议1365x1365以内,否则可能会导致如下问题:
+```
+1.界面卡顿,内存占用过高
+2.生成图片失真(模糊)
+3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
+```
+3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
+4.src属性设置网络图片时,图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。

+ 0 - 1
src/main.js

@@ -35,7 +35,6 @@ Vue.prototype.$hosts = hosts;
 // 上传
 import { up } from "@/utils/up";
 Vue.prototype.$up = up;
-
 // 页面跳转
 import { goto } from '@/utils/myfun.js';
 Vue.prototype.goto = goto;

+ 10 - 2
src/pages.json

@@ -293,6 +293,14 @@
 				"style": {
 					"navigationBarTitleText": "添加收货地址"
 				}
+			},
+			{
+				"path": "image-cropper/index",
+				"style": {
+					"navigationBarTitleText": "个人头像",
+					"enablePullDownRefresh": false,
+					"disableScroll": true
+				}
 			}
 		 ]
 	}],
@@ -303,8 +311,8 @@
 	// 	}
 	// },
 	"tabBar": {
-		"color": "#FE2C15",
-		"selectedColor": "#FCB736",
+		"color": "#18bb88",
+		"selectedColor": "#999",
 		"borderStyle": "black",
 		"backgroundColor": "#ffffff",
 		"list": [

+ 2 - 14
src/pages/account/conversion.vue

@@ -50,7 +50,6 @@ export default {
       post("moneyChabao").then((res) => {
         this.fee = res.data.data.fee;
         this.tips = res.data.data.tips;
-        console.log(res.data);
       });
     },
     exchange() {
@@ -82,33 +81,22 @@ export default {
       let data = {
         number: this.give_num,
       };
-      post("my/moneyTurn", data).then((res) => {
+      post("my/moneyTurn", data).then( async (res) => {
         if (res.code === 0) {
           this.give_num = undefined;
-          this.getuserInfo();
+          this.userinfo = await uni.userfun();
           appEv.errTips(res.msg);
         } else {
           appEv.errTips(res.msg);
         }
       });
     },
-    getuserInfo() {
-      post("/user/userinfo").then((res) => {
-        if (res.code === 0) {
-          uni.setStorageSync("userinfo", res.data.data);
-          this.userinfo = res.data.data;
-        }
-      });
-    },
   },
   computed: {},
   watch: {},
 };
 </script>
 <style scoped lang='scss'>
-.conversion {
-}
-
 // 页面配置
 page {
   background: #f4f4f4;

+ 2 - 7
src/pages/account/giveAsPresent.vue

@@ -163,13 +163,8 @@ export default {
       });
     },
 
-    getuserInfo() {
-      post("/user/userinfo").then((res) => {
-        if (res.code === 0) {
-          uni.setStorageSync("userinfo", res.data.data);
-          this.userinfo = res.data.data;
-        }
-      });
+    async getuserInfo() {
+      this.userinfo = await uni.userfun();
     },
 
     OnpickerShow() {

+ 2 - 11
src/pages/cash/index.vue

@@ -102,11 +102,11 @@ export default {
         pay_code: this.imgs,
         w_type: 1,
       };
-      post("/user/withdraw", data).then((res) => {
+      post("/user/withdraw", data).then( async (res) => {
         uni.hideLoading();
         if (res.code === 0) {
           this.inputMoney = "";
-          this.getuserInfo();
+          this.userinfo = await uni.userfun();
         } else {
           appEv.errTips(res.msg || "");
         }
@@ -151,15 +151,6 @@ export default {
         url: "/pages/accountDetails/withdraw",
       });
     },
-
-    getuserInfo() {
-      post("/user/userinfo").then((res) => {
-        if (res.code === 0) {
-          uni.setStorageSync("userinfo", res.data.data);
-          this.userinfo = res.data.data;
-        }
-      });
-    },
   },
 };
 </script>

+ 236 - 266
src/pages/my-tea-list/index.vue

@@ -1,66 +1,72 @@
 <template>
-  <view class="container">
-    <!-- 顶部 -->
-    <view class="head">
-      <view class="head_info flex_r flex_ac flex_jb">
-        <view class="flex_grow flex_r flex_ac">
-          <image class="head_img" :src="higherInfo.head_pic" mode=""></image>
-          <view class="flex_c flex_grow">
-            <view class="userInfo flex_r flex_ac">
-              <view class="user_name">{{ higherInfo.nickname }}</view>
-              <view class="level flex_r flex_ac flex_jc">缘起</view>
+    <view class="container">
+        <!-- 顶部 -->
+        <view class="head">
+            <view class="head_info flex_r flex_ac flex_jb">
+                <view class="flex_grow flex_r flex_ac">
+                    <img class="head_img" :src="higherInfo.head_pic" />
+                    <view class="flex_c flex_grow">
+                        <view class="userInfo flex_r flex_ac">
+                            <view class="user_name">{{ higherInfo.nickname }}</view>
+                            <view class="level flex_r flex_ac flex_jc">缘起</view>
+                        </view>
+                        <view class="flex_r flex_ac mar_t10">
+                            <view class="account f_din">{{ higherInfo.mobile || "" }}</view>
+                            <view class="copyBalance" @tap="copyAccount(higherInfo.mobile)">复制</view>
+                        </view>
+                    </view>
+                </view>
+                <view class="head_option flex_r flex_ac">
+                    <!-- <image class="option_weixin" src="/static/img/weixin.png" mode="" @tap="copyText"></image> -->
+                    <!-- <view class="option_hr"></view> -->
+                    <image class="option_phone" src="/static/img/dianhua.png" mode="" @tap="dial(higherInfo.mobile)"></image>
+                </view>
             </view>
-            <view class="flex_r flex_ac mar_t16">
-              <view class="account">{{ higherInfo.mobile || "" }}</view>
-              <view class="copyBalance" @tap="copyAccount(higherInfo.mobile)">复制</view>
-            </view>
-          </view>
-        </view>
-        <view class="head_option flex_r flex_ac">
-          <!-- <image class="option_weixin" src="/static/img/weixin.png" mode="" @tap="copyText"></image> -->
-          <view class="option_hr"></view>
-          <image class="option_phone" src="/static/img/dianhua.png" mode="" @tap="dial(higherInfo.mobile)"></image>
-        </view>
-      </view>
-      <view class="head_time flex_r flex_ac">注册时间:{{ $day(higherInfo.reg_time * 1000).format('YYYY-MM-DD HH:mm:ss') }}</view>
-    </view>
-    <!-- 顶部-end -->
-    <!-- 茶友数量 -->
-    <view class="tea_con mar_t30">
-      <view class="tea_total flex_r flex_ac flex_jc">
-        <view class="total_con flex_c flex_ac flex_jc">
-          <view class="total_num">{{ myQuanTeaFriendNum || 0 }}<text>人</text></view>
-          <view class="total_text">社区总数</view>
+            <view class="head_time flex_r flex_ac">注册时间:{{ $day(higherInfo.reg_time * 1000).format('YYYY-MM-DD HH:mm:ss') }}</view>
         </view>
-        <div class="total_info">
-          本日新增:{{ myChayoyTotal.day }}<br />
-          本周新增:{{ myChayoyTotal.week }}<br />
-          本月新增:{{ myChayoyTotal.month }}<br />
-        </div>
-      </view>
-      <view class="tea_info flex_r flex_ac">
-        <view class="info_list flex_c flex_ac flex_jc borRight" @tap="getToTeaList">
-          <view class="list_num">{{ myTeaFriendNum || 0 }}<text>人</text></view>
-          <view class="list_text">推广用户</view>
-          <div class="untotal_info">
-            本日新增:{{ myChayoyDirect.day }}<br />
-            本周新增:{{ myChayoyDirect.week }}<br />
-            本月新增:{{ myChayoyDirect.month }}<br />
-          </div>
-        </view>
-        <view class="info_list flex_c flex_ac flex_jc">
-          <view class="list_num">{{ myZhuanTeaFriendNum || 0 }}<text>人</text></view>
-          <view class="list_text">转介绍用户</view>
-          <div class="untotal_info">
-            本日新增:{{ myChayoyIndirect.day }}<br />
-            本周新增:{{ myChayoyIndirect.week }}<br />
-            本月新增:{{ myChayoyIndirect.month }}<br />
-          </div>
+        <!-- 顶部-end -->
+        <!-- 茶友数量 -->
+        <view class="tea_con mar_t30">
+            <view class="tea_total flex_r flex_ac flex_jc">
+                <view class="total_con flex_c flex_ac flex_jc">
+                    <view class="total_num f_dinB">{{ myQuanTeaFriendNum || 0 }}<text>人</text></view>
+                    <view class="total_text">社区总数</view>
+                </view>
+                <div class="total_info f_dinB">
+                    本日新增:{{ myChayoyTotal.day }}<br />
+                    本周新增:{{ myChayoyTotal.week }}<br />
+                    本月新增:{{ myChayoyTotal.month }}<br />
+                </div>
+            </view>
+            <view class="tea_info flex_r flex_ac">
+                <view class="info_list flex_c flex_ac flex_jc borRight" @tap="getToTeaList">
+                    <view class="list_num f_dinB">{{ myTeaFriendNum || 0 }}<text>人</text></view>
+                    <view class="list_text">推广用户</view>
+                    <div class="untotal_info f_dinB">
+                        <span class="ti_l">今日新增:</span>
+                        <span class="ti_r">{{ myChayoyDirect.day }}</span>
+                        <span class="ti_l">本周新增:</span>
+                        <span class="ti_r">{{ myChayoyDirect.week }}</span>
+                        <span class="ti_l">本月新增:</span>
+                        <span class="ti_r">{{ myChayoyDirect.month }}</span>
+                    </div>
+                </view>
+                <view class="info_list flex_c flex_ac flex_jc">
+                    <view class="list_num f_dinB">{{ myZhuanTeaFriendNum || 0 }}<text>人</text></view>
+                    <view class="list_text">转介绍用户</view>
+                    <div class="untotal_info f_dinB">
+                        <span class="ti_l">今日新增:</span>
+                        <span class="ti_r">{{ myChayoyIndirect.day }}</span>
+                        <span class="ti_l">本周新增:</span>
+                        <span class="ti_r">{{ myChayoyIndirect.week }}</span>
+                        <span class="ti_l">本月新增:</span>
+                        <span class="ti_r">{{ myChayoyIndirect.month }}</span>
+                    </div>
+                </view>
+            </view>
         </view>
-      </view>
+        <!-- 茶友数量-end -->
     </view>
-    <!-- 茶友数量-end -->
-  </view>
 </template>
 <script>
 let app = getApp();
@@ -69,225 +75,181 @@ import uniCopy from "@/utils/copy";
 import popup from "@/components/uni-popup/uni-popup.vue";
 import { post } from "@/request/api.js";
 export default {
-  components: {
-    popup,
-  },
-  data() {
-    return {
-      higherInfo: "",
-      myTeaFriendNum: "",
-      myZhuanTeaFriendNum: "",
-      myQuanTeaFriendNum: "",
-      wxcode: "",
-      mobile: "",
-
-      myChayoyTotal: {},
-      myChayoyDirect: {},
-      myChayoyIndirect: {},
-    };
-  },
-  onLoad: function() {
-    this.loadData();
-
-    this.getmyChayoyTotal()
-    this.getmyChayoyDirect()
-    this.getmyChayoyIndirect()
-  },
-  methods: {
-    loadData() {
-      post("/my/chayou").then((res) => {
-        if (res.code === 0) {
-          this.higherInfo = res.data.data.superior;
-          this.myTeaFriendNum = res.data.data.below;
-          this.myZhuanTeaFriendNum = res.data.data.lower_level;
-          this.myQuanTeaFriendNum =
-            res.data.data.below + res.data.data.lower_level;
-        }
-      });
+    components: {
+        popup,
     },
-
-    getmyChayoyTotal() {
-      post("/my/myChayoyTotal").then((res) => {
-        if (res.code === 0) {
-          this.myChayoyTotal = res.data.data;
-        }
-      });
-    },
-    getmyChayoyDirect() {
-      post("/my/myChayoyDirect").then((res) => {
-        if (res.code === 0) {
-          this.myChayoyDirect = res.data.data;
-        }
-      });
-    },
-    getmyChayoyIndirect() {
-      post("/my/myChayoyIndirect").then((res) => {
-        if (res.code === 0) {
-          this.myChayoyIndirect = res.data.data;
-        }
-      });
+    data() {
+        return {
+            higherInfo: "",
+            myTeaFriendNum: "",
+            myZhuanTeaFriendNum: "",
+            myQuanTeaFriendNum: "",
+            wxcode: "",
+            mobile: "",
+
+            myChayoyTotal: {},
+            myChayoyDirect: {},
+            myChayoyIndirect: {},
+        };
     },
+    onLoad: function() {
+        this.loadData();
 
-    // 填写我的信息
-    setUserInfo: function() {
-      let that = this;
-      let data = {
-        phone: this.mobile,
-        wxNumber: this.wxcode,
-      };
-      const info = reqApi.setUserInfo(data);
-      if (info) {
-        info.then((res) => {
-          if (res.data.status == 200) {
-            appEv.errTips(res.data.msg);
-            that.$refs.popup.close();
-            that.mobile = "";
-            that.wxcode = "";
-          } else {
-            appEv.errTips(res.data.msg);
-          }
-        });
-      }
-    },
-    // 复制微信号
-    copyText: function() {
-      let that = this;
-      if (that.higherInfo.higherNumber == "") {
-        appEv.errTips("用户暂未设置微信");
-        return false;
-      }
-      uniCopy({
-        content: that.higherInfo.higherNumber,
-        success: (res) => {},
-        error: (e) => {},
-      });
-    },
-    // 复制账号
-    copyAccount: function(e) {
-      uniCopy({
-        content: e,
-        success: (res) => {},
-        error: (e) => {},
-      });
-    },
-    // 设置联系方式
-    setContact: function() {
-      let that = this;
-      that.$refs.popup.open();
-    },
-    // 关闭窗口
-    closePopup: function() {
-      this.$refs.popup.close();
-    },
-    // 跳转到我的茶友列表
-    getToTeaList: function() {
-      uni.navigateTo({
-        url: "/pages/tea-list/index",
-      });
+        this.getmyChayoyTotal()
+        this.getmyChayoyDirect()
+        this.getmyChayoyIndirect()
     },
-    // 拨打电话
-    dial: function(e) {
-      let that = this;
-      if (that.higherInfo.higherPhone == "") {
-        appEv.errTips("用户暂未设置电话");
-        return false;
-      }
-      uni.makePhoneCall({
-        phoneNumber: e, //仅为示例
-      });
+    methods: {
+        loadData() {
+            post("/my/chayou").then((res) => {
+                if (res.code === 0) {
+                    this.higherInfo = res.data.data.superior;
+                    this.myTeaFriendNum = res.data.data.below;
+                    this.myZhuanTeaFriendNum = res.data.data.lower_level;
+                    this.myQuanTeaFriendNum = res.data.data.below + res.data.data.lower_level;
+                }
+            });
+        },
+
+        getmyChayoyTotal() {
+            post("/my/myChayoyTotal").then((res) => {
+                if (res.code === 0) {
+                    this.myChayoyTotal = res.data.data;
+                }
+            });
+        },
+        getmyChayoyDirect() {
+            post("/my/myChayoyDirect").then((res) => {
+                if (res.code === 0) {
+                    this.myChayoyDirect = res.data.data;
+                }
+            });
+        },
+        getmyChayoyIndirect() {
+            post("/my/myChayoyIndirect").then((res) => {
+                if (res.code === 0) {
+                    this.myChayoyIndirect = res.data.data;
+                }
+            });
+        },
+
+        // 复制账号
+        copyAccount: function(e) {
+            uniCopy({
+                content: e,
+                success: (res) => {},
+                error: (e) => {},
+            });
+        },
+
+        // 跳转到我的茶友列表
+        getToTeaList: function() {
+            uni.navigateTo({
+                url: "/pages/tea-list/index",
+            });
+        },
+        // 拨打电话
+        dial: function(e) {
+            let that = this;
+            if (!e) {
+                appEv.errTips("用户暂未设置电话");
+                return false;
+            }
+            uni.makePhoneCall({
+                phoneNumber: e, //仅为示例
+            });
+        },
     },
-  },
 };
 </script>
 <style lang="scss" scoped>
 page {
-  background: #f5f5f5;
+    background: #f5f5f5;
 }
 
 // 页面配置
 .container {
-  padding: 20rpx 30rpx;
-  box-sizing: border-box;
+    padding: 20rpx 30rpx;
+    box-sizing: border-box;
 }
 
 // 页面配置-end
 
 // 顶部
 .account {
-  font-size: 24rpx;
-  color: #7f7f7f;
+    font-size: 24rpx;
+    color: #7f7f7f;
 }
 
 .option_weixin {
-  width: 42rpx;
-  height: 35rpx;
+    width: 42rpx;
+    height: 35rpx;
 }
 
 .option_phone {
-  width: 36rpx;
-  height: 36rpx;
-  margin-left: 30rpx;
+    width: 36rpx;
+    height: 36rpx;
+    margin-left: 30rpx;
 }
 
 .user_name {
-  font-size: 30rpx;
-  color: #363638;
-  margin-right: 12rpx;
+    font-size: 30rpx;
+    color: #363638;
+    margin-right: 12rpx;
 }
 
 .head {
-  width: 100%;
-  overflow: hidden;
-  border-radius: 4rpx;
-  background: #fff;
-  border-bottom: 3rpx solid rgba(0, 0, 0, 0.12);
+    width: 100%;
+    overflow: hidden;
+    background: #fff;
+    border-radius: 5px;
 }
 
 .head_img {
-  width: 79rpx;
-  height: 79rpx;
-  margin-right: 30rpx;
-  border-radius: 50%;
+    width: 90rpx;
+    height: 90rpx;
+    margin-right: 30rpx;
+    border-radius: 50%;
 }
 
 .option_hr {
-  width: 3rpx;
-  height: 66rpx;
-  background: rgba(0, 0, 0, 0.12);
-  margin-left: 30rpx;
+    width: 3rpx;
+    height: 66rpx;
+    background: rgba(0, 0, 0, 0.12);
+    margin-left: 30rpx;
 }
 
 .copyBalance {
-  padding: 0 10rpx;
-  background: #1cbe8c;
-  color: #fff;
-  font-size: 22rpx;
-  margin-left: 12rpx;
+    padding: 0 10rpx;
+    background: #1cbe8c;
+    color: #fff;
+    font-size: 22rpx;
+    margin-left: 12rpx;
 }
 
 .level {
-  width: 74rpx;
-  height: 26rpx;
-  background: #1cbe8c;
-  color: #fff;
-  font-size: 20rpx;
-  line-height: 1;
+    width: 74rpx;
+    height: 26rpx;
+    background: #1cbe8c;
+    color: #fff;
+    font-size: 20rpx;
+    line-height: 1;
 }
 
 .head_time {
-  width: 100%;
-  height: 60rpx;
-  padding: 0 24rpx;
-  box-sizing: border-box;
-  font-size: 22rpx;
-  color: #606060;
+    width: 100%;
+    height: 60rpx;
+    padding: 0 24rpx;
+    box-sizing: border-box;
+    font-size: 22rpx;
+    color: #606060;
 }
 
 .head_info {
-  width: 100%;
-  overflow: hidden;
-  padding: 40rpx 24rpx 20rpx;
-  box-sizing: border-box;
+    width: 100%;
+    overflow: hidden;
+    padding: 40rpx 24rpx 20rpx;
+    box-sizing: border-box;
 
 }
 
@@ -295,83 +257,91 @@ page {
 
 // 茶友数量
 .list_num {
-  font-size: 36rpx;
-  color: #1cbe8c;
+    font-size: 36rpx;
+    color: #1cbe8c;
 }
 
 .total_num {
-  font-size: 40rpx;
-  color: #232323;
+    font-size: 40rpx;
+    color: #232323;
 }
 
 .list_text {
-  font-size: 26rpx;
-  color: #606060;
+    font-size: 26rpx;
+    color: #606060;
 }
 
 .list_num text {
-  font-size: 26rpx;
-  color: #1cbe8c;
+    font-size: 26rpx;
+    color: #1cbe8c;
 }
 
 .total_num text {
-  font-size: 26rpx;
-  color: #232323;
+    font-size: 26rpx;
+    color: #232323;
 }
 
 .borRight {
-  border-right: 3rpx solid rgba(0, 0, 0, 0.12);
+    border-right: 3rpx solid rgba(0, 0, 0, 0.12);
 }
 
 .info_list {
-  height: 100%;
-  width: 50%;
-  box-sizing: border-box;
+    height: 100%;
+    width: 50%;
+    box-sizing: border-box;
 }
 
 .total_text {
-  font-size: 26rpx;
-  color: #606060;
-  margin-top: 12rpx;
+    font-size: 26rpx;
+    color: #606060;
+    margin-top: 12rpx;
 }
 
 .total_info {
-  font-size: 22rpx;
-  color: #333;
-  margin-left: 32rpx;
+    font-size: 22rpx;
+    color: #333;
+    margin-left: 32rpx;
 }
 
 .untotal_info {
-  font-size: 22rpx;
-  color: #333;
-  margin-top: 20rpx;
+    font-size: 22rpx;
+    color: #333;
+    margin-top: 20rpx;
+    .ti_l,.ti_r{
+        display: inline-block;
+        width: 56%;
+        text-align: right;
+    }
+    .ti_r{
+        width: 44%;
+        text-align: left;
+    }
 }
 
 .tea_con {
-  width: 100%;
-  overflow: hidden;
-  border-radius: 8rpx;
-  background: #fff;
+    width: 100%;
+    overflow: hidden;
+    border-radius: 8rpx;
+    background: #fff;
 }
 
 .tea_info {
-  width: 100%;
-  // height: 156rpx;s
-  padding: 26rpx 0;
-  box-sizing: border-box;
+    width: 100%;
+    padding: 26rpx 0;
+    box-sizing: border-box;
 }
 
 .tea_total {
-  width: 100%;
-  height: 260rpx;
-  border-bottom: 3rpx solid rgba(0, 0, 0, 0.12);
+    width: 100%;
+    height: 260rpx;
+    border-bottom: 3rpx solid rgba(0, 0, 0, 0.12);
 }
 
 .total_con {
-  width: 164rpx;
-  height: 164rpx;
-  border: 6rpx solid #1cbe8c;
-  border-radius: 50%;
+    width: 164rpx;
+    height: 164rpx;
+    border: 6rpx solid #1cbe8c;
+    border-radius: 50%;
 }
 
 // 茶友数量-end

+ 11 - 45
src/pages/my/index.vue

@@ -226,12 +226,9 @@ export default {
 
       allChaYou: "",
 
-      isAuthentication: true, //是否开启实名模块
+      isAuthentication: uni.getStorageSync("isAuthentication"), //是否开启实名模块
     };
   },
-  created() {
-    this.isAuthentication = uni.getStorageSync("isAuthentication");
-  },
   onLoad(options) {
     //推荐人ID
     if (options.invite) {
@@ -245,11 +242,8 @@ export default {
     let token = uni.getStorageSync("token");
     this.userinfo = uni.getStorageSync("userinfo");
 
-    if (!token) {
-      this.login();
-    } else {
-      this.getuserInfo();
-    }
+    if (!token) this.login()
+    else this.getuserInfo()
   },
   methods: {
     login() {
@@ -258,13 +252,6 @@ export default {
       wx.login({
         success(res) {
           if (res.code) {
-            // wx.request({
-            //  url: `https://api.weixin.qq.com/sns/jscode2session?appid=wx8ebee994ea7c5af3&secret=f80039555c022bf0a805bed83358fa01&js_code=${res.code}&grant_type=authorization_code`,
-            //  success:(res)=>{
-            //    console.log(res);
-            //  }
-            // })
-
             post("appletLogin", {
               code: res.code,
               invite: that.invited,
@@ -303,15 +290,10 @@ export default {
       });
     },
     // 获取userinfo
-    getuserInfo() {
-      post("/user/userinfo").then((res) => {
-        if (res.code === 0) {
-          uni.setStorageSync("userinfo", res.data.data);
-          this.userinfo = res.data.data;
-          if (this.isAuthentication) this.isShiMing = this.userinfo.is_authentication == 0;
-          else this.isShiMing = false;
-        }
-      });
+    async getuserInfo() {
+      this.userinfo = await uni.userfun();
+      if (this.isAuthentication) this.isShiMing = this.userinfo.is_authentication == 0;
+      else this.isShiMing = false;
     },
     //授权并登录
     onAuthSuccess() {
@@ -335,9 +317,6 @@ export default {
     // 未开放提示信息
     SetHint() {
       this.goto('/pages/agreement/index',{tit:'用户身份及权益',type:24})
-      // uni.navigateTo({
-      //   url: "/pages/tea-rule/index",
-      // });
     },
     // 跳转到分享页面
     getImgPage() {
@@ -397,25 +376,12 @@ export default {
         url: "/pages/my-acc-money/my-acc-money",
       });
     },
-    // 更换头像
+    // 点击头像
     upheadimg() {
       let that = this;
-      uni.chooseImage({
-        count: 1, // 最多可以选择的图片张数,默认9
-        sizeType: ["original"], // original 原图,compressed 压缩图,默认二者都有
-        sourceType: ["album", "camera"], // album 从相册选图,camera 使用相机,默认二者都有
-        success: function(res) {
-          var arr = res.tempFiles;
-          that.$up(arr[0].path).then((res) => {
-            post("user/setup", {
-              head_pic: res,
-              nickname: that.userinfo.nickname,
-            }).then((res) => {
-              that.getuserInfo();
-              appEv.errTips("更换成功");
-            });
-          });
-        },
+      uni.previewImage({
+        urls: [that.userinfo.head_pic],
+        current: 0,
       });
     },
     // 获取茶友

+ 8 - 16
src/pages/my/userinfo.vue

@@ -24,9 +24,10 @@ export default {
             },
         };
     },
-    onLoad() {
+    onShow() {
         this.loadData();
     },
+    onLoad() {},
     methods: {
         loadData() {
             let da = uni.getStorageSync("userinfo");
@@ -34,7 +35,6 @@ export default {
             this.formDa.head_pic = da.head_pic;
         },
         upheadimg() {
-            // 上传图片uploadImg
             let that = this;
             uni.chooseImage({
                 count: 1, // 最多可以选择的图片张数,默认9
@@ -42,10 +42,8 @@ export default {
                 sourceType: ["album", "camera"], // album 从相册选图,camera 使用相机,默认二者都有
                 success: function(res) {
                     var arr = res.tempFiles;
-                    that.$up(arr[0].path).then((res) => {
-                        that.$set(that.formDa, "head_pic", res);
-                        // that.formDa.head_pic = res;
-                    });
+                    uni.setStorageSync("headImgPath",arr[0].path);
+                    that.goto("/pagesB/image-cropper/index")
                 },
             });
         },
@@ -54,19 +52,13 @@ export default {
                 if (res.code === 0) {
                     appEv.errTips(res.msg);
                     this.getuserInfo();
-                    uni.navigateBack({
-                        delta: 1, //返回层数,2则上上页
-                    });
+                    uni.navigateBack();
                 }
             });
         },
-        getuserInfo() {
-            post("/user/userinfo").then((res) => {
-                if (res.code === 0) {
-                    uni.setStorageSync("userinfo", res.data.data);
-                    this.loadData();
-                }
-            });
+        async getuserInfo() {
+            await uni.userfun();
+            this.loadData();
         },
     },
 };

+ 2 - 7
src/pages/product/p_details.vue

@@ -409,13 +409,8 @@ export default {
         }
       });
     },
-    getuserInfo() {
-      post("/user/userinfo").then((res) => {
-        if (res.code === 0) {
-          uni.setStorageSync("userinfo", res.data.data);
-          this.userinfo = res.data.data;
-        }
-      });
+    async getuserInfo() {
+      this.userinfo = await uni.userfun();
     },
   },
   onShareAppMessage: function () {

+ 1 - 5
src/pages/to-pay-list/index.vue

@@ -380,11 +380,7 @@ export default {
 
     // 获取userinfo
     async getuserInfo() {
-      let res = await post("/user/userinfo")
-      if (res.code === 0) {
-        uni.setStorageSync("userinfo", res.data.data);
-        this.userinfo = res.data.data;
-      }
+      this.userinfo = await uni.userfun();
     },
   },
 };

+ 2 - 7
src/pages/top-up/index.vue

@@ -105,13 +105,8 @@ export default {
                 }
             });
         },
-        getuserInfo() {
-            post("/user/userinfo").then((res) => {
-                if (res.code === 0) {
-                    uni.setStorageSync("userinfo", res.data.data);
-                    this.userinfo = res.data.data;
-                }
-            });
+        async getuserInfo() {
+            this.userinfo = await uni.userfun();
         },
         checkboxChange(e) {
             var value = e.detail.value;

+ 44 - 0
src/pagesB/image-cropper/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="image-cropper">
+    <image-cropper ref="imageCropper" @crop="handleCrop" />
+  </div>
+</template>
+
+<script>
+import { post } from "@/request/api.js";
+import imageCropper from "@/components/image-cropper/image-cropper.vue";
+export default {
+  name: "image-cropper",
+  props: {},
+  components: { imageCropper },
+  data() {
+    return {
+      headImgPath: uni.getStorageSync("headImgPath"),
+      userinfo: uni.getStorageSync("userinfo"),
+    };
+  },
+  methods: {
+    handleCrop(e) {
+      this.$up(e.tempFilePath).then((res) => {
+        post("user/setup", {
+          head_pic: res,
+          nickname: this.userinfo.nickname
+        }).then( async (res) => {
+          await uni.userfun();
+          setTimeout(() => {
+            uni.navigateBack();
+          }, 100);
+        });
+      });
+    },
+  },
+  onLoad(da) {
+    this.$refs.imageCropper.initImage(this.headImgPath);
+  },
+  onShow() {},
+  mounted() {},
+};
+</script>
+
+<style scoped lang='scss'>
+</style>

+ 4 - 25
src/request/request.js

@@ -1,15 +1,7 @@
 import host from "./config.js"
-import {
-	goto
-} from '@/utils/myfun.js';
+import { goto } from '@/utils/myfun.js';
 
-export default ({
-	url,
-	method,
-	params,
-	header,
-	baseURL
-}) => {
+export default ({ url, method, params, header, baseURL }) => {
 	baseURL = baseURL ? baseURL : host.Hhost;
 
 	return new Promise((resolve, reject) => {
@@ -25,9 +17,6 @@ export default ({
 			},
 			fail(err) {
 				reject(err);
-			},
-			complete() {
-				// uni.hideLoading();
 			}
 		});
 	});
@@ -52,21 +41,11 @@ uni.addInterceptor('request', {
 				goto("/pages/my/login");
 				// #endif
 				// #ifdef  MP-WEIXIN
-				uni.switchTab({
-					url: "/pages/my/index"
-				});
+				uni.switchTab({ url: "/pages/my/index" });
 				// #endif
 			}, 1500);
 		}
-		if (args.data.code == -1) {
-			uni.showToast({
-				title: args.data.msg,
-				duration: 3000,
-				icon: "none",
-			});
-		}
-
-		if (args.data.code == 301) {
+		if ([-1, 301].includes(args.data.code)) {
 			uni.showToast({
 				title: args.data.msg,
 				duration: 2000,

+ 0 - 25
src/utils/myfun.js

@@ -22,31 +22,6 @@ export function getDiffDate(minDate, maxDate) {
   return months;
 }
 
-// 时间格式化
-export function times(date, ym) {
-  date = new Date(date.replace(/-/g, '/'));
-  var y = date.getFullYear();
-  var m = date.getMonth() + 1;
-  m = m < 10 ? "0" + m : m;
-  var d = date.getDate();
-  d = d < 10 ? "0" + d : d;
-  var h = date.getHours();
-  h = h < 10 ? "0" + h : h;
-  var minute = date.getMinutes();
-  minute = minute < 10 ? "0" + minute : minute;
-  var second = date.getSeconds();
-  second = second < 10 ? "0" + second : second;
-  if (ym == "ym") {
-    return y + "-" + m;
-  }
-  else if (ym == "ymd") {
-    return y + "-" + m + "-" + d;
-  }
-  else if (ym == "ymds") {
-    return y + "-" + m + "-" + d + " " + h + ":" + minute + ":" + second;
-  }
-}
-
 // 将rgb颜色转成hex
 export function RtoH(color) {
   var rgb = color.split(',');

+ 20 - 1
src/utils/run_now.js

@@ -4,4 +4,23 @@ post('isAuthentication').then(res => {
     if (res.code === 0) {
         uni.setStorageSync("isAuthentication", res.data.data);
     }
-})
+})
+
+// 获取userinfo方法挂载uni公共对象上
+uni.userfun = () => {
+    return new Promise((resolve, reject) => {
+        post("/user/userinfo").then((res) => {
+            if (res.code === 0) {
+                uni.setStorageSync("userinfo", res.data.data);
+                resolve(res.data.data)
+            } else {
+                reject({})
+            }
+        });
+    })
+
+}
+
+// uni.userfun().then(res=>{
+//     console.log('----',res);
+// })