Browse Source

1. 新增图片压缩处理

Cee.Yang 3 năm trước cách đây
mục cha
commit
43fc1fd87a

+ 657 - 0
components/htz-image-upload/htz-image-upload.vue

@@ -0,0 +1,657 @@
+<template>
+	<view class="htz-image-upload-list">
+		<view class="htz-image-upload-Item" v-for="(item,index) in uploadLists" :key="index">
+			<view class="htz-image-upload-Item-video" v-if="(!/.(gif|jpg|jpeg|png|gif|jpg|png)$/i.test(item))">
+				<video :disabled="false" :controls="false" :src="item">
+					<cover-view class="htz-image-upload-Item-video-fixed" @click="previewVideo(item)"></cover-view>
+
+					<cover-view class="htz-image-upload-Item-del-cover" v-if="remove && previewVideoSrc==''"
+						@click="imgDel(index)">×</cover-view>
+
+				</video>
+
+			</view>
+
+			<image v-else :src="item" @click="imgPreview(item)"></image>
+
+			<view class="htz-image-upload-Item-del" v-if="remove" @click="imgDel(index)">×</view>
+		</view>
+		<view class="htz-image-upload-Item htz-image-upload-Item-add" v-if="uploadLists.length<max && add"
+			@click="chooseFile">
+			+
+		</view>
+		<view class="preview-full" v-if="previewVideoSrc!=''">
+			<video :autoplay="true" :src="previewVideoSrc" :show-fullscreen-btn="false">
+				<cover-view class="preview-full-close" @click="previewVideoClose"> ×
+				</cover-view>
+			</video>
+		</view>
+
+
+		<!--  -->
+	</view>
+</template>
+
+<style>
+	.ceshi {
+		width: 100%;
+		height: 100%;
+		position: relative;
+		top: 0;
+		left: 0;
+		bottom: 0;
+		right: 0;
+		background-color: #FFFFFF;
+		color: #2C405A;
+		opacity: 0.5;
+		z-index: 100;
+	}
+</style>
+
+<script>
+	export default {
+		name: 'htz-image-upload',
+		props: {
+			max: { //展示图片最大值
+				type: Number,
+				default: 1,
+			},
+			chooseNum: { //选择图片数
+				type: Number,
+				default: 9,
+			},
+			name: { //发到后台的文件参数名
+				type: String,
+				default: 'file',
+			},
+			remove: { //是否展示删除按钮
+				type: Boolean,
+				default: true,
+			},
+			add: { //是否展示添加按钮
+				type: Boolean,
+				default: true,
+			},
+			disabled: { //是否禁用
+				type: Boolean,
+				default: false,
+			},
+			sourceType: { //选择照片来源 【ps:H5就别费劲了,设置了也没用。不是我说的,官方文档就这样!!!】
+				type: Array,
+				default: () => ['album', 'camera'],
+			},
+			action: { //上传地址
+				type: String,
+				default: '',
+			},
+			headers: { //上传的请求头部
+				type: Object,
+				default: () => {},
+			},
+			formData: { //HTTP 请求中其他额外的 form data
+				type: Object,
+				default: () => {},
+			},
+			compress: { //是否需要压缩
+				type: Boolean,
+				default: true,
+			},
+			quality: { //压缩质量,范围0~100
+				type: Number,
+				default: 80,
+			},
+			value: { //受控图片列表
+				type: Array,
+				default: () => [],
+			},
+			uploadSuccess: {
+				default: (res) => {
+					return {
+						success: false,
+						url: ''
+					}
+				},
+			},
+			mediaType: { //文件类型 image/video/all
+				type: String,
+				default: 'image',
+			},
+			maxDuration: { //拍摄视频最长拍摄时间,单位秒。最长支持 60 秒。 (只针对拍摄视频有用)
+				type: Number,
+				default: 60,
+			},
+			camera: { //'front'、'back',默认'back'(只针对拍摄视频有用)
+				type: String,
+				default: 'back',
+			},
+
+		},
+		data() {
+			return {
+				uploadLists: [],
+				mediaTypeData: ['image', 'video', 'all'],
+				previewVideoSrc: '',
+			}
+		},
+		mounted: function() {
+			this.$nextTick(function() {
+				this.uploadLists = this.value;
+				if (this.mediaTypeData.indexOf(this.mediaType) == -1) {
+					uni.showModal({
+						title: '提示',
+						content: 'mediaType参数不正确',
+						showCancel: false,
+						success: function(res) {
+							if (res.confirm) {
+								//console.log('用户点击确定');
+							} else if (res.cancel) {
+								//console.log('用户点击取消');
+							}
+						}
+					});
+				}
+			});
+		},
+		watch: {
+			value(val, oldVal) {
+				//console.log('value',val, oldVal)
+				this.uploadLists = val;
+			},
+		},
+		methods: {
+			previewVideo(src) {
+				this.previewVideoSrc = src;
+				// this.previewVideoSrc =
+				// 	'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-fbd63a76-dc76-485c-b711-f79f2986daeb/ba804d82-860b-4d1a-a706-5a4c8ce137c3.mp4'
+			},
+			previewVideoClose() {
+				this.previewVideoSrc = ''
+				console.log('previewVideoClose', this.previewVideoSrc)
+			},
+			imgDel(index) {
+				uni.showModal({
+					title: '提示',
+					content: '您确定要删除么?',
+					success: (res) => {
+						if (res.confirm) {
+							// this.uploadLists.splice(index, 1)
+							// this.$emit("input", this.uploadLists);
+							// this.$emit("imgDelete", this.uploadLists);
+							let delUrl = this.uploadLists[index]
+							this.uploadLists.splice(index, 1)
+							this.$emit("input", this.uploadLists);
+							this.$emit("imgDelete", {
+								del: delUrl,
+								tempFilePaths: this.uploadLists
+							});
+						} else if (res.cancel) {}
+					}
+				});
+			},
+			imgPreview(index) {
+
+				var imgData = this.uploadLists.filter(item => /.(gif|jpg|jpeg|png|gif|jpg|png)$/i.test(item)) //只预览图片的
+				uni.previewImage({
+					urls: imgData,
+					current: index,
+					loop: true,
+				});
+
+
+			},
+			chooseFile() {
+				if (this.disabled) {
+					return false;
+				}
+				switch (this.mediaTypeData.indexOf(this.mediaType)) {
+					case 1: //视频
+						this.videoAdd();
+						break;
+					case 2: //全部
+						uni.showActionSheet({
+							itemList: ['相册', '视频'],
+							success: (res) => {
+								if (res.tapIndex == 1) {
+									this.videoAdd();
+								} else if (res.tapIndex == 0) {
+									this.imgAdd();
+								}
+							},
+							fail: (res) => {
+								console.log(res.errMsg);
+							}
+						});
+						break;
+					default: //图片
+						this.imgAdd();
+						break;
+				}
+
+
+				//if(this.mediaType=='image'){
+
+
+			},
+			videoAdd() {
+				console.log('videoAdd')
+				let nowNum = Math.abs(this.uploadLists.length - this.max);
+				let thisNum = (this.chooseNum > nowNum ? nowNum : this.chooseNum) //可选数量
+				uni.chooseVideo({
+					compressed: this.compress,
+					sourceType: this.sourceType,
+					camera: this.camera,
+					maxDuration: this.maxDuration,
+					success: (res) => {
+						console.log('videoAdd', res)
+						console.log(res.tempFilePath)
+						this.chooseSuccessMethod([res.tempFilePath], 1)
+						//this.imgUpload([res.tempFilePath]);
+						//console.log('tempFiles', res)
+						// if (this.action == '') { //未配置上传路径
+						// 	this.$emit("chooseSuccess", res.tempFilePaths);
+						// } else {
+						// 	if (this.compress && (res.tempFiles[0].size / 1024 > 1025)) { //设置了需要压缩 并且 文件大于1M,进行压缩上传
+						// 		this.imgCompress(res.tempFilePaths);
+						// 	} else {
+						// 		this.imgUpload(res.tempFilePaths);
+						// 	}
+						// }
+					}
+				});
+			},
+			imgAdd() {
+				console.log('imgAdd')
+				let nowNum = Math.abs(this.uploadLists.length - this.max);
+				let thisNum = (this.chooseNum > nowNum ? nowNum : this.chooseNum) //可选数量
+				console.log('nowNum', nowNum)
+				console.log('thisNum', thisNum)
+				// #ifdef APP-PLUS
+				if (this.sourceType.length > 1) {
+					uni.showActionSheet({
+						itemList: ['拍摄', '从手机相册选择'],
+						success: (res) => {
+							if (res.tapIndex == 1) {
+								this.appGallery(thisNum);
+							} else if (res.tapIndex == 0) {
+								this.appCamera();
+							}
+						},
+						fail: (res) => {
+							console.log(res.errMsg);
+						}
+					});
+				}
+				if (this.sourceType.length == 1 && this.sourceType.indexOf('album') > -1) {
+					this.appGallery(thisNum);
+				}
+
+				if (this.sourceType.length == 1 && this.sourceType.indexOf('camera') > -1) {
+					this.appCamera();
+				}
+				// #endif
+				//#ifndef APP-PLUS
+				uni.chooseImage({
+					count: thisNum,
+					sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
+					sourceType: this.sourceType,
+					success: (res) => {
+						this.chooseSuccessMethod(res.tempFilePaths, 0)
+						//console.log('tempFiles', res)
+						// if (this.action == '') { //未配置上传路径
+						// 	this.$emit("chooseSuccess", res.tempFilePaths);
+						// } else {
+						// 	if (this.compress && (res.tempFiles[0].size / 1024 > 1025)) { //设置了需要压缩 并且 文件大于1M,进行压缩上传
+						// 		this.imgCompress(res.tempFilePaths);
+						// 	} else {
+						// 		this.imgUpload(res.tempFilePaths);
+						// 	}
+						// }
+					}
+				});
+				// #endif
+			},
+			appCamera() {
+				var cmr = plus.camera.getCamera();
+				var res = cmr.supportedImageResolutions[0];
+				var fmt = cmr.supportedImageFormats[0];
+				//console.log("Resolution: " + res + ", Format: " + fmt);
+				cmr.captureImage((path) => {
+						//alert("Capture image success: " + path);
+						this.chooseSuccessMethod([path], 0)
+					},
+					(error) => {
+						//alert("Capture image failed: " + error.message);
+						console.log("Capture image failed: " + error.message)
+					}, {
+						resolution: res,
+						format: fmt
+					}
+				);
+			},
+			appGallery(maxNum) {
+				plus.gallery.pick((res) => {
+					this.chooseSuccessMethod(res.files, 0)
+				}, function(e) {
+					//console.log("取消选择图片");
+				}, {
+					filter: "image",
+					multiple: true,
+					maximum: maxNum
+				});
+			},
+			chooseSuccessMethod(filePaths, type) {
+				if (this.action == '') { //未配置上传路径
+					this.$emit("chooseSuccess", filePaths, type); //filePaths 路径 type 0 为图片 1为视频
+				} else {
+					if (type == 1) {
+						this.imgUpload(filePaths);
+					} else {
+						if (this.compress) { //设置了需要压缩
+							this.imgCompress(filePaths);
+						} else {
+							this.imgUpload(filePaths);
+						}
+					}
+
+				}
+			},
+			imgCompress(tempFilePaths) {
+				uni.showLoading({
+					title: '压缩中...'
+				});
+
+				let compressImgs = [];
+				let results = [];
+				tempFilePaths.forEach((item, index) => {
+					compressImgs.push(new Promise((resolve, reject) => {
+						// #ifndef H5
+						uni.compressImage({
+							src: item,
+							quality: this.quality,
+							success: res => {
+								//console.log('compressImage', res.tempFilePath)
+								results.push(res.tempFilePath);
+								resolve(res.tempFilePath);
+							},
+							fail: (err) => {
+								//console.log(err.errMsg);
+								reject(err);
+							},
+							complete: () => {
+								//uni.hideLoading();
+							}
+						})
+						// #endif
+						// #ifdef H5
+						this.canvasDataURL(item, {
+							quality: this.quality / 100
+						}, (base64Codes) => {
+							//this.imgUpload(base64Codes);
+							results.push(base64Codes);
+							resolve(base64Codes);
+						})
+						// #endif
+					}))
+				})
+				Promise.all(compressImgs) //执行所有需请求的接口
+					.then((results) => {
+						uni.hideLoading();
+						console.log('imgUpload', results)
+						this.imgUpload(results);
+					})
+					.catch((res, object) => {
+						uni.hideLoading();
+					});
+			},
+			imgUpload(tempFilePaths) {
+				// if (this.action == '') {
+				// 	uni.showToast({
+				// 		title: '未配置上传地址',
+				// 		icon: 'none',
+				// 		duration: 2000
+				// 	});
+				// 	return false;
+				// }
+				uni.showLoading({
+					title: '上传中'
+				});
+				console.log('imgUpload', tempFilePaths)
+				let uploadImgs = [];
+				tempFilePaths.forEach((item, index) => {
+					uploadImgs.push(new Promise((resolve, reject) => {
+						console.log(index, item)
+						const uploadTask = uni.uploadFile({
+							url: this.action, //仅为示例,非真实的接口地址
+							filePath: item,
+							name: this.name,
+							fileType: 'image',
+							formData: this.formData,
+							header: this.headers,
+							success: (uploadFileRes) => {
+								//uni.hideLoading();
+								//console.log(typeof this.uploadSuccess)
+								//console.log('')
+								if (typeof this.uploadSuccess == 'function') {
+									if (this.uploadSuccess(uploadFileRes).success) {
+										this.value.push(this.uploadSuccess(uploadFileRes)
+											.url)
+										this.$emit("input", this.uploadLists);
+									}
+								}
+								resolve(uploadFileRes);
+								this.$emit("uploadSuccess", uploadFileRes);
+							},
+							fail: (err) => {
+								console.log(err);
+								//uni.hideLoading();
+								reject(err);
+								this.$emit("uploadFail", err);
+							},
+							complete: () => {
+								//uni.hideLoading();
+							}
+						});
+					}))
+				})
+				Promise.all(uploadImgs) //执行所有需请求的接口
+					.then((results) => {
+						uni.hideLoading();
+					})
+					.catch((res, object) => {
+						uni.hideLoading();
+						this.$emit("uploadFail", res);
+					});
+				// uploadTask.onProgressUpdate((res) => {
+				// 	//console.log('',)
+				// 	uni.showLoading({
+				// 		title: '上传中' + res.progress + '%'
+				// 	});
+				// 	if (res.progress == 100) {
+				// 		uni.hideLoading();
+				// 	}
+				// });
+			},
+			canvasDataURL(path, obj, callback) {
+				var img = new Image();
+				img.src = path;
+				img.onload = function() {
+					var that = this;
+					// 默认按比例压缩
+					var w = that.width,
+						h = that.height,
+						scale = w / h;
+					w = obj.width || w;
+					h = obj.height || (w / scale);
+					var quality = 0.8; // 默认图片质量为0.8
+					//生成canvas
+					var canvas = document.createElement('canvas');
+					var ctx = canvas.getContext('2d');
+					// 创建属性节点
+					var anw = document.createAttribute("width");
+					anw.nodeValue = w;
+					var anh = document.createAttribute("height");
+					anh.nodeValue = h;
+					canvas.setAttributeNode(anw);
+					canvas.setAttributeNode(anh);
+					ctx.drawImage(that, 0, 0, w, h);
+					// 图像质量
+					if (obj.quality && obj.quality <= 1 && obj.quality > 0) {
+						quality = obj.quality;
+					}
+					// quality值越小,所绘制出的图像越模糊
+					var base64 = canvas.toDataURL('image/jpeg', quality);
+					// 回调函数返回base64的值
+					callback(base64);
+				}
+			},
+		}
+	}
+</script>
+
+<style>
+	.preview-full {
+		position: fixed;
+		top: 0;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 100%;
+		z-index: 1002;
+	}
+
+	.preview-full video {
+		width: 100%;
+		height: 100%;
+		z-index: 1002;
+	}
+
+	.preview-full-close {
+		position: fixed;
+		right: 32rpx;
+		top: 25rpx;
+		width: 80rpx;
+		height: 80rpx;
+		line-height: 60rpx;
+		text-align: center;
+		z-index: 1003;
+		/* 	background-color: #808080; */
+		color: #fff;
+		font-size: 65rpx;
+		font-weight: bold;
+		text-shadow: 1px 2px 5px rgb(0 0 0);
+	}
+
+
+
+	/* .preview-full-close-before,
+	.preview-full-close-after {
+		position: absolute;
+		top: 50%;
+		left: 50%;
+		content: '';
+		height: 60rpx;
+		margin-top: -30rpx;
+		width: 6rpx;
+		margin-left: -3rpx;
+		background-color: #FFFFFF;
+		z-index: 20000;
+	}
+
+	.preview-full-close-before {
+		transform: rotate(45deg);
+
+	}
+
+	.preview-full-close-after {
+		transform: rotate(-45deg);
+
+	} */
+
+	.htz-image-upload-list {
+		display: flex;
+		flex-wrap: wrap;
+	}
+
+	.htz-image-upload-Item {
+		width: 160rpx;
+		height: 160rpx;
+		margin: 13rpx;
+		border-radius: 10rpx;
+		position: relative;
+	}
+
+	.htz-image-upload-Item image {
+		width: 100%;
+		height: 100%;
+		border-radius: 10rpx;
+	}
+
+	.htz-image-upload-Item-video {
+		width: 100%;
+		height: 100%;
+		border-radius: 10rpx;
+		position: relative;
+
+	}
+
+	.htz-image-upload-Item-video-fixed {
+		position: absolute;
+		top: 0;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 100%;
+		border-radius: 10rpx;
+		z-index: 996;
+
+	}
+
+	.htz-image-upload-Item video {
+		width: 100%;
+		height: 100%;
+		border-radius: 10rpx;
+
+	}
+
+	.htz-image-upload-Item-add {
+		font-size: 105rpx;
+		/* line-height: 160rpx; */
+		text-align: center;
+		border: 1px dashed #d9d9d9;
+		color: #d9d9d9;
+	}
+
+	.htz-image-upload-Item-del {
+		background-color: #f5222d;
+		font-size: 24rpx;
+		position: absolute;
+		width: 35rpx;
+		height: 35rpx;
+		line-height: 35rpx;
+		text-align: center;
+		top: 0;
+		right: 0;
+		z-index: 997;
+		color: #fff;
+	}
+
+	.htz-image-upload-Item-del-cover {
+		background-color: #f5222d;
+		font-size: 24rpx;
+		position: absolute;
+		width: 35rpx;
+		height: 35rpx;
+		text-align: center;
+		top: 0;
+		right: 0;
+		color: #fff;
+		/* #ifdef APP-PLUS */
+		line-height: 25rpx;
+		/* #endif */
+		/* #ifndef APP-PLUS */
+		line-height: 35rpx;
+		/* #endif */
+		z-index: 997;
+
+	}
+</style>

