Browse Source

初始化 uniapp 小程序

YunaiV 3 years ago
parent
commit
6c4908e70e
85 changed files with 12117 additions and 0 deletions
  1. 16 0
      yudao-vue-ui/.hbuilderx/launch.json
  2. 19 0
      yudao-vue-ui/App.vue
  3. 182 0
      yudao-vue-ui/common/css/common.css
  4. 271 0
      yudao-vue-ui/common/css/icon.css
  5. 630 0
      yudao-vue-ui/components/jyf-parser/jyf-parser.vue
  6. 97 0
      yudao-vue-ui/components/jyf-parser/libs/CssHandler.js
  7. 534 0
      yudao-vue-ui/components/jyf-parser/libs/MpHtmlParser.js
  8. 93 0
      yudao-vue-ui/components/jyf-parser/libs/config.js
  9. 22 0
      yudao-vue-ui/components/jyf-parser/libs/handler.wxs
  10. 500 0
      yudao-vue-ui/components/jyf-parser/libs/trees.vue
  11. 55 0
      yudao-vue-ui/components/mescroll-uni/components/mescroll-down.css
  12. 47 0
      yudao-vue-ui/components/mescroll-uni/components/mescroll-down.vue
  13. 27 0
      yudao-vue-ui/components/mescroll-uni/components/mescroll-empty.vue
  14. 95 0
      yudao-vue-ui/components/mescroll-uni/components/mescroll-empty1.vue
  15. 83 0
      yudao-vue-ui/components/mescroll-uni/components/mescroll-top.vue
  16. 47 0
      yudao-vue-ui/components/mescroll-uni/components/mescroll-up.css
  17. 39 0
      yudao-vue-ui/components/mescroll-uni/components/mescroll-up.vue
  18. 14 0
      yudao-vue-ui/components/mescroll-uni/mescroll-body.css
  19. 344 0
      yudao-vue-ui/components/mescroll-uni/mescroll-body.vue
  20. 65 0
      yudao-vue-ui/components/mescroll-uni/mescroll-mixins.js
  21. 33 0
      yudao-vue-ui/components/mescroll-uni/mescroll-uni-option.js
  22. 36 0
      yudao-vue-ui/components/mescroll-uni/mescroll-uni.css
  23. 788 0
      yudao-vue-ui/components/mescroll-uni/mescroll-uni.js
  24. 408 0
      yudao-vue-ui/components/mescroll-uni/mescroll-uni.vue
  25. 23 0
      yudao-vue-ui/components/mescroll-uni/mixins/mescroll-comp.js
  26. 51 0
      yudao-vue-ui/components/mescroll-uni/mixins/mescroll-more-item.js
  27. 56 0
      yudao-vue-ui/components/mescroll-uni/mixins/mescroll-more.js
  28. 23 0
      yudao-vue-ui/components/mescroll-uni/wxs/bounce.js
  29. 102 0
      yudao-vue-ui/components/mescroll-uni/wxs/mixins.js
  30. 92 0
      yudao-vue-ui/components/mescroll-uni/wxs/renderjs.js
  31. 267 0
      yudao-vue-ui/components/mescroll-uni/wxs/wxs.wxs
  32. 82 0
      yudao-vue-ui/components/mix-action-sheet/mix-action-sheet.vue
  33. 154 0
      yudao-vue-ui/components/mix-button/mix-button.vue
  34. 113 0
      yudao-vue-ui/components/mix-code/mix-code.vue
  35. 209 0
      yudao-vue-ui/components/mix-empty/mix-empty.vue
  36. 66 0
      yudao-vue-ui/components/mix-icon-loading/mix-icon-loading.vue
  37. 117 0
      yudao-vue-ui/components/mix-list-cell/mix-list-cell.vue
  38. 60 0
      yudao-vue-ui/components/mix-load-more/mix-load-more.vue
  39. 114 0
      yudao-vue-ui/components/mix-loading/mix-loading.vue
  40. 105 0
      yudao-vue-ui/components/mix-modal/mix-modal.vue
  41. 139 0
      yudao-vue-ui/components/mix-nav-bar/mix-nav-bar.vue
  42. 180 0
      yudao-vue-ui/components/mix-number-box/mix-number-box.vue
  43. 53 0
      yudao-vue-ui/components/mix-price-view/mix-price-view.vue
  44. 137 0
      yudao-vue-ui/components/mix-timeline/mix-timeline.vue
  45. 11 0
      yudao-vue-ui/components/mix-upload-image/mix-upload-image.vue
  46. 14 0
      yudao-vue-ui/components/number-keyboard/number-keyboard.vue
  47. 97 0
      yudao-vue-ui/components/pay-password-keyboard/pay-password-keyboard.vue
  48. 25 0
      yudao-vue-ui/components/uni-popup/popup.js
  49. 302 0
      yudao-vue-ui/components/uni-popup/uni-popup.vue
  50. 245 0
      yudao-vue-ui/components/uni-swipe-action-item/bindingx.js
  51. 204 0
      yudao-vue-ui/components/uni-swipe-action-item/index.wxs
  52. 160 0
      yudao-vue-ui/components/uni-swipe-action-item/mpalipay.js
  53. 158 0
      yudao-vue-ui/components/uni-swipe-action-item/mpother.js
  54. 97 0
      yudao-vue-ui/components/uni-swipe-action-item/mpwxs.js
  55. 270 0
      yudao-vue-ui/components/uni-swipe-action-item/uni-swipe-action-item.vue
  56. 58 0
      yudao-vue-ui/components/uni-swipe-action/uni-swipe-action.vue
  57. 290 0
      yudao-vue-ui/components/uni-transition/uni-transition.vue
  58. 1250 0
      yudao-vue-ui/components/version-update/base-cloud-mobile.scss
  59. BIN
      yudao-vue-ui/components/version-update/static/airship.png
  60. BIN
      yudao-vue-ui/components/version-update/static/cloudLeft.png
  61. BIN
      yudao-vue-ui/components/version-update/static/cloudRight.png
  62. BIN
      yudao-vue-ui/components/version-update/static/login-wave.png
  63. BIN
      yudao-vue-ui/components/version-update/static/shipAir.png
  64. BIN
      yudao-vue-ui/components/version-update/static/shipGas.png
  65. BIN
      yudao-vue-ui/components/version-update/static/smallCloud.png
  66. BIN
      yudao-vue-ui/components/version-update/static/star.png
  67. 1811 0
      yudao-vue-ui/components/version-update/version-update.vue
  68. 14 0
      yudao-vue-ui/index.html
  69. 21 0
      yudao-vue-ui/main.js
  70. 72 0
      yudao-vue-ui/manifest.json
  71. 43 0
      yudao-vue-ui/pages.json
  72. 52 0
      yudao-vue-ui/pages/index/index.vue
  73. 257 0
      yudao-vue-ui/pages/tabbar/user.vue
  74. BIN
      yudao-vue-ui/static/backgroud/user.jpg
  75. BIN
      yudao-vue-ui/static/icon/arc.png
  76. BIN
      yudao-vue-ui/static/icon/default-avatar.png
  77. BIN
      yudao-vue-ui/static/logo.png
  78. BIN
      yudao-vue-ui/static/tarbar/index-active.png
  79. BIN
      yudao-vue-ui/static/tarbar/index.png
  80. BIN
      yudao-vue-ui/static/tarbar/logo.png
  81. BIN
      yudao-vue-ui/static/tarbar/product-active.png
  82. BIN
      yudao-vue-ui/static/tarbar/product.png
  83. BIN
      yudao-vue-ui/static/tarbar/ucenter-active.png
  84. BIN
      yudao-vue-ui/static/tarbar/ucenter.png
  85. 78 0
      yudao-vue-ui/uni.scss

+ 16 - 0
yudao-vue-ui/.hbuilderx/launch.json

@@ -0,0 +1,16 @@
+{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version": "0.0",
+    "configurations": [{
+     	"default" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"h5" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"type" : "uniCloud"
+     }
+    ]
+}

+ 19 - 0
yudao-vue-ui/App.vue

@@ -0,0 +1,19 @@
+<script>
+	export default {
+		onLaunch: function() {
+			console.log('App Launch')
+		},
+		onShow: function() {
+			console.log('App Show')
+		},
+		onHide: function() {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style>
+	/*每个页面公共css */
+	@import url("./common/css/common.css");
+	@import url("./common/css/icon.css");
+</style>

+ 182 - 0
yudao-vue-ui/common/css/common.css

@@ -0,0 +1,182 @@
+/* #ifndef APP-PLUS-NVUE */
+view,
+scroll-view,
+swiper,
+swiper-item,
+cover-view,
+cover-image,
+icon,
+text,
+rich-text,
+progress,
+button,
+checkbox,
+form,
+input,
+label,
+radio,
+slider,
+switch,
+textarea,
+navigator,
+audio,
+camera,
+image,
+video {
+	box-sizing: border-box;
+}
+image{
+	display: block;
+}
+text{
+	line-height: 1;
+	/* font-family: Helvetica Neue, Helvetica, sans-serif; */
+}
+button{
+	padding: 0;
+	margin: 0;
+	background-color: rgba(0,0,0,0) !important;
+}
+button:after{
+	border: 0;
+}
+.bottom-fill{
+	height: constant(safe-area-inset-bottom);
+	height: env(safe-area-inset-bottom); 
+}
+.fix-bot{
+	box-sizing: content-box;
+	padding-bottom: constant(safe-area-inset-bottom);
+	padding-bottom: env(safe-area-inset-bottom); 
+}
+
+/* 边框 */
+.round{
+	position: relative;
+	border-radius: 100rpx;
+}
+.round:after{
+	content: '';
+	position: absolute;
+	left: 0;
+	top: 0;
+	width: 200%;
+	height: 200%;
+	transform: scale(.5) translate(-50%,-50%);
+	border: 1px solid #878787;
+	border-radius: 100rpx;
+	box-sizing: border-box;
+}
+.b-b:after{
+	position: absolute;
+	z-index: 3;
+	left: 0;
+	top: auto;
+	bottom: 0;
+	right: 0;
+	height: 0;
+	content: '';
+	transform: scaleY(.5);
+	border-bottom: 1px solid #e0e0e0;
+}
+.b-t:before{
+	position: absolute;
+	z-index: 3;
+	left: 0;
+	top: 0;
+	right: 0;
+	height: 0;
+	content: '';
+	transform: scaleY(.5);
+	border-bottom: 1px solid #e5e5e5;
+}
+.b-r:after{
+	position: absolute;
+	z-index: 3;
+	right: 0;
+	top: 0;
+	bottom: 0;
+	width: 0;
+	content: '';
+	transform: scaleX(.5);
+	border-right: 1px solid #e5e5e5;
+}
+.b-l:before{
+	position: absolute;
+	z-index: 3;
+	left: 0;
+	top: 0;
+	bottom: 0;
+	width: 0;
+	content: '';
+	transform: scaleX(.5);
+	border-left: 1px solid #e5e5e5;
+}
+.b-b, .b-t, .b-l, .b-r{
+	position: relative;
+}
+/* 点击态 */
+.hover-gray {
+	background: #fafafa !important;
+}
+.hover-dark {
+	background: #f0f0f0 !important;
+}
+
+.hover-opacity {
+	opacity: 0.7;
+}
+
+/* #endif */
+
+.clamp {
+	/* #ifdef APP-PLUS-NVUE */
+	lines: 1;
+	/* #endif */
+	/* #ifndef APP-PLUS-NVUE */
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	display: block;
+	/* #endif */
+}
+.clamp2 {
+	/* #ifdef APP-PLUS-NVUE */
+	lines: 2;
+	/* #endif */
+	/* #ifndef APP-PLUS-NVUE */
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-line-clamp: 2;
+	overflow: hidden;
+	/* #endif */
+}
+
+/* 布局 */
+.row{
+	/* #ifndef APP-PLUS-NVUE */
+	display:flex;
+	/* #endif */
+	flex-direction:row;
+	align-items: center;
+}
+.column{
+	/* #ifndef APP-PLUS-NVUE */
+	display:flex;
+	/* #endif */
+	flex-direction: column;
+}
+.center{
+	/* #ifndef APP-PLUS-NVUE */
+	display:flex;
+	/* #endif */
+	align-items: center;
+	justify-content: center;
+}
+.fill{
+	flex: 1;
+}
+/* input */
+.placeholder{
+	color: #999 !important;
+}

+ 271 - 0
yudao-vue-ui/common/css/icon.css

@@ -0,0 +1,271 @@
+@font-face {
+	font-family: "mix-icon";
+	font-weight: normal;
+	font-style: normal;
+	src: url('https://at.alicdn.com/t/font_1913318_2ui3nitf38x.ttf') format('truetype');
+}
+
+.mix-icon {
+	font-family: "mix-icon" !important;
+	font-size: 16px;
+	font-style: normal;
+	-webkit-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+}
+
+.icon-fanhui:before {
+  content: "\e7d5";
+}
+
+.icon-shoujihaoma:before {
+  content: "\e7ec";
+}
+
+.icon-close:before {
+  content: "\e60f";
+}
+
+.icon-xingbie-nv:before {
+  content: "\e60e";
+}
+
+.icon-wuliuyunshu:before {
+  content: "\e7ed";
+}
+
+.icon-jingpin:before {
+  content: "\e608";
+}
+
+.icon-zhangdanmingxi01:before {
+  content: "\e637";
+}
+
+.icon-tixian1:before {
+  content: "\e625";
+}
+
+.icon-chongzhi:before {
+  content: "\e605";
+}
+
+.icon-wodezhanghu_zijinjilu:before {
+  content: "\e615";
+}
+
+.icon-tixian:before {
+  content: "\e6ab";
+}
+
+.icon-qianbao:before {
+  content: "\e6c4";
+}
+
+.icon-guanbi1:before {
+  content: "\e61a";
+}
+
+.icon-daipingjia:before {
+  content: "\e604";
+}
+
+.icon-daifahuo:before {
+  content: "\e6bd";
+}
+
+.icon-yue:before {
+  content: "\e600";
+}
+
+.icon-wxpay:before {
+  content: "\e602";
+}
+
+.icon-alipay:before {
+  content: "\e603";
+}
+
+.icon-tishi:before {
+  content: "\e662";
+}
+
+.icon-shoucang-1:before {
+  content: "\e607";
+}
+
+.icon-gouwuche:before {
+  content: "\e657";
+}
+
+.icon-shoucang:before {
+  content: "\e645";
+}
+
+.icon-home:before {
+  content: "\e60c";
+}
+
+.icon-bangzhu1:before {
+  content: "\e63d";
+}
+
+.icon-xingxing:before {
+  content: "\e70b";
+}
+
+.icon-shuxiangliebiao:before {
+  content: "\e635";
+}
+
+.icon-hengxiangliebiao:before {
+  content: "\e636";
+}
+
+.icon-guanbi2:before {
+  content: "\e7be";
+}
+
+.icon-down:before {
+  content: "\e65c";
+}
+
+.icon-arrow-top:before {
+  content: "\e63e";
+}
+
+.icon-xiaoxi:before {
+  content: "\e634";
+}
+
+.icon-saoma:before {
+  content: "\e655";
+}
+
+.icon-dizhi1:before {
+  content: "\e618";
+}
+
+.icon-ditu-copy:before {
+  content: "\e609";
+}
+
+.icon-lajitong:before {
+  content: "\e682";
+}
+
+.icon-bianji:before {
+  content: "\e60d";
+}
+
+.icon-yanzhengma1:before {
+  content: "\e613";
+}
+
+.icon-yanjing:before {
+  content: "\e65b";
+}
+
+.icon-mima:before {
+  content: "\e628";
+}
+
+.icon-biyan:before {
+  content: "\e633";
+}
+
+.icon-iconfontweixin:before {
+  content: "\e611";
+}
+
+.icon-shouye:before {
+  content: "\e626";
+}
+
+.icon-daifukuan:before {
+  content: "\e68f";
+}
+
+.icon-pinglun-copy:before {
+  content: "\e612";
+}
+
+.icon-lishijilu:before {
+  content: "\e6b9";
+}
+
+.icon-shoucang_xuanzhongzhuangtai:before {
+  content: "\e6a9";
+}
+
+.icon-share:before {
+  content: "\e656";
+}
+
+.icon-shezhi1:before {
+  content: "\e61d";
+}
+
+.icon-shouhoutuikuan:before {
+  content: "\e631";
+}
+
+.icon-dizhi:before {
+  content: "\e614";
+}
+
+.icon-yishouhuo:before {
+  content: "\e71a";
+}
+
+.icon-xuanzhong:before {
+  content: "\e632";
+}
+
+.icon-xiangzuo:before {
+  content: "\e653";
+}
+
+.icon-iconfontxingxing:before {
+  content: "\e6b0";
+}
+
+.icon-jia2:before {
+  content: "\e60a";
+}
+
+.icon-sousuo:before {
+  content: "\e7ce";
+}
+
+.icon-xiala:before {
+  content: "\e644";
+}
+
+.icon-xia:before {
+  content: "\e62d";
+}
+
+.icon--jianhao:before {
+  content: "\e60b";
+}
+
+.icon-you:before {
+  content: "\e606";
+}
+
+.icon-yk_yuanquan:before {
+  content: "\e601";
+}
+
+.icon-xing:before {
+  content: "\e627";
+}
+
+.icon-guanbi:before {
+  content: "\e71d";
+}
+
+.icon-loading:before {
+  content: "\e646";
+}
+

+ 630 - 0
yudao-vue-ui/components/jyf-parser/jyf-parser.vue

@@ -0,0 +1,630 @@
+<template>
+	<view>
+		<slot v-if="!nodes.length" />
+		<!--#ifdef APP-PLUS-NVUE-->
+		<web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
+		<!--#endif-->
+		<!--#ifndef APP-PLUS-NVUE-->
+		<view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
+			<!--#ifdef H5 || MP-360-->
+			<div :id="'rtf'+uid"></div>
+			<!--#endif-->
+			<!--#ifndef H5 || MP-360-->
+			<trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
+			<!--#endif-->
+		</view>
+		<!--#endif-->
+	</view>
+</template>
+
+<script>
+	// #ifndef H5 || APP-PLUS-NVUE || MP-360
+	import trees from './libs/trees';
+	var cache = {},
+		// #ifdef MP-WEIXIN || MP-TOUTIAO
+		fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
+		// #endif
+		Parser = require('./libs/MpHtmlParser.js');
+	var dom;
+	// 计算 cache 的 key
+	function hash(str) {
+		for (var i = str.length, val = 5381; i--;)
+			val += (val << 5) + str.charCodeAt(i);
+		return val;
+	}
+	// #endif
+	// #ifdef H5 || APP-PLUS-NVUE || MP-360
+	var windowWidth = uni.getSystemInfoSync().windowWidth,
+		cfg = require('./libs/config.js');
+	// #endif
+	// #ifdef APP-PLUS-NVUE
+	var weexDom = weex.requireModule('dom');
+	// #endif
+	/**
+	 * Parser 富文本组件
+	 * @tutorial https://github.com/jin-yufeng/Parser
+	 * @property {String} html 富文本数据
+	 * @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
+	 * @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
+	 * @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
+	 * @property {Number} compress 压缩等级
+	 * @property {String} domain 图片、视频等链接的主域名
+	 * @property {Boolean} lazyLoad 是否开启图片懒加载
+	 * @property {String} loadingImg 图片加载完成前的占位图
+	 * @property {Boolean} selectable 是否开启长按复制
+	 * @property {Object} tagStyle 标签的默认样式
+	 * @property {Boolean} showWithAnimation 是否使用渐显动画
+	 * @property {Boolean} useAnchor 是否使用锚点
+	 * @property {Boolean} useCache 是否缓存解析结果
+	 * @event {Function} parse 解析完成事件
+	 * @event {Function} load dom 加载完成事件
+	 * @event {Function} ready 所有图片加载完毕事件
+	 * @event {Function} error 错误事件
+	 * @event {Function} imgtap 图片点击事件
+	 * @event {Function} linkpress 链接点击事件
+	 * @author JinYufeng
+	 * @version 20200719
+	 * @listens MIT
+	 */
+	export default {
+		name: 'parser',
+		data() {
+			return {
+				// #ifdef H5 || MP-360
+				uid: this._uid,
+				// #endif
+				// #ifdef APP-PLUS-NVUE
+				height: 1,
+				// #endif
+				// #ifndef APP-PLUS-NVUE
+				showAm: '',
+				// #endif
+				nodes: []
+			}
+		},
+		// #ifndef H5 || APP-PLUS-NVUE || MP-360
+		components: {
+			trees
+		},
+		// #endif
+		props: {
+			html: String,
+			autopause: {
+				type: Boolean,
+				default: true
+			},
+			autoscroll: Boolean,
+			autosetTitle: {
+				type: Boolean,
+				default: true
+			},
+			// #ifndef H5 || APP-PLUS-NVUE || MP-360
+			compress: Number,
+			loadingImg: String,
+			useCache: Boolean,
+			// #endif
+			domain: String,
+			lazyLoad: Boolean,
+			selectable: Boolean,
+			tagStyle: Object,
+			showWithAnimation: Boolean,
+			useAnchor: Boolean
+		},
+		watch: {
+			html(html) {
+				this.setContent(html);
+			}
+		},
+		created() {
+			// 图片数组
+			this.imgList = [];
+			this.imgList.each = function(f) {
+				for (var i = 0, len = this.length; i < len; i++)
+					this.setItem(i, f(this[i], i, this));
+			}
+			this.imgList.setItem = function(i, src) {
+				if (i == void 0 || !src) return;
+				// #ifndef MP-ALIPAY || APP-PLUS
+				// 去重
+				if (src.indexOf('http') == 0 && this.includes(src)) {
+					var newSrc = src.split('://')[0];
+					for (var j = newSrc.length, c; c = src[j]; j++) {
+						if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
+						newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
+					}
+					newSrc += src.substr(j);
+					return this[i] = newSrc;
+				}
+				// #endif
+				this[i] = src;
+				// 暂存 data src
+				if (src.includes('data:image')) {
+					var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
+					if (!info) return;
+					// #ifdef MP-WEIXIN || MP-TOUTIAO
+					filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
+					fs && fs.writeFile({
+						filePath,
+						data: info[3],
+						encoding: info[2],
+						success: () => this[i] = filePath
+					})
+					// #endif
+					// #ifdef APP-PLUS
+					filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
+					var bitmap = new plus.nativeObj.Bitmap();
+					bitmap.loadBase64Data(src, () => {
+						bitmap.save(filePath, {}, () => {
+							bitmap.clear()
+							this[i] = filePath;
+						})
+					})
+					// #endif
+				}
+			}
+		},
+		mounted() {
+			// #ifdef H5 || MP-360
+			this.document = document.getElementById('rtf' + this._uid);
+			// #endif
+			// #ifndef H5 || APP-PLUS-NVUE || MP-360
+			if (dom) this.document = new dom(this);
+			// #endif
+			// #ifdef APP-PLUS-NVUE
+			this.document = this.$refs.web;
+			setTimeout(() => {
+				// #endif
+				if (this.html) this.setContent(this.html);
+				// #ifdef APP-PLUS-NVUE
+			}, 30)
+			// #endif
+		},
+		beforeDestroy() {
+			// #ifdef H5 || MP-360
+			if (this._observer) this._observer.disconnect();
+			// #endif
+			this.imgList.each(src => {
+				// #ifdef APP-PLUS
+				if (src && src.includes('_doc')) {
+					plus.io.resolveLocalFileSystemURL(src, entry => {
+						entry.remove();
+					});
+				}
+				// #endif
+				// #ifdef MP-WEIXIN || MP-TOUTIAO
+				if (src && src.includes(uni.env.USER_DATA_PATH))
+					fs && fs.unlink({
+						filePath: src
+					})
+				// #endif
+			})
+			clearInterval(this._timer);
+		},
+		methods: {
+			// 设置富文本内容
+			setContent(html, append) {
+				// #ifdef APP-PLUS-NVUE
+				if (!html)
+					return this.height = 1;
+				if (append)
+					this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
+						"';document.getElementById('parser').appendChild(b)");
+				else {
+					html =
+						'<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>html,body{width:100%;height:100%;overflow:hidden}body{margin:0}</style><base href="' +
+						this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
+						'</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}document.body.onclick=function(){e({action:"click"})},' +
+						(this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
+						'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight})},50);\x3c/script>';
+					this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
+				}
+				this.$refs.web.evalJs(
+					'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=1;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var a,c=document.getElementsByTagName("img"),s=[],i=0==c.length,d=0,l=0,g=0;a=c[l];l++)parseInt(a.style.width||a.getAttribute("width"))>' +
+					windowWidth + '&&(a.style.height="auto"),a.onload=function(){++d==c.length&&(i=!0)},a.onerror=function(){++d==c.length&&(i=!0),' + (cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
+					'e({action:"error",source:"img",target:this})},a.hasAttribute("ignore")||"A"==a.parentElement.nodeName||(a.i=g++,s.push(a.src),a.onclick=function(){e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var u,m=document.getElementsByTagName("a"),f=0;u=m[f];f++)u.onclick=function(){var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var h,y=document.getElementsByTagName("video"),v=0;h=y[v];v++)h.style.maxWidth="100%",h.onerror=function(){e({action:"error",source:"video",target:this})}' +
+					(this.autopause ? ',h.onplay=function(){for(var e,t=0;e=y[t];t++)e!=this&&e.pause()}' : '') +
+					';for(var _,p=document.getElementsByTagName("audio"),w=0;_=p[w];w++)_.onerror=function(){e({action:"error",source:"audio",target:this})};' +
+					(this.autoscroll ? 'for(var T,E=document.getElementsByTagName("table"),B=0;T=E[B];B++){var N=document.createElement("div");N.style.overflow="scroll",T.parentNode.replaceChild(N,T),N.appendChild(T)}' : '') +
+					'var x=document.getElementById("parser");clearInterval(window.timer),window.timer=setInterval(function(){i&&clearInterval(window.timer),e({action:"ready",ready:i,height:x.scrollHeight})},350)'
+				)
+				this.nodes = [1];
+				// #endif
+				// #ifdef H5 || MP-360
+				if (!html) {
+					if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
+					return;
+				}
+				var div = document.createElement('div');
+				if (!append) {
+					if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
+					this.rtf = div;
+				} else {
+					if (!this.rtf) this.rtf = div;
+					else this.rtf.appendChild(div);
+				}
+				div.innerHTML = this._handleHtml(html, append);
+				for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
+					style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
+					style.setAttribute('scoped', 'true');
+				}
+				// 懒加载
+				if (!this._observer && this.lazyLoad && IntersectionObserver) {
+					this._observer = new IntersectionObserver(changes => {
+						for (let item, i = 0; item = changes[i++];) {
+							if (item.isIntersecting) {
+								item.target.src = item.target.getAttribute('data-src');
+								item.target.removeAttribute('data-src');
+								this._observer.unobserve(item.target);
+							}
+						}
+					}, {
+						rootMargin: '500px 0px 500px 0px'
+					})
+				}
+				var _ts = this;
+				// 获取标题
+				var title = this.rtf.getElementsByTagName('title');
+				if (title.length && this.autosetTitle)
+					uni.setNavigationBarTitle({
+						title: title[0].innerText
+					})
+				// 图片处理
+				this.imgList.length = 0;
+				var imgs = this.rtf.getElementsByTagName('img');
+				for (let i = 0, j = 0, img; img = imgs[i]; i++) {
+					if (parseInt(img.style.width || img.getAttribute('width')) > windowWidth)
+						img.style.height = 'auto';
+					var src = img.getAttribute('src');
+					if (this.domain && src) {
+						if (src[0] == '/') {
+							if (src[1] == '/')
+								img.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
+							else img.src = this.domain + src;
+						} else if (!src.includes('://')) img.src = this.domain + '/' + src;
+					}
+					if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
+						img.i = j++;
+						_ts.imgList.push(img.src || img.getAttribute('data-src'));
+						img.onclick = function() {
+							var preview = true;
+							this.ignore = () => preview = false;
+							_ts.$emit('imgtap', this);
+							if (preview) {
+								uni.previewImage({
+									current: this.i,
+									urls: _ts.imgList
+								});
+							}
+						}
+					}
+					img.onerror = function() {
+						if (cfg.errorImg)
+							_ts.imgList[this.i] = this.src = cfg.errorImg;
+						_ts.$emit('error', {
+							source: 'img',
+							target: this
+						});
+					}
+					if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
+						img.setAttribute('data-src', img.src);
+						img.removeAttribute('src');
+						this._observer.observe(img);
+					}
+				}
+				// 链接处理
+				var links = this.rtf.getElementsByTagName('a');
+				for (var link of links) {
+					link.onclick = function() {
+						var jump = true,
+							href = this.getAttribute('href');
+						_ts.$emit('linkpress', {
+							href,
+							ignore: () => jump = false
+						});
+						if (jump && href) {
+							if (href[0] == '#') {
+								if (_ts.useAnchor) {
+									_ts.navigateTo({
+										id: href.substr(1)
+									})
+								}
+							} else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
+								return true;
+							else
+								uni.navigateTo({
+									url: href
+								})
+						}
+						return false;
+					}
+				}
+				// 视频处理
+				var videos = this.rtf.getElementsByTagName('video');
+				_ts.videoContexts = videos;
+				for (let video, i = 0; video = videos[i++];) {
+					video.style.maxWidth = '100%';
+					video.onerror = function() {
+						_ts.$emit('error', {
+							source: 'video',
+							target: this
+						});
+					}
+					video.onplay = function() {
+						if (_ts.autopause)
+							for (let item, i = 0; item = _ts.videoContexts[i++];)
+								if (item != this) item.pause();
+					}
+				}
+				// 音频处理
+				var audios = this.rtf.getElementsByTagName('audio');
+				for (var audio of audios)
+					audio.onerror = function() {
+						_ts.$emit('error', {
+							source: 'audio',
+							target: this
+						});
+					}
+				// 表格处理
+				if (this.autoscroll) {
+					var tables = this.rtf.getElementsByTagName('table');
+					for (var table of tables) {
+						let div = document.createElement('div');
+						div.style.overflow = 'scroll';
+						table.parentNode.replaceChild(div, table);
+						div.appendChild(table);
+					}
+				}
+				if (!append) this.document.appendChild(this.rtf);
+				this.$nextTick(() => {
+					this.nodes = [1];
+					this.$emit('load');
+				});
+				setTimeout(() => this.showAm = '', 500);
+				// #endif
+				// #ifndef APP-PLUS-NVUE
+				// #ifndef H5 || MP-360
+				var nodes;
+				if (!html) return this.nodes = [];
+				var parser = new Parser(html, this);
+				// 缓存读取
+				if (this.useCache) {
+					var hashVal = hash(html);
+					if (cache[hashVal])
+						nodes = cache[hashVal];
+					else {
+						nodes = parser.parse();
+						cache[hashVal] = nodes;
+					}
+				} else nodes = parser.parse();
+				this.$emit('parse', nodes);
+				if (append) this.nodes = this.nodes.concat(nodes);
+				else this.nodes = nodes;
+				if (nodes.length && nodes.title && this.autosetTitle)
+					uni.setNavigationBarTitle({
+						title: nodes.title
+					})
+				if (this.imgList) this.imgList.length = 0;
+				this.videoContexts = [];
+				this.$nextTick(() => {
+					(function f(cs) {
+						for (var i = cs.length; i--;) {
+							if (cs[i].top) {
+								cs[i].controls = [];
+								cs[i].init();
+								f(cs[i].$children);
+							}
+						}
+					})(this.$children)
+					this.$emit('load');
+				})
+				// #endif
+				var height;
+				clearInterval(this._timer);
+				this._timer = setInterval(() => {
+					// #ifdef H5 || MP-360
+					this.rect = this.rtf.getBoundingClientRect();
+					// #endif
+					// #ifndef H5 || MP-360
+					uni.createSelectorQuery().in(this)
+						.select('#_top').boundingClientRect().exec(res => {
+							if (!res) return;
+							this.rect = res[0];
+							// #endif
+							if (this.rect.height == height) {
+								this.$emit('ready', this.rect)
+								clearInterval(this._timer);
+							}
+							height = this.rect.height;
+							// #ifndef H5 || MP-360
+						});
+					// #endif
+				}, 350);
+				if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
+				// #endif
+			},
+			// 获取文本内容
+			getText(ns = this.nodes) {
+				var txt = '';
+				// #ifdef APP-PLUS-NVUE
+				txt = this._text;
+				// #endif
+				// #ifdef H5 || MP-360
+				txt = this.rtf.innerText;
+				// #endif
+				// #ifndef H5 || APP-PLUS-NVUE || MP-360
+				for (var i = 0, n; n = ns[i++];) {
+					if (n.type == 'text') txt += n.text.replace(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
+						.replace(/&amp;/g, '&');
+					else if (n.type == 'br') txt += '\n';
+					else {
+						// 块级标签前后加换行
+						var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
+							'0' && n.name[1] < '7');
+						if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
+						if (n.children) txt += this.getText(n.children);
+						if (block && txt[txt.length - 1] != '\n') txt += '\n';
+						else if (n.name == 'td' || n.name == 'th') txt += '\t';
+					}
+				}
+				// #endif
+				return txt;
+			},
+			// 锚点跳转
+			in (obj) {
+				if (obj.page && obj.selector && obj.scrollTop) this._in = obj;
+			},
+			navigateTo(obj) {
+				if (!this.useAnchor) return obj.fail && obj.fail('Anchor is disabled');
+				// #ifdef APP-PLUS-NVUE
+				if (!obj.id)
+					weexDom.scrollToElement(this.$refs.web);
+				else
+					this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
+						'");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
+				obj.success && obj.success();
+				// #endif
+				// #ifndef APP-PLUS-NVUE
+				var d = ' ';
+				// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
+				d = '>>>';
+				// #endif
+				var selector = uni.createSelectorQuery().in(this._in ? this._in.page : this).select((this._in ? this._in.selector :
+					'#_top') + (obj.id ? `${d}#${obj.id},${this._in?this._in.selector:'#_top'}${d}.${obj.id}` : '')).boundingClientRect();
+				if (this._in) selector.select(this._in.selector).scrollOffset().select(this._in.selector).boundingClientRect();
+				else selector.selectViewport().scrollOffset();
+				selector.exec(res => {
+					if (!res[0]) return obj.fail && obj.fail('Label not found')
+					var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (obj.offset || 0);
+					if (this._in) this._in.page[this._in.scrollTop] = scrollTop;
+					else uni.pageScrollTo({
+						scrollTop,
+						duration: 300
+					})
+					obj.success && obj.success();
+				})
+				// #endif
+			},
+			// 获取视频对象
+			getVideoContext(id) {
+				// #ifndef APP-PLUS-NVUE
+				if (!id) return this.videoContexts;
+				else
+					for (var i = this.videoContexts.length; i--;)
+						if (this.videoContexts[i].id == id) return this.videoContexts[i];
+				// #endif
+			},
+			// #ifdef H5 || APP-PLUS-NVUE || MP-360
+			_handleHtml(html, append) {
+				if (!append) {
+					// 处理 tag-style 和 userAgentStyles
+					var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%}';
+					for (var item in cfg.userAgentStyles)
+						style += `${item}{${cfg.userAgentStyles[item]}}`;
+					for (item in this.tagStyle)
+						style += `${item}{${this.tagStyle[item]}}`;
+					style += '</style>';
+					html = style + html;
+				}
+				// 处理 rpx
+				if (html.includes('rpx'))
+					html = html.replace(/[0-9.]+\s*rpx/g, $ => (parseFloat($) * windowWidth / 750) + 'px');
+				return html;
+			},
+			// #endif
+			// #ifdef APP-PLUS-NVUE
+			_message(e) {
+				// 接收 web-view 消息
+				var d = e.detail.data[0];
+				switch (d.action) {
+					case 'load':
+						this.$emit('load');
+						this.height = d.height;
+						this._text = d.text;
+						break;
+					case 'getTitle':
+						if (this.autosetTitle)
+							uni.setNavigationBarTitle({
+								title: d.title
+							})
+						break;
+					case 'getImgList':
+						this.imgList.length = 0;
+						for (var i = d.imgList.length; i--;)
+							this.imgList.setItem(i, d.imgList[i]);
+						break;
+					case 'preview':
+						var preview = true;
+						d.img.ignore = () => preview = false;
+						this.$emit('imgtap', d.img);
+						if (preview)
+							uni.previewImage({
+								current: d.img.i,
+								urls: this.imgList
+							})
+						break;
+					case 'linkpress':
+						var jump = true,
+							href = d.href;
+						this.$emit('linkpress', {
+							href,
+							ignore: () => jump = false
+						})
+						if (jump && href) {
+							if (href[0] == '#') {
+								if (this.useAnchor)
+									weexDom.scrollToElement(this.$refs.web, {
+										offset: d.offset
+									})
+							} else if (href.includes('://'))
+								plus.runtime.openWeb(href);
+							else
+								uni.navigateTo({
+									url: href
+								})
+						}
+						break;
+					case 'error':
+						if (d.source == 'img' && cfg.errorImg)
+							this.imgList.setItem(d.target.i, cfg.errorImg);
+						this.$emit('error', {
+							source: d.source,
+							target: d.target
+						})
+						break;
+					case 'ready':
+						this.height = d.height;
+						if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
+							this.rect = res[0];
+							this.$emit('ready', res[0]);
+						})
+						break;
+					case 'click':
+						this.$emit('click');
+						this.$emit('tap');
+				}
+			},
+			// #endif
+		}
+	}
+</script>
+
+<style>
+	@keyframes _show {
+		0% {
+			opacity: 0;
+		}
+
+		100% {
+			opacity: 1;
+		}
+	}
+
+	/* #ifdef MP-WEIXIN */
+	:host {
+		display: block;
+		overflow: scroll;
+		-webkit-overflow-scrolling: touch;
+	}
+
+	/* #endif */
+</style>

