baijunde 1 year ago
parent
commit
96aedd9b10

+ 16 - 1
common/router/modules/routes.js

@@ -54,7 +54,14 @@ const routes = [{
 			title: '主页',
 		},
 	},
-	
+	{
+		//注意:path必须跟pages.json中的地址对应,最前面别忘了加'/'哦
+		path: '/pages/index/userControl',
+		name: 'userControl',
+		meta: {
+			title: '用户管理',
+		},
+	},
 	{
 		//注意:path必须跟pages.json中的地址对应,最前面别忘了加'/'哦
 		path: '/pages/index/full',
@@ -127,6 +134,14 @@ const routes = [{
 			title: '预约规则',
 		},
 	},
+	{
+		//注意:path必须跟pages.json中的地址对应,最前面别忘了加'/'哦
+		path: '/pages/product/salesUser',
+		name: 'salesUser',
+		meta: {
+			title: '销量统计',
+		},
+	},
 	{
 		//注意:path必须跟pages.json中的地址对应,最前面别忘了加'/'哦
 		path: '/pages/evaluate/evaluate',

+ 2 - 2
common/service/config.service.js

@@ -2,10 +2,10 @@ let BASE_URL = ''
 
 
 if (process.env.NODE_ENV == 'development') {
-	BASE_URL = 'https://test.baoxianzhanggui.com/locallive-java' // 测试环境
+	// BASE_URL = 'https://test.baoxianzhanggui.com/locallive-java' // 测试环境
 	// BASE_URL = 'http://192.168.0.36:8080/jeecg-boot' // 开发环境
     // BASE_URL = 'http://192.168.0.52:8081/jeecg-boot' // 开发环境
-		// BASE_URL = 'http://192.168.0.42:8080/jeecg-boot' // 开发环境
+		BASE_URL = 'http://192.168.0.42:8080/jeecg-boot' // 开发环境
 		 // BASE_URL = 'http://192.168.0.103:8080/jeecg-boot' // 开发环境
 
 } else {

+ 8 - 0
pages.json

@@ -34,6 +34,10 @@
 			"path": "pages/index/index",
 			"style": {}
 		},
+		{
+			"path": "pages/index/userControl",
+			"style": {}
+		},
 		{
 			"path": "pages/order/index",
 			"style": {}
@@ -111,6 +115,10 @@
 			"path": "pages/product/checkMap",
 			"style": {}
 		},
+		{
+			"path": "pages/product/salesUser",
+			"style": {}
+		},
 		{
 			"path": "pages/evaluate/evaluate",
 			"style": {

+ 15 - 2
pages/index/index.vue

@@ -21,7 +21,7 @@
 				<view class="item">
 					<span>{{income}}</span>收益
 				</view>
-				<view class="item">
+				<view class="item" @click="goUser">
 					<span>{{newCustomer}}</span>新客
 				</view>
 			</view>
@@ -98,6 +98,12 @@
 			NavChange: function(e) {
 				this.PageCur = e.currentTarget.dataset.cur
 			},
+			//新客
+			goUser(){
+				uni.navigateTo({
+					url: '/pages/index/userControl'
+				})
+			},
 			//扫码核销
 			scan() {
 				uni.navigateTo({
@@ -124,7 +130,7 @@
 				this.$http.get("/merchant/app/home/todayData?merchantId=" + this.merchantId, {}).then(res => {
 					if (res.data.code == 200) {
 						this.income = res.data.result.income
-						this.newCustomer = res.data.result.newCustomer
+						// this.newCustomer = res.data.result.newCustomer
 						this.numberOrders = res.data.result.numberOrders
 					}
 				})
@@ -138,6 +144,12 @@
 					// 	this.numberOrders = res.data.result.numberOrders
 					}
 				})
+			},
+			getNum() {
+				let data = {}
+				this.$http.post('/newCustomer/count', data).then(res => {
+					this.newCustomer = res.data.result.newCustomerCount
+				})
 			}
 		},
 		mounted() {
@@ -154,6 +166,7 @@
 					that.merchantId = res.data.merchantId
           that.gettodayData()
           that.getMessage()
+					that.getNum()
 				}
 			});
 		}

+ 254 - 0
pages/index/userControl.vue