+ 380 - 0
components/robby-image-upload/robby-image-upload.vue

@@ -0,0 +1,380 @@
+<template>
+	<view class="imageUploadContainer">
+		<view class="imageUploadList">
+			<view class="imageItem" v-bind:key="index" v-for="(path,index) in imageListData">
+				<image :src="path" :class="{'dragging':isDragging(index)}" draggable="true" @tap="previewImage" :data-index="index" @touchstart="start" @touchmove.stop.prevent="move" @touchend="stop"></image>
+				<view v-if="isShowDel" class="imageDel" @tap="deleteImage" :data-index="index">x</view>
+			</view>
+			<view v-if="isShowAdd" class="imageUpload" @tap="selectImage">+</view>
+		</view>
+		<image v-if="showMoveImage" class="moveImage" :style="{left:posMoveImageLeft, top:posMoveImageTop}" :src="moveImagePath"></image>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:'robby-image-upload',
+		props: ['value','enableDel','enableAdd','enableDrag','serverUrl','formData','header', 'limit','fileKeyName','showUploadProgress','serverUrlDeleteImage'],
+		data() {
+			return {
+				imageBasePos:{
+					x0: -1,
+					y0: -1,
+					w:-1,
+					h:-1,
+				},
+				showMoveImage: false,
+				moveImagePath: '',
+				moveLeft: 0,
+				moveTop: 0,
+				deltaLeft: 0,
+				deltaTop: 0,
+				dragIndex: null,
+				targetImageIndex: null,
+				imageList: [],
+				isDestroyed: false
+			}
+		}, 
+		mounted: function(){
+			this.imageList = this.value
+			
+			if(this.showUploadProgress === false){
+				this.showUploadProgress = false
+			}else{
+				this.showUploadProgress = true
+			}
+		},
+		destroyed: function(){
+			this.isDestroyed = true
+		},
+		computed:{
+			imageListData: function(){
+				if(this.value){
+					return this.value
+				}
+			},
+			posMoveImageLeft: function(){ 
+				return this.moveLeft + 'px'
+			},
+			posMoveImageTop: function(){
+				return this.moveTop + 'px'
+			},
+			isShowDel: function(){
+				if(this.enableDel === false){
+					return false
+				}else{
+					return true
+				}
+			},
+			isShowAdd: function(){
+				if(this.enableAdd === false){
+					return false
+				}
+				
+				if(this.limit && this.imageList.length >= this.limit){
+					return false
+				}
+				
+				return true
+			},
+			isDragable: function(){
+				if(this.enableDrag === false){
+					return false
+				}else{
+					return true
+				}
+			}
+		},
+		methods:{
+			selectImage: function(){
+				var _self = this
+				if(!_self.imageList){
+					_self.imageList = []
+				} 
+				
+				uni.chooseImage({
+					count: _self.limit ? (_self.limit - _self.imageList.length) : 999,
+					success: function(e){
+						var imagePathArr = e.tempFilePaths
+						
+						//如果设置了limit限制,在web上count参数无效,这里做判断控制选择的数量是否合要求
+						//在非微信小程序里,虽然可以选多张,但选择的结果会被截掉
+						//在app里,会自动做选择数量的限制
+						if(_self.limit){
+							var availableImageNumber = _self.limit - _self.imageList.length
+							if(availableImageNumber < imagePathArr.length){
+								uni.showToast({
+									title: '图片总数限制为'+_self.limit+'张,当前还可以选'+availableImageNumber+'张',
+									icon:'none',
+									mask: false,
+									duration: 2000
+								});
+								return
+							}
+						}
+						
+						//检查服务器地址是否设置,设置即表示图片要上传到服务器
+						if(_self.serverUrl){
+							uni.showToast({
+								title: '上传进度:0/' + imagePathArr.length,
+								icon: 'none',
+								mask: false
+							});
+							
+							var remoteIndexStart = _self.imageList.length - imagePathArr.length
+							var promiseWorkList = []
+							var keyname = (_self.fileKeyName ? _self.fileKeyName : 'upload-images')
+							var completeImages = 0
+							
+							for(let i=0; i<imagePathArr.length;i++){
+								promiseWorkList.push(new Promise((resolve, reject)=>{
+									let remoteUrlIndex = remoteIndexStart + i
+									uni.uploadFile({
+										url:_self.serverUrl,
+										fileType: 'image',
+										header: _self.header,
+										formData:_self.formData,
+										filePath: imagePathArr[i], 
+										name: keyname,
+										success: function(res){
+											if(res.statusCode === 200){
+												if(_self.isDestroyed){
+													return
+												}
+												
+												completeImages ++
+												
+												if(_self.showUploadProgress){
+													uni.showToast({
+														title: '上传进度:' + completeImages + '/' + imagePathArr.length,
+														icon: 'none',
+														mask: false,
+														duration: 500
+													});
+												}
+												console.log('success to upload image: ' + res.data)
+												resolve(res.data)
+											}else{
+												console.log('fail to upload image:'+res.data)
+												reject('fail to upload image:' + remoteUrlIndex)
+											}
+										},
+										fail: function(res){
+											console.log('fail to upload image:'+res)
+											reject('fail to upload image:' + remoteUrlIndex)
+										}
+									})
+								}))
+							}
+							Promise.all(promiseWorkList).then((result)=>{
+								if(_self.isDestroyed){
+									return
+								}
+								
+								for(let i=0; i<result.length;i++){
+									_self.imageList.push(result[i])
+								}
+								
+								_self.$emit('add', {
+									currentImages: imagePathArr,
+									allImages: _self.imageList
+								})
+								_self.$emit('input', _self.imageList)
+							})
+						}else{
+							for(let i=0; i<imagePathArr.length;i++){
+								_self.imageList.push(imagePathArr[i])
+							}
+							
+							_self.$emit('add', {
+								currentImages: imagePathArr,
+								allImages: _self.imageList
+							})
+							_self.$emit('input', _self.imageList)
+						}
+					}
+				})
+			},
+			deleteImage: function(e){
+				var imageIndex = e.currentTarget.dataset.index
+				var deletedImagePath = this.imageList[imageIndex]
+				this.imageList.splice(imageIndex, 1) 
+				
+				//检查删除图片的服务器地址是否设置,如果设置则调用API,在服务器端删除该图片
+				if(this.serverUrlDeleteImage){
+					uni.request({
+						url: this.serverUrlDeleteImage,
+						method: 'GET',
+						data: {
+							imagePath: deletedImagePath
+						},
+						success: res => {
+							console.log(res.data)
+						}
+					});
+				}
+				
+				this.$emit('delete',{
+					currentImage: deletedImagePath,
+					allImages: this.imageList
+				})
+				this.$emit('input', this.imageList)
+			},
+			previewImage: function(e){
+				var imageIndex = e.currentTarget.dataset.index
+				uni.previewImage({
+					current: this.imageList[imageIndex],
+					indicator: "number",
+					loop: "true",
+					urls:this.imageList
+				})
+			},
+			initImageBasePos: function(){
+				let paddingRate = 0.024
+				var _self = this
+				//计算图片基准位置
+				uni.getSystemInfo({
+					success: function(obj) {
+						let screenWidth = obj.screenWidth
+						let leftPadding = Math.ceil(paddingRate * screenWidth)
+						let imageWidth = Math.ceil((screenWidth - 2*leftPadding)/4)
+						
+						_self.imageBasePos.x0 = leftPadding
+						_self.imageBasePos.w = imageWidth
+						_self.imageBasePos.h = imageWidth
+					}
+				})
+			},
+			findOverlapImage: function(posX, posY){
+				let rows = Math.floor((posX-this.imageBasePos.x0)/this.imageBasePos.w)
+				let cols = Math.floor((posY-this.imageBasePos.y0)/this.imageBasePos.h)
+				let indx = cols*4 + rows
+				return indx
+			},
+			isDragging: function(indx){
+				return this.dragIndex === indx
+			},
+			start: function(e){
+				console.log(this.isDragable)
+				if(!this.isDragable){
+					return
+				}
+				this.dragIndex = e.currentTarget.dataset.index
+				this.moveImagePath = this.imageList[this.dragIndex]
+				this.showMoveImage = true
+				
+				//计算纵向图片基准位置
+				if(this.imageBasePos.y0 === -1){
+					this.initImageBasePos()
+					
+					let basePosY = Math.floor(this.dragIndex / 4) * this.imageBasePos.h
+					let currentImageOffsetTop = e.currentTarget.offsetTop
+					this.imageBasePos.y0 = currentImageOffsetTop - basePosY
+				}
+				
+				//设置选中图片当前左上角的坐标
+				this.moveLeft = e.target.offsetLeft
+				this.moveTop = e.target.offsetTop
+			},
+			move: function(e){
+				if(!this.isDragable){
+					return
+				}
+				const touch = e.touches[0]
+				this.targetImageIndex = this.findOverlapImage(touch.clientX, touch.clientY)
+				
+				//初始化deltaLeft/deltaTop
+				if(this.deltaLeft === 0){
+					this.deltaLeft = touch.clientX - this.moveLeft
+					this.deltaTop = touch.clientY - this.moveTop 
+				}
+				
+				//设置移动图片位置
+				this.moveLeft = touch.clientX - this.deltaLeft
+				this.moveTop = touch.clientY - this.deltaTop
+			},
+			stop: function(e){
+				if(!this.isDragable){
+					return
+				}
+				if(this.dragIndex !== null && this.targetImageIndex !== null){
+					if(this.targetImageIndex<0){
+						this.targetImageIndex = 0
+					}
+				
+					if(this.targetImageIndex>=this.imageList.length){
+						this.targetImageIndex = this.imageList.length-1
+					}
+					//交换图片
+					if(this.dragIndex !== this.targetImageIndex){
+						this.imageList[this.dragIndex] = this.imageList[this.targetImageIndex]
+						this.imageList[this.targetImageIndex] = this.moveImagePath
+					}
+				}
+				
+				this.dragIndex = null
+				this.targetImageIndex = null
+				this.deltaLeft = 0
+				this.deltaTop = 0
+				this.showMoveImage = false
+				
+				this.$emit('input', this.imageList)
+			}
+		}
+	}
+</script>
+
+<style>
+	.imageUploadContainer{
+		padding: 10upx 5upx;
+		margin: 10upx 5upx;
+	}
+	
+	.dragging{
+		transform: scale(1.2)
+	}
+	
+	.imageUploadList{
+		display: flex;
+		flex-wrap: wrap;
+	}
+	
+	.imageItem, .imageUpload{
+		width: 160upx;
+		height: 160upx;
+		margin: 10upx;
+	}
+	
+	.imageDel{
+		position: relative;
+		left: 120upx;
+		bottom: 165upx;
+		background-color: rgba(0,0,0,0.5);
+		width: 36upx;
+		text-align: center;
+		line-height: 35upx;
+		border-radius: 17upx;
+		color: white;
+		font-size: 30upx;
+		padding-bottom: 2upx;
+	}
+	
+	.imageItem image, .moveImage{
+		width: 160upx;
+		height: 160upx;
+		border-radius: 8upx;
+	}
+	
+	.imageUpload{
+		line-height: 130upx;
+		text-align: center;
+		font-size: 150upx;
+		color: #D9D9D9;
+		border: 1px solid #D9D9D9;
+		border-radius: 8upx;
+	}
+	
+	.moveImage{
+		position: absolute;
+	}
+</style>