+ 97 - 0
yudao-vue-ui/components/jyf-parser/libs/CssHandler.js

@@ -0,0 +1,97 @@
+const cfg = require('./config.js'),
+	isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+
+function CssHandler(tagStyle) {
+	var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
+	for (var item in tagStyle)
+		styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
+	this.styles = styles;
+}
+CssHandler.prototype.getStyle = function(data) {
+	this.styles = new parser(data, this.styles).parse();
+}
+CssHandler.prototype.match = function(name, attrs) {
+	var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
+	if (attrs.class) {
+		var items = attrs.class.split(' ');
+		for (var i = 0, item; item = items[i]; i++)
+			if (tmp = this.styles['.' + item])
+				matched += tmp + ';';
+	}
+	if (tmp = this.styles['#' + attrs.id])
+		matched += tmp + ';';
+	return matched;
+}
+module.exports = CssHandler;
+
+function parser(data, init) {
+	this.data = data;
+	this.floor = 0;
+	this.i = 0;
+	this.list = [];
+	this.res = init;
+	this.state = this.Space;
+}
+parser.prototype.parse = function() {
+	for (var c; c = this.data[this.i]; this.i++)
+		this.state(c);
+	return this.res;
+}
+parser.prototype.section = function() {
+	return this.data.substring(this.start, this.i);
+}
+// 状态机
+parser.prototype.Space = function(c) {
+	if (c == '.' || c == '#' || isLetter(c)) {
+		this.start = this.i;
+		this.state = this.Name;
+	} else if (c == '/' && this.data[this.i + 1] == '*')
+		this.Comment();
+	else if (!cfg.blankChar[c] && c != ';')
+		this.state = this.Ignore;
+}
+parser.prototype.Comment = function() {
+	this.i = this.data.indexOf('*/', this.i) + 1;
+	if (!this.i) this.i = this.data.length;
+	this.state = this.Space;
+}
+parser.prototype.Ignore = function(c) {
+	if (c == '{') this.floor++;
+	else if (c == '}' && !--this.floor) this.state = this.Space;
+}
+parser.prototype.Name = function(c) {
+	if (cfg.blankChar[c]) {
+		this.list.push(this.section());
+		this.state = this.NameSpace;
+	} else if (c == '{') {
+		this.list.push(this.section());
+		this.Content();
+	} else if (c == ',') {
+		this.list.push(this.section());
+		this.Comma();
+	} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
+		this.state = this.Ignore;
+}
+parser.prototype.NameSpace = function(c) {
+	if (c == '{') this.Content();
+	else if (c == ',') this.Comma();
+	else if (!cfg.blankChar[c]) this.state = this.Ignore;
+}
+parser.prototype.Comma = function() {
+	while (cfg.blankChar[this.data[++this.i]]);
+	if (this.data[this.i] == '{') this.Content();
+	else {
+		this.start = this.i--;
+		this.state = this.Name;
+	}
+}
+parser.prototype.Content = function() {
+	this.start = ++this.i;
+	if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
+	var content = this.section();
+	for (var i = 0, item; item = this.list[i++];)
+		if (this.res[item]) this.res[item] += ';' + content;
+		else this.res[item] = content;
+	this.list = [];
+	this.state = this.Space;
+}

+ 534 - 0
yudao-vue-ui/components/jyf-parser/libs/MpHtmlParser.js

@@ -0,0 +1,534 @@
+/**
+ * html 解析器
+ * @tutorial https://github.com/jin-yufeng/Parser
+ * @version 20200719
+ * @author JinYufeng
+ * @listens MIT
+ */
+const cfg = require('./config.js'),
+	blankChar = cfg.blankChar,
+	CssHandler = require('./CssHandler.js'),
+	windowWidth = uni.getSystemInfoSync().windowWidth;
+var emoji;
+
+function MpHtmlParser(data, options = {}) {
+	this.attrs = {};
+	this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
+	this.data = data;
+	this.domain = options.domain;
+	this.DOM = [];
+	this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
+	options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
+	this.options = options;
+	this.state = this.Text;
+	this.STACK = [];
+	// 工具函数
+	this.bubble = () => {
+		for (var i = this.STACK.length, item; item = this.STACK[--i];) {
+			if (cfg.richOnlyTags[item.name]) {
+				if (item.name == 'table' && !Object.hasOwnProperty.call(item, 'c')) item.c = 1;
+				return false;
+			}
+			item.c = 1;
+		}
+		return true;
+	}
+	this.decode = (val, amp) => {
+		var i = -1,
+			j, en;
+		while (1) {
+			if ((i = val.indexOf('&', i + 1)) == -1) break;
+			if ((j = val.indexOf(';', i + 2)) == -1) break;
+			if (val[i + 1] == '#') {
+				en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
+				if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
+			} else {
+				en = val.substring(i + 1, j);
+				if (cfg.entities[en] || en == amp)
+					val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
+			}
+		}
+		return val;
+	}
+	this.getUrl = url => {
+		if (url[0] == '/') {
+			if (url[1] == '/') url = this.options.prot + ':' + url;
+			else if (this.domain) url = this.domain + url;
+		} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
+			url = this.domain + '/' + url;
+		return url;
+	}
+	this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
+	this.section = () => this.data.substring(this.start, this.i);
+	this.parent = () => this.STACK[this.STACK.length - 1];
+	this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
+}
+MpHtmlParser.prototype.parse = function() {
+	if (emoji) this.data = emoji.parseEmoji(this.data);
+	for (var c; c = this.data[this.i]; this.i++)
+		this.state(c);
+	if (this.state == this.Text) this.setText();
+	while (this.STACK.length) this.popNode(this.STACK.pop());
+	return this.DOM;
+}
+// 设置属性
+MpHtmlParser.prototype.setAttr = function() {
+	var name = this.attrName.toLowerCase(),
+		val = this.attrVal;
+	if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
+	else if (val) {
+		if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
+		else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
+		else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
+	}
+	this.attrVal = '';
+	while (blankChar[this.data[this.i]]) this.i++;
+	if (this.isClose()) this.setNode();
+	else {
+		this.start = this.i;
+		this.state = this.AttrName;
+	}
+}
+// 设置文本节点
+MpHtmlParser.prototype.setText = function() {
+	var back, text = this.section();
+	if (!text) return;
+	text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
+	if (back) {
+		this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
+		let j = this.start + text.length;
+		for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
+		return;
+	}
+	if (!this.pre) {
+		// 合并空白符
+		var tmp = [];
+		for (let i = text.length, c; c = text[--i];)
+			if (!blankChar[c] || (!blankChar[tmp[0]] && (c = ' '))) tmp.unshift(c);
+		text = tmp.join('');
+	}
+	this.siblings().push({
+		type: 'text',
+		text: this.decode(text)
+	});
+}
+// 设置元素节点
+MpHtmlParser.prototype.setNode = function() {
+	var node = {
+			name: this.tagName.toLowerCase(),
+			attrs: this.attrs
+		},
+		close = cfg.selfClosingTags[node.name];
+	this.attrs = {};
+	if (!cfg.ignoreTags[node.name]) {
+		// 处理属性
+		var attrs = node.attrs,
+			style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
+			styleObj = {};
+		if (attrs.id) {
+			if (this.options.compress & 1) attrs.id = void 0;
+			else if (this.options.useAnchor) this.bubble();
+		}
+		if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
+		switch (node.name) {
+			case 'a':
+			case 'ad': // #ifdef APP-PLUS
+			case 'iframe':
+				// #endif
+				this.bubble();
+				break;
+			case 'font':
+				if (attrs.color) {
+					styleObj['color'] = attrs.color;
+					attrs.color = void 0;
+				}
+				if (attrs.face) {
+					styleObj['font-family'] = attrs.face;
+					attrs.face = void 0;
+				}
+				if (attrs.size) {
+					var size = parseInt(attrs.size);
+					if (size < 1) size = 1;
+					else if (size > 7) size = 7;
+					var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
+					styleObj['font-size'] = map[size - 1];
+					attrs.size = void 0;
+				}
+				break;
+			case 'embed':
+				// #ifndef APP-PLUS
+				var src = node.attrs.src || '',
+					type = node.attrs.type || '';
+				if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
+					node.name = 'video';
+				else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
+						'.aac'))
+					node.name = 'audio';
+				else break;
+				if (node.attrs.autostart)
+					node.attrs.autoplay = 'T';
+				node.attrs.controls = 'T';
+				// #endif
+				// #ifdef APP-PLUS
+				this.bubble();
+				break;
+				// #endif
+			case 'video':
+			case 'audio':
+				if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
+				else this[`${node.name}Num`]++;
+				if (node.name == 'video') {
+					if (this.videoNum > 3)
+						node.lazyLoad = 1;
+					if (attrs.width) {
+						styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
+						attrs.width = void 0;
+					}
+					if (attrs.height) {
+						styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
+						attrs.height = void 0;
+					}
+				}
+				attrs.source = [];
+				if (attrs.src) {
+					attrs.source.push(attrs.src);
+					attrs.src = void 0;
+				}
+				this.bubble();
+				break;
+			case 'td':
+			case 'th':
+				if (attrs.colspan || attrs.rowspan)
+					for (var k = this.STACK.length, item; item = this.STACK[--k];)
+						if (item.name == 'table') {
+							item.c = void 0;
+							break;
+						}
+		}
+		if (attrs.align) {
+			styleObj['text-align'] = attrs.align;
+			attrs.align = void 0;
+		}
+		// 压缩 style
+		var styles = style.split(';');
+		style = '';
+		for (var i = 0, len = styles.length; i < len; i++) {
+			var info = styles[i].split(':');
+			if (info.length < 2) continue;
+			let key = info[0].trim().toLowerCase(),
+				value = info.slice(1).join(':').trim();
+			if (value.includes('-webkit') || value.includes('-moz') || value.includes('-ms') || value.includes('-o') || value.includes(
+					'safe'))
+				style += `;${key}:${value}`;
+			else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
+				styleObj[key] = value;
+		}
+		if (node.name == 'img') {
+			if (attrs.src && !attrs.ignore) {
+				if (this.bubble())
+					attrs.i = (this.imgNum++).toString();
+				else attrs.ignore = 'T';
+			}
+			if (attrs.ignore) {
+				style += ';-webkit-touch-callout:none';
+				styleObj['max-width'] = '100%';
+			}
+			var width;
+			if (styleObj.width) width = styleObj.width;
+			else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : attrs.width + 'px';
+			if (width) {
+				styleObj.width = width;
+				attrs.width = '100%';
+				if (parseInt(width) > windowWidth) {
+					styleObj.height = '';
+					if (attrs.height) attrs.height = void 0;
+				}
+			}
+			if (styleObj.height) {
+				attrs.height = styleObj.height;
+				styleObj.height = '';
+			} else if (attrs.height && !attrs.height.includes('%'))
+				attrs.height += 'px';
+		}
+		for (var key in styleObj) {
+			var value = styleObj[key];
+			if (!value) continue;
+			if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
+			// 填充链接
+			if (value.includes('url')) {
+				var j = value.indexOf('(');
+				if (j++ != -1) {
+					while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
+					value = value.substr(0, j) + this.getUrl(value.substr(j));
+				}
+			}
+			// 转换 rpx
+			else if (value.includes('rpx'))
+				value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
+			else if (key == 'white-space' && value.includes('pre') && !close)
+				this.pre = node.pre = true;
+			style += `;${key}:${value}`;
+		}
+		style = style.substr(1);
+		if (style) attrs.style = style;
+		if (!close) {
+			node.children = [];
+			if (node.name == 'pre' && cfg.highlight) {
+				this.remove(node);
+				this.pre = node.pre = true;
+			}
+			this.siblings().push(node);
+			this.STACK.push(node);
+		} else if (!cfg.filter || cfg.filter(node, this) != false)
+			this.siblings().push(node);
+	} else {
+		if (!close) this.remove(node);
+		else if (node.name == 'source') {
+			var parent = this.parent();
+			if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
+				parent.attrs.source.push(node.attrs.src);
+		} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
+	}
+	if (this.data[this.i] == '/') this.i++;
+	this.start = this.i + 1;
+	this.state = this.Text;
+}
+// 移除标签
+MpHtmlParser.prototype.remove = function(node) {
+	var name = node.name,
+		j = this.i;
+	// 处理 svg
+	var handleSvg = () => {
+		var src = this.data.substring(j, this.i + 1);
+		if (!node.attrs.xmlns) src = ' xmlns="http://www.w3.org/2000/svg"' + src;
+		var i = j;
+		while (this.data[j] != '<') j--;
+		src = this.data.substring(j, i).replace("viewbox", "viewBox") + src;
+		var parent = this.parent();
+		if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
+			parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
+		this.siblings().push({
+			name: 'img',
+			attrs: {
+				src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
+				style: (/vertical[^;]+/.exec(node.attrs.style) || []).shift(),
+				ignore: 'T'
+			}
+		})
+	}
+	if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
+	while (1) {
+		if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
+			if (name == 'pre' || name == 'svg') this.i = j;
+			else this.i = this.data.length;
+			return;
+		}
+		this.start = (this.i += 2);
+		while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
+		if (this.section().toLowerCase() == name) {
+			// 代码块高亮
+			if (name == 'pre') {
+				this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
+					.substr(this.i - 5);
+				return this.i = j;
+			} else if (name == 'style')
+				this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
+			else if (name == 'title')
+				this.DOM.title = this.data.substring(j + 1, this.i - 7);
+			if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
+			if (name == 'svg') handleSvg();
+			return;
+		}
+	}
+}
+// 节点出栈处理
+MpHtmlParser.prototype.popNode = function(node) {
+	// 空白符处理
+	if (node.pre) {
+		node.pre = this.pre = void 0;
+		for (let i = this.STACK.length; i--;)
+			if (this.STACK[i].pre)
+				this.pre = true;
+	}
+	var siblings = this.siblings(),
+		len = siblings.length,
+		childs = node.children;
+	if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
+		return siblings.pop();
+	var attrs = node.attrs;
+	// 替换一些标签名
+	if (cfg.blockTags[node.name]) node.name = 'div';
+	else if (!cfg.trustTags[node.name]) node.name = 'span';
+	// 去除块标签前后空串
+	if (node.name == 'div' || node.name == 'p' || node.name[0] == 't') {
+		if (len > 1 && siblings[len - 2].text == ' ')
+			siblings.splice(--len - 1, 1);
+		if (childs.length && childs[childs.length - 1].text == ' ')
+			childs.pop();
+	}
+	// 处理列表
+	if (node.c && (node.name == 'ul' || node.name == 'ol')) {
+		if ((node.attrs.style || '').includes('list-style:none')) {
+			for (let i = 0, child; child = childs[i++];)
+				if (child.name == 'li')
+					child.name = 'div';
+		} else if (node.name == 'ul') {
+			var floor = 1;
+			for (let i = this.STACK.length; i--;)
+				if (this.STACK[i].name == 'ul') floor++;
+			if (floor != 1)
+				for (let i = childs.length; i--;)
+					childs[i].floor = floor;
+		} else {
+			for (let i = 0, num = 1, child; child = childs[i++];)
+				if (child.name == 'li') {
+					child.type = 'ol';
+					child.num = ((num, type) => {
+						if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
+						if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
+						if (type == 'i' || type == 'I') {
+							num = (num - 1) % 99 + 1;
+							var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
+								ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
+								res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
+							if (type == 'i') return res.toLowerCase();
+							return res;
+						}
+						return num;
+					})(num++, attrs.type) + '.';
+				}
+		}
+	}
+	// 处理表格的边框
+	if (node.name == 'table') {
+		var padding = attrs.cellpadding,
+			spacing = attrs.cellspacing,
+			border = attrs.border;
+		if (node.c) {
+			this.bubble();
+			attrs.style = (attrs.style || '') + ';display:table';
+			if (!padding) padding = 2;
+			if (!spacing) spacing = 2;
+		}
+		if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
+		if (spacing) attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
+		if (border || padding || node.c)
+			(function f(ns) {
+				for (var i = 0, n; n = ns[i]; i++) {
+					if (n.type == 'text') continue;
+					var style = n.attrs.style || '';
+					if (node.c && n.name[0] == 't') {
+						n.c = 1;
+						style += ';display:table-' + (n.name == 'th' || n.name == 'td' ? 'cell' : (n.name == 'tr' ? 'row' : 'row-group'));
+					}
+					if (n.name == 'th' || n.name == 'td') {
+						if (border) style = `border:${border}px solid gray;${style}`;
+						if (padding) style = `padding:${padding}px;${style}`;
+					} else f(n.children || []);
+					if (style) n.attrs.style = style;
+				}
+			})(childs)
+		if (this.options.autoscroll) {
+			var table = Object.assign({}, node);
+			node.name = 'div';
+			node.attrs = {
+				style: 'overflow:scroll'
+			}
+			node.children = [table];
+		}
+	}
+	this.CssHandler.pop && this.CssHandler.pop(node);
+	// 自动压缩
+	if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
+		siblings[len - 1] = childs[0];
+}
+// 状态机
+MpHtmlParser.prototype.Text = function(c) {
+	if (c == '<') {
+		var next = this.data[this.i + 1],
+			isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+		if (isLetter(next)) {
+			this.setText();
+			this.start = this.i + 1;
+			this.state = this.TagName;
+		} else if (next == '/') {
+			this.setText();
+			if (isLetter(this.data[++this.i + 1])) {
+				this.start = this.i + 1;
+				this.state = this.EndTag;
+			} else this.Comment();
+		} else if (next == '!' || next == '?') {
+			this.setText();
+			this.Comment();
+		}
+	}
+}
+MpHtmlParser.prototype.Comment = function() {
+	var key;
+	if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
+	else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
+	else key = '>';
+	if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
+	else this.i += key.length - 1;
+	this.start = this.i + 1;
+	this.state = this.Text;
+}
+MpHtmlParser.prototype.TagName = function(c) {
+	if (blankChar[c]) {
+		this.tagName = this.section();
+		while (blankChar[this.data[this.i]]) this.i++;
+		if (this.isClose()) this.setNode();
+		else {
+			this.start = this.i;
+			this.state = this.AttrName;
+		}
+	} else if (this.isClose()) {
+		this.tagName = this.section();
+		this.setNode();
+	}
+}
+MpHtmlParser.prototype.AttrName = function(c) {
+	if (c == '=' || blankChar[c] || this.isClose()) {
+		this.attrName = this.section();
+		if (blankChar[c])
+			while (blankChar[this.data[++this.i]]);
+		if (this.data[this.i] == '=') {
+			while (blankChar[this.data[++this.i]]);
+			this.start = this.i--;
+			this.state = this.AttrValue;
+		} else this.setAttr();
+	}
+}
+MpHtmlParser.prototype.AttrValue = function(c) {
+	if (c == '"' || c == "'") {
+		this.start++;
+		if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
+		this.attrVal = this.section();
+		this.i++;
+	} else {
+		for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
+		this.attrVal = this.section();
+	}
+	this.setAttr();
+}
+MpHtmlParser.prototype.EndTag = function(c) {
+	if (blankChar[c] || c == '>' || c == '/') {
+		var name = this.section().toLowerCase();
+		for (var i = this.STACK.length; i--;)
+			if (this.STACK[i].name == name) break;
+		if (i != -1) {
+			var node;
+			while ((node = this.STACK.pop()).name != name) this.popNode(node);
+			this.popNode(node);
+		} else if (name == 'p' || name == 'br')
+			this.siblings().push({
+				name,
+				attrs: {}
+			});
+		this.i = this.data.indexOf('>', this.i);
+		this.start = this.i + 1;
+		if (this.i == -1) this.i = this.data.length;
+		else this.state = this.Text;
+	}
+}
+module.exports = MpHtmlParser;

+ 93 - 0
yudao-vue-ui/components/jyf-parser/libs/config.js

@@ -0,0 +1,93 @@
+/* 配置文件 */
+// #ifdef MP-WEIXIN
+const canIUse = wx.canIUse('editor'); // 高基础库标识,用于兼容
+// #endif
+module.exports = {
+	// 出错占位图
+	errorImg: null,
+	// 过滤器函数
+	filter: null,
+	// 代码高亮函数
+	highlight: null,
+	// 文本处理函数
+	onText: null,
+	// 实体编码列表
+	entities: {
+		quot: '"',
+		apos: "'",
+		semi: ';',
+		nbsp: '\xA0',
+		ensp: '\u2002',
+		emsp: '\u2003',
+		ndash: '–',
+		mdash: '—',
+		middot: '·',
+		lsquo: '‘',
+		rsquo: '’',
+		ldquo: '“',
+		rdquo: '”',
+		bull: '•',
+		hellip: '…'
+	},
+	blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
+	boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
+	// 块级标签,将被转为 div
+	blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,section' + (
+		// #ifdef MP-WEIXIN
+		canIUse ? '' :
+		// #endif
+		',pre')),
+	// 将被移除的标签
+	ignoreTags: makeMap(
+		'area,base,canvas,frame,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'
+		// #ifdef MP-WEIXIN
+		+ (canIUse ? ',rp' : '')
+		// #endif
+		// #ifndef APP-PLUS
+		+ ',iframe'
+		// #endif
+	),
+	// 只能被 rich-text 显示的标签
+	richOnlyTags: makeMap('a,colgroup,fieldset,legend,table'
+		// #ifdef MP-WEIXIN
+		+ (canIUse ? ',bdi,bdo,caption,rt,ruby' : '')
+		// #endif
+	),
+	// 自闭合的标签
+	selfClosingTags: makeMap(
+		'area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
+	),
+	// 信任的标签
+	trustTags: makeMap(
+		'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
+		// #ifdef MP-WEIXIN
+		+ (canIUse ? ',bdi,bdo,caption,pre,rt,ruby' : '')
+		// #endif
+		// #ifdef APP-PLUS
+		+ ',embed,iframe'
+		// #endif
+	),
+	// 默认的标签样式
+	userAgentStyles: {
+		address: 'font-style:italic',
+		big: 'display:inline;font-size:1.2em',
+		blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
+		caption: 'display:table-caption;text-align:center',
+		center: 'text-align:center',
+		cite: 'font-style:italic',
+		dd: 'margin-left:40px',
+		mark: 'background-color:yellow',
+		pre: 'font-family:monospace;white-space:pre;overflow:scroll',
+		s: 'text-decoration:line-through',
+		small: 'display:inline;font-size:0.8em',
+		u: 'text-decoration:underline'
+	}
+}
+
+function makeMap(str) {
+	var map = Object.create(null),
+		list = str.split(',');
+	for (var i = list.length; i--;)
+		map[list[i]] = true;
+	return map;
+}

+ 22 - 0
yudao-vue-ui/components/jyf-parser/libs/handler.wxs

@@ -0,0 +1,22 @@
+var inline = {
+	abbr: 1,
+	b: 1,
+	big: 1,
+	code: 1,
+	del: 1,
+	em: 1,
+	i: 1,
+	ins: 1,
+	label: 1,
+	q: 1,
+	small: 1,
+	span: 1,
+	strong: 1,
+	sub: 1,
+	sup: 1
+}
+module.exports = {
+	use: function(item) {
+		return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
+	}
+}

+ 500 - 0
yudao-vue-ui/components/jyf-parser/libs/trees.vue

@@ -0,0 +1,500 @@
+<template>
+	<view :class="'interlayer '+(c||'')" :style="s">
+		<block v-for="(n, i) in nodes" v-bind:key="i">
+			<!--图片-->
+			<view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap="imgtap">
+				<rich-text v-if="controls[i]!=0" :nodes="[{attrs:{src:loading&&(controls[i]||0)<2?loading:(lazyLoad&&!controls[i]?placeholder:(controls[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
+				<image class="_image" :src="lazyLoad&&!controls[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
+				 :show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
+				 @error="error" />
+			</view>
+			<!--文本-->
+			<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
+			<!--#ifndef MP-BAIDU-->
+			<text v-else-if="n.name=='br'">\n</text>
+			<!--#endif-->
+			<!--视频-->
+			<view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&controls[i]==undefined" :id="n.attrs.id" :class="'_video '+(n.attrs.class||'')"
+			 :style="n.attrs.style" :data-i="i" @tap="_loadVideo" />
+			<video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||controls[i]==0"
+			 :controls="!n.attrs.autoplay||n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[controls[i]||0]"
+			 :unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
+			<!--音频-->
+			<audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
+			 :autoplay="n.attrs.autoplay" :controls="!n.attrs.autoplay||n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
+			 :src="n.attrs.source[controls[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio"
+			 @error.native="error" @play.native="play" />
+			<!--链接-->
+			<view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
+			 :data-attrs="n.attrs" @tap="linkpress">
+				<trees class="_span" c="_span" :nodes="n.children" />
+			</view>
+			<!--广告-->
+			<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
+			<!--列表-->
+			<view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex'">
+				<view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
+				<view v-else class="_ul-bef">
+					<view v-if="n.floor%3==0" class="_ul-p1">█</view>
+					<view v-else-if="n.floor%3==2" class="_ul-p2" />
+					<view v-else class="_ul-p1" style="border-radius:50%">█</view>
+				</view>
+				<trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
+			</view>
+			<!--表格-->
+			<view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
+				<view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
+					<view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
+						<trees v-if="tr.name=='td'" :nodes="tr.children" />
+						<trees v-else v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :c="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
+						 :s="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')" :nodes="td.children" />
+					</view>
+				</view>
+			</view>
+			<!--#ifdef APP-PLUS-->
+			<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
+			 :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
+			<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
+			<!--#endif-->
+			<!--富文本-->
+			<!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
+			<rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
+			<!--#endif-->
+			<!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
+			<rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
+			<!--#endif-->
+			<trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :c="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')"
+			 :style="n.attrs.style" :s="n.attrs.style" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
+		</block>
+	</view>
+</template>
+<script module="handler" lang="wxs" src="./handler.wxs"></script>
+<script>
+	global.Parser = {};
+	import trees from './trees'
+	const errorImg = require('../libs/config.js').errorImg;
+	export default {
+		components: {
+			trees
+		},
+		name: 'trees',
+		data() {
+			return {
+				controls: [],
+				placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
+				errorImg,
+				loadVideo: typeof plus == 'undefined',
+				// #ifndef MP-ALIPAY
+				c: '',
+				s: ''
+				// #endif
+			}
+		},
+		props: {
+			nodes: Array,
+			lazyLoad: Boolean,
+			loading: String,
+			// #ifdef MP-ALIPAY
+			c: String,
+			s: String
+			// #endif
+		},
+		mounted() {
+			for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
+			this.init();
+		},
+		// #ifdef APP-PLUS
+		beforeDestroy() {
+			this.observer && this.observer.disconnect();
+		},
+		// #endif
+		methods: {
+			init() {
+				for (var i = this.nodes.length, n; n = this.nodes[--i];) {
+					if (n.name == 'img') {
+						this.top.imgList.setItem(n.attrs.i, n.attrs.src);
+						// #ifdef APP-PLUS
+						if (this.lazyLoad && !this.observer) {
+							this.observer = uni.createIntersectionObserver(this).relativeToViewport({
+								top: 500,
+								bottom: 500
+							});
+							setTimeout(() => {
+								this.observer.observe('._img', res => {
+									if (res.intersectionRatio) {
+										for (var j = this.nodes.length; j--;)
+											if (this.nodes[j].name == 'img')
+												this.$set(this.controls, j, 1);
+										this.observer.disconnect();
+									}
+								})
+							}, 0)
+						}
+						// #endif
+					} else if (n.name == 'video' || n.name == 'audio') {
+						var ctx;
+						if (n.name == 'video') {
+							ctx = uni.createVideoContext(n.attrs.id
+								// #ifndef MP-BAIDU
+								, this
+								// #endif
+							);
+						} else if (this.$refs[n.attrs.id])
+							ctx = this.$refs[n.attrs.id][0];
+						if (ctx) {
+							ctx.id = n.attrs.id;
+							this.top.videoContexts.push(ctx);
+						}
+					}
+				}
+				// #ifdef APP-PLUS
+				// APP 上避免 video 错位需要延时渲染
+				setTimeout(() => {
+					this.loadVideo = true;
+				}, 1000)
+				// #endif
+			},
+			play(e) {
+				var contexts = this.top.videoContexts;
+				if (contexts.length > 1 && this.top.autopause)
+					for (var i = contexts.length; i--;)
+						if (contexts[i].id != e.currentTarget.dataset.id)
+							contexts[i].pause();
+			},
+			imgtap(e) {
+				var attrs = e.currentTarget.dataset.attrs;
+				if (!attrs.ignore) {
+					var preview = true,
+						data = {
+							id: e.target.id,
+							src: attrs.src,
+							ignore: () => preview = false
+						};
+					global.Parser.onImgtap && global.Parser.onImgtap(data);
+					this.top.$emit('imgtap', data);
+					if (preview) {
+						var urls = this.top.imgList,
+							current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
+						uni.previewImage({
+							current,
+							urls
+						})
+					}
+				}
+			},
+			loadImg(e) {
+				var i = e.currentTarget.dataset.i;
+				if (this.lazyLoad && !this.controls[i]) {
+					// #ifdef QUICKAPP-WEBVIEW
+					this.$set(this.controls, i, 0);
+					this.$nextTick(function() {
+						// #endif
+						// #ifndef APP-PLUS
+						this.$set(this.controls, i, 1);
+						// #endif
+						// #ifdef QUICKAPP-WEBVIEW
+					})
+					// #endif
+				} else if (this.loading && this.controls[i] != 2) {
+					// #ifdef QUICKAPP-WEBVIEW
+					this.$set(this.controls, i, 0);
+					this.$nextTick(function() {
+						// #endif
+						this.$set(this.controls, i, 2);
+						// #ifdef QUICKAPP-WEBVIEW
+					})
+					// #endif
+				}
+			},
+			linkpress(e) {
+				var jump = true,
+					attrs = e.currentTarget.dataset.attrs;
+				attrs.ignore = () => jump = false;
+				global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
+				this.top.$emit('linkpress', attrs);
+				if (jump) {
+					// #ifdef MP
+					if (attrs['app-id']) {
+						return uni.navigateToMiniProgram({
+							appId: attrs['app-id'],
+							path: attrs.path
+						})
+					}
+					// #endif
+					if (attrs.href) {
+						if (attrs.href[0] == '#') {
+							if (this.top.useAnchor)
+								this.top.navigateTo({
+									id: attrs.href.substring(1)
+								})
+						} else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
+							// #ifdef APP-PLUS
+							plus.runtime.openWeb(attrs.href);
+							// #endif
+							// #ifndef APP-PLUS
+							uni.setClipboardData({
+								data: attrs.href,
+								success: () =>
+									uni.showToast({
+										title: '链接已复制'
+									})
+							})
+							// #endif
+						} else
+							uni.navigateTo({
+								url: attrs.href,
+								fail() {
+									uni.switchTab({
+										url: attrs.href,
+									})
+								}
+							})
+					}
+				}
+			},
+			error(e) {
+				var target = e.currentTarget,
+					source = target.dataset.source,
+					i = target.dataset.i;
+				if (source == 'video' || source == 'audio') {
+					// 加载其他 source
+					var index = this.controls[i] ? this.controls[i].i + 1 : 1;
+					if (index < this.nodes[i].attrs.source.length)
+						this.$set(this.controls, i, index);
+					if (e.detail.__args__)
+						e.detail = e.detail.__args__[0];
+				} else if (errorImg && source == 'img') {
+					this.top.imgList.setItem(target.dataset.index, errorImg);
+					this.$set(this.controls, i, 3);
+				}
+				this.top && this.top.$emit('error', {
+					source,
+					target,
+					errMsg: e.detail.errMsg
+				});
+			},
+			_loadVideo(e) {
+				this.$set(this.controls, e.target.dataset.i, 0);
+			}
+		}
+	}
+</script>
+
+<style>
+	/* 在这里引入自定义样式 */
+
+	/* 链接和图片效果 */
+	._a {
+		display: inline;
+		padding: 1.5px 0 1.5px 0;
+		color: #366092;
+		word-break: break-all;
+	}
+
+	._hover {
+		text-decoration: underline;
+		opacity: 0.7;
+	}
+
+	._img {
+		display: inline-block;
+		max-width: 100%;
+		overflow: hidden;
+	}
+
+	/* #ifdef MP-WEIXIN */
+	:host {
+		display: inline;
+	}
+
+	/* #endif */
+
+	/* #ifndef MP-ALIPAY || APP-PLUS */
+	.interlayer {
+		display: inherit;
+		flex-direction: inherit;
+		flex-wrap: inherit;
+		align-content: inherit;
+		align-items: inherit;
+		justify-content: inherit;
+		width: 100%;
+		white-space: inherit;
+	}
+
+	/* #endif */
+
+	._b,
+	._strong {
+		font-weight: bold;
+	}
+
+	/* #ifndef MP-ALIPAY */
+	._blockquote,
+	._div,
+	._p,
+	._ol,
+	._ul,
+	._li {
+		display: block;
+	}
+	
+	/* #endif */
+
+	._code {
+		font-family: monospace;
+	}
+
+	._del {
+		text-decoration: line-through;
+	}
+
+	._em,
+	._i {
+		font-style: italic;
+	}
+
+	._h1 {
+		font-size: 2em;
+	}
+
+	._h2 {
+		font-size: 1.5em;
+	}
+
+	._h3 {
+		font-size: 1.17em;
+	}
+
+	._h5 {
+		font-size: 0.83em;
+	}
+
+	._h6 {
+		font-size: 0.67em;
+	}
+
+	._h1,
+	._h2,
+	._h3,
+	._h4,
+	._h5,
+	._h6 {
+		display: block;
+		font-weight: bold;
+	}
+
+	._image {
+		display: block;
+		width: 100%;
+		height: 360px;
+		margin-top: -360px;
+		opacity: 0;
+	}
+
+	._ins {
+		text-decoration: underline;
+	}
+
+	._li {
+		flex: 1;
+		width: 0;
+	}
+
+	._ol-bef {
+		width: 36px;
+		margin-right: 5px;
+		text-align: right;
+	}
+
+	._ul-bef {
+		margin: 0 12px 0 23px;
+		line-height: normal;
+	}
+
+	._ol-bef,
+	._ul_bef {
+		flex: none;
+		user-select: none;
+	}
+
+	._ul-p1 {
+		display: inline-block;
+		width: 0.3em;
+		height: 0.3em;
+		overflow: hidden;
+		line-height: 0.3em;
+	}
+
+	._ul-p2 {
+		display: inline-block;
+		width: 0.23em;
+		height: 0.23em;
+		border: 0.05em solid black;
+		border-radius: 50%;
+	}
+
+	._q::before {
+		content: '"';
+	}
+
+	._q::after {
+		content: '"';
+	}
+
+	._sub {
+		font-size: smaller;
+		vertical-align: sub;
+	}
+
+	._sup {
+		font-size: smaller;
+		vertical-align: super;
+	}
+
+	/* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
+	._abbr,
+	._b,
+	._code,
+	._del,
+	._em,
+	._i,
+	._ins,
+	._label,
+	._q,
+	._span,
+	._strong,
+	._sub,
+	._sup {
+		display: inline;
+	}
+
+	/* #endif */
+
+	/* #ifdef MP-WEIXIN || MP-QQ */
+	.__bdo,
+	.__bdi,
+	.__ruby,
+	.__rt {
+		display: inline-block;
+	}
+
+	/* #endif */
+	._video {
+		position: relative;
+		display: inline-block;
+		width: 300px;
+		height: 225px;
+		background-color: black;
+	}
+
+	._video::after {
+		position: absolute;
+		top: 50%;
+		left: 50%;
+		margin: -15px 0 0 -15px;
+		content: '';
+		border-color: transparent transparent transparent white;
+		border-style: solid;
+		border-width: 15px 0 15px 30px;
+	}
+</style>