@@ -0,0 +1,254 @@
+<template>
+	<view class="user">
+		<view class="fix">
+			<view class="head_top">
+				<view class="top"></view>
+				<view class="head">
+					<span class="cuIcon-back" @click="back"></span>
+					<view class="case">
+						用户管理
+					</view>
+				</view>
+			</view>
+			<view class="type">
+				<view class="type_item" v-for="(item,index) in user" :key="index" :class="currentIndex == index?'active':''"
+					@click="change(item,index)">
+					{{item.name}}  
+					<span>({{item.num}}) </span>
+				</view>
+			</view>
+		</view>
+		<mescroll-uni class="list" :fixed="false" ref="mescrollRef" @up="getNewList" @init="mescrollInit" :use="false"
+			v-if="currentIndex == 0">
+
+		</mescroll-uni>
+		<mescroll-uni class="list" :fixed="false" ref="mescrollRef" @up="getAllList" @init="mescrollInit" :use="false"
+			v-if="currentIndex == 1">
+			<view class="item" v-for="(item,index) in allList" :key="index">
+				<view class="img">
+					<image :src="item.userAvatar?item.userAvatar:'/static/product/user.png'" mode=""></image>
+				</view>
+				<view class="right">
+					{{item.nickName}}<br />
+					<span>{{item.phone}}</span>
+				</view>
+			</view>
+		</mescroll-uni>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				user: [{
+						name: '新客',
+						num: '0'
+					},
+					{
+						name: '合计',
+						num: '0'
+					}
+				],
+				currentIndex: 0,
+				upOption: {
+					page: {
+						num: 0,
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				newList: [],
+				allList: []
+			}
+		},
+		methods: {
+			//返回
+			back() {
+				uni.navigateBack({
+					delta: 1
+				});
+			},
+			change(item, index) {
+				this.currentIndex = index
+				let page = {
+					size: 10,
+					num: 1,
+				}
+				this.currentIndex == 0 ?this.allList=[]:this.newList = []
+				this.currentIndex == 0 ? this.getNewList(page) : this.getAllList(page)
+
+			},
+			mescrollInit(mescroll) {
+				this.mescroll = mescroll;
+			},
+			getNewList(page) {
+				let data = {
+					type: 1,
+					pageSize: page.size,
+					pageNo: page.num,
+				}
+				this.$http.post('/newCustomer/list', data).then(res => {
+					console.log(res)
+					this.newList = this.newList.concat(res.data.result.records)
+					this.mescroll.endSuccess(res.data.result.records.length, res.data.result.total);
+				})
+			},
+			getAllList(page) {
+				let data = {
+					type: 2,
+					pageSize: page.size,
+					pageNo: page.num,
+				}
+				this.$http.post('/newCustomer/list', data).then(res => {
+					this.allList = this.allList.concat(res.data.result.records)
+					this.mescroll.endSuccess(res.data.result.records.length, res.data.result.total);
+				})
+			},
+			getNum() {
+				let data = {}
+				this.$http.post('/newCustomer/count', data).then(res => {
+					this.user[0].num = res.data.result.newCustomerCount
+					this.user[1].num = res.data.result.totalCustomerCount
+				})
+			}
+		},
+		mounted() {
+			this.mescroll.optDown.use = false
+			this.getNum()
+			// this.getList()
+		},
+	}
+</script>
+
+<style scoped lang="scss">
+	.user {
+		width: 100%;
+		height: 100%;
+		background: #F7F8FC;
+		display: flex;
+		flex-direction: column;
+		justify-content: space-between;
+		position: fixed;
+
+		.fix {
+			.head_top {
+				// background: linear-gradient( 90deg, #DFF9FF 4%, #E6F8F8 67%, #DDF1FA 100%), linear-gradient( 0, #FFFFFF 0%, rgba(247,248,252,0) 100%);
+				background:
+					linear-gradient(360deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%),
+					linear-gradient(90deg, #DFF9FF 4%, #E6F8F8 67%, #DDF1FA 100%);
+
+				/*#ifdef APP-PLUS*/
+				.top {
+					width: 100%;
+					height: var(--status-bar-height);
+				}
+
+				/*#endif*/
+				.head {
+					width: 100%;
+					height: 112rpx;
+					padding: 20rpx;
+					display: flex;
+					align-items: center;
+					position: relative;
+
+					.cuIcon-back {
+						font-size: 40rpx;
+						margin-right: 40rpx;
+					}
+
+					.case {
+						position: absolute;
+						left: 50%;
+						transform: translateX(-50%);
+						font-size: 36rpx;
+						font-weight: 600;
+					}
+				}
+			}
+
+			.type {
+				display: flex;
+				justify-content: space-around;
+				align-items: center;
+				height: 79rpx;
+				border-top: 1px solid #EEEEEE;
+				border-bottom: 1px solid #EEEEEE;
+
+				.type_item {
+					font-weight: normal;
+					font-size: 28rpx;
+					color: #666666;
+					span{
+						margin-left: 20rpx;
+					}
+				}
+
+				.active {
+					font-size: 28rpx;
+					color: #333333;
+					font-weight: 600;
+					position: relative;
+				}
+
+				.active::after {
+					content: "";
+					position: absolute;
+					bottom: -10rpx;
+					left: 50%;
+					transform: translateX(-50%);
+					width: 44rpx;
+					height: 6rpx;
+					background-color: #2D88F4;
+					border-radius: 7rpx;
+				}
+			}
+		}
+
+		.list {
+			background-color: transparent;
+			padding: 16rpx 32rpx;
+			overflow-y: auto;
+
+			.item {
+				width: 100%;
+				height: 128rpx;
+				padding: 20rpx;
+				background: #FFFFFF;
+				border-radius: 16rpx;
+				margin-bottom: 16rpx;
+				display: flex;
+				align-items: center;
+
+				.img {
+					margin-right: 16rpx;
+
+					image {
+						width: 80rpx;
+						height: 80rpx;
+					}
+				}
+
+				.right {
+					font-size: 28rpx;
+					color: #000000;
+					font-weight: 600;
+					height: 100%;
+					display: flex;
+					flex-direction: column;
+					justify-content: space-between;
+
+					span {
+						font-size: 24rpx;
+						color: #8A91A8;
+						font-weight: 400;
+					}
+				}
+			}
+		}
+	}
+</style>

+ 13 - 6
pages/product/index.vue

@@ -66,7 +66,7 @@
 					<view class="data">
 						<span>{{item.productName}}</span>
 						<!-- <span>售卖时间:{{item.time}}</span> -->
-						<view class="repertory">库存:{{item.stockQuantity}} <span></span> 销量:{{item.soldQuantity}}</view>
+						<view class="repertory">库存:{{item.stockQuantity}} <span @click.stop="goUser(item)">销量:{{item.soldQuantity}}</span> </view>
 						<view class="price">
 							<span style="font-size: 22rpx;margin: 0px;">¥</span>{{item.soldPrice}}
 							<span v-if="item.categoryId == 1945013773786701826 ">~</span>
@@ -237,6 +237,13 @@
 					url: '/pages/index/index'
 				})
 			},
