Browse Source

完成商品发布页面

sslyg 3 years ago
parent
commit
617db6cac4
40 changed files with 3693 additions and 33 deletions
  1. 11 1
      http.js
  2. 10 0
      main.js
  3. 11 0
      package-lock.json
  4. 14 2
      pages.json
  5. 1 1
      pages/message/message.vue
  6. 1 1
      pages/order/order.vue
  7. 157 5
      pages/product/my-product.vue
  8. 293 5
      pages/product/product-edit.vue
  9. 4 2
      pages/shopping-cart/shopping-cart.vue
  10. 1 1
      pages/user/kefu.vue
  11. 1 1
      pages/user/notice.vue
  12. 1 1
      pages/user/problem.vue
  13. 1 1
      pages/user/strategy.vue
  14. 9 7
      pages/user/user-center.vue
  15. 1 0
      ssly.scss
  16. 5 5
      store/modules/cart.js
  17. 3 0
      uni_modules/robin-editor/changelog.md
  18. 297 0
      uni_modules/robin-editor/components/robin-color-picker/robin-color-picker.vue
  19. 58 0
      uni_modules/robin-editor/components/robin-editor-header/robin-editor-header.vue
  20. 236 0
      uni_modules/robin-editor/components/robin-editor/editor-icon.css
  21. BIN
      uni_modules/robin-editor/components/robin-editor/editor-icon.ttf
  22. 494 0
      uni_modules/robin-editor/components/robin-editor/robin-editor.vue
  23. 79 0
      uni_modules/robin-editor/package.json
  24. 85 0
      uni_modules/robin-editor/readme.md
  25. 8 0
      uni_modules/uni-popup/changelog.md
  26. 45 0
      uni_modules/uni-popup/components/uni-popup-dialog/keypress.js
  27. 284 0
      uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue
  28. 138 0
      uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue
  29. 165 0
      uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue
  30. 45 0
      uni_modules/uni-popup/components/uni-popup/keypress.js
  31. 22 0
      uni_modules/uni-popup/components/uni-popup/message.js
  32. 50 0
      uni_modules/uni-popup/components/uni-popup/popup.js
  33. 16 0
      uni_modules/uni-popup/components/uni-popup/share.js
  34. 321 0
      uni_modules/uni-popup/components/uni-popup/uni-popup.vue
  35. 84 0
      uni_modules/uni-popup/package.json
  36. 294 0
      uni_modules/uni-popup/readme.md
  37. 2 0
      uni_modules/uni-transition/changelog.md
  38. 280 0
      uni_modules/uni-transition/components/uni-transition/uni-transition.vue
  39. 82 0
      uni_modules/uni-transition/package.json
  40. 84 0
      uni_modules/uni-transition/readme.md

+ 11 - 1
http.js