+ 55 - 0
yudao-vue-ui/components/mescroll-uni/components/mescroll-down.css

@@ -0,0 +1,55 @@
+/* 下拉刷新区域 */
+.mescroll-downwarp {
+	position: absolute;
+	top: -100%;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	text-align: center;
+}
+
+/* 下拉刷新--内容区,定位于区域底部 */
+.mescroll-downwarp .downwarp-content {
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	width: 100%;
+	min-height: 60rpx;
+	padding: 20rpx 0;
+	text-align: center;
+}
+
+/* 下拉刷新--提示文本 */
+.mescroll-downwarp .downwarp-tip {
+	display: inline-block;
+	font-size: 28rpx;
+	vertical-align: middle;
+	margin-left: 16rpx;
+	/* color: gray; 已在style设置color,此处删去*/
+}
+
+/* 下拉刷新--旋转进度条 */
+.mescroll-downwarp .downwarp-progress {
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid gray;
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+	vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-downwarp .mescroll-rotate {
+	animation: mescrollDownRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollDownRotate {
+	0% {
+		transform: rotate(0deg);
+	}
+
+	100% {
+		transform: rotate(360deg);
+	}
+}

+ 47 - 0
yudao-vue-ui/components/mescroll-uni/components/mescroll-down.vue

@@ -0,0 +1,47 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
+			<view class="downwarp-tip">{{downText}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object , // down的配置项
+		type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+		rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 是否在加载中
+		isDownLoading(){
+			return this.type === 3
+		},
+		// 旋转的角度
+		downRotate(){
+			return 'rotate(' + 360 * this.rate + 'deg)'
+		},
+		// 文本提示
+		downText(){
+			switch (this.type){
+				case 1: return this.mOption.textInOffset;
+				case 2: return this.mOption.textOutOffset;
+				case 3: return this.mOption.textLoading;
+				case 4: return this.mOption.textLoading;
+				default: return this.mOption.textInOffset;
+			}
+		}
+	}
+};
+</script>
+
+<style>
+@import "./mescroll-down.css";
+</style>

+ 27 - 0
yudao-vue-ui/components/mescroll-uni/components/mescroll-empty.vue

@@ -0,0 +1,27 @@
+<template>
+	<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
+		<mix-empty :type="option.type" :backgroundColor="option.backgroundColor"></mix-empty>
+	</view>
+</template>
+
+<script>
+	import mixEmpty from '@/components/mix-empty/mix-empty.vue'
+	export default {
+		components: {
+			mixEmpty
+		},
+		props: {
+			// empty的配置项: 默认为GlobalOption.up.empty
+			option: {
+				type: Object,
+				default() {
+					return {};
+				}
+			}
+		}
+	};
+</script>
+
+<style scoped lang="scss">
+	
+</style>

+ 95 - 0
yudao-vue-ui/components/mescroll-uni/components/mescroll-empty1.vue

@@ -0,0 +1,95 @@
+<!--空布局
+
+可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
+import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
+
+-->
+<template>
+	<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
+		<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
+		<view v-if="tip" class="empty-tip">{{ tip }}</view>
+		<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
+	</view>
+</template>
+
+<script>
+// 引入全局配置
+import GlobalOption from './../mescroll-uni-option.js';
+export default {
+	props: {
+		// empty的配置项: 默认为GlobalOption.up.empty
+		option: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	// 使用computed获取配置,用于支持option的动态配置
+	computed: {
+		// 图标
+		icon() {
+			return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
+		},
+		// 文本提示
+		tip() {
+			return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
+		}
+	},
+	methods: {
+		// 点击按钮
+		emptyClick() {
+			this.$emit('emptyclick');
+		}
+	}
+};
+</script>
+
+<style>
+/* 无任何数据的空布局 */
+.mescroll-empty {
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	flex-direction: column;
+	width: 100%;
+	padding: 30vh 50rpx 100rpx;
+	text-align: center;
+}
+
+.mescroll-empty.empty-fixed {
+	z-index: 99;
+	position: absolute; /*transform会使fixed失效,最终会降级为absolute */
+	top: 100rpx;
+	left: 0;
+}
+
+.mescroll-empty .empty-icon {
+	width: 170rpx;
+	height: 170rpx;
+	transform: translateX(16rpx);
+}
+
+.mescroll-empty .empty-tip {
+	margin-top: 20rpx;
+	font-size: 24rpx;
+	color: #666;
+}
+
+.mescroll-empty .empty-btn {
+	display: inline-block;
+	margin-top: 40rpx;
+	min-width: 200rpx;
+	padding: 18rpx;
+	font-size: 28rpx;
+	border: 1rpx solid #e04b28;
+	border-radius: 60rpx;
+	color: #e04b28;
+}
+
+.mescroll-empty .empty-btn:active {
+	opacity: 0.75;
+}
+</style>

+ 83 - 0
yudao-vue-ui/components/mescroll-uni/components/mescroll-top.vue

@@ -0,0 +1,83 @@
+<!-- 回到顶部的按钮 -->
+<template>
+	<image
+		v-if="mOption.src"
+		class="mescroll-totop"
+		:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
+		:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
+		:src="mOption.src"
+		mode="widthFix"
+		@click="toTopClick"
+	/>
+</template>
+
+<script>
+export default {
+	props: {
+		// up.toTop的配置项
+		option: Object,
+		// 是否显示
+		value: false
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 优先显示左边
+		left(){
+			return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
+		},
+		// 右边距离 (优先显示左边)
+		right() {
+			return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
+		}
+	},
+	methods: {
+		addUnit(num){
+			if(!num) return 0;
+			if(typeof num === 'number') return num + 'rpx';
+			return num
+		},
+		toTopClick() {
+			this.$emit('input', false); // 使v-model生效
+			this.$emit('click'); // 派发点击事件
+		}
+	}
+};
+</script>
+
+<style>
+/* 回到顶部的按钮 */
+.mescroll-totop {
+	z-index: 9990;
+	position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
+	right: 20rpx;
+	bottom: 120rpx;
+	width: 72rpx;
+	height: auto;
+	border-radius: 50%;
+	opacity: 0;
+	transition: opacity 0.5s; /* 过渡 */
+	margin-bottom: var(--window-bottom); /* css变量 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-totop-safearea {
+		margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
+		margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
+	}
+}
+
+/* 显示 -- 淡入 */
+.mescroll-totop-in {
+	opacity: 1;
+}
+
+/* 隐藏 -- 淡出且不接收事件*/
+.mescroll-totop-out {
+	opacity: 0;
+	pointer-events: none;
+}
+</style>

+ 47 - 0
yudao-vue-ui/components/mescroll-uni/components/mescroll-up.css

@@ -0,0 +1,47 @@
+/* 上拉加载区域 */
+.mescroll-upwarp {
+	box-sizing: border-box;
+	min-height: 110rpx;
+	padding: 30rpx 0;
+	text-align: center;
+	clear: both;
+}
+
+/*提示文本 */
+.mescroll-upwarp .upwarp-tip,
+.mescroll-upwarp .upwarp-nodata {
+	display: inline-block;
+	font-size: 28rpx;
+	vertical-align: middle;
+	/* color: gray; 已在style设置color,此处删去*/
+}
+
+.mescroll-upwarp .upwarp-tip {
+	margin-left: 16rpx;
+}
+
+/*旋转进度条 */
+.mescroll-upwarp .upwarp-progress {
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid gray;
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+	vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-upwarp .mescroll-rotate {
+	animation: mescrollUpRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollUpRotate {
+	0% {
+		transform: rotate(0deg);
+	}
+
+	100% {
+		transform: rotate(360deg);
+	}
+}

+ 39 - 0
yudao-vue-ui/components/mescroll-uni/components/mescroll-up.vue

@@ -0,0 +1,39 @@
+<!-- 上拉加载区域 -->
+<template>
+	<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+		<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+		<view v-show="isUpLoading">
+			<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
+			<view class="upwarp-tip">{{ mOption.textLoading }}</view>
+		</view>
+		<!-- 无数据 -->
+		<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object, // up的配置项
+		type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption() {
+			return this.option || {};
+		},
+		// 加载中
+		isUpLoading() {
+			return this.type === 1;
+		},
+		// 没有更多了
+		isUpNoMore() {
+			return this.type === 2;
+		}
+	}
+};
+</script>
+
+<style>
+@import './mescroll-up.css';
+</style>

+ 14 - 0
yudao-vue-ui/components/mescroll-uni/mescroll-body.css

@@ -0,0 +1,14 @@
+.mescroll-body {
+	position: relative; /* 下拉刷新区域相对自身定位 */
+	height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
+	overflow: hidden; /* 遮住顶部下拉刷新区域 */
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 344 - 0
yudao-vue-ui/components/mescroll-uni/mescroll-body.vue

@@ -0,0 +1,344 @@
+<template>
+	<view 
+	class="mescroll-body mescroll-render-touch" 
+	:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+	@touchstart="wxsBiz.touchstartEvent" 
+	@touchmove="wxsBiz.touchmoveEvent" 
+	@touchend="wxsBiz.touchendEvent" 
+	@touchcancel="wxsBiz.touchendEvent"
+	:change:prop="wxsBiz.propObserver"
+	:prop="wxsProp"
+	>
+		<!-- 状态栏 -->
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+				<view class="downwarp-content" :change:prop="renderBiz.propObserver" :prop="wxsProp">
+					<!-- <view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view> -->
+					<view class="downwarp-tip">
+						<image v-show="downText === '下拉刷新'" style="width:80rpx;height:86rpx" src="/static/loading/hamster.png"></image>
+						<image v-show="downText !== '下拉刷新'" style="width:80rpx;height:86rpx" src="/static/loading/hamster.gif"></image>
+					</view>
+					<!-- {{downText}} -->
+				</view>
+			</view>
+	
+			<!-- 列表内容 -->
+			<slot></slot>
+
+			<!-- 空布局 -->
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+			<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+			<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+			<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+				<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+				<view v-show="upLoadType===1" :style="{height: upLoadType===1 ? 'auto' : 0}" style="display: flex;align-items: center;justify-content: center;overflow: hidden">
+					<!-- <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> -->
+					<image style="width: 64rpx;height: 68rpx" src="/static/loading/hamster.gif"></image>
+					<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+				</view>
+				<!-- 无数据 -->
+				<!-- <view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> -->
+				<view v-if="upLoadType===2" class="mix-nodata center">
+					<image class="logo" src="/static/logo-b-w.png"></image>
+					<text>国云网络提供技术支持</text>
+				</view>
+			</view>
+		</view>
+		
+		<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+		<!-- #ifdef H5 -->
+		<!-- <view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> -->
+		<!-- #endif -->
+		
+		<!-- 适配iPhoneX -->
+		<view v-if="safearea" class="mescroll-safearea"></view>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<!-- <mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> -->
+	</view>
+</template>
+
+<!-- 微信小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || APP-PLUS || H5-->
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				windowHeight: 0, // 可使用窗口的高度
+				windowBottom: 0, // 可使用窗口的底部位置
+				statusBarHeight: 0 // 状态栏高度
+			};
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			minHeight(){
+				return this.toPx(this.height || '100%') + 'px'
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			padTop() {
+				return this.numTop + 'px';
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom);
+			},
+			padBottom() {
+				return this.numBottom + 'px';
+			},
+			// 是否为重置下拉的状态
+			isDownReset() {
+				return this.downLoadType === 3 || this.downLoadType === 4;
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.optDown.textLoading;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num) {
+				if (typeof num === 'string') {
+					if (num.indexOf('px') !== -1) {
+						if (num.indexOf('rpx') !== -1) {
+							// "10rpx"
+							num = num.replace('rpx', '');
+						} else if (num.indexOf('upx') !== -1) {
+							// "10upx"
+							num = num.replace('upx', '');
+						} else {
+							// "10px"
+							return Number(num.replace('px', ''));
+						}
+					} else if (num.indexOf('%') !== -1) {
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace('%', '')) / 100;
+						return this.windowHeight * rate;
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0;
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll);
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					auto: false,
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
+							if(vm.downLoadType === 4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll);
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+					}
+				}
+			};
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y必须为元素的id,不带#)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
+							let top = rect.top
+							top += vm.mescroll.getScrollTop()
+							uni.pageScrollTo({
+								scrollTop: top,
+								duration: t
+							})
+						}).exec()
+					},30)
+				} else{
+					// 滚动到指定位置 (y必须为数字)
+					uni.pageScrollTo({
+						scrollTop: y,
+						duration: t
+					})
+				}
+			});
+
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+		}
+	};
+</script>
+
+<style lang="scss">
+	@import "./mescroll-body.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+	
+	.mix-nodata{
+		height: 52rpx;
+		font-size: 26rpx;
+		color: #999;
+		
+		.logo{
+			width: 34rpx;
+			height: 34rpx;
+			margin-right: 12rpx;
+		}
+	}
+</style>

+ 65 - 0
yudao-vue-ui/components/mescroll-uni/mescroll-mixins.js

@@ -0,0 +1,65 @@
+// mescroll-body 和 mescroll-uni 通用
+
+// import MescrollUni from "./mescroll-uni.vue";
+// import MescrollBody from "./mescroll-body.vue";
+
+const MescrollMixin = {
+	// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
+	// 	MescrollUni,
+	// 	MescrollBody
+	// },
+	data() {
+		return {
+			mescroll: null //mescroll实例对象
+		}
+	},
+	// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	onPullDownRefresh(){
+		this.mescroll && this.mescroll.onPullDownRefresh();
+	},
+	// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onPageScroll(e) {
+		this.mescroll && this.mescroll.onPageScroll(e);
+	},
+	// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onReachBottom() {
+		this.mescroll && this.mescroll.onReachBottom();
+	},
+	methods: {
+		// mescroll组件初始化的回调,可获取到mescroll对象
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			this.mescrollInitByRef(); // 兼容字节跳动小程序
+		},
+		// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序: http://www.mescroll.com/qa.html?v=20200107#q26)
+		mescrollInitByRef() {
+			if(!this.mescroll || !this.mescroll.resetUpScroll){
+				let mescrollRef = this.$refs.mescrollRef;
+				if(mescrollRef) this.mescroll = mescrollRef.mescroll
+			}
+		},
+		// 下拉刷新的回调 (mixin默认resetUpScroll)
+		downCallback() {
+			if(this.mescroll.optUp.use){
+				this.mescroll.resetUpScroll()
+			}else{
+				setTimeout(()=>{
+					this.mescroll.endSuccess();
+				}, 500)
+			}
+		},
+		// 上拉加载的回调
+		upCallback() {
+			// mixin默认延时500自动结束加载
+			setTimeout(()=>{
+				this.mescroll.endErr();
+			}, 500)
+		}
+	},
+	mounted() {
+		this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
+	}
+	
+}
+
+export default MescrollMixin;

+ 33 - 0
yudao-vue-ui/components/mescroll-uni/mescroll-uni-option.js

@@ -0,0 +1,33 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '- 我也是有底线的 -', // 没有更多数据的提示文本
+		offset: 80, // 距底部多远时,触发upCallback
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: "http://www.mescroll.com/img/mescroll-totop.png?v=1", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
+			right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: "/static/empty/hamster.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+			tip: '~ 空空如也 ~' // 提示
+		}
+	}
+}
+
+export default GlobalOption

+ 36 - 0
yudao-vue-ui/components/mescroll-uni/mescroll-uni.css

@@ -0,0 +1,36 @@
+.mescroll-uni-warp{
+	height: 100%;
+}
+
+.mescroll-uni-content{
+	height: 100%;
+}
+
+.mescroll-uni {
+	position: relative;
+	width: 100%;
+	height: 100%;
+	min-height: 200rpx;
+	overflow-y: auto;
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 定位的方式固定高度 */
+.mescroll-uni-fixed{
+	z-index: 1;
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	width: auto; /* 使right生效 */
+	height: auto; /* 使bottom生效 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 788 - 0
yudao-vue-ui/components/mescroll-uni/mescroll-uni.js

@@ -0,0 +1,788 @@
+/* mescroll
+ * version 1.3.0
+ * 2020-07-10 wenju
+ * http://www.mescroll.com
+ */
+
+export default function MeScroll(options, isScrollBody) {
+	let me = this;
+	me.version = '1.3.0'; // mescroll版本号
+	me.options = options || {}; // 配置
+	me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
+
+	me.isDownScrolling = false; // 是否在执行下拉刷新的回调
+	me.isUpScrolling = false; // 是否在执行上拉加载的回调
+	let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
+
+	// 初始化下拉刷新
+	me.initDownScroll();
+	// 初始化上拉加载,则初始化
+	me.initUpScroll();
+
+	// 自动加载
+	setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+		// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
+		if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
+			if (me.optDown.autoShowLoading) {
+				me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
+			} else {
+				me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
+			}
+		}
+		// 自动触发上拉加载
+		if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
+			setTimeout(function(){
+				me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
+			},100)
+		}
+	}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
+}
+
+/* 配置参数:下拉刷新 */
+MeScroll.prototype.extendDownScroll = function(optDown) {
+	// 下拉刷新的配置
+	MeScroll.extend(optDown, {
+		use: true, // 是否启用下拉刷新; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
+		native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+		autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
+		isLock: false, // 是否锁定下拉刷新,默认false;
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
+		inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+		outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+		bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
+		minAngle: 45, // 向下滑动最少偏移的角度,取值区间  [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 下拉刷新初始化完毕的回调
+		inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+		outOffset: null, // 下拉的距离大于offset那一刻的回调
+		onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+		beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+		showLoading: null, // 显示下拉刷新进度的回调
+		afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+		beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+		endDownScroll: null, // 结束下拉刷新的回调
+		afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+		callback: function(mescroll) {
+			// 下拉刷新的回调;默认重置上拉加载列表为第一页
+			mescroll.resetUpScroll();
+		}
+	})
+}
+
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function(optUp) {
+	// 上拉加载的配置
+	MeScroll.extend(optUp, {
+		use: true, // 是否启用上拉加载; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+		isLock: false, // 是否锁定上拉加载,默认false;
+		isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+		callback: null, // 上拉加载的回调;function(page,mescroll){ }
+		page: {
+			num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+			size: 10, // 每页数据的数量
+			time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+		},
+		noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+		offset: 80, // 距底部多远时,触发upCallback
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- 我也是有底线的 --', // 没有更多数据的提示文本
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 初始化完毕的回调
+		showLoading: null, // 显示加载中的回调
+		showNoMore: null, // 显示无更多数据的回调
+		hideUpScroll: null, // 隐藏上拉加载的回调
+		errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: null, // 图片路径,默认null (绝对路径或网络图)
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+			duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			zIndex: 9990, // fixed定位z-index值
+			left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+			width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: null, // 图标路径
+			tip: '~ 暂无相关数据 ~', // 提示
+			btnText: '', // 按钮
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+			top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+			zIndex: 99 // fixed定位z-index值
+		},
+		onScroll: false // 是否监听滚动事件
+	})
+}
+
+/* 配置参数 */
+MeScroll.extend = function(userOption, defaultOption) {
+	if (!userOption) return defaultOption;
+	for (let key in defaultOption) {
+		if (userOption[key] == null) {
+			let def = defaultOption[key];
+			if (def != null && typeof def === 'object') {
+				userOption[key] = MeScroll.extend({}, def); // 深度匹配
+			} else {
+				userOption[key] = def;
+			}
+		} else if (typeof userOption[key] === 'object') {
+			MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
+		}
+	}
+	return userOption;
+}
+
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function(color) {
+	if(!color) return false;
+	let c = color.toLowerCase();
+	return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
+}
+
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optDown = me.options.down || {};
+	if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendDownScroll(me.optDown);
+	
+	// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+	if(me.isScrollBody && me.optDown.native){
+		me.optDown.use = false
+	}else{
+		me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+	}
+	
+	me.downHight = 0; // 下拉区域的高度
+
+	// 在页面中加入下拉布局
+	if (me.optDown.use && me.optDown.inited) {
+		// 初始化完毕的回调
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+			me.optDown.inited(me);
+		}, 0)
+	}
+}
+
+/* 列表touchstart事件 */
+MeScroll.prototype.touchstartEvent = function(e) {
+	if (!this.optDown.use) return;
+
+	this.startPoint = this.getPoint(e); // 记录起点
+	this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
+	this.startAngle = 0; // 初始角度
+	this.lastPoint = this.startPoint; // 重置上次move的点
+	this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+	this.inTouchend = false; // 标记不是touchend
+}
+
+/* 列表touchmove事件 */
+MeScroll.prototype.touchmoveEvent = function(e) {
+	if (!this.optDown.use) return;
+	let me = this;
+
+	let scrollTop = me.getScrollTop(); // 当前滚动条的距离
+	let curPoint = me.getPoint(e); // 当前点
+
+	let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+
+	// 向下拉 && 在顶部
+	// mescroll-body,直接判定在顶部即可
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+	if (moveY > 0 && (
+			(me.isScrollBody && scrollTop <= 0)
+			||
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+		)) {
+		// 可下拉的条件
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+				me.optUp.isBoth))) {
+
+			// 下拉的初始角度是否在配置的范围内
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+			if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
+
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+				me.inTouchend = true; // 标记执行touchend
+				me.touchendEvent(); // 提前触发touchend
+				return;
+			}
+			
+			me.preventDefault(e); // 阻止默认事件
+
+			let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+
+			// 下拉距离  < 指定距离
+			if (me.downHight < me.optDown.offset) {
+				if (me.movetype !== 1) {
+					me.movetype = 1; // 加入标记,保证只执行一次
+					me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+
+				// 指定距离  <= 下拉距离
+			} else {
+				if (me.movetype !== 2) {
+					me.movetype = 2; // 加入标记,保证只执行一次
+					me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				if (diff > 0) { // 向下拉
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+				} else { // 向上收
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+				}
+			}
+			
+			me.downHight = Math.round(me.downHight) // 取整
+			let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+			me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+		}
+	}
+
+	me.lastPoint = curPoint; // 记录本次移动的点
+}
+
+/* 列表touchend事件 */
+MeScroll.prototype.touchendEvent = function(e) {
+	if (!this.optDown.use) return;
+	// 如果下拉区域高度已改变,则需重置回来
+	if (this.isMoveDown) {
+		if (this.downHight >= this.optDown.offset) {
+			// 符合触发刷新的条件
+			this.triggerDownScroll();
+		} else {
+			// 不符合的话 则重置
+			this.downHight = 0;
+			this.endDownScrollCall(this);
+		}
+		this.movetype = 0;
+		this.isMoveDown = false;
+	} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
+		let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 上滑
+		if (isScrollUp) {
+			// 需检查滑动的角度
+			let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
+			if (angle > 80) {
+				// 检查并触发上拉
+				this.triggerUpScroll(true);
+			}
+		}
+	}
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+MeScroll.prototype.getPoint = function(e) {
+	if (!e) {
+		return {
+			x: 0,
+			y: 0
+		}
+	}
+	if (e.touches && e.touches[0]) {
+		return {
+			x: e.touches[0].pageX,
+			y: e.touches[0].pageY
+		}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {
+			x: e.changedTouches[0].pageX,
+			y: e.changedTouches[0].pageY
+		}
+	} else {
+		return {
+			x: e.clientX,
+			y: e.clientY
+		}
+	}
+}
+
+/* 计算两点之间的角度: 区间 [0,90]*/
+MeScroll.prototype.getAngle = function(p1, p2) {
+	let x = Math.abs(p1.x - p2.x);
+	let y = Math.abs(p1.y - p2.y);
+	let z = Math.sqrt(x * x + y * y);
+	let angle = 0;
+	if (z !== 0) {
+		angle = Math.asin(y / z) / Math.PI * 180;
+	}
+	return angle
+}
+
+/* 触发下拉刷新 */
+MeScroll.prototype.triggerDownScroll = function() {
+	if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
+		//return true则处于完全自定义状态
+	} else {
+		this.showDownScroll(); // 下拉刷新中...
+		!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+	}
+}
+
+/* 显示下拉进度布局 */
+MeScroll.prototype.showDownScroll = function() {
+	this.isDownScrolling = true; // 标记下拉中
+	if (this.optDown.native) {
+		uni.startPullDownRefresh(); // 系统自带的下拉刷新
+		this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+	} else{
+		this.downHight = this.optDown.offset; // 更新下拉区域高度
+		this.showDownLoadingCall(this.downHight); // 下拉刷新中...
+	}
+}
+
+MeScroll.prototype.showDownLoadingCall = function(downHight) {
+	this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
+	this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
+}
+
+/* 显示系统自带的下拉刷新时需要处理的业务 */
+MeScroll.prototype.onPullDownRefresh = function() {
+	this.isDownScrolling = true; // 标记下拉中
+	this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+	this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+}
+
+/* 结束下拉刷新 */
+MeScroll.prototype.endDownScroll = function() {
+	if (this.optDown.native) { // 结束原生下拉刷新
+		this.isDownScrolling = false;
+		this.endDownScrollCall(this);
+		uni.stopPullDownRefresh();
+		return
+	}
+	let me = this;
+	// 结束下拉刷新的方法
+	let endScroll = function() {
+		me.downHight = 0;
+		me.isDownScrolling = false;
+		me.endDownScrollCall(me);
+		if(!me.isScrollBody){
+			me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
+			me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
+		}
+	}
+	// 结束下拉刷新时的回调
+	let delay = 0;
+	if (me.optDown.beforeEndDownScroll) delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
+	if (typeof delay === 'number' && delay > 0) {
+		setTimeout(endScroll, delay);
+	} else {
+		endScroll();
+	}
+}
+
+MeScroll.prototype.endDownScrollCall = function() {
+	this.optDown.endDownScroll && this.optDown.endDownScroll(this);
+	this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
+}
+
+/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockDownScroll = function(isLock) {
+	if (isLock == null) isLock = true;
+	this.optDown.isLock = isLock;
+}
+
+/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockUpScroll = function(isLock) {
+	if (isLock == null) isLock = true;
+	this.optUp.isLock = isLock;
+}
+
+/* -------初始化上拉加载------- */
+MeScroll.prototype.initUpScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optUp = me.options.up || {use: false}
+	if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendUpScroll(me.optUp);
+
+	if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
+	me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
+	me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
+
+	// 初始化完毕的回调
+	if (me.optUp.inited) {
+		setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+			me.optUp.inited(me);
+		}, 0)
+	}
+}
+
+/*滚动到底部的事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onReachBottom = function() {
+	if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
+		if (!this.optUp.isLock && this.optUp.hasNext) {
+			this.triggerUpScroll();
+		}
+	}
+}
+
+/*列表滚动事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onPageScroll = function(e) {
+	if (!this.isScrollBody) return;
+	
+	// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
+	this.setScrollTop(e.scrollTop);
+
+	// 顶部按钮的显示隐藏
+	if (e.scrollTop >= this.optUp.toTop.offset) {
+		this.showTopBtn();
+	} else {
+		this.hideTopBtn();
+	}
+}
+
+/*列表滚动事件*/
+MeScroll.prototype.scroll = function(e, onScroll) {
+	// 更新滚动条的位置
+	this.setScrollTop(e.scrollTop);
+	// 更新滚动内容高度
+	this.setScrollHeight(e.scrollHeight);
+
+	// 向上滑还是向下滑动
+	if (this.preScrollY == null) this.preScrollY = 0;
+	this.isScrollUp = e.scrollTop - this.preScrollY > 0;
+	this.preScrollY = e.scrollTop;
+
+	// 上滑 && 检查并触发上拉
+	this.isScrollUp && this.triggerUpScroll(true);
+
+	// 顶部按钮的显示隐藏
+	if (e.scrollTop >= this.optUp.toTop.offset) {
+		this.showTopBtn();
+	} else {
+		this.hideTopBtn();
+	}
+
+	// 滑动监听
+	this.optUp.onScroll && onScroll && onScroll()
+}
+
+/* 触发上拉加载 */
+MeScroll.prototype.triggerUpScroll = function(isCheck) {
+	if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
+		// 是否校验在底部; 默认不校验
+		if (isCheck === true) {
+			let canUp = false;
+			// 还有下一页 && 没有锁定 && 不在下拉中
+			if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
+				if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
+					canUp = true; // 标记可上拉
+				}
+			}
+			if (canUp === false) return;
+		}
+		this.showUpScroll(); // 上拉加载中...
+		this.optUp.page.num++; // 预先加一页,如果失败则减回
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+		this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+		this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.optUp.callback(this); // 执行回调,联网加载数据
+	}
+}
+
+/* 显示上拉加载中 */
+MeScroll.prototype.showUpScroll = function() {
+	this.isUpScrolling = true; // 标记上拉加载中
+	this.optUp.showLoading && this.optUp.showLoading(this); // 回调
+}
+
+/* 显示上拉无更多数据 */
+MeScroll.prototype.showNoMore = function() {
+	this.optUp.hasNext = false; // 标记无更多数据
+	this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
+}
+
+/* 隐藏上拉区域**/
+MeScroll.prototype.hideUpScroll = function() {
+	this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
+}
+
+/* 结束上拉加载 */
+MeScroll.prototype.endUpScroll = function(isShowNoMore) {
+	if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
+		if (isShowNoMore) {
+			this.showNoMore(); // isShowNoMore=true,显示无更多数据
+		} else {
+			this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
+		}
+	}
+	this.isUpScrolling = false; // 标记结束上拉加载
+}
+
+/* 重置上拉加载列表为第一页
+ *isShowLoading 是否显示进度布局;
+ * 1.默认null,不传参,则显示上拉加载的进度布局
+ * 2.传参true, 则显示下拉刷新的进度布局
+ * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
+ */
+MeScroll.prototype.resetUpScroll = function(isShowLoading) {
+	if (this.optUp && this.optUp.use) {
+		let page = this.optUp.page;
+		this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
+		this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
+		page.num = this.startNum; // 重置为第一页
+		page.time = null; // 重置时间为空
+		if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
+			if (isShowLoading == null) {
+				this.removeEmpty(); // 移除空布局
+				this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
+			} else {
+				this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
+			}
+		}
+		this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+		this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+		this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+		this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
+	}
+}
+
+/* 设置page.num的值 */
+MeScroll.prototype.setPageNum = function(num) {
+	this.optUp.page.num = num - 1;
+}
+
+/* 设置page.size的值 */
+MeScroll.prototype.setPageSize = function(size) {
+	this.optUp.page.size = size;
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalPage: 总页数(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
+	let hasNext;
+	if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
+	this.endSuccess(dataSize, hasNext, systime);
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalSize: 列表所有数据总数量(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
+	let hasNext;
+	if (this.optUp.use && totalSize != null) {
+		let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
+		hasNext = loadSize < totalSize; // 是否还有下一页
+	}
+	this.endSuccess(dataSize, hasNext, systime);
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
+ * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
+ * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
+ */
+MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
+	let me = this;
+	// 结束下拉刷新
+	if (me.isDownScrolling) me.endDownScroll();
+
+	// 结束上拉加载
+	if (me.optUp.use) {
+		let isShowNoMore; // 是否已无更多数据
+		if (dataSize != null) {
+			let pageNum = me.optUp.page.num; // 当前页码
+			let pageSize = me.optUp.page.size; // 每页长度
+			// 如果是第一页
+			if (pageNum === 1) {
+				if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
+			}
+			if (dataSize < pageSize || hasNext === false) {
+				// 返回的数据不满一页时,则说明已无更多数据
+				me.optUp.hasNext = false;
+				if (dataSize === 0 && pageNum === 1) {
+					// 如果第一页无任何数据且配置了空布局
+					isShowNoMore = false;
+					me.showEmpty();
+				} else {
+					// 总列表数少于配置的数量,则不显示无更多数据
+					let allDataSize = (pageNum - 1) * pageSize + dataSize;
+					if (allDataSize < me.optUp.noMoreSize) {
+						isShowNoMore = false;
+					} else {
+						isShowNoMore = true;
+					}
+					me.removeEmpty(); // 移除空布局
+				}
+			} else {
+				// 还有下一页
+				isShowNoMore = false;
+				me.optUp.hasNext = true;
+				me.removeEmpty(); // 移除空布局
+			}
+		}
+
+		// 隐藏上拉
+		me.endUpScroll(isShowNoMore);
+	}
+}
+
+/* 回调失败,结束下拉刷新和上拉加载 */
+MeScroll.prototype.endErr = function(errDistance) {
+	// 结束下拉,回调失败重置回原来的页码和时间
+	if (this.isDownScrolling) {
+		let page = this.optUp.page;
+		if (page && this.prePageNum) {
+			page.num = this.prePageNum;
+			page.time = this.prePageTime;
+		}
+		this.endDownScroll();
+	}
+	// 结束上拉,回调失败重置回原来的页码
+	if (this.isUpScrolling) {
+		this.optUp.page.num--;
+		this.endUpScroll(false);
+		// 如果是mescroll-body,则需往回滚一定距离
+		if(this.isScrollBody && errDistance !== 0){ // 不处理0
+			if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
+			this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
+		}
+	}
+}
+
+/* 显示空布局 */
+MeScroll.prototype.showEmpty = function() {
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
+}
+
+/* 移除空布局 */
+MeScroll.prototype.removeEmpty = function() {
+	this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
+}
+
+/* 显示回到顶部的按钮 */
+MeScroll.prototype.showTopBtn = function() {
+	if (!this.topBtnShow) {
+		this.topBtnShow = true;
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
+	}
+}
+
+/* 隐藏回到顶部的按钮 */
+MeScroll.prototype.hideTopBtn = function() {
+	if (this.topBtnShow) {
+		this.topBtnShow = false;
+		this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
+	}
+}
+
+/* 获取滚动条的位置 */
+MeScroll.prototype.getScrollTop = function() {
+	return this.scrollTop || 0
+}
+
+/* 记录滚动条的位置 */
+MeScroll.prototype.setScrollTop = function(y) {
+	this.scrollTop = y;
+}
+
+/* 滚动到指定位置 */
+MeScroll.prototype.scrollTo = function(y, t) {
+	this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
+}
+
+/* 自定义scrollTo */
+MeScroll.prototype.resetScrollTo = function(myScrollTo) {
+	this.myScrollTo = myScrollTo
+}
+
+/* 滚动条到底部的距离 */
+MeScroll.prototype.getScrollBottom = function() {
+	return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
+}
+
+/* 计步器
+ star: 开始值
+ end: 结束值
+ callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
+ t: 计步时长,传0则直接回调end值;不传则默认300ms
+ rate: 周期;不传则默认30ms计步一次
+ * */
+MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
+	let diff = end - star; // 差值
+	if (t === 0 || diff === 0) {
+		callback && callback(end);
+		return;
+	}
+	t = t || 300; // 时长 300ms
+	rate = rate || 30; // 周期 30ms
+	let count = t / rate; // 次数
+	let step = diff / count; // 步长
+	let i = 0; // 计数
+	let timer = setInterval(function() {
+		if (i < count - 1) {
+			star += step;
+			callback && callback(star, timer);
+			i++;
+		} else {
+			callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
+			clearInterval(timer);
+		}
+	}, rate);
+}
+
+/* 滚动容器的高度 */
+MeScroll.prototype.getClientHeight = function(isReal) {
+	let h = this.clientHeight || 0
+	if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
+		h = this.getBodyHeight()
+	}
+	return h
+}
+MeScroll.prototype.setClientHeight = function(h) {
+	this.clientHeight = h;
+}
+
+/* 滚动内容的高度 */
+MeScroll.prototype.getScrollHeight = function() {
+	return this.scrollHeight || 0;
+}
+MeScroll.prototype.setScrollHeight = function(h) {
+	this.scrollHeight = h;
+}
+
+/* body的高度 */
+MeScroll.prototype.getBodyHeight = function() {
+	return this.bodyHeight || 0;
+}
+MeScroll.prototype.setBodyHeight = function(h) {
+	this.bodyHeight = h;
+}
+
+/* 阻止浏览器默认滚动事件 */
+MeScroll.prototype.preventDefault = function(e) {
+	// 小程序不支持e.preventDefault, 已在wxs中禁止
+	// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
+	// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
+	if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
+}

+ 408 - 0
yudao-vue-ui/components/mescroll-uni/mescroll-uni.vue