+ 537 - 0
components/uniapp-nice-cropper/cropper.js

@@ -0,0 +1,537 @@
+const ABS = Math.abs
+const calcLen = (v) => { // distance between two coordinate 
+  return Math.sqrt(v.x * v.x + v.y * v.y)
+}
+const calcAngle = (a, b) => { // angle of the two vectors
+  var l = calcLen(a) * calcLen(b); var cosValue; var angle
+  if (l) {
+    cosValue = (a.x * b.x + a.y * b.y) / l
+    angle = Math.acos(Math.min(cosValue, 1))
+    angle = a.x * b.y - b.x * a.y > 0 ? -angle : angle
+    return angle * 180 / Math.PI
+  }
+  return 0
+}
+const generateCanvasId = () => { // generate a random string
+  const seeds = 'abcdefghijklmnopqrstuvwxyz'
+  const arr = seeds.split('').concat(seeds.toUpperCase().split('')).concat('0123456789'.split(''))
+  let m = arr.length; let i
+  while (m) {
+    i = Math.floor(Math.random() * m--)
+    const temp = arr[m]
+    arr[m] = arr[i]
+    arr[i] = temp
+  }
+  return arr.slice(0, 16).join('')
+}
+
+export default {
+  props: {
+    width: { // width of the container
+      type: [String, Number],
+      default: '100%'
+    },
+    height: { // height of the container
+      type: [String, Number],
+      default: '100%'
+    },
+    cutWidth: { // cutter width
+      type: [String, Number],
+      default: '50%'
+    },
+    cutHeight: { // cutter height
+      type: [String, Number],
+      default: 0
+    },
+    minWidth: { // minWidth of the cutter
+      type: Number,
+      default: 50
+    },
+    minHeight: { // minHeight of the cutter
+      type: Number,
+      default: 50
+    },
+    center: { // autoCenter
+      type: Boolean,
+      default: true
+    },
+    src: String, 
+    disableScale: Boolean, // disable to zoom
+    disableRotate: Boolean, 
+    disableTranslate: Boolean, 
+    disableCtrl: Boolean, // disable to resize the cutter
+    boundDetect: Boolean, // open boundary detection
+    freeBoundDetect: Boolean, // open boundary detection while doing rotation
+    keepRatio: Boolean, // keep the ratio of the cutter
+    disablePreview: Boolean, // disable preview after cutting
+    showCtrlBorder: Boolean, // show cutter border
+    resetCut: Boolean, // reset cut while img change
+    fit: {
+      type: Boolean,
+      default: true
+    },
+    imageCenter: Boolean, // auto center/middle for image
+    maxZoom: { // maximum scaling factor 
+      type: Number,
+      default: 10 // can not be Infinity in baidu-MiniProgram
+    },
+    minZoom: { // minimum scaling factor
+      type: Number,
+      default: 1
+    },
+    angle: { // initial angle of rotation
+      type: Number,
+      default: 0
+    },
+    zoom: { // initial scaling factor
+      type: Number,
+      default: 1
+    },
+    offset: { // initial offset relative to the cutter left border
+      type: Array,
+      default() {
+        return [0, 0]
+      }
+    },
+    background: {
+      type: String,
+      default: '#000'
+    },
+    canvasBackground: { // background for the exported image
+      type: String,
+      default: '#fff'
+    },
+    canvasZoom: {  // export multiples of the cutter size
+      type: Number,
+      default: 1
+    },
+    fileType: {
+      type: String,
+      default: 'png',
+      validator(t) {
+        return ['png', 'jpg'].includes(t)
+      }
+    },
+    quality: {
+      type: Number,
+      default: 1
+    },
+    maskType: { // type for mask
+      type: String,
+      default: "shadow"
+    },
+    circleView: Boolean // circle clip view
+  },
+  data() {
+    return {
+      transform: {
+        angle: 0,
+        translate: {
+          x: 0,
+          y: 0
+        },
+        zoom: 1
+      },
+      corner: {
+        left: 50,
+        right: 50,
+        bottom: 50,
+        top: 50
+      },
+      image: {
+        originWidth: 0,
+        originHeight: 0,
+        width: 0,
+        height: 0
+      },
+      ctrlWidth: 0,
+      ctrlHeight: 0,
+      view: false,
+      canvasId: ''
+    }
+  },
+  computed: {
+    transformMeta: function() {
+      const transform = this.transform
+      return `translate3d(${transform.translate.x}px, ${transform.translate.y}px, 0) rotate(${transform.angle}deg) scale(${transform.zoom})`
+    },
+    ctrlStyle: function() {
+      const corner = this.corner
+      let cssStr = `left: ${corner.left}px;top: ${corner.top}px;right: ${corner.right}px;bottom: ${corner.bottom }px;`
+      if(this.maskType !== 'outline') {
+        cssStr += `box-shadow: 0 0 0 50000rpx rgba(0,0,0, ${this.view ? 0.8 : 0.4})`
+      } else {
+        cssStr += `outline: rgba(0,0,0, ${this.view ? 0.8 : 0.4}) solid 5000px`
+      }
+      return cssStr
+    }
+  },
+  watch: {
+    src: function() {
+      if(this.resetCut) this.resetCutReact()
+      this.initImage()
+    }
+  },
+  created() {
+    this.canvasId = generateCanvasId()
+    uni.getSystemInfo().then(result => {
+      result = result[1] || {windowWidth: 375, windowHeight: 736}
+      this.ratio = result.windowWidth / 750
+      this.windowHeight = result.windowHeight
+      this.init()
+      this.initCanvas()
+    })
+  },
+  methods: {
+    toPx(str) {
+      if (str.indexOf('%') !== -1) {
+        return Math.floor(Number(str.replace('%', '')) / 100 * this.containerWidth)
+      }
+      if (str.indexOf('rpx') !== -1) {
+        return Math.floor(Number(str.replace('rpx', '')) * this.ratio)
+      }
+      return Math.floor(Number(str.replace('px', '')))
+    },
+    initCanvas() {
+			// #ifdef MP-ALIPAY
+			const context = uni.createSelectorQuery()
+			// #endif
+			// #ifndef MP-ALIPAY
+			const context = uni.createSelectorQuery().in(this)
+			// #endif
+      
+      // get contianer size
+      context.select('.nice-cropper').boundingClientRect()
+      context.exec(res => {
+        this.containerWidth = res[0].width
+        this.containerHeight = res[0].height
+        this.initCut()
+      })
+    },
+    resetCutReact() {// init size and position of the cutter
+      this.ctrlWidth = Math.min(this.toPx(this.cutWidth), this.containerWidth)
+      if (this.cutHeight) {
+        this.ctrlHeight = Math.min(this.toPx(this.cutHeight), this.containerHeight)
+      } else { // 默认为正方形
+        this.ctrlHeight = Math.min(this.ctrlWidth, this.containerHeight)
+      }
+      const cornerStartX = this.center ? Math.floor((this.containerWidth - this.ctrlWidth) / 2) : 0
+      const cornerStartY = this.center ? Math.floor((this.containerHeight - this.ctrlHeight) / 2) : 0
+      this.cutRatio = this.ctrlHeight / this.ctrlWidth
+      this.corner = {
+        left: cornerStartX,
+        right: this.containerWidth - this.ctrlWidth - cornerStartX,
+        top: cornerStartY,
+        bottom: this.containerHeight - this.ctrlHeight - cornerStartY
+      }
+    },
+    initCut() { 
+      this.resetCutReact()
+      this.initImage()
+    },
+    async initImage() {
+      if (!this.src) return
+
+      const [err, res] = await uni.getImageInfo({
+        src: this.src
+      })
+
+      if(err) {
+        this.$emit("error", err)
+      } else {
+        this.$emit('load', res)
+      }   
+
+      // init image size
+      this.image.originWidth = err ? this.ctrlWidth : res.width
+      this.image.originHeight = err ? this.ctrlHeight : res.height
+      this.image.width = this.fit ? this.ctrlWidth : this.image.originWidth
+      this.image.height = err ? this.ctrlHeight : res.height / res.width * this.image.width
+      this.img = res.path
+
+      const offset = [0, 0]
+      if(this.imageCenter) {
+        offset[0] = (this.ctrlWidth - this.image.width) / 2
+        offset[1] = (this.ctrlHeight - this.image.height) / 2
+      }
+      offset[0] += this.offset[0] || 0
+      offset[1] += this.offset[1] || 0
+      
+      this.setTranslate(offset)
+      this.setZoom(this.zoom)
+      this.transform.angle = this.freeBoundDetect || !this.disableRotate ? this.angle : 0
+
+      this.setBoundary() // boundary detect
+      this.preview() // preview
+      this.draw()
+    },
+    init() {
+      this.pretouch = {}
+      this.handles = {}
+      this.preVector = {
+        x: 0,
+        y: 0
+      }
+      this.distance = 30
+      this.touch = {}
+      this.movetouch = {}
+      this.cutMode = false
+      this.params = {
+        zoom: 1,
+        deltaX: 0,
+        deltaY: 0,
+        diffX: 0,
+        diffY: 0,
+        angle: 0
+      }
+    },
+    start(e) {
+      if(!this.src) e.preventDefault() 
+      const point = e.touches ? e.touches[0] : e
+      const touch = this.touch
+      const now = Date.now()
+      touch.startX = point.pageX
+      touch.startY = point.pageY
+      touch.startTime = now
+      this.doubleTap = false
+      this.view = false
+      clearTimeout(this.previewTimer)
+      if (e.touches.length > 1) {
+        var point2 = e.touches[1]
+        this.preVector = {
+          x: point2.pageX - this.touch.startX,
+          y: point2.pageY - this.touch.startY
+        }
+        this.startDistance = calcLen(this.preVector)
+      } else {
+        let pretouch = this.pretouch
+        this.doubleTap = this.pretouch.time && now - this.pretouch.time < 300 && ABS(touch.startX - pretouch.startX) < 30 && ABS(touch.startY - pretouch.startY) < 30 && ABS(touch.startTime - pretouch.time) < 300
+        pretouch = { // reserve the last touch
+          startX: this.touch.startX,
+          startY: this.touch.startY,
+          time: this.touch.startTime
+        }
+      }
+    },
+    move(e) {
+      if(!this.src) return 
+      const point = e.touches ? e.touches[0] : e
+      if (e.touches.length > 1) { // multi touch
+        const point2 = e.touches[1]
+        const v = {
+          x: point2.pageX - point.pageX,
+          y: point2.pageY - point.pageY
+        }
+
+        if (this.preVector.x !== null) {
+          if (this.startDistance) { // zoom
+            const len = calcLen(v)
+            this.params.zoom = calcLen(v) / this.startDistance
+            this.startDistance = len
+            this.cutMode && !this.disableCtrl ? this.setCut() : !this.disableScale && this.pinch()
+          }
+          // rotate
+          this.params.angle = calcAngle(v, this.preVector)
+          this.cutMode && !this.disableCtrl ? this.setCut() : !this.disableRotate && this.rotate()
+        }
+        this.preVector.x = v.x
+        this.preVector.y = v.y
+      } else { // translate
+        const diffX = point.pageX - this.touch.startX
+        const diffY = point.pageY - this.touch.startY
+        this.params.diffY = diffY
+        this.params.diffX = diffX
+        if (this.movetouch.x) {
+          this.params.deltaX = point.pageX - this.movetouch.x
+          this.params.deltaY = point.pageY - this.movetouch.y
+        } else {
+          this.params.deltaX = this.params.deltaY = 0
+        }
+        if (ABS(diffX) > 30 || ABS(diffY) > 30) {
+          this.doubleTap = false
+        }
+        this.cutMode && !this.disableCtrl ? this.setCut() : !this.disableTranslate && this.translate()
+        this.movetouch.x = point.pageX
+        this.movetouch.y = point.pageY
+      }
+      !this.cutMode && this.setBoundary()
+      if (e.touches.length > 1) {
+        e.preventDefault()
+      }
+    },
+    end() {
+      this.doubleTap && this.$emit('doubleTap')
+      this.cutMode && this.setBoundary()
+      this.init()
+      !this.disablePreview && this.preview()
+      this.draw()
+    },
+    translate() {
+      const transform = this.transform.translate
+      const meta = this.params
+      transform.x += meta.deltaX
+      transform.y += meta.deltaY
+    },
+    pinch() {
+      this.transform.zoom *= this.params.zoom
+    },
+    rotate() {
+      this.transform.angle += this.params.angle
+    },
+    setZoom(scale) {
+      scale = Math.min(Math.max(Number(scale) || 1, this.minZoom), this.maxZoom)
+      this.transform.zoom = scale
+    },
+    setTranslate(offset) {
+      if(Array.isArray(offset)) {
+        const x = Number(offset[0])
+        const y = Number(offset[1])
+        this.transform.translate.x = isNaN(x) ? this.transform.translate.x : this.corner.left + x
+        this.transform.translate.y = isNaN(y) ? this.transform.translate.y : this.corner.top + y
+      }
+    },
+    setRotate(angle) {
+      this.transform.angle = Number(angle) || 0
+    },
+    setTransform(x, y, angle, scale) {
+      this.setTranslate([x, y])
+      this.setZoom(scale)
+      this.setRotate(angle)
+    },
+    setCutMode(type) {
+      if(!this.src) return 
+      this.cutMode = true
+      this.cutDirection = type
+    },
+    setCut() {
+      const corner = this.corner
+      const meta = this.params
+      this.setMeta(this.cutDirection, meta) // correct cutter position
+      if (this.keepRatio) {
+        if (this.cutDirection === 'lt' || this.cutDirection === 'rb') {
+          meta.deltaY = meta.deltaX * this.cutRatio
+        } else {
+          meta.deltaX = meta.deltaY / this.cutRatio
+        }
+      }
+      switch (this.cutDirection) {
+        case 'lt':
+          corner.top += meta.deltaY
+          corner.left += meta.deltaX
+          break
+        case 'rt':
+          corner.top += meta.deltaY
+          corner.right -= this.keepRatio ? -meta.deltaX : meta.deltaX
+          break
+        case 'rb':
+          corner.right -= meta.deltaX
+          corner.bottom -= meta.deltaY
+          break
+        case 'lb':
+          corner.bottom -= meta.deltaY
+          corner.left += this.keepRatio ? -meta.deltaX : meta.deltaX
+          break
+      }
+      this.ctrlWidth = this.containerWidth - corner.left - corner.right
+      this.ctrlHeight = this.containerHeight - corner.top - corner.bottom
+    },
+    setMeta(direction, meta) {
+      const {ctrlWidth, ctrlHeight, minWidth, minHeight } = this
+      switch(direction) {
+        case 'lt':
+          if(meta.deltaX > 0 || meta.deltaY > 0) {
+            meta.deltaX = Math.min(meta.deltaX, ctrlWidth - minWidth)
+            meta.deltaY = Math.min(meta.deltaY, ctrlHeight - minHeight)
+          }
+          break
+        case 'rt':
+          if(meta.deltaX < 0 || meta.deltaY > 0) {
+            meta.deltaX = Math.max(meta.deltaX, minWidth - ctrlWidth)
+            meta.deltaY = Math.min(meta.deltaY, ctrlHeight - minHeight)
+          }
+          break
+        case 'rb':
+          if(meta.deltaX < 0 || meta.deltaY < 0) {
+            meta.deltaX = Math.max(meta.deltaX, minWidth - ctrlWidth)
+            meta.deltaY = Math.max(meta.deltaY, minHeight - ctrlHeight)
+          }
+          break
+        case 'lb':
+          if(meta.deltaX > 0 || meta.deltaY < 0) {
+            meta.deltaX = Math.min(meta.deltaX, ctrlWidth - minWidth)
+            meta.deltaY = Math.max(meta.deltaY, minHeight - ctrlHeight)
+          }
+          break
+      }
+
+    },
+    setBoundary() {
+      let zoom = this.transform.zoom
+      zoom = zoom < this.minZoom ? this.minZoom : (zoom > this.maxZoom ? this.maxZoom : zoom)
+      this.transform.zoom = zoom
+      if (!this.boundDetect || !this.disableRotate && !this.freeBoundDetect) return true
+      const translate = this.transform.translate
+      const corner = this.corner
+      const minX = corner.left - this.image.width + this.ctrlWidth - this.image.width * (zoom - 1) / 2
+      const maxX = corner.left + this.image.width * (zoom - 1) / 2
+      const minY = corner.top - this.image.height + this.ctrlHeight - this.image.height * (zoom - 1) / 2
+      const maxY = corner.top + this.image.height * (zoom - 1) / 2
+      translate.x = Math.floor(translate.x < minX ? minX : (translate.x > maxX ? maxX : translate.x))
+      translate.y = Math.floor(translate.y < minY ? minY : (translate.y > maxY ? maxY : translate.y))
+    },
+    preview() {
+      clearTimeout(this.previewTimer)
+      this.previewTimer = setTimeout(() => {
+        this.view = true
+      }, 500)
+    },
+    draw() {
+      // #ifdef MP-ALIPAY
+      const context = uni.createCanvasContext(this.canvasId)
+      // #endif
+      // #ifndef MP-ALIPAY
+      const context = uni.createCanvasContext(this.canvasId, this)
+      // #endif
+      const transform = this.transform
+      const corner = this.corner
+      const canvasZoom = this.canvasZoom
+      const img = this.image
+      context.save()
+      context.setFillStyle(this.canvasBackground)
+      this.$emit('beforeDraw', context, transform) // beforeDraw hook
+
+      const zoom = transform.zoom
+      context.fillRect(0, 0, this.ctrlWidth * canvasZoom, this.ctrlHeight * canvasZoom) // clear canvas
+      context.translate((transform.translate.x - corner.left + img.width / 2) *canvasZoom, (transform.translate.y - corner.top + img.height / 2) * canvasZoom) // translate the canvas's orgin to the image center
+      context.rotate(transform.angle * Math.PI / 180)
+      context.translate(-img.width * zoom * 0.5 * canvasZoom, -img.height * zoom * 0.5 * canvasZoom)
+      context.drawImage(this.img, 0, 0, img.width * zoom * canvasZoom, img.height * zoom * canvasZoom)
+      context.restore()
+      this.$emit('afterDraw', context, {
+        width: this.ctrlWidth * canvasZoom,
+        height: this.ctrlHeight * canvasZoom
+      }) // afterDraw hook
+      context.draw(false, () => {
+        uni.canvasToTempFilePath({
+          canvasId: this.canvasId,
+          quality: this.quality || 1,
+          fileType: this.fileType,
+          success: (res) => {
+            this.$emit('cropped', res.tempFilePath, {
+              originWidth: this.image.originWidth,
+              originHeight: this.image.originHeight,
+              width: this.ctrlWidth * canvasZoom,
+              height: this.ctrlHeight * canvasZoom,
+              scale: zoom,
+              translate: {
+                x: transform.translate.x,
+                y: transform.translate.y
+              },
+              rotate: transform.angle
+            }) // draw callback
+          }
+        }, this)
+      })
+    }
+  }
+}