+			//销量页面
+			goUser(item){
+				console.log(item)
+				uni.navigateTo({
+					url: '/pages/product/salesUser?productName='+item.productName+'&productImage='+item.productImage+'&productId='+item.productId
+				})
+			},
 			handleClear() {
 				this.getProductList()
 				// 这里可以添加其他逻辑
@@ -761,11 +768,11 @@
 							font-weight: 400;
 
 							span {
-								display: block;
-								width: 1rpx;
-								height: 20rpx;
-								background: #d9d9d9;
-								margin: 0 12rpx;
+								// display: block;
+								// width: 1rpx;
+								// height: 20rpx;
+								// background: #d9d9d9;
+								margin-left:  12rpx;
 							}
 						}
 

+ 220 - 0
pages/product/salesUser.vue

@@ -0,0 +1,220 @@
+<template>
+	<view class="sales">
+		<view class="fix">
+			<view class="head_top">
+				<view class="top"></view>
+				<view class="head">
+					<span class="cuIcon-back" @click="back"></span>
+					<view class="case">
+						<image :src="productImage" alt="" /> {{productName}}
+					</view>
+				</view>
+			</view>
+			<view class="type">
+				<view class="type_item">
+					<image src="/static/product/sales.png" mode=""></image>
+					{{salesNum}}
+				</view>
+				<view class="line"></view>
+				<view class="type_item">
+					<image src="/static/product/num.png" mode=""></image>
+					{{numberBuyers}}
+				</view>
+			</view>
+		</view>
+		<mescroll-uni class="list" :fixed="false" ref="mescrollRef" @up="getList" @init="mescrollInit" :use="false">
+			<view class="item" v-for="(item,index) in list" :key="index">
+				<view class="img">
+					<image :src="item.userAvatar?item.userAvatar:'/static/product/user.png'" mode=""></image>
+				</view>
+				<view class="right">
+					{{item.nickName}}<br/>
+					<span>{{item.phone}}</span>
+				</view>
+			</view>
+		</mescroll-uni>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				productName: '',
+				productImage: '',
+				productId: '',
+				salesNum: '100',
+				numberBuyers: '200',
+				upOption: {
+					page: {
+						num: 0,
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				list:[]
+			}
+		},
+		methods: {
+			//返回
+			back() {
+				uni.navigateBack({
+					delta: 1
+				});
+			},
+			mescrollInit(mescroll) {
+				this.mescroll = mescroll;
+			},
+			getList(page){
+				let data = {
+					productId:this.productId,
+					pageSize: page.size,
+					pageNo: page.num,
+				}
+				this.$http.post('/productSales/list',data).then(res => {
+					console.log(res)
+					this.list = this.list.concat(res.data.result.records)
+					this.mescroll.endSuccess(res.data.result.records.length, res.data.result.total);
+				})
+			},
+			getCount(){
+				this.$http.get('/productSales/count?productId='+this.productId).then(res => {
+					this.numberBuyers = res.data.result.numberBuyers
+					this.salesNum = res.data.result.salesNum
+				})
+			}
+		},
+		mounted() {
+			this.mescroll.optDown.use = false
+			this.getCount()
+		},
+		onLoad(options) {
+			this.productName = options.productName
+			this.productImage = options.productImage
+			this.productId = options.productId
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.sales {
+		width: 100%;
+		height: 100%;
+		background: #F7F8FC;
+		display: flex;
+		flex-direction: column;
+		justify-content: space-between;
+		position: fixed;
+
+		.fix {
+			height: 422rpx;
+			background: 
+			  linear-gradient(360deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%),
+			  linear-gradient(90deg, #FFFBFA 2%, #FEFFCF 100%);
+			.head_top {
+
+				/*#ifdef APP-PLUS*/
+				.top {
+					width: 100%;
+					height: var(--status-bar-height);
+				}
+
+				/*#endif*/
+				.head {
+					width: 100vw;
+					height: 112rpx;
+					padding: 20rpx;
+					display: flex;
+					align-items: center;
+					position: relative;
+
+					.cuIcon-back {
+						font-size: 40rpx;
+						margin-right: 40rpx;
+						position: absolute;
+						left: 24rpx;
+					}
+
+					.case {
+						flex: 1;
+						font-size: 36rpx;
+						font-weight: 600;
+						display: flex;
+						align-items: center;
+						justify-content: center;
+						image {
+							width: 40rpx;
+							height: 40rpx;
+							border-radius: 12rpx;
+							margin-right: 12rpx;
+						}
+					}
+				}
+			}
+
+			.type {
+				margin-top: 35rpx;
+				height: 208rpx;
+				display: flex;
+				justify-content: space-around;
+				align-items: center;
+
+				.type_item {
+					font-size: 48rpx;
+					display: flex;
+					align-items: center;
+					
+					image{
+						width: 120rpx;
+						height: 152rpx;
+						margin-right: 16rpx;
+					}
+				}
+				.line{
+					height: 60rpx;
+					width: 1rpx;
+					background-color: #E7E4B6;
+				}
+			}
+		}
+		.list{
+			background-color:transparent;
+			padding:0px 32rpx;
+			overflow-y: auto;
+			.item{
+				width: 100%;
+				height: 128rpx;
+				padding: 20rpx;
+				background: #FFFFFF;
+				border-radius: 16rpx;
+				margin-bottom: 16rpx;
+				display: flex;
+				align-items: center;
+				.img{
+					margin-right: 16rpx;
+					image{
+						width: 80rpx;
+						height: 80rpx;
+					}
+				}
+				.right{
+					font-size: 28rpx;
+					color: #000000;
+					font-weight: 600;
+					height: 100%;
+					display: flex;
+					flex-direction: column;
+					justify-content: space-between;
+					span{
+						font-size: 24rpx;
+						color: #8A91A8;
+						font-weight: 400;
+					}
+				}
+			}
+		}
+	}
+</style>

+ 19 - 2
pages/store/advertising.vue

@@ -39,7 +39,7 @@
 							<i class="text-bg "></i>
 						</view>
 					</view>
-					<view class="grid col-4 grid-square flex-sub booder-top">
+					<!-- <view class="grid col-4 grid-square flex-sub booder-top">
 						<view class="bg-img" v-for="(item, index) in form.photo" :key="index" @tap="ViewImage"
 							:data-url="form.photo[index]">
 							<image :src="form.photo[index]" mode="aspectFill"></image>
@@ -53,7 +53,8 @@
 							<text class='cuIcon-cameraadd'></text>
 						</view>
 						
-					</view>
+					</view> -->
+					<shmily-drag-image v-model="form.photo" :addImage="ChooseImage" :delImage="delImage"></shmily-drag-image>
 					<view class="business-prompt">
 						最多上传20张,每张大小不超过5M,建议宽高比9:16
 					</view>
@@ -117,6 +118,21 @@
 			}
 		},
 		methods: {
+			 addImage() {
+			      // 将图片地址添加到图片数组
+			      this.list.push('http://web.shmily.ren/shmily-drag-image/static/4.jpg')
+			    },
+			    delImage(done) {
+			      uni.showModal({
+			        content: '是否删除?',
+			        success: (res) => {
+			          if(res.confirm) {
+			            // 执行 done() 删除
+			            done()
+			          }
+			        }
+			      })
+			    },
 			hoursChange(index) {
 				const queryString = encodeURIComponent(JSON.stringify({
 					index,
@@ -209,6 +225,7 @@
 					video: this.video,
 					photo: this.form.photo.join(",")
 				}
+				console.log(this.form.photo)
 				if (this.form.videoStatus == null) {
 					this.$http.post('/merchant/LocalMerchantOperate/add', data).then(res => {
 						if (res.data.success) {

BIN
static/product/num.png


BIN
static/product/sales.png


BIN
static/product/user.png


+ 112 - 0
uni_modules/shmily-drag-image/README.md

@@ -0,0 +1,112 @@
+# 图片拖拽排序插件
+<font color='red'>1. H5 和微信小程序已测试通过</font>
+
+<font color='red'>2. App 和其他平台小程序未测试,大概率支持</font>
+
+<font color='red'>3. 非uni_modules版本未更新</font>
+
+## shmily-drag-image
+
+> 点击预览、长按拖拽排序、自定义添加图片、删除确认、支持对象数组
+
+## 预览
+您可以通过微信扫码,查看最佳的演示效果。
+
+![wp](https://web.shmily.ren/shmily-drag-image/static/wp.jpg)
+
+## 基本使用:
+
+```
+<shmily-drag-image v-model="list"></shmily-drag-image>
+
+
+export default {
+  data() {
+    return {
+      list: [
+        'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/1.jpg',
+        'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/2.jpg',
+        'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/3.jpg',
+      ]
+    }
+  }
+}
+```
+
+## 自定义添加、删除确认:
+```
+<shmily-drag-image v-model="list" :addImage="addImage" :delImage="delImage"></shmily-drag-image>
+
+
+export default {
+  data() {
+    return {
+      list: [
+        'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/1.jpg',
+        'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/2.jpg',
+        'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/3.jpg',
+      ]
+    }
+  },
+  methods:{
+    addImage() {
+      // 将图片地址添加到图片数组
+      this.list.push('http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/4.jpg')
+    },
+    delImage(done) {
+      uni.showModal({
+        content: '是否删除?',
+        success: (res) => {
+          if(res.confirm) {
+            // 执行 done() 删除
+            done()
+          }
+        }
+      })
+    }
+  }
+}
+```
+
+## 支持对象数组:
+```
+<shmily-drag-image v-model="list" keyName="src"></shmily-drag-image>
+
+
+export default {
+  data() {
+    return {
+      list: [
+        {
+          src: 'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/1.jpg',
+        },
+        {
+          src: 'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/2.jpg',
+        },
+        {
+          src: 'http://static-11ea0c21-6b8f-47f7-b77f-cb0c7ea3f355.bspapp.com/shmily-drag-image/static/3.jpg',
+        }
+      ]
+    }
+  }
+}
+```
+
+---
+
+## 属性说明
+
+属性名 | 类型 | 默认值 | 说明
+:-:|:-:|:-:|---
+v-model | Array | [ ] | 图片数组,元素为图片地址字符串或对象,默认为`Array<String>`
+keyName | String | null | 从图片数组元素对象中读取的键名
+number | Number | 6 | 图片数量限制
+imageWidth | Number | 0 | 图片父容器宽度(实际显示的图片宽度为 imageWidth / 1.1 ),单位 rpx。imageWidth > 0 则 cols 无效
+cols | Number | 3 | 图片列数(cols > 0 则 imageWidth 无效)
+borderRadius | Number | 0 | 图片圆角,单位 rpx
+padding | Number | 10 | 图片周围空白填充,单位 rpx
+scale | Number | 1.1 | 拖动图片时放大倍数 [0, ∞)
+opacity | Number | 0.7 | 拖动图片时不透明度
+addImage | Function | null | 自定义添加,见上方示例
+delImage | Function | null | 删除确认,见上方示例
+

+ 10 - 0
uni_modules/shmily-drag-image/changelog.md

@@ -0,0 +1,10 @@
+## 2.0.2(2022-09-17)
+- 修复图片删除
+## 2.0.1(2022-09-16)
+- 修改文档
+## 2.0.0(2022-09-16)
+- 支持 Vue 3
+- 图片数组支持对象元素
+- 新增删除确认
+- 解决了一些已知问题
+- 调整为 uni_modules 目录规范

+ 560 - 0
uni_modules/shmily-drag-image/components/shmily-drag-image/shmily-drag-image.vue

@@ -0,0 +1,560 @@
+<template>
+  <view class="con">
+    <template v-if="viewWidth">
+      <movable-area class="area" :style="{ height: areaHeight }" @mouseenter="mouseenter" @mouseleave="mouseleave">
+        <movable-view v-for="(item, index) in imageList" :key="item.id" class="view" direction="all" :y="item.y"
+          :x="item.x" :damping="40" :disabled="item.disable" @change="onChange($event, item)"
+          @touchstart="touchstart(item)" @mousedown="touchstart(item)" @touchend="touchend(item)"
+          @mouseup="touchend(item)" :style="{
+          width: viewWidth + 'px', 
+          height: viewWidth + 'px', 
+          'z-index': item.zIndex, 
+          opacity: item.opacity 
+        }">
+          <view class="area-con" :style="{
+            width: childWidth, 
+            height: childWidth, 
+            borderRadius: borderRadius + 'rpx',
+            transform: 'scale(' + item.scale + ')' 
+          }">
+            <image class="pre-image" :src="item.src" mode="aspectFill"></image>
+            <view class="del-con" @click="delImages(item, index)" @touchstart.stop="delImageMp(item, index)"
+              @touchend.stop="nothing()" @mousedown.stop="nothing()" @mouseup.stop="nothing()">
+              <view class="del-wrap">
+                <image class="del-image"
+                  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAhdEVYdENyZWF0aW9uIFRpbWUAMjAyMDowNzoyNSAyMTo1NDoyOU4TkJAAAADcSURBVFhH7ZfRCoMwDEXLvkjwwVf/bH/emmAyN6glTW9WBjsgwm28OeCLpj81Sil7zvlJ90UiONS/yY5VogsO6XrBg3IEQ5a/s8vRSWUAKmLqp2w5jz5BiNQEGMo3GbloDLtFXJ1IkaEuhAiiY6gEIqB4yqACSk9piIBiKQ8VUFpLviKg3C2rESKgWERCBZSWiEfgIfffYvrrsAgoISJ3Apy3zuTxcSxLQkV6ykNEPKVQkZEyiAiiZKgDIaC4upACSlcn5fM/+WuDCAHF1E/Z/N9AhkMZnPNDPI+UDjPIXgAQIGjNAAAAAElFTkSuQmCC">
+                </image>
+              </view>
+            </view>
+          </view>
+        </movable-view>
+        <view class="add" v-if="imageList.length < number"
+          :style="{ top: add.y, left: add.x, width: viewWidth + 'px', height: viewWidth + 'px' }" @click="addImages">
+          <view class="add-wrap" :style="{ width: childWidth, height: childWidth, borderRadius: borderRadius + 'rpx' }">
+            <image style="width: 54rpx;height: 54rpx;"
+              src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAAA2CAYAAACMRWrdAAABIUlEQVRoQ+2a2w2DMAxFeQzWrsMUbadAsEw3S1CqVgppKwLX8BEOP4iHTXx8uUgWdVXoVhdaV0VhSmf7vr/H8V3XzY6V3P9iD+nYOI5P7/01LMI596AwoZV0TIBXIUWFXhKLFBWYSFGhhxQN6SFFQ5i4ogITKSr0cEVDekjRECauqMBEigq9U7piOk2yAti27SUe5ljlTfPEQ6KZecTvwl4P3ytvOv06R2HDMNzes7+6aRrvnHvtf50L13Lp50rx88zcvNlS3JpwKQ67XyK04nq2nFbk/LqVjin0TvmBNgQ2S4UUDcliHgpMpKjQwxUN6SFFQ5i4ogITKSr0cEVDekjRECauqMAsVoph+hVPtYr5+03p9tbYQ96xrYtT4ootbAJGVxxVTapVswAAAABJRU5ErkJggg==">
+            </image>
+          </view>
+        </view>
+      </movable-area>
+
+    </template>
+  </view>
+</template>
+
+<script>
+  export default {
+    emits: ['input', 'update:modelValue'],
+    props: {
+      // 排序图片
+      value: {
+        type: Array,
+        default: function() {
+          return []
+        }
+      },
+      // 排序图片
+      modelValue: {
+        type: Array,
+        default: function() {
+          return []
+        }
+      },
+      // 从 list 元素对象中读取的键名
+      keyName: {
+        type: String,
+        default: null
+      },
+      // 选择图片数量限制
+      number: {
+        type: Number,
+        default: 6
+      },
+      // 图片父容器宽度(实际显示的图片宽度为 imageWidth / 1.1 ),单位 rpx
+      // imageWidth > 0 则 cols 无效
+      imageWidth: {
+        type: Number,
+        default: 0
+      },
+      // 图片列数
+      cols: {
+        type: Number,
+        default: 3
+      },
+      // 图片圆角,单位 rpx
+      borderRadius: {
+        type: Number,
+        default: 0
+      },
+      // 图片周围空白填充,单位 rpx
+      padding: {
+        type: Number,
+        default: 10
+      },
+      // 拖动图片时放大倍数 [0, ∞)
+      scale: {
+        type: Number,
+        default: 1.1
+      },
+      // 拖动图片时不透明度
+      opacity: {
+        type: Number,
+        default: 0.7
+      },
+      // 自定义添加
+      addImage: {
+        type: Function,
+        default: null
+      },
+      // 删除确认
+      delImage: {
+        type: Function,
+        default: null
+      }
+    },
+    data() {
+      return {
+        imageList: [],
+        width: 0,
+        add: {
+          x: 0,
+          y: 0
+        },
+        colsValue: 0,
+        viewWidth: 0,
+        tempItem: null,
+        timer: null,
+        changeStatus: true,
+        preStatus: true,
+        first: true,
+      }
+    },
+    computed: {
+      areaHeight() {
+        let height = ''
+        // return '355px'
+        if (this.imageList.length < this.number) {
+          height = (Math.ceil((this.imageList.length + 1) / this.colsValue) * this.viewWidth).toFixed() + 'px'
+        } else {
+          height = (Math.ceil(this.imageList.length / this.colsValue) * this.viewWidth).toFixed() + 'px'
+        }
+        console.log('areaHeight', height)
+        return height
+      },
+      childWidth() {
+        return this.viewWidth - this.rpx2px(this.padding) * 2 + 'px'
+      },
+    },
+    watch: {
+      value: {
+        handler(n) {
+          if (!this.first && this.changeStatus) {
+            console.log('watch', n)
+            let flag = false
+            for (let i = 0; i < n.length; i++) {
+              if (flag) {
+                this.addProperties(this.getSrc(n[i]))
+                continue
+              }
+              if (this.imageList.length === i || this.imageList[i].src !== this.getSrc(n[i])) {
+                flag = true
+                this.imageList.splice(i)
+                this.addProperties(this.getSrc(n[i]))
+              }
+            }
+          }
+        },
+        deep: true
+      },
+      modelValue: {
+        handler(n) {
+          if (!this.first && this.changeStatus) {
+            console.log('watch', n)
+            let flag = false
+            for (let i = 0; i < n.length; i++) {
+              if (flag) {
+                this.addProperties(this.getSrc(n[i]))
+                continue
+              }
+              if (this.imageList.length === i || this.imageList[i].src !== this.getSrc(n[i])) {
+                flag = true
+                this.imageList.splice(i)
+                this.addProperties(this.getSrc(n[i]))
+              }
+            }
+          }
+        },
+        deep: true
+      },
+    },
+    created() {
+      this.width = uni.getSystemInfoSync().windowWidth
+    },
+    mounted() {
+      const query = uni.createSelectorQuery().in(this)
+      query.select('.con').boundingClientRect(data => {
+        this.colsValue = this.cols
+        this.viewWidth = data.width / this.cols
+        if (this.imageWidth > 0) {
+          this.viewWidth = this.rpx2px(this.imageWidth)
+          this.colsValue = Math.floor(data.width / this.viewWidth)
+        }
+        let list = this.value
+        // #ifdef VUE3
+        list = this.modelValue
+        // #endif
+        for (let item of list) {
+          this.addProperties(this.getSrc(item))
+        }
+        this.first = false
+
+      })
+      query.exec()
+    },
+    methods: {
+      getSrc(item) {
+        if(this.keyName !== null) {
+          return item[this.keyName]
+        }
+        return item
+      },
+      onChange(e, item) {
+        if (!item) return
+        item.oldX = e.detail.x
+        item.oldY = e.detail.y
+        if (e.detail.source === 'touch') {
+          if (item.moveEnd) {
+            item.offset = Math.sqrt(Math.pow(item.oldX - item.absX * this.viewWidth, 2) + Math.pow(item.oldY - item
+              .absY * this.viewWidth, 2))
+          }
+          let x = Math.floor((e.detail.x + this.viewWidth / 2) / this.viewWidth)
+          if (x >= this.colsValue) return
+          let y = Math.floor((e.detail.y + this.viewWidth / 2) / this.viewWidth)
+          let index = this.colsValue * y + x
+          if (item.index != index && index < this.imageList.length) {
+            this.changeStatus = false
+            for (let obj of this.imageList) {
+              if (item.index > index && obj.index >= index && obj.index < item.index) {
+                this.change(obj, 1)
+              } else if (item.index < index && obj.index <= index && obj.index > item.index) {
+                this.change(obj, -1)
+              } else if (obj.id != item.id) {
+                obj.offset = 0
+                obj.x = obj.oldX
+                obj.y = obj.oldY
+                setTimeout(() => {
+                  this.$nextTick(() => {
+                    obj.x = obj.absX * this.viewWidth
+                    obj.y = obj.absY * this.viewWidth
+                  })
+                }, 0)
+              }
+            }
+            item.index = index
+            item.absX = x
+            item.absY = y
+            if (!item.moveEnd) {
+              setTimeout(() => {
+                this.$nextTick(() => {
+                  item.x = item.absX * this.viewWidth
+                  item.y = item.absY * this.viewWidth
+                })
+              }, 0)
+            }
+            // console.log('bbb', JSON.parse(JSON.stringify(item)));
+            this.sortList()
+          }
+        }
+      },
+      change(obj, i) {
+        obj.index += i
+        obj.offset = 0
+        obj.x = obj.oldX
+        obj.y = obj.oldY
+        obj.absX = obj.index % this.colsValue
+        obj.absY = Math.floor(obj.index / this.colsValue)
+        setTimeout(() => {
+          this.$nextTick(() => {
+            obj.x = obj.absX * this.viewWidth
+            obj.y = obj.absY * this.viewWidth
+          })
+        }, 0)
+      },
+      touchstart(item) {
+        this.imageList.forEach(v => {
+          v.zIndex = v.index + 9
+        })
+        item.zIndex = 99
+        item.moveEnd = true
+        this.tempItem = item
+        this.timer = setTimeout(() => {
+          item.scale = this.scale
+          item.opacity = this.opacity
+          clearTimeout(this.timer)
+          this.timer = null
+        }, 200)
+      },
+      touchend(item) {
+        this.previewImage(item)
+        item.scale = 1
+        item.opacity = 1
+        item.x = item.oldX
+        item.y = item.oldY
+        item.offset = 0
+        item.moveEnd = false
+        setTimeout(() => {
+          this.$nextTick(() => {
+            item.x = item.absX * this.viewWidth
+            item.y = item.absY * this.viewWidth
+            this.tempItem = null
+            this.changeStatus = true
+          })
+          // console.log('ccc', JSON.parse(JSON.stringify(item)));
+        }, 0)
+        // console.log('ddd', JSON.parse(JSON.stringify(item)));
+      },
+      previewImage(item) {
+        if (this.timer && this.preStatus && this.changeStatus && item.offset < 28.28) {
+          clearTimeout(this.timer)
+          this.timer = null
+          const list = this.value || this.modelValue
+          let srcList = list.map(v => this.getSrc(v))
+          console.log(list, srcList);
+          uni.previewImage({
+            urls: srcList,
+            current: item.src,
+            success: () => {
+              this.preStatus = false
+              setTimeout(() => {
+                this.preStatus = true
+              }, 600)
+            },
+            fail: (e) => {
+              console.log(e);
+            }
+          })
+        } else if (this.timer) {
+          clearTimeout(this.timer)
+          this.timer = null
+        }
+      },
+      mouseenter() {
+        //#ifdef H5
+        this.imageList.forEach(v => {
+          v.disable = false
+        })
+        //#endif
+
+      },
+      mouseleave() {
+        //#ifdef H5
+        if (this.tempItem) {
+          this.imageList.forEach(v => {
+            v.disable = true
+            v.zIndex = v.index + 9
+            v.offset = 0
+            v.moveEnd = false
+            if (v.id == this.tempItem.id) {
+              if (this.timer) {
+                clearTimeout(this.timer)
+                this.timer = null
+              }
+              v.scale = 1
+              v.opacity = 1
+              v.x = v.oldX
+              v.y = v.oldY
+              this.$nextTick(() => {
+                v.x = v.absX * this.viewWidth
+                v.y = v.absY * this.viewWidth
+                this.tempItem = null
+              })
+            }
+          })
+          this.changeStatus = true
+        }
+        //#endif
+      },
+      addImages() {
+        if (typeof this.addImage === 'function') {
+          this.addImage.bind(this.$parent)()
+        } else {
+          let checkNumber = this.number - this.imageList.length
+          uni.chooseImage({
+            count: checkNumber,
+            sourceType: ['album', 'camera'],
+            success: res => {
+              let count = checkNumber <= res.tempFilePaths.length ? checkNumber : res.tempFilePaths.length
+              for (let i = 0; i < count; i++) {
+                this.addProperties(res.tempFilePaths[i])
+              }
+              this.sortList()
+            }
+          })
+        }
+      },
+      delImages(item, index) {
+        if (typeof this.delImage === 'function') {
+          this.delImage.bind(this.$parent)(() => {
+            this.delImageHandle(item, index)
+          })
+        } else {
+          this.delImageHandle(item, index)
+        }
+      },
+      delImageHandle(item, index) {
+        this.imageList.splice(index, 1)
+        for (let obj of this.imageList) {
+          if (obj.index > item.index) {
+            obj.index -= 1
+            obj.x = obj.oldX
+            obj.y = obj.oldY
+            obj.absX = obj.index % this.colsValue
+            obj.absY = Math.floor(obj.index / this.colsValue)
+            this.$nextTick(() => {
+              obj.x = obj.absX * this.viewWidth
+              obj.y = obj.absY * this.viewWidth
+            })
+          }
+        }
+        this.add.x = (this.imageList.length % this.colsValue) * this.viewWidth + 'px'
+        this.add.y = Math.floor(this.imageList.length / this.colsValue) * this.viewWidth + 'px'
+        this.sortList()
+      },
+      delImageMp(item, index) {
+        //#ifdef MP
+        this.delImages(item, index)
+        //#endif
+      },
+      sortList() {
+        console.log('sortList');
+        const result = []
+        let source = this.value
+        // #ifdef VUE3
+        source = this.modelValue
+        // #endif
+        
+        let list = this.imageList.slice()
+        list.sort((a, b) => {
+          return a.index - b.index
+        })
+        for (let s of list) {
+          let item = source.find(d => this.getSrc(d) == s.src)
+          if (item) {
+            result.push(item)
+          } else {
+            if(this.keyName !== null) {
+              result.push({
+                [this.keyName]: s.src
+              })
+            } else {
+              result.push(s.src)
+            }
+          }
+        }
+        
+        this.$emit("input", result);
+        this.$emit("update:modelValue", result);
+      },
+      addProperties(item) {
+        console.log(item);
+        let absX = this.imageList.length % this.colsValue
+        let absY = Math.floor(this.imageList.length / this.colsValue)
+        let x = absX * this.viewWidth
+        let y = absY * this.viewWidth
+        this.imageList.push({
+          src: item,
+          x,
+          y,
+          oldX: x,
+          oldY: y,
+          absX,
+          absY,
+          scale: 1,
+          zIndex: 9,
+          opacity: 1,
+          index: this.imageList.length,
+          id: this.guid(16),
+          disable: false,
+          offset: 0,
+          moveEnd: false
+        })
+        this.add.x = (this.imageList.length % this.colsValue) * this.viewWidth + 'px'
+        this.add.y = Math.floor(this.imageList.length / this.colsValue) * this.viewWidth + 'px'
+      },
+      nothing() {},
+      rpx2px(v) {
+        return this.width * v / 750
+      },
+      guid(len = 32) {
+        const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
+        const uuid = []
+        const radix = chars.length
+        for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]
+        uuid.shift()
+        return `u${uuid.join('')}`
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .con {
+    // padding: 30rpx;
+
+    .area {
+      width: 100%;
+
+      .view {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        .area-con {
+          position: relative;
+          overflow: hidden;
+
+          .pre-image {
+            width: 100%;
+            height: 100%;
+          }
+
+          .del-con {
+            position: absolute;
+            top: 0rpx;
+            right: 0rpx;
+            padding: 0 0 20rpx 20rpx;
+
+            .del-wrap {
+              width: 36rpx;
+              height: 36rpx;
+              background-color: rgba(0, 0, 0, 0.4);
+              border-radius: 0 0 0 10rpx;
+              display: flex;
+              justify-content: center;
+              align-items: center;
+
+              .del-image {
+                width: 20rpx;
+                height: 20rpx;
+              }
+            }
+          }
+        }
+      }
+
+      .add {
+        position: absolute;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        .add-wrap {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          background-color: #eeeeee;
+        }
+      }
+    }
+  }
+</style>

+ 84 - 0
uni_modules/shmily-drag-image/package.json

@@ -0,0 +1,84 @@
+{
+  "id": "shmily-drag-image",
+  "displayName": "图片拖拽排序",
+  "version": "2.0.2",
+  "description": "图片排序、图片拖动排序",
+  "keywords": [
+    "drag",
+    "图片",
+    "排序",
+    "拖拽",
+    "拖动"
+],
+  "repository": "https://github.com/shmilyany/shmily-drag-image",
+  "engines": {
+    "HBuilderX": "^3.2.8"
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": "617942150"
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": ["uni-scss"],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "u",
+          "app-nvue": "u"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "n",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "u",
+          "百度": "u",
+          "字节跳动": "u",
+          "QQ": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+          "vue2": "y",
+          "vue3": "y"
+        }
+      }
+    }
+  }
+}