@@ -0,0 +1,408 @@
+<template>
+	<view class="mescroll-uni-warp">
+		<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-into-view="scrollToViewId" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true">
+			<view class="mescroll-uni-content mescroll-render-touch"
+			@touchstart="wxsBiz.touchstartEvent" 
+			@touchmove="wxsBiz.touchmoveEvent" 
+			@touchend="wxsBiz.touchendEvent" 
+			@touchcancel="wxsBiz.touchendEvent"
+			:change:prop="wxsBiz.propObserver"
+			:prop="wxsProp">
+				<!-- 状态栏 -->
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+						<view class="downwarp-content" :change:prop="renderBiz.propObserver" :prop="wxsProp">
+							<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+							<view class="downwarp-tip">{{downText}}</view>
+						</view>
+					</view>
+
+					<!-- 列表内容 -->
+					<slot></slot>
+
+					<!-- 空布局 -->
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+					<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+					<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+					<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+						<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+						<view v-show="upLoadType===1">
+							<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+							<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+						</view>
+						<!-- 无数据 -->
+						<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+					</view>
+				</view>
+			
+				<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+				<!-- #ifdef H5 -->
+				<!-- <view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> -->
+				<!-- #endif -->
+				
+				<!-- 适配iPhoneX -->
+				<view v-if="safearea" class="mescroll-safearea"></view>
+			</view>
+		</scroll-view>
+
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+	</view>
+</template>
+
+<!-- 微信小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || APP-PLUS || H5-->
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins:[renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				scrollTop: 0, // 滚动条的位置
+				scrollAnim: false, // 是否开启滚动动画
+				windowTop: 0, // 可使用窗口的顶部位置
+				windowBottom: 0, // 可使用窗口的底部位置
+				windowHeight: 0, // 可使用窗口的高度
+				statusBarHeight: 0, // 状态栏高度
+				scrollToViewId: '' // 滚动到指定view的id
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			// 是否使用fixed定位 (当height有值,则不使用)
+			isFixed(){
+				return !this.height && this.fixed
+			},
+			// mescroll的高度
+			scrollHeight(){
+				if (this.isFixed) {
+					return "auto"
+				} else if(this.height){
+					return this.toPx(this.height) + 'px'
+				}else{
+					return "100%"
+				}
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			fixedTop() {
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+			},
+			padTop() {
+				return !this.isFixed ? this.numTop + 'px' : 0
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom)
+			},
+			fixedBottom() {
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+			},
+			padBottom() {
+				return !this.isFixed ? this.numBottom + 'px' : 0
+			},
+			// 是否为重置下拉的状态
+			isDownReset(){
+				return this.downLoadType===3 || this.downLoadType===4
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 列表是否可滑动
+			scrollable(){
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.optDown.textLoading;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num){
+				if(typeof num === "string"){
+					if (num.indexOf('px') !== -1) {
+						if(num.indexOf('rpx') !== -1) { // "10rpx"
+							num = num.replace('rpx', '');
+						} else if(num.indexOf('upx') !== -1) { // "10upx"
+							num = num.replace('upx', '');
+						} else { // "10px"
+							return Number(num.replace('px', ''))
+						}
+					}else if (num.indexOf('%') !== -1){
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace("%","")) / 100
+						return this.windowHeight * rate
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0
+			},
+			//注册列表滚动事件,用于下拉刷新和上拉加载
+			scroll(e) {
+				this.mescroll.scroll(e.detail, () => {
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+				})
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll)
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			},
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+			setClientHeight() {
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+					this.isExec = true; // 避免多次获取
+					this.$nextTick(() => { // 确保dom已渲染
+						let query = uni.createSelectorQuery();
+						// #ifndef MP-ALIPAY
+						query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+						// #endif
+						let view = query.select('#' + this.viewId);
+						view.boundingClientRect(data => {
+							this.isExec = false;
+							if (data) {
+								this.mescroll.setClientHeight(data.height);
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+								setTimeout(() => {
+									this.setClientHeight()
+								}, this.clientNum * 100)
+							}
+						}).exec();
+					})
+				}
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+							if(vm.downLoadType===4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll)
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+						// 更新容器的高度 (多mescroll的情况)
+						vm.setClientHeight()
+					}
+				}
+			}
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+			
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是scrollview,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+				if(typeof y === 'string'){ // 第一个参数如果为字符串,则使用scroll-into-view
+					// #ifdef MP-WEIXIN
+					// 微信小程序暂不支持slot里面的scroll-into-view,只能计算位置实现
+					uni.createSelectorQuery().select('#'+vm.viewId).boundingClientRect(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
+							let curY = vm.mescroll.getScrollTop()
+							let top = rect.top - mescrollTop
+							top += curY
+							if(!vm.isFixed) top -= vm.numTop
+							vm.scrollTop = curY;
+							vm.$nextTick(function() {
+								vm.scrollTop = top
+							})
+						}).exec()
+					}).exec()
+					// #endif
+					
+					// #ifndef MP-WEIXIN
+					if (vm.scrollToViewId != y) {
+						vm.scrollToViewId = y;
+					} else{
+						vm.scrollToViewId = ''; // scrollToViewId必须变化才会生效,所以此处先置空再赋值
+						vm.$nextTick(function(){
+							vm.scrollToViewId = y;
+						})
+					}
+					// #endif
+					return;
+				}
+				let curY = vm.mescroll.getScrollTop()
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+					vm.scrollTop = curY;
+					vm.$nextTick(function() {
+						vm.scrollTop = y
+					})
+				} else {
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+						vm.scrollTop = step
+					}, t)
+				}
+			})
+			
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		}
+	}
+</script>
+
+<style>
+	@import "./mescroll-uni.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 23 - 0
yudao-vue-ui/components/mescroll-uni/mixins/mescroll-comp.js

@@ -0,0 +1,23 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
+ * 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
+ * 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
+ */
+const MescrollCompMixin = {
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
+	onPageScroll(e) {
+		let item = this.$refs["mescrollItem"];
+		if(item && item.mescroll) item.mescroll.onPageScroll(e);
+	},
+	onReachBottom() {
+		let item = this.$refs["mescrollItem"];
+		if(item && item.mescroll) item.mescroll.onReachBottom();
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		let item = this.$refs["mescrollItem"];
+		if(item && item.mescroll) item.mescroll.onPullDownRefresh();
+	}
+}
+
+export default MescrollCompMixin;

+ 51 - 0
yudao-vue-ui/components/mescroll-uni/mixins/mescroll-more-item.js

@@ -0,0 +1,51 @@
+/**
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
+ */
+const MescrollMoreItemMixin = {
+	// 支付宝小程序不支持props的mixin,需写在具体的页面中
+	// #ifndef MP-ALIPAY
+	props:{
+		i: Number, // 每个tab页的专属下标
+		index: { // 当前tab的下标
+			type: Number,
+			default(){
+				return 0
+			}
+		}
+	},
+	// #endif
+	data() {
+		return {
+			downOption:{
+				auto:false // 不自动加载
+			},
+			upOption:{
+				auto:false // 不自动加载
+			},
+			isInit: false // 当前tab是否已初始化
+		}
+	},
+	watch:{
+		// 监听下标的变化
+		index(val){
+			if (this.i === val && !this.isInit) {
+				this.isInit = true; // 标记为true
+				this.mescroll && this.mescroll.triggerDownScroll();
+			}
+		}
+	},
+	methods: {
+		// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
+			// 自动加载当前tab的数据
+			if(this.i === this.index){
+				this.isInit = true; // 标记为true
+				this.mescroll.triggerDownScroll();
+			}
+		},
+	}
+}
+
+export default MescrollMoreItemMixin;

+ 56 - 0
yudao-vue-ui/components/mescroll-uni/mixins/mescroll-more.js

@@ -0,0 +1,56 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
+ * 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
+ * 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
+ */
+const MescrollMoreMixin = {
+	data() {
+		return {
+			tabIndex: 0 // 当前tab下标
+		}
+	},
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
+	onPageScroll(e) {
+		let mescroll = this.getMescroll(this.tabIndex);
+		mescroll && mescroll.onPageScroll(e);
+	},
+	onReachBottom() {
+		let mescroll = this.getMescroll(this.tabIndex);
+		mescroll && mescroll.onReachBottom();
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		let mescroll = this.getMescroll(this.tabIndex);
+		mescroll && mescroll.onPullDownRefresh();
+	},
+	methods:{
+		// 根据下标获取对应子组件的mescroll
+		getMescroll(i){
+			if(!this.mescrollItems) this.mescrollItems = [];
+			if(!this.mescrollItems[i]) {
+				// v-for中的refs
+				let vForItem = this.$refs["mescrollItem"];
+				if(vForItem){
+					this.mescrollItems[i] = vForItem[i]
+				}else{
+					// 普通的refs,不可重复
+					this.mescrollItems[i] = this.$refs["mescrollItem"+i];
+				}
+			}
+			let item = this.mescrollItems[i]
+			return item ? item.mescroll : null
+		},
+		// 切换tab,恢复滚动条位置
+		tabChange(i){
+			let mescroll = this.getMescroll(i);
+			if(mescroll){
+				// 延时(比$nextTick靠谱一些),确保元素已渲染
+				setTimeout(()=>{
+					mescroll.scrollTo(mescroll.getScrollTop(),0)
+				},30)
+			}
+		}
+	}
+}
+
+export default MescrollMoreMixin;

+ 23 - 0
yudao-vue-ui/components/mescroll-uni/wxs/bounce.js

@@ -0,0 +1,23 @@
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果, 适用于h5和renderjs (下拉刷新时禁止)
+const bounce = {
+	// false: 禁止bounce; true:允许bounce
+	setBounce: function(isBounce){
+		window.$isMescrollBounce = isBounce
+	}
+}
+
+// 引入即自动初始化 (仅初始化一次)
+if(window && window.$isMescrollBounce == null){
+	// 是否允许bounce, 默认允许
+	window.$isMescrollBounce = true
+	// 每次点击时重置bounce
+	window.addEventListener('touchstart', function(){
+		window.$isMescrollBounce = true
+	}, {passive: true})
+	// 滑动中标记是否禁止bounce (如:下拉刷新时禁止)
+	window.addEventListener('touchmove', function(e){
+		!window.$isMescrollBounce && e.preventDefault() // 禁止bounce
+	}, {passive: false})
+}
+
+export default bounce;

+ 102 - 0
yudao-vue-ui/components/mescroll-uni/wxs/mixins.js

@@ -0,0 +1,102 @@
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
+const WxsMixin = {
+	data() {
+		return {
+			// 传入wxs视图层的数据 (响应式)
+			wxsProp: {
+				optDown:{}, // 下拉刷新的配置
+				scrollTop:0, // 滚动条的距离
+				bodyHeight:0, // body的高度
+				isDownScrolling:false, // 是否正在下拉刷新中
+				isUpScrolling:false, // 是否正在上拉加载中
+				isScrollBody:true, // 是否为mescroll-body滚动
+				isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 标记调用wxs视图层的方法
+			callProp: {
+				callType: '', // 方法名
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
+			// #ifndef MP-WEIXIN || APP-PLUS || H5
+			wxsBiz: {
+				//注册列表touchstart事件,用于下拉刷新
+				touchstartEvent: e=> {
+					this.mescroll.touchstartEvent(e);
+				},
+				//注册列表touchmove事件,用于下拉刷新
+				touchmoveEvent: e=> {
+					this.mescroll.touchmoveEvent(e);
+				},
+				//注册列表touchend事件,用于下拉刷新
+				touchendEvent: e=> {
+					this.mescroll.touchendEvent(e);
+				},
+				propObserver(){}, // 抹平wxs的写法
+				callObserver(){} // 抹平wxs的写法
+			},
+			// #endif
+			
+			// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
+			// #ifndef APP-PLUS || H5
+			renderBiz: {
+				propObserver(){} // 抹平renderjs的写法
+			}
+			// #endif
+		}
+	},
+	methods: {
+		// wxs视图层调用逻辑层的回调
+		wxsCall(msg){
+			if(msg.type === 'setWxsProp'){
+				// 更新wxsProp数据 (值改变才触发更新)
+				this.wxsProp = {
+					optDown: this.mescroll.optDown,
+					scrollTop: this.mescroll.getScrollTop(),
+					bodyHeight: this.mescroll.getBodyHeight(),
+					isDownScrolling: this.mescroll.isDownScrolling,
+					isUpScrolling: this.mescroll.isUpScrolling,
+					isUpBoth: this.mescroll.optUp.isBoth,
+					isScrollBody:this.mescroll.isScrollBody,
+					t: Date.now()
+				}
+			}else if(msg.type === 'setLoadType'){
+				// 设置inOffset,outOffset的状态
+				this.downLoadType = msg.downLoadType
+			}else if(msg.type === 'triggerDownScroll'){
+				// 主动触发下拉刷新
+				this.mescroll.triggerDownScroll();
+			}else if(msg.type === 'endDownScroll'){
+				// 结束下拉刷新
+				this.mescroll.endDownScroll();
+			}else if(msg.type === 'triggerUpScroll'){
+				// 主动触发上拉加载
+				this.mescroll.triggerUpScroll(true);
+			}
+		}
+	},
+	mounted() {
+		// #ifdef MP-WEIXIN || APP-PLUS || H5
+		// 配置主动触发wxs显示加载进度的回调
+		this.mescroll.optDown.afterLoading = ()=>{
+			this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+		}
+		// 配置主动触发wxs隐藏加载进度的回调
+		this.mescroll.optDown.afterEndDownScroll = ()=>{
+			this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+			setTimeout(()=>{
+				if(this.downLoadType === 4 || this.downLoadType === 0){
+					this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+				}
+			},320)
+		}
+		// 初始化wxs的数据
+		this.wxsCall({type: 'setWxsProp'})
+		// #endif
+	}
+}
+
+export default WxsMixin;

+ 92 - 0
yudao-vue-ui/components/mescroll-uni/wxs/renderjs.js

@@ -0,0 +1,92 @@
+// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
+// https://uniapp.dcloud.io/frame?id=renderjs
+
+// 与wxs的me实例一致
+var me = {}
+
+// 初始化window对象的touch事件 (仅初始化一次)
+if(window && !window.$mescrollRenderInit){
+	window.$mescrollRenderInit = true
+	
+	
+	window.addEventListener('touchstart', function(e){
+		if (me.disabled()) return;
+		me.startPoint = me.getPoint(e); // 记录起点
+	}, {passive: true})
+	
+	
+	window.addEventListener('touchmove', function(e){
+		if (me.disabled()) return;
+		if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
+		
+		var curPoint = me.getPoint(e); // 当前点
+		var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 向下拉
+		if (moveY > 0) {
+			// 可下拉的条件
+			if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
+				
+				// 只有touch在mescroll的view上面,才禁止bounce
+				var el = e.target;
+				var isMescrollTouch = false;
+				while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
+					var cls = el.classList;
+					if (cls && cls.contains('mescroll-render-touch')) {
+						isMescrollTouch = true
+						break;
+					}
+					el = el.parentNode; // 继续检查其父元素
+				}
+				// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
+				if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
+			}
+		}
+	}, {passive: false})
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || 0
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(e) {
+	if (!e) {
+		return {x: 0,y: 0}
+	}
+	if (e.touches && e.touches[0]) {
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+	} else {
+		return {x: e.clientX,y: e.clientY}
+	}
+}
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+}
+
+/* 导出模块 */
+const renderBiz = {
+	data() {
+		return {
+			propObserver: propObserver,
+		}
+	}
+}
+
+export default renderBiz;

+ 267 - 0
yudao-vue-ui/components/mescroll-uni/wxs/wxs.wxs

@@ -0,0 +1,267 @@
+// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
+// https://uniapp.dcloud.io/frame?id=wxs
+// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 
+
+// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
+var me = {}
+
+// ------ 自定义下拉刷新动画 start ------
+
+/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
+me.onMoving = function (ins, rate, downHight){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			transform: 'translateY(' + downHight + 'px)',
+			transition: ''
+		})
+		// 环形进度条
+		var progress = ins.selectComponent('.mescroll-wxs-progress')
+		progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
+	})
+}
+
+/* 显示下拉刷新进度 */
+me.showLoading = function (ins){
+	me.downHight = me.optDown.offset
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			transform: 'translateY(' + me.downHight + 'px)',
+			transition: 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉 */
+me.endDownScroll = function (ins){
+	me.downHight = 0;
+	me.isDownScrolling = false;
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			transform: 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
+			transition: 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
+me.clearTransform = function (ins){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			transform: '',
+			transition: ''
+		})
+	})
+}
+
+// ------ 自定义下拉刷新动画 end ------
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.bodyHeight = wxsProp.bodyHeight
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+	me.isScrollBody = wxsProp.isScrollBody
+	me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
+}
+
+/**
+ * 监听逻辑层数据的变化 (调用wxs的方法)
+ */
+function callObserver(callProp, oldValue, ins) {
+	if (me.disabled()) return;
+	if(callProp.callType){
+		// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
+		if(callProp.callType === 'showLoading'){
+			me.showLoading(ins)
+		}else if(callProp.callType === 'endDownScroll'){
+			me.endDownScroll(ins)
+		}else if(callProp.callType === 'clearTransform'){
+			me.clearTransform(ins)
+		}
+	}
+}
+
+/**
+ * touch事件
+ */
+function touchstartEvent(e, ins) {
+	if (me.disabled()) return true;
+	
+	me.downHight = 0; // 下拉的距离
+	me.startPoint = me.getPoint(e); // 记录起点
+	me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
+	me.startAngle = 0; // 初始角度
+	me.lastPoint = me.startPoint; // 重置上次move的点
+	me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+	me.inTouchend = false; // 标记不是touchend
+	
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+function touchmoveEvent(e, ins) {
+	var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+	
+	if (me.disabled()) return isPrevent;
+	
+	var scrollTop = me.getScrollTop(); // 当前滚动条的距离
+	var curPoint = me.getPoint(e); // 当前点
+	
+	var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+	
+	// 向下拉 && 在顶部
+	// mescroll-body,直接判定在顶部即可
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+	if (moveY > 0 && (
+			(me.isScrollBody && scrollTop <= 0)
+			||
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+		)) {
+		// 可下拉的条件
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+				me.isUpBoth))) {
+	
+			// 下拉的角度是否在配置的范围内
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+			if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
+	
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+				me.inTouchend = true; // 标记执行touchend
+				touchendEvent(e, ins); // 提前触发touchend
+				return isPrevent;
+			}
+			
+			isPrevent = false // 小程序是return false
+	
+			var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+	
+			// 下拉距离  < 指定距离
+			if (me.downHight < me.optDown.offset) {
+				if (me.movetype !== 1) {
+					me.movetype = 1; // 加入标记,保证只执行一次
+					// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+	
+				// 指定距离  <= 下拉距离
+			} else {
+				if (me.movetype !== 2) {
+					me.movetype = 2; // 加入标记,保证只执行一次
+					// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				if (diff > 0) { // 向下拉
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+				} else { // 向上收
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+				}
+			}
+			
+			me.downHight = Math.round(me.downHight) // 取整
+			var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+			// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+			me.onMoving(ins, rate, me.downHight)
+		}
+	}
+	
+	me.lastPoint = curPoint; // 记录本次移动的点
+	
+	return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+}
+
+function touchendEvent(e, ins) {
+	if (me.disabled()) return true;
+	// 如果下拉区域高度已改变,则需重置回来
+	if (me.isMoveDown) {
+		if (me.downHight >= me.optDown.offset) {
+			// 符合触发刷新的条件
+			me.downHight = me.optDown.offset; // 更新下拉区域高度
+			// me.triggerDownScroll();
+			me.callMethod(ins, {type: 'triggerDownScroll'})
+		} else {
+			// 不符合的话 则重置
+			me.downHight = 0;
+			// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
+			me.callMethod(ins, {type: 'endDownScroll'})
+		}
+		me.movetype = 0;
+		me.isMoveDown = false;
+	} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
+		var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 上滑
+		if (isScrollUp) {
+			// 需检查滑动的角度
+			var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
+			if (angle > 80) {
+				// 检查并触发上拉
+				// me.triggerUpScroll(true);
+				me.callMethod(ins, {type: 'triggerUpScroll'})
+			}
+		}
+	}
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(e) {
+	if (!e) {
+		return {x: 0,y: 0}
+	}
+	if (e.touches && e.touches[0]) {
+		return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+	} else if (e.changedTouches && e.changedTouches[0]) {
+		return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+	} else {
+		return {x: e.clientX,y: e.clientY}
+	}
+}
+
+/* 计算两点之间的角度: 区间 [0,90]*/
+me.getAngle = function (p1, p2) {
+	var x = Math.abs(p1.x - p2.x);
+	var y = Math.abs(p1.y - p2.y);
+	var z = Math.sqrt(x * x + y * y);
+	var angle = 0;
+	if (z !== 0) {
+		angle = Math.asin(y / z) / Math.PI * 180;
+	}
+	return angle
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || 0
+}
+
+/* 获取body的高度 */
+me.getBodyHeight = function() {
+	return me.bodyHeight || 0;
+}
+
+/* 调用逻辑层的方法 */
+me.callMethod = function(ins, param) {
+	if(ins) ins.callMethod('wxsCall', param)
+}
+
+/* 导出模块 */
+module.exports = {
+	propObserver: propObserver,
+	callObserver: callObserver,
+	touchstartEvent: touchstartEvent,
+	touchmoveEvent: touchmoveEvent,
+	touchendEvent: touchendEvent
+}

+ 82 - 0
yudao-vue-ui/components/mix-action-sheet/mix-action-sheet.vue