+ 75 - 0
components/uniapp-nice-cropper/cropper.vue

@@ -0,0 +1,75 @@
+<template>
+  <view class="nice-cropper" :style="{height: height, width: width, background: background}" @touchstart="start" @touchmove.stop="move" @touchcancel="end" @touchend="end">
+    <image class="nice-cropper__image" :src="src" :style="{transform: transformMeta, width: image.width + 'px', height: image.height + 'px'}" />
+    <view class="nice-cropper__ctrls" :class="{'nice-cropper__ctrls--view' : view, 'nice-cropper__ctrls--border': showCtrlBorder, 'nice-cropper__ctrls--circle': view && circleView && maskType !== 'outline'}" :style="ctrlStyle">
+      <view class="nice-cropper__corner nice-cropper__corner--lt" @touchstart="setCutMode('lt')" />
+      <view class="nice-cropper__corner nice-cropper__corner--rt" @touchstart="setCutMode('rt')" />
+      <view class="nice-cropper__corner nice-cropper__corner--rb" @touchstart="setCutMode('rb')" />
+      <view class="nice-cropper__corner nice-cropper__corner--lb" @touchstart="setCutMode('lb')" />
+    </view>
+    <canvas v-if="canvasId" :id="canvasId" :canvas-id="canvasId" style="position: absolute;left:-500000px;top: -500000px" :style="{width: ctrlWidth * canvasZoom+'px', height: ctrlHeight * canvasZoom + 'px'}" />
+  </view>
+</template>
+<script src="./cropper.js"></script>
+<style>
+.nice-cropper {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background: #000;
+  overflow: hidden;
+}
+.nice-cropper__image {
+  position: absolute;
+  left: 0;
+  top: 0;
+  transform-origin: 50% 50%;
+}
+.nice-cropper__corner {
+  width: 30rpx;
+  height: 30rpx;
+  position: absolute;
+}
+.nice-cropper__corner::after {
+  position: absolute;
+  left: -5px;
+  right: -5px;
+  bottom: -5px;
+  top: -5px;
+  content: '';
+}
+.nice-cropper__ctrls {
+  position: absolute;
+  box-shadow: inset 0 0 10rpx 0 rgba(0,0,0,.3);
+}
+.nice-cropper__ctrls--circle {
+  border-radius: 50%;
+}
+.nice-cropper__ctrls--border {
+  border: 2rpx solid #fff;
+}
+.nice-cropper__corner--lt {
+  left: 0;
+  top: 0;
+  border-top: 4rpx solid #FFF;
+  border-left: 4rpx solid #FFF;
+}
+.nice-cropper__corner--rt {
+  right: 0;
+  top: 0;
+  border-top: 4rpx solid #FFF;
+  border-right: 4rpx solid #FFF;
+}
+.nice-cropper__corner--rb {
+  right: 0;
+  bottom: 0;
+  border-right: 4rpx solid #FFF;
+  border-bottom: 4rpx solid #FFF;
+}
+.nice-cropper__corner--lb {
+  left: 0;
+  bottom: 0;
+  border-left: 4rpx solid #FFF;
+  border-bottom: 4rpx solid #FFF;
+}
+</style>

