Browse Source

完成商品发布页面

sslyg 6 months 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: {