@@ -0,0 +1,82 @@
+<template>
+	<uni-popup ref="popup" type="bottom">
+		<view class="content">
+			<view v-if="data.title" class="cell b-b center title">
+				<text >{{ data.title }}</text>
+			</view>
+			<view class="cell b-b center" v-for="(item, index) in data.list" :key="index" @click="confirm(item)">
+				<text>{{ item.text }}</text>
+			</view>
+			<view class="cell center cancel-btn" @click="close">
+				<text>取消</text>
+			</view>
+		</view>
+	</uni-popup>
+</template>
+
+<script>
+	/**
+	 * 底部选择菜单
+	 */
+	export default {
+		data() {
+			return {
+				data: {}
+			};
+		},
+		methods: {
+			//选择回调
+			confirm(item){
+				this.$util.throttle(()=>{
+					this.$emit('onConfirm', item)
+				})
+				this.close();
+			},
+			open(data){
+				this.data = data;
+				this.$refs.popup.open();
+			},
+			close(){
+				this.$refs.popup.close();
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.content{
+		background-color: #fff;
+		border-radius: 16rpx 16rpx 0 0;
+		overflow: hidden;
+	}
+	.cell{
+		min-height: 88rpx;
+		font-size: 32rpx;
+		color: #333;
+		position: relative;
+		
+		&:after{
+			position: absolute;
+			z-index: 3;
+			left: 0;
+			top: auto;
+			bottom: 0;
+			right: 0;
+			height: 0;
+			content: '';
+			transform: scaleY(.5);
+			border-bottom: 1px solid #f5f5f5;
+		}
+		&:last-child{
+			height: 96rpx;
+			border-top: 12rpx solid #f7f7f7;
+		}
+		&.title{
+			height: 100rpx;
+			font-size: 28rpx;
+			color: #999;
+		}
+	}
+
+
+</style>

+ 154 - 0
yudao-vue-ui/components/mix-button/mix-button.vue

@@ -0,0 +1,154 @@
+<template>
+	<view 
+		class="mix-btn-content" 
+		:class="{
+				disabled: loading || disabled || dead, 
+			}"
+		:style="[
+			{marginTop: marginTop}
+		]"
+		@click="confirm"
+	>
+		<image v-if="loading" class="loading-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAE3UlEQVRoQ82Z7ZEQNwyGrQoCFQQqgFRAqCBcBSEVQCqAqyBJBTkqACoAKsilAo4KQioQ8+zIO16tvbaWM4P/7lrSq2/JkiYeVX2SUnpmLP4SkTez2MkswgbitaN/MQvMTCBo/xcH5IOI/DxDeWEgqvq0cJfLloZV9X1K6VEEiKo+NNpvo5YLAYm4SxSIqt5JKf2TUrqXUvosIncjlosCqbnLTUrpJxH5XDI+AeRlSumF0Qi7YBTIn4VblXLjYgiynggQVcUKWAOrcB6LCK45fKJAYPixQh1rYBWss5wgELIbqZrzSkSIw9AJATEBW1Z5IyIXUSCqShZ7Z/f+Tyk9LBUyiuYMEMyP5n+oMFldYtQiqopLka04OxedBsSsgun/rjC5EZH7o65lqTzT+WTW2CSN00CKtgKCtBXVoGtoHL6/iciVql6llH51glAflliwdEu85QBf7vUEt8SQvPttXMv5a6YJcUy+BrIJUvp2yX9JnQ1apeuVsdZNtwacvi1nx02744GUubwUDuvAGAutpm9ofRXKwOQMdJWta0L9VzAg4123rKGqWBbZyJr5/C4iyLQcD+R5SumPA/NilZci8qpwDx/43cbQ3COncZQD390xRVAkfX8G/+elUj0Q/JWYeNDxVf5BI9cmFFpHW6vWB3ydTEWq3cWFWQyF+nrywRS5i9tq+rVsgtlqKbaUsav9HiD/XVWxABbKSYBfqC9YoJkMmnXEtIJf5sGoJlM3SEeBWOdLhS/jgOuXxKfv5TzdbkE010ETviWH1m0C8YnmrVlhky1biukCyRct8AD0Y0Hs1lzL6hf0yYpPpzaNlqlyYL+PMht1szP/DVvkDPFveWcBYnHg24mWHPRTSx35no5YtqADjZxmEYsQqf1rsYJSy/R7RJaO4BIgrbbkUCYRmeKWqkrrMgoiy7gA6bUlNUCfRMTn+681xnJfVdFwr7PwvC4AAnqWCrU6UROOKvtkZsayVD+qGGL2ZnUPA5QntRYR1jTNLnWU84z/pvj5DEF7NMNArKmjraZ1oAM+NZp6wcwjyFb/nnHbYSCWFmmtyyBfR9eexnrf3ZBGzKKkoT4L2l0gB8MN92c2jdCnNGym0pZCjtp4NM9scLQsO72+aRRDBKcclHMQrssscthN7IAUQ74fbkrerG4gfusPN8afoc63TEyFKK661amNumz9WmmYGsKQ4/e8xA7AYUJr300AqsodVkMIVxt3SSjw8fWtutWJLB92A79VYt/ibLYb1suxh1rrj1sV7fbGpelt7IZHOQdxh9XSSnMECAM/brQrhJUlGzKsQFz7s8aT3Su3L4eLa/sfi+dnhw2fXdZy7Uo3Dip7LVzvXnYtt43cZLhKs9p9SrBxI7t1ex3Uy/XO5MSRb/83a88OEHo8rJxdhonzcUSG8t9uHWkRVlWSQrk429WUIyAWX34ZPrT/rcl0CkjjLXHnGj0gBqZ8NK0+441Y6SwQ1p1lq1IN1kEgfhl+qsiGgVSCtPnKNALErFI+QRym43CL0mghCNDyTYPfmhoMAMG6BH5uTcLviCGLVBYVh69Mo0BqhTW6EwgBMYb5rb078gaBlC8B4a46DMTAEKDXvZ4qAiS7Mu3L1MFqJAVWpr4ytYa1HOF5yiKjDFq91uj9yH9TgZgbLiPB7O3LF2dInShyDo35AAAAAElFTkSuQmCC"></image>
+		<text v-if="icon" class="mix-icon" :class="icon" :style="{fontSize: iconSize + 'rpx'}"></text>
+		<text class="mix-text">{{ text }}</text>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 按钮组件
+	 * @prop text 按钮显示文字
+	 * @prop icon 按钮图标
+	 * @prop iconSize 按钮显示文字
+	 * @prop isConfirm 点击后是否处理loading状态
+	 * @prop disabled 按钮禁用
+	 * @prop marginTop 按钮上边距
+	 */
+	let stopTimer = null;
+	export default {
+		name: 'MixButton',
+		data() {
+			return {
+				dead: false,
+				loading: false
+			};
+		},
+		props: {
+			text: {
+				type: String,
+				default: '提交'
+			},
+			icon: {
+				type: String,
+				default: ''
+			},
+			iconSize: {
+				type: Number,
+				default: 32
+			},
+			isConfirm: {
+				type: Boolean,
+				default: true
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			marginTop: {
+				type: String,
+				default: '0rpx'
+			}
+		},
+		methods: {
+			stop(){
+				if(stopTimer){
+					clearTimeout(stopTimer);
+					stopTimer = null;
+				}
+				this.loading = false;
+			},
+			death(){
+				this.loading = false;
+				this.dead = true;
+			},
+			confirm(){
+				if(this.dead){
+					return;
+				}
+				if(this.loading || this.disabled){
+					return;
+				}
+				if(this.isConfirm){
+					this.loading = true;
+					
+					stopTimer = setTimeout(()=>{
+						this.loading = false;
+						clearTimeout(stopTimer);
+						stopTimer = null;
+					}, 10000)
+				}
+				this.$emit('onConfirm');
+			}
+		}
+	}
+</script>
+
+<style scoped lang='scss'>
+	.mix-btn-content{
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 610rpx;
+		height: 88rpx;
+		margin: 0 auto;
+		font-size: 32rpx;
+		color: #fff;
+		border-radius: 100rpx;
+		background-color: $base-color;
+		position: relative;
+		
+		&:after{
+			content: '';
+			position: absolute;
+			left: 50%;
+			top: 25%;
+			transform: translateX(-50%);
+			width: 85%;
+			height: 85%;
+			background: linear-gradient(131deg, rgba(255,115,138,1) 0%, rgba(255,83,111,1) 100%);
+			border-radius: 100rpx;
+			opacity: 0.4;
+			filter:blur(10rpx);
+		}
+		&.disabled {
+			opacity: .65;
+		}
+		.mix-text{
+			position: relative;
+			z-index: 1;
+		}
+		.mix-icon{
+			position: relative;
+			z-index: 1;
+			margin-right: 8rpx;
+		}
+		.loading-icon{
+			width: 34rpx;
+			height: 34rpx;
+			transform-origin:50% 50%;
+			margin-right: 16rpx;
+			animation: rotate 2s linear infinite;
+			position: relative;
+			z-index: 1;
+		}
+	}
+	@keyframes rotate{
+		from {
+			transform:rotate(0deg)
+		}
+		to {
+			transform:rotate(360deg)
+		}
+	}
+</style>

+ 113 - 0
yudao-vue-ui/components/mix-code/mix-code.vue

@@ -0,0 +1,113 @@
+<template>
+	<view class="mix-get-code" @click="getCode">
+		<view v-if="loading" class="loading">
+			<mix-icon-loading size="28rpx" color="#0083ff"></mix-icon-loading>
+		</view>
+		<text class="text" :class="{disabled: timeDown > 0}">
+			{{ timeDown > 0 ? '重新获取 ' + timeDown + 's' : '获取验证码' }}
+		</text>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 手机验证码
+	 * @prop mobile 手机号
+	 * @prop templateCode 短信模版id
+	 */
+	import {checkStr} from '@/common/js/util'
+	export default {
+		//获取手机验证码
+		name: 'MixMobileCode',
+		data() {
+			return {
+				loading: false,
+				timeDown: ''
+			}
+		},
+		props: {
+			mobile: {
+				type: String,
+				default: ''
+			},
+			templateCode: {
+				type: String,
+				default: ''
+			},
+			action: {
+				type: String,
+				default: '用户注册' //设置支付密码
+			}
+		},
+		methods: {
+			//获取验证码
+			async getCode(){
+				if(this.timeDown > 0){
+					return;
+				}
+				this.$util.throttle(()=>{
+					const mobile = this.mobile || this.$store.state.userInfo.username;;
+					if(!checkStr(mobile, 'mobile')){
+						this.$util.msg('手机号码格式不正确');
+						return;
+					}
+					this.loading = true;
+					this.$request('smsCode', 'send', {
+						mobile,
+						action: this.action, //uni短信必填
+						TemplateCode: this.templateCode, //阿里云必填
+					}).then(response=>{
+						this.$util.msg(response.msg);
+						this.loading = false;
+						if(response.status === 1){
+							this.countDown(60);
+						}
+					}).catch(err=>{
+						this.$util.msg('验证码发送失败');
+						this.loading = false;
+						console.log(err);
+					})
+				}, 2000)
+			},
+			//倒计时
+			countDown(timer){
+				timer --;
+				this.timeDown = timer;
+				if(timer > 0){
+					setTimeout(()=>{
+						this.countDown(timer);
+					}, 1000)
+				}
+			},
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.mix-get-code{
+		flex-shrink: 0;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		height: 36rpx;
+		
+		&:before{
+			content: '';
+			width: 0;
+			height: 40;
+			border-right: 1px solid #f0f0f0;
+		}
+		.loading{
+			margin-right: 8rpx;
+		}
+		.text{
+			line-height: 28rpx;
+			font-size: 26rpx;
+			color: #40a2ff;
+			
+			&.disabled{
+				color: #ccc;
+			}
+		}
+	}
+</style>

+ 209 - 0
yudao-vue-ui/components/mix-empty/mix-empty.vue

@@ -0,0 +1,209 @@
+<template>
+	<view class="mix-empty" :style="{backgroundColor: backgroundColor}">
+		<view v-if="type==='cart'" class="cart column center">
+			<image class="icon" src="/static/empty/cart.png"></image>
+			<text class="title">{{ hasLogin ? '空空如也' : '先去登录嘛' }}</text>
+			<text class="text">别忘了买点什么犒赏一下自己哦</text>
+			<view class="btn center" @click="onCartBtnClick">
+				<text>{{ hasLogin ? '随便逛逛' : '去登录' }}</text>
+			</view>
+		</view>
+		<view v-else-if="type==='address'" class="address column center">
+			<image class="icon" src="/static/empty/address.png"></image>
+			<text class="text">找不到您的收货地址哦,先去添加一个吧~</text>
+			<view class="btn center" @click="navTo('manage')">
+				<text class="mix-icon icon-jia2"></text>
+			</view>
+		</view>
+		<view v-else-if="type==='favorite'" class="favorite column center">
+			<image class="icon" src="/static/empty/favorite.png"></image>
+			<text class="text">收藏夹空空的,先去逛逛吧~</text>
+			<view class="btn center" @click="switchTab('/pages/tabbar/home')">
+				<text>随便逛逛</text>
+			</view>
+		</view>
+		<view v-else class="default column center">
+			<image class="icon" src="/static/empty/default.png"></image>
+			<text class="text">{{ text }}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 缺省显示
+	 * @prop text 缺省文字提示
+	 * @prop type 缺省类型
+	 * @prop backgroundColor 缺省页面背景色
+	 */
+	export default {
+		computed: {
+			hasLogin(){
+				return !!this.$store.getters.hasLogin;
+			}
+		},
+		props: {
+			text: {
+				type: String,
+				default: '暂时没有数据'
+			},
+			type: {
+				type: String,
+				default: ''
+			},
+			backgroundColor: {
+				type: String,
+				default: 'rgba(0,0,0,0)'
+			}
+		},
+		methods: {
+			onCartBtnClick(){
+				if(this.hasLogin){
+					uni.switchTab({
+						url: '/pages/tabbar/home'
+					})
+				}else{
+					this.navTo('/pages/auth/login');
+				}
+			},
+			switchTab(url){
+				uni.switchTab({
+					url
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.mix-empty{
+		position: fixed;
+		left: 0;
+		right: 0;
+		top: 0;
+		bottom: 0;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		animation: show .5s 1;
+	}
+	@keyframes show{
+		from {
+			opacity: 0;
+		}
+		to {
+			opacity: 1;
+		}
+	}
+	.default{
+		padding-top: 26vh;
+		/* #ifdef H5 */
+		padding-top: 30vh;
+		/* #endif */
+		
+		.icon{
+			width: 460rpx;
+			height: 342rpx;
+		}
+		.text{
+			margin-top: 10rpx;
+			font-size: 28rpx;
+			color: #999;
+		}
+	}
+	.cart{
+		padding-top: 14vh;
+		/* #ifdef H5 */
+		padding-top: 18vh;
+		/* #endif */
+		
+		.icon{
+			width: 320rpx;
+			height: 320rpx;
+		}
+		.title{
+			margin: 50rpx 0 26rpx;
+			font-size: 34rpx;
+			color: #333;
+		}
+		.text{
+			font-size: 28rpx;
+			color: #aaa;
+		}
+		.btn{
+			width: 320rpx;
+			height: 80rpx;
+			margin-top: 80rpx;
+			text-indent: 2rpx;
+			letter-spacing: 2rpx;
+			font-size: 32rpx;
+			color: #fff;
+			border-radius: 100rpx;
+			background: linear-gradient(to bottom right, #ffb2bf, $base-color);
+		}
+	}
+	.address{
+		padding-top: 6vh;
+		/* #ifdef H5 */
+		padding-top: 10vh;
+		/* #endif */
+		
+		.icon{
+			width: 380rpx;
+			height: 380rpx;
+		}
+		.text{
+			width: 400rpx;
+			margin-top: 40rpx;
+			font-size: 30rpx;
+			color: #999;
+			text-align: center;
+			line-height: 1.6;
+		}
+		.btn{
+			position: fixed;
+			left: 50%;
+			bottom: 120rpx;
+			width: 110rpx;
+			height: 110rpx;
+			background-color: $base-color;
+			border-radius: 100rpx;
+			transform: translateX(-50%);
+			box-shadow: 2rpx 2rpx 10rpx rgba(255, 83, 111, .5);
+		}
+		.icon-jia2{
+			font-size: 50rpx;
+			color: #fff;
+		}
+	}
+	.favorite{
+		padding-top: 6vh;
+		/* #ifdef H5 */
+		padding-top: 10vh;
+		/* #endif */
+		
+		.icon{
+			width: 360rpx;
+			height: 360rpx;
+		}
+		.text{
+			width: 400rpx;
+			margin-top: 40rpx;
+			font-size: 30rpx;
+			color: #999;
+			text-align: center;
+			line-height: 1.6;
+		}
+		.btn{
+			width: 320rpx;
+			height: 80rpx;
+			margin-top: 40rpx;
+			text-indent: 2rpx;
+			letter-spacing: 2rpx;
+			font-size: 32rpx;
+			color: #fff;
+			border-radius: 100rpx;
+			background: linear-gradient(to bottom right, #ffb2bf, $base-color);
+		}
+	}
+</style>

+ 66 - 0
yudao-vue-ui/components/mix-icon-loading/mix-icon-loading.vue

@@ -0,0 +1,66 @@
+<template>
+	<view class="mix-icon-loading">
+		<view 
+			class="loading-icon"
+			:style="{
+				width: size,
+				height: size,
+				borderRightColor: color
+			}"
+		></view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 菊花loading小图标
+	 * @prop size 尺寸,单位rpx
+	 * @prop color 颜色
+	 */
+	export default {
+		name: 'MixIconLoading',
+		data() {
+			return {
+				
+			};
+		},
+		props: {
+			size: {
+				type: String,
+				default: '26rpx'
+			},
+			color: {
+				type: String,
+				default: '#999'
+			}
+		},
+		methods: {
+			
+		}
+	}
+</script>
+
+<style scoped lang='scss'>
+	.mix-icon-loading{
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: auto;
+		height: auto;
+	}
+	.loading-icon{
+		width: 28rpx;
+		height: 28rpx;
+		border: 4rpx solid #ddd;
+		animation: mix-loading 1.8s steps(12) infinite;
+		border-radius: 100rpx;
+	}
+	@keyframes mix-loading{
+		from {
+			transform:rotate(0deg)
+		}
+		to {
+			transform: rotate(1turn)
+		}
+	}
+</style>

+ 117 - 0
yudao-vue-ui/components/mix-list-cell/mix-list-cell.vue

@@ -0,0 +1,117 @@
+<template>
+	<view class="content">
+		<view class="mix-list-cell" :class="border" @click="onClick" hover-class="cell-hover"  :hover-stay-time="50">
+			<text
+				v-if="icon"
+				class="cell-icon mix-icon"
+				:style="[{
+					color: iconColor,
+				}]"
+				:class="icon"
+			></text>
+			<text class="cell-tit clamp">{{ title }}</text>
+			<text v-if="tips" class="cell-tip" :style="{color: tipsColor}">{{ tips }}</text>
+			<text class="cell-more mix-icon"
+				:class="typeList[navigateType]"
+			></text>
+		</view>
+	</view>
+</template>
+ 
+<script>
+	/**
+	 *  简单封装了下, 应用范围比较狭窄,可以在此基础上进行扩展使用
+	 *  比如加入image, iconSize可控等
+	 */
+	export default {
+		data() {
+			return {
+				typeList: {
+					left: 'icon-zuo',
+					right: 'icon-you',
+					up: 'icon-shang',
+					down: 'icon-xia'
+				},
+			}
+		},
+		props: {
+			icon: {
+				type: String,
+				default: ''
+			},
+			title: {
+				type: String,
+				default: '标题'
+			},
+			tips: {
+				type: String,
+				default: ''
+			},
+			tipsColor: {
+				type: String,
+				default: '#999'
+			},
+			navigateType: {
+				type: String,
+				default: 'right'
+			},
+			border: {
+				type: String,
+				default: 'b-b'
+			},
+			hoverClass: {
+				type: String,
+				default: 'cell-hover'
+			},
+			iconColor: {
+				type: String,
+				default: '#333'
+			}
+		},
+		methods: {
+			onClick(){
+				this.$emit('onClick');
+			}
+		},
+	}
+</script>
+
+<style scoped lang='scss'>
+	.mix-list-cell{
+		display:flex;
+		align-items: center;
+		height: 96rpx;
+		padding: 0 24rpx;
+		position:relative;
+		
+		&.cell-hover{
+			background:#fafafa;
+		}
+		&.b-b{
+			&:after{
+				left: 30rpx;
+				border-color: #f0f0f0;
+			}
+		}
+		.cell-icon{
+			align-self: center;
+			width: 60rpx;
+			font-size: 38rpx;
+		}
+		.cell-more{
+			align-self: center;
+			font-size: 24rpx;
+			color: #999;
+			margin-left: 16rpx;
+		}
+		.cell-tit{
+			flex: 1;
+			font-size: 30rpx;
+			color: #333;
+			margin-right:10rpx;
+		}
+		.cell-tip{
+			font-size: 26rpx;
+		}
+	}
+</style>

+ 60 - 0
yudao-vue-ui/components/mix-load-more/mix-load-more.vue

@@ -0,0 +1,60 @@
+<template>
+	<view>
+		<view v-show="status !== 2" class="mix-load-more center">
+			<image class="loading-icon" src="/static/loading/hamster.gif"></image>
+			<text class="text">{{ textList[status] }}</text>
+		</view>
+		<view v-show="status === 2" class="mix-load-more center">
+			<image class="logo" src="/static/logo-b-w.png"></image>
+			<text class="text">国云网络提供技术支持</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 上划加载更多
+	 * @prop {Number} status 0加载前,1加载中,2没有更多
+	 * @prop {Array} textList ['加载前提示', '加载中提示', '加载完提示']
+	 */
+	export default {
+		name: "mix-load-more",
+		props: {
+			status: {
+				type: Number,
+				default: 0
+			},
+			textList: {
+				type: Array,
+				default () {
+					return [
+						'上拉显示更多',
+						'正在加载 ..',
+						'我也是有底线的~'
+					];
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.mix-load-more{
+		width: 750rpx;
+		height: 110rpx;
+	}
+	.loading-icon{
+		width: 64rpx;
+		height: 68rpx;
+		margin-right: 20rpx;
+	}
+	.text{
+		font-size: 26rpx;
+		color: #999;
+	}
+	.logo{
+		width: 34rpx;
+		height: 34rpx;
+		margin-right: 12rpx;
+	}
+</style>

+ 114 - 0
yudao-vue-ui/components/mix-loading/mix-loading.vue

@@ -0,0 +1,114 @@
+<template>
+	<view class="mix-loading center">
+		<view v-if="!isTimeout" class="center">
+			<view v-if="mask" class="mask" @click.stop.prevent="stopPrevent" @touchmove.stop.prevent="stopPrevent"></view>
+			<!-- 黑底菊花 -->
+			<view v-if="type === 1" class="chry column center">
+				<view class="icon"></view>
+				<text class="tit">{{ title }}</text>
+			</view>
+			<!-- 仓鼠 -->
+			<image v-else-if="type === 2" class="hamster" src="/static/loading/hamster.gif"></image>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * loading
+	 * @prop type 1 黑底菊花 2 小胖仓鼠
+	 * @prop mask 遮罩层
+	 * @prop timeout 超时时间(秒) 默认10s
+	 */
+	export default {
+		name: 'MixIconLoading',
+		data(){
+			return {
+				isTimeout: false
+			}
+		},
+		props: {
+			type: {
+				type: Number,
+				default: 1
+			},
+			mask: {
+				type: Boolean,
+				default: false
+			},
+			timeout: {
+				type: Number,
+				default: 10
+			},
+			title: {
+				type: String,
+				default: '请稍候'
+			}
+		},
+		created() {
+			this._timer = setTimeout(()=>{
+				if(!this.isLoading){
+					return;
+				}
+				this.isTimeout = true;
+				uni.showToast({
+					title: '加载超时,请重试',
+					icon: 'none'
+				})
+			}, this.timeout * 1000)
+		},
+		destroyed() {
+			this._timer && clearTimeout(this._timer);
+		}
+	}
+</script>
+
+<style scoped lang='scss'>
+	.mix-loading{
+		position: fixed;
+		left: 50vw;
+		top: 46vh;
+		width: 0;
+		height: 0;
+		z-index: 999;
+	}
+	.mask{
+		position: fixed;
+		left: 0;
+		top: 0;
+		right: 0;
+		bottom: 0;
+	}
+	.chry{
+		min-width: 120rpx;
+		min-height: 116rpx;
+		border-radius: 10rpx;
+		background-color: rgba(17,17,17,.7);
+		
+		.icon{
+			width: 64rpx;
+			height: 64rpx;
+			background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=);
+			background-repeat: no-repeat;
+			background-size: 100% 100%;
+			animation: mix-loading 1s steps(12) infinite;
+		}
+		.tit{
+			margin: 10rpx 0 6rpx;
+			font-size: 20rpx;
+			color: #e9e9e9;
+		}
+	}
+	@keyframes mix-loading{
+		from {
+			transform:rotate(0deg)
+		}
+		to {
+			transform: rotate(1turn)
+		}
+	}
+	.hamster{
+		width: 106rpx;
+		height: 120rpx;
+	}
+</style>

+ 105 - 0
yudao-vue-ui/components/mix-modal/mix-modal.vue

@@ -0,0 +1,105 @@
+<template>
+	<uni-popup ref="popup">
+		<view class="pop-content">
+			<text class="title">{{ title }}</text>
+			<view class="con center">
+				<text class="text">{{ text }}</text>
+			</view>
+			<view class="btn-group row b-t">
+				<view class="btn center" @click="close">
+					<text>{{ cancelText }}</text>
+				</view>
+				<view class="btn center b-l" @click="confirm">
+					<text>{{ confirmText }}</text>
+				</view>
+			</view>
+		</view>
+	</uni-popup>
+</template>
+
+<script>
+	/**
+	 * 确认对话框
+	 * @prop title 标题
+	 * @prop text 提示内容
+	 * @prop cancelText 取消按钮文字
+	 * @prop confirmText 确认按钮文字
+	 * @event onConfirm 确认按钮点击时触发
+	 */
+	export default {
+		data() {
+			return {
+				
+			};
+		},
+		props: {
+			title: String,
+			text: String,
+			cancelText: {
+				type: String,
+				default: '取消'
+			},
+			confirmText: {
+				type: String,
+				default: '确定'
+			}
+		},
+		methods: {
+			confirm(){
+				this.$emit('onConfirm');
+				this.close();
+			},
+			open(){
+				this.$refs.popup.open();
+			},
+			close(){
+				this.$refs.popup.close();
+			}
+		}
+	}
+</script>
+
+<style scoped lang='scss'>
+	.pop-content{
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		width: 540rpx;
+		padding-top: 36rpx;
+		background-color: #fff;
+		border-radius: 24rpx;
+		overflow: hidden;
+	
+		.title{
+			font-size: 32rpx;
+			color: #333;
+			line-height: 48rpx;
+			font-weight: 700;
+		}
+		.con{
+			padding: 36rpx 40rpx 54rpx;
+		}
+		.text{
+			width: 460rpx;
+			font-size: 26rpx;
+			color: #333;
+			line-height: 40rpx;
+			text-align: center;
+		}
+		.btn-group{
+			width: 100%;
+		}
+		.btn{
+			flex: 1;
+			height: 88rpx;
+			line-height: 88rpx;
+			font-size: 32rpx;
+			color: #777;
+			
+			&:last-child{
+				color: #007aff;
+			}
+		}
+	}
+</style>

+ 139 - 0
yudao-vue-ui/components/mix-nav-bar/mix-nav-bar.vue

@@ -0,0 +1,139 @@
+<template>
+	<view class="nav-bar b-b">
+		<view
+			class="nav-item" 
+			v-for="(item, index) in navs" 
+			:key="index"
+			@click="navChange(index)"
+		>
+			<view class="tit-wrap">
+				<text class="tit" :class="{'active': current == index}">{{ item.name }}</text>
+				<text v-if="counts.length > index && counts[index] > 0" class="number">{{ counts[index] }}</text>
+			</view>
+			<view class="line" :class="{'line--show': current === index}"></view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 顶部tab栏
+	 */
+	export default {
+		data(){
+			return {
+				countList: [],
+			}
+		},
+		props: {
+			navs: {
+				type: Array,
+				default(){
+					return [];
+				}
+			},
+			current: {
+				type: Number,
+				default: 0
+			},
+			counts: {
+				type: Array,
+				default(){
+					return [];
+				}
+			}
+		},
+		watch: {
+			
+		},
+		methods: {
+			navChange(index){
+				this.$emit('onChange', index);
+			}
+		}
+	}
+</script>
+
+<style scoped lang='scss'>
+	/* #ifndef APP-NVUE */
+	view{
+		display: flex;
+		flex-direction: column;
+	}
+	/* #endif */
+	.fill-view{
+		height: 84rpx;
+		width: 100%;
+	}
+	.nav-bar{
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		justify-content: space-around;
+		width: 750rpx;
+		height: 84rpx;
+		background-color: #fff;
+		z-index: 90;
+		position: fixed;
+		left: 0;
+		top: 0;
+		/* #ifdef H5 */
+		top: var(--window-top);
+		/* #endif */
+		
+		&:after{
+			border-color: #f7f7f7;
+		}
+	}
+	.nav-item{
+		flex: 1;
+		align-items: center;
+		justify-content: center;
+		height: 84rpx;
+		padding-top: 4rpx;
+		position: relative;
+	}
+	.tit-wrap{
+		flex: 1;
+		flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		position: relative;
+	}
+	.number{
+		position: absolute;
+		right: -20rpx;
+		top: 0px;
+		min-width: 36rpx;
+		height: 36rpx;
+		padding: 0 6rpx;
+		text-align: center;
+		line-height: 28rpx;
+		border: 4rpx solid #fff;
+		background-color: $base-color;
+		border-radius: 100rpx;
+		font-size: 20rpx;
+		color: #fff;
+	}
+	.tit{
+		font-size: 30rpx;
+		color: #333;
+	}
+	.active{
+		color: #ff4443;
+		font-weight: 700;
+	}
+	.line{
+		width: 34rpx;
+		height: 4rpx;
+		border-radius: 100rpx;
+		background-color: #ff4443;
+		opacity: 0;
+		
+		&--show{
+			opacity: 1;
+		}
+	}
+</style>

+ 180 - 0
yudao-vue-ui/components/mix-number-box/mix-number-box.vue

@@ -0,0 +1,180 @@
+<template>
+	<view class="uni-numbox">
+		<view class="uni-numbox-minus" 
+			@click="_calcValue('subtract')"
+		>
+			<text class="mix-icon icon--jianhao" :class="minDisabled?'uni-numbox-disabled': ''" ></text>
+		</view>
+		<input
+			class="uni-numbox-value" 
+			type="number" 
+			:disabled="inputDisabled"
+			:value="inputValue" 
+			@blur="_onBlur"
+		>
+		
+		<view 
+			class="uni-numbox-plus" 
+			@click="_calcValue('add')"
+		>
+			<text class="mix-icon icon-jia2" :class="maxDisabled?'uni-numbox-disabled': ''" ></text>
+		</view>
+	</view>
+</template>
+<script>
+	/**
+	 * index 当前行下标
+	 * value 默认值
+	 * min 最小值
+	 * max 最大值
+	 * step 步进值
+	 * disabled 禁用
+	 */
+	export default {
+		name: 'uni-number-box',
+		props: {
+			index: {
+				type: Number,
+				default: 0
+			},
+			value: {
+				type: Number,
+				default: 1
+			},
+			min: {
+				type: Number,
+				default: -Infinity
+			},
+			max: {
+				type: Number,
+				default: 99
+			},
+			step: {
+				type: Number,
+				default: 1
+			},
+			inputDisabled: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				inputValue: this.value,
+			}
+		},
+		created(){
+			
+		},
+		computed: {
+			maxDisabled(){
+				return this.inputValue >= this.max;
+			},
+			minDisabled(){
+				return this.inputValue <= this.min;
+			},
+		},
+		watch: {
+			inputValue(number) {
+				const data = {
+					number: number,
+					index: this.index
+				}
+				this.$emit('eventChange', data);
+			},
+		},
+		methods: {
+			_calcValue(type) {
+				let value = this.inputValue;
+				let newValue = 0;
+				let step = this.step;
+				
+				if(type === 'subtract'){
+					newValue = value - step;
+					
+					if(newValue < this.min){
+						newValue = this.min
+						if(this.min > 1){
+							this.$api.msg(this.limit_message);
+						}
+					}
+				}else if(type === 'add'){
+					newValue = value + step;
+					
+					if(newValue > this.max){
+						newValue = this.max
+					}
+				}
+				if(newValue === value){
+					return;
+				}
+				this.inputValue = newValue;
+			},
+			_onBlur(event) {
+				let value = event.detail.value;
+				let constValue = value;
+				if (!value) {
+					this.inputValue = 0;
+					return
+				}
+				value = +value;
+				if (value > this.max) {
+					value = this.max;
+				} else if (value < this.min) {
+					value = this.min
+				}
+				
+				if(constValue != value){
+					this.inputValue = constValue;
+					this.$nextTick(()=>{
+						this.inputValue = value
+					})
+				}else{
+					this.inputValue = value
+				}
+			}
+		}
+	}
+</script>
+<style>
+	.uni-numbox {
+		display: flex;
+		justify-content: flex-start;
+		flex-direction: row;
+		align-items: center;
+		height: 50rpx;
+	}
+
+	.uni-numbox-minus,
+	.uni-numbox-plus {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		width: 50rpx;
+		height: 100%;
+		line-height: 1;
+		background-color: #f7f7f7;
+	}
+	.uni-numbox-minus .mix-icon,
+	.uni-numbox-plus .mix-icon{
+		font-size: 32rpx;
+		color: #333;
+	}
+
+	.uni-numbox-value {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		background-color: #fff;
+		width: 60rpx;
+		height: 50rpx;
+		min-height: 50rpx;
+		text-align: center;
+		font-size: 28rpx;
+		color: #333;
+	}
+
+	.uni-numbox-disabled.mix-icon {
+		color: #C0C4CC;
+	}
+</style>

+ 53 - 0
yudao-vue-ui/components/mix-price-view/mix-price-view.vue

@@ -0,0 +1,53 @@
+<template>
+	<view class="mix-price-view" :style="{fontSize: size - 8 + 'rpx'}">
+		<text>¥</text>
+		<text class="price" :style="{fontSize: size + 'rpx'}">{{ priceArr[0] }}</text>
+		<text>.{{ priceArr[1] }}</text>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 价格显示组件
+	 */
+	export default {
+		data() {
+			return {
+				priceArr: []
+			};
+		},
+		props: {
+			price: {
+				type: Number,
+				default: 0
+			},
+			size: {
+				type: Number,
+				default: 36
+			}
+		},
+		watch: {
+			price(){
+				this.render();
+			}
+		},
+		created() {
+			this.render();
+		},
+		methods: {
+			render(){
+				const price = parseFloat(this.price).toFixed(2);
+				this.priceArr = (''+price).split('.');
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.mix-price-view{
+		color: $base-color;
+	}
+	.price{
+		font-weight: 700;
+	}
+</style>

+ 137 - 0
yudao-vue-ui/components/mix-timeline/mix-timeline.vue

@@ -0,0 +1,137 @@
+<template>
+	<view class="mix-timeline">
+		<view class="cell" v-for="(item, index) in list" :key="index">
+			<view class="left column center">
+				<text class="time">{{ item.time | date('H:i') }}</text>
+				<text class="date">{{ item.time | date('m/d') }}</text>
+			</view>
+			<view class="cen center">
+				<view class="circle center"></view>
+			</view>
+			<view class="right column">
+				<text class="title">{{ item.title }}</text>
+				<text v-if="item.tip" class="tip">{{ item.tip }}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * 时间轴
+	 * {
+	 *	title: 标题
+	 *	tip: 小字
+	 *	time: 时间戳
+	 *	}  
+	 */
+	export default {
+		data() {
+			return {
+				
+			};
+		},
+		props: {
+			list: {
+				type: Array,
+				default(){
+					return []
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.mix-timeline{
+		
+	}
+	.cell{
+		display: flex;
+		align-items: flex-start;
+		width: 100%;
+		padding: 0 30rpx 0;
+		
+		&:first-child .circle{
+			&:before{
+				background-color: $base-color;
+			}
+			&:after{
+				content: '';
+				position: absolute;
+				width: 28rpx;
+				height: 28rpx;
+				background-color: #f9e0eb;
+				border-radius: 100rpx;
+			}
+		}
+		&:last-child .right:before{
+			display: none;
+		}
+	}
+	.left{
+		
+		.time{
+			font-size: 26rpx;
+			color: #333;
+			line-height: 44rpx;
+		}
+		.date{
+			font-size: 20rpx;
+			color: #333;
+		}
+	}
+	.cen{
+		width: 80rpx;
+		height: 44rpx;
+		
+		.circle{
+			width: 16rpx;
+			height: 16rpx;
+			position: relative;
+			z-index: 1;
+			
+			&:before{
+				content: '';
+				width: 16rpx;
+				height: 16rpx;
+				background-color: #ddd;
+				border-radius: 100rpx;
+				position: relative;
+				z-index: 5;
+			}
+		}
+	}
+	.right{
+		flex: 1;
+		padding-bottom: 30rpx;
+		position: relative;
+		min-height: 96rpx;
+		
+		&:before{
+			content: '';
+			width: 2rpx;
+			position: absolute;
+			left: 0;
+			top: 0;
+			bottom: 0;
+			background-color: #ddd;
+			transform: translate(-42rpx, 22rpx);
+		}
+		.title{
+			font-size: 28rpx;
+			color: #333;
+			line-height: 44rpx;
+			font-weight: 700;
+		}
+		.tip{
+			margin-top: 6rpx;
+			font-size: 24rpx;
+			color: #999;
+			line-height: 36rpx;
+		}
+	}
+	
+
+
+</style>

File diff suppressed because it is too large
+ 11 - 0
yudao-vue-ui/components/mix-upload-image/mix-upload-image.vue


File diff suppressed because it is too large
+ 14 - 0
yudao-vue-ui/components/number-keyboard/number-keyboard.vue


+ 97 - 0
yudao-vue-ui/components/pay-password-keyboard/pay-password-keyboard.vue

@@ -0,0 +1,97 @@
+<template>
+	<uni-popup ref="uniPopup" type="bottom">
+		<view class="content">
+			<text class="mix-icon icon-guanbi" @click="close"></text>
+			<view class="center title">
+				<text>输入支付密码</text>
+			</view>
+			<view class="input center">
+				<view class="item center" :class="{has: pwd.length > index}" v-for="(item, index) in 6" :key="index"></view>
+			</view>
+			<view class="reset-btn center" @click="navTo('/pages/auth/payPassword')">
+				<text>重置密码</text>
+			</view>
+			<number-keyboard ref="keybord" @onChange="onNumberChange"></number-keyboard>
+		</view>
+	</uni-popup>
+</template>
+
+<script>
+	/**
+	 * 支付密码键盘
+	 */
+	export default {
+		data() {
+			return {
+				pwd: ''
+			};
+		},
+		watch: {
+			pwd(pwd){
+				if(pwd.length === 0){
+					this.$refs.keybord.val = '';
+				}
+			}
+		},
+		methods: {
+			open(){
+				this.$refs.uniPopup.open();
+			},
+			close(){
+				this.$refs.uniPopup.close();
+			},
+			onNumberChange(pwd){
+				this.pwd = pwd;
+				if(pwd.length >= 6){
+					this.$emit('onConfirm', pwd.substring(0,6));
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.content{
+		border-radius: 20rpx 20rpx 0 0;
+		background-color: #fff;
+		position: relative;
+	}
+	.title{
+		height: 110rpx;
+		font-size: 32rpx;
+		color: #333;
+		font-weight: 700;
+	}
+	.input{
+		padding: 30rpx 0 60rpx;
+		
+		.item{
+			width: 88rpx;
+			height: 88rpx;
+			margin: 0 10rpx;
+			border: 1px solid #ddd;
+			border-radius: 4rpx;
+		}
+		.has:after{
+			content: '';
+			width: 16rpx;
+			height: 16rpx;
+			border-radius: 100rpx;
+			background-color: #333;
+		}
+	}
+	.reset-btn{
+		padding-bottom: 20rpx;
+		margin-top: -10rpx;
+		margin-bottom: 30rpx;
+		font-size: 28rpx;
+		color: #007aff;
+	}
+	.icon-guanbi{
+		position: absolute;
+		left: 10rpx;
+		top: 24rpx;
+		padding: 20rpx;
+		font-size: 28rpx;
+	}
+</style>

+ 25 - 0
yudao-vue-ui/components/uni-popup/popup.js

@@ -0,0 +1,25 @@
+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
+		}
+	},
+	mixins: [message],
+}

+ 302 - 0
yudao-vue-ui/components/uni-popup/uni-popup.vue

@@ -0,0 +1,302 @@
+<template>
+	<view v-if="showPopupState" class="uni-popup" :class="[popupstyle]" @touchmove.stop.prevent="clear">
+		<uni-transition v-if="maskShow" :mode-class="['fade']" :styles="maskClass" :maskBackgroundColor="maskBackgroundColor" :duration="duration" :show="showTrans"
+		 @click="onTap" />
+		<uni-transition :mode-class="ani" :styles="transClass" maskBackgroundColor="rgba(,0,0,0,0)" :duration="duration" :show="showTrans" @click="onTap">
+			<view class="uni-popup__wrapper-box" @click.stop="clear">
+				<slot />
+			</view>
+		</uni-transition>
+	</view>
+</template>
+
+<script>
+	import uniTransition from '../uni-transition/uni-transition.vue'
+	/**
+	 * 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: {
+			uniTransition
+		},
+		props: {
+			// 开启动画
+			animation: {
+				type: Boolean,
+				default: true
+			},
+			// 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
+			// message: 消息提示 ; dialog : 对话框
+			type: {
+				type: String,
+				default: 'center'
+			},
+			// maskClick
+			maskClick: {
+				type: Boolean,
+				default: true
+			},
+			maskBackgroundColor: {
+				type: String,
+				default: 'rgba(0, 0, 0, .5)'
+			}
+		},
+		provide() {
+			return {
+				popup: this
+			}
+		},
+		watch: {
+			/**
+			 * 监听type类型
+			 */
+			type: {
+				handler: function(newVal) {
+					this[this.config[newVal]]()
+				},
+				immediate: true
+			},
+			/**
+			 * 监听遮罩是否可点击
+			 * @param {Object} val
+			 */
+			maskClick(val) {
+				this.mkclick = val
+			}
+		},
+		data() {
+			return {
+				duration: 300,
+				ani: [],
+				showPopupState: false,
+				showTrans: false,
+				maskClass: {
+					'position': 'fixed',
+					'bottom': 0,
+					'top': 0,
+					'left': 0,
+					'right': 0
+				},
+				transClass: {
+					'position': 'fixed',
+					'left': 0,
+					'right': 0,
+				},
+				maskShow: true,
+				mkclick: true,
+				popupstyle: 'top',
+				config: {
+					top:'top',
+					// 底部弹出
+					bottom:'bottom',
+					// 居中弹出
+					center:'center'
+				}
+			}
+		},
+		created() {
+			this.mkclick = this.maskClick
+			if (this.animation) {
+				this.duration = 300
+			} else {
+				this.duration = 0
+			}
+		},
+		methods: {
+			clear(e) {
+				// TODO nvue 取消冒泡
+				e.stopPropagation()
+			},
+			open() {
+				this.showPopupState = 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.showPopupState = false
+					}, 300)
+				})
+			},
+			onTap() {
+				if (!this.mkclick) return
+				this.close()
+			},
+			/**
+			 * 顶部弹出样式处理
+			 */
+			top() {
+				this.popupstyle = '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 */
+	}
+
+	.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 */
+	}
+
+	.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>

+ 245 - 0
yudao-vue-ui/components/uni-swipe-action-item/bindingx.js

@@ -0,0 +1,245 @@
+const BindingX = uni.requireNativePlugin('bindingx');
+const dom = uni.requireNativePlugin('dom');
+const animation = uni.requireNativePlugin('animation');
+
+export default {
+	data() {
+		return {
+			right: 0,
+			button: [],
+			preventGesture: false
+		}
+	},
+
+	watch: {
+		show(newVal) {
+			if (!this.position || JSON.stringify(this.position) === '{}') return;
+			if (this.autoClose) return
+			if (this.isInAnimation) return
+			if (newVal) {
+				this.open()
+			} else {
+				this.close()
+			}
+		},
+	},
+	created() {
+		if (this.swipeaction.children !== undefined) {
+			this.swipeaction.children.push(this)
+		}
+	},
+	mounted() {
+		this.boxSelector = this.getEl(this.$refs['selector-box-hock']);
+		this.selector = this.getEl(this.$refs['selector-content-hock']);
+		this.buttonSelector = this.getEl(this.$refs['selector-button-hock']);
+		this.position = {}
+		this.x = 0
+		setTimeout(() => {
+			this.getSelectorQuery()
+		}, 200)
+	},
+	beforeDestroy() {
+		if (this.timing) {
+			BindingX.unbind({
+				token: this.timing.token,
+				eventType: 'timing'
+			})
+		}
+		if (this.eventpan) {
+			BindingX.unbind({
+				token: this.eventpan.token,
+				eventType: 'pan'
+			})
+		} 
+		this.swipeaction.children.forEach((item, index) => {
+			if (item === this) {
+				this.swipeaction.children.splice(index, 1)
+			}
+		})
+	},
+	methods: {
+		onClick(index, item) {
+			this.$emit('click', {
+				content: item,
+				index
+			})
+		},
+		touchstart(e) {
+			if (this.isInAnimation) return
+			if (this.stop) return
+			this.stop = true
+			if (this.autoClose) {
+				this.swipeaction.closeOther(this)
+			}
+			let endWidth = this.right
+			let boxStep = `(x+${this.x})`
+			let pageX = `${boxStep}> ${-endWidth} && ${boxStep} < 0?${boxStep}:(x+${this.x} < 0? ${-endWidth}:0)`
+
+			let props = [{
+				element: this.selector,
+				property: 'transform.translateX',
+				expression: pageX
+			}]
+
+			let left = 0
+			for (let i = 0; i < this.options.length; i++) {
+				let buttonSelectors = this.getEl(this.$refs['button-hock'][i]);
+				if (this.button.length === 0 || !this.button[i] || !this.button[i].width) return
+				let moveMix = endWidth - left
+				left += this.button[i].width
+				let step = `(${this.x}+x)/${endWidth}`
+				let moveX = `(${step}) * ${moveMix}`
+				let pageButtonX = `${moveX}&& (x+${this.x} > ${-endWidth})?${moveX}:${-moveMix}`
+				props.push({
+					element: buttonSelectors,
+					property: 'transform.translateX',
+					expression: pageButtonX
+				})
+			}
+
+			this.eventpan = this._bind(this.boxSelector, props, 'pan', (e) => {
+				if (e.state === 'end') {
+					this.x = e.deltaX + this.x;
+					if (this.x < -endWidth) {
+						this.x = -endWidth
+					}
+					if (this.x > 0) {
+						this.x = 0
+					}
+					this.stop = false
+					this.bindTiming();
+				}
+			})
+		},
+		touchend(e) {
+			this.$nextTick(() => {
+				if (this.isopen && !this.isDrag && !this.isInAnimation) {
+					this.close()
+				}
+			})
+		},
+		bindTiming() {
+			if (this.isopen) {
+				this.move(this.x, -this.right)
+			} else {
+				this.move(this.x, -40)
+			}
+		},
+		move(left, value) {
+			if (left >= value) {
+				this.close()
+			} else {
+				this.open()
+			}
+		},
+		/**
+		 * 开启swipe
+		 */
+		open() {
+			this.animation(true)
+		},
+		/**
+		 * 关闭swipe
+		 */
+		close() {
+			this.animation(false)
+		},
+		/**
+		 * 开启关闭动画
+		 * @param {Object} type
+		 */
+		animation(type) {
+			this.isDrag = true
+			let endWidth = this.right
+			let time = 200
+			this.isInAnimation = true;
+
+			let exit = `t>${time}`;
+			let translate_x_expression = `easeOutExpo(t,${this.x},${type?(-endWidth-this.x):(-this.x)},${time})`
+			let props = [{
+				element: this.selector,
+				property: 'transform.translateX',
+				expression: translate_x_expression
+			}]
+
+			let left = 0
+			for (let i = 0; i < this.options.length; i++) {
+				let buttonSelectors = this.getEl(this.$refs['button-hock'][i]);
+				if (this.button.length === 0 || !this.button[i] || !this.button[i].width) return
+				let moveMix = endWidth - left
+				left += this.button[i].width
+				let step = `${this.x}/${endWidth}`
+				let moveX = `(${step}) * ${moveMix}`
+				let pageButtonX = `easeOutExpo(t,${moveX},${type ? -moveMix + '-' + moveX: 0 + '-' + moveX},${time})`
+				props.push({
+					element: buttonSelectors,
+					property: 'transform.translateX',
+					expression: pageButtonX
+				})
+			}
+
+			this.timing = BindingX.bind({
+				eventType: 'timing',
+				exitExpression: exit,
+				props: props
+			}, (e) => {
+				if (e.state === 'end' || e.state === 'exit') {
+					this.x = type ? -endWidth : 0
+					this.isInAnimation = false;
+
+					this.isopen = this.isopen || false
+					if (this.isopen !== type) {
+						this.$emit('change', type)
+					}
+					this.isopen = type
+					this.isDrag = false
+				}
+			});
+		},
+		/**
+		 * 绑定  BindingX
+		 * @param {Object} anchor
+		 * @param {Object} props
+		 * @param {Object} fn
+		 */
+		_bind(anchor, props, eventType, fn) {
+			return BindingX.bind({
+				anchor,
+				eventType,
+				props
+			}, (e) => {
+				typeof(fn) === 'function' && fn(e)
+			});
+		},
+		/**
+		 * 获取ref
+		 * @param {Object} el
+		 */
+		getEl(el) {
+			return el.ref
+		},
+		/**
+		 * 获取节点信息
+		 */
+		getSelectorQuery() {
+			dom.getComponentRect(this.$refs['selector-content-hock'], (data) => {
+				if (this.position.content) return
+				this.position.content = data.size
+			})
+			for (let i = 0; i < this.options.length; i++) {
+				dom.getComponentRect(this.$refs['button-hock'][i], (data) => {
+					if (!this.button) {
+						this.button = []
+					}
+					if (this.options.length === this.button.length) return
+					this.button.push(data.size)
+					this.right += data.size.width
+					if (this.autoClose) return
+					if (this.show) {
+						this.open()
+					}
+				})
+			}
+		}
+	}
+}

+ 204 - 0
yudao-vue-ui/components/uni-swipe-action-item/index.wxs

@@ -0,0 +1,204 @@
+/**
+ * 监听页面内值的变化,主要用于动态开关swipe-action
+ * @param {Object} newValue
+ * @param {Object} oldValue
+ * @param {Object} ownerInstance
+ * @param {Object} instance
+ */
+function sizeReady(newValue, oldValue, ownerInstance, instance) {
+	var state = instance.getState()
+	state.position = JSON.parse(newValue)
+	if (!state.position || state.position.length === 0) return
+	var show = state.position[0].show
+	state.left = state.left || state.position[0].left;
+	// 通过用户变量,开启或关闭
+	if (show) {
+		openState(true, instance, ownerInstance)
+	} else {
+		openState(false, instance, ownerInstance)
+	}
+}
+
+/**
+ * 开始触摸操作
+ * @param {Object} e
+ * @param {Object} ins
+ */
+function touchstart(e, ins) {
+	var instance = e.instance;
+	var state = instance.getState();
+	var pageX = e.touches[0].pageX;
+	// 开始触摸时移除动画类
+	instance.removeClass('ani');
+	var owner = ins.selectAllComponents('.button-hock')
+	for (var i = 0; i < owner.length; i++) {
+		owner[i].removeClass('ani');
+	}
+	// state.position = JSON.parse(instance.getDataset().position);
+	state.left = state.left || state.position[0].left;
+	// 获取最终按钮组的宽度
+	state.width = pageX - state.left;
+	ins.callMethod('closeSwipe')
+}
+
+/**
+ * 开始滑动操作
+ * @param {Object} e
+ * @param {Object} ownerInstance
+ */
+function touchmove(e, ownerInstance) {
+	var instance = e.instance;
+	var disabled = instance.getDataset().disabled
+	var state = instance.getState()
+	// fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复
+	disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false;
+
+	if (disabled) return
+	var pageX = e.touches[0].pageX;
+	move(pageX - state.width, instance, ownerInstance)
+}
+
+/**
+ * 结束触摸操作
+ * @param {Object} e
+ * @param {Object} ownerInstance
+ */
+function touchend(e, ownerInstance) {
+	var instance = e.instance;
+	var disabled = instance.getDataset().disabled
+	var state = instance.getState()
+
+	// fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复
+	disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false;
+
+	if (disabled) return
+	// 滑动过程中触摸结束,通过阙值判断是开启还是关闭
+	// fixed by mehaotian 定时器解决点击按钮,touchend 触发比 click 事件时机早的问题 ,主要是 ios13
+	moveDirection(state.left, -40, instance, ownerInstance)
+}
+
+/**
+ * 设置移动距离
+ * @param {Object} value
+ * @param {Object} instance
+ * @param {Object} ownerInstance
+ */
+function move(value, instance, ownerInstance) {
+	var state = instance.getState()
+	// 获取可滑动范围
+	var x = Math.max(-state.position[1].width, Math.min((value), 0));
+	state.left = x;
+	instance.setStyle({
+		transform: 'translateX(' + x + 'px)',
+		'-webkit-transform': 'translateX(' + x + 'px)'
+	})
+	// 折叠按钮动画
+	buttonFold(x, instance, ownerInstance)
+}
+
+/**
+ * 移动方向判断
+ * @param {Object} left
+ * @param {Object} value
+ * @param {Object} ownerInstance
+ * @param {Object} ins
+ */
+function moveDirection(left, value, ins, ownerInstance) {
+	var state = ins.getState()
+	var position = state.position
+	var isopen = state.isopen
+	if (!position[1].width) {
+		openState(false, ins, ownerInstance)
+		return
+	}
+	// 如果已经是打开状态,进行判断是否关闭,还是保留打开状态
+	if (isopen) {
+		if (-left <= position[1].width) {
+			openState(false, ins, ownerInstance)
+		} else {
+			openState(true, ins, ownerInstance)
+		}
+		return
+	}
+	// 如果是关闭状态,进行判断是否打开,还是保留关闭状态
+	if (left <= value) {
+		openState(true, ins, ownerInstance)
+	} else {
+		openState(false, ins, ownerInstance)
+	}
+}
+
+/**
+ * 设置按钮移动距离
+ * @param {Object} value
+ * @param {Object} instance
+ * @param {Object} ownerInstance
+ */
+function buttonFold(value, instance, ownerInstance) {
+	var ins = ownerInstance.selectAllComponents('.button-hock');
+	var state = instance.getState();
+	var position = state.position;
+	var arr = [];
+	var w = 0;
+	for (var i = 0; i < ins.length; i++) {
+		if (!ins[i].getDataset().button) return
+		var btnData = JSON.parse(ins[i].getDataset().button)
+
+		// fix by mehaotian TODO 在 app-vue 中,字符串转对象,需要转两次,这里先这么兼容
+		if (typeof(btnData) === 'string') {
+			btnData = JSON.parse(btnData)
+		}
+
+		var button = btnData[i] && btnData[i].width || 0
+		w += button
+		arr.push(-w)
+		// 动态计算按钮组每个按钮的折叠动画移动距离
+		var distance = arr[i - 1] + value * (arr[i - 1] / position[1].width)
+		if (i != 0) {
+			ins[i].setStyle({
+				transform: 'translateX(' + distance + 'px)',
+			})
+		}
+	}
+}
+
+/**
+ * 开启状态
+ * @param {Boolean} type
+ * @param {Object} ins
+ * @param {Object} ownerInstance
+ */
+function openState(type, ins, ownerInstance) {
+	var state = ins.getState()
+	var position = state.position
+	if (state.isopen === undefined) {
+		state.isopen = false
+	}
+	// 只有状态有改变才会通知页面改变状态
+	if (state.isopen !== type) {
+		// 通知页面,已经打开
+		ownerInstance.callMethod('change', {
+			open: type
+		})
+	}
+	// 设置打开和移动状态
+	state.isopen = type
+
+
+	// 添加动画类
+	ins.addClass('ani');
+	var owner = ownerInstance.selectAllComponents('.button-hock')
+	for (var i = 0; i < owner.length; i++) {
+		owner[i].addClass('ani');
+	}
+	// 设置最终移动位置
+	move(type ? -position[1].width : 0, ins, ownerInstance)
+
+}
+
+module.exports = {
+	sizeReady: sizeReady,
+	touchstart: touchstart,
+	touchmove: touchmove,
+	touchend: touchend
+}

+ 160 - 0
yudao-vue-ui/components/uni-swipe-action-item/mpalipay.js

@@ -0,0 +1,160 @@
+export default {
+	data() {
+		return {
+			isshow: false,
+			viewWidth: 0,
+			buttonWidth: 0,
+			disabledView: false,
+			x: 0,
+			transition: false
+		}
+	},
+	watch: {
+		show(newVal) {
+			if (this.autoClose) return
+			if (newVal) {
+				this.open()
+			} else {
+				this.close()
+			}
+		},
+	},
+	created() {
+		if (this.swipeaction.children !== undefined) {
+			this.swipeaction.children.push(this)
+		}
+	},
+	beforeDestroy() {
+		this.swipeaction.children.forEach((item, index) => {
+			if (item === this) {
+				this.swipeaction.children.splice(index, 1)
+			}
+		})
+	},
+	mounted() {
+		this.isopen = false
+		this.transition = true
+		setTimeout(() => {
+			this.getQuerySelect()
+		}, 50)
+
+	},
+	methods: {
+		onClick(index, item) {
+			this.$emit('click', {
+				content: item,
+				index
+			})
+		},
+		touchstart(e) {
+			let {
+				pageX,
+				pageY
+			} = e.changedTouches[0]
+			this.transition = false
+			this.startX = pageX
+			if (this.autoClose) {
+				this.swipeaction.closeOther(this)
+			}
+		},
+		touchmove(e) {
+			let {
+				pageX,
+			} = e.changedTouches[0]
+			this.slide = this.getSlide(pageX)
+			if (this.slide === 0) {
+				this.disabledView = false
+			}
+
+		},
+		touchend(e) {
+			this.stop = false
+			this.transition = true
+			if (this.isopen) {
+				if (this.moveX === -this.buttonWidth) {
+					this.close()
+					return
+				}
+				this.move()
+			} else {
+				if (this.moveX === 0) {
+					this.close()
+					return
+				}
+				this.move()
+			}
+		},
+		open() {
+			this.x = this.moveX
+			this.$nextTick(() => {
+				this.x = -this.buttonWidth
+				this.moveX = this.x
+				
+				if(!this.isopen){
+					this.isopen = true
+					this.$emit('change', true)
+				}
+			})
+		},
+		close() {
+			this.x = this.moveX
+			this.$nextTick(() => {
+				this.x = 0
+				this.moveX = this.x
+				if(this.isopen){
+					this.isopen = false
+					this.$emit('change', false)
+				}
+			})
+		},
+		move() {
+			if (this.slide === 0) {
+				this.open()
+			} else {
+				this.close()
+			}
+		},
+		onChange(e) {
+			let x = e.detail.x
+			this.moveX = x
+			if (x >= this.buttonWidth) {
+				this.disabledView = true
+				this.$nextTick(() => {
+					this.x = this.buttonWidth
+				})
+			}
+		},
+		getSlide(x) {
+			if (x >= this.startX) {
+				this.startX = x
+				return 1
+			} else {
+				this.startX = x
+				return 0
+			}
+
+		},
+		getQuerySelect() {
+			const query = uni.createSelectorQuery().in(this);
+			query.selectAll('.viewWidth-hook').boundingClientRect(data => {
+
+				this.viewWidth = data[0].width
+				this.buttonWidth = data[1].width
+				this.transition = false
+				this.$nextTick(() => {
+					this.transition = true
+				})
+
+				if (!this.buttonWidth) {
+					this.disabledView = true
+				}
+
+				if (this.autoClose) return
+				if (this.show) {
+					this.open()
+				}
+			}).exec();
+
+		}
+	}
+}

+ 158 - 0
yudao-vue-ui/components/uni-swipe-action-item/mpother.js

@@ -0,0 +1,158 @@
+// #ifdef APP-NVUE
+const dom = weex.requireModule('dom');
+// #endif
+export default {
+	data() {
+		return {
+			uniShow: false,
+			left: 0
+		}
+	},
+	computed: {
+		moveLeft() {
+			return `translateX(${this.left}px)`
+		}
+	},
+	watch: {
+		show(newVal) {
+			if (!this.position || JSON.stringify(this.position) === '{}') return;
+			if (this.autoClose) return
+			if (newVal) {
+				this.$emit('change', true)
+				this.open()
+			} else {
+				this.$emit('change', false)
+				this.close()
+			}
+		}
+	},
+	mounted() {
+		this.position = {}
+		if (this.swipeaction.children !== undefined) {
+			this.swipeaction.children.push(this)
+		}
+		setTimeout(() => {
+			this.getSelectorQuery()
+		}, 100)
+	},
+	beforeDestoy() {
+		this.swipeaction.children.forEach((item, index) => {
+			if (item === this) {
+				this.swipeaction.children.splice(index, 1)
+			}
+		})
+	},
+	methods: {
+		onClick(index, item) {
+			this.$emit('click', {
+				content: item,
+				index
+			})
+			this.close()
+		},
+		touchstart(e) {
+			const {
+				pageX
+			} = e.touches[0]
+			if (this.disabled) return
+			const left = this.position.content.left
+			if (this.autoClose) {
+				this.swipeaction.closeOther(this)
+			}
+			this.width = pageX - left
+			if (this.isopen) return
+			if (this.uniShow) {
+				this.uniShow = false
+				this.isopen = true
+				this.openleft = this.left + this.position.button.width
+			}
+		},
+		touchmove(e, index) {
+			if (this.disabled) return
+			const {
+				pageX
+			} = e.touches[0]
+			this.setPosition(pageX)
+		},
+		touchend() {
+			if (this.disabled) return
+			if (this.isopen) {
+				this.move(this.openleft, 0)
+				return
+			}
+			this.move(this.left, -40)
+		},
+		setPosition(x, y) {
+			if (!this.position.button.width) {
+				return
+			}
+			// this.left = x - this.width
+			this.setValue(x - this.width)
+		},
+		setValue(value) {
+			// 设置最大最小值
+			this.left = Math.max(-this.position.button.width, Math.min(parseInt(value), 0))
+			this.position.content.left = this.left
+			if (this.isopen) {
+				this.openleft = this.left + this.position.button.width
+			}
+		},
+		move(left, value) {
+			if (left >= value) {
+				this.$emit('change', false)
+				this.close()
+			} else {
+				this.$emit('change', true)
+				this.open()
+			}
+		},
+		open() {
+			this.uniShow = true
+			this.left = -this.position.button.width
+			this.setValue(-this.position.button.width)
+		},
+		close() {
+			this.uniShow = true
+			this.setValue(0)
+			setTimeout(() => {
+				this.uniShow = false
+				this.isopen = false
+			}, 300)
+		},
+		getSelectorQuery() {
+			// #ifndef APP-NVUE
+			const views = uni.createSelectorQuery()
+				.in(this)
+			views
+				.selectAll('.selector-query-hock')
+				.boundingClientRect(data => {
+					this.position.content = data[1]
+					this.position.button = data[0]
+					if (this.autoClose) return
+					if (this.show) {
+						this.open()
+					} else {
+						this.close()
+					}
+				})
+				.exec()
+			// #endif
+			// #ifdef APP-NVUE
+			dom.getComponentRect(this.$refs['selector-content-hock'], (data) => {
+				if (this.position.content) return
+				this.position.content = data.size
+			})
+			dom.getComponentRect(this.$refs['selector-button-hock'], (data) => {
+				if (this.position.button) return
+				this.position.button = data.size
+				if (this.autoClose) return
+				if (this.show) {
+					this.open()
+				} else {
+					this.close()
+				}
+			})
+			// #endif
+		}
+	}
+}

+ 97 - 0
yudao-vue-ui/components/uni-swipe-action-item/mpwxs.js

@@ -0,0 +1,97 @@
+export default {
+	data() {
+		return {
+			position: [],
+			button: []
+		}
+	},
+	computed: {
+		pos() {
+			return JSON.stringify(this.position)
+		},
+		btn() {
+			return JSON.stringify(this.button)
+		}
+	},
+	watch: {
+		show(newVal) {
+			if (this.autoClose) return
+			let valueObj = this.position[0]
+			if (!valueObj) {
+				this.init()
+				return
+			}
+			valueObj.show = newVal
+			this.$set(this.position, 0, valueObj)
+		}
+	},
+	created() {
+		if (this.swipeaction.children !== undefined) {
+			this.swipeaction.children.push(this)
+		}
+	},
+	mounted() {
+		this.init()
+
+	},
+	beforeDestroy() {
+		this.swipeaction.children.forEach((item, index) => {
+			if (item === this) {
+				this.swipeaction.children.splice(index, 1)
+			}
+		})
+	},
+	methods: {
+		init() {
+			
+			setTimeout(() => {
+				this.getSize()
+				this.getButtonSize()
+			}, 50)
+		},
+		closeSwipe(e) {
+			if (!this.autoClose) return
+			this.swipeaction.closeOther(this)
+		},
+		
+		change(e) {
+			this.$emit('change', e.open)
+			let valueObj = this.position[0]
+			if (valueObj.show !== e.open) {
+				valueObj.show = e.open
+				this.$set(this.position, 0, valueObj)
+			}
+		},
+		onClick(index, item) {
+			this.$emit('click', {
+				content: item,
+				index
+			})
+		},
+		appTouchStart(){},
+		appTouchEnd(){},
+		getSize() {
+			const views = uni.createSelectorQuery().in(this)
+			views
+				.selectAll('.selector-query-hock')
+				.boundingClientRect(data => {
+					if (this.autoClose) {
+						data[0].show = false
+					} else {
+						data[0].show = this.show
+					}
+					this.position = data
+				})
+				.exec()
+		},
+		getButtonSize() {
+			const views = uni.createSelectorQuery().in(this)
+			views
+				.selectAll('.button-hock')
+				.boundingClientRect(data => {
+					this.button = data
+				})
+				.exec()
+		}
+	}
+}

+ 270 - 0
yudao-vue-ui/components/uni-swipe-action-item/uni-swipe-action-item.vue

@@ -0,0 +1,270 @@
+<template>
+	<view class="uni-swipe">
+		<!-- 在微信小程序 app vue端 h5 使用wxs 实现-->
+		<!-- #ifdef APP-VUE || MP-WEIXIN || H5 -->
+		<view class="uni-swipe_content">
+			<view :data-disabled="disabled" :data-position="pos" :change:prop="swipe.sizeReady" :prop="pos" class="uni-swipe_move-box selector-query-hock move-hock"
+			 @touchstart="swipe.touchstart" @touchmove="swipe.touchmove" @touchend="swipe.touchend" @change="change">
+				<view class="uni-swipe_box">
+					<slot />
+				</view>
+				<view ref="selector-button-hock" class="uni-swipe_button-group selector-query-hock move-hock">
+					<!-- 使用 touchend 解决 ios 13 不触发按钮事件的问题-->
+					<view v-for="(item,index) in options" :data-button="btn" :key="index" :style="{
+		          backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',
+		          fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px'
+		        }"
+					 class="uni-swipe_button button-hock" @touchend="onClick(index,item)"><text class="uni-swipe_button-text" :style="{color: item.style && item.style.color ? item.style.color : '#FFFFFF',}">{{ item.text }}</text></view>
+				</view>
+			</view>
+		</view>
+		<!-- #endif -->
+
+		<!--  app nvue端 使用 bindingx -->
+		<!-- #ifdef APP-NVUE -->
+		<view ref="selector-box-hock" class="uni-swipe_content" @horizontalpan="touchstart" @touchend="touchend">
+			<view ref="selector-button-hock" class="uni-swipe_button-group selector-query-hock move-hock" :style="{width:right+'px'}">
+				<view ref="button-hock" v-for="(item,index) in options" :key="index" :style="{
+		  backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',left: right+'px'}"
+				 class="uni-swipe_button " @click.stop="onClick(index,item)"><text class="uni-swipe_button-text" :style="{color: item.style && item.style.color ? item.style.color : '#FFFFFF',fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px'}">{{ item.text }}</text></view>
+			</view>
+			<view ref='selector-content-hock' class="uni-swipe_move-box selector-query-hock">
+				<view class="uni-swipe_box">
+					<slot />
+				</view>
+			</view>
+		</view>
+		<!-- #endif -->
+
+		<!-- 在非 app 端、非微信小程序、支付宝小程序、h5端使用 js -->
+		<!-- #ifndef APP-PLUS || MP-WEIXIN || MP-ALIPAY || H5 -->
+		<view class="uni-swipe_content">
+			<view ref="selector-button-hock" class="uni-swipe_button-group selector-query-hock move-hock">
+				<view v-for="(item,index) in options" :data-button="btn" :key="index" :style="{
+		    backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',
+		    fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px'
+		  }"
+				 class="uni-swipe_button button-hock" @click.stop="onClick(index,item)"><text class="uni-swipe_button-text" :style="{color: item.style && item.style.color ? item.style.color : '#FFFFFF',}">{{ item.text }}</text></view>
+			</view>
+			<view ref='selector-content-hock' class="selector-query-hock" @touchstart="touchstart" @touchmove="touchmove"
+			 @touchend="touchend" :class="{'ani':uniShow}" :style="{transform:moveLeft}">
+				<view class="uni-swipe_move-box"  >
+					<view class="uni-swipe_box">
+						<slot />
+					</view>
+				</view>
+			</view>
+		</view>
+		<!-- #endif -->
+		<!-- #ifdef MP-ALIPAY -->
+		<view class="uni-swipe-box" @touchstart="touchstart" @touchmove="touchmove" @touchend="touchend">
+			<view class="viewWidth-hook">
+				<movable-area v-if="viewWidth !== 0" class="movable-area" :style="{width:(viewWidth-buttonWidth)+'px'}">
+					<movable-view class="movable-view" direction="horizontal" :animation="!transition" :style="{width:viewWidth+'px'}"
+					 :class="[transition?'transition':'']" :x="x" :disabled="disabledView" @change="onChange">
+						<view class="movable-view-box">
+							<slot></slot>
+						</view>
+					</movable-view>
+				</movable-area>
+			</view>
+			<view ref="selector-button-hock" class="uni-swipe_button-group viewWidth-hook">
+				<view v-for="(item,index) in options" :data-button="btn" :key="index" :style="{
+				  backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',
+				  fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px'
+				}"
+				 class="uni-swipe_button button-hock" @click.stop="onClick(index,item)"><text class="uni-swipe_button-text" :style="{color: item.style && item.style.color ? item.style.color : '#FFFFFF',}">{{ item.text }}</text></view>
+			</view>
+		</view>
+		<!-- #endif -->
+	</view>
+</template>
+<script src="./index.wxs" module="swipe" lang="wxs"></script>
+<script>
+	// #ifdef APP-VUE|| MP-WEIXIN || H5
+	import mpwxs from './mpwxs'
+	// #endif
+
+	// #ifdef APP-NVUE
+	import bindingx from './bindingx.js'
+	// #endif
+
+	// #ifndef APP-PLUS|| MP-WEIXIN || MP-ALIPAY ||  H5
+	import mixins from './mpother'
+	// #endif
+
+	// #ifdef MP-ALIPAY
+	import mpalipay from './mpalipay'
+	// #endif
+
+	/**
+	 * SwipeActionItem 滑动操作子组件
+	 * @description 通过滑动触发选项的容器
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=181
+	 * @property {Boolean} show = [true|false] 开启关闭组件,auto-close = false 时生效
+	 * @property {Boolean} disabled = [true|false] 是否禁止滑动
+	 * @property {Boolean} autoClose = [true|false] 其他组件开启的时候,当前组件是否自动关闭
+	 * @property {Array} options 组件选项内容及样式
+	 * @event {Function} click 点击选项按钮时触发事件,e = {content,index} ,content(点击内容)、index(下标)
+	 * @event {Function} change 组件打开或关闭时触发,true:开启状态;false:关闭状态
+	 */
+
+	export default {
+		// #ifdef APP-VUE|| MP-WEIXIN||H5
+		mixins: [mpwxs],
+		// #endif
+
+		// #ifdef APP-NVUE
+		mixins: [bindingx],
+		// #endif
+
+		// #ifndef APP-PLUS|| MP-WEIXIN || MP-ALIPAY ||  H5
+		mixins: [mixins],
+		// #endif
+
+		// #ifdef MP-ALIPAY
+		mixins: [mpalipay],
+		// #endif
+
+		props: {
+			/**
+			 * 按钮内容
+			 */
+			options: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			/**
+			 * 禁用
+			 */
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			/**
+			 * 变量控制开关
+			 */
+			show: {
+				type: Boolean,
+				default: false
+			},
+			/**
+			 * 是否自动关闭
+			 */
+			autoClose: {
+				type: Boolean,
+				default: true
+			}
+		},
+		inject: ['swipeaction']
+
+
+	}
+</script>
+<style lang="scss" scoped>
+	.uni-swipe {
+		overflow: hidden;
+	}
+
+	.uni-swipe-box {
+		position: relative;
+		width: 100%;
+	}
+
+	.uni-swipe_content {
+		flex: 1;
+		position: relative;
+	}
+
+	.uni-swipe_move-box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		position: relative;
+		flex-direction: row;
+	}
+
+	.uni-swipe_box {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		flex-direction: row;
+		width: 100%;
+		flex-shrink: 0;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		flex: 1;
+		/* #endif */
+		font-size: 14px;
+		background-color: #fff;
+	}
+
+	.uni-swipe_button-group {
+		/* #ifndef APP-VUE|| MP-WEIXIN||H5 */
+		position: absolute;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		z-index: 0;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: flex;
+		flex-shrink: 0;
+		/* #endif */
+		flex-direction: row;
+	}
+
+	.uni-swipe_button {
+		/* #ifdef APP-NVUE */
+		position: absolute;
+		left: 0;
+		top: 0;
+		bottom: 0;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		display: flex;
+		/* #endif */
+		flex-direction: row;
+		justify-content: center;
+		align-items: center;
+		padding: 0 20px;
+	}
+
+	.uni-swipe_button-text {
+		/* #ifndef APP-NVUE */
+		flex-shrink: 0;
+		/* #endif */
+		font-size: 14px;
+	}
+
+	.ani {
+		transition-property: transform;
+		transition-duration: 0.3s;
+		transition-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1);
+	}
+
+	/* #ifdef MP-ALIPAY */
+	.movable-area {
+		width: 300px;
+		height: 100%;
+		height: 45px;
+	}
+
+	.movable-view {
+		position: relative;
+		width: 160%;
+		height: 45px;
+		z-index: 2;
+	}
+	.transition {
+		transition: all 0.3s;
+	}
+
+	.movable-view-box {
+		width: 100%;
+		height: 100%;
+		background-color: #fff;
+	}
+	/* #endif */
+</style>

+ 58 - 0
yudao-vue-ui/components/uni-swipe-action/uni-swipe-action.vue

@@ -0,0 +1,58 @@
+<template>
+	<view>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	/**
+	 * SwipeAction 滑动操作
+	 * @description 通过滑动触发选项的容器
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=181
+	 */
+	export default {
+		data() {
+			return {};
+		},
+		provide() {
+			return {
+				swipeaction: this
+			}
+		},
+		created() {
+			this.children = []
+		},
+		methods: {
+			closeOther(vm) {
+				let children = this.children
+				children.forEach((item, index) => {
+					if (vm === item) return
+					// 支付宝执行以下操作
+					// #ifdef MP-ALIPAY
+					if (item.isopen) {
+						item.close()
+					}
+					// #endif
+
+					// app vue 端、h5 、微信、支付宝  执行以下操作
+					// #ifdef APP-VUE || H5 || MP-WEIXIN
+					let position = item.position[0]
+					let show = position.show
+					if (show) {
+						position.show = false
+					}
+					// #endif
+
+					// nvue 执行以下操作
+					// #ifdef APP-NVUE || MP-BAIDU || MP-QQ || MP-TOUTIAO
+					item.close()
+					// #endif
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 290 - 0
yudao-vue-ui/components/uni-transition/uni-transition.vue

@@ -0,0 +1,290 @@
+<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 {}
+				}
+			},
+			maskBackgroundColor: {
+				type: String,
+				default: 'rgba(0, 0, 0, 0.4)'
+			}
+		},
+		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,
+					backgroundColor: this.maskBackgroundColor,
+					'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;
+	}
+
+	.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>

+ 1250 - 0
yudao-vue-ui/components/version-update/base-cloud-mobile.scss

@@ -0,0 +1,1250 @@
+div,a,img,span,page,view,navigator,image,text,input,textarea,button,form{
+	box-sizing: border-box;
+}
+
+a{
+	text-decoration: none;
+	color: $main;
+}
+
+form{
+	display: block;
+	width: 100%;
+}
+
+image{will-change: transform}
+
+input,textarea,form{
+	width: 100%;
+	height: auto;
+	min-height: 10px;
+	display: block;
+	font-size: inherit;
+}
+
+button{
+	color: inherit;
+	line-height: inherit;
+	margin: 0;
+	background-color: transparent;
+	padding: 0;
+	-webkit-border-radius: 0;
+	-moz-border-radius: 0;
+	border-radius: 0;
+	&:after{
+		display: none;
+	}
+}
+
+switch .uni-switch-input{
+	margin-right: 0;
+}
+.wx-switch-input,.uni-switch-input{width:42px !important;height:22px !important;}
+	.wx-switch-input::before,.uni-switch-input::before{width:40px !important;height: 20px !important;}
+	.wx-switch-input::after,.uni-switch-input::after{width: 20px !important;height: 20px !important;}
+
+
+/**背景颜色**/
+.bg{
+	background-color: $main;
+	color: $mainInverse;
+}
+.gradualBg{
+	background-image: $mainGradual;
+	color: $mainGradualInverse ;
+}
+.grayBg{
+	background-color: #f7f7f7;
+	color: #30302f;
+}
+.whiteBg{
+	background-color: #fff;
+	color: #000;
+}
+.blackBg{
+	background-color: #000;
+	color: #fff;
+}
+.orangeBg{
+	background-color: $orange;
+	color: #fff;
+}
+.redBg{
+	background-color: $red;
+	color: #fff;
+}
+.yellowBg{
+	background-color: $yellow;
+	color: #000;
+}
+.greenBg{
+	background-color: $green;
+	color: #fff;
+}
+.brownBg{
+	background-color: $brown;
+	color: #fff;
+}
+.blueBg{
+	background-color: $blue;
+	color: #fff;
+}
+.purpleBg{
+	background-color: $purple;
+	color: #fff;
+}
+
+/* 文字颜色 */
+.main{
+	color: $main;
+}
+.green{
+	color: $green;
+}
+.red{
+	color: $red;
+}
+.yellow{
+	color: $yellow;
+}
+.black{
+	color: $black;
+}
+.white{
+	color: $white;
+}
+.gray{
+	color: $gray;
+}
+.grey{
+	color: $grey;
+}
+.orange{
+	color: $orange;
+}
+.brown{
+	color: $brown;
+}
+.blue{
+	color: $blue;
+}
+.purple{
+	color: $purple;
+}
+
+.hoverMain{
+	&:hover{
+		color: $main;
+	}
+}
+
+.hoverGreen{
+	&:hover{
+		color: $green;
+	}
+}
+
+.hoverRed{
+	&:hover{
+		color: $red;
+	}
+}
+
+.hoverBlue{
+	&:hover{
+		color: $blue;
+	}
+}
+
+.hoverGray{
+	&:hover{
+		color: $gray;
+	}
+}
+
+.hoverWhite{
+	&:hover{
+		color: $white;
+	}
+}
+
+.hoverBlack{
+	&:hover{
+		color: $black;
+	}
+}
+
+.hoverOrange{
+	&:hover{
+		color: $orange;
+	}
+}
+
+.hoverYellow{
+	&:hover{
+		color: $yellow;
+	}
+}
+
+.hoverBrown{
+	&:hover{
+		color: $brown;
+	}
+}
+
+.hoverPurple{
+	&:hover{
+		color: $purple;
+	}
+}
+
+/* 宽度 高度 */
+$w:0;
+@while $w <= 500 {
+	@if $w <= 100 {
+		.w#{$w}p{
+			width: $w*1%;
+		}
+		.h#{$w}p{
+			height: $w*1%;
+		}
+		@if $w == 100 {
+			.100p{
+				width: 100%;
+				height: 100%;
+			}
+			.ww{
+				width: 100vw;
+			}
+			.hh{
+				height: 100vh;
+			}
+		}
+	}
+	.w#{$w}{
+		width: $w*2upx;
+	}
+	.h#{$w}{
+		height: $w*2upx;
+	}
+	@if $w < 10 {
+		$w : $w + 1 ;
+	} @else{
+		$w : $w + 5 ;
+	}
+}
+
+
+/* 字号 */
+@for $fz from 12 through 100 {
+	.fz#{$fz}{
+		font-size: $fz*2upx !important;
+	}
+}
+
+/* 边距 - 覆盖顺序是小的尺寸覆盖大的尺寸 少的方向覆盖多的方向 */
+$s : 0 ;
+@while $s <= 500 {
+	.pd#{$s}{
+		padding: $s*2upx!important;
+	}
+	.m#{$s}{
+		margin: $s*2upx!important;
+	}
+	@if $s == 15 {
+		.pd{
+			padding: 30upx!important;
+		}
+		.m{
+			margin: 30upx!important;
+		}
+	}
+	@if $s < 10 {
+		$s : $s + 1 ;
+	} @else if($s < 100){
+		$s : $s + 5 ;
+	}   @else if($s < 300){
+		$s : $s + 10 ;
+	} @else{
+		$s : $s + 50 ;
+	}
+}
+
+$s : 0 ;
+@while $s <= 500 {
+	.ptb#{$s}{
+		padding-top: $s*2upx!important;
+		padding-bottom: $s*2upx!important;
+	}
+	.plr#{$s}{
+		padding-left: $s*2upx!important;
+		padding-right: $s*2upx!important;
+	}
+	.mtb#{$s}{
+		margin-top: $s*2upx!important;
+		margin-bottom: $s*2upx!important;
+	}
+	.mlr#{$s}{
+		margin-left: $s*2upx!important;
+		margin-right: $s*2upx!important;
+	}
+	@if $s == 15 {
+		.ptb{
+			padding-top: 30upx!important;
+			padding-bottom: 30upx!important;
+		}
+		.plr{
+			padding-left: 30upx!important;
+			padding-right: 30upx!important;
+		}
+		
+		.mlr{
+			margin-left: 30upx!important;
+			margin-right: 30upx!important;
+		}
+		.mtb{
+			margin-top: 30upx!important;
+			margin-bottom: 30upx!important;
+		}
+	}
+	@if $s < 10 {
+		$s : $s + 1 ;
+	} @else if($s < 100){
+		$s : $s + 5 ;
+	}   @else if($s < 300){
+		$s : $s + 10 ;
+	} @else{
+		$s : $s + 50 ;
+	}
+}
+
+$s : 0 ;
+@while $s <= 500 {
+	.pl#{$s}{
+		padding-left: $s*2upx!important;
+	}
+	.pr#{$s}{
+		padding-right: $s*2upx!important;
+	}
+	.pt#{$s}{
+		padding-top: $s*2upx!important;
+	}
+	.pb#{$s}{
+		padding-bottom: $s*2upx!important;
+	}
+	.ml#{$s}{
+		margin-left: $s*2upx!important;
+	}
+	.mr#{$s}{
+		margin-right: $s*2upx!important;
+	}
+	.mt#{$s}{
+		margin-top: $s*2upx!important;
+	}
+	.mb#{$s}{
+		margin-bottom: $s*2upx!important;
+	}
+	@if $s == 15 {
+		.pt{
+			padding-top: 30upx!important;
+		}
+		.pb{
+			padding-bottom: 30upx!important;
+		}
+		.pl{
+			padding-left: 30upx!important;
+		}
+		.pr{
+			padding-right: 30upx!important;
+		}
+		.mt{
+			margin-top: 30upx!important;
+		}
+		.mr{
+			margin-right: 30upx!important;
+		}
+		.ml{
+			margin-left: 30upx!important;
+		}
+		.mb{
+			margin-bottom: 30upx!important;
+		}
+	}
+	@if $s < 10 {
+		$s : $s + 1 ;
+	} @else if($s < 100){
+		$s : $s + 5 ;
+	} @else if($s < 300){
+		$s : $s + 10 ;
+	} @else{
+		$s : $s + 50 ;
+	}
+}
+
+
+
+/* 文字溢出隐藏 */
+.clip{
+	width: 100%;
+	display: -webkit-box;
+	overflow: hidden;
+	-webkit-line-clamp: 1;
+	-webkit-box-orient: vertical;
+	@for $i from 2 through 10{
+		&.c#{$i}{
+			-webkit-line-clamp: $i;
+		}
+	}
+}
+
+.cut{
+	display: block;
+	width: 100%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+/* 价格 */
+.price{
+	font-size: inherit ;
+	&:before{
+		content: "¥";
+		font-size: 70%;
+		color: inherit;
+		font-weight: normal;
+		font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif ;
+	}
+	.point{
+		display: inline-block;
+		font-size: 70%;
+		font-weight: inherit;
+		letter-spacing: 1px;
+		color: inherit;
+	}
+	&.noPrefix{
+		&:before{
+			content: '';
+		}
+	}
+}
+
+/* 布局 */
+.grid,.gridNoPd,.gridSmPd,.gridNoMb{
+	display: -webkit-box;
+	display: -webkit-flex;
+	display: -ms-flexbox;
+	display: flex;
+	-webkit-flex-wrap: wrap;
+	-ms-flex-wrap: wrap;
+	flex-wrap: wrap;
+	width: 100%;
+	padding: 20upx 20upx 0 20upx;
+	>.item,>image,>view,>navigator,>text,>button{
+		width: 50%;
+		padding: 0 10upx;
+		margin-bottom: 20upx;
+	}
+	@for $i from 1 through 50{
+		&.g#{$i}{
+			>.item,>image,>view,>navigator,>text,>button{
+				width: 100%/$i;
+			}
+		}
+	}
+}
+
+.gridNoMb{
+	>.item,>image,>view,>navigator,>text,>button{
+		margin-bottom: 0;
+	}
+}
+
+.gridNoPd{
+	padding: 0;
+	>.item,>image,>view,>navigator,>text,>button{
+		padding: 0;
+		margin-bottom: 0;
+	}
+}
+.gridSmPd{
+	padding: 0;
+	>.item,>image,>view,>navigator,>text,>button{
+		padding-right: 0;
+		&:first-child{
+			padding-left: 0;
+			padding-right: 10upx;
+		}
+	}
+}
+
+/* flex布局 */
+.flex{
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	&.t{
+		align-items: flex-start;
+	}
+	&.b{
+		align-items: flex-end;
+	}
+	&.cv{ //垂直方向铺满
+		align-items: stretch;
+	}
+	&.bk{ //水平方向铺满
+		flex-direction: column;
+	}
+	&.lt{
+		justify-content: flex-start;
+	}
+	&.ct{
+		justify-content: center;
+	}
+	&.rt{
+		justify-content: flex-end;
+	}
+	&.ar{
+		justify-content: space-around;
+	}
+	&.av{
+		>.item,view,button,navigator,image,text{
+			flex:1;
+		}
+	}
+}
+
+/* 定位布局 */
+.father{
+	position: relative;
+}
+.abs,.fixed{
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 1;
+	&image{
+		width: 100%;
+		height: 100%;
+	}
+	&.top{
+		bottom: auto;
+	}
+	&.bottom{
+		top: auto;
+	}
+	&.left{
+		right: auto;
+	}
+	&.right{
+		left: auto;
+	}
+}
+@for $i from 0 through 20 {
+	.z#{$i}{
+		z-index: $i !important;
+	}
+}
+
+@for $i from 1 through 200 {
+	.top-#{$i}{
+		bottom: auto;
+		top: $i * -2upx;
+	}
+	.left-#{$i}{
+		right: auto;
+		left: $i *  -2upx;
+	}
+	.bottom-#{$i}{
+		top: auto;
+		bottom: $i *  -2upx;
+	}
+	.right-#{$i}{
+		left: auto;
+		right: $i *  -2upx;
+	}
+	.top#{$i}{
+		bottom: auto;
+		top: $i * 2upx;
+	}
+	.left#{$i}{
+		right: auto;
+		left: $i *  2upx;
+	}
+	.bottom#{$i}{
+		top: auto;
+		bottom: $i *  2upx;
+	}
+	.right#{$i}{
+		left: auto;
+		right: $i *  2upx;
+	}
+	.top-#{$i}p{
+		bottom: auto;
+		top: $i * -1%;
+	}
+	.left-#{$i}p{
+		right: auto;
+		left: $i * -1%;
+	}
+	.bottom-#{$i}p{
+		top: auto;
+		bottom: $i * -1%;
+	}
+	.right-#{$i}p{
+		left: auto;
+		right: $i * -1%;
+	}
+	.top#{$i}p{
+		bottom: auto;
+		top: $i * 1%;
+	}
+	.left#{$i}p{
+		right: auto;
+		left: $i * 1%;
+	}
+	.bottom#{$i}p{
+		top: auto;
+		bottom: $i * 1%;
+	}
+	.right#{$i}p{
+		left: auto;
+		right: $i * 1%;
+	}
+}
+
+.fixed{
+	position: fixed;
+}
+
+/* fix-auto布局 */
+.fixAuto,.fixAutoNoPd,.fixAutoSmPd{
+	display: table;
+	width: 100%;
+	padding: 20upx 10upx;
+	>.item,>view,>image,>navigator,>text,>button{
+		vertical-align: top;
+		padding: 0 10upx;
+		display: table-cell ;
+	}
+	&.middle{
+		>.item,>view,>image,>navigator,>text{
+			vertical-align: middle;
+		}
+	}
+	&.bottom{
+		>.item,>view,>image,>navigator,>text{
+			vertical-align: bottom;
+		}
+	}
+}
+.fixAutoSmPd{
+	padding: 0;
+	>.item,>view,>image,>navigator,>text{
+		padding-right: 0;
+		&:first-child{
+			padding-left: 0;
+			padding-right: 10upx;
+		}
+	}
+}
+.fixAutoNoPd{
+	padding: 0;
+	>.item,>view,>image,>navigator,>text{
+		padding: 0;
+	}
+}
+
+/* 浮动组件 */
+.clear{
+	&:after{
+		content: "";
+		clear: both;
+		height: 0;
+		display: block;
+		visibility: hidden;
+	}
+}
+.fl{
+	float: left;
+}
+.fr{
+	float: right;
+}
+
+/* 按钮样式类 */
+.btn,.roundBtn{
+	cursor: pointer;
+	display: inline-block;
+	text-align: center;
+	padding: 8upx 24upx;
+	background-color: $main;
+	color: $mainInverse ;
+	font-size: 28upx;
+	border: 1px solid $main;
+	-webkit-border-radius: 8upx;
+	-moz-border-radius: 8upx;
+	border-radius: 8upx;
+	transition: 0.4s;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+	&.gradualBg{
+		border-color: transparent;
+		background-image: $mainGradual;
+		color: $mainGradualInverse;
+		
+	}
+	&.blackBg{
+		background-color: $black;
+		border-color: $black;
+		color: #fff;
+		&.shadow{
+			box-shadow: 0px 2px 9px -1px rgba($black , 0.4);
+		}
+	}
+	&.greenBg{
+		background-color: $green;
+		border-color: $green;
+		color: #fff;
+		&.shadow{
+			box-shadow: 0px 2px 9px -1px rgba($green , 0.4);
+		}
+	}
+	&.grayBg{
+		border-color: rgba(#30302f,0.2);
+		background-color: #f7f7f7;
+		color: #30302f;
+		&.shadow{
+			box-shadow: 0px 2px 9px -1px rgba( #30302f , 0.2);
+		}
+	}
+	&.whiteBg{
+		border-color: rgba(#fff,0.2);
+		background-color: #fff;
+		color: #000;
+	}
+	
+	&.orangeBg{
+		border-color: $orange;
+		background-color: $orange;
+		color: #fff;
+		&.shadow{
+			box-shadow: 0px 2px 9px -1px rgba( $orange , 0.4);
+		}
+	}
+	&.redBg{
+		border-color: $red;
+		background-color: $red;
+		color: #fff;
+		&.shadow{
+			box-shadow: 0px 2px 9px -1px rgba( $red , 0.4);
+		}
+	}
+	&.yellowBg{
+		border-color: $yellow;
+		background-color: $yellow;
+		color: #000;
+		&.shadow{
+			box-shadow: 0px 2px 9px -1px rgba( $yellow , 0.4);
+		}
+	}
+	
+	&.line{
+		background-color: transparent;
+		background-image: none;
+		color: $main;
+		&.blackBg{
+			color: $black;
+		}
+		&.greenBg{
+			color: $green;
+		}
+		&.yellowBg{
+			color: $yellow;
+		}
+		&.grayBg{
+			color: #30302f;
+		}
+		&.whiteBg{
+			border-color:  rgba(#fff,0.7);
+			color: #fff;
+		}
+		&.orangeBg{
+			color: $orange;
+		}
+		&.redBg{
+			color: $red;
+		}
+	}
+	&+.btn,&+.roundBtn{
+		margin-left: 20upx;
+	}
+	&.block{
+	   margin: 0;
+	   padding: 20upx 24upx;
+	   display: block;
+	   width: 100%;
+	   &+.btn{
+		   margin-left: 0;
+	   }
+	}
+	&:hover{
+		-webkit-transform: scale(0.99);
+		-moz-transform: scale(0.99);
+		-ms-transform: scale(0.99);
+		-o-transform: scale(0.99);
+		transform: scale(0.99);
+		opacity: 0.8;
+	}
+	&.disabled{
+		-webkit-transform: scale(1);
+		-moz-transform: scale(1);
+		-ms-transform: scale(1);
+		-o-transform: scale(1);
+		transform: scale(1);
+		opacity: 0.4;
+		cursor: not-allowed;
+	}
+}
+
+[class^="tag"] , [class*=" tag"]{
+	display: inline-block;
+	font-size: 24upx;
+	padding: 4upx 14upx;
+	border-radius: 4upx;
+	margin-right: 6upx;
+	margin-left: 6upx;
+}
+.tag{
+	background-color: rgba($main,0.2);
+	color: $main;
+}
+.tagBlue{
+	background-color: rgba($blue,0.2);
+	color: $blue;
+}
+.tagGray{
+	background-color: rgba($gray,0.2);
+	color: $gray;
+}
+
+.tagGradual{
+	background-image: linear-gradient(to top right,rgba($main,0.2),rgba($main,0.1));
+	color: $main;
+}
+
+.tagBlack{
+	background-color: rgba($black,0.2);
+	color: $black;
+}
+.tagGreen{
+	background-color: rgba($green,0.2);
+	color: $green;
+}
+
+.tagWhite{
+	background-color: rgba($white,0.2);
+	color: $white;
+}
+
+.tagOrange{
+	background-color: rgba($orange,0.2);
+	color: $orange;
+}
+.tagRed{
+	background-color: rgba($red,0.2);
+	color: $red;
+}
+.tagYellow{
+	background-color: rgba($yellow,0.2);
+	color: $yellow;
+}
+
+/* 边线(实线、虚线) */
+.bdn{
+	border: none;
+}
+.bd{
+	border: 1px solid $borderColor;
+	&.dashed{
+		border-style: dashed;
+	}
+}
+.bt{
+	border-top:1px solid $borderColor;
+	&.dashed{
+		border-top-style: dashed;
+	}
+}
+.bb{
+	border-bottom:1px solid $borderColor;
+	&.dashed{
+		border-bottom-style: dashed;
+	}
+}
+.bl{
+	border-left:1px solid $borderColor;
+	&.dashed{
+		border-left-style: dashed;
+	}
+}
+.br{
+	border-right: 1px solid $borderColor;
+	&.dashed{
+		border-right-style: dashed;
+	}
+}
+
+$b:2;
+@while $b <= 10 {
+	.bd#{$b}{
+		border: #{$b}px solid $borderColor;
+		&.dashed{
+			border-style: dashed;
+		}
+	}
+	.bt#{$b}{
+		border-top:#{$b}px solid $borderColor;
+		&.dashed{
+			border-top-style: dashed;
+		}
+	}
+	.bb#{$b}{
+		border-bottom:#{$b}px solid $borderColor;
+		&.dashed{
+			border-bottom-style: dashed;
+		}
+	}
+	.bl#{$b}{
+		border-left:#{$b}px solid $borderColor;
+		&.dashed{
+			border-left-style: dashed;
+		}
+	}
+	.br#{$b}{
+		border-right: #{$b}px solid $borderColor;
+		&.dashed{
+			border-right-style: dashed;
+		}
+	}
+	$b : $b + 1 ;
+}
+
+/* 边线颜色 */
+.mainBd{
+	border-color: $main;
+}
+.greenBd{
+	border-color:  $green;
+}
+.redBd{
+	border-color: $red;
+}
+.yellowBd{
+	border-color:$yellow ;
+}
+.blackBd{
+	border-color: $black;
+}
+.whiteBd{
+	border-color:$white ;
+}
+.grayBd{
+	border-color:$gray;
+}
+.greyBd{
+	border-color:$grey;
+}
+.orangeBd{
+	border-color:$orange;
+}
+
+/* 圆角 */
+.radius,.rds{
+	-webkit-border-radius: 100%!important;
+	-moz-border-radius: 100%!important;
+	border-radius: 100%!important;
+}
+
+$r:0;
+@while $r <= 50{
+	.rds#{$r},&.radius#{$r}{
+		-webkit-border-radius:$r*2upx!important;
+		-moz-border-radius:$r*2upx!important;
+		border-radius:$r*2upx!important;
+	}
+	$r : $r + 1;
+}
+
+.rdsTl,.radiusTopLeft{
+	border-top-left-radius:100%!important;
+}
+.rdsTr,.radiusTopRight{
+	border-top-right-radius: 100%!important;
+}
+.rdsBl,.radiusBottomLeft{
+	border-bottom-left-radius: 100%!important;
+}
+.rdsBr,.radiusBottomRight{
+	border-bottom-right-radius: 100%!important;
+}
+
+$r:0;
+@while $r <= 50{
+	.rdsTl#{$r},.radiusTopLeft#{$r}{
+		border-top-left-radius: $r*2upx!important;
+	}
+	.rdsTr#{$r},.radiusTopRight#{$r}{
+		border-top-right-radius: $r*2upx!important;
+	}
+	.rdsBl#{$r},.radiusBottomLeft#{$r}{
+		border-bottom-left-radius: $r*2upx!important;
+	}
+	.rdsBr#{$r},.radiusBottomRight#{$r}{
+		border-bottom-right-radius: $r*2upx!important;
+	}
+	$r : $r + 1;
+}
+
+/* 正方形&长方形 */
+[class^="square"] , [class*=" square"]{
+	width: 100%;
+	position: relative;
+	height: auto;
+	>.item,>image,>view,>navigator,>text,>button{
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+	}
+}
+
+$p : 200 ;
+@while $p > 0 {
+	.square#{$p}{
+		padding-top: $p*1%;
+	}
+	@if $p == 100 {
+		.square{
+			padding-top: 100%;
+		}
+	}
+	$p : $p - 5 ;
+}
+
+
+
+/* 阴影 */
+.shadow{
+	box-shadow: 0px 2px 9px -1px rgba(0, 0, 0, 0.1);
+}
+
+/* 遮罩层 */
+.wrapper-top,.wt{
+	background-image: linear-gradient(rgba(0,0,0,0.3) , rgba(0,0,0,0.02));
+}
+.wrapper-bottom,.wb{
+	background-image: linear-gradient( rgba(0,0,0,0.02) , rgba(0,0,0,0.3) );
+}
+
+[class^="wp"],[class*=" wp"] {
+	z-index: 10;
+}
+
+/* 透明度 */
+@for $op from 0 through 10 {
+	.op#{$op}{
+		opacity: $op * 0.1!important;
+	}
+	.wp#{$op}{
+		background-color: rgba(#000,$op*0.1);
+	}
+	@if $op == 5 {
+		.wp{
+			background-color: rgba(#000,0.5);
+		}
+	}
+}
+
+/* 分割线 */
+[class*=" split"],[class^="split"] {
+	position: relative;
+	&:before{
+		content:"";
+		display: block;
+		position: absolute;
+		left: 0;
+		top: 50%;
+		border-left: 1px solid $borderColor;
+	}
+}
+
+$s:10;
+@while $s <= 100 {
+	.split#{$s}{
+		&:before{
+			height: #{$s*2}upx;
+			margin-top: -#{$s}upx;
+		}
+	}
+	@if $s == 10 {
+		.split{
+			&:before{
+				height: 20upx;
+				margin-top: -10upx;
+			}
+		}
+	}
+	$s:$s+2;
+}
+
+.hover,[class^="hover"],[class*=" hover"]{
+	transition: all 0.4s;
+	cursor: pointer;
+	&:hover{
+		opacity: 0.8 !important;
+	}
+}
+
+
+
+.statusBar{
+	height: var(--status-bar-height);
+}
+
+.winBottom{
+	height: var(--windown-bottom);
+}
+
+.safeBottom{
+	padding-bottom: constant(safe-area-inset-bottom);
+	padding-bottom: env(safe-area-inset-bottom);
+}
+
+.disabled{
+	opacity:0.8;
+	cursor: not-allowed;
+}
+
+
+
+.grid,.gridNoMb,.gridNoPd{
+	>.btn,>.roundBtn{
+		&+.btn,&+.roundBtn{
+			margin-left: 0 ;
+		}
+	}
+}
+
+.roundBtn{
+	-webkit-border-radius: 100upx;
+	-moz-border-radius: 100upx;
+	border-radius: 100upx;
+} 
+ 
+ 
+ 
+ /* 位置 */
+ .text-center,.tc{
+ 	text-align: center!important;
+ }
+ .text-left,.tl{
+ 	text-align: left!important;
+ }
+ .text-right,.tr{
+ 	text-align: right!important;
+ }
+ .text-justify,.tj{
+ 	text-align: justify!important;
+ }
+ .text-bold,.bold{
+ 	font-weight: bold!important;
+ }
+ .text-normal,.normal{
+ 	font-weight: normal!important;
+ }
+ .break{
+ 	white-space: normal;
+ 	word-break: break-all;
+ }
+ .noBreak{
+ 	white-space: nowrap;
+ 	word-break: keep-all;
+ }
+ .inline{
+ 	display: inline-block;
+ }
+ .block{
+ 	display: block;
+ 	width: 100%;
+ }
+ .none{
+ 	display: none;
+ }
+ .center-block{
+ 	margin: 0 auto;
+ 	display: block;
+ }
+ .hidden{
+ 	overflow: hidden;
+ }
+ .hiddenX{
+ 	overflow-x: hidden;
+ }
+ .hiddenY{
+ 	overflow-y: hidden;
+ }
+ .auto{
+ 	overflow: auto;
+ }
+ .autoX{
+ 	overflow-x: auto;
+ }
+ .autoY{
+ 	overflow-y: auto;
+ }
+ .showInMb{
+	 display: block;
+ }
+ .showInPc{
+	 display: none;
+ }
+ table{
+ 	width: 100%;
+ 	border-collapse: collapse;
+ 	border-spacing: 0;
+ 	border: 1px solid #e6e6e6;
+ 	thead{
+ 		tr{
+ 			background-color: #f2f2f2;
+ 			th{
+ 				color: #8799a3;
+ 				width: 1%;
+ 			}
+ 		}
+ 	}
+ 	tr{
+ 		background-color: #fff;
+ 		transition: all 0.4s;
+ 		td,th{
+ 			border: 1px solid #e6e6e6;
+ 			overflow: hidden;
+ 			-o-text-overflow: ellipsis;
+ 			text-overflow: ellipsis;
+ 			white-space: nowrap;
+ 			word-wrap: break-word;
+ 			padding: 5px 10px;
+ 			height: 28px;
+ 			line-height: 28px;
+ 			&.autoWidth{
+ 				width: auto;
+ 			}
+ 		}
+ 		&:hover{
+ 			background-color: #f2f2f2;
+ 		}
+ 	}
+ }