+ 1 - 1
package-lock.json

@@ -1,5 +1,5 @@
 {
-  "name": "i2School-employee-2.0",
+  "name": "i2-school-employee-uniapp",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {

+ 18 - 6
package.json

@@ -1,7 +1,19 @@
 {
-  "dependencies": {
-    "vue-i18n": "^8.26.7",
-    "vuex": "^3.6.2",
-    "vuex-persistedstate": "^4.1.0"
-  }
-}
+    "id": "htz-image-upload",
+    "name": "图片 视频上传 预览 【简单好上手】",
+    "version": "0.1.6",
+    "description": "可选择添加/删除图片/视频,可以预览,可以压缩(包括H5压缩)(解决了安卓10问题),支持上传到服务器后台,能满足大部分场景",
+    "keywords": [
+        "上传",
+        "压缩图片",
+        "预览图片",
+        "删除图片",
+        "视频上传"
+    ],
+    "dcloudext": {
+        "category": [
+            "前端组件",
+            "通用组件"
+        ]
+    }
+}

+ 6 - 0
pages.json

@@ -30,6 +30,12 @@
 			"style": {
 				"navigationBarTitleText": "设置"
 			}
+		},
+		{
+			"path": "pages/test/upload",
+			"style": {
+				"navigationBarTitleText": "上传"
+			}
 		}
 	],
 	"globalStyle": {

+ 18 - 2
pages/calendar/index.vue

@@ -10,6 +10,14 @@
 		</u-navbar>
 		
 		 <uni-calendar insert foldAble :selected="selected" @change="change" />
+		 
+		 <u-gap height="120"/>
+		 <button class="cu-btn block bg-red margin-tb-sm lg radio" @click="uploadAction">
+		 	<text> 上传 </text>
+		 </button>
+		 <button class="cu-btn block bg-red margin-tb-sm lg radio" @click="monitoringBtnClick">
+		 	<text> 监控 </text>
+		 </button>
 		
 	</view>
 </template>
@@ -57,13 +65,21 @@
 				})
 			},
 			previousBtnDidClick(){
-				console.log('previousBtnDidClick');
+				console.log('previousBtnDidClick')
 			},
 			nextBtnDidClick(){
-				console.log('nextBtnDidClick');
+				console.log('nextBtnDidClick')
 			},
 			change(e) {
 				console.log(e)
+			},
+			uploadAction() {
+				uni.navigateTo({url: '../test/upload'})
+			},
+			monitoringBtnClick() {
+				uni.navigateTo({
+					url: '../launch/index'
+				})
 			}
 		}
 	}