@@ -23,6 +23,11 @@ function initPramas() {
 	const success = arguments[0].success
 	arguments[0].success = (res) => {
 		console.log(res)
+		if(typeof res.data === "string"){
+			try{				
+				res.data = JSON.parse(res.data)
+			}catch(err){}
+		}
 		if (res.data.code === 0) {
 			uni.showToast({
 				title: res.data.msg,
@@ -33,7 +38,7 @@ function initPramas() {
 		if (res.data.code === 1) {
 			success(res)
 		}
-		if (res.data.code === 401) {
+		if (res.data.code === 401 || res.statusCode ===401 ) {			
 			uni.navigateTo({
 				url: "/pages/user/login"
 			})
@@ -62,5 +67,10 @@ export default {
 		obj.header["Content-type"] = "application/x-www-form-urlencoded"
 		console.log(obj);
 		uni.request(obj);
+	},
+	upload: function() {
+		let obj = initPramas(arguments[0]);
+		obj.url = baseApiUrl + '/common/upload';		
+		uni.uploadFile(obj)
 	}
 }

+ 10 - 0
main.js

@@ -3,6 +3,14 @@ import App from './App'
 import http from 'http.js'
 import verified from 'verified.js'
 import store from './store'
+// import VueQuillEditor from 'vue-quill-editor'
+
+// // require styles
+// import 'quill/dist/quill.core.css'
+// import 'quill/dist/quill.snow.css'
+// import 'quill/dist/quill.bubble.css'
+// Vue.use(VueQuillEditor)
+
 
 Vue.prototype.$http = http
 Vue.prototype.$verified = verified
@@ -11,6 +19,8 @@ Vue.prototype.$store = store
 Vue.config.productionTip = false
 App.mpType = 'app'
 
+
+
 Vue.filter('imagesFilter', (images) => {
 	let image = '';
 	if (images) {

+ 11 - 0
package-lock.json

@@ -0,0 +1,11 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "mp-html": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npm.taobao.org/mp-html/download/mp-html-2.1.0.tgz",
+      "integrity": "sha1-uS81Rl3Bt4+q68iPkUNf9m28j2k="
+    }
+  }
+}

+ 14 - 2
pages.json

@@ -26,7 +26,7 @@
 		}, {
 			"path": "pages/index/index",
 			"style": {
-				"bounce":"none",
+				"bounce": "none",
 				"navigationStyle": "custom"
 			}
 		},
@@ -154,7 +154,19 @@
 				"navigationBarTitleText": "商品详情",
 				"enablePullDownRefresh": false
 			}
-
+		},
+		{
+			"path": "pages/product/my-product",
+			"style": {
+				"navigationBarTitleText": "我的商品",
+				"enablePullDownRefresh": false
+			}
+		},{
+			"path": "pages/product/product-edit",
+			"style": {
+				"navigationBarTitleText": "发布商品",
+				"enablePullDownRefresh": false
+			}
 		}, {
 			"path": "pages/order/order-details",
 			"style": {

+ 1 - 1
pages/message/message.vue

@@ -49,7 +49,7 @@
 			height: 100upx;
 			font-size: 32upx;
 			line-height: 100upx;
-			font-weight: bold;
+			// font-weight: bold;
 		}
 
 		.tabs {

+ 1 - 1
pages/order/order.vue

@@ -108,7 +108,7 @@
 			height: 100upx;
 			font-size: 30upx;
 			line-height: 100upx;
-			font-weight: bold;
+			// font-weight: bold;
 		}
 
 		.tabs {

+ 157 - 5
pages/product/my-product.vue

@@ -1,6 +1,41 @@
 <template>
-	<view>
-		
+	<view class="my-product">
+		<view class="header">
+			<view class="title">
+				我的商品
+			</view>
+			<view style="position: relative;">
+				<view class="sub-title">全部商品</view>
+				<view class="release">
+					<navigator url="/pages/product/product-edit">发布商品</navigator>
+				</view>
+			</view>
+		</view>
+
+		<view class="product-list">
+			<view class="product-item">
+				<view class="product-image">
+					<image class="image" :src="image" mode="scaleToFill"></image>
+				</view>
+				<view>
+					<view class="row row-1">
+						<text class="title"><text class="sxzg-icon">省心直供</text>{{title}}</text>
+					</view>
+					<view class="row row-2">
+						<text class="org-price">¥{{orgPrice}}</text>
+					</view>
+					<view class="row row-3">
+						<text class="sxj-icon">省心价</text>
+						<text class="price">¥{{price}}</text>
+					</view>
+					<view class="row row-4">
+						<text>发布日期:</text>
+						<text class="time">2021-02-25 13:12:41</text>
+					</view>
+				</view>
+
+			</view>
+		</view>
 	</view>
 </template>
 
@@ -8,15 +43,132 @@
 	export default {
 		data() {
 			return {
-				
+				title: "商品名称商品名称商品名称商品名称商品名称商品名称商品名称商品名称商品名称商品名称商品名称商品名称商品名称",
+				orgPrice: "102.20",
+				price: "100.20"
 			}
 		},
 		methods: {
-			
+
 		}
 	}
 </script>
 
-<style>
+<style lang="scss" scoped>
+	.my-product {
+		overflow: hidden;
+	}
+
+	.header {
+		margin: 20upx;
+		background: white;
+		text-align: center;
+		border-radius: 20upx;
+		padding-bottom: 10upx;
+
+		.title {
+			height: 100upx;
+			font-size: 32upx;
+			line-height: 100upx;
+			// font-weight: bold;
+		}
+
+		.sub-title {
+			font-size: 28upx;
+			color: #999999;
+		}
+
+		.release {
+			position: absolute;
+			right: 20upx;
+			font-size: 24upx;
+			bottom: 4upx;
+		}
+	}
+
+
+	.product-list {
+		.product-item {
+			display: flex;
+			background: white;
+			margin: 20upx;
+			padding: 20upx;
+
+			.product-image {
+				width: 180upx;
+				height: 180upx;
+				margin-right: 20upx;
+			}
+
+			.image {
+				width: 180upx;
+				height: 180upx;
+				background: #EEEEEE;
+			}
+		}
 
+		.sxzg-icon {
+			color: $primary-color;
+			font-size: 16rpx;
+			width: 80rpx;
+			text-align: center;
+			line-height: normal;
+			border: 2rpx solid $primary-color;
+			border-radius: 20rpx;
+			display: inline-block;
+			position: relative;
+			top: -4rpx;
+			margin-right: 10upx;
+			// transform: scale(0.9);
+		}
+
+		.title {
+			font-size: 24rpx;
+			display: inline-block;
+			white-space: normal;
+			display: -webkit-box;
+			-webkit-box-orient: vertical;
+			-webkit-line-clamp: 2;
+			overflow: hidden;
+			height: 68upx;
+		}
+
+		.row {
+			// padding: 0 10upx;
+		}
+
+		.row-2 {
+			display: flex;
+			justify-content: space-between;
+		}
+
+		.org-price {
+			font-size: 26rpx;
+			color: #cccccc;
+		}
+
+		.org-price {
+			text-decoration: line-through;
+		}
+
+		.sxj-icon {
+			background: $primary-color;
+			color: white;
+			font-size: 20upx;
+			padding: 0 5upx;
+			border-radius: 5upx;
+			vertical-align: middle;
+		}
+
+		.price {
+			font-size: 26rpx;
+			color: $primary-color;
+			font-weight: bold;
+		}
+
+		.row-4 {
+			font-size: 24upx;
+			color: #999999;
+		}
+	}
 </style>

+ 293 - 5
pages/product/product-edit.vue

@@ -1,22 +1,310 @@
 <template>
-	<view>
-		
+	<view style="overflow: hidden;">
+		<view class="block input-item inline-form">
+			<label class="label" for="">商品标题</label>
+			<input class="input" type="text" v-model="form.name">
+		</view>
+
+		<view class="images-upload">
+			<label class="label" for="">商品主图(需要上传5张图片)</label>
+			<view class="block">
+				<view class="images-wrapper">
+					<view class="image-item" v-for="(image,i) in mainImages">
+						<view class="del" @tap="mainImages.splice(i,1)">删除</view>
+						<image class="image" :src="image|imagesFilter" mode="aspectFill" @tap="replaceImage(i)"></image>
+					</view>
+
+					<view class="image-item" v-if="mainImagesLimit>mainImages.length">
+						<view class="add" @tap="addImage">
+							<uni-icons type="plusempty" size="50" color="#EEEEEE"></uni-icons>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<view class="block spec" v-for="(spec,i) in specs">
+			<view class="input-item inline-form">
+				<label class="label" for="">颜色{{i+1}}</label>
+				<view class="add" @tap="specImage(i)">
+					<image v-if="spec.image" class="image" :src="spec.image|imagesFilter" mode="aspectFill"></image>
+					<uni-icons v-else type="plusempty" size="24" color="#eeeeee"></uni-icons>
+				</view>
+				<view v-if="specs.length>1" class="remove" @tap="specs.splice(i,1)">移除</view>
+			</view>
+			<view class="input-item inline-form">
+				<label class="label" for="">填写颜色</label>
+				<input class="input" type="text" v-model="spec.name">
+			</view>
+			<view class="input-item inline-form">
+				<label class="label" for="">库存</label>
+				<input class="input" type="number" v-model="spec.stock">
+			</view>
+			<view class="input-item inline-form">
+				<label class="label" for="">正常售价</label>
+				<input class="input" type="digit" v-model="spec.org_price">
+			</view>
+			<view class="input-item inline-form">
+				<label class="label" for="">直销价</label>
+				<input class="input" type="digit" v-model="spec.price">
+			</view>
+		</view>
+		<view class="">
+			<button class="btn spec-btn" type="default" @tap="addSpec()">新增颜色分类</button>
+		</view>
+
+		<view class="details">
+			<label class="label" for="">商品详情</label>
+			<view class="" style="position: relative;background: white;margin: 0 20upx;">
+				<robin-editor :imageUploader="imageUploader" v-model="form.details"></robin-editor>
+			</view>
+		</view>
+
+		<view class="">
+			<button class="btn submit-btn" type="default" @tap="submit()">立即发布</button>
+		</view>
 	</view>
 </template>
 
 <script>
+	import robinEditor from '@/uni_modules/robin-editor/components/robin-editor/robin-editor'
+	const specTpl = {
+		image: '',
+		name: '',
+		stock: undefined,
+		org_price: undefined,
+		price: undefined
+	}
 	export default {
 		data() {
 			return {
-				
+				html: "333",
+				mainImages: [],
+				mainImagesLimit: 5,
+				form: {},
+				specs: [{
+					...specTpl
+				}]
 			}
 		},
+		components: {
+			robinEditor
+		},
 		methods: {
-			
+			imageUploader(path, callback) {
+				this.$http.upload({
+					filePath: path,
+					name: 'file',
+					success: (res) => {
+						console.log(res.data.data)
+						callback(res.data.data.fullurl)
+					}
+				})
+
+			},
+			specImage(index) {
+				this.chooseImage().then((res) => {
+					console.log(res)
+					this.$http.upload({
+						file: res.tempFiles[0],
+						filePath: res.tempFilePaths[0],
+						name: 'file',
+						success: (res) => {
+							console.log(res.data.data)
+							this.$set(this.specs[index], 'image', res.data.data.url)
+						}
+					})
+				})
+			},
+			addSpec() {
+				this.specs.push({
+					...specTpl
+				})
+			},
+			addImage() {
+				this.chooseImage().then((res) => {
+					console.log(res)
+					for (let i in res.tempFilePaths) {
+						this.$http.upload({
+							file: res.tempFiles[i],
+							filePath: res.tempFilePaths[i],
+							name: 'file',
+							success: (res) => {
+								console.log(res.data.data)
+								this.mainImages.push(res.data.data.url)
+							}
+						})
+					}
+				});
+			},
+			replaceImage(index) {
+				console.log(index)
+				this.chooseImage().then((res) => {
+					this.$http.upload({
+						file: res.tempFiles[0],
+						filePath: res.tempFilePaths[0],
+						name: 'file',
+						success: (res) => {
+							// this.mainImages[index] = res.data.data.url
+							this.$set(this.mainImages, index, res.data.data.url)
+						}
+					})
+
+				});
+			},
+			chooseImage() {
+				return new Promise((resolve, reject) => {
+					uni.chooseImage({
+						count: this.mainImagesLimit - this.mainImages.length,
+						extension: ['jpg', 'png', 'bmp', 'jpeg'],
+						success(res) {
+							resolve(res)
+						},
+						fail(err) {
+							reject(err)
+						}
+					})
+				})
+			},
+			submit() {
+				this.form.org_price = this.specs.reduce((accumulator, currentValue, currentIndex, array) => {
+					return Math.min(accumulator, currentValue.org_price)
+				}, this.specs[0].org_price)
+
+				this.form.price = this.specs.reduce((accumulator, currentValue, currentIndex, array) => {
+					return Math.min(accumulator, currentValue.price)
+				}, this.specs[0].price)
+
+				this.form.stock = this.specs.reduce((accumulator, currentValue, currentIndex, array) => {
+					return parseInt(accumulator) + parseInt(currentValue.stock)
+				}, 0)
+				this.form.images = this.mainImages.join(",");
+				this.form.specs = JSON.stringify(this.specs); 
+				console.log(this.form)
+			}
 		}
 	}
 </script>
 
-<style>
+<style lang="scss" scoped>
+	.block {
+		background: white;
+		margin: 20upx;
+		border-radius: 10upx;
+	}
+
+	.input-item.inline-form {
+		display: flex;
+		align-items: center;
+		height: 80upx;
+		font-size: 28upx;
+		padding: 0 20upx;
+
+		.label {
+			width: 160upx;
+			// flex: 1 1 160upx;
+		}
+
+		.input {
+			flex-grow: 1;
+		}
+	}
+
+	.images-upload {
+		.label {
+			margin: 20upx;
+			padding: 20upx;
+			font-size: 28upx;
+		}
+
+		.images-wrapper {
+			padding: 0;
+			padding-top: 40upx;
+			display: flex;
+			flex-wrap: wrap;
+
+			// justify-content: space-between;
+
+			.image-item {
+				position: relative;
+				width: 33.33%;
+				text-align: center;
+				margin-bottom: 40upx;
+
+				.del {
+					position: absolute;
+					top: 0;
+					right: 42upx;
+					z-index: 1;
+					font-size: 24upx;
+					background-color: $primary-color;
+					color: white;
+					padding: 2upx 15upx;
+				}
+			}
+
+			.image {
+				width: 150upx;
+				height: 150upx;
+				background: #EEEEEE;
+				flex: 0 0 150upx;
+
+			}
 
+			.add {
+				width: 150upx;
+				height: 150upx;
+				display: inline-block;
+				line-height: 140upx;
+
+				border: 4upx #EEEEEE dashed;
+				box-sizing: border-box;
+			}
+		}
+	}
+
+	.details {
+		.label {
+			height: 80upx;
+			font-size: 28upx;
+			padding: 0 40upx;
+			display: block;
+			line-height: 80upx;
+		}
+	}
+
+	.btn {
+		margin: 0 20upx;
+		line-height: 80upx;
+		font-size: 30upx;
+		color: white;
+	}
+
+	.btn.spec-btn {
+		background: #007AFF;
+	}
+
+	.btn.submit-btn {
+		background: $primary-color;
+		margin: 20upx;
+	}
+
+	.spec.block {
+		.add {
+			border: 4upx dashed #eeeeee;
+			width: 50upx;
+			height: 50upx;
+
+			.image {
+				width: 50upx;
+				height: 50upx;
+
+			}
+		}
+
+		.remove {
+			margin-left: 380upx;
+			color: #007AFF;
+		}
+	}
 </style>

+ 4 - 2
pages/shopping-cart/shopping-cart.vue

@@ -190,6 +190,8 @@
 
 			.product-image {
 				margin-right: 20upx;
+				width: 180upx;
+				height: 180upx;
 
 				.image {
 					width: 180upx;
@@ -216,14 +218,14 @@
 				color: white;
 				padding: 0 5upx;
 				border-radius: 10upx;
-
+				margin-top: 10upx;
 				// display: inline-block;
 
 			}
 
 			.counter {
 				position: absolute;
-				bottom: 10upx;
+				bottom: 0upx;
 				right: 10upx;
 				display: flex;
 

+ 1 - 1
pages/user/kefu.vue

@@ -53,7 +53,7 @@
 	}
 	.title{
 		font-size: 32upx;
-		font-weight: bold;
+		// font-weight: bold;
 		padding: 20upx;
 	}
 	.qrcode{

+ 1 - 1
pages/user/notice.vue

@@ -46,7 +46,7 @@
 			height: 100upx;
 			font-size: 32upx;
 			line-height: 100upx;
-			font-weight: bold;
+			// font-weight: bold;
 		}
 	}
 

+ 1 - 1
pages/user/problem.vue

@@ -85,7 +85,7 @@
 			height: 100upx;
 			font-size: 32upx;
 			line-height: 100upx;
-			font-weight: bold;
+			// font-weight: bold;
 		}
 	}
 

+ 1 - 1
pages/user/strategy.vue

@@ -46,7 +46,7 @@
 			height: 100upx;
 			font-size: 32upx;
 			line-height: 100upx;
-			font-weight: bold;
+			// font-weight: bold;
 		}
 	}
 

+ 9 - 7
pages/user/user-center.vue

@@ -5,7 +5,7 @@
 				<image class="image" src="../../static/images/login/logo.png" mode="scaleToFill"></image>
 			</view>
 			<view>
-				<view class="nickname">{{nickname}}</view>				
+				<view class="nickname">{{nickname}}</view>
 				<view>
 					<text v-show="user_type===3" class="wddz">网店店主</text>
 					<text v-show="user_type===2" class="zgcj">直供厂家</text>
@@ -54,13 +54,15 @@
 					</navigator>
 				</view>
 				<view>
-					<view class="icon">
-						<image class="image" src="../../static/images/menu/b2.png" mode=""></image>
-					</view>
-					<view class="name">我的商品</view>
+					<navigator url="/pages/product/my-product" open-type="navigate">
+						<view class="icon">
+							<image class="image" src="../../static/images/menu/b2.png" mode=""></image>
+						</view>
+						<view class="name">我的商品</view>
+					</navigator>
 				</view>
 				<view>
-					<navigator url="/pages/user/notice" open-type="navigate">
+					<navigator url="/pages/user/strategy" open-type="navigate">
 						<view class="icon">
 							<image class="image" src="../../static/images/menu/b1.png" mode=""></image>
 						</view>
@@ -113,7 +115,7 @@
 		data() {
 			return {
 				balance: '',
-				deposit: ''				
+				deposit: ''
 			};
 		},
 		computed: mapState({

+ 1 - 0
ssly.scss

@@ -0,0 +1 @@
+

+ 5 - 5
store/modules/cart.js

@@ -12,7 +12,7 @@ const state = JSON.parse(JSON.stringify(defaults));
 const getters = {
 	total(state) {
 		let sum = 0;
-		console.log(state)
+		// console.log(state)
 		for (let sellerIndex in state) {
 			for (let productIndex in state[sellerIndex].products) {
 				if (state[sellerIndex].products[productIndex].checked) {
@@ -55,7 +55,7 @@ const mutations = {
 				checked: false
 			})
 		}
-		console.log(this)
+		// console.log(this)
 		this.commit("cart/save", state)
 	},
 	remove(state, payload) {
@@ -87,7 +87,7 @@ const mutations = {
 		this.commit("cart/save", state)
 	},
 	selectAll(state, payload) {
-		console.log(payload)
+		// console.log(payload)
 		for (let sellerIndex in state) {
 			state[sellerIndex].checked = payload.checked
 			for (let productIndex in state[sellerIndex].products) {
@@ -97,7 +97,7 @@ const mutations = {
 		this.commit("cart/save", state)
 	},
 	select(state, payload) {
-		console.log(payload)
+		// console.log(payload)
 		for (let sellerIndex in state) {
 			for (let productIndex in state[sellerIndex].products) {
 				if (payload.id === state[sellerIndex].products[productIndex].id &&
@@ -134,7 +134,7 @@ const actions = {
 					id: payload.id
 				},
 				success: (res) => {
-					console.log(res.data.data.specs)
+					// console.log(res.data.data.specs)
 					const spec = JSON.parse(res.data.data.specs)[payload.specIndex];
 					const data = {
 						id: res.data.data.id,

+ 3 - 0
uni_modules/robin-editor/changelog.md

@@ -0,0 +1,3 @@
+## 2.0.0(2021-02-08)
+迁移至uni_modules
+迁移至uni_modules

+ 297 - 0
uni_modules/robin-editor/components/robin-color-picker/robin-color-picker.vue

@@ -0,0 +1,297 @@
+<template>
+    <view class="content">
+        <robin-editor-header class="head" @cancel="cancel" @save="confirm"></robin-editor-header>
+        <view class="color-picker">
+            <view class="color-name">{{ colorName }}</view>
+            <view class="show-view" :style="{ background: colorName }"></view>
+            <view class="hue-view" @touchstart="pickHue" @touchmove="pickHue"><text class="anchor" :style="{ left: hueView.anchorLeft + 'px' }"></text></view>
+            <view class="color-view" @touchstart="pickColor" @touchmove="pickColor" :style="{ backgroundColor: 'hsl(' + hueView.H + ', 100%, 50%)' }">
+                <text class="anchor" :style="{ top: colorView.anchorTop + 'px', left: colorView.anchorLeft + 'px' }"></text>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script>
+export default {
+    inject: ['popup'],
+    props: {
+        color: {
+            type: String,
+            default: ''
+        }
+    },
+    data() {
+        return {
+            hueView: {},
+            colorView: {},
+            colorName: '',
+            hueLeft: 0.5, // 色相选择器初始位置 [0, 1]
+            anchorTop: 0.5, // 颜色选择器初始 top [0, 1]
+            anchorLeft: 0.5, // 颜色选择器初始 left [0, 1],
+        };
+    },
+    created() {
+        this.popup.childrenMsg = this;
+    },
+    methods: {
+        open: function() {
+            setTimeout(() => {
+                this.init();
+            }, this.popup.duration);
+        },
+        init() {
+            const reg = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})*$/;
+            if (this.color !== '' && reg.test(this.color)) {
+                this.getColorOffset();
+            }
+            Promise.all([this.getHueViewOffset(), this.getColorViewOffset()]).then(() => {
+                this.colorName = this.getColorString(); // 根据 HLS 计算 RGB 字符串
+            });
+        },
+        getHueViewOffset() {
+            // 获取色相选择区域尺寸
+            return new Promise(resolve =>
+                uni
+                    .createSelectorQuery()
+                    .in(this)
+                    .select('.hue-view')
+                    .boundingClientRect(data => {
+                        this.hueView = {
+                            ...data,
+                            anchorLeft: data.width * this.hueLeft,
+                            H: this.hueLeft * 360
+                        };
+                        resolve();
+                    })
+                    .exec()
+            );
+        },
+        getColorViewOffset() {
+            // 获取颜色选择区域尺寸
+            return new Promise(resolve =>
+                uni
+                    .createSelectorQuery()
+                    .in(this)
+                    .select('.color-view')
+                    .boundingClientRect(data => {
+                        this.colorView = {
+                            ...data,
+                            anchorTop: data.height * this.anchorTop,
+                            anchorLeft: data.width * this.anchorLeft,
+                            S: this.anchorLeft,
+                            L: 1 - this.anchorLeft * 0.5 - this.anchorTop / (this.anchorLeft + 1)
+                        };
+                        resolve();
+                    })
+                    .exec()
+            );
+        },
+        getColorString() {
+            // 获取 RGB 颜色字符串
+            const arr = hslToRgb(this.hueView.anchorLeft / this.hueView.width, this.colorView.S, this.colorView.L);
+            const r = arr[0].toString(16).length === 1 ? `0${arr[0].toString(16)}` : arr[0].toString(16);
+            const g = arr[1].toString(16).length === 1 ? `0${arr[1].toString(16)}` : arr[1].toString(16);
+            const b = arr[2].toString(16).length === 1 ? `0${arr[2].toString(16)}` : arr[2].toString(16);
+            return `#${r.toUpperCase()}${g.toUpperCase()}${b.toUpperCase()}`;
+        },
+        getColorOffset() {
+            var color = this.color.substr(1);
+            color = color.length == 6 ? color : color.charAt(0) + color.charAt(0) + color.charAt(1) + color.charAt(1) + color.charAt(2) + color.charAt(2);
+            const r = parseInt('0x' + color.substr(0, 2));
+            const g = parseInt('0x' + color.substr(2, 2));
+            const b = parseInt('0x' + color.substr(4, 2));
+            const arr = rgbToHsl(r, g, b);
+            this.hueLeft = arr[0];
+            this.anchorLeft = arr[1];
+            this.anchorTop = (1 - arr[1] * 0.5 - arr[2]) * (arr[1] + 1);
+        },
+        pickColor(e) {
+            // 选择颜色
+            const top = e.touches[0].clientY - this.colorView.top;
+            const left = e.touches[0].clientX - this.colorView.left;
+            if (top < 0) {
+                this.colorView.anchorTop = 0;
+            } else if (top > this.colorView.height) {
+                this.colorView.anchorTop = this.colorView.height;
+            } else {
+                this.colorView.anchorTop = top;
+            }
+            if (left < 0) {
+                this.colorView.anchorLeft = 0;
+            } else if (left > this.colorView.width) {
+                this.colorView.anchorLeft = this.colorView.width;
+            } else {
+                this.colorView.anchorLeft = e.touches[0].clientX - this.colorView.left;
+            }
+            this.colorView.S = this.colorView.anchorLeft / this.colorView.width;
+            this.colorView.L = this.floor(1 - this.colorView.S * 0.5 - this.colorView.anchorTop / this.colorView.height / (this.colorView.S + 1));
+            this.colorName = this.getColorString(); // 根据 HLS 计算 RGB 字符串
+        },
+        pickHue(e) {
+            // 选择色相
+            if (e.touches[0].clientX >= this.hueView.left && e.touches[0].clientX <= this.hueView.right) {
+                this.hueView.anchorLeft = e.touches[0].clientX - this.hueView.left;
+                this.hueView.H = (this.hueView.anchorLeft / this.hueView.width) * 360;
+                this.colorName = this.getColorString(); // 根据 HLS 计算 RGB 字符串
+            }
+        },
+        floor(num) {
+            return num < 0.09 ? 0 : num;
+        },
+        confirm() {
+            this.$emit('confirm', {
+                color: this.colorName
+            });
+            this.popup.close();
+        },
+        cancel() {
+            this.$emit('cancel');
+            this.popup.close();
+        },
+        close() {}
+    }
+};
+
+function hslToRgb(h, s, l) {
+    // HSL 转 RGB 方法
+    var r, g, b;
+    if (s == 0) {
+        r = g = b = l; // achromatic
+    } else {
+        var hue2rgb = function hue2rgb(p, q, t) {
+            if (t < 0) t += 1;
+            if (t > 1) t -= 1;
+            if (t < 1 / 6) return p + (q - p) * 6 * t;
+            if (t < 1 / 2) return q;
+            if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+            return p;
+        };
+        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+        var p = 2 * l - q;
+        r = hue2rgb(p, q, h + 1 / 3);
+        g = hue2rgb(p, q, h);
+        b = hue2rgb(p, q, h - 1 / 3);
+    }
+    return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+}
+
+function rgbToHsl(r, g, b) {
+    (r /= 255), (g /= 255), (b /= 255);
+    var max = Math.max(r, g, b),
+        min = Math.min(r, g, b);
+    var h,
+        s,
+        l = (max + min) / 2;
+    if (max == min) {
+        h = s = 0; // achromatic
+    } else {
+        var d = max - min;
+        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+        switch (max) {
+            case r:
+                h = (g - b) / d + (g < b ? 6 : 0);
+                break;
+            case g:
+                h = (b - r) / d + 2;
+                break;
+            case b:
+                h = (r - g) / d + 4;
+                break;
+        }
+        h /= 6;
+    }
+
+    var round = function(n, l) {
+        return Math.round(n * Math.pow(10, l)) / Math.pow(10, l);
+    };
+    return [round(h, 3), round(s, 3), round(l, 3)];
+}
+</script>
+
+<style lang="scss" scoped>
+.content {
+    height: 100%;
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+    justify-content: center;
+    background-color: #fff;
+
+    .head {
+        width: 100%;
+    }
+
+    .color-picker {
+        display: flex;
+        align-items: center;
+        flex-direction: column;
+        justify-content: center;
+
+        .color-name {
+            margin: 23rpx;
+            font-size: 45rpx;
+            font-weight: bold;
+            letter-spacing: 8rpx;
+        }
+
+        .show-view {
+            height: 56rpx;
+            width: 567rpx;
+        }
+
+        .hue-view {
+            width: 567rpx;
+            height: 56rpx;
+            margin: 12rpx 0;
+            position: relative;
+            background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
+
+            .anchor {
+                width: 12rpx;
+                height: 100%;
+                position: absolute;
+                background: #ffffff;
+                transform: translate(-50%);
+                box-shadow: 0 0 2rpx rgba(0, 0, 0, 0.6);
+            }
+        }
+
+        .color-view {
+            width: 567rpx;
+            height: 345rpx;
+            position: relative;
+            margin-bottom: 12upx;
+
+            &::before,
+            &::after {
+                content: '';
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                position: absolute;
+            }
+
+            &::before {
+                background: linear-gradient(to right, white, transparent);
+            }
+
+            &::after {
+                background: linear-gradient(to top, black, transparent);
+            }
+
+            .anchor {
+                z-index: 1;
+                width: 24rpx;
+                height: 24rpx;
+                border-radius: 50%;
+                position: absolute;
+                border: 4rpx solid #ffffff;
+                background: rgba(0, 0, 0, 0.3);
+                transform: translate(-50%, -50%);
+            }
+        }
+    }
+}
+</style>

+ 58 - 0
uni_modules/robin-editor/components/robin-editor-header/robin-editor-header.vue

@@ -0,0 +1,58 @@
+<template>
+    <view class="head">
+        <view class="btn left" @tap="cancel" v-if="labelCancel">{{ labelCancel }}</view>
+        <view class="btn right" @tap="save" v-if="labelConfirm">{{ labelConfirm }}</view>
+    </view>
+</template>
+
+<script>
+export default {
+    name: 'robin-editor-header',
+    props: {
+        labelCancel: {
+            type: String,
+            default: '取消'
+        },
+        labelConfirm: {
+            type: String,
+            default: '确定'
+        }
+    },
+    methods: {
+        cancel: function() {
+            this.$emit('cancel');
+        },
+        save: function() {
+            this.$emit('save');
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.head {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    height: 100%;
+    border-bottom: 1px #eee solid;
+    // box-shadow: 1px 0 2px rgba(0, 0, 0, 0.1);
+    background: #fff;
+    .btn {
+        display: block;
+        width: 150upx;
+        height: 80upx;
+        line-height: 80upx;
+        font-size: 30upx;
+        color: #666;
+        padding-left: 20upx;
+        text-align: center;
+        &.left {
+            float: left;
+        }
+        &.right {
+            float: right;
+        }
+    }
+}
+</style>

+ 236 - 0
uni_modules/robin-editor/components/robin-editor/editor-icon.css

@@ -0,0 +1,236 @@
+@font-face {
+	font-family: "iconfont";
+	src: url('~./editor-icon.ttf') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-redo:before {
+  content: "\e627";
+}
+
+.icon-undo:before {
+  content: "\e633";
+}
+
+.icon-indent:before {
+  content: "\eb28";
+}
+
+.icon-outdent:before {
+  content: "\e6e8";
+}
+
+.icon-fontsize:before {
+  content: "\e6fd";
+}
+
+.icon-format-header-1:before {
+  content: "\e860";
+}
+
+.icon-format-header-4:before {
+  content: "\e863";
+}
+
+.icon-format-header-5:before {
+  content: "\e864";
+}
+
+.icon-format-header-6:before {
+  content: "\e865";
+}
+
+.icon-clearup:before {
+  content: "\e64d";
+}
+
+.icon-preview:before {
+  content: "\e631";
+}
+
+.icon-date:before {
+  content: "\e63e";
+}
+
+.icon-fontbgcolor:before {
+  content: "\e678";
+}
+
+.icon-clearedformat:before {
+  content: "\e67e";
+}
+
+.icon-font:before {
+  content: "\e684";
+}
+
+.icon-723bianjiqi_duanhouju:before {
+  content: "\e65f";
+}
+
+.icon-722bianjiqi_duanqianju:before {
+  content: "\e660";
+}
+
+.icon-text_color:before {
+  content: "\e72c";
+}
+
+.icon-format-header-2:before {
+  content: "\e75c";
+}
+
+.icon-format-header-3:before {
+  content: "\e75d";
+}
+
+.icon--checklist:before {
+  content: "\e664";
+}
+
+.icon-baocun:before {
+  content: "\ec09";
+}
+
+.icon-line-height:before {
+  content: "\e7f8";
+}
+
+.icon-quanping:before {
+  content: "\ec13";
+}
+
+.icon-direction-rtl:before {
+  content: "\e66e";
+}
+
+.icon-direction-ltr:before {
+  content: "\e66d";
+}
+
+.icon-selectall:before {
+  content: "\e62b";
+}
+
+.icon-fuzhi:before {
+  content: "\ec7a";
+}
+
+.icon-shanchu:before {
+  content: "\ec7b";
+}
+
+.icon-bianjisekuai:before {
+  content: "\ec7c";
+}
+
+.icon-fengexian:before {
+  content: "\ec7f";
+}
+
+.icon-dianzan:before {
+  content: "\ec80";
+}
+
+.icon-charulianjie:before {
+  content: "\ec81";
+}
+
+.icon-charutupian:before {
+  content: "\ec82";
+}
+
+.icon-wuxupailie:before {
+  content: "\ec83";
+}
+
+.icon-juzhongduiqi:before {
+  content: "\ec84";
+}
+
+.icon-yinyong:before {
+  content: "\ec85";
+}
+
+.icon-youxupailie:before {
+  content: "\ec86";
+}
+
+.icon-youduiqi:before {
+  content: "\ec87";
+}
+
+.icon-zitidaima:before {
+  content: "\ec88";
+}
+
+.icon-xiaolian:before {
+  content: "\ec89";
+}
+
+.icon-zitijiacu:before {
+  content: "\ec8a";
+}
+
+.icon-zitishanchuxian:before {
+  content: "\ec8b";
+}
+
+.icon-zitishangbiao:before {
+  content: "\ec8c";
+}
+
+.icon-zitibiaoti:before {
+  content: "\ec8d";
+}
+
+.icon-zitixiahuaxian:before {
+  content: "\ec8e";
+}
+
+.icon-zitixieti:before {
+  content: "\ec8f";
+}
+
+.icon-zitiyanse:before {
+  content: "\ec90";
+}
+
+.icon-zuoduiqi:before {
+  content: "\ec91";
+}
+
+.icon-zitiyulan:before {
+  content: "\ec92";
+}
+
+.icon-zitixiabiao:before {
+  content: "\ec93";
+}
+
+.icon-zuoyouduiqi:before {
+  content: "\ec94";
+}
+
+.icon-duigoux:before {
+  content: "\ec9e";
+}
+
+.icon-guanbi:before {
+  content: "\eca0";
+}
+
+.icon-shengyin_shiti:before {
+  content: "\eca5";
+}
+
+.icon-Character-Spacing:before {
+  content: "\e964";
+}

BIN
uni_modules/robin-editor/components/robin-editor/editor-icon.ttf


+ 494 - 0
uni_modules/robin-editor/components/robin-editor/robin-editor.vue

@@ -0,0 +1,494 @@
+<template>
+    <view class="wrapper" :style="{ 'padding-top': keyboardHeight }">
+        <!-- <robin-editor-header class="header" @cancel="cancel" @save="save" :labelConfirm="labelConfirm" :labelCancel="labelCancel"></robin-editor-header> -->
+
+        <view class="toolbar" @tap="format" v-if="!showPreview" v-show="keyboardHeight || !autoHideToolbar" :style="'bottom:' + (isIOS ? keyboardHeight : 0) + 'px'">
+            <block v-for="(t, i) in tools" :key="i">
+                <view v-if="t == 'bold'" :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" data-name="bold" data-label="加粗"></view>
+                <view v-if="t == 'italic'" :class="formats.italic ? 'ql-active' : ''" class="iconfont icon-zitixieti" data-name="italic" data-label="斜体"></view>
+                <view v-if="t == 'underline'" :class="formats.underline ? 'ql-active' : ''" class="iconfont icon-zitixiahuaxian" data-name="underline" data-label="下滑线"></view>
+                <view v-if="t == 'strike'" :class="formats.strike ? 'ql-active' : ''" class="iconfont icon-zitishanchuxian" data-name="strike" data-label="删除线"></view>
+                <view
+                    v-if="t == 'align-left'"
+                    :class="formats.align === 'left' || !formats.align ? 'ql-active' : ''"
+                    class="iconfont icon-zuoduiqi"
+                    data-name="align"
+                    data-value="left"
+                    data-label="居左"
+                ></view>
+                <view
+                    v-if="t == 'align-center'"
+                    :class="formats.align === 'center' ? 'ql-active' : ''"
+                    class="iconfont icon-juzhongduiqi"
+                    data-name="align"
+                    data-value="center"
+                    data-label="居中"
+                ></view>
+                <view
+                    v-if="t == 'align-right'"
+                    :class="formats.align === 'right' ? 'ql-active' : ''"
+                    class="iconfont icon-youduiqi"
+                    data-name="align"
+                    data-value="right"
+                    data-label="居右"
+                ></view>
+                <view
+                    v-if="t == 'align-justify'"
+                    :class="formats.align === 'justify' ? 'ql-active' : ''"
+                    class="iconfont icon-zuoyouduiqi"
+                    data-name="align"
+                    data-value="justify"
+                    data-label="平铺"
+                ></view>
+                <!--                  <view :class="formats.lineHeight ? 'ql-active' : ''" class="iconfont icon-line-height" data-name="lineHeight"
+                             data-value="2"></view>
+                    <view :class="formats.letterSpacing ? 'ql-active' : ''" class="iconfont icon-Character-Spacing" data-name="letterSpacing"
+                             data-value="2em"></view>
+                    <view :class="formats.marginTop ? 'ql-active' : ''" class="iconfont icon-722bianjiqi_duanqianju" data-name="marginTop"
+                             data-value="20px"></view>
+                    <view :class="formats.previewarginBottom ? 'ql-active' : ''" class="iconfont icon-723bianjiqi_duanhouju"
+                             data-name="marginBottom" data-value="20px"></view> -->
+                <view v-if="t == 'remove'" class="iconfont icon-clearedformat" @tap.stop="removeFormat"></view>
+                <picker v-if="t == 'font'" class="iconfont" mode="selector" :range="fontSizeRange" @change="fontSize"><view class="icon-fontsize"></view></picker>
+                <view
+                    v-if="t == 'color'"
+                    :style="fontColor != '#FFFFFF' ? 'color:' + formats.color : ''"
+                    class="iconfont icon-text_color"
+                    data-name="color"
+                    @tap.stop="openColor"
+                ></view>
+                <view
+                    v-if="t == 'backgroundColor'"
+                    :style="bgColor ? 'color:' + formats.backgroundColor : ''"
+                    class="iconfont icon-fontbgcolor"
+                    data-name="backgroundColor"
+                    @tap.stop="openColor"
+                ></view>
+                <view v-if="t == 'image'" class="iconfont icon-charutupian" @tap.stop="insertImage"></view>
+                <view v-if="t == 'clear'" class="iconfont icon-shanchu" @tap.stop="clear"></view>
+                <view v-if="t == 'preview'" class="iconfont icon-preview" @tap.stop="preview"></view>
+                <view v-if="t == 'date'" class="iconfont icon-date" @tap="insertDate"></view>
+                <view v-if="t == 'list-check'" class="iconfont icon-checklist" data-name="list" data-value="check"></view>
+                <view
+                    v-if="t == 'list-ordered'"
+                    :class="formats.list === 'ordered' ? 'ql-active' : ''"
+                    class="iconfont icon-youxupailie"
+                    data-name="list"
+                    data-value="ordered"
+                ></view>
+                <view v-if="t == 'list-bullet'" :class="formats.list === 'bullet' ? 'ql-active' : ''" class="iconfont icon-wuxupailie" data-name="list" data-value="bullet"></view>
+                <view v-if="t == 'undo'" class="iconfont icon-undo" @tap="undo"></view>
+                <view v-if="t == 'redo'" class="iconfont icon-redo" @tap="redo"></view>
+                <view v-if="t == 'outdent'" class="iconfont icon-outdent" data-name="indent" data-value="-1"></view>
+                <view v-if="t == 'indent'" class="iconfont icon-indent" data-name="indent" data-value="+1"></view>
+                <view v-if="t == 'divider'" class="iconfont icon-fengexian" @tap="insertDivider"></view>
+                <view v-if="t == 'h1'" :class="formats.header === 1 ? 'ql-active' : ''" class="iconfont icon-format-header-1" data-name="header" :data-value="1"></view>
+                <view v-if="t == 'h2'" :class="formats.header === 2 ? 'ql-active' : ''" class="iconfont icon-format-header-2" data-name="header" :data-value="2"></view>
+                <view v-if="t == 'h3'" :class="formats.header === 3 ? 'ql-active' : ''" class="iconfont icon-format-header-3" data-name="header" :data-value="3"></view>
+                <view v-if="t == 'h4'" :class="formats.header === 4 ? 'ql-active' : ''" class="iconfont icon-format-header-4" data-name="header" :data-value="4"></view>
+                <view v-if="t == 'h5'" :class="formats.header === 5 ? 'ql-active' : ''" class="iconfont icon-format-header-5" data-name="header" :data-value="5"></view>
+                <view v-if="t == 'h6'" :class="formats.header === 6 ? 'ql-active' : ''" class="iconfont icon-format-header-6" data-name="header" :data-value="6"></view>
+                <view v-if="t == 'sub'" :class="formats.script === 'sub' ? 'ql-active' : ''" class="iconfont icon-zitixiabiao" data-name="script" data-value="sub"></view>
+                <view v-if="t == 'super'" :class="formats.script === 'super' ? 'ql-active' : ''" class="iconfont icon-zitishangbiao" data-name="script" data-value="super"></view>
+                <view
+                    v-if="t == 'rtl'"
+                    :class="formats.direction === 'rtl' ? 'ql-active' : ''"
+                    class="iconfont icon-direction-rtl"
+                    data-name="direction"
+                    :data-value="formats.direction === 'rtl' ? '' : 'rtl'"
+                ></view>
+            </block>
+        </view>
+		<view :style="'height:' + editorHeight + 'px;'" class="container" v-if="!previewMode" v-show="!showPreview">
+		    <editor
+		        v-if="!previewMode"
+		        v-show="!showPreview"
+		        id="editor"
+		        class="ql-container"
+		        placeholder="开始输入..."
+		        showImgSize
+		        showImgToolbar
+		        showImgResize
+		        @statuschange="onStatusChange"
+		        :read-only="readOnly"
+		        @ready="onEditorReady"
+				@blur="blur"
+		    ></editor>
+		</view>
+        <uni-popup type="bottom" ref="color"><robin-color-picker :color="color" @confirm="colorChanged"></robin-color-picker></uni-popup>
+        <view class="preview" v-show="showPreview"><rich-text :nodes="htmlData" class="previewNodes"></rich-text></view>
+    </view>
+</template>
+
+<script>
+export default {
+    props: {
+        value: {
+            type: String
+        },
+        imageUploader: {
+            type: Function
+        },
+        muiltImage: {
+            type: Boolean,
+            default: false
+        },
+        compressImage: {
+            type: Boolean,
+            default: false
+        },
+        previewMode: {
+            type: Boolean,
+            default: false
+        },
+        autoHideToolbar: {
+            type: Boolean,
+            default: false
+        },
+        tools: {
+            type: Array,
+            default: function() {
+                return [
+                    'bold',
+                    'italic',
+                    'underline',
+                    'strike',
+                    'align-left',
+                    'align-center',
+                    'align-right',
+                    'remove',
+                    'font',
+                    'color',
+                    'backgroundColor',
+                    'image',
+                    'clear',
+                    // 'preview'
+                ];
+            }
+        }
+    },
+    data() {
+        return {
+            show: true,
+            readOnly: false,
+            formats: {},
+            fontColor: '#000000',
+            bgColor: '',
+            color: '',
+            colorPickerName: '',
+            showColor: true,
+            fontSizeRange: [10, 12, 14, 16, 18, 24, 32],
+            showPreview: false,
+            htmlData: '',
+            html: '',
+            keyboardHeight: 0,
+            editorHeight: 0,
+            isIOS: false
+        };
+    },
+    watch: {
+        value: function(newvar) {
+            this.html = newvar;
+        },
+        html: function(newvar) {
+            if (this.previewMode) {
+                this.previewData(this.html);
+            }
+            if (this.editorCtx) {
+                this.editorCtx.setContents({
+                    html: this.html
+                });
+            }
+        }
+    },
+    created() {
+        this.html = this.value;
+    },
+    mounted: function() {
+        const platform = uni.getSystemInfoSync().platform;
+        this.isIOS = platform === 'ios';
+        if (this.previewMode) {
+            this.previewData(this.html);
+        }
+        let keyboardHeight = 0;
+        this.updatePosition(0);
+        uni.onKeyboardHeightChange(res => {
+            console.log(res, keyboardHeight);
+            if (res.height === keyboardHeight) return;
+            const duration = res.height > 0 ? res.duration * 1000 : 0;
+            keyboardHeight = res.height;
+            setTimeout(() => {
+                uni.pageScrollTo({
+                    selector: "#editor",
+                    success: () => {
+                        this.updatePosition(keyboardHeight);
+                        this.editorCtx && this.editorCtx.scrollIntoView();
+                    }
+                });
+            }, duration);
+        });
+    },
+    computed: {
+        labelConfirm: function() {
+            return this.showPreview ? '关闭' : '保存';
+        },
+        labelCancel: function() {
+            return this.showPreview ? '' : '取消';
+        }
+    },
+    methods: {
+        updatePosition(keyboardHeight) {
+            const { windowHeight, windowWidth, platform } = uni.getSystemInfoSync();
+            const rpx = windowWidth / 750;
+            let titleHeight = 0;
+            //#ifdef H5
+            titleHeight = 44; //H5标题栏高度
+            //#endif
+            const toolbarHeight = (70 * Math.ceil(this.tools.length / 15) + 1) * rpx; //底部工具栏高度
+
+            const bodyHeight = windowHeight - titleHeight-300;
+            this.keyboardHeight = keyboardHeight;
+            this.editorHeight = keyboardHeight > 0 ? bodyHeight - keyboardHeight - toolbarHeight : this.autoHideToolbar ? bodyHeight : bodyHeight - toolbarHeight;
+        },
+        openColor(e) {
+            var name = e.currentTarget.dataset.name;
+            var color = this.formats[name];
+            this.colorPickerName = name;
+            if (name == 'backgroundColor' && !color) {
+                color = '#FFFFFF';
+            }
+            if (name == 'color' && !color) {
+                color = '#000000';
+            }
+            this.color = color;
+            this.$refs.color.open(color);
+        },
+        colorChanged(e) {
+            let label = '';
+            switch (this.colorPickerName) {
+                case 'backgroundColor':
+                    if (e.color == '#FFFFFF') {
+                        e.color = '';
+                    }
+                    this.bgColor = e.color;
+                    label = '背景色';
+                    break;
+                case 'color':
+                    this.fontColor = e.color;
+                    label = '颜色';
+                    break;
+            }
+            this._format(this.colorPickerName, e.color, label + e.color);
+        },
+        readOnlyChange() {
+            this.readOnly = !this.readOnly;
+        },
+        onEditorReady() {
+            uni.createSelectorQuery()
+                .in(this)
+                .select('#editor')
+                .context(res => {
+                    this.editorCtx = res.context;
+                    if (this.html) {
+                        this.editorCtx.setContents({
+                            html: this.html
+                        });
+                    }
+                })
+                .exec();
+        },
+        undo() {
+            this.editorCtx.undo();
+            this.toast('撤销');
+        },
+        redo() {
+            this.editorCtx.redo();
+            this.toast('重做');
+        },
+        format(e) {
+            let { name, value, label } = e.target.dataset;
+            if (!name) return;
+            this._format(name, value, label);
+        },
+        _format(name, value, label) {
+            this.editorCtx.format(name, value);
+            this.toast(label);
+        },
+        toast(label) {
+            uni.showToast({
+                duration: 600,
+                icon: 'none',
+                title: label
+            });
+        },
+        onStatusChange(e) {
+            const formats = e.detail;
+            this.formats = formats;			
+        },
+		blur(e){
+			this.save()
+		},
+        insertDivider() {
+            this.editorCtx.insertDivider({
+                success: function() {
+                    this.toast('插入分割线');
+                }
+            });
+        },
+        clear() {
+            this.editorCtx.clear({
+                success: res => {
+                    this.toast('清空');
+                }
+            });
+        },
+        removeFormat() {
+            this.editorCtx.removeFormat();
+            this.toast('清除格式');
+        },
+        insertDate() {
+            const date = new Date();
+            const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
+            this.editorCtx.insertText({
+                text: formatDate
+            });
+            this.toast('插入日期');
+        },
+        insertImage() {
+            let params = {};
+            params.count = this.muiltImage ? 9 : 1;
+            params.sizeType = this.compressImage ? ['compressed'] : ['original'];
+            uni.chooseImage({
+                ...params,
+                success: res => {
+                    res.tempFilePaths.map(path => {
+                        this.imageUploader(path, url => {
+                            this.editorCtx.insertImage({
+                                src: url,
+                                alt: '图像'
+                            });
+                        });
+                    });
+                },
+				fail:err=> {
+					uni.showModal({
+						content:JSON.stringify(err)
+					})
+				}
+            });
+        },
+        fontSize(e) {
+            const index = e.detail.value;
+            const fz = this.fontSizeRange[index] + 'px';
+            this._format('fontSize', fz, '字体大小:' + fz);
+        },
+        cancel() {
+            this.$emit('cancel');
+        },
+        save() {
+            if (this.showPreview) {
+                if (this.previewMode) {
+                    this.cancel();
+                } else {
+                    this.showPreview = false;
+                }
+            } else {
+                this.editorCtx.getContents({
+                    success: res => {
+                        this.$emit('save', res);
+                        this.$emit('input', res.html);
+                    }
+                });
+            }
+        },
+        previewData: function(html) {
+            this.htmlData = html.replace(/\<img/gi, '<img style="max-width:100%;height:auto"');
+            this.showPreview = true;
+        },
+        preview: function() {
+            this.editorCtx.getContents({
+                success: res => {
+                    this.previewData(res.html);
+                }
+            });
+        }
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+@import './editor-icon.css';
+
+.wrapper {
+    padding: 5px;
+	box-sizing: border-box;
+    width: 100%;
+    position: relative;
+	border: 2upx solid #EEEEEE;
+    .header {
+        width: 100%;
+        position: fixed;
+        z-index: 9;
+        left: 0;
+        height: 75rpx;
+        /* #ifndef H5 */
+        top: 0;
+        /* #endif */
+        /* #ifdef H5 */
+        top: 44px;
+        /* #endif */
+    }
+
+    .container {
+        width: 100%;
+        // margin-top: 75rpx;
+        background: #fff;
+
+        .ql-container {
+            box-sizing: border-box;
+            width: 100%;
+            height: 100%;
+            font-size: 16px;
+            line-height: 1.5;
+            overflow: auto;
+            padding: 20rpx;
+        }
+    }
+
+    .toolbar {
+        // position: fixed;
+        // width: 100%;
+        // left: 0;
+        // bottom: 0;
+        box-sizing: border-box;
+        font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
+        background-color: #fff;
+        border-bottom: 2upx solid #eee;
+        line-height: 50rpx;
+
+        .iconfont {
+            display: inline-block;
+            padding: 10rpx;
+			margin: 0 10upx;
+            width: 50rpx;
+            text-align: center;
+            font-size: 42rpx;
+            box-sizing: border-box;
+        }
+    }
+}
+
+.preview {
+    width: 100%;
+    margin-top: 90rpx;
+
+    .previewNodes {
+        width: 100%;
+        word-break: break-all;
+    }
+}
+
+.ql-active {
+    color: #06c;
+}
+</style>

+ 79 - 0
uni_modules/robin-editor/package.json

@@ -0,0 +1,79 @@
+{
+    "id": "robin-editor",
+    "displayName": "robin-editor",
+    "version": "2.0.0",
+    "description": "基于原生editor组件的富文本编辑器组件,支持颜色选择,插入图片",
+    "keywords": [
+        "robin-editor",
+        "编辑器",
+        "富文本",
+        "小程序"
+    ],
+    "repository": "https://github.com/health901/uniapp-editor",
+    "engines": {
+        "HBuilderX": "^3.1.0"
+    },
+    "dcloudext": {
+        "category": [
+            "前端组件",
+            "通用组件"
+        ],
+        "sale": {
+            "regular": {
+                "price": "0.00"
+            },
+            "sourcecode": {
+                "price": "0.00"
+            }
+        },
+        "contact": {
+            "qq": ""
+        },
+        "declaration": {
+            "ads": "无",
+            "data": "无",
+            "permissions": "无"
+        },
+        "npmurl": ""
+    },
+    "uni_modules": {
+        "dependencies": ["uni-popup"],
+        "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": "y",
+                    "Edge": "y",
+                    "Firefox": "y",
+                    "Safari": "u"
+                },
+                "小程序": {
+                    "微信": "y",
+                    "阿里": "u",
+                    "百度": "u",
+                    "字节跳动": "u",
+                    "QQ": "u"
+                },
+                "快应用": {
+                    "华为": "u",
+                    "联盟": "u"
+                }
+            }
+        }
+    }
+}

+ 85 - 0
uni_modules/robin-editor/readme.md

@@ -0,0 +1,85 @@
+# 富文本编辑器插件
+uniapp 富文本编辑器插件
+
+## 兼容性
+|微信小程序|H5|APP|
+|:--:|:--:|:--:|
+|√|√ |x|
+
+## 使用方式
+在 `script` 中引用组件
+```js
+import myeditor from "@/components/robin-editor/editor.vue"
+export default {
+    components: {myeditor}
+}
+```
+在 `template` 中使用组件
+```html
+<myeditor class="editor" 
+    @cancel="hideEditor" 
+    @save="saveEditor" 
+    v-model="html"
+    :imageUploader="uploadImg" 
+    :muiltImage="true">
+</myeditor>
+```
+
+## Demo
+https://github.com/health901/uniapp-editor-demo
+
+## 属性说明
+|属性|类型|默认值|说明|
+|--|--|--|--|
+|v-model|String| |富文本,双向绑定|
+|imageUploader|function(img,callback)| |上传图片处理函数 接受参数 img:本地图片地址,callback:上传成功回调传入图片链接|
+|muiltImage|Boolean|false|是否支持多图上传|
+|compressImage|Boolean|true|图片上传是否压缩|
+|previewMode|Boolean|false|预览模式,不可编辑|
+|autoHideToolbar|Boolean|false|失去焦点时自动隐藏工具栏|
+|tools|Array|['bold', 'italic', 'underline', 'strike', 'align-left', 'align-center', 'align-right', 'remove', 'font', 'color', 'backgroundColor','image', 'clear', 'preview']|工具栏|
+
+### 工具栏
+|名称|值|
+|--|--|
+|加粗|`bold`|
+|斜体|`italic`|
+|下划线|`underline`|
+|删除线|`strike`|
+|右对齐|`align-left`|
+|居中|`align-center`|
+|左对齐|`align-right`|
+|清除格式|`remove`|
+|字体大小|`font`|
+|字体颜色|`color`|
+|背景色|`backgroundColor`|
+|插入图片|`image`|
+|清空|`clear`|
+|预览|`preview`|
+|插入日期|`date`|
+|列表|`list-check`,`list-ordered`,`list-bullet`|
+|上下标|`sub`,`super`|
+|撤销,恢复撤销|`undo`,`redo`|
+|缩进|`indent`,`outdent`|
+|分割线|`divider`|
+|标题|`h1`,`h2`,`h3`,`h4`,`h5`,`h6`|
+|书写方向|`rtl`|
+
+## 事件说明
+|事件|说明|参数|
+|--|--|--|
+|cancel|点击取消按钮|
+|save|点击保存按钮|e={html,text,delta}|
+
+## 依赖
+|组件|链接|备注|
+|---|--|--|
+|Popup 弹出层<sup>[[1]](#注)</sup>|https://ext.dcloud.net.cn/plugin?id=329|uni-ui库|
+|Transition动画|https://ext.dcloud.net.cn/plugin?id=1231|uni-ui库,Popup依赖|
+|颜色选择器ColorPicker<sup>[[2]](#注)</sup>|https://ext.dcloud.net.cn/plugin?id=1237|字体颜色,背景色|
+
+
+## 注
+
+1. 修改:新增动画结束事件
+2. 修改:添加按钮,支持预设颜色值

+ 8 - 0
uni_modules/uni-popup/changelog.md

@@ -0,0 +1,8 @@
+## 1.2.9(2021-02-05)
+- 优化 组件引用关系,通过uni_modules引用组件
+## 1.2.8(2021-02-05)
+- 调整为uni_modules目录规范
+## 1.2.7(2021-02-05)
+- 调整为uni_modules目录规范
+- 新增 支持 PC 端
+- 新增 uni-popup-message 、uni-popup-dialog扩展组件支持 PC 端

+ 45 - 0
uni_modules/uni-popup/components/uni-popup-dialog/keypress.js

@@ -0,0 +1,45 @@
+// #ifdef H5
+export default {
+  name: 'Keypress',
+  props: {
+    disable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  mounted () {
+    const keyNames = {
+      esc: ['Esc', 'Escape'],
+      tab: 'Tab',
+      enter: 'Enter',
+      space: [' ', 'Spacebar'],
+      up: ['Up', 'ArrowUp'],
+      left: ['Left', 'ArrowLeft'],
+      right: ['Right', 'ArrowRight'],
+      down: ['Down', 'ArrowDown'],
+      delete: ['Backspace', 'Delete', 'Del']
+    }
+    const listener = ($event) => {
+      if (this.disable) {
+        return
+      }
+      const keyName = Object.keys(keyNames).find(key => {
+        const keyName = $event.key
+        const value = keyNames[key]
+        return value === keyName || (Array.isArray(value) && value.includes(keyName))
+      })
+      if (keyName) {
+        // 避免和其他按键事件冲突
+        setTimeout(() => {
+          this.$emit(keyName, {})
+        }, 0)
+      }
+    }
+    document.addEventListener('keyup', listener)
+    this.$once('hook:beforeDestroy', () => {
+      document.removeEventListener('keyup', listener)
+    })
+  },
+	render: () => {}
+}
+// #endif

+ 284 - 0
uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue

@@ -0,0 +1,284 @@
+<template>
+	<view class="uni-popup-dialog">
+		<view class="uni-dialog-title">
+			<text class="uni-dialog-title-text" :class="['uni-popup__'+dialogType]">{{title}}</text>
+		</view>
+		<view class="uni-dialog-content">
+			<text class="uni-dialog-content-text" v-if="mode === 'base'">{{content}}</text>
+			<input v-else class="uni-dialog-input" v-model="val" type="text" :placeholder="placeholder" :focus="focus">
+		</view>
+		<view class="uni-dialog-button-group">
+			<view class="uni-dialog-button" @click="close">
+				<text class="uni-dialog-button-text">取消</text>
+			</view>
+			<view class="uni-dialog-button uni-border-left" @click="onOk">
+				<text class="uni-dialog-button-text uni-button-color">确定</text>
+			</view>
+		</view>
+		<view v-if="popup.isDesktop" class="uni-popup-dialog__close" @click="close">
+			<span class="uni-popup-dialog__close-icon "></span>
+		</view>
+		<!-- #ifdef H5 -->
+		<keypress @esc="close" @enter="onOk"/>
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	// #ifdef H5
+	import keypress from './keypress.js'
+	// #endif
+	/**
+	 * PopUp 弹出层-对话框样式
+	 * @description 弹出层-对话框样式
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=329
+	 * @property {String} value input 模式下的默认值
+	 * @property {String} placeholder input 模式下输入提示
+	 * @property {String} type = [success|warning|info|error] 主题样式
+	 *  @value success 成功
+	 * 	@value warning 提示
+	 * 	@value info 消息
+	 * 	@value error 错误
+	 * @property {String} mode = [base|input] 模式、
+	 * 	@value base 基础对话框
+	 * 	@value input 可输入对话框
+	 * @property {String} content 对话框内容
+	 * @property {Boolean} beforeClose 是否拦截取消事件
+	 * @event {Function} confirm 点击确认按钮触发
+	 * @event {Function} close 点击取消按钮触发
+	 */
+
+	export default {
+		name: "uniPopupDialog",
+		components: {
+			// #ifdef H5
+			keypress
+			// #endif
+		},
+		props: {
+			value: {
+				type: [String, Number],
+				default: ''
+			},
+			placeholder: {
+				type: [String, Number],
+				default: '请输入内容'
+			},
+			/**
+			 * 对话框主题 success/warning/info/error	  默认 success
+			 */
+			type: {
+				type: String,
+				default: 'error'
+			},
+			/**
+			 * 对话框模式 base/input
+			 */
+			mode: {
+				type: String,
+				default: 'base'
+			},
+			/**
+			 * 对话框标题
+			 */
+			title: {
+				type: String,
+				default: '提示'
+			},
+			/**
+			 * 对话框内容
+			 */
+			content: {
+				type: String,
+				default: ''
+			},
+			/**
+			 * 拦截取消事件 ,如果拦截取消事件,必须监听close事件,执行 done()
+			 */
+			beforeClose: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				dialogType: 'error',
+				focus: false,
+				val: ""
+			}
+		},
+		inject: ['popup'],
+		watch: {
+			type(val) {
+				this.dialogType = val
+			},
+			mode(val) {
+				if (val === 'input') {
+					this.dialogType = 'info'
+				}
+			},
+			value(val) {
+				this.val = val
+			}
+		},
+		created() {
+			// 对话框遮罩不可点击
+			this.popup.mkclick = false
+			if (this.mode === 'input') {
+				this.dialogType = 'info'
+				this.val = this.value
+			} else {
+				this.dialogType = this.type
+			}
+		},
+		mounted() {
+			this.focus = true
+		},
+		methods: {
+			/**
+			 * 点击确认按钮
+			 */
+			onOk() {
+				this.$emit('confirm', () => {
+					this.popup.close()
+					if (this.mode === 'input') this.val = this.value
+				}, this.mode === 'input' ? this.val : '')
+			},
+			/**
+			 * 点击取消按钮
+			 */
+			close() {
+				if (this.beforeClose) {
+					this.$emit('close', () => {
+						this.popup.close()
+					})
+					return
+				}
+				this.popup.close()
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.uni-popup-dialog {
+		width: 300px;
+		border-radius: 5px;
+		background-color: #fff;
+	}
+
+	.uni-dialog-title {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		padding-top: 15px;
+		padding-bottom: 5px;
+	}
+
+	.uni-dialog-title-text {
+		font-size: 16px;
+		font-weight: 500;
+	}
+
+	.uni-dialog-content {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		padding: 5px 15px 15px 15px;
+	}
+
+	.uni-dialog-content-text {
+		font-size: 14px;
+		color: #6e6e6e;
+	}
+
+	.uni-dialog-button-group {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		border-top-color: #f5f5f5;
+		border-top-style: solid;
+		border-top-width: 1px;
+	}
+
+	.uni-dialog-button {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+
+		flex: 1;
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		height: 45px;
+		/* #ifdef H5 */
+		cursor: pointer;
+		/* #endif */
+	}
+
+	.uni-border-left {
+		border-left-color: #f0f0f0;
+		border-left-style: solid;
+		border-left-width: 1px;
+	}
+
+	.uni-dialog-button-text {
+		font-size: 14px;
+	}
+
+	.uni-button-color {
+		color: $uni-color-primary;
+	}
+
+	.uni-dialog-input {
+		flex: 1;
+		font-size: 14px;
+	}
+
+	.uni-popup__success {
+		color: $uni-color-success;
+	}
+
+	.uni-popup__warn {
+		color: $uni-color-warning;
+	}
+
+	.uni-popup__error {
+		color: $uni-color-error;
+	}
+
+	.uni-popup__info {
+		color: #909399;
+	}
+
+	.uni-popup-dialog__close {
+		display: block;
+		cursor: pointer;
+		position: absolute;
+		top: 9px;
+		right: 17px;
+	}
+
+	.uni-popup-dialog__close-icon {
+		display: inline-block;
+		width: 13px;
+		height: 1px;
+		background: #909399;
+		transform: rotate(45deg);
+	}
+
+	.uni-popup-dialog__close-icon::after {
+		content: '';
+		display: block;
+		width: 13px;
+		height: 1px;
+		background: #909399;
+		transform: rotate(-90deg);
+	}
+</style>

+ 138 - 0
uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue

@@ -0,0 +1,138 @@
+<template>
+	<view class="uni-popup-message">
+		<view class="uni-popup-message__box fixforpc-width" :class="'uni-popup__'+[type]">
+			<text class="uni-popup-message-text" :class="'uni-popup__'+[type]+'-text'">{{message}}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * PopUp 弹出层-消息提示
+	 * @description 弹出层-消息提示
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=329
+	 * @property {String} type = [success|warning|info|error] 主题样式
+	 *  @value success 成功
+	 * 	@value warning 提示
+	 * 	@value info 消息
+	 * 	@value error 错误
+	 * @property {String} message 消息提示文字
+	 * @property {String} duration 显示时间,设置为 0 则不会自动关闭
+	 */
+
+	export default {
+		name: 'UniPopupMessage',
+		props: {
+			/**
+			 * 主题 success/warning/info/error	  默认 success
+			 */
+			type: {
+				type: String,
+				default: 'success'
+			},
+			/**
+			 * 消息文字
+			 */
+			message: {
+				type: String,
+				default: ''
+			},
+			/**
+			 * 显示时间,设置为 0 则不会自动关闭
+			 */
+			duration: {
+				type: Number,
+				default: 3000
+			}
+		},
+		inject: ['popup'],
+		data() {
+			return {}
+		},
+		created() {
+			this.popup.childrenMsg = this
+		},
+		methods: {
+			open() {
+				if (this.duration === 0) return
+				clearTimeout(this.popuptimer)
+				this.popuptimer = setTimeout(() => {
+					this.popup.close()
+				}, this.duration)
+			},
+			close() {
+				clearTimeout(this.popuptimer)
+			}
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	.uni-popup-message {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+	}
+
+	.uni-popup-message__box {
+		background-color: #e1f3d8;
+		padding: 10px 15px;
+		border-color: #eee;
+		border-style: solid;
+		border-width: 1px;
+		flex: 1;
+	}
+
+	@media screen and (min-width: 500px) {
+		.fixforpc-width {
+			margin-top: 20px;
+			border-radius: 4px;
+			flex: none;
+			min-width: 380px;
+			/* #ifndef APP-NVUE */
+			max-width: 50%;
+			/* #endif */
+			/* #ifdef APP-NVUE */
+			max-width: 500px;
+			/* #endif */
+		}
+	}
+
+	.uni-popup-message-text {
+		font-size: 14px;
+		padding: 0;
+	}
+
+	.uni-popup__success {
+		background-color: #e1f3d8;
+	}
+
+	.uni-popup__success-text {
+		color: #67C23A;
+	}
+
+	.uni-popup__warn {
+		background-color: #faecd8;
+	}
+
+	.uni-popup__warn-text {
+		color: #E6A23C;
+	}
+
+	.uni-popup__error {
+		background-color: #fde2e2;
+	}
+
+	.uni-popup__error-text {
+		color: #F56C6C;
+	}
+
+	.uni-popup__info {
+		background-color: #F2F6FC;
+	}
+
+	.uni-popup__info-text {
+		color: #909399;
+	}
+</style>

+ 165 - 0
uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue

@@ -0,0 +1,165 @@
+<template>
+	<view class="uni-popup-share">
+		<view class="uni-share-title"><text class="uni-share-title-text">{{title}}</text></view>
+		<view class="uni-share-content">
+			<view class="uni-share-content-box">
+				<view class="uni-share-content-item" v-for="(item,index) in bottomData" :key="index" @click.stop="select(item,index)">
+					<image class="uni-share-image" :src="item.icon" mode="aspectFill"></image>
+					<text class="uni-share-text">{{item.text}}</text>
+				</view>
+
+			</view>
+		</view>
+		<view class="uni-share-button-box">
+			<button class="uni-share-button" @click="close">取消</button>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'UniPopupShare',
+		props: {
+			title: {
+				type: String,
+				default: '分享到'
+			}
+		},
+		inject: ['popup'],
+		data() {
+			return {
+				bottomData: [{
+						text: '微信',
+						icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/c2b17470-50be-11eb-b680-7980c8a877b8.png',
+						name: 'wx'
+					},
+					{
+						text: '支付宝',
+						icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/d684ae40-50be-11eb-8ff1-d5dcf8779628.png',
+						name: 'wx'
+					},
+					{
+						text: 'QQ',
+						icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/e7a79520-50be-11eb-b997-9918a5dda011.png',
+						name: 'qq'
+					},
+					{
+						text: '新浪',
+						icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/0dacdbe0-50bf-11eb-8ff1-d5dcf8779628.png',
+						name: 'sina'
+					},
+					{
+						text: '百度',
+						icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/1ec6e920-50bf-11eb-8a36-ebb87efcf8c0.png',
+						name: 'copy'
+					},
+					{
+						text: '其他',
+						icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/2e0fdfe0-50bf-11eb-b997-9918a5dda011.png',
+						name: 'more'
+					}
+				]
+			}
+		},
+		created() {},
+		methods: {
+			/**
+			 * 选择内容
+			 */
+			select(item, index) {
+				this.$emit('select', {
+					item,
+					index
+				}, () => {
+					this.popup.close()
+				})
+			},
+			/**
+			 * 关闭窗口
+			 */
+			close() {
+				this.popup.close()
+			}
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	.uni-popup-share {
+		background-color: #fff;
+	}
+	.uni-share-title {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		height: 40px;
+	}
+	.uni-share-title-text {
+		font-size: 14px;
+		color: #666;
+	}
+	.uni-share-content {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		padding-top: 10px;
+	}
+	
+	.uni-share-content-box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		flex-wrap: wrap;
+		width: 360px;
+	}
+	
+	.uni-share-content-item {
+		width: 90px;
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: column;
+		justify-content: center;
+		padding: 10px 0;
+		align-items: center;
+	}
+	
+	.uni-share-content-item:active {
+		background-color: #f5f5f5;
+	}
+	
+	.uni-share-image {
+		width: 30px;
+		height: 30px;
+	}
+	
+	.uni-share-text {
+		margin-top: 10px;
+		font-size: 14px;
+		color: #3B4144;
+	}
+	
+	.uni-share-button-box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		padding: 10px 15px;
+	}
+	
+	.uni-share-button {
+		flex: 1;
+		border-radius: 50px;
+		color: #666;
+		font-size: 16px;
+	}
+	
+	.uni-share-button::after {
+		border-radius: 50px;
+	}
+</style>

+ 45 - 0
uni_modules/uni-popup/components/uni-popup/keypress.js

@@ -0,0 +1,45 @@
+// #ifdef H5
+export default {
+  name: 'Keypress',
+  props: {
+    disable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  mounted () {
+    const keyNames = {
+      esc: ['Esc', 'Escape'],
+      tab: 'Tab',
+      enter: 'Enter',
+      space: [' ', 'Spacebar'],
+      up: ['Up', 'ArrowUp'],
+      left: ['Left', 'ArrowLeft'],
+      right: ['Right', 'ArrowRight'],
+      down: ['Down', 'ArrowDown'],
+      delete: ['Backspace', 'Delete', 'Del']
+    }
+    const listener = ($event) => {
+      if (this.disable) {
+        return
+      }
+      const keyName = Object.keys(keyNames).find(key => {
+        const keyName = $event.key
+        const value = keyNames[key]
+        return value === keyName || (Array.isArray(value) && value.includes(keyName))
+      })
+      if (keyName) {
+        // 避免和其他按键事件冲突
+        setTimeout(() => {
+          this.$emit(keyName, {})
+        }, 0)
+      }
+    }
+    document.addEventListener('keyup', listener)
+    this.$once('hook:beforeDestroy', () => {
+      document.removeEventListener('keyup', listener)
+    })
+  },
+	render: () => {}
+}
+// #endif

+ 22 - 0
uni_modules/uni-popup/components/uni-popup/message.js

@@ -0,0 +1,22 @@
+export default {
+	created() {
+		if (this.type === 'message') {
+			// 不显示遮罩
+			this.maskShow = false 
+			// 获取子组件对象
+			this.childrenMsg = null
+		}
+	},
+	methods: {
+		customOpen() {
+			if (this.childrenMsg) {
+				this.childrenMsg.open()
+			}
+		},
+		customClose() {
+			if (this.childrenMsg) {
+				this.childrenMsg.close()
+			}
+		}
+	}
+}

+ 50 - 0
uni_modules/uni-popup/components/uni-popup/popup.js

@@ -0,0 +1,50 @@
+import message from './message.js';
+// 定义 type 类型:弹出类型:top/bottom/center
+const config = {
+	// 顶部弹出
+	top: 'top',
+	// 底部弹出
+	bottom: 'bottom',
+	// 居中弹出
+	center: 'center',
+	// 消息提示
+	message: 'top',
+	// 对话框
+	dialog: 'center',
+	// 分享
+	share: 'bottom',
+}
+
+export default {
+	data() {
+		return {
+			config: config,
+			popupWidth: 0,
+			popupHeight: 0
+		}
+	},
+	mixins: [message],
+	computed: {
+		isDesktop() {
+			return this.popupWidth >= 500 && this.popupHeight >= 500
+		}
+	},
+	mounted() {
+		const fixSize = () => {
+			const {
+				windowWidth,
+				windowHeight,
+				windowTop
+			} = uni.getSystemInfoSync()
+			this.popupWidth = windowWidth
+			this.popupHeight = windowHeight + windowTop
+		}
+		fixSize()
+		// #ifdef H5
+		window.addEventListener('resize', fixSize)
+		this.$once('hook:beforeDestroy', () => {
+			window.removeEventListener('resize', fixSize)
+		})
+		// #endif
+	},
+}

+ 16 - 0
uni_modules/uni-popup/components/uni-popup/share.js

@@ -0,0 +1,16 @@
+export default {
+	created() {
+		if (this.type === 'share') {
+			// 关闭点击
+			this.mkclick = false
+		}
+	},
+	methods: {
+		customOpen() {
+			console.log('share 打开了');
+		},
+		customClose() {
+			console.log('share 关闭了');
+		}
+	}
+}

+ 321 - 0
uni_modules/uni-popup/components/uni-popup/uni-popup.vue

@@ -0,0 +1,321 @@
+<template>
+	<view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']"
+	 @touchmove.stop.prevent="clear">
+		<uni-transition v-if="maskShow" class="uni-mask--hook" :mode-class="['fade']" :styles="maskClass" :duration="duration"
+		 :show="showTrans" @click="onTap" />
+		<uni-transition :mode-class="ani" :styles="transClass" :duration="duration" :show="showTrans" @click="onTap">
+			<view class="uni-popup__wrapper-box" @click.stop="clear">
+				<slot />
+			</view>
+		</uni-transition>
+		<!-- #ifdef H5 -->
+		<keypress v-if="maskShow" @esc="onTap" />
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	import popup from './popup.js'
+	// #ifdef H5
+	import keypress from './keypress.js'
+	// #endif
+	/**
+	 * PopUp 弹出层
+	 * @description 弹出层组件,为了解决遮罩弹层的问题
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=329
+	 * @property {String} type = [top|center|bottom] 弹出方式
+	 * 	@value top 顶部弹出
+	 * 	@value center 中间弹出
+	 * 	@value bottom 底部弹出
+	 * 	@value message 消息提示
+	 * 	@value dialog 对话框
+	 * 	@value share 底部分享示例
+	 * @property {Boolean} animation = [ture|false] 是否开启动画
+	 * @property {Boolean} maskClick = [ture|false] 蒙版点击是否关闭弹窗
+	 * @event {Function} change 打开关闭弹窗触发,e={show: false}
+	 */
+
+	export default {
+		name: 'UniPopup',
+		components: {
+			// #ifdef H5
+			keypress
+			// #endif
+		},
+		props: {
+			// 开启动画
+			animation: {
+				type: Boolean,
+				default: true
+			},
+			// 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
+			// message: 消息提示 ; dialog : 对话框
+			type: {
+				type: String,
+				default: 'center'
+			},
+			// maskClick
+			maskClick: {
+				type: Boolean,
+				default: true
+			}
+		},
+		provide() {
+			return {
+				popup: this
+			}
+		},
+		mixins: [popup],
+		watch: {
+			/**
+			 * 监听type类型
+			 */
+			type: {
+				handler: function(newVal) {
+					this[this.config[newVal]]()
+				},
+				immediate: true
+			},
+			isDesktop: {
+				handler: function(newVal) {
+					this[this.config[this.type]]()
+				},
+				immediate: true
+			},
+			/**
+			 * 监听遮罩是否可点击
+			 * @param {Object} val
+			 */
+			maskClick: {
+				handler: function(val) {
+					this.mkclick = val
+				},
+				immediate: true
+			}
+		},
+		data() {
+			return {
+				duration: 300,
+				ani: [],
+				showPopup: false,
+				showTrans: false,
+				maskClass: {
+					'position': 'fixed',
+					'bottom': 0,
+					'top': 0,
+					'left': 0,
+					'right': 0,
+					'backgroundColor': 'rgba(0, 0, 0, 0.4)'
+				},
+				transClass: {
+					'position': 'fixed',
+					'left': 0,
+					'right': 0,
+				},
+				maskShow: true,
+				mkclick: true,
+				popupstyle: this.isDesktop ? 'fixforpc-top' : 'top'
+			}
+		},
+		created() {
+			this.mkclick = this.maskClick
+			if (this.animation) {
+				this.duration = 300
+			} else {
+				this.duration = 0
+			}
+		},
+		methods: {
+			clear(e) {
+				// TODO nvue 取消冒泡
+				e.stopPropagation()
+			},
+			open() {
+				this.showPopup = true
+				this.$nextTick(() => {
+					new Promise(resolve => {
+						clearTimeout(this.timer)
+						this.timer = setTimeout(() => {
+							this.showTrans = true
+							// fixed by mehaotian 兼容 app 端
+							this.$nextTick(() => {
+								resolve();
+							})
+						}, 50);
+					}).then(res => {
+						// 自定义打开事件
+						clearTimeout(this.msgtimer)
+						this.msgtimer = setTimeout(() => {
+							this.customOpen && this.customOpen()
+						}, 100)
+						this.$emit('change', {
+							show: true,
+							type: this.type
+						})
+					})
+				})
+			},
+			close(type) {
+				this.showTrans = false
+				this.$nextTick(() => {
+					this.$emit('change', {
+						show: false,
+						type: this.type
+					})
+					clearTimeout(this.timer)
+					// 自定义关闭事件
+					this.customOpen && this.customClose()
+					this.timer = setTimeout(() => {
+						this.showPopup = false
+					}, 300)
+				})
+			},
+			onTap() {
+				if (!this.mkclick) return
+				this.close()
+			},
+			/**
+			 * 顶部弹出样式处理
+			 */
+			top() {
+				this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
+				this.ani = ['slide-top']
+				this.transClass = {
+					'position': 'fixed',
+					'left': 0,
+					'right': 0,
+				}
+			},
+			/**
+			 * 底部弹出样式处理
+			 */
+			bottom() {
+				this.popupstyle = 'bottom'
+				this.ani = ['slide-bottom']
+				this.transClass = {
+					'position': 'fixed',
+					'left': 0,
+					'right': 0,
+					'bottom': 0
+				}
+			},
+			/**
+			 * 中间弹出样式处理
+			 */
+			center() {
+				this.popupstyle = 'center'
+				this.ani = ['zoom-out', 'fade']
+				this.transClass = {
+					'position': 'fixed',
+					/* #ifndef APP-NVUE */
+					'display': 'flex',
+					'flexDirection': 'column',
+					/* #endif */
+					'bottom': 0,
+					'left': 0,
+					'right': 0,
+					'top': 0,
+					'justifyContent': 'center',
+					'alignItems': 'center'
+				}
+			}
+		}
+	}
+</script>
+<style lang="scss" scoped>
+	.uni-popup {
+		position: fixed;
+		/* #ifndef APP-NVUE */
+		z-index: 99;
+		/* #endif */
+	}
+
+	.fixforpc-z-index {
+		/* #ifndef APP-NVUE */
+		z-index: 999;
+		/* #endif */
+	}
+
+	.uni-popup__mask {
+		position: absolute;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		background-color: $uni-bg-color-mask;
+		opacity: 0;
+	}
+
+	.mask-ani {
+		transition-property: opacity;
+		transition-duration: 0.2s;
+	}
+
+	.uni-top-mask {
+		opacity: 1;
+	}
+
+	.uni-bottom-mask {
+		opacity: 1;
+	}
+
+	.uni-center-mask {
+		opacity: 1;
+	}
+
+	.uni-popup__wrapper {
+		/* #ifndef APP-NVUE */
+		display: block;
+		/* #endif */
+		position: absolute;
+	}
+
+	.top {
+		/* #ifdef H5 */
+		top: var(--window-top);
+		/* #endif */
+		/* #ifndef H5 */
+		top: 0;
+		/* #endif */
+	}
+
+	.fixforpc-top {
+		top: 0;
+	}
+
+	.bottom {
+		bottom: 0;
+	}
+
+	.uni-popup__wrapper-box {
+		/* #ifndef APP-NVUE */
+		display: block;
+		/* #endif */
+		position: relative;
+		/* iphonex 等安全区设置,底部安全区适配 */
+		/* #ifndef APP-NVUE */
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+		/* #endif */
+	}
+
+	.content-ani {
+		// transition: transform 0.3s;
+		transition-property: transform, opacity;
+		transition-duration: 0.2s;
+	}
+
+
+	.uni-top-content {
+		transform: translateY(0);
+	}
+
+	.uni-bottom-content {
+		transform: translateY(0);
+	}
+
+	.uni-center-content {
+		transform: scale(1);
+		opacity: 1;
+	}
+</style>

+ 84 - 0
uni_modules/uni-popup/package.json

@@ -0,0 +1,84 @@
+{
+  "id": "uni-popup",
+  "displayName": "PopUp 弹出层",
+  "version": "1.2.9",
+  "description": " Popup 组件,提供常用的弹层",
+  "keywords": [
+    "popup",
+    "uni-ui",
+    "弹出层",
+    "uni-popup"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+  "dcloudext": {
+    "category": [
+      "前端组件",
+      "通用组件"
+    ],
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
+  },
+  "uni_modules": {
+    "dependencies": [
+      "uni-transition"
+    ],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        }
+      }
+    }
+  }
+}

+ 294 - 0
uni_modules/uni-popup/readme.md

@@ -0,0 +1,294 @@
+
+
+## Popup 弹出层
+> 代码块: `uPopup`
+> 关联组件:`uni-transition`,`uni-popup-dialog`,`uni-popup-message`,`uni-popup-share`
+
+
+弹出层组件,在应用中弹出一个消息提示窗口、提示框等
+
+
+> 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。
+> - 组件需要依赖 `sass` 插件 ,请自行手动安装
+> - `uni-popup-message` 、 `uni-popup-dialog` 等扩展ui组件,需要和 `uni-popup` 配套使用,暂不支持单独使用
+> - `nvue` 中使用 `uni-popup` 时,尽量将组件置于其他元素后面,避免出现层级问题
+> - `uni-popup` 并不能完全阻止页面滚动,可在打开 `uni-popup` 的时候手动去做一些处理,禁止页面滚动
+> - 如果需要在子扩展组件内关闭 `uni-popup` ,请使用扩展(provide/inject)方式,其他方式可能会出现不可预知问题
+> - 如果想在页面渲染完毕后就打开 `uni-popup` ,请在 `onReady` 或 `mounted` 生命周期内调用,确保组件渲染完毕
+> - 在微信小程序开发者工具中,启用真机调试,popup 会延时出现,是因为 setTimeout 在真机调试中的延时问题导致的,预览和发布小程序不会出现此问题
+> - 使用 `npm` 方式引入组件,如果确认引用正确,但是提示未注册组件或显示不正常,请尝试重新编译项目
+> - `uni-popup` 中尽量不要使用 `scroll-view` 嵌套过多的内容,可能会影响组件的性能,导致组件无法打开或者打开卡顿
+> - `uni-popup` 不会覆盖原生 tabbar 和原生导航栏
+
+
+
+### 安装方式
+
+本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。
+
+如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55)
+
+
+## 基本用法 
+
+```html
+<button @click="open">打开弹窗</button>
+<uni-popup ref="popup" type="bottom">底部弹出 Popup</uni-popup>
+```
+
+```javascript
+export default {
+   methods:{
+      open(){
+		 // 通过组件定义的ref调用uni-popup方法
+         this.$refs.popup.open()
+      }
+   }
+
+}
+
+```
+
+## API
+
+### Popup Props 
+
+| 属性名		| 类型		| 默认值	| 说明					|
+| :-:		| :-:		| :-:	| :-:					|
+| animation	| Boolean	|true	| 是否开启动画			|
+| type		| String	|center	| 弹出方式				|
+| maskClick	| Boolean	|true	| 蒙版点击是否关闭弹窗		|
+
+#### Type Options
+
+| 属性名		| 说明						|
+| :-:		| :-:						|
+| top		| 顶部弹出					|
+| center	| 居中弹出					|
+| bottom	| 底部弹出					|
+| message	| 预置样式 :消息提示			|
+| dialog	| 预置样式 :对话框			|
+| share 	| 预置样式 :底部弹出分享示例	|
+
+
+### Popup Methods
+
+|方法称名	|说明			|参数|
+|:-:		|:-:			|:-:|
+|open		|打开弹出层	|-	|
+|close	|关闭弹出层	|-	|
+
+
+### Popup Events
+
+|事件称名		|说明								|返回值			   				 							|
+|:-:			|:-:								|:-:																|
+|change		|组件状态发生变化触发		|e={show: true|false,type:当前模式}	|
+
+
+## 扩展组件说明
+`uni-popup` 其实并没有任何样式,只提供基础的动画效果,给用户一个弹出层解决方案,仅仅是这样并不能满足开发需求,所以我们提供了三种基础扩展样式
+
+### uni-popup-message 提示信息
+
+将 `uni-popup` 的`type`属性改为 `message`,并引入对应组件即可使用消息提示 ,*该组件不支持单独使用*
+
+**示例**
+
+```html
+<uni-popup ref="popup" type="message">
+	<uni-popup-message type="success" message="成功消息" :duration="2000"></uni-popup-message>
+</uni-popup>
+```
+
+### uni-popup-message 属性说明
+
+| 属性名		| 类型		| 默认值	| 说明																		|
+| :-:		| :-:		| :-:	| :-:																		|
+| type		| String	|success| 消息提示主题,可选值: success/warn/info/error								|
+| message	| String	|-		| 消息提示文字																|
+| duration	| Number	|3000	| 消息显示时间,超过显示时间组件自动关闭,设置为0 将不会关闭,需手动调用 close 方法关闭	|
+
+### uni-popup-dialog 对话框
+
+将 `uni-popup` 的`type`属性改为 `dialog`,并引入对应组件即可使用对话框 ,*该组件不支持单独使用*
+
+**示例**
+
+```html
+<uni-popup ref="popup" type="dialog">
+	<uni-popup-dialog type="input" message="成功消息" :duration="2000" :before-close="true" @close="close" @confirm="confirm"></uni-popup-dialog>
+</uni-popup>
+```
+
+```javascript
+export default {
+	methods:{
+		/**
+		 * 点击取消按钮触发
+		 * @param {Object} done
+		 */
+		close(done){
+			// TODO 做一些其他的事情,before-close 为true的情况下,手动执行 done 才会关闭对话框
+			// ...
+			done()
+		},
+		/**
+		 * 点击确认按钮触发
+		 * @param {Object} done
+		 * @param {Object} value
+		 */
+		confirm(done,value){
+			// 输入框的值
+			console.log(value)
+			// TODO 做一些其他的事情,手动执行 done 才会关闭对话框
+			// ...
+			done()
+		}
+	}
+}
+```
+
+### uni-popup-dialog 属性说明
+
+| 属性名			| 类型			| 默认值	| 说明														|
+| :-:			| :-:			| :-:	| :-:														|
+| type			| String		|success| 对话框标题主题,可选值: success/warn/info/error				|
+| mode			| String		|base	| 对话框模式,可选值:base(提示对话框)/input(可输入对话框)		|
+| title			| String		|-		| 对话框标题													|
+| content		| String		|-		| 对话框内容,base模式下生效									|
+| value			| String\Number	|-		| 输入框默认值,input模式下生效									|
+| placeholder	| String		|-		| 输入框提示文字,input模式下生效								|
+| before-close	| Boolean		|false	| 是否拦截取消按钮,如为true,则不会关闭对话框,关闭需要监听 dialog 的 close 事件,并执行 done()|
+
+#### dialog 事件说明
+
+|事件称名		|说明					|返回值											|
+|:-:		|:-:					|:-:											|
+|close		|点击dialog取消按钮触发	|done:执行关闭对话框								|
+|confirm	|点击dialog确定按钮触发	|done:执行关闭对话框:value:input模式下输入框的值	|
+
+
+### uni-popup-share 分享示例
+
+分享示例,不作为最终可使用的组件,将 `uni-popup` 的 `type` 属性改为 `share`,并引入对应组件即可使用 ,*该组件不支持单独使用*
+
+**示例**
+
+```html
+<uni-popup ref="popup" type="share">
+	<uni-popup-share title="分享到" @select="select"></uni-popup-share>
+</uni-popup>
+```
+
+### uni-popup-share 属性说明
+
+| 属性名| 类型		| 默认值	| 说明			|
+| :-:	| :-:		| :-:	| :-:			|
+| title	| String	|		| 分享弹窗标题	|
+
+### uni-popup-share 事件说明
+
+|事件称名		|说明		|返回值											|
+|:-:		|:-:		|:-:											|
+|select		|选择触发		|e = {item,index}:所选参数,done:执行关闭窗口	|
+
+**Tips**
+- share 分享组件,只是作为一个扩展示例,如果需要修改数据源,请到组件内修改
+
+## 如何扩展自己的 uni-popup 弹出层样式?
+`uni-popup` 组件内容是通过 `slot` 插槽的方式去实现的,所以这极大的方便了我们的扩展。
+
+现在我们可以在不改动 `uni-popup` 组件主体的情况下,方便的去扩展我们自己的弹出层样式。
+
+### 添加自定义类型
+如果要去扩展 `uni-popup`,我们需要把组件*引入本地*,才能去进行扩展。
+
+组件放到本地后,在组件目录找到 `popup.js` ,在 `config` 变量中定义自己的类型,key 为当前要定义的类型,value 为弹出类型(top/bottom/center)
+我们以 `uni-popup-share` 为例,看如何扩展一个`share` 底部分享的一个 `uni-popup` 子组件,代码参考 `uni-popup-share.vue`。
+
+```javascript
+
+// popup.js
+const config = {
+	// ...
+	// 分享 key:share 为我们定义的类型 value : 'bottom' 为弹出方向(top/bottom/center)
+	// 这样配置好之后,我们自定义的弹出层就会从底部弹出
+	share:'bottom',
+}
+
+```
+
+### 创建扩展组件
+
+在组件目录创建文件 ,例 `uni-popup-share/uni-popup-share.vue`,结构与其他组件没有区别。
+
+在组件内直接编写样式逻辑即可,如需自定义效果更强,可以通过 props 接受页面参数。
+
+
+### 与父组件 `uni-popup` 进行通讯
+
+组件通讯我们使用了 `provide/inject` , 具体逻辑我们不需要关心,只要在子组件配置 `inject` ,即可获取父组件方法变量等。
+
+```javascript
+
+// uni-popup-share.vue
+export default {
+	name: 'UniPopupShare',
+	props: {
+		title: {
+			type: String,
+			default: '分享到'
+		}
+	},
+	// 直接把下面这一行代码,放到自己的组件内
+	inject: ['popup'],
+	// ...
+	methons:{
+		/**
+		 * 定义的选择事件,选择内容后触发
+		 */
+		select(item, index) {
+			// 将事件发送到页面,在页面进行监听
+			this.$emit('select', {
+				item,
+				index
+			}, () => {
+				// 延迟操作,执行父组件的close事件,关闭弹出层
+				this.popup.close()
+			})
+		},
+		/**
+		 * 关闭窗口
+		 */
+		close() {
+			// 执行父组件的close事件,关闭弹出层
+			this.popup.close()
+		}
+	}
+}
+```
+
+
+### 使用自定义组件
+
+通过上面几个步骤 ,我们就可以使用这个组件了,只需要把我们自定义的组件放置到 `uni-popup` 组件内即可 ,指定 `uni-popup` 的 `type` 为我们第一步定义好的 `share` 
+
+```html
+<uni-popup ref="popup" type="share">
+	<uni-popup-share title="分享到" @select="select"></uni-popup-share>
+</uni-popup>
+```
+
+之后就可以按照 `uni-popup` 的使用方式去打开关闭弹出层了。更多细节可以参考 `uni-popup-message` 和 `uni-popup-dialog`。
+
+
+**Tips**
+- 如果扩展组件目录名和组件名不一致,可能不会被 	`easycom` 正确引用,请配置`easycom`规则或修改组件名称
+
+### 分享你的组件
+
+通过组件扩展,你可以扩展出更丰富的弹出层样式,如果您想让更多人使用你定制的组件,或者您有更好的点子或更好的实现方式,欢迎给我们提交 [PR](https://github.com/dcloudio/uni-ui/pulls),如被采用,会合并到示例中。
+
+在使用中如遇到无法解决的问题,请提 [Issues](https://github.com/dcloudio/uni-ui/issues) 给我们。
+

+ 2 - 0
uni_modules/uni-transition/changelog.md

@@ -0,0 +1,2 @@
+## 1.0.2(2021-02-05)
+- 调整为uni_modules目录规范

+ 280 - 0
uni_modules/uni-transition/components/uni-transition/uni-transition.vue

@@ -0,0 +1,280 @@
+<template>
+	<view v-if="isShow" ref="ani" class="uni-transition" :class="[ani.in]" :style="'transform:' +transform+';'+stylesObject"
+	 @click="change">
+		 <slot></slot>
+	</view>
+</template>
+
+<script>
+	// #ifdef APP-NVUE
+	const animation = uni.requireNativePlugin('animation');
+	// #endif
+	/**
+	 * Transition 过渡动画
+	 * @description 简单过渡动画组件
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=985
+	 * @property {Boolean} show = [false|true] 控制组件显示或隐藏
+     * @property {Array} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
+     *  @value fade 渐隐渐出过渡
+     *  @value slide-top 由上至下过渡
+     *  @value slide-right 由右至左过渡
+     *  @value slide-bottom 由下至上过渡
+     *  @value slide-left 由左至右过渡
+     *  @value zoom-in 由小到大过渡
+     *  @value zoom-out 由大到小过渡
+	 * @property {Number} duration 过渡动画持续时间
+	 * @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
+	 */
+	export default {
+		name: 'uniTransition',
+		props: {
+			show: {
+				type: Boolean,
+				default: false
+			},
+			modeClass: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			duration: {
+				type: Number,
+				default: 300
+			},
+			styles: {
+				type: Object,
+				default () {
+					return {}
+				}
+			}
+		},
+		data() {
+			return {
+				isShow: false,
+				transform: '',
+				ani: { in: '',
+					active: ''
+				}
+			};
+		},
+		watch: {
+			show: {
+				handler(newVal) {
+					if (newVal) {
+						this.open()
+					} else {
+						this.close()
+					}
+				},
+				immediate: true
+			}
+		},
+		computed: {
+			stylesObject() {
+				let styles = {
+					...this.styles,
+					'transition-duration': this.duration / 1000 + 's'
+				}
+				let transfrom = ''
+				for (let i in styles) {
+					let line = this.toLine(i)
+					transfrom += line + ':' + styles[i] + ';'
+				}
+				return transfrom
+			}
+		},
+		created() {
+			// this.timer = null
+			// this.nextTick = (time = 50) => new Promise(resolve => {
+			// 	clearTimeout(this.timer)
+			// 	this.timer = setTimeout(resolve, time)
+			// 	return this.timer
+			// });
+		},
+		methods: {
+			change() {
+				this.$emit('click', {
+					detail: this.isShow
+				})
+			},
+			open() {
+				clearTimeout(this.timer)
+				this.isShow = true
+				this.transform = ''
+				this.ani.in = ''
+				for (let i in this.getTranfrom(false)) {
+					if (i === 'opacity') {
+						this.ani.in = 'fade-in'
+					} else {
+						this.transform += `${this.getTranfrom(false)[i]} `
+					}
+				}
+				this.$nextTick(() => {
+					setTimeout(() => {
+						this._animation(true)
+					}, 50)
+				})
+
+			},
+			close(type) {
+				clearTimeout(this.timer)
+				this._animation(false)
+			},
+			_animation(type) {
+				let styles = this.getTranfrom(type)
+				// #ifdef APP-NVUE
+				if(!this.$refs['ani']) return
+				animation.transition(this.$refs['ani'].ref, {
+					styles,
+					duration: this.duration, //ms
+					timingFunction: 'ease',
+					needLayout: false,
+					delay: 0 //ms
+				}, () => {
+					if (!type) {
+						this.isShow = false
+					}
+					this.$emit('change', {
+						detail: this.isShow
+					})
+				})
+				// #endif
+				// #ifndef APP-NVUE
+				this.transform = ''
+				for (let i in styles) {
+					if (i === 'opacity') {
+						this.ani.in = `fade-${type?'out':'in'}`
+					} else {
+						this.transform += `${styles[i]} `
+					}
+				}
+				this.timer = setTimeout(() => {
+					if (!type) {
+						this.isShow = false
+					}
+					this.$emit('change', {
+						detail: this.isShow
+					})
+
+				}, this.duration)
+				// #endif
+
+			},
+			getTranfrom(type) {
+				let styles = {
+					transform: ''
+				}
+				this.modeClass.forEach((mode) => {
+					switch (mode) {
+						case 'fade':
+							styles.opacity = type ? 1 : 0
+							break;
+						case 'slide-top':
+							styles.transform += `translateY(${type?'0':'-100%'}) `
+							break;
+						case 'slide-right':
+							styles.transform += `translateX(${type?'0':'100%'}) `
+							break;
+						case 'slide-bottom':
+							styles.transform += `translateY(${type?'0':'100%'}) `
+							break;
+						case 'slide-left':
+							styles.transform += `translateX(${type?'0':'-100%'}) `
+							break;
+						case 'zoom-in':
+							styles.transform += `scale(${type?1:0.8}) `
+							break;
+						case 'zoom-out':
+							styles.transform += `scale(${type?1:1.2}) `
+							break;
+					}
+				})
+				return styles
+			},
+			_modeClassArr(type) {
+				let mode = this.modeClass
+				if (typeof(mode) !== "string") {
+					let modestr = ''
+					mode.forEach((item) => {
+						modestr += (item + '-' + type + ',')
+					})
+					return modestr.substr(0, modestr.length - 1)
+				} else {
+					return mode + '-' + type
+				}
+			},
+			// getEl(el) {
+			// 	console.log(el || el.ref || null);
+			// 	return el || el.ref || null
+			// },
+			toLine(name) {
+				return name.replace(/([A-Z])/g, "-$1").toLowerCase();
+			}
+		}
+	}
+</script>
+
+<style>
+	.uni-transition {
+		transition-timing-function: ease;
+		transition-duration: 0.3s;
+		transition-property: transform, opacity;
+		z-index: 998;
+	}
+
+	.fade-in {
+		opacity: 0;
+	}
+
+	.fade-active {
+		opacity: 1;
+	}
+
+	.slide-top-in {
+		/* transition-property: transform, opacity; */
+		transform: translateY(-100%);
+	}
+
+	.slide-top-active {
+		transform: translateY(0);
+		/* opacity: 1; */
+	}
+
+	.slide-right-in {
+		transform: translateX(100%);
+	}
+
+	.slide-right-active {
+		transform: translateX(0);
+	}
+
+	.slide-bottom-in {
+		transform: translateY(100%);
+	}
+
+	.slide-bottom-active {
+		transform: translateY(0);
+	}
+
+	.slide-left-in {
+		transform: translateX(-100%);
+	}
+
+	.slide-left-active {
+		transform: translateX(0);
+		opacity: 1;
+	}
+
+	.zoom-in-in {
+		transform: scale(0.8);
+	}
+
+	.zoom-out-active {
+		transform: scale(1);
+	}
+
+	.zoom-out-in {
+		transform: scale(1.2);
+	}
+</style>

+ 82 - 0
uni_modules/uni-transition/package.json

@@ -0,0 +1,82 @@
+{
+  "id": "uni-transition",
+  "displayName": "Transition 过渡动画",
+  "version": "1.0.2",
+  "description": "元素的简单过渡动画",
+  "keywords": [
+    "动画",
+    "过渡",
+    "uni-transition",
+    "过渡动画"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+  "dcloudext": {
+    "category": [
+      "前端组件",
+      "通用组件"
+    ],
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        }
+      }
+    }
+  }
+}

+ 84 - 0
uni_modules/uni-transition/readme.md

@@ -0,0 +1,84 @@
+
+
+## Transition 过渡动画
+> 代码块: `uTransition`
+
+
+元素的简单过渡动画,组件名:`uni-transition`
+
+### 安装方式
+
+本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。
+
+如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55)
+
+### 基本用法
+
+在 ``template`` 中使用组件
+
+```html
+<template>
+	<view>
+		<button type="primary">fade</button>
+		<uni-transition :mode-class="['fade']" :styles="{'width':'100px','height':'100px';'backgroundColor':'red'}" :show="show" @change="change" />
+	</view>
+</template>
+```
+``` javascript
+
+import uniTransition from '@/components/uni-transition/uni-transition.vue'
+export default {
+		components: {
+			uniTransition
+		},
+		data() {
+			return {
+				show: false,
+			}
+		},
+		onLoad() {},
+		methods: {
+			open(mode) {
+				this.show = !this.show
+			},
+			change() {
+				console.log('触发动画')
+			}
+		}
+	}
+```
+
+## API
+
+### Transition Props
+
+|属性名		|类型	|默认值	|说明					|
+|:-:	|:-:	|:-:					|:-:|
+|show		|Boolean|false	|控制组件显示或隐藏,	|
+|modeClass	|Array	|-		|过渡动画类型			|
+|duration	|Number	|300	|过渡动画持续时间		|
+|styles		|Object	|-		|组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`	|
+
+#### modeClass 类型说明
+**格式为** :`['fade','slide-top']`
+
+|属性名			|说明			|
+|:-:			|:-:			|
+|fade			|渐隐渐出过渡	|
+|slide-top		|由上至下过渡	|
+|slide-right	|由右至左过渡	|
+|slide-bottom	|由下至上过渡	|
+|slide-left		|由左至右过渡	|
+|zoom-in		|由小到大过渡	|
+|zoom-out		|由大到小过渡	|
+
+**注意** 
+
+组合使用时,同一种类型相反的过渡动画如(slide-top、slide-bottom)同时使用时,只有最后一个生效
+
+### Transition Events
+
+|事件称名	|说明				|返回值			|
+|:-:		|:-:				|:-:			|
+|click		|点击组件触发		|-				|
+|change		|过渡动画结束时触发	| e = {detail:true}	|