BIN
yudao-vue-ui/components/version-update/static/airship.png


BIN
yudao-vue-ui/components/version-update/static/cloudLeft.png


BIN
yudao-vue-ui/components/version-update/static/cloudRight.png


BIN
yudao-vue-ui/components/version-update/static/login-wave.png


BIN
yudao-vue-ui/components/version-update/static/shipAir.png


BIN
yudao-vue-ui/components/version-update/static/shipGas.png


BIN
yudao-vue-ui/components/version-update/static/smallCloud.png


BIN
yudao-vue-ui/components/version-update/static/star.png


+ 1811 - 0
yudao-vue-ui/components/version-update/version-update.vue

@@ -0,0 +1,1811 @@
+/**
+* BaseCloud APP更新检测组件
+v1.0.4
+*/
+<template>
+	<view class="base-cloud" style="display: inline-block;">
+		<view class="father" style="display: flex;align-items: center;" v-if="showVersion" @click.stop.prevent="checkVersion">
+			<text class="version-text">版本{{version}}</text>
+			<view class="abs top right" v-if="updateData.updated" style="top: -3px;right: -7px;">
+				<view class="w7 h7 rds redBg"></view>
+			</view>
+		</view>
+		<view class="fixed z20 wp6 flex ct plr50 pb50 " v-if="show">
+			<view class="w100p showIn" :animation="inData" style="max-width: 300px;">
+				<view class="rds12" :style="{'background-color':color}">
+					<view class="father">
+						<view class="h120  father hidden">
+							<view class="abs top left50p">
+								<image src="./static/cloudRight.png" mode="widthFix" class="w500 h120  animated goAway infinite"></image>
+							</view>
+							<view class="abs top right50p">
+								<image src="./static/cloudLeft.png" mode="widthFix" class="w500 h120  animated goAwayLeft infinite"></image>
+							</view>
+							<image class="abs top30 left30 w10 h10 animated infinite fadeOut slow" src="./static/star.png" mode="widthFix"></image>
+							<image class="abs top60 left80 w10 h10  animated infinite fadeOut slowest delay-1s" src="./static/star.png" mode="widthFix"></image>
+							<image class="abs top20 right20 w10 h10  animated infinite fadeOut slower delay-2s" src="./static/star.png" mode="widthFix"></image>
+							<image class="abs top20 right50 w30 h30 animated fadeOutRight infinite slowest" src="./static/smallCloud.png"
+							 mode="widthFix"></image>
+							<image class="abs top30 left50 w30 h30 animated fadeOutRight infinite slow8 " src="./static/smallCloud.png" mode="widthFix"></image>
+						</view>
+						<view class="abs bottom animated bounceUp infinite">
+							<view class="animated goUp delay-06s">
+								<image src="./static/airship.png" mode="widthFix" class="w80 h75 center-block father z3"></image>
+								<view class="father" style="top: -5px;">
+									<image src="./static/shipAir.png" mode="widthFix" class="w40 h85 center-block  animated infinite splashOut"></image>
+									<view class="abs">
+										<image src="./static/shipAir.png" mode="widthFix" class="w40 h85 center-block animated infinite splashOut delay-03s"></image>
+									</view>
+									<view class="abs">
+										<image src="./static/shipAir.png" mode="widthFix" class="w40 h85 center-block animated infinite splashOut delay-06s"></image>
+									</view>
+									<view class="abs bottom" style="bottom: -80upx;">
+										<image src="./static/shipGas.png" mode="widthFix" class="w40 h85 center-block animated infinite splash"></image>
+									</view>
+								</view>
+							</view>
+						</view>
+					</view>
+
+					<view class=" whiteBg hidden plr20 father z3 rdsBr12 rdsBl12" :class="{'pb100':progress <= 0 || progress >= 100 || completed}">
+						<view class="ptb10 fz16 bold">
+							<block v-if="progress == 0">
+								{{title}} {{ updateData.version ? 'v' + updateData.version : ''}}
+							</block>
+							<block v-else-if="progress <=100 && !completed">
+								<view>
+									<view class="fz30 normal avanti pt15 text-center">
+										{{ progress }}
+										<text class="fz12 ml2">
+											%
+										</text>
+									</view>
+
+									<view class="text-center pb40 op8 gray fz14 normal">
+										版本更新中,不要离开...
+									</view>
+								</view>
+							</block>
+							<view class="text-center pt15" v-else-if="completed">
+								版本升级成功
+								<view class="pb40 op8 gray fz14 normal pt5">
+									更新已完成,请重启应用体验新版
+								</view>
+							</view>
+						</view>
+						<scroll-view scroll-y="true" class="scroll-view h60 autoY pb20" v-if="progress == 0">
+							<view class="column">
+								<text v-if="updateData.description.length === 0">
+									{{ defaultContent }}
+								</text>
+								<text  v-for="(item, index) in updateData.description" :key="index">
+									{{ index + 1 }}.{{ item }}
+								</text>
+							</view>
+						</scroll-view>
+						<view class="pd50 pt25" v-else-if="progress < 100">
+							<view class="grayBg bd rds23">
+								<view class="grayBg rds23">
+									<view class="ptb3 rds23" :style="{width:progress+'%','background-color':color}"></view>
+								</view>
+							</view>
+						</view>
+						<view class="father">
+							<view class="abs top left50p  roundBox rds text-center" :style="{'background-color':color}">
+								<view class="pt30" v-if="!completed">
+									<button hover-class="op9" @tap.stop="download" class="btn bd2 whiteBg line rds23 inline plr50 ptb10 fz16">
+										立即升级
+									</button>
+								</view>
+								<view class="pt30" v-else>
+									<button hover-class="op8" @tap.stop="restart" class="btn bd2 whiteBg line rds23 inline plr50 ptb10 fz16">
+										立即重启
+									</button>
+								</view>
+							</view>
+						</view>
+					</view>
+				</view>
+				<view class="op9 father" v-if="progress == 0">
+					<view class="abs">
+						<view class="flex ct ">
+							<view class="h30 bl3 whiteBd"></view>
+						</view>
+						<view class="close-btn" @click="hide">
+							<text class="mix-icon icon-close"></text>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+		
+		<mix-loading v-if="isLoading"></mix-loading>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "version-update",
+		props: {
+			title: {
+				default: "发现新版本"
+			},
+			defaultContent: {
+				default: "快来升级,体验最新的功能吧!"
+			},
+			showVersion: {
+				default: true
+			},
+			autoShow: {
+				default: false
+			},
+			isCache: {
+				default: true
+			},
+			updateUrl: {
+				default: "api/base-app-version"
+			},
+			color: {
+				default: "#ff536f"
+			}
+		},
+		data() {
+			return {
+				show: false,
+				version: "1.0.0",
+				updateData: {
+					description: []
+				},
+				progress: 0,
+				completed: false,
+				inData: null
+			};
+		},
+		created() {
+			// #ifdef APP-PLUS
+			plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
+				this.version = widgetInfo.version;
+			});
+			// #endif
+		},
+		methods: {
+			async checkVersion(e) {
+				console.log(e);
+				const res = await this.$request('version', 'check', {
+					version: this.version
+				}, {
+					showLoading: true
+				})
+				if (res.status === 0) {
+					this.$util.msg(res.msg);
+				} else {
+					res.data.description = res.data.description.split(';');
+					this.updateData = res.data;
+					this.show = true;
+				}
+			},
+
+			download(e) {
+				if(this.isDownloading){
+					return;
+				}
+				this.isDownloading = true;
+				const task = uni.downloadFile({
+					url: this.updateData.downloadLink,
+					success: (downloadResult) => {
+						this.isDownloading =false;
+						uni.hideLoading();
+						if (downloadResult.statusCode === 200) {
+							plus.runtime.install(downloadResult.tempFilePath, {
+								force: false
+							}, (e) => {
+								this.downloadSuccess(e);
+							}, (err) => {
+								this.downloadError(err);
+							});
+						}
+					},
+					fail: err => {
+						this.isDownloading =false;
+						this.downloadError(err);
+					}
+				});
+
+				task.onProgressUpdate((e) => {
+					console.log(e.progress);
+					this.progress = e.progress;
+				});
+			},
+			downloadError(e) {
+				this.show = false;
+				this.progress = 0;
+				uni.showModal({
+					title: '提示',
+					content: '更新失败,请稍后再试',
+					showCancel: false,
+					confirmColor: '#414cd9'
+				});
+			},
+			downloadSuccess(e) {
+				this.completed = true;
+			},
+			restart(e) {
+				this.show = false;
+				this.completed = false;
+				this.progress = 0;
+				// #ifdef APP-PLUS
+				plus.runtime.restart();
+				// #endif
+			},
+			hide(e) {
+				var animation = uni.createAnimation({
+					duration: 300,
+					timingFunction: 'ease',
+				});
+				animation.scale(0).opacity(0).step();
+				this.inData = animation.export();
+				setTimeout((e) => {
+					this.show = false;
+					this.inData = null;
+				}, 420);
+			},
+
+		},
+	}
+</script>
+
+<style scoped lang="scss">
+	.version-text{
+		margin-right: 10rpx;
+		font-size: 26rpx;
+		color: #999;
+		position: relative;
+		z-index: -2rpx;
+	}
+	.close-btn{
+		display: flex;
+		justify-content: center;
+		color: #fff;
+		
+		.mix-icon{
+			margin-top: -20rpx;
+			padding: 20rpx;
+			font-size: 48rpx;
+		}
+	}
+	.scroll-view{
+		height: auto !important;
+		min-height: 120rpx;
+		max-height: 17vh;
+		
+		text{
+			margin-bottom: 16rpx;
+			line-height: 1.5;
+			font-size: 28rpx;
+		}
+	}
+	.roundBox {
+		width: 5000upx;
+		height: 5000upx;
+		margin-left: -2500upx;
+	}
+
+	.animated {
+		-webkit-animation-duration: 1s;
+		animation-duration: 1s;
+		-webkit-animation-fill-mode: both;
+		animation-fill-mode: both;
+		animation-timing-function: linear;
+	}
+
+	.animated.infinite {
+		-webkit-animation-iteration-count: infinite;
+		animation-iteration-count: infinite;
+	}
+
+	@keyframes goUp {
+		from {
+			-webkit-transform: translate3d(0, 70%, 0);
+			transform: translate3d(0, 70%, 0);
+		}
+
+		to {
+			-webkit-transform: translate3d(0, 0, 0);
+			transform: translate3d(0, 0, 0);
+		}
+	}
+
+	.goUp {
+		-webkit-animation-name: goUp;
+		animation-name: goUp;
+		-webkit-animation-duration: 600ms;
+		animation-duration: 600ms;
+		animation-timing-function: ease-in;
+	}
+
+	@keyframes splash {
+
+		0,
+		100% {
+			transform: scaleX(0.9);
+		}
+
+		5%,
+		95% {
+			transform: scaleX(1.02);
+		}
+
+		10%,
+		80% {
+			transform: scaleX(1.05);
+		}
+
+		25%,
+		75% {
+			transform: scaleX(1.08);
+		}
+
+		50% {
+			transform: scaleX(1.1);
+		}
+	}
+
+	.splash {
+		-webkit-animation-name: splash;
+		animation-name: splash;
+		-webkit-animation-duration: 0.6s;
+		animation-duration: 0.6s;
+		animation-timing-function: linear;
+	}
+
+	@-webkit-keyframes splashOut {
+		from {
+			opacity: 1;
+			transform: scaleX(0);
+		}
+
+		to {
+			opacity: 0;
+			transform: scaleX(2);
+		}
+	}
+
+	.splashOut {
+		-webkit-animation-name: splashOut;
+		animation-name: splashOut;
+		-webkit-animation-duration: 1s;
+		animation-duration: 1s;
+	}
+
+	@keyframes bounceUp {
+		0% {
+			transform: translate3d(0, 0, 0);
+		}
+
+		50% {
+			transform: translate3d(0, -30rpx, 0);
+		}
+	}
+
+	.bounceUp {
+		-webkit-animation-name: bounceUp;
+		animation-name: bounceUp;
+		-webkit-animation-duration: 1.6s;
+		animation-duration: 1.6s;
+		animation-timing-function: linear;
+	}
+
+	@keyframes fadeOut {
+
+		0,
+		100% {
+			opacity: 1;
+		}
+
+		50% {
+			opacity: 0;
+		}
+	}
+
+	.fadeOut {
+		-webkit-animation-name: fadeOut;
+		animation-name: fadeOut;
+	}
+
+	@keyframes fadeOutRight {
+		0% {
+			opacity: 0;
+			transform: translate3d(-200%, 0, 0);
+		}
+
+		50% {
+			opacity: 1;
+			transform: translate3d(0, 0, 0);
+		}
+
+		100% {
+			opacity: 0;
+			transform: translate3d(200%, 0, 0);
+		}
+	}
+
+	.fadeOutRight {
+		-webkit-animation-name: fadeOutRight;
+		animation-name: fadeOutRight;
+	}
+
+	.animated.delay-03s {
+		-webkit-animation-delay: 0.3s;
+		animation-delay: 0.3s;
+	}
+
+	.animated.delay-06s {
+		-webkit-animation-delay: 0.6s;
+		animation-delay: 0.6s;
+	}
+
+	.animated.delay-1s {
+		-webkit-animation-delay: 1s;
+		animation-delay: 1s;
+	}
+
+	.animated.delay-2s {
+		-webkit-animation-delay: 2s;
+		animation-delay: 2s;
+	}
+
+	.animated.delay-3s {
+		-webkit-animation-delay: 3s;
+		animation-delay: 3s;
+	}
+
+	.animated.fast {
+		-webkit-animation-duration: 800ms;
+		animation-duration: 800ms;
+	}
+
+	.animated.faster {
+		-webkit-animation-duration: 500ms;
+		animation-duration: 500ms;
+	}
+
+	.animated.fastest {
+		-webkit-animation-duration: 200ms;
+		animation-duration: 200ms;
+	}
+
+	.animated.slow {
+		-webkit-animation-duration: 2s;
+		animation-duration: 2s;
+	}
+
+	.animated.slower {
+		-webkit-animation-duration: 3s;
+		animation-duration: 3s;
+	}
+
+	.animated.slowest {
+		-webkit-animation-duration: 10s;
+		animation-duration: 10s;
+	}
+
+	.animated.slow4 {
+		-webkit-animation-duration: 5s;
+		animation-duration: 5s;
+	}
+
+	.animated.slow5 {
+		-webkit-animation-duration: 5s;
+		animation-duration: 5s;
+	}
+
+	.animated.slow8 {
+		-webkit-animation-duration: 8s;
+		animation-duration: 8s;
+	}
+
+	.goAway {
+		transform: translate3d(-50%, 10%, 0);
+		-webkit-animation-name: goAway;
+		animation-name: goAway;
+		-webkit-animation-duration: 2s;
+		animation-duration: 2s;
+	}
+
+	@keyframes goAway {
+		from {
+			transform: translate3d(-50%, 10%, 0);
+		}
+
+		to {
+			transform: translate3d(-1.3%, -17.6%, 0);
+		}
+	}
+
+	.goAwayLeft {
+		transform: translate3d(50%, 10%, 0);
+		-webkit-animation-name: goAwayLeft;
+		animation-name: goAwayLeft;
+		-webkit-animation-duration: 2s;
+		animation-duration: 2s;
+	}
+
+	@keyframes goAwayLeft {
+		from {
+			transform: translate3d(50%, 10%, 0);
+		}
+
+		to {
+			transform: translate3d(2%, -17%, 0);
+		}
+	}
+
+	@keyframes showIn {
+
+		0% {
+			opacity: 0;
+			transform: scale3d(0.5, 0.5, 0.5);
+		}
+
+		100% {
+			opacity: 1;
+			transform: scale3d(1, 1, 1);
+		}
+	}
+
+	.showIn {
+		animation-duration: 0.4s;
+		animation-name: showIn;
+	}
+	
+	div,a,img,span,page,view,navigator,image,text,input,textarea,button,form{
+		box-sizing: border-box;
+	}
+	
+	a{
+		text-decoration: none;
+		color: $main;
+	}
+	
+	form{
+		display: block;
+		width: 100%;
+	}
+	
+	image{will-change: transform}
+	
+	input,textarea,form{
+		width: 100%;
+		height: auto;
+		min-height: 10px;
+		display: block;
+		font-size: inherit;
+	}
+	
+	button{
+		color: inherit;
+		line-height: inherit;
+		margin: 0;
+		background-color: transparent;
+		padding: 0;
+		-webkit-border-radius: 0;
+		-moz-border-radius: 0;
+		border-radius: 0;
+		&:after{
+			display: none;
+		}
+	}
+	
+	switch .uni-switch-input{
+		margin-right: 0;
+	}
+	.wx-switch-input,.uni-switch-input{width:42px !important;height:22px !important;}
+		.wx-switch-input::before,.uni-switch-input::before{width:40px !important;height: 20px !important;}
+		.wx-switch-input::after,.uni-switch-input::after{width: 20px !important;height: 20px !important;}
+	
+	
+	/**背景颜色**/
+	.bg{
+		background-color: $main;
+		color: $mainInverse;
+	}
+	.gradualBg{
+		background-image: $mainGradual;
+		color: $mainGradualInverse ;
+	}
+	.grayBg{
+		background-color: #f7f7f7;
+		color: #30302f;
+	}
+	.whiteBg{
+		background-color: #fff;
+		color: #000;
+	}
+	.blackBg{
+		background-color: #000;
+		color: #fff;
+	}
+	.orangeBg{
+		background-color: $orange;
+		color: #fff;
+	}
+	.redBg{
+		background-color: $red;
+		color: #fff;
+	}
+	.yellowBg{
+		background-color: $yellow;
+		color: #000;
+	}
+	.greenBg{
+		background-color: $green;
+		color: #fff;
+	}
+	.brownBg{
+		background-color: $brown;
+		color: #fff;
+	}
+	.blueBg{
+		background-color: $blue;
+		color: #fff;
+	}
+	.purpleBg{
+		background-color: $purple;
+		color: #fff;
+	}
+	
+	/* 文字颜色 */
+	.main{
+		color: $main;
+	}
+	.green{
+		color: $green;
+	}
+	.red{
+		color: $red;
+	}
+	.yellow{
+		color: $yellow;
+	}
+	.black{
+		color: $black;
+	}
+	.white{
+		color: $white;
+	}
+	.gray{
+		color: $gray;
+	}
+	.grey{
+		color: $grey;
+	}
+	.orange{
+		color: $orange;
+	}
+	.brown{
+		color: $brown;
+	}
+	.blue{
+		color: $blue;
+	}
+	.purple{
+		color: $purple;
+	}
+	
+	.hoverMain{
+		&:hover{
+			color: $main;
+		}
+	}
+	
+	.hoverGreen{
+		&:hover{
+			color: $green;
+		}
+	}
+	
+	.hoverRed{
+		&:hover{
+			color: $red;
+		}
+	}
+	
+	.hoverBlue{
+		&:hover{
+			color: $blue;
+		}
+	}
+	
+	.hoverGray{
+		&:hover{
+			color: $gray;
+		}
+	}
+	
+	.hoverWhite{
+		&:hover{
+			color: $white;
+		}
+	}
+	
+	.hoverBlack{
+		&:hover{
+			color: $black;
+		}
+	}
+	
+	.hoverOrange{
+		&:hover{
+			color: $orange;
+		}
+	}
+	
+	.hoverYellow{
+		&:hover{
+			color: $yellow;
+		}
+	}
+	
+	.hoverBrown{
+		&:hover{
+			color: $brown;
+		}
+	}
+	
+	.hoverPurple{
+		&:hover{
+			color: $purple;
+		}
+	}
+	
+	/* 宽度 高度 */
+	$w:0;
+	@while $w <= 500 {
+		@if $w <= 100 {
+			.w#{$w}p{
+				width: $w*1%;
+			}
+			.h#{$w}p{
+				height: $w*1%;
+			}
+			@if $w == 100 {
+				.100p{
+					width: 100%;
+					height: 100%;
+				}
+				.ww{
+					width: 100vw;
+				}
+				.hh{
+					height: 100vh;
+				}
+			}
+		}
+		.w#{$w}{
+			width: $w*2upx;
+		}
+		.h#{$w}{
+			height: $w*2upx;
+		}
+		@if $w < 10 {
+			$w : $w + 1 ;
+		} @else{
+			$w : $w + 5 ;
+		}
+	}
+	
+	
+	/* 字号 */
+	@for $fz from 12 through 100 {
+		.fz#{$fz}{
+			font-size: $fz*2upx !important;
+		}
+	}
+	
+	/* 边距 - 覆盖顺序是小的尺寸覆盖大的尺寸 少的方向覆盖多的方向 */
+	$s : 0 ;
+	@while $s <= 500 {
+		.pd#{$s}{
+			padding: $s*2upx!important;
+		}
+		.m#{$s}{
+			margin: $s*2upx!important;
+		}
+		@if $s == 15 {
+			.pd{
+				padding: 30upx!important;
+			}
+			.m{
+				margin: 30upx!important;
+			}
+		}
+		@if $s < 10 {
+			$s : $s + 1 ;
+		} @else if($s < 100){
+			$s : $s + 5 ;
+		}   @else if($s < 300){
+			$s : $s + 10 ;
+		} @else{
+			$s : $s + 50 ;
+		}
+	}
+	
+	$s : 0 ;
+	@while $s <= 500 {
+		.ptb#{$s}{
+			padding-top: $s*2upx!important;
+			padding-bottom: $s*2upx!important;
+		}
+		.plr#{$s}{
+			padding-left: $s*2upx!important;
+			padding-right: $s*2upx!important;
+		}
+		.mtb#{$s}{
+			margin-top: $s*2upx!important;
+			margin-bottom: $s*2upx!important;
+		}
+		.mlr#{$s}{
+			margin-left: $s*2upx!important;
+			margin-right: $s*2upx!important;
+		}
+		@if $s == 15 {
+			.ptb{
+				padding-top: 30upx!important;
+				padding-bottom: 30upx!important;
+			}
+			.plr{
+				padding-left: 30upx!important;
+				padding-right: 30upx!important;
+			}
+			
+			.mlr{
+				margin-left: 30upx!important;
+				margin-right: 30upx!important;
+			}
+			.mtb{
+				margin-top: 30upx!important;
+				margin-bottom: 30upx!important;
+			}
+		}
+		@if $s < 10 {
+			$s : $s + 1 ;
+		} @else if($s < 100){
+			$s : $s + 5 ;
+		}   @else if($s < 300){
+			$s : $s + 10 ;
+		} @else{
+			$s : $s + 50 ;
+		}
+	}
+	
+	$s : 0 ;
+	@while $s <= 500 {
+		.pl#{$s}{
+			padding-left: $s*2upx!important;
+		}
+		.pr#{$s}{
+			padding-right: $s*2upx!important;
+		}
+		.pt#{$s}{
+			padding-top: $s*2upx!important;
+		}
+		.pb#{$s}{
+			padding-bottom: $s*2upx!important;
+		}
+		.ml#{$s}{
+			margin-left: $s*2upx!important;
+		}
+		.mr#{$s}{
+			margin-right: $s*2upx!important;
+		}
+		.mt#{$s}{
+			margin-top: $s*2upx!important;
+		}
+		.mb#{$s}{
+			margin-bottom: $s*2upx!important;
+		}
+		@if $s == 15 {
+			.pt{
+				padding-top: 30upx!important;
+			}
+			.pb{
+				padding-bottom: 30upx!important;
+			}
+			.pl{
+				padding-left: 30upx!important;
+			}
+			.pr{
+				padding-right: 30upx!important;
+			}
+			.mt{
+				margin-top: 30upx!important;
+			}
+			.mr{
+				margin-right: 30upx!important;
+			}
+			.ml{
+				margin-left: 30upx!important;
+			}
+			.mb{
+				margin-bottom: 30upx!important;
+			}
+		}
+		@if $s < 10 {
+			$s : $s + 1 ;
+		} @else if($s < 100){
+			$s : $s + 5 ;
+		} @else if($s < 300){
+			$s : $s + 10 ;
+		} @else{
+			$s : $s + 50 ;
+		}
+	}
+	
+	
+	
+	/* 文字溢出隐藏 */
+	.clip{
+		width: 100%;
+		display: -webkit-box;
+		overflow: hidden;
+		-webkit-line-clamp: 1;
+		-webkit-box-orient: vertical;
+		@for $i from 2 through 10{
+			&.c#{$i}{
+				-webkit-line-clamp: $i;
+			}
+		}
+	}
+	
+	.cut{
+		display: block;
+		width: 100%;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+	}
+	
+	/* 价格 */
+	.price{
+		font-size: inherit ;
+		&:before{
+			content: "¥";
+			font-size: 70%;
+			color: inherit;
+			font-weight: normal;
+			font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif ;
+		}
+		.point{
+			display: inline-block;
+			font-size: 70%;
+			font-weight: inherit;
+			letter-spacing: 1px;
+			color: inherit;
+		}
+		&.noPrefix{
+			&:before{
+				content: '';
+			}
+		}
+	}
+	
+	/* 布局 */
+	.grid,.gridNoPd,.gridSmPd,.gridNoMb{
+		display: -webkit-box;
+		display: -webkit-flex;
+		display: -ms-flexbox;
+		display: flex;
+		-webkit-flex-wrap: wrap;
+		-ms-flex-wrap: wrap;
+		flex-wrap: wrap;
+		width: 100%;
+		padding: 20upx 20upx 0 20upx;
+		>.item,>image,>view,>navigator,>text,>button{
+			width: 50%;
+			padding: 0 10upx;
+			margin-bottom: 20upx;
+		}
+		@for $i from 1 through 50{
+			&.g#{$i}{
+				>.item,>image,>view,>navigator,>text,>button{
+					width: 100%/$i;
+				}
+			}
+		}
+	}
+	
+	.gridNoMb{
+		>.item,>image,>view,>navigator,>text,>button{
+			margin-bottom: 0;
+		}
+	}
+	
+	.gridNoPd{
+		padding: 0;
+		>.item,>image,>view,>navigator,>text,>button{
+			padding: 0;
+			margin-bottom: 0;
+		}
+	}
+	.gridSmPd{
+		padding: 0;
+		>.item,>image,>view,>navigator,>text,>button{
+			padding-right: 0;
+			&:first-child{
+				padding-left: 0;
+				padding-right: 10upx;
+			}
+		}
+	}
+	
+	/* flex布局 */
+	.flex{
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		&.t{
+			align-items: flex-start;
+		}
+		&.b{
+			align-items: flex-end;
+		}
+		&.cv{ //垂直方向铺满
+			align-items: stretch;
+		}
+		&.bk{ //水平方向铺满
+			flex-direction: column;
+		}
+		&.lt{
+			justify-content: flex-start;
+		}
+		&.ct{
+			justify-content: center;
+		}
+		&.rt{
+			justify-content: flex-end;
+		}
+		&.ar{
+			justify-content: space-around;
+		}
+		&.av{
+			>.item,view,button,navigator,image,text{
+				flex:1;
+			}
+		}
+	}
+	
+	/* 定位布局 */
+	.father{
+		position: relative;
+	}
+	.abs,.fixed{
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		z-index: 1;
+		&image{
+			width: 100%;
+			height: 100%;
+		}
+		&.top{
+			bottom: auto;
+		}
+		&.bottom{
+			top: auto;
+		}
+		&.left{
+			right: auto;
+		}
+		&.right{
+			left: auto;
+		}
+	}
+	@for $i from 0 through 20 {
+		.z#{$i}{
+			z-index: $i !important;
+		}
+	}
+	
+	@for $i from 1 through 200 {
+		.top-#{$i}{
+			bottom: auto;
+			top: $i * -2upx;
+		}
+		.left-#{$i}{
+			right: auto;
+			left: $i *  -2upx;
+		}
+		.bottom-#{$i}{
+			top: auto;
+			bottom: $i *  -2upx;
+		}
+		.right-#{$i}{
+			left: auto;
+			right: $i *  -2upx;
+		}
+		.top#{$i}{
+			bottom: auto;
+			top: $i * 2upx;
+		}
+		.left#{$i}{
+			right: auto;
+			left: $i *  2upx;
+		}
+		.bottom#{$i}{
+			top: auto;
+			bottom: $i *  2upx;
+		}
+		.right#{$i}{
+			left: auto;
+			right: $i *  2upx;
+		}
+		.top-#{$i}p{
+			bottom: auto;
+			top: $i * -1%;
+		}
+		.left-#{$i}p{
+			right: auto;
+			left: $i * -1%;
+		}
+		.bottom-#{$i}p{
+			top: auto;
+			bottom: $i * -1%;
+		}
+		.right-#{$i}p{
+			left: auto;
+			right: $i * -1%;
+		}
+		.top#{$i}p{
+			bottom: auto;
+			top: $i * 1%;
+		}
+		.left#{$i}p{
+			right: auto;
+			left: $i * 1%;
+		}
+		.bottom#{$i}p{
+			top: auto;
+			bottom: $i * 1%;
+		}
+		.right#{$i}p{
+			left: auto;
+			right: $i * 1%;
+		}
+	}
+	
+	.fixed{
+		position: fixed;
+	}
+	
+	/* fix-auto布局 */
+	.fixAuto,.fixAutoNoPd,.fixAutoSmPd{
+		display: table;
+		width: 100%;
+		padding: 20upx 10upx;
+		>.item,>view,>image,>navigator,>text,>button{
+			vertical-align: top;
+			padding: 0 10upx;
+			display: table-cell ;
+		}
+		&.middle{
+			>.item,>view,>image,>navigator,>text{
+				vertical-align: middle;
+			}
+		}
+		&.bottom{
+			>.item,>view,>image,>navigator,>text{
+				vertical-align: bottom;
+			}
+		}
+	}
+	.fixAutoSmPd{
+		padding: 0;
+		>.item,>view,>image,>navigator,>text{
+			padding-right: 0;
+			&:first-child{
+				padding-left: 0;
+				padding-right: 10upx;
+			}
+		}
+	}
+	.fixAutoNoPd{
+		padding: 0;
+		>.item,>view,>image,>navigator,>text{
+			padding: 0;
+		}
+	}
+	
+	/* 浮动组件 */
+	.clear{
+		&:after{
+			content: "";
+			clear: both;
+			height: 0;
+			display: block;
+			visibility: hidden;
+		}
+	}
+	.fl{
+		float: left;
+	}
+	.fr{
+		float: right;
+	}
+	
+	/* 按钮样式类 */
+	.btn,.roundBtn{
+		cursor: pointer;
+		display: inline-block;
+		text-align: center;
+		padding: 8upx 24upx;
+		background-color: $main;
+		color: $mainInverse ;
+		font-size: 28upx;
+		border: 1px solid $main;
+		-webkit-border-radius: 8upx;
+		-moz-border-radius: 8upx;
+		border-radius: 8upx;
+		transition: 0.4s;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		&.gradualBg{
+			border-color: transparent;
+			background-image: $mainGradual;
+			color: $mainGradualInverse;
+			
+		}
+		&.blackBg{
+			background-color: $black;
+			border-color: $black;
+			color: #fff;
+			&.shadow{
+				box-shadow: 0px 2px 9px -1px rgba($black , 0.4);
+			}
+		}
+		&.greenBg{
+			background-color: $green;
+			border-color: $green;
+			color: #fff;
+			&.shadow{
+				box-shadow: 0px 2px 9px -1px rgba($green , 0.4);
+			}
+		}
+		&.grayBg{
+			border-color: rgba(#30302f,0.2);
+			background-color: #f7f7f7;
+			color: #30302f;
+			&.shadow{
+				box-shadow: 0px 2px 9px -1px rgba( #30302f , 0.2);
+			}
+		}
+		&.whiteBg{
+			border-color: rgba(#fff,0.2);
+			background-color: #fff;
+			color: #000;
+		}
+		
+		&.orangeBg{
+			border-color: $orange;
+			background-color: $orange;
+			color: #fff;
+			&.shadow{
+				box-shadow: 0px 2px 9px -1px rgba( $orange , 0.4);
+			}
+		}
+		&.redBg{
+			border-color: $red;
+			background-color: $red;
+			color: #fff;
+			&.shadow{
+				box-shadow: 0px 2px 9px -1px rgba( $red , 0.4);
+			}
+		}
+		&.yellowBg{
+			border-color: $yellow;
+			background-color: $yellow;
+			color: #000;
+			&.shadow{
+				box-shadow: 0px 2px 9px -1px rgba( $yellow , 0.4);
+			}
+		}
+		
+		&.line{
+			background-color: transparent;
+			background-image: none;
+			color: $main;
+			&.blackBg{
+				color: $black;
+			}
+			&.greenBg{
+				color: $green;
+			}
+			&.yellowBg{
+				color: $yellow;
+			}
+			&.grayBg{
+				color: #30302f;
+			}
+			&.whiteBg{
+				border-color:  rgba(#fff,0.7);
+				color: #fff;
+			}
+			&.orangeBg{
+				color: $orange;
+			}
+			&.redBg{
+				color: $red;
+			}
+		}
+		&+.btn,&+.roundBtn{
+			margin-left: 20upx;
+		}
+		&.block{
+		   margin: 0;
+		   padding: 20upx 24upx;
+		   display: block;
+		   width: 100%;
+		   &+.btn{
+			   margin-left: 0;
+		   }
+		}
+		&:hover{
+			-webkit-transform: scale(0.99);
+			-moz-transform: scale(0.99);
+			-ms-transform: scale(0.99);
+			-o-transform: scale(0.99);
+			transform: scale(0.99);
+			opacity: 0.8;
+		}
+		&.disabled{
+			-webkit-transform: scale(1);
+			-moz-transform: scale(1);
+			-ms-transform: scale(1);
+			-o-transform: scale(1);
+			transform: scale(1);
+			opacity: 0.4;
+			cursor: not-allowed;
+		}
+	}
+	
+	[class^="tag"] , [class*=" tag"]{
+		display: inline-block;
+		font-size: 24upx;
+		padding: 4upx 14upx;
+		border-radius: 4upx;
+		margin-right: 6upx;
+		margin-left: 6upx;
+	}
+	.tag{
+		background-color: rgba($main,0.2);
+		color: $main;
+	}
+	.tagBlue{
+		background-color: rgba($blue,0.2);
+		color: $blue;
+	}
+	.tagGray{
+		background-color: rgba($gray,0.2);
+		color: $gray;
+	}
+	
+	.tagGradual{
+		background-image: linear-gradient(to top right,rgba($main,0.2),rgba($main,0.1));
+		color: $main;
+	}
+	
+	.tagBlack{
+		background-color: rgba($black,0.2);
+		color: $black;
+	}
+	.tagGreen{
+		background-color: rgba($green,0.2);
+		color: $green;
+	}
+	
+	.tagWhite{
+		background-color: rgba($white,0.2);
+		color: $white;
+	}
+	
+	.tagOrange{
+		background-color: rgba($orange,0.2);
+		color: $orange;
+	}
+	.tagRed{
+		background-color: rgba($red,0.2);
+		color: $red;
+	}
+	.tagYellow{
+		background-color: rgba($yellow,0.2);
+		color: $yellow;
+	}
+	
+	/* 边线(实线、虚线) */
+	.bdn{
+		border: none;
+	}
+	.bd{
+		border: 1px solid $borderColor;
+		&.dashed{
+			border-style: dashed;
+		}
+	}
+	.bt{
+		border-top:1px solid $borderColor;
+		&.dashed{
+			border-top-style: dashed;
+		}
+	}
+	.bb{
+		border-bottom:1px solid $borderColor;
+		&.dashed{
+			border-bottom-style: dashed;
+		}
+	}
+	.bl{
+		border-left:1px solid $borderColor;
+		&.dashed{
+			border-left-style: dashed;
+		}
+	}
+	.br{
+		border-right: 1px solid $borderColor;
+		&.dashed{
+			border-right-style: dashed;
+		}
+	}
+	
+	$b:2;
+	@while $b <= 10 {
+		.bd#{$b}{
+			border: #{$b}px solid $borderColor;
+			&.dashed{
+				border-style: dashed;
+			}
+		}
+		.bt#{$b}{
+			border-top:#{$b}px solid $borderColor;
+			&.dashed{
+				border-top-style: dashed;
+			}
+		}
+		.bb#{$b}{
+			border-bottom:#{$b}px solid $borderColor;
+			&.dashed{
+				border-bottom-style: dashed;
+			}
+		}
+		.bl#{$b}{
+			border-left:#{$b}px solid $borderColor;
+			&.dashed{
+				border-left-style: dashed;
+			}
+		}
+		.br#{$b}{
+			border-right: #{$b}px solid $borderColor;
+			&.dashed{
+				border-right-style: dashed;
+			}
+		}
+		$b : $b + 1 ;
+	}
+	
+	/* 边线颜色 */
+	.mainBd{
+		border-color: $main;
+	}
+	.greenBd{
+		border-color:  $green;
+	}
+	.redBd{
+		border-color: $red;
+	}
+	.yellowBd{
+		border-color:$yellow ;
+	}
+	.blackBd{
+		border-color: $black;
+	}
+	.whiteBd{
+		border-color:$white ;
+	}
+	.grayBd{
+		border-color:$gray;
+	}
+	.greyBd{
+		border-color:$grey;
+	}
+	.orangeBd{
+		border-color:$orange;
+	}
+	
+	/* 圆角 */
+	.radius,.rds{
+		-webkit-border-radius: 100%!important;
+		-moz-border-radius: 100%!important;
+		border-radius: 100%!important;
+	}
+	
+	$r:0;
+	@while $r <= 50{
+		.rds#{$r},&.radius#{$r}{
+			-webkit-border-radius:$r*2upx!important;
+			-moz-border-radius:$r*2upx!important;
+			border-radius:$r*2upx!important;
+		}
+		$r : $r + 1;
+	}
+	
+	.rdsTl,.radiusTopLeft{
+		border-top-left-radius:100%!important;
+	}
+	.rdsTr,.radiusTopRight{
+		border-top-right-radius: 100%!important;
+	}
+	.rdsBl,.radiusBottomLeft{
+		border-bottom-left-radius: 100%!important;
+	}
+	.rdsBr,.radiusBottomRight{
+		border-bottom-right-radius: 100%!important;
+	}
+	
+	$r:0;
+	@while $r <= 50{
+		.rdsTl#{$r},.radiusTopLeft#{$r}{
+			border-top-left-radius: $r*2upx!important;
+		}
+		.rdsTr#{$r},.radiusTopRight#{$r}{
+			border-top-right-radius: $r*2upx!important;
+		}
+		.rdsBl#{$r},.radiusBottomLeft#{$r}{
+			border-bottom-left-radius: $r*2upx!important;
+		}
+		.rdsBr#{$r},.radiusBottomRight#{$r}{
+			border-bottom-right-radius: $r*2upx!important;
+		}
+		$r : $r + 1;
+	}
+	
+	/* 正方形&长方形 */
+	[class^="square"] , [class*=" square"]{
+		width: 100%;
+		position: relative;
+		height: auto;
+		>.item,>image,>view,>navigator,>text,>button{
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+		}
+	}
+	
+	$p : 200 ;
+	@while $p > 0 {
+		.square#{$p}{
+			padding-top: $p*1%;
+		}
+		@if $p == 100 {
+			.square{
+				padding-top: 100%;
+			}
+		}
+		$p : $p - 5 ;
+	}
+	
+	
+	
+	/* 阴影 */
+	.shadow{
+		box-shadow: 0px 2px 9px -1px rgba(0, 0, 0, 0.1);
+	}
+	
+	/* 遮罩层 */
+	.wrapper-top,.wt{
+		background-image: linear-gradient(rgba(0,0,0,0.3) , rgba(0,0,0,0.02));
+	}
+	.wrapper-bottom,.wb{
+		background-image: linear-gradient( rgba(0,0,0,0.02) , rgba(0,0,0,0.3) );
+	}
+	
+	[class^="wp"],[class*=" wp"] {
+		z-index: 10;
+	}
+	
+	/* 透明度 */
+	@for $op from 0 through 10 {
+		.op#{$op}{
+			opacity: $op * 0.1!important;
+		}
+		.wp#{$op}{
+			background-color: rgba(#000,$op*0.1);
+		}
+		@if $op == 5 {
+			.wp{
+				background-color: rgba(#000,0.5);
+			}
+		}
+	}
+	
+	/* 分割线 */
+	[class*=" split"],[class^="split"] {
+		position: relative;
+		&:before{
+			content:"";
+			display: block;
+			position: absolute;
+			left: 0;
+			top: 50%;
+			border-left: 1px solid $borderColor;
+		}
+	}
+	
+	$s:10;
+	@while $s <= 100 {
+		.split#{$s}{
+			&:before{
+				height: #{$s*2}upx;
+				margin-top: -#{$s}upx;
+			}
+		}
+		@if $s == 10 {
+			.split{
+				&:before{
+					height: 20upx;
+					margin-top: -10upx;
+				}
+			}
+		}
+		$s:$s+2;
+	}
+	
+	.hover,[class^="hover"],[class*=" hover"]{
+		transition: all 0.4s;
+		cursor: pointer;
+		&:hover{
+			opacity: 0.8 !important;
+		}
+	}
+	
+	
+	
+	.statusBar{
+		height: var(--status-bar-height);
+	}
+	
+	.winBottom{
+		height: var(--windown-bottom);
+	}
+	
+	.safeBottom{
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+	
+	.disabled{
+		opacity:0.8;
+		cursor: not-allowed;
+	}
+	
+	
+	
+	.grid,.gridNoMb,.gridNoPd{
+		>.btn,>.roundBtn{
+			&+.btn,&+.roundBtn{
+				margin-left: 0 ;
+			}
+		}
+	}
+	
+	.roundBtn{
+		-webkit-border-radius: 100upx;
+		-moz-border-radius: 100upx;
+		border-radius: 100upx;
+	} 
+	 
+	 
+	 
+	 /* 位置 */
+	 .text-center,.tc{
+	 	text-align: center!important;
+	 }
+	 .text-left,.tl{
+	 	text-align: left!important;
+	 }
+	 .text-right,.tr{
+	 	text-align: right!important;
+	 }
+	 .text-justify,.tj{
+	 	text-align: justify!important;
+	 }
+	 .text-bold,.bold{
+	 	font-weight: bold!important;
+	 }
+	 .text-normal,.normal{
+	 	font-weight: normal!important;
+	 }
+	 .break{
+	 	white-space: normal;
+	 	word-break: break-all;
+	 }
+	 .noBreak{
+	 	white-space: nowrap;
+	 	word-break: keep-all;
+	 }
+	 .inline{
+	 	display: inline-block;
+	 }
+	 .block{
+	 	display: block;
+	 	width: 100%;
+	 }
+	 .none{
+	 	display: none;
+	 }
+	 .center-block{
+	 	margin: 0 auto;
+	 	display: block;
+	 }
+	 .hidden{
+	 	overflow: hidden;
+	 }
+	 .hiddenX{
+	 	overflow-x: hidden;
+	 }
+	 .hiddenY{
+	 	overflow-y: hidden;
+	 }
+	 .auto{
+	 	overflow: auto;
+	 }
+	 .autoX{
+	 	overflow-x: auto;
+	 }
+	 .autoY{
+	 	overflow-y: auto;
+	 }
+	 .showInMb{
+		 display: block;
+	 }
+	 .showInPc{
+		 display: none;
+	 }
+	 table{
+	 	width: 100%;
+	 	border-collapse: collapse;
+	 	border-spacing: 0;
+	 	border: 1px solid #e6e6e6;
+	 	thead{
+	 		tr{
+	 			background-color: #f2f2f2;
+	 			th{
+	 				color: #8799a3;
+	 				width: 1%;
+	 			}
+	 		}
+	 	}
+	 	tr{
+	 		background-color: #fff;
+	 		transition: all 0.4s;
+	 		td,th{
+	 			border: 1px solid #e6e6e6;
+	 			overflow: hidden;
+	 			-o-text-overflow: ellipsis;
+	 			text-overflow: ellipsis;
+	 			white-space: nowrap;
+	 			word-wrap: break-word;
+	 			padding: 5px 10px;
+	 			height: 28px;
+	 			line-height: 28px;
+	 			&.autoWidth{
+	 				width: auto;
+	 			}
+	 		}
+	 		&:hover{
+	 			background-color: #f2f2f2;
+	 		}
+	 	}
+	 }
+</style>

+ 14 - 0
yudao-vue-ui/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 21 - 0
yudao-vue-ui/main.js

@@ -0,0 +1,21 @@
+import App from './App'
+
+// #ifndef VUE3
+import Vue from 'vue'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+    ...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+  const app = createSSRApp(App)
+  return {
+    app
+  }
+}
+// #endif