+ 1 - 1
pages/mine/setting.vue

@@ -39,7 +39,7 @@
 				]
 			};
 		},
-		name: 'tabbar-mine',
+		name: 'tabbar-setting',
 		computed: {
 			...mapState(['userInfo'])
 		},

+ 70 - 0
pages/test/upload.vue

@@ -0,0 +1,70 @@
+<template>
+	<view class="padding-lr">
+		<view v-for="(section,i) in mineItems" :key="i">
+			<u-gap height="15"/>
+			<view class="cu-card padding radius shadow bg-white">
+				<text class="text-bold padding-sm"> {{ section.title }} </text>
+				<htz-image-upload :max="3" v-model="section.files" mediaType="all" @chooseSuccess="chooseSuccess" />
+			</view>
+		</view>	
+	</view>
+</template>
+
+<script>
+	import htzImageUpload from '@/components/htz-image-upload/htz-image-upload.vue'
+	export default {
+		components:{ htzImageUpload },
+		data() {
+			return {
+				mineItems: [
+					{'index': 0, 'title': '课前', files: []},
+					// {'index': 1, 'title': '课中', files: []},
+					// {'index': 2, 'title': '课后', files: []},
+				]
+			};
+		},
+		methods: {
+			chooseSuccess(res){
+				this.mineItems[0].files.push(res)
+	        },
+		}
+	}
+</script>
+
+
+<style lang="scss">
+	.mine {
+		width: 100%;
+		position: absolute;
+		display: flex;
+		flex-direction: column;
+		height: 100%;
+		.mine-top {
+			width: 100%;
+			.mine-bg {
+				width: 100%;
+				height: 55vw;
+			}
+		}
+		.mine-content {
+			position: absolute;
+			top: 20vw;
+			width: 90%;
+			left: 5%;
+			.mine-card{
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+				align-items: center;
+				.user-card-top {
+					position: absolute;
+					top: 0;
+					left: 0;
+					align-self: flex-start;
+					width: 40px;
+					height: 20px;
+				}
+			}
+		}
+	}
+</style>