+ 72 - 0
yudao-vue-ui/manifest.json

@@ -0,0 +1,72 @@
+{
+    "name" : "ruoyi-vue-ui",
+    "appid" : "__UNI__764D04C",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "2"
+}

+ 43 - 0
yudao-vue-ui/pages.json

@@ -0,0 +1,43 @@
+{
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "uni-app"
+			}
+		}, {
+			"path": "pages/tabbar/user",
+			"style": {
+				"navigationBarTitleText": "",
+				"enablePullDownRefresh": false
+			}
+		}
+	],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "uni-app",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	},
+	"tabBar": {
+		"color": "#666",
+		"selectedColor": "#FF5A5F",
+		"borderStyle": "black",
+		"list": [{
+			"text": "首页",
+			"pagePath": "pages/index/index",
+			"iconPath": "static/tarbar/index.png",
+			"selectedIconPath": "static/tarbar/index-active.png"
+		}, {
+			"text": "商品",
+			"pagePath": "pages/product/list",
+			"iconPath": "static/tarbar/product.png",
+			"selectedIconPath": "static/tarbar/product-active.png"
+		}, {
+			"text": "我的",
+			"pagePath": "pages/tabbar/user",
+			"iconPath": "static/tarbar/ucenter.png",
+			"selectedIconPath": "static/tarbar/ucenter-active.png"
+		}]
+	}
+}

+ 52 - 0
yudao-vue-ui/pages/index/index.vue

@@ -0,0 +1,52 @@
+<template>
+	<view class="content">
+		<image class="logo" src="/static/logo.png"></image>
+		<view class="text-area">
+			<text class="title">{{title}}</text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				title: 'Hello'
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style>
+	.content {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.logo {
+		height: 200rpx;
+		width: 200rpx;
+		margin-top: 200rpx;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 50rpx;
+	}
+
+	.text-area {
+		display: flex;
+		justify-content: center;
+	}
+
+	.title {
+		font-size: 36rpx;
+		color: #8f8f94;
+	}
+</style>

+ 257 - 0
yudao-vue-ui/pages/tabbar/user.vue

@@ -0,0 +1,257 @@
+<template>
+	<view class="app">
+		<!-- 个人信息 -->
+		<view class="user-wrapper">
+			<image class="user-background" src="/static/backgroud/user.jpg"></image>
+			<view class="user">
+				<!-- 头像 -->
+				<image class="avatar" :src="userInfo.avatar || '/static/icon/default-avatar.png'" @click="navTo('/pages/set/userInfo', {login: true})"></image>
+				<!-- 已登陆,展示昵称 -->
+				<view class="cen column" v-if="hasLogin">
+					<text class="username f-m">{{ userInfo.nickname }}</text>
+					<text class="group">普通会员</text>
+				</view>
+				<!-- 未登陆,引导登陆 -->
+				<view class="login-box" v-else @click="navTo('/pages/auth/login')">
+					<text>点击注册/登录</text>
+				</view>
+			</view>
+			<!-- 下面的圆弧 -->
+			<image class="user-background-arc-line" src="/static/icon/arc.png" mode="aspectFill"></image>
+		</view>
+		
+		<!-- 订单信息 -->
+		<view class="order-wrap">
+			<view class="order-header row" @click="navTo('/pages/order/list?current=0', {login: true})">
+				<text class="title">我的订单</text>
+				<text class="more">查看全部</text>
+				<text class="mix-icon icon-you"></text>
+			</view>
+			<view class="order-list">
+				<view class="item center" @click="navTo('/pages/order/list?current=1', {login: true})"  hover-class="hover-gray" :hover-stay-time="50">
+					<text class="mix-icon icon-daifukuan"></text>
+					<text>待付款</text>
+					<text v-if="orderCount.c0 > 0" class="number">{{ orderCount.c0 }}</text>
+				</view>
+				<view class="item center" @click="navTo('/pages/order/list?current=2', {login: true})" hover-class="hover-gray" :hover-stay-time="50">
+					<text class="mix-icon icon-daifahuo"></text>
+					<text>待发货</text>
+					<text v-if="orderCount.c1 > 0" class="number">{{ orderCount.c1 }}</text>
+				</view>
+				<view class="item center" @click="navTo('/pages/order/list?current=3', {login: true})" hover-class="hover-gray" :hover-stay-time="50">
+					<text class="mix-icon icon-yishouhuo"></text>
+					<text>待收货</text>
+					<text v-if="orderCount.c2 > 0" class="number">{{ orderCount.c2 }}</text>
+				</view>
+				<view class="item center" @click="navTo('/pages/order/list?current=4', {login: true})" hover-class="hover-gray" :hover-stay-time="50">
+					<text class="mix-icon icon-daipingjia"></text>
+					<text>待评价</text>
+					<text v-if="orderCount.c3 > 0" class="number">{{ orderCount.c3 }}</text>
+				</view>
+			</view>
+		</view>
+		
+		<!-- 功能入口 -->
+		<view class="option-wrap">
+			<mix-list-cell icon="icon-iconfontweixin" iconColor="#fa436a" title="我的钱包" @onClick="navTo('/pages/wallet/index', {login: true})"></mix-list-cell>
+			<mix-list-cell icon="icon-dizhi" iconColor="#5fcda2" title="地址管理" @onClick="navTo('/pages/address/list', {login: true})"></mix-list-cell>
+			<!-- <mix-list-cell icon="icon-share" iconColor="#9789f7" title="分享" tips="呼朋唤友赢好礼"></mix-list-cell> -->
+			<mix-list-cell icon="icon-shoucang_xuanzhongzhuangtai" iconColor="#54b4ef" title="我的收藏" @onClick="navTo('/pages/favorite/favorite', {login: true})"></mix-list-cell>
+			<mix-list-cell icon="icon-pinglun-copy" iconColor="#ee883b" title="意见反馈" @onClick="navTo('/pages/feedback/feedback', {login: true})"></mix-list-cell>
+			<mix-list-cell icon="icon-shezhi1" iconColor="#37b0fb" title="设置" border="" @onClick="navTo('/pages/set/set', {login: true})"></mix-list-cell>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				userInfo: { // TODO 芋艿:读取
+					nickname: '芋艿'
+				},
+				hasLogin: true, // TODO 芋艿:读取
+				orderCount: { // TODO 芋艿:读取
+					c0: 1,
+					c1: 2,
+					c2: 3,
+					c3: 4
+				}
+			};
+		},
+		onShow() {
+			// TODO 芋艿:加载用户信息
+			// TODO 芋艿:获得订单数量
+		}
+	}
+</script>
+
+<style lang="scss">
+	.app {
+		padding-bottom: 20rpx;
+	}
+	.user-wrapper {
+		position: relative;
+		overflow: hidden;
+		padding-top: calc(var(--status-bar-height) + 52rpx);	
+		padding-bottom: 6rpx;
+		.user {
+			display: flex;
+			flex-direction: column;
+			flex-direction: row;
+			align-items: center;
+			position: relative;
+			z-index: 5;
+			padding: 20rpx 30rpx 60rpx;
+			.avatar {
+				flex-shrink: 0;
+				width: 130rpx;
+				height: 130rpx;
+				border-radius: 100px;
+				margin-right: 24rpx;
+				border: 4rpx solid #fff;
+				background-color: #fff;
+			}
+			.username {
+				font-size: 34rpx;
+				color: #fff;
+			}
+			.group {
+				align-self: flex-start;
+				padding: 10rpx 14rpx;
+				margin: 16rpx 10rpx; // 10rpx 避免距离昵称太近
+				font-size: 20rpx;
+				color: #fff;
+				background-color: rgba(255, 255, 255,.3);
+				border-radius: 100rpx;
+			}
+			.login-box {
+				font-size: 36rpx;
+				color: #fff;
+			}
+		}
+		.user-background {
+			position: absolute;
+			left: 0;
+			top: 0;
+			width: 100%;
+			height: 330rpx;
+		}
+		.user-background-arc-line {
+			position: absolute;
+			left: 0;
+			bottom: 0;
+			z-index: 9;
+			width: 100%;
+			height: 32rpx;
+		}
+	}
+
+	.order-wrap {
+		width: 700rpx;
+		margin: 20rpx auto 0;
+		background: #fff;
+		border-radius: 10rpx;
+		.order-header{
+			padding: 28rpx 20rpx 6rpx 26rpx;
+			.title {
+				flex: 1;
+				font-size: 32rpx;
+				color: #333;
+				font-weight: 700;
+			}
+			.more {
+				font-size: 24rpx;
+				color: #999;
+			}
+			.icon-you {
+				margin-left: 4rpx;
+				font-size: 20rpx;
+				color: #999;
+			}
+		}
+		.order-list {
+			display:flex;
+			justify-content: space-around;
+			padding: 20rpx 0;
+			.item{
+				flex-direction: column;
+				width: 130rpx;
+				height: 130rpx;
+				border-radius: 8rpx;
+				font-size: 24rpx;
+				color: #606266;
+				position: relative;
+				.mix-icon {
+					font-size: 50rpx;
+					margin-bottom: 20rpx;
+					color: #fa436a;
+				}
+				.icon-shouhoutuikuan {
+					font-size: 44rpx;
+				}
+				.number {
+					position: absolute;
+					right: 22rpx;
+					top: 6rpx;
+					min-width: 34rpx;
+					height: 34rpx;
+					line-height: 30rpx;
+					text-align: center;
+					padding: 0 8rpx;
+					font-size: 18rpx;
+					color: #fff;
+					border: 2rpx solid #fff;
+					background-color: $base-color;
+					border-radius: 100rpx;
+				}
+			}
+		}
+	}
+
+	.option-wrap{
+		width: 700rpx;
+		margin: 20rpx auto 0;
+		margin-top: 20rpx;
+		background: #fff;
+		border-radius:10rpx;
+		
+		.sec-header{
+			padding: 26rpx 14rpx 0 24rpx;
+			font-size: 28rpx;
+			color: #333;
+			
+			.icon-lishijilu{
+				font-size: 46rpx;
+				color: #50bf8b;
+				margin-right: 16rpx;
+				line-height: 40rpx;
+			}
+			.icon-lajitong{
+				padding: 4rpx 10rpx;
+				font-size: 36rpx;
+				color: #999;
+			}
+		}
+		.pro-list{
+			flex-wrap: nowrap;
+			padding: 20rpx 0 12rpx;
+			
+			&:before, &:after{
+				content: '';
+				min-width: 30rpx;
+				height: 30rpx;
+			}
+			&:after{
+				min-width: 20rpx;
+			}
+			image{
+				flex-shrink: 0;
+				width: 144rpx;
+				height: 144rpx;
+				margin-right: 16rpx;
+				border-radius: 8rpx;
+			}
+		}
+	}
+</style>

BIN
yudao-vue-ui/static/backgroud/user.jpg


BIN
yudao-vue-ui/static/icon/arc.png


BIN
yudao-vue-ui/static/icon/default-avatar.png


BIN
yudao-vue-ui/static/logo.png


BIN
yudao-vue-ui/static/tarbar/index-active.png


BIN
yudao-vue-ui/static/tarbar/index.png


BIN
yudao-vue-ui/static/tarbar/logo.png


BIN
yudao-vue-ui/static/tarbar/product-active.png


BIN
yudao-vue-ui/static/tarbar/product.png


BIN
yudao-vue-ui/static/tarbar/ucenter-active.png


BIN
yudao-vue-ui/static/tarbar/ucenter.png


+ 78 - 0
yudao-vue-ui/uni.scss

@@ -0,0 +1,78 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$base-color: #ff536f;
+
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;

Some files were not shown because too many files changed in this diff