+ 2 - 2
unpackage/dist/dev/app-plus/app-config-service.js

@@ -1,8 +1,8 @@
 
 var isReady=false;var onReadyCallbacks=[];
 var isServiceReady=false;var onServiceReadyCallbacks=[];
-var __uniConfig = {"pages":["pages/launch/index","pages/login/index","pages/index/index","pages/login/env_page","pages/mine/setting"],"window":{"navigationBarTextStyle":"black","navigationBarTitleText":"uni-app","navigationBarBackgroundColor":"#F8F8F8","backgroundColor":"#F8F8F8","enablePullDownRefresh":true},"nvueCompiler":"uni-app","nvueStyleCompiler":"uni-app","renderer":"auto","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":false},"appname":"i2School-employee","compilerVersion":"3.3.5","entryPagePath":"pages/launch/index","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000}};
-var __uniRoutes = [{"path":"/pages/launch/index","meta":{"isQuit":true},"window":{"navigationStyle":"custom"}},{"path":"/pages/login/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/index/index","meta":{},"window":{"navigationStyle":"custom","enablePullDownRefresh":true}},{"path":"/pages/login/env_page","meta":{},"window":{"navigationBarTitleText":"开发者工具"}},{"path":"/pages/mine/setting","meta":{},"window":{"navigationBarTitleText":"设置"}}];
+var __uniConfig = {"pages":["pages/launch/index","pages/login/index","pages/index/index","pages/login/env_page","pages/mine/setting","pages/test/upload"],"window":{"navigationBarTextStyle":"black","navigationBarTitleText":"uni-app","navigationBarBackgroundColor":"#F8F8F8","backgroundColor":"#F8F8F8","enablePullDownRefresh":true},"nvueCompiler":"uni-app","nvueStyleCompiler":"uni-app","renderer":"auto","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":false},"appname":"i2School","compilerVersion":"3.3.5","entryPagePath":"pages/launch/index","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000}};
+var __uniRoutes = [{"path":"/pages/launch/index","meta":{"isQuit":true},"window":{"navigationStyle":"custom"}},{"path":"/pages/login/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/index/index","meta":{},"window":{"navigationStyle":"custom","enablePullDownRefresh":true}},{"path":"/pages/login/env_page","meta":{},"window":{"navigationBarTitleText":"开发者工具"}},{"path":"/pages/mine/setting","meta":{},"window":{"navigationBarTitleText":"设置"}},{"path":"/pages/test/upload","meta":{},"window":{"navigationBarTitleText":"上传"}}];
 __uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
 __uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
 service.register("uni-app-config",{create(a,b,c){if(!__uniConfig.viewport){var d=b.weex.config.env.scale,e=b.weex.config.env.deviceWidth,f=Math.ceil(e/d);Object.assign(__uniConfig,{viewport:f,defaultFontSize:Math.round(f/20)})}return{instance:{__uniConfig:__uniConfig,__uniRoutes:__uniRoutes,global:void 0,window:void 0,document:void 0,frames:void 0,self:void 0,location:void 0,navigator:void 0,localStorage:void 0,history:void 0,Caches:void 0,screen:void 0,alert:void 0,confirm:void 0,prompt:void 0,fetch:void 0,XMLHttpRequest:void 0,WebSocket:void 0,webkit:void 0,print:void 0}}}});

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 3 - 3
unpackage/dist/dev/app-plus/app-service.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 250 - 219
unpackage/dist/dev/app-plus/app-view.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
unpackage/dist/dev/app-plus/manifest.json


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác