gcz 2 år sedan
incheckning
e6d34f096f
100 ändrade filer med 9813 tillägg och 0 borttagningar
  1. 15 0
      .gitignore
  2. 42 0
      App.vue
  3. 184 0
      center/addcredits.vue
  4. 141 0
      center/center.vue
  5. 191 0
      center/minuscredits.vue
  6. 157 0
      center/paycode.vue
  7. 173 0
      center/resetpass.vue
  8. 159 0
      common/apiurl.js
  9. 27 0
      common/config.js
  10. 29 0
      common/http.api.js
  11. 86 0
      common/http.interceptor.js
  12. 123 0
      common/request.js
  13. 411 0
      components/ay-qrcode/ay-qrcode.vue
  14. 872 0
      components/ay-qrcode/qrcode_wx.js
  15. 263 0
      components/ay-qrcode/weapp-qrcode.js
  16. 78 0
      components/cartfixed.vue
  17. 127 0
      components/tabbar.vue
  18. 129 0
      examination/answersheet.vue
  19. 30 0
      examination/complete.vue
  20. 339 0
      examination/examination.vue
  21. 143 0
      examination/question.vue
  22. 106 0
      examination/report.vue
  23. 83 0
      examination/typelist.vue
  24. 20 0
      index.html
  25. 82 0
      main.js
  26. 100 0
      manifest.json
  27. 18 0
      mixin.js
  28. 24 0
      package.json
  29. 204 0
      pages.json
  30. 38 0
      pages/about/about.vue
  31. 157 0
      pages/index/index.vue
  32. 183 0
      pages/login/login.vue
  33. 134 0
      pages/news/newsdetails.vue
  34. 61 0
      pages/regulation/regulation.vue
  35. 211 0
      shopping/order.vue
  36. 163 0
      shopping/orderdetails.vue
  37. 88 0
      shopping/paysuccess.vue
  38. 299 0
      shopping/productdetails.vue
  39. 293 0
      shopping/supermarket.vue
  40. 82 0
      static/css/common.scss
  41. 71 0
      static/css/flex.scss
  42. BIN
      static/img/center-bg.png
  43. BIN
      static/img/center-code.png
  44. BIN
      static/img/center-icon-1.png
  45. BIN
      static/img/center-icon-2.png
  46. BIN
      static/img/center-icon-3.png
  47. BIN
      static/img/client-bg.png
  48. BIN
      static/img/client-ico.png
  49. BIN
      static/img/defaultavatar.png
  50. BIN
      static/img/empty.png
  51. BIN
      static/img/icon-user.png
  52. BIN
      static/img/icon-warning.png
  53. BIN
      static/img/index-nav-1.png
  54. BIN
      static/img/index-nav-2.png
  55. BIN
      static/img/index-nav-3.png
  56. BIN
      static/img/index-nav-4.png
  57. BIN
      static/img/integral-ico-y.png
  58. BIN
      static/img/integral-ico.png
  59. BIN
      static/img/order-banner.png
  60. BIN
      static/img/report-bg.png
  61. BIN
      static/img/report-ico.png
  62. BIN
      static/img/supermarket-top.png
  63. BIN
      static/img/tabbar-gift-gray.png
  64. BIN
      static/img/tabbar-gift.png
  65. BIN
      static/img/tabbar-home-gray.png
  66. BIN
      static/img/tabbar-home.png
  67. BIN
      static/img/tabbar-my-gray.png
  68. BIN
      static/img/tabbar-my.png
  69. BIN
      static/img/type-default-ico.png
  70. BIN
      static/img/typelist-bg.png
  71. BIN
      static/logo.png
  72. BIN
      static/qrlogo.png
  73. 29 0
      store/$u.mixin.js
  74. 87 0
      store/index.js
  75. 233 0
      study/studydetails.vue
  76. 184 0
      study/studylist.vue
  77. 24 0
      template.h5.html
  78. 286 0
      uni.scss
  79. 6 0
      uni_modules/mescroll-uni/changelog.md
  80. 19 0
      uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.css
  81. 400 0
      uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.vue
  82. 47 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.css
  83. 39 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.vue
  84. 360 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-body.vue
  85. 49 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni-option.js
  86. 437 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni.vue
  87. 44 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.css
  88. 53 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.vue
  89. 32 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.css
  90. 40 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.vue
  91. 380 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-body.vue
  92. 64 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni-option.js
  93. 462 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni.vue
  94. 116 0
      uni_modules/mescroll-uni/components/mescroll-empty/mescroll-empty.vue
  95. 55 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.css
  96. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.vue
  97. 83 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-top.vue
  98. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.css
  99. 39 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.vue
  100. 15 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-i18n.js

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+.DS_Store
+
+unpackage/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.hbuilderx
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 42 - 0
App.vue

@@ -0,0 +1,42 @@
+<script>
+	export default {
+		globalData: {
+		    statusBarHeight: 0, // 状态导航栏高度
+		    navHeight: 0, // 总体高度
+		    navigationBarHeight: 0, // 导航栏高度(标题栏高度)
+		},
+		onLaunch: function() {
+			// console.log('App Launch')
+			 // 状态栏高度
+			this.globalData.statusBarHeight = uni.getSystemInfoSync().statusBarHeight
+		 
+			// #ifdef MP-WEIXIN
+			// 获取微信胶囊的位置信息 width,height,top,right,left,bottom
+			const custom = wx.getMenuButtonBoundingClientRect()
+			// console.log(custom)
+		 
+			// 导航栏高度(标题栏高度) = 胶囊高度 + (顶部距离 - 状态栏高度) * 2
+			this.globalData.navigationBarHeight = custom.height + (custom.top - this.globalData.statusBarHeight) * 2
+			// console.log("导航栏高度:"+this.globalData.navigationBarHeight)
+		 
+			// 总体高度 = 状态栏高度 + 导航栏高度
+			this.globalData.navHeight = this.globalData.navigationBarHeight + this.globalData.statusBarHeight
+		 
+			// #endif
+		 
+			// console.log('this.globalData==========',this.globalData)
+		},
+		onShow: function() {
+			// console.log('App Show')
+		},
+		onHide: function() {
+			// console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="scss">
+	/*每个页面公共css */
+	@import "@/uni_modules/uview-ui/index.scss";
+	@import "@/static/css/common.scss";
+</style>

+ 184 - 0
center/addcredits.vue

@@ -0,0 +1,184 @@
+<template>
+	<view class="">
+	<!-- 	<u-navbar
+			title="我的业绩"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<view class="tabs-wrap">
+			<u-tabs 
+			:list="tabsList" 
+			lineColor="#009AEF" 
+			 :activeStyle="{color:'#333','font-weight': '600','font-size':'30rpx'}"
+			 :inactiveStyle="{color:'#999'}"
+			@click="tabsClick"></u-tabs>
+		</view>
+		<!-- <view class="totalAmount">
+			合计:{{totalAmount}}
+		</view> -->
+		<mescroll-body class="" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view class="page-wrap" v-show="dataList.length>0" >
+				<view class="list">
+					<view class="item u-flex u-row-between" v-for="item in dataList" :key="item.id">
+						<view class="left">
+							<view class="name">{{item.title}}</view>
+							<view class="time">{{item.createTime}}</view>
+						</view>
+						<text class="num">+{{item.credit}}</text>
+					</view>
+				</view>
+			</view>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	// 引入mescroll-mixins.js
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		data() {
+			return {
+				totalAmount:'',
+				downOption: {},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				tabsList:[{name:'全部',recordType:''},{name:'答题',recordType:1},{name:'安全学习',recordType:3}],
+				recordType:'',
+				params:{
+				},
+				activeIndex:0,
+				dataList: []
+			}
+		},
+		onShow() {
+			
+		},
+		onLoad() {
+			this.params.recordType = this.tabsList[this.activeIndex].recordType;
+			// console.log('1111', this.tabsList[this.activeIndex]);
+		},
+		methods: {
+			/*下拉刷新的回调, 重置列表为第一页 (此处可删,mixins已默认)
+			downCallback(){
+				this.mescroll.resetUpScroll();
+			},
+			/*上拉加载的回调*/
+			upCallback(page) {
+				// 此处可以继续请求其他接口
+				// if(page.num == 1){
+				// 	// 请求其他接口...
+				// }
+			
+				// 如果希望先请求其他接口,再触发upCallback,可参考以下写法
+				// if(!this.params.id){
+				// 	this.mescroll.endErr()
+				// 	return // 此处return,先获取xx
+				// }
+			
+				let pageNum = page.num; // 页码, 默认从1开始
+				let pageSize = page.size; // 页长, 默认每页10条isAsc:0//时间排序 0:降序 1:升序 (默认星级降序排序)
+
+				this.params = Object.assign(this.params,{pageNum:pageNum,pageSize:pageSize});
+				this.$u.api.creditLog(this.params).then(data => {
+					console.log('data',JSON.parse(JSON.stringify(data)));
+					// 接口返回的当前页数据列表 (数组)
+					let curPageData = data.data.rows;
+					this.totalAmount = data.data.total;
+					console.log('curPageData',JSON.parse(JSON.stringify(curPageData)));
+					// 接口返回的当前页数据长度 (如列表有26个数据,当前页返回8个,则curPageLen=8)
+					let curPageLen = curPageData.length; 
+					// 接口返回的总页数 (如列表有26个数据,每页10条,共3页; 则totalPage=3)
+					// let totalPage =  data.data.data.totalPage; 
+					// 接口返回的总数据量(如列表有26个数据,每页10条,共3页; 则totalSize=26)
+					let totalSize = curPageData.total; 
+					// 接口返回的是否有下一页 (true/false)
+					// let hasNext = data.xxx; 
+					// console.log('totalPage',totalPage,'curPageLen',curPageLen);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动置空列表
+					this.dataList = this.dataList.concat(curPageData); //追加新数据
+					// 请求成功,隐藏加载状态
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					// this.mescroll.endByPage(curPageLen, totalPage); 
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					this.mescroll.endBySize(curPageLen, totalSize); 
+				}).catch(err => {
+					this.mescroll.endErr()
+					console.log(err)
+				});	
+			
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList() {
+				this.mescroll.resetUpScroll();
+			},
+			tabsClick(item){
+				this.params.recordType = item.recordType;
+				this.reloadList()
+				console.log('item',item);
+			},
+		}
+	}
+</script>
+<style>
+page{
+	background-color: #F5F9FC;
+}
+</style>
+<style lang="scss" scoped>
+.totalAmount{
+	padding: 20rpx;
+	background-color: #fff;
+	border-radius: 8rpx;
+	margin: 0 0 20rpx;
+	font-size: 36rpx;
+	font-weight: 600;
+	color: #00A447;
+}
+.tabs-wrap{
+	background-color: #fff;
+	margin-bottom: 10rpx;
+}
+.list{
+	border-radius: 8rpx;
+	padding: 0 20rpx;
+	background-color: #fff;
+	.item{
+		padding: 28rpx 0;
+		background-color: #fff;
+		.name{
+			font-size: 30rpx;
+			font-weight: 400;
+			color: #333333;
+			line-height: 42rpx;
+			margin-bottom: 8rpx;
+		}
+		.time{
+			font-size: 24rpx;
+			font-weight: 400;
+			color: #999999;
+			line-height: 33rpx;
+		}
+		.num{
+			font-size: 30rpx;
+			font-weight: 600;
+			color: #009AEF;
+		}
+		&:not(:last-of-type){
+			border-bottom: 0.5px solid #ddd;
+			padding-bottom: 20rpx;
+		}
+	}
+}
+</style>

+ 141 - 0
center/center.vue

@@ -0,0 +1,141 @@
+<template>
+	<view class="pages">
+		<!-- <u-navbar
+			title="我的"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<view class="top u-flex">
+			<view class="userinfo u-flex">
+				<img class="defaultavatar" src="../static/img/defaultavatar.png" alt="">
+				<view class="text">
+					<view class="name">{{vuex_user_info.username}}</view>
+					<view class="dept">
+						<text>{{vuex_member_info.dept}}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="page-wrap">
+			<view class="tool-wrap">
+				<u-cell-group :border="false">
+					<u-cell
+					    title="积分获取记录"
+					    value=""
+					    center
+						:isLink="true"
+						:border="true"
+						icon="/static/img/center-icon-1.png"
+						url="/center/addcredits"
+						:customStyle="cellCustomStyle"
+					></u-cell>
+					<u-cell
+					    title="积分兑换记录"
+					    value=""
+					    center
+						:isLink="true"
+						:border="true"
+						icon="/static/img/center-icon-2.png"
+						url="/center/minuscredits"
+						:customStyle="cellCustomStyle"
+					></u-cell>
+				    <u-cell
+				        title="修改密码"
+				        value=""
+				        center
+						:isLink="true"
+						:border="true"
+						icon="/static/img/center-icon-3.png"
+						url="/center/resetpass"
+						:customStyle="cellCustomStyle"
+				    ></u-cell>
+				</u-cell-group>
+			</view>
+		</view>
+		<tabbar :tabbarIndexProps='2' />
+	</view>
+</template>
+
+<script>
+	import { systemInfo } from "@/mixin.js";
+	import tabbar from "../components/tabbar.vue";
+	export default {
+		components:{
+			tabbar
+		},
+		mixins:[systemInfo],
+		data() {
+			return {
+				staticUrl:this.$commonConfig.staticUrl,
+				memberInfo:{},
+				cellCustomStyle:{
+					'background-color':'#fff',
+					'border-radius':'8rpx',
+					'margin-bottom':'14rpx',
+				}
+			}
+		},
+		onShow() {
+			this.getMemberInfo()
+		},
+		onLoad() {
+			this.getSystemInfo();
+		},
+		methods: {
+			getMemberInfo(){
+				this.$u.api.memberInfo({id:this.vuex_member_info.id}).then(res=>{
+					this.memberInfo = res.data;
+					this.$u.vuex('vuex_member_info', res.data);
+					}).catch(err=>{
+					console.log('memberInfo',err.data);
+				})
+			},
+		}
+	}
+</script>
+<style>
+page{
+	background-color: #fff;
+}
+</style>
+<style lang="scss" scoped>
+.top{
+	color: #fff;
+	height: 280rpx;
+	background: url(../static/img/center-bg.png) no-repeat;
+	position: relative;
+	padding: 50rpx 30rpx;
+	box-sizing: border-box;
+	align-items: end;
+	.userinfo{
+		text-align: left;
+		.defaultavatar{
+			width: 110rpx;
+			height: 110rpx;
+		}
+		.text{
+			margin-left: 32rpx;
+			font-size: 26rpx;
+			font-family: PingFangSC-Regular, PingFang SC;
+			font-weight: 400;
+			color: #FFFFFF;
+			line-height: 36rpx;
+			.name{
+				font-size: 34rpx;
+				font-weight: 600;
+				color: #FFFFFF;
+				line-height: 48rpx;
+				margin-bottom: 8rpx;
+			}
+		}
+	}
+}
+.tool-wrap{
+	margin-top: 20rpx;
+	/deep/ .u-cell__body{
+		padding-left: 0;
+	}
+}
+</style>

+ 191 - 0
center/minuscredits.vue

@@ -0,0 +1,191 @@
+<template>
+	<view class="">
+	<!-- 	<u-navbar
+			title="我的业绩"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<view class="tabs-wrap">
+			<u-tabs 
+			:list="tabsList" 
+			lineColor="#009AEF" 
+			 :activeStyle="{color:'#333','font-weight': '600','font-size':'30rpx'}"
+			 :inactiveStyle="{color:'#999'}"
+			@click="tabsClick"></u-tabs>
+		</view>
+		<!-- <view class="totalAmount">
+			合计:{{totalAmount}}
+		</view> -->
+		<mescroll-body class="" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view class="page-wrap" v-show="dataList.length>0" >
+				<view class="list">
+					<view class="item u-flex u-row-between" @click="goDetail(item)" v-for="item in dataList" :key="item.id">
+						<view class="left">
+							<view class="name">{{item.title}}</view>
+							<view class="time">{{item.createTime}}</view>
+						</view>
+						<text class="num">-{{item.credit}}</text>
+					</view>
+				</view>
+			</view>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	// 引入mescroll-mixins.js
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		data() {
+			return {
+				totalAmount:'',
+				downOption: {},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				tabsList:[{name:'兑换商品',recordType:2}],
+				recordType:'',
+				params:{
+				},
+				activeIndex:0,
+				dataList: []
+			}
+		},
+		onShow() {
+			
+		},
+		onLoad() {
+			this.params.recordType = this.tabsList[this.activeIndex].recordType;
+			// console.log('1111', this.tabsList[this.activeIndex]);
+		},
+		methods: {
+			/*下拉刷新的回调, 重置列表为第一页 (此处可删,mixins已默认)
+			downCallback(){
+				this.mescroll.resetUpScroll();
+			},
+			/*上拉加载的回调*/
+			upCallback(page) {
+				// 此处可以继续请求其他接口
+				// if(page.num == 1){
+				// 	// 请求其他接口...
+				// }
+			
+				// 如果希望先请求其他接口,再触发upCallback,可参考以下写法
+				// if(!this.params.id){
+				// 	this.mescroll.endErr()
+				// 	return // 此处return,先获取xx
+				// }
+			
+				let pageNum = page.num; // 页码, 默认从1开始
+				let pageSize = page.size; // 页长, 默认每页10条isAsc:0//时间排序 0:降序 1:升序 (默认星级降序排序)
+
+				this.params = Object.assign(this.params,{pageNum:pageNum,pageSize:pageSize});
+				this.$u.api.creditLog(this.params).then(data => {
+					console.log('data',JSON.parse(JSON.stringify(data)));
+					// 接口返回的当前页数据列表 (数组)
+					let curPageData = data.data.rows;
+					this.totalAmount = data.data.total;
+					console.log('curPageData',JSON.parse(JSON.stringify(curPageData)));
+					// 接口返回的当前页数据长度 (如列表有26个数据,当前页返回8个,则curPageLen=8)
+					let curPageLen = curPageData.length; 
+					// 接口返回的总页数 (如列表有26个数据,每页10条,共3页; 则totalPage=3)
+					// let totalPage =  data.data.data.totalPage; 
+					// 接口返回的总数据量(如列表有26个数据,每页10条,共3页; 则totalSize=26)
+					let totalSize = curPageData.total; 
+					// 接口返回的是否有下一页 (true/false)
+					// let hasNext = data.xxx; 
+					// console.log('totalPage',totalPage,'curPageLen',curPageLen);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动置空列表
+					this.dataList = this.dataList.concat(curPageData); //追加新数据
+					// 请求成功,隐藏加载状态
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					// this.mescroll.endByPage(curPageLen, totalPage); 
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					this.mescroll.endBySize(curPageLen, totalSize); 
+				}).catch(err => {
+					this.mescroll.endErr()
+					console.log(err)
+				});	
+			
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList() {
+				this.mescroll.resetUpScroll();
+			},
+			tabsClick(item){
+				this.params.recordType = item.recordType;
+				this.reloadList()
+				console.log('item',item);
+			},
+			goDetail(item){
+				// console.log('goDetail',item);
+				if(item.recordContent!='兑换商品'){return}
+				uni.$u.route('/shopping/orderdetails', {
+					id: item.relationId,
+				});
+			}
+		}
+	}
+</script>
+<style>
+page{
+	background-color: #F5F9FC;
+}
+</style>
+<style lang="scss" scoped>
+.totalAmount{
+	padding: 20rpx;
+	background-color: #fff;
+	border-radius: 8rpx;
+	margin: 0 0 20rpx;
+	font-size: 36rpx;
+	font-weight: 600;
+	color: #00A447;
+}
+.tabs-wrap{
+	background-color: #fff;
+	margin-bottom: 10rpx;
+}
+.list{
+	border-radius: 8rpx;
+	padding: 0 20rpx;
+	background-color: #fff;
+	.item{
+		padding: 28rpx 0;
+		background-color: #fff;
+		.name{
+			font-size: 30rpx;
+			font-weight: 400;
+			color: #333333;
+			line-height: 42rpx;
+			margin-bottom: 8rpx;
+		}
+		.time{
+			font-size: 24rpx;
+			font-weight: 400;
+			color: #999999;
+			line-height: 33rpx;
+		}
+		.num{
+			font-size: 30rpx;
+			font-weight: 600;
+			color: #009AEF;
+		}
+		&:not(:last-of-type){
+			border-bottom: 0.5px solid #ddd;
+			padding-bottom: 20rpx;
+		}
+	}
+}
+</style>

+ 157 - 0
center/paycode.vue

@@ -0,0 +1,157 @@
+<template>
+	<view class="pages">
+		<u-navbar
+			title="兑换码"
+			:placeholder="true"
+			:autoBack="true"
+			@leftClick="leftClick"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar>
+
+		<view class="avatar-wrap u-flex">
+			<u-avatar :src="vuex_member_info.avatar||'/static/img/defaultavatar.png'" size="138rpx"></u-avatar>
+		</view>
+		<view class="name">{{vuex_member_info.name}}</view>
+		<view class="ayQrcode">
+			<view class="code">
+				数字码:{{code}}
+			</view>
+			<ayQrcode ref="qrcode" :modal="modal_qr" :url="qrContent" @hideQrcode="hideQrcode" :height="200" :width="200" />
+		</view>
+
+	</view>
+</template>
+
+<script>
+	import ayQrcode from "@/components/ay-qrcode/ay-qrcode.vue"
+	export default {
+		components:{
+			ayQrcode
+		},
+		data() {
+			return {
+				code:'',
+				staticUrl:this.$commonConfig.staticUrl,
+				modal_qr: false,
+				qrContent: {}, // 要生成的二维码值
+				params:{
+					
+				},
+				timer: null,
+			}
+		},
+		onShow() {
+		},
+		onLoad(page) {
+			this.code = page.qrContent;
+			this.qrContent.qrcode = page.qrContent;
+			this.qrContent = JSON.stringify(this.qrContent);
+			let that = this;
+			that.showQrcode();//一加载生成二维码
+			// this.timer = setInterval(() => {
+			//   this.refreshCode()
+			// }, 30000);
+		},
+		onUnload() {
+		    // 页面离开时停止计时器
+		    // clearInterval(this.timer)
+		},
+		methods: {
+			leftClick(){
+				 // uni.reLaunch({url: `/pages/index/index`});
+			},
+			// 展示二维码
+			showQrcode() {
+				let _this = this;
+				this.modal_qr = true;
+				// uni.showLoading()
+				setTimeout(function() {
+					// uni.hideLoading()
+					_this.$refs.qrcode.crtQrCode()
+				}, 50)
+			},
+
+			//传入组件的方法
+			hideQrcode() {
+				this.modal_qr = false;
+			},
+			refreshCode(){
+				this.qrContent = {};
+				this.qrContent.qrcode = this.code;
+				this.qrContent = JSON.stringify(this.qrContent);
+				this.showQrcode()
+			}
+		}
+	}
+</script>
+<style>
+	page{
+		background-color: #009AEF;
+	}
+</style>
+<style lang="scss" scoped>
+
+.avatar-wrap{
+	margin-top: 42rpx;
+	margin-bottom: 8rpx;
+	justify-content: center;
+}
+.name{
+	font-size: 34rpx;
+	font-family: PingFangSC-Semibold, PingFang SC;
+	font-weight: 600;
+	color: #FFFFFF;
+	line-height: 48rpx;
+	text-align: center;
+	margin-bottom: 60rpx;
+}
+.ayQrcode{
+	position: relative;
+	background-color: #fff;
+	margin: 40rpx 60rpx 20rpx;
+	padding: 40rpx 40rpx 100rpx;
+	border-radius: 20rpx;
+	text-align: center;
+	.code{
+		padding-bottom: 38rpx;
+		border-bottom: 1px dashed #ddd;
+		margin-bottom: 90rpx;
+		font-size: 30rpx;
+		font-family: PingFangSC-Regular, PingFang SC;
+		font-weight: 400;
+		color: #666666;
+		line-height: 42rpx;
+	}
+	.show-qrcode{
+		overflow: hidden;
+	}
+	&:before,&:after{
+		content: '';
+		width: 40rpx;
+		height: 40rpx;
+		background-color: #009AEF;
+		position: absolute;
+		top: 100rpx;
+		border-radius: 50%;
+	}
+	&:before{
+		left: -20rpx;		
+	}
+	&:after{
+		right: -20rpx;
+	}
+}
+.tip{
+	margin-top: 40rpx;
+	font-size: 24rpx;
+	font-weight: 400;
+	color: #999999;
+	line-height: 33rpx;
+	.refresh-ico{
+		width: 27rpx;
+		height: 27rpx;
+		margin-right: 10rpx;
+	}
+}
+</style>

+ 173 - 0
center/resetpass.vue

@@ -0,0 +1,173 @@
+<template>
+	<view class="pages">
+		<u-navbar
+			title="修改密码"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar>
+		<view class="page-wrap">
+			<u--form labelPosition="left" :model="form"  :rules="rules" ref="uForm" >
+				<u-form-item label="" prop="oldPassword" :borderBottom="false" ref="oldPassword" >
+					<u--input
+						v-model="form.oldPassword"
+						border="none"
+						placeholder="请输入旧密码"
+						:customStyle="inputCustomStyle"
+					></u--input>
+				</u-form-item>
+				<u-form-item label="" prop="password" :borderBottom="false" ref="password" >
+					<u--input
+						v-model="form.password"
+						border="none"
+						:password="true"
+						placeholder="请输入新密码"
+						:customStyle="inputCustomStyle"
+					></u--input>
+				</u-form-item>
+				<u-form-item label="" prop="comfpassword" :borderBottom="false" ref="comfpassword" >
+					<u--input
+						v-model="form.comfpassword"
+						border="none"
+						:password="true"
+						placeholder="确认密码"
+						:customStyle="inputCustomStyle"
+					></u--input>
+				</u-form-item>
+			</u--form>
+			<u-button
+				@click="submit"
+				text="确认" 
+				type="primary" 
+				shape="circle" 
+				:customStyle="{'margin-top':'60rpx',height:'98rpx','box-sizing':'border-box'}"
+				>
+			</u-button>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		
+		data() {
+			return {
+				form:{
+					oldPassword:'',
+					password:'',
+					comfpassword:'',
+				},
+				
+				rules: {
+					oldPassword: {
+						type: 'string',
+						required: true,
+						message: '请输入旧密码',
+						trigger: ['blur', 'change']
+					},
+					password: [
+						{
+							type: 'string',
+							required: true,
+							message: '请输入新密码',
+							trigger: ['blur', 'change']
+						},
+						{
+							min: 6,
+							max: 10,
+							message: '长度在6-10个字符之间',
+							trigger: 'blur',
+						},
+					],
+					comfpassword: [
+						{
+							type: 'string',
+							required: true,
+							message: '请确认密码',
+							trigger: ['blur', 'change']
+						},
+						{
+							validator: (rule, value, callback) => {
+								if (value === '') {
+									callback(new Error('请再次输入密码'));
+								} else if (value !== this.form.password) {
+									callback(new Error('两次输入密码不一致!'));
+								} else {
+									callback();
+								}
+							},
+							trigger: 'blur',
+						}
+					],
+				},
+				inputCustomStyle:{
+					// height:'98rpx',
+					'border-color':'#eee',
+					'background-color': '#F5F9FC',
+					'padding-left':'30rpx',
+					'box-sizing':'border-box',
+				}
+			}
+		},
+		onShow() {
+		},
+		onReady() {
+			this.$refs.uForm.setRules(this.rules)
+		},
+		onLoad() {
+
+		},
+		methods: {
+			chekPass(){
+				console.log('chekPass---',this.form);
+				return new Promise((resolve, reject)=>{
+					if(!password){
+						 reject('needAuth');
+					}
+				})
+			},
+			async submit(){
+				// console.log('form',this.form);
+				this.$refs.uForm.validate().then(res => {
+					// let chekPassResult = await this.chekPass();
+					// return
+					// uni.$u.toast('校验通过')
+					const param = {
+						oldPassword:this.form.oldPassword,
+						password:this.form.password
+					}
+					this.$u.api.updatePassword(param).then(res=>{
+						// console.log('res',res.data);
+						this.$u.vuex('vuex_user_info', res.data);
+						uni.showToast({
+							title:res.msg,
+							icon:'success'
+						})
+						setTimeout(()=>{
+							uni.$u.route('/pages/login/login');
+						},2000)
+						// uni.reLaunch({url: this.backUrl});
+					}).catch(err=>{
+						console.log('login',err);
+					})
+				}).catch(errors => {
+					uni.$u.toast('请正确填写表单')
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.page-wrap{
+	.u-form-item{
+		margin-bottom: 40rpx;
+	}
+	/deep/ .u-form-item__body{
+		background-color:#F5F9FC;
+		border-radius: 16rpx;
+	}
+}
+
+</style>

+ 159 - 0
common/apiurl.js

@@ -0,0 +1,159 @@
+/*
+	接口统一管理
+*/
+const apiurl = {
+	// 用户注册登陆
+	login: {
+		url: '/auth/accountLogin',
+		type: 'post'
+	},
+	//登录用户信息
+	memberInfo: {
+		url: '/memberInfo/getInfoById',
+		type: 'get'
+	},
+	// 重新登录
+	reLogin: {
+		url: '/auth/login',
+		type: 'post'
+	},
+	// 修改密码
+	updatePassword: {
+		url: '/memberInfo/updatePassword',
+		type: 'put'
+	},
+	// 修改用户信息
+	updateMemberInfo: {
+		url: '/memberInfo/update',
+		type: 'put'
+	},
+	//关于我们
+	getCompanyInfo: {
+		url: '/companyInfo/getCompanyInfo',
+		type: 'get'
+	},
+	//代理人端首页信息
+	indexData: {
+		url: '/agentInfo/index',
+		type: 'get'
+	},
+	//轮播图
+	swiperList: {
+		url: '/advList/pageList',
+		type: 'get'
+	},
+	//轮播图详情
+	swiperDetails: {
+		url: '/advList/selectById',
+		type: 'get'
+	},
+	//积分记录
+	creditLog: {
+		url: '/memberCreditLog/pageList',
+		type: 'get'
+	},
+	//安全学习列表分类
+	getclassify: {
+		url: '/classifyInfo/pageList',
+		type: 'get'
+	},
+	//安全学习列表
+	studyLib: {
+		url: '/studyLib/pageList',
+		type: 'get'
+	},
+	//课程详情
+	courseDetails: {
+		url: '/studyLib/selectById',
+		type: 'get'
+	},
+	//添加阅读量
+	addViewCount: {
+		url: '/studyLib/addViewCount',
+		type: 'put',
+		custom:{
+			noload:true
+		}
+	},
+	// 试卷做题
+	//获取分类下试卷
+	findPager: {
+		url: '/paperInfo/findPager',
+		type: 'get',
+	},
+	//获取试卷下题目
+	findPagerTopic: {
+		url: '/paperInfo/findPagerTopic',
+		type: 'get'
+	},
+	//试卷提交
+	paperSubmit: {
+		url: '/paperInfo/paperSubmit',
+		type: 'put'
+	},
+	//获取答题卡
+	findPagerResult: {
+		url: '/paperInfo/findPagerResult',
+		type: 'get'
+	},
+	//答题卡解析详情
+	findAnswerResult: {
+		url: '/paperInfo/findAnswerResult',
+		type: 'get'
+	},
+	// 云超市
+	//商品分类
+	goodsTypeList: {
+		url: '/goods/pageClassList',
+		type: 'get'
+	},
+	//商品列表
+	memberGoodList: {
+		url: '/goods/pageList',
+		type: 'get'
+	},
+	//商品列表
+	memberGoodDetails: {
+		url: '/goods/selectById',
+		type: 'get'
+	},
+	//兑换商品
+	exchangeGoods: {
+		url: '/goods/exchangeGoods',
+		type: 'post'
+	},
+	//奖品兑换列表
+	memberExchangeRecord: {
+		url: '/memberExchangeRecord/pageList',
+		type: 'get'
+	},
+	//兑换详情
+	exchangeDetails: {
+		url: '/memberExchangeRecord/selectById',
+		type: 'get'
+	},
+	//查询登录用户积分
+	memberCredit: {
+		url: '/memberCreditLog/getMemberCredit',
+		type: 'get'
+	},
+	//规则
+	memberCreditDesc: {
+		url: '/memberCreditDesc/getByName',
+		type: 'get'
+	},
+}
+
+
+/*
+* 特殊处理接口
+*/ 
+const otherApiUrl = {
+	// 文件上传
+	uploadFile: '/file/upload/single/minio',
+}
+
+export {
+	apiurl,
+	otherApiUrl
+}

+ 27 - 0
common/config.js

@@ -0,0 +1,27 @@
+/**
+ * 配置通用 
+ */
+const node_dev = process.env.H_NODE_ENV;
+//运行到浏览器用的
+let baseUrl='/api';
+let upFileUrl='https://fileupload.hw.hongweisoft.com/upload/single/minio';
+
+//打包用的
+if (node_dev) {
+ baseUrl = process.env.H_BASE_URL;
+ upFileUrl = process.env.H_UP_FILE_URL;
+}
+
+const commonConfig = {
+	wxAppid: '', // 测试wxAppid
+	baseUrl: baseUrl, // 服务器地址
+	uploadFileUrl: upFileUrl, // 上传文件路径
+	paginationConfig:{
+		  pageNum: 1,
+		  pageSize: 10
+	},//分页参数
+	successCode:200,//接口返回状态
+}
+export {
+	commonConfig
+}

+ 29 - 0
common/http.api.js

@@ -0,0 +1,29 @@
+import {
+	apiurl
+} from "./apiurl.js"
+
+// 此处第二个参数vm,就是我们在页面使用的this,你可以通过vm获取vuex等操作,更多内容详见uView对拦截器的介绍部分:
+const install = (Vue, vm) => {
+
+	let httpMap = {}
+	const http = uni.$u.http
+	
+	// 循环请求路径对象生成对应的方式请求
+	Object.keys(apiurl).forEach((key) => {
+		if(apiurl[key]?.type=='get'){
+			httpMap[key] = (data = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url, {params:data}, config);
+		}else{
+			httpMap[key] = (params = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url, params, config);
+		}
+	    
+	});
+	
+	// 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
+	vm.$u.api = {
+		...httpMap
+	};
+}
+
+export default {
+	install
+}

+ 86 - 0
common/http.interceptor.js

@@ -0,0 +1,86 @@
+import { commonConfig } from '@/common/config.js';
+import store  from '../store/index.js'
+import {againToken}  from '../utils/leaderToken.js'
+// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token
+// 同时,我们也可以在此使用getApp().globalData,如果你把token放在getApp().globalData的话,也是可以使用的
+const install = (Vue, vm) => {
+	Vue.prototype.$u.http.setConfig({
+		baseUrl: commonConfig.baseUrl,
+		timeout: 40000,
+		// 如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data
+		// 设置为true后,就需要在this.$u.http.interceptor.response进行多一次的判断,请打印查看具体值
+		// originalData: true, 
+		// 设置自定义头部content-type
+		// header: {
+		// 	'content-type': 'xxx'
+		// }
+	});
+	// 请求拦截,配置Token等参数
+	Vue.prototype.$u.http.interceptor.request = (config) => {
+		// config.header.Token = 'xxxxxx';
+		// 方式一,存放在vuex的token,假设使用了uView封装的vuex方式,见:https://uviewui.com/components/globalVariable.html
+		// config.header.token = vm.vuex_token;
+		if(vm.vuex_user.accessToken){config.header.Authorization = `Bearer ${vm.vuex_user.accessToken}`;}
+		if(vm.vuex_user.userId){config.header['user_id'] = `${vm.vuex_user.userId}`;}
+		// 方式二,如果没有使用uView封装的vuex方法,那么需要使用$store.state获取
+		// config.header.token = vm.$store.state.token;
+		
+		// 方式三,如果token放在了globalData,通过getApp().globalData获取
+		// config.header.token = getApp().globalData.username;
+		
+		// 方式四,如果token放在了Storage本地存储中,拦截是每次请求都执行的,所以哪怕您重新登录修改了Storage,下一次的请求将会是最新值
+		// const token = uni.getStorageSync('token');
+		// config.header.token = token;
+		// url加时间戳
+		if(config.url.indexOf('?') > -1){
+		    config.url = config.url + '&t=' + Date.now()	
+		} else {
+			config.url = config.url + '?t=' + Date.now()	
+		}
+		// 此url参数为this.$u.get(url)中的url值
+		let noTokenList = ['/wechat/h5/user','/client/auth/verifyCode'];
+		if(noTokenList.includes(config.url)) config.header.noToken = true;
+		// console.log('noTokenList.includes(config.url)',noTokenList.includes(config.url));
+		// console.log('config.url',config.url);
+		return config; 
+	}
+	// 响应拦截,判断状态码是否通过
+	Vue.prototype.$u.http.interceptor.response = (res) => {
+		// 如果把originalData设置为了true,这里得到将会是服务器返回的所有的原始数据
+		// 判断可能变成了res.statueCode,或者res.data.code之类的,请打印查看结果
+		// console.log('interceptor res',res);
+		if(res.code == 200) {
+			// 如果把originalData设置为了true,这里return回什么,this.$u.post的then回调中就会得到什么
+			return res;  
+		} else if(res.msg == "令牌不能为空" || res.code == 401){
+			if(vm.vuex_user.userId) {
+				againToken(vm.vuex_user.userId)
+			} else {
+				const backUrl = location.href
+				const loginUrl = 'phoneLogin'
+				if (backUrl.indexOf(loginUrl) > 0) {
+					localStorage.clear()
+				} else {
+					// uni.showToast({
+					// 	title: res.msg + "即将跳转到登录页",
+					// 	icon: "none",
+					// 	duration: 2000
+					// });
+					localStorage.setItem('backUrl', location.href)
+					// alert('还未登录,即将跳转登录');
+					setTimeout(() => {
+						uni.navigateTo({
+							url: "/pages/login/login"
+						})
+					}, 1000)
+				}
+			}
+		
+			
+		}else return res;
+	}
+}
+
+export default {
+	install
+}

+ 123 - 0
common/request.js

@@ -0,0 +1,123 @@
+import { commonConfig } from '@/common/config.js';
+import {againToken}  from '../utils/againToken.js'
+import { showFullScreenLoading , tryHideFullScreenLoading } from '../utils/loading.js'
+// 此vm参数为页面的实例,可以通过它引用vuex中的变量
+module.exports = (vm) => {
+    // 初始化请求配置
+    uni.$u.http.setConfig((config) => {
+        /* config 为默认全局配置*/
+        config.baseURL = commonConfig.baseUrl; /* 根域名 */
+        return config
+    })
+	
+	// 请求拦截
+	uni.$u.http.interceptors.request.use((config) => { // 可使用async await 做异步操作
+	// console.log('config========',config);
+	    // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{}
+	    config.data = config.data || {}
+		// 根据custom参数中配置的是否需要token,添加对应的请求头
+		if(!config?.custom?.auth) {
+			// 可以在此通过vm引用vuex中的变量,具体值在vm.$store.state中
+			// config.header.token = vm.vuex_user_info.token;
+			config.header.Authorization = `Bearer ${vm.vuex_user_info.token}`;
+		}
+		if(!config?.custom?.noload){
+			showFullScreenLoading()
+		}
+	    return config 
+	}, config => { // 可使用async await 做异步操作
+	    return Promise.reject(config)
+	})
+	
+	let unlogin = function(){
+		if(vm.vuex_user_info.userid&&vm.vuex_wechatOpenid) {
+			againToken(vm.$u,vm.vuex_wechatOpenid,vm.vuex_user_info.userid)
+		} else {
+			let pages = getCurrentPages();
+			// console.log('pages',pages);
+			let backUrl = pages[pages.length - 1].route;
+			let options =uni.$u.queryParams( pages[pages.length - 1].options);
+			let fullBackUrl = backUrl+options;
+			const loginUrl = 'login'
+			if (backUrl.indexOf(loginUrl) > 0) {
+				uni.removeStorage({
+					key: 'backUrl',
+					success: function (res) {
+						// console.log('success');
+					}
+				});
+			} else {
+				// uni.showToast({
+				// 	title: res.msg + "即将跳转到登录页",
+				// 	icon: "none",
+				// 	duration: 2000
+				// });
+				uni.setStorage({
+					key: 'backUrl',
+					data: fullBackUrl,
+					success: function () {
+						// console.log('setStorage success');
+					}
+				});
+				setTimeout(() => {
+					uni.$u.route('/pages/login/login');
+				}, 1000)
+			}
+		}
+	}
+	
+	// 响应拦截
+	uni.$u.http.interceptors.response.use((response) => {/* 对响应成功做点什么 可使用async await 做异步操作*/
+		// console.log('response====response',response);
+		const data = response.data
+
+		// 自定义参数
+		const custom = response.config?.custom
+		if (data.code !== 200) {
+			// console.log('data====',data);
+			// 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示
+			if (custom.toast !== false) {
+				const unshowmsg = ['令牌不能为空'];
+				if (!unshowmsg.includes(data.msg)) {
+				  uni.$u.toast(data.msg)
+				}
+			}
+			if(data.msg == "令牌验证失败" || data.msg == "令牌不能为空" || data.code == 401){
+				unlogin()
+			}
+			
+			if(data.msg == "用户不存在!"||data.msg == "用户未注册"){
+				uni.clearStorage();
+				unlogin()
+			}
+			
+			return Promise.reject(data)
+			// 如果需要catch返回,则进行reject
+			// if (custom?.catch) {
+			// 	return Promise.reject(data)
+			// } else {
+			// 	// 否则返回一个pending中的promise,请求不会进入catch中
+			// 	return new Promise(() => { })
+			// }
+		}
+		// console.log('data--',data);
+		tryHideFullScreenLoading()
+		return data === undefined ? {} : data
+	}, (response) => {
+		tryHideFullScreenLoading()
+		// console.log('response==',response);
+		const data = response.data;
+		// console.log('data==',data);
+		// 对响应错误做点什么 (statusCode !== 200)
+		let errMap = {
+			'404':'接口不存在'
+		};
+		if (response.statusCode in errMap) {
+		  uni.$u.toast(errMap[response.statusCode])
+		}
+		if(data.msg == "令牌不能为空" || data.code == 401){
+			unlogin()
+		}
+		return Promise.reject(response)
+	})
+}

+ 411 - 0
components/ay-qrcode/ay-qrcode.vue

@@ -0,0 +1,411 @@
+<template>
+	<view :class="modal?'show-qrcode':'hide-qrcode'">
+		<view class="box-qrcode" :style="{'margin-left':  marginLeft + 'px'}" @longtap="longtapCode">
+			<!-- style="width: 550rpx;height: 550rpx;" -->
+			
+			<canvas class="canvas-qrcode" :style="style_w_h" :canvas-id="qrcode_id">
+				
+				<!-- #ifndef MP -->
+				<view v-if="modal&&is_themeImg" :style="style_w_h" class="box-img-qrcode">
+					<image :style="style_w_h_img" mode="scaleToFill" :src="themeImg"></image>
+				</view>
+				<!-- #endif -->
+				
+			</canvas>
+			
+			<!-- <image mode="scaleToFill" :src="imagePath"></image> -->
+			
+		</view>
+	</view>
+</template>
+
+<script>
+	var qr_we = require("./qrcode_wx.js");
+	const qrCode = require('./weapp-qrcode.js')
+	export default {
+		data() {
+			return {
+				isAndroid : false ,
+				show: true,
+				imagePath: '',
+				// qrcode_id: 'qrcode_id',
+				marginLeft: 0,
+				//一般的安卓app只需加30就能显示全
+				//苹果app的不加就能显示全,加了就要弄margin-left
+				//有些安卓app显示不全
+				add_num : 30 ,
+				add_num_key : 'rectify_code_key',
+			}
+		},
+		props: {
+			modal: {
+				type: Boolean,
+				default: false
+			},
+			url: {
+				type: String,
+				default: ''
+			},
+			height: {
+				type: Number,
+				default: 260
+			},
+			width: {
+				type: Number,
+				default: 260
+			},
+			themeColor: {
+				type: String,
+				default: '#333333',
+			},
+			qrcode_id: {
+				type: String,
+				default: 'qrcode_id',
+			},
+			is_themeImg: {
+				type: Boolean,
+				default: false,
+			},
+			themeImg: {
+				type: String,
+				default: 'https://cdn.pixabay.com/photo/2016/11/29/13/24/balloons-1869816__340.jpg',
+			},
+			h_w_img: {
+				type: Number,
+				default: 30
+			},
+			
+			
+		},
+		watch:{
+			
+		},
+		computed: {
+			style_w_h() {
+				return this.set_style_w_h();
+			},
+			style_w_h_img() {
+				let that = this;
+				var height = parseInt(that.h_w_img);
+				var width = parseInt(that.h_w_img);
+				var style = '';
+				if (height > 0) {
+					style = `height:${height*2}rpx;`;
+				}
+				if (width > 0) {
+					style += `width:${width*2}rpx;z-index: 2;`;
+				}
+			
+				return style;
+			},
+		},
+		created: function() {
+			let that = this;
+			try {
+				//app苹果二维码不居中
+				//#ifndef MP
+				let isAndroid = false ;
+			    const res = uni.getSystemInfoSync();
+			    if(res.platform == 'android'){
+					isAndroid = true ;
+				}else{
+					isAndroid = false ;
+				}
+				
+				
+				if (!isAndroid) {
+					that.marginLeft = 46;
+				}
+				
+				that.isAndroid = isAndroid ;
+				try {
+					const add_num = uni.getStorageSync(that.add_num_key);
+					if (add_num) {
+						that.add_num = add_num;
+					}
+					
+				} catch (e) {
+					// error
+				
+				}
+				// #endif
+
+			} catch (e) {
+			    // error
+			}
+
+			//#ifdef MP
+			//that.marginLeft = 40;
+			// #endif
+
+		},
+		methods: {
+			set_style_w_h(){
+				
+				let that = this;
+				var height = parseInt(that.height);
+				var width = parseInt(that.width);
+				var style = '';
+				var height = height*2 ;
+				var width = width*2 ;
+				
+				//#ifndef MP
+				var add = that.add_num ;
+				
+				height +=  add;
+				width +=  add;
+				// #endif
+				
+				if (height > 0) {
+					style = `height:${height}rpx;`;
+				}
+				if (width > 0) {
+					style += `width:${width}rpx;`;
+				}
+				
+				return style;
+			},
+			hideQrcode() {
+				this.$emit("hideQrcode")
+			},
+			// 二维码生成工具
+			crtQrCode() {
+				let that = this;
+				//#ifndef MP
+				new qrCode(that.qrcode_id, {
+					text: this.url,
+					width: that.width,
+					height: that.height,
+					colorDark: that.themeColor,//#333333
+					colorLight: "#FFFFFF",
+					correctLevel: qrCode.CorrectLevel.H,
+				})
+				// #endif
+				//#ifdef MP
+				that.createQrCode(this.url, that.qrcode_id, that.width, that.height,that.themeColor,that.is_themeImg,that.themeImg,that.h_w_img);
+				// #endif
+
+				//that.createQrCode(this.url, that.qrcode_id, that.width, that.height);
+			},
+			//#ifdef MP
+
+			createQrCode: function(url, canvasId, cavW, cavH,cavColor,haveImg,imgurl,imgsize) {
+				//调用插件中的draw方法,绘制二维码图片
+				qr_we.api.draw(url, canvasId, cavW, cavH,cavColor,haveImg,imgurl,imgsize, this, this.canvasToTempImage);
+				// setTimeout(() => { this.canvasToTempImage();},100);
+
+			},
+			
+			// #endif
+			//获取临时缓存照片路径,存入data中
+			canvasToTempImage: function() {
+				var that = this;
+			},
+			saveImage: function() {
+				var that = this;
+				uni.canvasToTempFilePath({
+					canvasId: that.qrcode_id,
+					success: function(res) {
+						var tempFilePath = res.tempFilePath;
+						console.log(tempFilePath);
+						that.imagePath = tempFilePath;
+						
+						//保存到相册
+						// uni.saveFile({
+						//       tempFilePath: tempFilePath,
+						//       success: function (res2) {
+						//         var savedFilePath = res2.savedFilePath;
+								
+								
+						//       }
+						// });
+						uni.saveImageToPhotosAlbum({
+							filePath : tempFilePath ,
+							success: function (res3) {
+								uni.showModal({
+									title: '提示',
+									content: '保存成功',
+									confirmText: '确定',
+									showCancel: false,
+									confirmColor: '#33CCCC',
+									success(res4) {
+										
+									}
+								}) 
+							},
+						});
+					},
+					fail: function(res) {
+						console.log(res);
+					}
+				}, that);
+			},
+			//微信小程序支持:长按二维码,提示是否保存相册
+			//安卓APP长按校正二维码
+			longtapCode(){
+				var that = this;
+				
+				//#ifndef MP
+				uni.showModal({
+					title: '校正二维码',
+					content: '二维码是否异常',
+					confirmText: '确定',
+					confirmColor: '#33CCCC',
+					success(res) {
+						if (res.confirm) {
+							that.rectify_code();
+						}
+					}
+				})
+				// #endif
+				
+				//#ifdef MP-WEIXIN
+				uni.showModal({
+					title: '提示',
+					content: '是否保存到相册',
+					confirmText: '确定',
+					confirmColor: '#33CCCC',
+					success(res) {
+						if (res.confirm) {
+							that.saveImage();
+						}
+					}
+				})
+				// #endif
+			},
+			//安卓有些手机不正常,长按可选择矫正
+			rectify_code(){
+				var that = this;
+				let add_num = that.add_num ;
+				add_num += 30 ;
+				that.add_num = add_num;
+				that.crtQrCode();//重新生成才会立即覆盖
+				try {
+					//第一次长按校正设置了就不用在设置
+					uni.setStorage({
+						key: that.add_num_key,
+						data: add_num,
+						success: function() {
+							
+						}
+					});
+				} catch (e) {
+					// error
+				
+				}
+			},
+		},
+		mounted() {}
+	}
+</script>
+
+<style scoped lang="scss">
+	// .qrcode-box {
+	// 	position: fixed;
+	// 	left: 0;
+	// 	top: 0;
+	// 	right: 0;
+	// 	bottom: 0;
+	// 	height: 100vh;
+	// 	width: 100vw;
+	// 	background-color: rgba(59, 59, 59, 0.6);
+	// 	// opacity: 0.8;
+	// 	text-align: center;
+	// 	display: flex;
+	// 	align-items: center;
+	// 	display: none;
+
+	// 	.qrcode-item {
+	// 		flex: 1;
+	// 		position: relative;
+	// 		text-align: center;
+
+	// 		.item-box {
+	// 			width: 90%;
+	// 			margin: auto;
+	// 			display: inline-block;
+	// 			margin-top: 30%;
+	// 			padding-bottom: 30rpx;
+
+	// 			// animation: show 0.7s;
+	// 			.title {
+	// 				font-size: 46rpx;
+	// 				text-align: center;
+	// 				margin-bottom: 24rpx;
+	// 			}
+
+	// 			.canvas {
+	// 				margin: auto;
+	// 				display: inline-block;
+	// 				margin: auto;
+	// 			}
+
+	// 			background-color: #FFFFFF;
+	// 		}
+
+	// 	}
+	// }
+	.box-qrcode{
+		text-align: center;
+		position: relative;
+		.box-img-qrcode{
+			position: absolute;
+			display: flex;
+			flex-direction: column;
+			justify-content: center;
+			align-items: center;
+			z-index: 2;
+		}
+	}
+	image{
+		width: 60upx;
+		height: 60upx;
+		border-radius: 50%;
+		
+	}
+	.canvas-qrcode {
+		
+		margin: auto;
+		display: inline-block;
+		float: left;
+	}
+	
+	
+	.opacity-qrcode {
+		opacity: 0;
+		display: block;
+	}
+
+	.show-qrcode {
+		display: block;
+		animation: fade 0.7s;
+
+		// -moz-animation: fade 0.5s; /* Firefox */
+		// -webkit-animation: fade 0.5s; /* Safari 和 Chrome */
+		// -o-animation: fade 0.5s;
+	}
+
+	.hide-qrcode {
+		animation: hide 0.7s;
+	}
+
+	@keyframes fade {
+		from {
+			opacity: 0.8;
+		}
+
+		to {
+			opacity: 1;
+		}
+	}
+
+	@keyframes hide {
+		from {
+			opacity: 1;
+		}
+
+		to {
+			opacity: 0;
+		}
+	}
+</style>

+ 872 - 0
components/ay-qrcode/qrcode_wx.js

@@ -0,0 +1,872 @@
+!(function() {
+
+	// alignment pattern
+	var adelta = [
+		0, 11, 15, 19, 23, 27, 31,
+		16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,
+		26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28
+	];
+
+	// version block
+	var vpat = [
+		0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d,
+		0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9,
+		0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75,
+		0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64,
+		0x541, 0xc69
+	];
+
+	// final format bits with mask: level << 3 | mask
+	var fmtword = [
+		0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, //L
+		0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, //M
+		0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, //Q
+		0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b //H
+	];
+
+	// 4 per version: number of blocks 1,2; data width; ecc width
+	var eccblocks = [
+		1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17,
+		1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28,
+		1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22,
+		1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16,
+		1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22,
+		2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28,
+		2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4, 1, 13, 26,
+		2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26,
+		2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24,
+		2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28,
+		4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24,
+		2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28,
+		4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22,
+		3, 1, 115, 30, 4, 5, 40, 24, 11, 5, 16, 20, 11, 5, 12, 24,
+		5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24,
+		5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30,
+		1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28,
+		5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28,
+		3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26,
+		3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,
+		4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30,
+		2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13, 24,
+		4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30,
+		6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
+		8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30,
+		10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30,
+		8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30,
+		3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30,
+		7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30,
+		5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30,
+		13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30,
+		17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30,
+		17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30,
+		13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30,
+		12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30,
+		6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30,
+		17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30,
+		4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30,
+		20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30,
+		19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
+	];
+
+	// Galois field log table
+	var glog = [
+		0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,
+		0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,
+		0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,
+		0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,
+		0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
+		0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,
+		0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,
+		0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,
+		0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,
+		0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
+		0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,
+		0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,
+		0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,
+		0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,
+		0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
+		0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
+	];
+
+	// Galios field exponent table
+	var gexp = [
+		0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,
+		0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,
+		0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,
+		0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,
+		0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
+		0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,
+		0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,
+		0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,
+		0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,
+		0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
+		0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,
+		0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,
+		0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,
+		0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,
+		0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
+		0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00
+	];
+
+	// Working buffers:
+	// data input and ecc append, image working buffer, fixed part of image, run lengths for badness
+	var strinbuf = [],
+		eccbuf = [],
+		qrframe = [],
+		framask = [],
+		rlens = [];
+	// Control values - width is based on version, last 4 are from table.
+	var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
+	var ecclevel = 2;
+	// set bit to indicate cell in qrframe is immutable.  symmetric around diagonal
+	function setmask(x, y) {
+		var bt;
+		if (x > y) {
+			bt = x;
+			x = y;
+			y = bt;
+		}
+		// y*y = 1+3+5...
+		bt = y;
+		bt *= y;
+		bt += y;
+		bt >>= 1;
+		bt += x;
+		framask[bt] = 1;
+	}
+
+	// enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
+	function putalign(x, y) {
+		var j;
+
+		qrframe[x + width * y] = 1;
+		for (j = -2; j < 2; j++) {
+			qrframe[(x + j) + width * (y - 2)] = 1;
+			qrframe[(x - 2) + width * (y + j + 1)] = 1;
+			qrframe[(x + 2) + width * (y + j)] = 1;
+			qrframe[(x + j + 1) + width * (y + 2)] = 1;
+		}
+		for (j = 0; j < 2; j++) {
+			setmask(x - 1, y + j);
+			setmask(x + 1, y - j);
+			setmask(x - j, y - 1);
+			setmask(x + j, y + 1);
+		}
+	}
+
+	//========================================================================
+	// Reed Solomon error correction
+	// exponentiation mod N
+	function modnn(x) {
+		while (x >= 255) {
+			x -= 255;
+			x = (x >> 8) + (x & 255);
+		}
+		return x;
+	}
+
+	var genpoly = [];
+
+	// Calculate and append ECC data to data block.  Block is in strinbuf, indexes to buffers given.
+	function appendrs(data, dlen, ecbuf, eclen) {
+		var i, j, fb;
+
+		for (i = 0; i < eclen; i++)
+			strinbuf[ecbuf + i] = 0;
+		for (i = 0; i < dlen; i++) {
+			fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
+			if (fb != 255) /* fb term is non-zero */
+				for (j = 1; j < eclen; j++)
+					strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
+			else
+				for (j = ecbuf; j < ecbuf + eclen; j++)
+					strinbuf[j] = strinbuf[j + 1];
+			strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
+		}
+	}
+
+	//========================================================================
+	// Frame data insert following the path rules
+
+	// check mask - since symmetrical use half.
+	function ismasked(x, y) {
+		var bt;
+		if (x > y) {
+			bt = x;
+			x = y;
+			y = bt;
+		}
+		bt = y;
+		bt += y * y;
+		bt >>= 1;
+		bt += x;
+		return framask[bt];
+	}
+
+	//========================================================================
+	//  Apply the selected mask out of the 8.
+	function applymask(m) {
+		var x, y, r3x, r3y;
+
+		switch (m) {
+			case 0:
+				for (y = 0; y < width; y++)
+					for (x = 0; x < width; x++)
+						if (!((x + y) & 1) && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+				break;
+			case 1:
+				for (y = 0; y < width; y++)
+					for (x = 0; x < width; x++)
+						if (!(y & 1) && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+				break;
+			case 2:
+				for (y = 0; y < width; y++)
+					for (r3x = 0, x = 0; x < width; x++, r3x++) {
+						if (r3x == 3)
+							r3x = 0;
+						if (!r3x && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+					}
+				break;
+			case 3:
+				for (r3y = 0, y = 0; y < width; y++, r3y++) {
+					if (r3y == 3)
+						r3y = 0;
+					for (r3x = r3y, x = 0; x < width; x++, r3x++) {
+						if (r3x == 3)
+							r3x = 0;
+						if (!r3x && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+					}
+				}
+				break;
+			case 4:
+				for (y = 0; y < width; y++)
+					for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++, r3x++) {
+						if (r3x == 3) {
+							r3x = 0;
+							r3y = !r3y;
+						}
+						if (!r3y && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+					}
+				break;
+			case 5:
+				for (r3y = 0, y = 0; y < width; y++, r3y++) {
+					if (r3y == 3)
+						r3y = 0;
+					for (r3x = 0, x = 0; x < width; x++, r3x++) {
+						if (r3x == 3)
+							r3x = 0;
+						if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+					}
+				}
+				break;
+			case 6:
+				for (r3y = 0, y = 0; y < width; y++, r3y++) {
+					if (r3y == 3)
+						r3y = 0;
+					for (r3x = 0, x = 0; x < width; x++, r3x++) {
+						if (r3x == 3)
+							r3x = 0;
+						if (!(((x & y & 1) + (r3x && (r3x == r3y))) & 1) && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+					}
+				}
+				break;
+			case 7:
+				for (r3y = 0, y = 0; y < width; y++, r3y++) {
+					if (r3y == 3)
+						r3y = 0;
+					for (r3x = 0, x = 0; x < width; x++, r3x++) {
+						if (r3x == 3)
+							r3x = 0;
+						if (!(((r3x && (r3x == r3y)) + ((x + y) & 1)) & 1) && !ismasked(x, y))
+							qrframe[x + y * width] ^= 1;
+					}
+				}
+				break;
+		}
+		return;
+	}
+
+	// Badness coefficients.
+	var N1 = 3,
+		N2 = 3,
+		N3 = 40,
+		N4 = 10;
+
+	// Using the table of the length of each run, calculate the amount of bad image 
+	// - long runs or those that look like finders; called twice, once each for X and Y
+	function badruns(length) {
+		var i;
+		var runsbad = 0;
+		for (i = 0; i <= length; i++)
+			if (rlens[i] >= 5)
+				runsbad += N1 + rlens[i] - 5;
+		// BwBBBwB as in finder
+		for (i = 3; i < length - 1; i += 2)
+			if (rlens[i - 2] == rlens[i + 2] &&
+				rlens[i + 2] == rlens[i - 1] &&
+				rlens[i - 1] == rlens[i + 1] &&
+				rlens[i - 1] * 3 == rlens[i]
+				// white around the black pattern? Not part of spec
+				&&
+				(rlens[i - 3] == 0 // beginning
+					||
+					i + 3 > length // end
+					||
+					rlens[i - 3] * 3 >= rlens[i] * 4 || rlens[i + 3] * 3 >= rlens[i] * 4)
+			)
+				runsbad += N3;
+		return runsbad;
+	}
+
+	// Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
+	function badcheck() {
+		var x, y, h, b, b1;
+		var thisbad = 0;
+		var bw = 0;
+
+		// blocks of same color.
+		for (y = 0; y < width - 1; y++)
+			for (x = 0; x < width - 1; x++)
+				if ((qrframe[x + width * y] && qrframe[(x + 1) + width * y] &&
+						qrframe[x + width * (y + 1)] && qrframe[(x + 1) + width * (y + 1)]) // all black
+					||
+					!(qrframe[x + width * y] || qrframe[(x + 1) + width * y] ||
+						qrframe[x + width * (y + 1)] || qrframe[(x + 1) + width * (y + 1)])) // all white
+					thisbad += N2;
+
+		// X runs
+		for (y = 0; y < width; y++) {
+			rlens[0] = 0;
+			for (h = b = x = 0; x < width; x++) {
+				if ((b1 = qrframe[x + width * y]) == b)
+					rlens[h]++;
+				else
+					rlens[++h] = 1;
+				b = b1;
+				bw += b ? 1 : -1;
+			}
+			thisbad += badruns(h);
+		}
+
+		// black/white imbalance
+		if (bw < 0)
+			bw = -bw;
+
+		var big = bw;
+		var count = 0;
+		big += big << 2;
+		big <<= 1;
+		while (big > width * width)
+			big -= width * width, count++;
+		thisbad += count * N4;
+
+		// Y runs
+		for (x = 0; x < width; x++) {
+			rlens[0] = 0;
+			for (h = b = y = 0; y < width; y++) {
+				if ((b1 = qrframe[x + width * y]) == b)
+					rlens[h]++;
+				else
+					rlens[++h] = 1;
+				b = b1;
+			}
+			thisbad += badruns(h);
+		}
+		return thisbad;
+	}
+
+	function genframe(instring) {
+		var x, y, k, t, v, i, j, m;
+
+		// find the smallest version that fits the string
+		t = instring.length;
+		version = 0;
+		do {
+			version++;
+			k = (ecclevel - 1) * 4 + (version - 1) * 16;
+			neccblk1 = eccblocks[k++];
+			neccblk2 = eccblocks[k++];
+			datablkw = eccblocks[k++];
+			eccblkwid = eccblocks[k];
+			k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
+			if (t <= k)
+				break;
+		} while (version < 40);
+
+		// FIXME - insure that it fits insted of being truncated
+		width = 17 + 4 * version;
+
+		// allocate, clear and setup data structures
+		v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+		for (t = 0; t < v; t++)
+			eccbuf[t] = 0;
+		strinbuf = instring.slice(0);
+
+		for (t = 0; t < width * width; t++)
+			qrframe[t] = 0;
+
+		for (t = 0; t < (width * (width + 1) + 1) / 2; t++)
+			framask[t] = 0;
+
+		// insert finders - black to frame, white to mask
+		for (t = 0; t < 3; t++) {
+			k = 0;
+			y = 0;
+			if (t == 1)
+				k = (width - 7);
+			if (t == 2)
+				y = (width - 7);
+			qrframe[(y + 3) + width * (k + 3)] = 1;
+			for (x = 0; x < 6; x++) {
+				qrframe[(y + x) + width * k] = 1;
+				qrframe[y + width * (k + x + 1)] = 1;
+				qrframe[(y + 6) + width * (k + x)] = 1;
+				qrframe[(y + x + 1) + width * (k + 6)] = 1;
+			}
+			for (x = 1; x < 5; x++) {
+				setmask(y + x, k + 1);
+				setmask(y + 1, k + x + 1);
+				setmask(y + 5, k + x);
+				setmask(y + x + 1, k + 5);
+			}
+			for (x = 2; x < 4; x++) {
+				qrframe[(y + x) + width * (k + 2)] = 1;
+				qrframe[(y + 2) + width * (k + x + 1)] = 1;
+				qrframe[(y + 4) + width * (k + x)] = 1;
+				qrframe[(y + x + 1) + width * (k + 4)] = 1;
+			}
+		}
+
+		// alignment blocks
+		if (version > 1) {
+			t = adelta[version];
+			y = width - 7;
+			for (;;) {
+				x = width - 7;
+				while (x > t - 3) {
+					putalign(x, y);
+					if (x < t)
+						break;
+					x -= t;
+				}
+				if (y <= t + 9)
+					break;
+				y -= t;
+				putalign(6, y);
+				putalign(y, 6);
+			}
+		}
+
+		// single black
+		qrframe[8 + width * (width - 8)] = 1;
+
+		// timing gap - mask only
+		for (y = 0; y < 7; y++) {
+			setmask(7, y);
+			setmask(width - 8, y);
+			setmask(7, y + width - 7);
+		}
+		for (x = 0; x < 8; x++) {
+			setmask(x, 7);
+			setmask(x + width - 8, 7);
+			setmask(x, width - 8);
+		}
+
+		// reserve mask-format area
+		for (x = 0; x < 9; x++)
+			setmask(x, 8);
+		for (x = 0; x < 8; x++) {
+			setmask(x + width - 8, 8);
+			setmask(8, x);
+		}
+		for (y = 0; y < 7; y++)
+			setmask(8, y + width - 7);
+
+		// timing row/col
+		for (x = 0; x < width - 14; x++)
+			if (x & 1) {
+				setmask(8 + x, 6);
+				setmask(6, 8 + x);
+			}
+		else {
+			qrframe[(8 + x) + width * 6] = 1;
+			qrframe[6 + width * (8 + x)] = 1;
+		}
+
+		// version block
+		if (version > 6) {
+			t = vpat[version - 7];
+			k = 17;
+			for (x = 0; x < 6; x++)
+				for (y = 0; y < 3; y++, k--)
+					if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
+						qrframe[(5 - x) + width * (2 - y + width - 11)] = 1;
+						qrframe[(2 - y + width - 11) + width * (5 - x)] = 1;
+					}
+			else {
+				setmask(5 - x, 2 - y + width - 11);
+				setmask(2 - y + width - 11, 5 - x);
+			}
+		}
+
+		// sync mask bits - only set above for white spaces, so add in black bits
+		for (y = 0; y < width; y++)
+			for (x = 0; x <= y; x++)
+				if (qrframe[x + width * y])
+					setmask(x, y);
+
+		// convert string to bitstream
+		// 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
+		v = strinbuf.length;
+
+		// string to array
+		for (i = 0; i < v; i++)
+			eccbuf[i] = strinbuf.charCodeAt(i);
+		strinbuf = eccbuf.slice(0);
+
+		// calculate max string length
+		x = datablkw * (neccblk1 + neccblk2) + neccblk2;
+		if (v >= x - 2) {
+			v = x - 2;
+			if (version > 9)
+				v--;
+		}
+
+		// shift and repack to insert length prefix
+		i = v;
+		if (version > 9) {
+			strinbuf[i + 2] = 0;
+			strinbuf[i + 3] = 0;
+			while (i--) {
+				t = strinbuf[i];
+				strinbuf[i + 3] |= 255 & (t << 4);
+				strinbuf[i + 2] = t >> 4;
+			}
+			strinbuf[2] |= 255 & (v << 4);
+			strinbuf[1] = v >> 4;
+			strinbuf[0] = 0x40 | (v >> 12);
+		} else {
+			strinbuf[i + 1] = 0;
+			strinbuf[i + 2] = 0;
+			while (i--) {
+				t = strinbuf[i];
+				strinbuf[i + 2] |= 255 & (t << 4);
+				strinbuf[i + 1] = t >> 4;
+			}
+			strinbuf[1] |= 255 & (v << 4);
+			strinbuf[0] = 0x40 | (v >> 4);
+		}
+		// fill to end with pad pattern
+		i = v + 3 - (version < 10);
+		while (i < x) {
+			strinbuf[i++] = 0xec;
+			// buffer has room    if (i == x)      break;
+			strinbuf[i++] = 0x11;
+		}
+
+		// calculate and append ECC
+
+		// calculate generator polynomial
+		genpoly[0] = 1;
+		for (i = 0; i < eccblkwid; i++) {
+			genpoly[i + 1] = 1;
+			for (j = i; j > 0; j--)
+				genpoly[j] = genpoly[j] ?
+				genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
+			genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
+		}
+		for (i = 0; i <= eccblkwid; i++)
+			genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step
+
+		// append ecc to data buffer
+		k = x;
+		y = 0;
+		for (i = 0; i < neccblk1; i++) {
+			appendrs(y, datablkw, k, eccblkwid);
+			y += datablkw;
+			k += eccblkwid;
+		}
+		for (i = 0; i < neccblk2; i++) {
+			appendrs(y, datablkw + 1, k, eccblkwid);
+			y += datablkw + 1;
+			k += eccblkwid;
+		}
+		// interleave blocks
+		y = 0;
+		for (i = 0; i < datablkw; i++) {
+			for (j = 0; j < neccblk1; j++)
+				eccbuf[y++] = strinbuf[i + j * datablkw];
+			for (j = 0; j < neccblk2; j++)
+				eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
+		}
+		for (j = 0; j < neccblk2; j++)
+			eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
+		for (i = 0; i < eccblkwid; i++)
+			for (j = 0; j < neccblk1 + neccblk2; j++)
+				eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
+		strinbuf = eccbuf;
+
+		// pack bits into frame avoiding masked area.
+		x = y = width - 1;
+		k = v = 1; // up, minus
+		/* inteleaved data and ecc codes */
+		m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+		for (i = 0; i < m; i++) {
+			t = strinbuf[i];
+			for (j = 0; j < 8; j++, t <<= 1) {
+				if (0x80 & t)
+					qrframe[x + width * y] = 1;
+				do { // find next fill position
+					if (v)
+						x--;
+					else {
+						x++;
+						if (k) {
+							if (y != 0)
+								y--;
+							else {
+								x -= 2;
+								k = !k;
+								if (x == 6) {
+									x--;
+									y = 9;
+								}
+							}
+						} else {
+							if (y != width - 1)
+								y++;
+							else {
+								x -= 2;
+								k = !k;
+								if (x == 6) {
+									x--;
+									y -= 8;
+								}
+							}
+						}
+					}
+					v = !v;
+				} while (ismasked(x, y));
+			}
+		}
+
+		// save pre-mask copy of frame
+		strinbuf = qrframe.slice(0);
+		t = 0; // best
+		y = 30000; // demerit
+		// for instead of while since in original arduino code
+		// if an early mask was "good enough" it wouldn't try for a better one
+		// since they get more complex and take longer.
+		for (k = 0; k < 8; k++) {
+			applymask(k); // returns black-white imbalance
+			x = badcheck();
+			if (x < y) { // current mask better than previous best?
+				y = x;
+				t = k;
+			}
+			if (t == 7)
+				break; // don't increment i to a void redoing mask
+			qrframe = strinbuf.slice(0); // reset for next pass
+		}
+		if (t != k) // redo best mask - none good enough, last wasn't t
+			applymask(t);
+
+		// add in final mask/ecclevel bytes
+		y = fmtword[t + ((ecclevel - 1) << 3)];
+		// low byte
+		for (k = 0; k < 8; k++, y >>= 1)
+			if (y & 1) {
+				qrframe[(width - 1 - k) + width * 8] = 1;
+				if (k < 6)
+					qrframe[8 + width * k] = 1;
+				else
+					qrframe[8 + width * (k + 1)] = 1;
+			}
+		// high byte
+		for (k = 0; k < 7; k++, y >>= 1)
+			if (y & 1) {
+				qrframe[8 + width * (width - 7 + k)] = 1;
+				if (k)
+					qrframe[(6 - k) + width * 8] = 1;
+				else
+					qrframe[7 + width * 8] = 1;
+			}
+		return qrframe;
+	}
+
+
+
+
+	var _canvas = null;
+
+	var api = {
+
+		get ecclevel() {
+			return ecclevel;
+		},
+
+		set ecclevel(val) {
+			ecclevel = val;
+		},
+
+		get size() {
+			return _size;
+		},
+
+		set size(val) {
+			_size = val
+		},
+
+		get canvas() {
+			return _canvas;
+		},
+
+		set canvas(el) {
+			_canvas = el;
+		},
+
+		getFrame: function(string) {
+			return genframe(string);
+		},
+		//这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文
+		utf16to8: function(str) {
+			var out, i, len, c;
+
+			out = "";
+			len = str.length;
+			for (i = 0; i < len; i++) {
+				c = str.charCodeAt(i);
+				if ((c >= 0x0001) && (c <= 0x007F)) {
+					out += str.charAt(i);
+				} else if (c > 0x07FF) {
+					out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
+					out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
+					out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+				} else {
+					out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
+					out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+				}
+			}
+			return out;
+		},
+		/**
+		 * 新增$this参数,传入组件的this,兼容在组件中生成
+		 */
+		draw: function(str, canvas, cavW, cavH, cavColor, haveImg, imageUrl, imageSize, $this, cb = function() {}, ecc) {
+			var that = this;
+			ecclevel = ecc || ecclevel;
+			canvas = canvas || _canvas;
+			if (!canvas) {
+				console.warn('No canvas provided to draw QR code in!')
+				return;
+			}
+			
+			
+			let pre_background = "#ffffff";
+			var size = Math.min(cavW, cavH);
+			str = that.utf16to8(str); //增加中文显示
+
+			var frame = that.getFrame(str);
+				// 组件中生成qrcode需要绑定this 
+			var ctx = uni.createCanvasContext(canvas, $this);
+			var px = Math.round(size / (width ));
+			
+			var roundedSize = px * (width);
+			// var px = 1 ;
+			// var roundedSize = px * (width + 8) ;
+			
+			//var roundedSize = 0 ;
+			//var offset = Math.floor((size - roundedSize) / 2);
+			var offset = 0 ;
+			size = roundedSize;
+			//ctx.clearRect(0, 0, cavW, cavW);
+			ctx.setFillStyle(pre_background)
+			ctx.fillRect(0, 0, cavW, cavW);
+			ctx.setFillStyle(cavColor);
+			for (var i = 0; i < width; i++) {
+				for (var j = 0; j < width; j++) {
+					if (frame[j * width + i]) {
+						ctx.fillRect(px * ( i) + offset, px * ( j) + offset, px, px);
+					}
+				}
+			}
+
+			//画图片
+			if (haveImg) {
+				try {
+					var x = Number(((cavW - imageSize - 14) / 2).toFixed(2));
+					var y = Number(((cavH - imageSize -14) / 2).toFixed(2));
+					drawRoundedRect(ctx, x, y, imageSize, imageSize, imageSize / 2, 6, true, true)
+
+					let isNetImg = false;
+
+					isNetImg = imageUrl.substr(0, 4) == 'http' ? true : false;
+
+					if (isNetImg) {
+						//网络图片下载到本地
+						uni.getImageInfo({
+							src: imageUrl,
+							success: function(res) {
+								ctx.drawImage(res.path, x, y, imageSize, imageSize);
+								//--增加绘制完成回调
+								ctx.draw(false, function() {
+									cb();
+								})
+							}
+						})
+					} else {
+						ctx.drawImage(imageUrl, x, y, imageSize, imageSize);
+						//--增加绘制完成回调
+						ctx.draw(false, function() {
+							cb();
+						})
+					}
+
+
+
+
+					// 画圆角矩形
+					function drawRoundedRect(ctxi, x, y, width, height, r, lineWidth, fill, stroke) {
+						ctxi.setLineWidth(lineWidth);
+						ctxi.setFillStyle(pre_background);
+						ctxi.setStrokeStyle(pre_background);
+						ctxi.beginPath(); // draw top and top right corner 
+						ctxi.moveTo(x + r, y);
+						ctxi.arcTo(x + width, y, x + width, y + r, r); // draw right side and bottom right corner 
+						ctxi.arcTo(x + width, y + height, x + width - r, y + height, r); // draw bottom and bottom left corner 
+						ctxi.arcTo(x, y + height, x, y + height - r, r); // draw left and top left corner 
+						ctxi.arcTo(x, y, x + r, y, r);
+						ctxi.closePath();
+						if (fill) {
+							ctxi.fill();
+						}
+						if (stroke) {
+							ctxi.stroke();
+						}
+					}
+				} catch (e) {
+					//TODO handle the exception
+				}
+
+			} else {
+				//--增加绘制完成回调
+				ctx.draw(false, function() {
+					cb();
+				})
+			}
+
+
+
+		}
+	}
+	module.exports = {
+		api
+	}
+})();

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 263 - 0
components/ay-qrcode/weapp-qrcode.js


+ 78 - 0
components/cartfixed.vue

@@ -0,0 +1,78 @@
+<template>
+	<view class="">
+		<view class="cart u-flex u-row-center" @click="$u.route('/shopping/cart',{type:'reLaunch'})">
+			<u-icon size="52rpx" class="icon" :name="staticUrl+'/img/cartico.png'"></u-icon>
+			<u-badge type="error" max="99" :value="cartTotal" :absolute="true" :offset="[0,5]"></u-badge>
+		</view>
+		<u-toast ref="uToast"></u-toast>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:'cartfixed',
+		data() {
+			return {
+				cartTotal:0,
+				staticUrl:this.$commonConfig.staticUrl,
+			}
+		},
+		onShow() {
+		},
+		onLoad() {
+		},
+		mounted(){
+			this.getCartList()
+		},
+		methods: {
+			getCartList(isAdd){
+				this.$u.api.cartList().then(res=>{
+					if(isAdd){
+						if(res.data.total==this.cartTotal){
+							uni.showToast({
+								title:'已在购物车',
+								icon:'success'
+							})
+							
+							// this.$refs.uToast.show({
+							// 	type:"success",
+							// 	message:'已在购物车'
+							// });
+						}else{
+							uni.showToast({
+								title:'加入成功',
+								icon:'success'
+							})
+							// this.$refs.uToast.show({
+							// 	type:"success",
+							// 	message:'加入成功'
+							// });
+						}
+					}
+					this.cartTotal = res.data.total;
+					console.log('getCartList',res);
+					}).catch(err=>{
+					console.log('getCartList',err.data);
+				})
+			},
+			addToCart(num){
+				this.getCartList()
+				// this.cartTotal += num;
+				console.log('addToCart',num);
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.cart{
+	position: fixed;
+	width: 100rpx;
+	height: 100rpx;
+	border-radius: 50%;
+	right: 20rpx;
+	bottom: 300rpx;
+	background: #FFFFFF;
+	box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.12);
+}
+</style>

+ 127 - 0
components/tabbar.vue

@@ -0,0 +1,127 @@
+<template>
+	<view class="tabbar">
+		<u-tabbar
+			:value="tabbarValue"
+			@change="tabbarChange"
+			:fixed="true"
+			:placeholder="true"
+			:border="false"
+			inactiveColor="#999"
+			activeColor="#009AEF"
+			:customStyle="{'padding-top':'5px'}"
+			:safeAreaInsetBottom="true"
+		>
+			<u-tabbar-item text="首页" >
+				<image
+					class="u-page__item__slot-icon"
+					slot="active-icon"
+					src="../static/img/tabbar-home.png"
+				></image>
+				<image
+					class="u-page__item__slot-icon"
+					slot="inactive-icon"
+					src="../static/img/tabbar-home-gray.png"
+				></image>
+			</u-tabbar-item>
+			<u-tabbar-item text="奖品兑换" >
+				<image
+					class="u-page__item__slot-icon"
+					slot="active-icon"
+					src="../static/img/tabbar-gift.png"
+				></image>
+				<image
+					class="u-page__item__slot-icon"
+					slot="inactive-icon"
+					src="../static/img/tabbar-gift-gray.png"
+				></image>
+			</u-tabbar-item>
+			<u-tabbar-item text="个人中心" >
+				<image
+					class="u-page__item__slot-icon"
+					slot="active-icon"
+					src="../static/img/tabbar-my.png"
+				></image>
+				<image
+					class="u-page__item__slot-icon"
+					slot="inactive-icon"
+					src="../static/img/tabbar-my-gray.png"
+				></image>
+			</u-tabbar-item>
+		</u-tabbar>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'tabbar',
+	props:{
+		tabbarIndexProps:{
+			type:Number,
+			default:0
+		},
+	},
+	data() {
+		return {
+			staticUrl:this.$commonConfig.staticUrl,
+			tabbarValue:0,
+		}
+	},
+	watch:{
+		'tabbarIndexProps': {
+			handler(newVal, oldVal) {
+				// let pages = getCurrentPages();
+				// console.log('pages============',pages);
+				this.tabbarValue = newVal
+			},
+			immediate: true
+		}
+	},
+	onLoad() {
+		console.log('onLoad tabbarIndex',this.tabbarIndex);
+	},
+	onShow() {
+		console.log('tabbarIndex',this.tabbarIndex);
+		this.tabbarValue = this.tabbarIndex
+	},
+	methods: {
+		tabbarChange(name){
+			// console.log('name====',name);
+			const tabBarRoutes = {
+			0: '/pages/index/index',
+			// 1: {
+			//   url: '/shopping/producTypetList',
+			//   query: {
+			// 	navType: 'navigateTo'
+			//   }
+			// },
+			1: '/shopping/order',
+			2: '/center/center'
+		  };
+		  
+		  const targetRoute = tabBarRoutes[name];
+		  
+		  if (typeof targetRoute === 'string') {
+			uni.reLaunch({url: targetRoute});
+		  } else if (typeof targetRoute === 'object') {
+			  if(targetRoute.query.navType=='navigateTo'){
+				  this.$u.route(targetRoute.url, targetRoute.query);
+				  return
+			  }
+			let queryParams = uni.$u.queryParams(targetRoute.query);
+			uni.reLaunch({url: targetRoute.url+queryParams});
+		  }
+			// this.tabbarValue = name
+		}
+	}
+}	
+</script>
+
+<style lang="scss" scoped>
+.u-page__item__slot-icon{
+	width: 40rpx;
+	height: 40rpx;
+	&.big{
+		transform: scale(1.5) translateY(-0.5em);
+	}
+}
+</style>

+ 129 - 0
examination/answersheet.vue

@@ -0,0 +1,129 @@
+<template>
+	<view class="pages">
+		<view class="bg"></view>
+		<view class="content">
+			<view class="title">答题卡</view>
+			<view class="list u-flex u-flex-wrap">
+				<view 
+					class="item" 
+					:class="{error:item.status==0,undo:item.status==2}"
+					@click="topicClick(item)"
+					v-for="(item,index) in pagerResult.topicList" :key="item.paperAnswerId">
+					{{index+1}}
+				</view>
+			</view>
+			<view class="statistics u-flex">
+				<view class="item">
+					答对:<text class="num">{{pagerResult.topicTrueCount}}</text>
+				</view>
+				<view class="item">
+					答错:<text class="num" style="color: #FFB100;">{{pagerResult.topicFalseCount}}</text>
+				</view>
+				<view class="item">
+					未答:<text class="num" style="color: #999;">{{ pagerResult.topicList.length - pagerResult.answerTopicCount}}</text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id:'',
+				pagerResult:{
+					topicList:[]
+				}
+			}
+		},
+		onShow() {
+		},
+		onLoad(page) {
+			this.id = page.id;
+			this.findPagerResult();
+		},
+		methods: {
+			findPagerResult(){
+				this.$u.api.findPagerResult({paperScoreId:this.id}).then(res=>{
+					this.pagerResult = res.data;
+					// console.log('findPagerResult',res.data);
+				}).catch(err=>{
+					console.log('findPagerResult',err);
+				})
+			},
+			topicClick(item){
+				console.log('topicClick',item);
+				uni.$u.route('/examination/question', {
+					paperScoreId: this.id,
+					paperTopicId: item.paperTopicId
+				});
+			}
+		}
+	}
+</script>
+<style lang="scss" scoped>
+.bg{
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 200rpx;
+	background-color: #009AEF;
+	z-index: -1;
+}
+.content{
+	background: #FFFFFF;
+	border-radius: 24rpx;
+	padding: 40rpx 0;
+	margin-top: 40rpx;
+	.title{
+		margin-bottom: 30rpx;
+		font-size: 34rpx;
+		font-weight: 400;
+		color: #333333;
+		line-height: 48rpx;
+		margin-left: 46rpx;
+	}
+	.list{
+		.item{
+			width: 72rpx;
+			height: 72rpx;
+			line-height: 72rpx;
+			text-align: center;
+			background: #009AEF;
+			color: #fff;
+			border-radius: 8rpx;
+			margin-left: 46rpx;
+			margin-bottom: 46rpx;
+			&.error{
+				background: #FFB100;
+			}
+			&.undo{
+				background: #F5F9FC;
+				color: #999;
+			}
+		}
+	}
+	.statistics{
+		margin: 20rpx 30rpx;
+		padding: 40rpx 20rpx;
+		background: #F5F9FC;
+		border-radius: 24rpx;
+		text-align: center;
+		font-size: 30rpx;
+		font-weight: 400;
+		color: #999999;
+		.item{
+			flex: 1;
+			& + .item{
+				border-left: 1px solid #eee;
+			}
+			.num{
+				font-weight: 600;
+				color: #009AEF ;
+			}
+		}
+	}
+}
+</style>

+ 30 - 0
examination/complete.vue

@@ -0,0 +1,30 @@
+<template>
+	<view class="pages">
+		
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				params:{
+					
+				}
+				
+			}
+		},
+		onShow() {
+		},
+		onLoad() {
+
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 339 - 0
examination/examination.vue

@@ -0,0 +1,339 @@
+<template>
+	<view class="pages">
+		<u-empty mode="search" v-if="!formData.paperId&&hasload==true" :text="emptyText" marginTop="20vh"></u-empty>
+		<view class="info" v-if="!start">
+			<view class="page-wrap">
+				<view class="paper-name">{{info.paperName}}</view>
+				<view class="info-item" v-if="info.paperAttention">
+					<view class="til">注意事项</view>
+					<view class="con">{{info.paperAttention}}</view>
+				</view>
+				<view class="info-item" v-if="info.remark">
+					<view class="til">备注信息</view>
+					<view class="con">{{info.remark}}</view>
+				</view>
+			</view>
+		</view>
+		<view class="" v-else>
+			<view class="swiper-wrap">
+				<swiper class="swiper" 
+					:circular="false"
+					:indicator-dots="false" 
+					:autoplay="false" 
+					:interval="5000"
+					:current="current"
+					@change="swiperChange"
+					:duration="500">
+					<swiper-item v-for="item in topicList" :key="item.id">
+						<view class="swiper-item">
+							<!-- 题目类型: 1-单选题 2-多选题 3-判断题 -->
+							<view class="type-1" v-if="item.topicType==1||item.topicType==3">
+								<text class="topic-type">{{item.topicType|filterTopicType}}</text>
+								<view class="question">{{item.content}}</view>
+								<view class="choice u-flex" :class="formData.answerList[current].answer == choice.split(':')[0] ? 'selected':''"
+									@click="choiceAnswer(choice)"
+									v-for="(choice,index) in item.choiceList" :key="index">
+									<view class="sort">
+										{{choice.split(":")[0]}}
+									</view>
+									<view class="answer">
+										{{choice.split(":")[1]}}
+									</view>
+									<view class="status"></view>
+								</view>
+							</view>
+							<view class="type-2" v-else-if="item.topicType==2">
+								<text class="topic-type">{{item.topicType|filterTopicType}}</text>
+								<view class="question">{{item.content}}</view>
+								<view class="choice u-flex" :class="formData.answerList[current].answer.includes(choice.split(':')[0]) ? 'selected':''"
+									@click="multipleChoice(choice)"
+									v-for="(choice,index) in item.choiceList" :key="index">
+									<view class="sort">
+										{{choice.split(":")[0]}}
+									</view>
+									<view class="answer">
+										{{choice.split(":")[1]}}
+									</view>
+									<view class="status"></view>
+								</view>
+							</view>
+							<!-- <view class="type-3" v-else-if="item.topicType==3">
+								
+							</view> -->
+						</view>
+					</swiper-item>
+				</swiper>
+			</view>
+		</view>
+		
+		<view class="bottom-btn-static">
+			<view class="bottom-btn-fixed">
+				<view class="btn" v-if="!start&&formData.paperId" @click="startAnswering">开始答题</view>
+				<view class="btns u-flex" v-else>
+					<view class="btn" @click="preQuestion" v-if="current>0">上一题</view>
+					<view class="btn yellow" @click="nextQuestion" v-if="current<topicList.length-1">下一题</view>
+					<view class="btn yellow" v-else-if="current==topicList.length-1" @click="submit">提交试卷</view>
+				</view>
+			</view>
+		</view>
+		<u-modal 
+			:show="submitShow" 
+			:title="submitShowTitle" 
+			@confirm="paperSubmit"
+			@cancel="submitShow=false"
+			:showCancelButton="true"
+			:content='submitShowContent'>
+			<view class="slot-content" style="text-align: center;">
+				<img src="../static/img/icon-warning.png" alt="">
+				<view class="" style="text-align: left;font-size: 28rpx;color: #999;margin-top: 20rpx;">
+					{{submitShowContent}}
+				</view>
+			</view>
+		</u-modal>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id:'',
+				start:false,
+				info:{},
+				topicList:[],
+				current:0,
+				formData:{
+					paperId:'',
+					timeUsed:'',
+					startTime:'',
+					endTime:'',
+					answerList:[]
+				},
+				submitShow:false,
+				submitShowTitle:'提示',
+				submitShowContent:'确认提交?',
+				hasload:false,
+				emptyText:'没有可用的试卷,请联系管理员'
+			}
+		},
+		onShow() {
+		},
+		onLoad(page) {
+			this.id = page.id;
+			this.findPager();
+		},
+		computed: {
+			totalAnswer() {
+			  let that = this;
+			  return this.formData.answerList.reduce((total, item) => {
+					if(item.answer){
+						total += 1;
+					}
+				return total;
+			  }, 0);
+			}
+		},
+		methods: {
+			findPager(){
+				this.$u.api.findPager({classifyId:this.id}).then(res=>{
+					this.info = res.data;
+					this.formData.paperId = res.data.id;
+					this.findPagerTopic(this.formData.paperId);
+					// console.log('findPager',res);
+				}).catch(err=>{
+					this.hasload = true;
+					this.emptyText = err.msg
+					console.log('findPager',err);
+				})				
+			},
+			findPagerTopic(id){
+				this.$u.api.findPagerTopic({paperId:id}).then(res=>{
+					this.topicList = res.data.topicsList;
+					for (let i=0;i<this.topicList.length;i++) {
+						let data = {
+							paperTopicId:this.topicList[i].id,
+							topicLibId:this.topicList[i].topicLibId,
+							answer:''
+						}
+						this.formData.answerList.push(data)
+					}
+					console.log('answerList',this.formData.answerList);
+					// console.log('findPagerTopic',res);
+				}).catch(err=>{
+					console.log('findPagerTopic',err);
+				})				
+			},
+			startAnswering(){
+				this.start = true;
+				const now = new Date();
+				this.formData.startTime = uni.$u.timeFormat(now, 'yyyy-mm-dd hh:MM:ss');
+				uni.setNavigationBarTitle({
+				    title: `${this.current+1}/${this.topicList.length}`
+				});
+			},
+			choiceAnswer(choice){
+				// console.log('choiceAnswer',choice.split(":")[0]);
+				this.formData.answerList[this.current].answer = choice.split(":")[0];
+				// console.log('answerList',this.formData.answerList);
+			},
+			multipleChoice(choice){
+				if (this.formData.answerList[this.current].answer.includes(choice.split(":")[0])) {
+				  this.formData.answerList[this.current].answer =  this.formData.answerList[this.current].answer.replace(choice.split(":")[0], "");
+				} else {
+				  this.formData.answerList[this.current].answer = this.formData.answerList[this.current].answer.concat(choice.split(":")[0]).split("").sort().join("");
+				}
+				// console.log('answerList-',this.formData.answerList);
+			},
+			swiperChange(e){
+				this.current = e.detail.current;
+				uni.setNavigationBarTitle({
+				    title: `${this.current+1}/${this.topicList.length}`
+				});
+			},
+			nextQuestion(){
+				this.current++
+			},
+			preQuestion(){
+				this.current--
+			},
+			submit(){
+				let that = this;
+				if(this.totalAnswer<this.topicList.length){
+					this.submitShowContent = `本套试卷共${this.topicList.length}题,您已完成${this.totalAnswer}题,还剩${this.topicList.length - this.totalAnswer}题,确认提交吗?`
+				}else{
+					this.submitShowContent = '确认提交?'
+				}
+				this.submitShow = true;
+			},
+			paperSubmit(){
+				const now = new Date();
+				this.formData.endTime = uni.$u.timeFormat(now, 'yyyy-mm-dd hh:MM:ss');
+				let t1 = new Date(this.formData.startTime.replace(/-/g, '/')).getTime();
+				let t2 = new Date(this.formData.endTime.replace(/-/g, '/')).getTime();
+				this.formData.timeUsed = t2 - t1;
+				
+				// this.formData.timeUsed = new Date(this.formData.endTime).getTime() - new Date(this.formData.startTime).getTime();
+				// console.log('formData',this.formData);
+				// return
+				
+				this.$u.api.paperSubmit(this.formData).then(res=>{
+					let queryParams = uni.$u.queryParams({result:JSON.stringify(res.data)});
+					uni.reLaunch({url: '/examination/report'+queryParams});
+					// console.log('paperSubmit',res.data);
+				}).catch(err=>{
+					console.log('paperSubmit',err);
+				})
+			}
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.topic-type{
+	background: rgba(0,154,239,0.2);
+	border-radius: 8rpx;
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #009AEF;
+	line-height: 36rpx;
+	padding: 5rpx 14rpx;
+}
+.info{
+	.paper-name{
+		font-size: 34rpx;
+		font-weight: 600;
+		color: #333333;
+		line-height: 48rpx;
+		margin-bottom: 42rpx;
+	}
+	.info-item{
+		margin-bottom: 24rpx;
+		.til{
+			font-size: 32rpx;
+			font-weight: 600;
+			color: #333333;
+			line-height: 44rpx;
+			margin-bottom: 12rpx;
+		}
+		.con{
+			font-size: 30rpx;
+			font-weight: 400;
+			color: #999999;
+			line-height: 42rpx;
+		}
+	}
+	
+}
+.bottom-btn-static{
+	height: 98rpx;
+	.bottom-btn-fixed{
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		.btn{
+			text-align: center;
+			height: 98rpx;
+			line-height: 98rpx;
+			background-color: $uni-color-primary;
+			color: #fff;
+			&.yellow{
+				background-color: $uni-color-warning;
+			}
+		}
+		.btns{
+			.btn{
+				flex: 1;
+			}
+		}
+	}
+}
+.swiper-wrap{
+	.swiper{
+		min-height: calc( 100vh - 44px - 98rpx) ;
+		background-color: #F5F9FC;
+		.swiper-item{
+			padding: 30rpx;
+			font-size: 32rpx;
+			font-weight: 400;
+			color: #333333;
+			line-height: 44rpx;
+		}
+		.question{
+			font-size: 34rpx;
+			font-weight: 600;
+			line-height: 48rpx;
+			margin-bottom: 40rpx;
+			margin-top: 20rpx;
+		}
+		.choice{
+			padding: 36rpx 40rpx;
+			background-color: #fff;
+			border-radius: 24rpx;
+			margin-bottom: 20rpx;
+			.sort{
+				text-transform: uppercase;
+				font-weight: 600;
+			}
+			.answer{
+				padding-left: 18rpx;
+				margin-left: 20rpx;
+				border-left: 1px solid #ccc;
+				flex: 1;
+			}
+			.status{
+				width: 32rpx;
+				height: 32rpx;
+				background: #F5F9FC;
+				border-radius: 50%;
+			}
+			&.selected{
+				.status{
+					background-color: #009AEF;
+				}
+			}
+		}
+	}
+}
+</style>

+ 143 - 0
examination/question.vue

@@ -0,0 +1,143 @@
+<template>
+	<view class="pages">
+		<view class="page-wrap">
+			<text class="topic-type">{{answerResult.topicType|filterTopicType}}</text>
+			<!-- 题目类型: 1-单选题 2-多选题 3-判断题 -->
+			<view class="question-wrap">
+				<view class="question">{{answerResult.content}}</view>
+				<view class="choice u-flex" 
+					:class="{
+						selected:  answerResult.resultAnswer&&answerResult.resultAnswer.includes(choice.split(':')[0]),
+						answer:answerResult.answer.includes(choice.split(':')[0])
+					}"
+					v-for="(choice,index) in answerResult.choiceList" :key="index">
+					<view class="sort">
+						{{choice.split(":")[0]}}
+					</view>
+					<view class="answer">
+						{{choice.split(":")[1]}}
+					</view>
+					<view class="status"></view>
+				</view>
+			</view>
+			<view class="answers u-flex u-row-between">
+				<view class="">
+					正确答案:
+					<text class="answer">{{answerResult.answer}}</text> 
+				</view>
+				<view class="">
+					已选答案:
+					<text class="answer" style="color: #009AEF;">{{answerResult.resultAnswer||'没做'}}</text> 
+				</view>
+			</view>
+			<view class="analysis">答案解析:{{answerResult.answerAnalysis}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				paperScoreId:'',
+				paperTopicId:'',
+				answerResult:{}
+			}
+		},
+		onShow() {
+		},
+		onLoad(page) {
+			this.paperScoreId = page.paperScoreId;
+			this.paperTopicId = page.paperTopicId;
+			this.findAnswerResult();
+		},
+		methods: {
+			findAnswerResult(){
+				this.$u.api.findAnswerResult({paperScoreId:this.paperScoreId,paperTopicId:this.paperTopicId}).then(res=>{
+					this.answerResult = res.data;
+					// console.log('findAnswerResult',res.data);
+				}).catch(err=>{
+					console.log('findAnswerResult',err);
+				})
+			},
+		}
+	}
+</script>
+<style>
+	page{
+		background-color: #F5F9FC;
+	}
+</style>
+<style lang="scss" scoped>
+.topic-type{
+	background: rgba(0,154,239,0.2);
+	border-radius: 8rpx;
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #009AEF;
+	line-height: 36rpx;
+	padding: 5rpx 14rpx;
+}
+.question-wrap{
+	margin-top: 20rpx;
+	margin-bottom: 38rpx;
+	.question{
+		font-size: 34rpx;
+		font-weight: 600;
+		line-height: 48rpx;
+		margin-bottom: 40rpx;
+	}
+	.choice{
+		padding: 36rpx 40rpx;
+		background-color: #fff;
+		border-radius: 24rpx;
+		margin-bottom: 20rpx;
+		.sort{
+			text-transform: uppercase;
+			font-weight: 600;
+		}
+		.answer{
+			padding-left: 18rpx;
+			margin-left: 20rpx;
+			border-left: 1px solid #ccc;
+			flex: 1;
+		}
+		.status{
+			width: 32rpx;
+			height: 32rpx;
+			background: #F5F9FC;
+			border-radius: 50%;
+		}
+		&.selected{
+			border: 1px solid #FFB100;
+			.status{
+				background-color: #FFB100;
+			}
+		}
+		&.answer{
+			border: 1px solid #009AEF;
+			.status{
+					background-color: #009AEF;
+
+			}
+		}
+	}
+}
+.answers{
+	font-size: 32rpx;
+	font-weight: 400;
+	color: #333333;
+	line-height: 44rpx;
+	margin-bottom: 22rpx;
+	.answer{
+		font-weight: 600;
+		color: #333;
+	}
+}
+.analysis{
+	font-size: 30rpx;
+	font-weight: 400;
+	color: #999999;
+	line-height: 52rpx;
+}
+</style>

+ 106 - 0
examination/report.vue

@@ -0,0 +1,106 @@
+<template>
+	<view class="pages">
+		<img src="../static/img/report-ico.png" alt="">
+		<view class="title">恭喜你!答题完成</view>
+		<view class="score u-flex">
+			<view class="score-item">
+				<view class="up">
+					<text class="num">{{result.score}}</text>分
+				</view>
+				<view class="down">本次答题成绩</view>
+			</view>
+			<view class="score-item ">
+				<view class="up">
+					<text class="num" style="color: #FFB100;">{{result.credit}}</text>个
+				</view>
+				<view class="down">成功获得积分</view>
+			</view>
+		</view>
+		<u-button 
+			type="primary" 
+			:plain="true" 
+			:customStyle="btnCustomStyle" 
+			shape="circle" 
+			@click="$u.route('/examination/answersheet',{type:'reLaunch',id:paperScoreId})"
+			text="查看答题卡">
+		</u-button>
+		<u-button 
+			type="primary" 
+			shape="circle" 
+			@click="$u.route('/examination/typelist',{type:'reLaunch'})"
+			:customStyle="btnCustomStyle" 
+			text="继续答题">
+		</u-button>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				btnCustomStyle:{
+					width:'400rpx',
+					'margin-bottom':'20rpx'
+				},
+				paperScoreId:'',
+				result:{
+					score:'',
+					credit:''
+				},
+			}
+		},
+		onShow() {
+		},
+		onLoad(page) {
+			this.result = JSON.parse(page.result);
+			// console.log('this.result',this.result);
+			this.paperScoreId = this.result.paperScoreId;
+		},
+		methods: {
+
+		}
+	}
+</script>
+<style>
+	page{
+		background: url(../static/img/report-bg.png) no-repeat;
+		background-size: 100% auto;
+		text-align: center;
+		padding-top: 58rpx;
+	}
+</style>
+<style lang="scss" scoped>
+.title{
+	margin: 20rpx auto 40rpx;
+	font-size: 34rpx;
+	font-weight: 600;
+	color: #333333;
+	line-height: 48rpx;
+}
+.score{
+	margin: 40rpx 30rpx 40rpx;
+	padding: 46rpx 20rpx 50rpx;
+	background: #F5F9FC;
+	border-radius: 24rpx;
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #999999;
+	line-height: 36rpx;
+	.score-item{
+		flex: 1;
+		.up{
+			margin-bottom: 10rpx;
+		}
+		.num{
+			font-size: 60rpx;
+			font-weight: 600;
+			color: #009AEF;
+			line-height: 84rpx;
+			margin-right: 10rpx;
+		}
+		& + .score-item{
+			border-left: 1px solid #eee;
+		}
+	}
+}
+</style>

+ 83 - 0
examination/typelist.vue

@@ -0,0 +1,83 @@
+<template>
+	<view class="pages">
+		<u--image :showLoading="false" src="../static/img/typelist-bg.png" width="100%" mode="widthFix"></u--image>
+		<view class="list-wrap u-flex u-flex-wrap u-row-between">
+			<view class="item" @click="typeClick(item)" v-for="(item,index) in typelist" :key="item.id">
+				<view class="name">{{item.name}}</view>
+				<img class="img" :src="item.classifyLogo||'../static/img/type-default-ico.png'" alt="">
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				typelist:[]
+			}
+		},
+		onShow() {
+			this.getclassify()
+		},
+		onLoad() {
+
+		},
+		methods: {
+			getclassify(){
+				this.$u.api.getclassify().then(res=>{
+					this.typelist = res.data.rows.map(item=>{
+						return {name:item.classifyName,id:item.id,classifyLogo:item.classifyLogo}
+					});
+					// console.log('res',res.data);
+				}).catch(err=>{
+					console.log('getclassify',err);
+				})
+			},
+			typeClick(item){
+				console.log('typeClick',item);
+				uni.$u.route('/examination/examination',{
+					id:item.id
+				});
+				
+			}
+
+		}
+	}
+</script>
+<style>
+	page{
+		background-color: #F5F9FC;
+	}
+</style>
+<style lang="scss" scoped>
+.list-wrap{
+	position: relative;
+	top: -24rpx;
+	border-radius: 24rpx 24rpx 0rpx 0rpx;
+	background-color: #F5F9FC;
+	padding: 30rpx 30rpx 0 0 ;
+	.item{
+		box-sizing: border-box;
+		width: calc((100% - 60rpx) / 2);
+		padding: 30rpx;
+		border-radius: 24rpx;
+		background-color: #fff;
+		margin: 0 0 30rpx 30rpx;
+		text-align: right;
+		.name{
+			text-align: left;
+			font-size: 34rpx;
+			font-family: PingFangSC-Semibold, PingFang SC;
+			font-weight: 600;
+			color: #333333;
+			line-height: 48rpx;
+		}
+		.img{
+			width: 120rpx;
+			height: 120rpx;
+			border-radius: 50%;
+		}
+	}
+}
+</style>

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 82 - 0
main.js

@@ -0,0 +1,82 @@
+import App from './App'
+
+// #ifndef VUE3
+import Vue from 'vue'
+Vue.config.productionTip = false
+App.mpType = 'app'
+
+try {
+  function isPromise(obj) {
+    return (
+      !!obj &&
+      (typeof obj === "object" || typeof obj === "function") &&
+      typeof obj.then === "function"
+    );
+  }
+
+  // 统一 vue2 API Promise 化返回格式与 vue3 保持一致
+  uni.addInterceptor({
+    returnValue(res) {
+      if (!isPromise(res)) {
+        return res;
+      }
+      return new Promise((resolve, reject) => {
+        res.then((res) => {
+          if (res[0]) {
+            reject(res[0]);
+          } else {
+            resolve(res[1]);
+          }
+        });
+      });
+    },
+  });
+} catch (error) { }
+
+import uView from '@/uni_modules/uview-ui'
+Vue.use(uView)
+
+import { commonConfig } from './common/config';
+Vue.prototype.$commonConfig = commonConfig;
+
+import './utils/filter' 
+
+import store from '@/store';
+
+// 引入uView提供的对vuex的简写法文件
+let vuexStore = require('@/store/$u.mixin.js');
+Vue.mixin(vuexStore);
+
+const app = new Vue({
+  ...App,
+  store
+})
+
+//uviewui v1
+// // http拦截器,将此部分放在new Vue()和app.$mount()之间,才能App.vue中正常使用
+// import httpInterceptor from '@/common/http.interceptor.js';
+// Vue.use(httpInterceptor, app);
+
+
+// 引入请求封装,将app参数传递到配置中
+require('./common/request.js')(app)
+// http接口API抽离,免于写url或者一些固定的参数
+import httpApi from '@/common/http.api.js';
+Vue.use(httpApi, app);
+
+app.$mount()
+
+
+
+
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+  const app = createSSRApp(App)
+  return {
+    app
+  }
+}
+// #endif

+ 100 - 0
manifest.json

@@ -0,0 +1,100 @@
+{
+    "name" : "security_study_h5",
+    "appid" : "__UNI__ED8B114",
+    "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,
+            "minified" : true,
+            "postcss" : true
+        },
+        "usingComponents" : true,
+        "permission" : {
+            "scope.userLocation" : {
+                "desc" : "你的位置信息将用于小程序位置接口的效果展示"
+            }
+        },
+        "requiredPrivateInfos" : [ "getLocation", "chooseLocation" ],
+        "lazyCodeLoading" : "requiredComponents"
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "2",
+    "h5" : {
+        "devServer" : {
+            "proxy" : {
+                "/api" : {
+                    "target" : "https://securityapi.hw.hongweisoft.com/appapi/app", //请求的目标域名
+                    "changeOrigin" : true,
+                    "secure" : false,
+                    "pathRewrite" : {
+                        //使用代理; 告诉他你这个连接要用代理
+                        "^/api" : ""
+                    }
+                }
+            }
+        },
+        "sdkConfigs" : {
+            "maps" : {}
+        },
+        "template" : "template.h5.html"
+    }
+}

+ 18 - 0
mixin.js

@@ -0,0 +1,18 @@
+export const systemInfo = {
+  data: () => ({
+    statusBarHeight: 0,
+    navigationBarHeight: 0,
+    navHeight: 0,
+    windowHeight: 0, // 可使用窗口高度
+  }),
+ 
+  methods: {
+    // 获取设备信息
+    getSystemInfo() {
+      this.statusBarHeight = getApp().globalData.statusBarHeight
+      this.navigationBarHeight = getApp().globalData.navigationBarHeight
+      this.windowHeight = uni.getSystemInfoSync().windowHeight
+      this.navHeight = getApp().globalData.navHeight
+    },
+  },
+}

+ 24 - 0
package.json

@@ -0,0 +1,24 @@
+{
+  "uni-app": {
+    "scripts": {
+      "build:build64": {
+        "title": "build:build64",
+        "env": {
+          "UNI_PLATFORM": "h5",
+          "H_NODE_ENV": "development",
+          "H_BASE_URL": "https://securityapi.hw.hongweisoft.com/appapi/app",
+          "H_UP_FILE_URL": "https://fileupload.hw.hongweisoft.com/upload/single/minio"
+        }
+      },
+      "build:buildOnline": {
+        "title": "build:online",
+        "env": {
+          "UNI_PLATFORM": "h5",
+          "H_NODE_ENV": "production",
+          "H_BASE_URL": "https://xusapi.gzxsjt.cn/appapi/app",
+          "H_UP_FILE_URL": "https://xusapi.gzxsjt.cn/thirdapi/upload/single/minio"
+        }
+      }
+    }
+  }
+}

+ 204 - 0
pages.json

@@ -0,0 +1,204 @@
+{
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "首页",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/login/login",
+			"style": {
+				"navigationBarTitleText": "登录",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/about/about",
+			"style": {
+				"navigationBarTitleText": "关于我们"
+				// "navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/news/newsdetails",
+			"style": {
+				"navigationBarTitleText": "详情"
+				// "navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/regulation/regulation",
+			"style": {
+				"navigationBarTitleText": "规则"
+				// "navigationStyle": "custom"
+			}
+		}
+	],
+	"subPackages": [
+		{
+			"root": "center",
+			"pages": [
+				{
+					"path": "center",
+					"style": {
+						"navigationBarTitleText": "我的"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "resetpass",
+					"style": {
+						"navigationBarTitleText": "修改密码"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "addcredits",
+					"style": {
+						"navigationBarTitleText": "积分获取记录"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "minuscredits",
+					"style": {
+						"navigationBarTitleText": "积分兑换记录"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "paycode",
+					"style": {
+						"navigationBarTitleText": "兑换码",
+						"navigationStyle": "custom"
+					}
+				}
+			]
+		}, 
+		{
+			"root": "examination",
+			"pages": [
+				{
+					"path": "typelist",
+					"style": {
+						"navigationBarTitleText": "题库"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "examination",
+					"style": {
+						"navigationBarTitleText": "答题"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "complete",
+					"style": {
+						"navigationBarTitleText": "结束答题"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "report",
+					"style": {
+						"navigationBarTitleText": "成绩单"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "answersheet",
+					"style": {
+						"navigationBarTitleText": "答题卡"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "question",
+					"style": {
+						"navigationBarTitleText": "题目解析"
+						// "navigationStyle": "custom"
+					}
+				}
+			]
+		}, 
+		{
+			"root": "study",
+			"pages": [
+				{
+					"path": "studylist",
+					"style": {
+						"navigationBarTitleText": "安全学习"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "studydetails",
+					"style": {
+						"navigationBarTitleText": "学习详情"
+						// "navigationStyle": "custom"
+					}
+				}
+			]
+		}, 
+		{
+			"root": "shopping",
+			"pages": [
+				{
+					"path": "supermarket",
+					"style": {
+						"navigationBarTitleText": "云超市"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "productdetails",
+					"style": {
+						"navigationBarTitleText": "产品详情"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "paysuccess",
+					"style": {
+						"navigationBarTitleText": "兑换成功"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "order",
+					"style": {
+						"navigationBarTitleText": "兑换记录"
+						// "navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "orderdetails",
+					"style": {
+						"navigationBarTitleText": "兑换详情"
+						// "navigationStyle": "custom"
+					}
+				}
+			]
+		}
+	],
+	"preloadRule": {
+		"shopping/productList": {
+			"network": "all",
+			"packages": ["__APP__"]
+		},
+		"xushuo/xushuo": {
+			"network": "all",
+			"packages": ["__APP__"]
+		}
+	},
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "uni-app",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	},
+	"uniIdRouter": {}
+}

+ 38 - 0
pages/about/about.vue

@@ -0,0 +1,38 @@
+<template>
+	<view class="pages">
+		<view class="page-wrap">
+			<u-parse :content="data.content"></u-parse>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				data:{
+					content:'',
+				},
+			}
+		},
+		onShow() {
+			this.getCompanyInfo()
+		},
+		onLoad() {
+		},
+		methods: {
+			getCompanyInfo(){
+				this.$u.api.getCompanyInfo().then(res=>{
+					this.data = res.data;
+					console.log('res',res.data);
+				}).catch(err=>{
+					console.log('getCompanyInfo',err);
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 157 - 0
pages/index/index.vue

@@ -0,0 +1,157 @@
+<template>
+	<view class="pages">
+		<view class="" :style="{height: navHeight+'px' }"></view>
+		<u-swiper
+			:list="bannerList"
+			indicator
+			height="130"
+			keyName="sliderImg"
+			indicatorMode="dot"
+			@click="bannerClick"
+		></u-swiper>
+		<view class="page-wrap">
+			<view class="icon-nav u-flex u-row-around">
+			<!-- 	<view class="nav-item" @click="$u.route('/center/mybalance')">
+					<u--image class="image" :src="staticUrl+'/img/index-nav-4.png'" width="101rpx" height="101rpx"></u--image>
+					需要删除
+				</view> -->
+				<view class="nav-item" @click="$u.route('/examination/typelist')">
+					<u--image class="image" src="../../static/img/index-nav-1.png" width="101rpx" height="101rpx"></u--image>
+					题库
+				</view>
+				<view class="nav-item" @click="$u.route('/study/studylist')">
+					<u--image class="image" src="../../static/img/index-nav-2.png" width="101rpx" height="101rpx"></u--image>
+					安全学习
+				</view>
+				<view class="nav-item" @click="$u.route('/shopping/supermarket')">
+					<u--image class="image" src="../../static/img/index-nav-3.png" width="101rpx" height="101rpx"></u--image>
+					云超市
+				</view>
+				<view class="nav-item" @click="$u.route('/pages/about/about')">
+					<u--image class="image" src="../../static/img/index-nav-4.png" width="101rpx" height="101rpx"></u--image>
+					关于我们
+				</view>
+			</view>
+			<view class="hot-product">
+				<view class="single-til u-flex u-row-between">
+					<text class="text">学习资料</text>
+					<view class="u-flex" @click="$u.route('/study/studylist')">
+						<text class="more-text">更多</text>
+						<u-icon name="arrow-right" color="#676767" size="24rpx"></u-icon>
+					</view>
+				</view>
+				<view class="course u-flex u-col-top" v-for="(item,index) in courseList" :key="item.id" @click="$u.route('/study/studydetails',{id:item.id})">
+					<view class="pic">
+						<u--image mode="scaleToFill" width="180rpx" height="170rpx" radius="10rpx" :showLoading="true" class="image" :src="item.mainImg"></u--image>
+					</view>
+					<view class="text">
+						<view class="title ellipsis-2">
+							<text class="new" v-if="index<9">最新</text>
+							{{item.title}}
+						</view>
+						<view class="classify">
+							<!-- <text class="classify-item">测试</text> -->
+						</view>
+						<view class="studyUserNum">
+							{{item.studyUserNum}}人已学习
+						</view>
+					</view>
+				</view>
+			</view>
+			<tabbar :tabbarIndexProps='0' />
+		</view>
+		<u-toast ref="uToast"></u-toast>
+	</view>
+</template>
+
+<script>
+	import { systemInfo } from "@/mixin.js";
+	import tabbar from "../../components/tabbar.vue";
+	export default {
+		components:{
+			tabbar,
+		},
+		mixins:[systemInfo],
+		data() {
+			return {
+				staticUrl:this.$commonConfig.staticUrl,
+				advantageSize:14,
+				bannerList: [],
+				courseList:[],
+				xsIntro:{},
+				indexData:{},
+			}
+		},
+		onLoad() {
+			// let userInfo = uni.getStorageSync('userInfo');
+			// console.log('userInfo',userInfo);
+			let that = this;
+			this.swiperList();
+			this.getSystemInfo();
+			// console.log('statusBarHeight',this.statusBarHeight);
+			// console.log('navigationBarHeight',this.navigationBarHeight);
+			// console.log('windowHeight',this.windowHeight);
+			// console.log('navHeight',this.navHeight);
+		},
+		onShow() {
+			this.getStudyLib();
+		},
+		methods: {
+			swiperList(){
+				this.$u.api.swiperList({postion:1}).then(res=>{
+					this.bannerList = res.data.rows;
+					// console.log('res',res.data.rows);
+				}).catch(err=>{
+					console.log('swiperList',err.data);
+				})
+			},
+			bannerClick(e){
+				console.log('e',e);
+				// console.log('bannerClick',this.bannerList[e]);
+				let item = this.bannerList[e];
+				uni.$u.route('/pages/news/newsdetails', {
+					type: 'swiperDetail',
+					id: item.id
+				});
+			},
+			getStudyLib(){
+				this.$u.api.studyLib({pageNum:1,pageSize:10}).then(res=>{
+					this.courseList = res.data.rows;
+					// console.log('res',res);
+				}).catch(err=>{
+					console.log('getHotGoods',err.data);
+				})
+			}
+
+		}
+	}
+</script>
+<style>
+page{
+	background-color: #fff;
+	padding-top: 0;
+}
+</style>
+<style lang="scss" scoped>
+.page-bg{
+	.img{
+		height: 100vh;
+	}
+}
+.icon-nav{
+	margin: 50rpx auto;
+}
+.product{
+	background-color: #F5F5F5;
+	border-bottom: 0;
+	.btn{
+		padding: 14rpx 40rpx;
+		background: linear-gradient(90deg, #00D17D 0%, #00A447 100%);
+		color: #fff;
+		position: relative;
+		right: -20rpx;
+		border-top-left-radius: 50rpx;
+		border-bottom-left-radius: 50rpx;
+	}
+}
+</style>

+ 183 - 0
pages/login/login.vue

@@ -0,0 +1,183 @@
+<template>
+	<view class="body" :style="{height:screenHeight}">
+		<view class="header">
+			<view class="text1">安全生产<br />宣传教育<br />平台</view>
+			<view class="text2 u-flex"><view class="line"></view>员工学习</view>
+		</view>
+		<view class="login-box">
+			<view class="title">账号密码登录</view>
+			<u--form labelPosition="left" :model="form" :rules="rules" ref="uForm" >
+				<u-form-item label="" prop="mobile" ref="mobile" borderBottom >
+					<u--input
+						v-model="form.mobile"
+						border="none"
+						placeholder="请输入手机号码"
+						:customStyle="inputCustomStyle"
+					></u--input>
+				</u-form-item>
+				<u-form-item label="" prop="password" ref="password" borderBottom >
+					<u--input
+						v-model="form.password"
+						border="none"
+						:password="true"
+						placeholder="输入密码"
+						:customStyle="inputCustomStyle"
+					></u--input>
+				</u-form-item>
+			</u--form>
+			<u-button 
+				@click="submit"
+				text="登录" 
+				type="primary" 
+				:customStyle="{'margin-top':'60rpx',height:'98rpx','box-sizing':'border-box'}"
+				>
+			</u-button>
+		</view>
+		<view class="tip">
+			贵州省公路建设养护集团有限公司贵阳分公司
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				bname:'',
+				//屏幕高度
+				screenHeight: "",
+				backUrl:null,
+				form:{
+					mobile:'',
+					password:''
+				},
+				rules: {
+					mobile: {
+						type: 'string',
+						required: true,
+						message: '请输入手机号码',
+						trigger: ['blur', 'change']
+					},
+					password: {
+						type: 'string',
+						required: true,
+						message: '请填写密码',
+						trigger: ['blur', 'change']
+					},
+				},
+				inputCustomStyle:{
+					height:'98rpx',
+					'border-color':'#eee',
+					'padding-left':'0',
+					'box-sizing':'border-box',
+				}
+			};
+		},
+		onLoad() {
+			// 测试环境填充用户名密码
+			if(process.env.NODE_ENV=='development'){
+				this.form.mobile = '18085000374';
+				this.form.password = '000000';
+			}
+			let that = this;
+			uni.getSystemInfo({
+				success: (res) => {
+					this.screenHeight = res.windowHeight + "px"
+				}
+			});
+			uni.getStorage({
+				key: 'backUrl',
+				success: function (res) {
+					console.log('getStorage',res);
+				},
+				complete(res) {
+					if(res.data){
+						that.backUrl = '/'+res.data;
+					}else{
+						that.backUrl = '/pages/index/index';
+					}
+					console.log('backUrl',that.backUrl);
+				}
+			});
+		},
+		onReady() {
+			//onReady 为uni-app支持的生命周期之一
+			this.$refs.uForm.setRules(this.rules)
+		},
+		onShow() {
+		},
+		methods: {
+			submit(){
+				// console.log('form',this.form);
+				this.$refs.uForm.validate().then(res => {
+					// uni.$u.toast('校验通过')
+					this.$u.api.login(this.form).then(res=>{
+						// console.log('res',res.data);
+						this.$u.vuex('vuex_user_info', res.data);
+						uni.removeStorage({
+							key:'backUrl'
+						})
+						uni.reLaunch({url: this.backUrl});
+					}).catch(err=>{
+						console.log('login',err);
+					})
+				}).catch(errors => {
+					uni.$u.toast('请正确填写表单')
+				})
+			}
+			
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.body {
+	background-color: #fff;
+	padding: 45rpx;
+	box-sizing: border-box;
+}
+.header{
+	color: $uni-color-primary;
+	.text1{
+		font-size: 72rpx;
+		font-family: AlibabaPuHuiTi_2_105_Heavy;
+		line-height: 88rpx;
+		margin-bottom: 40rpx;
+		font-weight: bold;
+	}
+	.text2{
+		font-size: 32rpx;
+		font-family: PingFangSC-Regular, PingFang SC;
+		font-weight: 400;
+		margin-bottom: 100rpx;
+		.line{
+			width: 116rpx;
+			height: 2rpx;
+			background-color:  $uni-color-primary;
+			margin-right: 24rpx;
+		}
+	}
+}
+.login-box{
+	position: relative;
+	background-color: #fff;
+	.title{
+		font-size: 36rpx;
+		font-family: PingFangSC-Regular, PingFang SC;
+		font-weight: 400;
+		color: #333333;
+		line-height: 50rpx;
+	}
+}
+.tip{
+	position: fixed;
+	bottom: 108rpx;
+	width: calc( 100vw - 90rpx );
+	box-sizing: border-box;
+	text-align: center;
+	font-size: 24rpx;
+	font-weight: 400;
+	color: #999;
+}
+</style>
+

+ 134 - 0
pages/news/newsdetails.vue

@@ -0,0 +1,134 @@
+<template>
+	<view class="">
+		<u-navbar
+			title="详情"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar>
+		<view class="page-wrap">
+			<view class="base-info" v-if="type=='swiperDetail'">
+				<view class="title">{{swiperDetails.name}}</view>
+				<view class="info u-flex u-row-between">
+					<view class="time">{{swiperDetails.publicTime}}</view>
+					<!-- <view class="view-count">{{swiperDetails.viewCount}}</view> -->
+				</view>
+			</view>
+			<view v-else class="base-info">
+				<view class="title">{{pageData.title}}</view>
+				<view class="info u-flex u-row-between">
+					<view class="time">{{pageData.publicTime}}</view>
+					<view class="view-count">浏览量:{{pageData.viewCount}}</view>
+				</view>
+			</view>
+			
+			<view class="" v-if="type=='swiperDetail'">
+				<u--image :src="swiperDetails.sliderImg" :showLoading="true" width="100%" mode="aspectFit"></u--image>
+				<!-- <view class="" v-html="swiperDetails.detail"></view> -->
+				<u-parse :content="swiperDetails.detail"></u-parse>
+			</view>
+			<view v-else class="content-wrap">
+				<u--image :src="pageData.mainImg" :showLoading="true" width="100%" mode="aspectFit"></u--image>
+				<view class="content">
+					<u-parse :content="pageData.content"></u-parse>
+					<!-- {{pageData.content}} -->
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default{
+	data(){
+		return{
+			id:null,
+			type:'',
+			pageData:{
+				title:'',
+				content:''
+			},
+			swiperDetails:{},
+		}
+	},
+	onLoad(page) {
+		this.id = page.id;
+		this.type = page.type;
+		if(this.type=='swiperDetail'){
+			console.log('this.id',this.id);
+			this.getSwiperDetails(this.id);
+		}else{
+			this.getDetails(this.id);
+			this.addViewCount(this.id);
+		}
+		
+	},
+	methods:{
+		getDetails(id){
+			this.$u.api.shopNews({id:id}).then(res=>{
+				this.pageData = res.data;
+				// console.log('res',res);
+				}).catch(err=>{
+				console.log('shopNews',err.data);
+			})
+		},
+		getSwiperDetails(id){
+			this.$u.api.swiperDetails({id:id}).then(res=>{
+				this.swiperDetails = res.data;
+				// console.log('res',res);
+				}).catch(err=>{
+				console.log('getSwiperDetails',err.data);
+			})
+		},
+		addViewCount(id){
+			this.$u.api.addViewCount({id:id}).then(res=>{
+				// console.log('res',res);
+				}).catch(err=>{
+				console.log('addViewCount',err.data);
+			})
+		}
+	}
+}
+</script>
+<style>
+page{background-color: #F5F5F5;}
+</style>
+<style lang="scss" scoped>
+.title{
+	font-size: 36rpx;
+	font-weight: 600;
+	color: #333333;
+	line-height: 50rpx;
+	margin-bottom: 20rpx;
+}
+.info{
+	margin-bottom: 50rpx;
+	font-size: 24rpx;
+	font-weight: 400;
+	color: #999999;
+	line-height: 33rpx;
+}
+.page-wrap{
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #666666;
+	line-height: 44rpx;
+	p{
+		text-indent: 2em;
+	}
+	/deep/ .u-image{
+		margin-bottom: 20rpx;
+	}
+}
+.content{
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #666666;
+	line-height: 44rpx;
+	margin: 20rpx 0;
+}
+.u-image{
+	max-width: 100%;
+}
+</style>

+ 61 - 0
pages/regulation/regulation.vue

@@ -0,0 +1,61 @@
+<template>
+	<view class="">
+		<!-- <u-navbar
+			:title="title"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<!-- <view class="title">积分规则</view> -->
+		<view class="page-wrap">
+			<u-parse :content="content"></u-parse>
+		</view>
+		
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				title:'',
+				regulationName:'',
+				content:'',
+			}
+		},
+		onShow() {	
+			
+		},
+		onLoad(page) {
+			this.title = page.regulationName;;
+			this.regulationName = page.regulationName;
+			// console.log('page',page);
+			this.getPageData();
+			uni.setNavigationBarTitle({
+			    title: this.title
+			});
+		},
+		methods: {
+			getPageData(){
+				this.$u.api.memberCreditDesc({type:1,name:this.regulationName}).then(res=>{
+					this.content = res.data.content
+					console.log('res',res.data);
+				}).catch(err=>{
+					console.log('memberCreditDesc',err);
+				})
+			}
+
+		}
+	}
+</script>
+<style>
+page{
+	background-color: #F5F9FC;
+}
+</style>
+<style lang="scss" scoped>
+.title{
+	text-align: center;
+}
+</style>

+ 211 - 0
shopping/order.vue

@@ -0,0 +1,211 @@
+<template>
+	<view class="">
+	<!-- 	<u-navbar
+			title="我的业绩"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<view class="page-wrap">
+			<img src="../static/img/order-banner.png" alt="">
+		</view>
+		<view class="tabs-wrap">
+			<u-tabs 
+			:list="tabsList" 
+			lineColor="#009AEF" 
+			 :activeStyle="{color:'#333','font-weight': '600','font-size':'30rpx'}"
+			 :inactiveStyle="{color:'#999'}"
+			@click="tabsClick"></u-tabs>
+		</view>
+		<mescroll-body class="" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view class="page-wrap" v-show="dataList.length>0" >
+				<view class="list">
+					<view class="item u-flex u-col-top" @click="goDetail(item)" v-for="item in dataList" :key="item.id">
+						<img class="img" :src="item.mainImg" alt="">
+						<view class="text">
+							<view class="title ellipsis-2">{{item.goodsName}}</view>
+							<view class="credit">{{item.credit}}积分</view>
+							<view class="bottom u-flex u-row-between">
+								<view class="quantity">兑换数量:{{item.quantity}}</view>
+								<u-button v-if="item.status==0" @click.native.stop="viewCode(item)" type="primary" :customStyle="{height:'58rpx',width: 'auto','margin-right': 'unset'}" text="兑换码"></u-button>
+								<u-button v-if="item.status==1" @click.native.stop="goDetail(item)" type="primary" :customStyle="{height:'58rpx',width: 'auto','margin-right': 'unset','background-color':'#CCEBFC','border-color':'#CCEBFC'}" text="详情"></u-button>
+								<u-button v-if="item.status==2" @click.native.stop="goDetail(item)" type="primary" :customStyle="{height:'58rpx',width: 'auto','margin-right': 'unset','background-color':'#CCEBFC','border-color':'#CCEBFC'}" text="详情"></u-button>
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</mescroll-body>
+		<tabbar :tabbarIndexProps='1' />
+	</view>
+</template>
+
+<script>
+	import tabbar from "../components/tabbar.vue";
+	// 引入mescroll-mixins.js
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		components:{
+			tabbar,
+		},
+		data() {
+			return {
+				totalAmount:'',
+				downOption: {},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				tabsList:[{name:'未领取',status:'0'},{name:'已领取',status:1},{name:'已失效',status:2}],
+				status:'',
+				params:{
+				},
+				activeIndex:0,
+				dataList: []
+			}
+		},
+		onShow() {
+			
+		},
+		onLoad() {
+			this.params.status = this.tabsList[this.activeIndex].status;
+			// console.log('1111', this.tabsList[this.activeIndex]);
+		},
+		methods: {
+			/*下拉刷新的回调, 重置列表为第一页 (此处可删,mixins已默认)
+			downCallback(){
+				this.mescroll.resetUpScroll();
+			},
+			/*上拉加载的回调*/
+			upCallback(page) {
+				// 此处可以继续请求其他接口
+				// if(page.num == 1){
+				// 	// 请求其他接口...
+				// }
+			
+				// 如果希望先请求其他接口,再触发upCallback,可参考以下写法
+				// if(!this.params.id){
+				// 	this.mescroll.endErr()
+				// 	return // 此处return,先获取xx
+				// }
+			
+				let pageNum = page.num; // 页码, 默认从1开始
+				let pageSize = page.size; // 页长, 默认每页10条isAsc:0//时间排序 0:降序 1:升序 (默认星级降序排序)
+
+				this.params = Object.assign(this.params,{pageNum:pageNum,pageSize:pageSize});
+				this.$u.api.memberExchangeRecord(this.params).then(data => {
+					// console.log('data',JSON.parse(JSON.stringify(data)));
+					// 接口返回的当前页数据列表 (数组)
+					let curPageData = data.data.rows;
+					this.totalAmount = data.data.total;
+					// console.log('curPageData',JSON.parse(JSON.stringify(curPageData)));
+					// 接口返回的当前页数据长度 (如列表有26个数据,当前页返回8个,则curPageLen=8)
+					let curPageLen = curPageData.length; 
+					// 接口返回的总页数 (如列表有26个数据,每页10条,共3页; 则totalPage=3)
+					// let totalPage =  data.data.data.totalPage; 
+					// 接口返回的总数据量(如列表有26个数据,每页10条,共3页; 则totalSize=26)
+					let totalSize = curPageData.total; 
+					// 接口返回的是否有下一页 (true/false)
+					// let hasNext = data.xxx; 
+					// console.log('totalPage',totalPage,'curPageLen',curPageLen);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动置空列表
+					this.dataList = this.dataList.concat(curPageData); //追加新数据
+					// 请求成功,隐藏加载状态
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					// this.mescroll.endByPage(curPageLen, totalPage); 
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					this.mescroll.endBySize(curPageLen, totalSize); 
+				}).catch(err => {
+					this.mescroll.endErr()
+					console.log(err)
+				});	
+			
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList() {
+				this.mescroll.resetUpScroll();
+			},
+			tabsClick(item){
+				this.params.status = item.status;
+				this.reloadList()
+				console.log('item',item);
+			},
+			viewCode(item){
+				// console.log('viewCode',item);
+				uni.$u.route('/center/paycode', {
+					qrContent: item.exchangeCode,
+				});
+			},
+			goDetail(item){
+				// console.log('goDetail',item);
+				uni.$u.route('/shopping/orderdetails', {
+					id: item.id,
+				});
+			}
+		}
+	}
+</script>
+<style>
+page{
+	background-color: #F5F9FC;
+}
+</style>
+<style lang="scss" scoped>
+.tabs-wrap{
+	background-color: #fff;
+	margin-bottom: 0;
+}
+.list{
+	border-radius: 8rpx;
+	padding: 0 20rpx;
+	background-color: #fff;
+	.item{
+		padding: 28rpx 0;
+		background-color: #fff;
+		.img{
+			width: 200rpx;
+			height: 202rpx;
+		}
+		.text{
+			flex: 1;
+			margin-left: 40rpx;
+			.title{
+				font-size: 30rpx;
+				font-family: PingFangSC-Regular, PingFang SC;
+				font-weight: 400;
+				color: #333333;
+				line-height: 42rpx;
+				margin-bottom: 20rpx;
+			}
+			.credit{
+				font-size: 32rpx;
+				font-family: PingFangSC-Semibold, PingFang SC;
+				font-weight: 600;
+				color: #FFB100;
+				line-height: 44rpx;
+				margin-bottom: 20rpx;
+			}
+			.quantity{
+				font-size: 28rpx;
+				font-family: PingFangSC-Regular, PingFang SC;
+				font-weight: 400;
+				color: #999999;
+				line-height: 40rpx;
+			}
+		}
+		&:not(:last-of-type){
+			border-bottom: 0.5px solid #ddd;
+			padding-bottom: 20rpx;
+		}
+	}
+}
+</style>

+ 163 - 0
shopping/orderdetails.vue

@@ -0,0 +1,163 @@
+<template>
+    <view class="">
+		<!-- <u-navbar
+			:title="title"
+			:placeholder="true"
+			@leftClick="leftClick"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<view class="page-wrap">
+			<view class="block goods u-flex u-col-top">
+				<img class="img" :src="orderDetails.mainImg" alt="">
+				<view class="text">
+					<view class="title ellipsis-2">{{orderDetails.goodsName}}</view>
+					<view class="bottom u-flex u-row-between">
+						<view class="price">{{orderDetails.credit}}积分</view>
+						<view class="quantity">共{{orderDetails.quantity}}件</view>
+					</view>
+				</view>
+			</view>
+			<view class="block info">
+				<view class="info-item u-flex">
+					<text class="til">兑换时间</text>
+					<text>{{orderDetails.startTime}}</text>
+				</view>
+				<view class="info-item u-flex" v-if="orderDetails.checkTime">
+					<text class="til">核销时间</text>
+					<text>{{orderDetails.checkTime}}</text>
+				</view>
+				<view class="info-item u-flex" >
+					<text class="til">失效时间</text>
+					<text>{{orderDetails.startEnd}}</text>
+				</view>
+				<view class="info-item u-flex"  v-if="orderDetails.checkName">
+					<text class="til">核销人</text>
+					<text>{{orderDetails.checkName}}</text>
+				</view>
+			</view>
+			<view class="status"
+				:class="{
+					status0:orderDetails.status == 0,
+					status1:orderDetails.status == 1,
+					status2:orderDetails.status == 2,
+				}"
+				>
+				{{orderDetails.status|filterOrderStatus}}
+			</view>
+		</view>
+    </view>
+</template>
+<script>
+    export default {
+        data() {
+            return {
+				id:'',
+				orderDetails:{},
+            }
+        },
+		onLoad(page) {
+			this.id = page.id;
+		},
+		onShow() {
+			this.getOrderDetails(this.id);
+		},
+		computed:{
+			// payType(){
+			// 	return function(value){
+			// 		let payTypeList = ['', '微信', '余额', '积分']
+			// 		return '-' + payTypeList[value]
+			// 	}
+			// }
+		},
+        methods: {
+			leftClick(){
+				console.log('orderdetails leftClick');
+			},
+			getOrderDetails(id){
+				this.$u.api.exchangeDetails({id:id}).then(res=>{
+					this.orderDetails = res.data
+					// console.log('orderDetails',JSON.parse(JSON.stringify(res.data)));
+				}).catch(err=>{
+					console.log('getOrderDetails',err);
+				})
+			},
+        }
+    }
+</script>
+<style>
+	page{
+		background: #F5F9FC;
+	}
+</style>
+<style lang="scss" scoped>
+.block{
+	background: #FFFFFF;
+	border-radius: 24rpx;
+	margin-bottom: 10rpx;
+	padding: 30rpx;
+}
+.goods{
+	.img{
+		width: 180rpx;
+		height: 180rpx;
+	}
+	.text{
+		flex: 1;
+		margin-left: 40rpx;
+		.title{
+			font-size: 30rpx;
+			font-family: PingFangSC-Regular, PingFang SC;
+			font-weight: 400;
+			color: #333333;
+			line-height: 42rpx;
+			margin-bottom: 20rpx;
+			min-height: 84rpx;
+		}
+		.bottom{
+			font-size: 28rpx;
+			font-family: PingFangSC-Regular, PingFang SC;
+			font-weight: 400;
+			color: #999999;
+			line-height: 40rpx;
+			.price{
+				font-size: 32rpx;
+				font-weight: 600;
+				color: #FFB100;
+				line-height: 44rpx;
+			}
+		}
+	}
+}
+.info{
+	.info-item{
+		font-size: 30rpx;
+		font-family: PingFangSC-Regular, PingFang SC;
+		font-weight: 400;
+		color: #333333;
+		line-height: 42rpx;
+		margin-bottom: 20rpx;
+		.til{
+			color: #999;
+			margin-right: 40rpx;
+		}
+	}
+}
+.status{
+	padding: 26rpx 0;
+	background: #FFFFFF;
+	border-radius: 24rpx;
+	font-size: 34rpx;
+	font-weight: 600;
+	color: #009AEF;
+	line-height: 48rpx;
+	text-align: center;
+	&.status1{
+		color: #333;
+	}
+	&.status2{
+		color: #999;
+	}
+}
+</style>

+ 88 - 0
shopping/paysuccess.vue

@@ -0,0 +1,88 @@
+<template>
+	<view class="pages">
+		<view class="icon-wrap u-flex u-row-center" v-if="success=='true'">
+			<u-icon name="checkmark" color="#fff" size="80"></u-icon>
+		</view>
+		<view class="icon-wrap u-flex u-row-center" v-else style="background-color: #FFB100;">
+			<u-icon name="close" color="#fff" size="80"></u-icon>
+		</view>
+		<view class="title">{{title}}</view>
+		<view class="full-btn" v-if="success=='true'" @click="btnClick">查看兑换码</view>
+		<view class="msg" v-else>{{msg}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				title:'兑换成功',
+				codeData:null,
+				msg:null,
+				success:null,
+			}
+		},
+		onShow() {		
+		},
+		onLoad(page) {
+			this.codeData = page.codeData;
+			this.msg = page.msg;
+			this.success = page.success;
+			console.log('success',this.success);
+			if(this.success=='true'){
+				this.title = '兑换成功';
+			}else{
+				this.title = '兑换失败';
+			}
+			uni.setNavigationBarTitle({
+			    title:this.title
+			});
+		},
+		methods: {
+			btnClick(){
+				uni.$u.route('/center/paycode', {
+					qrContent: this.codeData,
+				});
+				// uni.reLaunch({url: `/center/paycode?qrContent=${this.codeData}`});
+			}
+		}
+	}
+</script>
+<style>
+	page{
+		background-color: #F5F5F5;
+	}
+</style>
+<style lang="scss" scoped>
+.pages{
+	text-align: center;
+}
+
+.icon-wrap{
+	width: 180rpx;
+	height: 180rpx;
+	margin: 120rpx auto 20rpx;
+	padding: 20rpx;
+	background-color: #00EFB2;
+	border-radius: 50%;
+}
+.title{
+	font-size: 34rpx;
+	font-family: PingFangSC-Semibold, PingFang SC;
+	font-weight: 600;
+	color: #333333;
+	line-height: 48rpx;
+	margin-bottom: 120rpx;
+}
+.full-btn{
+	margin: 20rpx 10%;
+	background-color: #009AEF;
+}
+.msg{
+	font-size: 30rpx;
+	font-family: PingFangSC-Regular, PingFang SC;
+	font-weight: 400;
+	color: #999999;
+	line-height: 42rpx;
+}
+</style>

+ 299 - 0
shopping/productdetails.vue

@@ -0,0 +1,299 @@
+<template>
+	<view class="pages">
+	<!-- 	<u-navbar
+			title="产品详情"
+			:placeholder="true"
+			:autoBack="false"
+			@leftClick="leftClick"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		 <u-swiper
+			v-if="details.slideImgList.length>0"
+			:list="details.slideImgList"
+			height="700rpx"
+			@change="e => currentNum = e.current"
+			:autoplay="false"
+			indicatorStyle="right: 20px"
+		>
+			<view
+				slot="indicator"
+				class="indicator-num"
+			>
+				<text class="indicator-num__text">{{ currentNum + 1 }}/{{ details.slideImgList.length }}</text>
+			</view>
+		</u-swiper>
+		<view class="product-info view-wrap">
+			<view class="price u-flex u-row-between">
+				<view class="u-flex">
+					<img style="margin-right: 10rpx;" src="../static/img/integral-ico-y.png" alt="">
+					{{details.exchangeCredit}}积分
+				</view>
+				<view class="exchange-num">已兑换{{details.saleCount}}</view>
+			</view>
+			<view class="name">{{details.goodsName}}</view>
+		</view>
+		<view class="specification info-line u-flex view-wrap" v-if="details.specification">
+			<view class="info-til">规格</view>
+			<view class="info-con u-flex">{{details.specification}}</view>
+		</view>
+	<!-- 	<view class="addr view-wrap" v-if="details.period">
+			<view class="addr-line u-flex" v-if="details.period">
+				<view class="info-til">保质期</view>
+				<view class="info-con u-flex">
+					{{details.period}}(天)
+				</view>
+			</view>
+		</view> -->
+		<view class="detail view-wrap">
+			<view class="til">详情</view>
+			<view class="con">
+				<u-parse :content="details.detail"></u-parse>
+			</view>
+		</view>
+		<view class="details-tool-wrap">
+			<view class="details-tool">
+				<view class="full-btn" @click="addCreditOrder">立即兑换</view>
+			</view>
+		</view>
+		<u-popup :show="popupShow" mode="bottom"  @close="close">
+			<view class="select-num">
+				<view class="header u-flex">
+					<u-icon name="close" color="#666" size="28" @click="close"></u-icon>
+				</view>
+				<view class="select-wrap u-flex u-row-between">
+					<view class="title">购买数量(件)</view>
+					<u-number-box v-model="param.quantity" @change="quantityChange" :max="Math.floor(credit/details.exchangeCredit)"></u-number-box>
+				</view>
+				<view class="full-btn" @click="exchange">确定兑换</view>
+			</view>
+		</u-popup>
+		<u-toast ref="uToast"></u-toast>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				id:'',
+				cartTotal:0,
+				currentNum:0,
+				swiperList: [],
+				details:{
+					slideImgList:[],
+					exchangeCredit:1
+				},
+				credit:0,
+				popupShow:false,
+				param:{
+					goodsId:'',
+					quantity:1,
+				}
+			}
+		},
+		onLoad(page) {
+			this.id = page.id;
+			this.param.goodsId = page.id;
+			// console.log('this.id',this.id);
+			this.getDetails(this.id);
+			this.getMemberCredit();
+		},
+		onShow(){
+			// this.getCartList();
+			// this.getAddrList();
+		},
+		methods: {
+			leftClick(e){
+				let pages = getCurrentPages();
+				if(pages.length==1){
+					uni.$u.route('/pages/index/index')
+				}else{
+					uni.navigateBack()
+				};
+			},
+			getMemberCredit(){
+				this.$u.api.memberCredit().then(res=>{
+					this.credit = res.data.credit
+					// console.log('memberCredit',this.credit);
+				}).catch(err=>{
+					console.log('memberCredit',err.data);
+				})
+			},
+			getDetails(id){
+				this.$u.api.memberGoodDetails({id:id}).then(res=>{
+					// console.log('res',res.data);
+					this.details = res.data;
+					if(!res.data.slideImgList&&res.data.mainImg){
+						this.details.slideImgList = [];
+						this.details.slideImgList.push(res.data.mainImg)
+					}
+				}).catch(err=>{
+					console.log('getDetails',err.data);
+				})
+			},
+			addCreditOrder(id){
+				this.popupShow = true
+			},
+			exchange(){
+				// console.log('param',this.param);
+				this.$u.api.exchangeGoods(this.param).then(res=>{
+					this.close();
+					uni.$u.route('/shopping/paysuccess', {
+						codeData: res.data.exchangeCode,
+						msg:res.msg,
+						success:true
+					});
+					// console.log('exchangeGoods',res.data);
+				}).catch(err=>{
+					this.close();
+					uni.$u.route('/shopping/paysuccess', {
+						codeData: null,
+						msg:err.msg,
+						success:false
+					});
+					console.log('exchangeGoods',err.data);
+				})
+			},
+			close() {
+				this.popupShow = false
+			},
+			quantityChange(e){
+				// console.log('quantityChange',e);
+			}
+
+		}
+	}
+</script>
+<style lang="scss" scoped>
+.indicator-num {
+	padding: 2px 0;
+	background-color: rgba(0, 0, 0, 0.35);
+	border-radius: 100px;
+	width: 35px;
+	@include flex;
+	justify-content: center;
+
+	&__text {
+		 color: #FFFFFF;
+		 font-size: 12px;
+	 }
+}
+.product-info{
+	.price{
+		font-size: 44rpx;
+		font-weight: 600;
+		color: #FFB100;
+		line-height: 60rpx;
+		.exchange-num{
+			font-size: 28rpx;
+			font-weight: 400;
+			color: #999999;
+			line-height: 40rpx;
+		}
+	}
+	.name{
+		margin-top: 30rpx;
+		font-size: 36rpx;
+		font-weight: 600;
+		color: #333;
+		line-height: 50rpx;
+	}
+}
+.info-line{
+	margin-bottom: 20rpx;
+}
+.info-til{
+	color: #999;
+	margin-right: 40rpx;
+}
+.info-con{
+	color: #666;
+}
+.addr{
+	font-size: 30rpx;
+	.addr-line:not(:last-child){
+		
+	}
+	
+}
+.comment{
+	color: #999;
+	.til{
+		font-size: 30rpx;
+		font-weight: 600;
+		margin-right: 20rpx;
+		color: #333;
+	}
+}
+.detail{
+	.til{
+		font-size: 30rpx;
+		color: #333;
+		font-weight: 600;
+		margin-bottom: 20rpx;
+	}
+	.con{
+		// background-color: #F5F5F5;
+		img{max-width: 100%;}
+	}
+}
+.details-tool-wrap{
+	height: 98rpx;
+	.details-tool{
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		height: 98rpx;
+		background-color: #fff;
+	}
+	.left{
+		text-align: center;
+		color: #666;
+		font-size: 24rpx;
+		.icon-wrap{
+			position: relative;
+			margin-left: 20rpx;
+		}
+	}
+	.right{
+		.btn{
+			padding: 10rpx 40rpx;
+			border-radius: 40rpx;
+			color: #fff;
+			margin-right: 20rpx;
+			font-size: 30rpx;
+		}
+		.add-btn{
+			background-color: #FFB100;
+		}
+		.buy-btn{
+			background-color: #FF3C3F;
+		}
+		.gray{
+			background-color: #ddd;
+			color: #999;
+		}
+	}
+}
+.full-btn{
+	margin: 0;
+	border-radius: 0;
+	background-color: #009AEF;
+	padding: 0;
+	height: 98rpx;
+	line-height: 98rpx;
+}
+.select-num{
+	.header{
+		padding: 30rpx 30rpx 0;
+		text-align: right;
+		justify-content: end;
+		margin-bottom: 40rpx;
+	}
+	.select-wrap{
+		padding: 30rpx;
+	}
+}
+</style>

+ 293 - 0
shopping/supermarket.vue

@@ -0,0 +1,293 @@
+<template>
+	<view class="page-wrap">
+		<!-- <u-navbar
+			title="积分商城"
+			:placeholder="true"
+			:autoBack="true"
+			:safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<view class="header">
+			<view class="inner">
+				<view class="top u-flex u-row-between">
+					<view class="u-flex">
+						<img src="../static/img/integral-ico.png" alt="">
+						<text>积分余额</text>
+					</view>
+					<view class="right u-flex" @click="$u.route('/pages/regulation/regulation',{regulationName:'积分规则'})">
+						<u-icon name="error-circle" color="#fff" size="34rpx"></u-icon>
+						<text>规则</text>
+					</view>
+				</view>
+				<view class="num" @click="$u.route('/center/addcredits')">{{credit}}</view>
+				<!-- <view class="bottom">
+					<text  @click="$u.route('/credits/creditslist')">积分明细</text>
+				</view> -->
+			</view>
+		</view>
+		<view class="tabs-wrap">
+			<u-tabs 
+			:list="tabsList" 
+			lineColor="#009AEF" 
+			 :activeStyle="{color:'#333','font-weight': '600','font-size':'30rpx'}"
+			 :inactiveStyle="{color:'#999'}"
+			@click="tabsClick"></u-tabs>
+		</view>
+		<view class="">
+			<mescroll-body class="" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+				<view class="credit-product u-flex u-flex-wrap">
+					<view class="credit-product-item" @click="goDetails(item.id)" v-for="(item,index) in dataList" :key="item.id">
+						<view class="pic">
+							<u--image class="image" :src="item.mainImg" width="100%" height="340rpx"></u--image>
+						</view>
+						<view class="text">
+							<view class="til ellipsis-2">
+								{{item.goodsName}}
+							</view>
+							<view class="price u-flex">
+								<img style="margin-right: 10rpx;" src="../static/img/integral-ico-y.png" alt="">
+								{{item.exchangeCredit}}积分
+							</view>
+						</view>
+					</view>
+				</view>
+			</mescroll-body>
+		</view>
+	</view>
+</template>
+
+<script>
+	// 引入mescroll-mixins.js
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		data() {
+			return {
+				downOption: {},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 3, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				hasAddr:false,
+				staticUrl:this.$commonConfig.staticUrl,
+				tabsList:[],
+				credit:'',
+				hasTypeId:false,
+				typeId:'',
+				dataList:[],
+			}
+		},
+		onShow() {	
+			this.getCredit();//积分
+			// this.getAddrList();
+		},
+		onLoad() {
+			// this.getTypeList();
+		},
+		methods: {
+			/*下拉刷新的回调, 重置列表为第一页 (此处可删,mixins已默认)
+			downCallback(){
+				this.mescroll.resetUpScroll();
+			},
+			/*上拉加载的回调*/
+			upCallback(page) {
+				// 此处可以继续请求其他接口
+				// if(page.num == 1){
+				// 	// 请求其他接口...
+				// }
+			
+				// 如果希望先请求其他接口,再触发upCallback,可参考以下写法
+				if(!this.hasTypeId){
+					this.getTypeList();
+					return // 此处return,先获取xx
+				}
+			
+				let pageNum = page.num; // 页码, 默认从1开始
+				let pageSize = page.size; // 页长, 默认每页10条
+			
+				let params = {
+					pageNum : page.num,
+					pageSize :  page.size,
+					classifyId : this.typeId
+				}
+				// console.log('this.params',params);
+				this.$u.api.memberGoodList(params).then(data => {
+					// console.log('data',JSON.parse(JSON.stringify(data)));
+					// 接口返回的当前页数据列表 (数组)
+					let curPageData = data.data.rows;
+					// console.log('curPageData',JSON.parse(JSON.stringify(curPageData)));
+					// 接口返回的当前页数据长度 (如列表有26个数据,当前页返回8个,则curPageLen=8)
+					let curPageLen = curPageData.length; 
+					// 接口返回的总页数 (如列表有26个数据,每页10条,共3页; 则totalPage=3)
+					// let totalPage =  data.data.data.totalPage; 
+					// 接口返回的总数据量(如列表有26个数据,每页10条,共3页; 则totalSize=26)
+					let totalSize = data.data.total; 
+					// 接口返回的是否有下一页 (true/false)
+					// let hasNext = data.xxx; 
+					// console.log('totalPage',totalPage,'curPageLen',curPageLen);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动置空列表
+					this.dataList = this.dataList.concat(curPageData); //追加新数据
+					// 请求成功,隐藏加载状态
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					// this.mescroll.endByPage(curPageLen, totalPage); 
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					this.mescroll.endBySize(curPageLen, totalSize); 
+				}).catch(err => {
+					this.mescroll.endErr()
+					console.log(err)
+				});	
+			
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList() {
+				this.mescroll.resetUpScroll();
+			},
+			getTypeList(){
+				this.$u.api.goodsTypeList({parentId:0}).then(res=>{
+					// console.log('getTypeList',res.data.rows);
+					this.hasTypeId = true;
+					this.tabsList = res.data.rows;
+					this.typeId =res.data.rows[0].id;
+					this.mescroll.resetUpScroll()
+				}).catch(err=>{
+					console.log('goodsTypeList',err);
+				})
+			},
+			tabsClick(item){
+				this.typeId = item.id;
+				this.reloadList()
+				// console.log('item',item);
+			},
+			getCredit(){
+				this.$u.api.memberCredit().then(res=>{
+					this.credit = res.data.credit;
+					// console.log('memberCredit',res.data);
+				}).catch(err=>{
+					console.log('memberCredit',err);
+				})
+			},
+			goDetails(id){
+				// if(!this.hasAddr){
+				// 	uni.showModal({
+				// 		title: '温馨提示',
+				// 		content: '请先设置地址!',
+				// 		success: res => {
+				// 			if (res.confirm) {
+				// 				uni.$u.route('/center/addrlist', {
+				// 					from: 'cart',
+				// 					backUrl:'/credits/credits'
+				// 				});
+				// 			}
+				// 		}
+				// 	})
+				// 	return
+				// }
+				uni.$u.route('/shopping/productdetails', {
+					id: id
+				});
+			},
+			getAddrList(){
+				this.$u.api.addrList().then(res=>{
+					// this.dataList = res.data.rows;
+					if( res.data.total>0){
+						this.hasAddr = true;
+					}else{
+						this.hasAddr = false;
+					}
+				}).catch(err=>{
+					console.log('getAddrList',err.data);
+				})
+			},
+		}
+	}
+</script>
+<style>
+	page{
+		background-color: #F5F9FC;
+	}
+</style>
+<style lang="scss" scoped>
+.header{
+	color: #fff;
+	position: relative;
+	height: 260rpx;
+	margin-bottom: 30rpx;
+	background: url(../static/img/supermarket-top.png) no-repeat;
+	.inner{
+		padding: 40rpx;
+		.top{
+			font-size: 26rpx;
+			text{margin-left: 10rpx;}
+			margin-bottom: 30rpx;
+		}
+		.num{
+			font-size: 50rpx;
+			font-weight: 600;
+			color: #FFFFFF;
+			line-height: 58rpx;
+		}
+		.bottom{
+			text-align: right;
+			text{
+				display: inline-block;
+				padding: 4rpx 20rpx;
+				background: rgba(0,0,0,0.2);
+				color: #fff;
+				border-radius: 23rpx;
+				font-size: 22rpx;
+			}
+		}
+	}
+}
+.tabs-wrap{
+	background-color: #fff;
+	margin-bottom: 10rpx;
+}
+.credit-product{
+	margin-top: 20rpx;
+	&-item{
+		width: 47%;
+		background-color: #fff;
+		margin-bottom: 30rpx;
+		border-radius: 24rpx;
+		&:nth-child(even){
+			margin-left: 6%;
+		}
+		.til{
+			font-size: 32rpx;
+			height: 80rpx;
+		}
+		.text{
+			padding: 20rpx;
+			.price{
+				margin-top: 17rpx;
+				font-size: 36rpx;
+				font-weight: 600;
+				color: #FFB100;
+				line-height: 50rpx;
+			}
+		}
+		.bottom{
+			font-size: 30rpx;
+			font-weight: 600;
+			color: #00A447;
+			line-height: 42rpx;
+			.btn{
+				font-size: 22rpx;
+				font-weight: 400;
+				color: #FFFFFF;
+				padding: 5rpx 18rpx;
+				background: #00A447;
+				border-radius: 4px;
+			}
+		}
+	}
+}
+</style>

+ 82 - 0
static/css/common.scss

@@ -0,0 +1,82 @@
+
+.page-wrap{padding: 20rpx;}
+.search-wrap{
+	background-color: #fff;
+	position: relative;
+	padding: 20rpx;
+	margin-bottom: 24rpx;
+}
+.search-wrap::after {
+  content: "";
+  position: absolute;
+  width: 100%;
+  height: 14rpx;
+  bottom: -20rpx;
+  left: 0;
+  background: linear-gradient(to bottom, #F5F5F5, #fff);
+  z-index: -1;
+}
+.gray{
+	color: #999;
+}
+.red{
+	color: #FF3C3F;
+}
+.line-through{
+	text-decoration: line-through;
+}
+
+.view-wrap{
+	padding: 30rpx 20rpx;
+	background-color: #fff;
+	border-bottom: 10rpx solid #F5F5F5;
+}
+
+.full-btn{
+	background-color: #00A447;
+	color: #fff;
+	border-radius: 44rpx;
+	padding: 22rpx 0;
+	text-align: center;
+	margin-bottom: 40rpx;
+	margin-top: 20rpx;
+	&.gray{
+		background-color: #ddd;
+		color: #999;
+	}
+}
+
+.single-til{
+	margin-bottom: 20rpx;
+	.text{
+		font-size: 32rpx;
+		color: #333;
+		font-weight: 600;
+	}
+	.more-text{
+		font-size: 24rpx;
+		color: #999;
+	}
+}
+.news-content{
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #666666;
+	line-height: 44rpx;
+	p{
+		text-indent: 2em;
+	}
+	/deep/ .u-image{
+		margin-bottom: 20rpx;
+		max-width: 100%;
+	}
+	.title{
+		font-size: 30rpx;
+		font-weight: 400;
+		color: #333333;
+		line-height: 50rpx;
+		margin: 30rpx auto 15rpx;
+	}
+}
+
+

+ 71 - 0
static/css/flex.scss

@@ -0,0 +1,71 @@
+.u-flex {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+}
+
+.u-flex-wrap {
+	flex-wrap: wrap;
+}
+
+.u-flex-nowrap {
+	flex-wrap: nowrap;
+}
+
+.u-col-center {
+	align-items: center;
+}
+
+.u-col-top {
+	align-items: flex-start;
+}
+
+.u-col-bottom {
+	align-items: flex-end;
+}
+
+.u-row-center {
+	justify-content: center;
+}
+
+.u-row-left {
+	justify-content: flex-start;
+}
+
+.u-row-right {
+	justify-content: flex-end;
+}
+
+.u-row-between {
+	justify-content: space-between;
+}
+
+.u-row-around {
+	justify-content: space-around;
+}
+
+.u-text-left {
+	text-align: left;
+}
+
+.u-text-center {
+	text-align: center;
+}
+
+.u-text-right {
+	text-align: right;
+}
+
+.u-flex-col {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: column;
+}
+
+// 定义flex等分
+@for $i from 0 through 12 {
+	.u-flex-#{$i} {
+		flex: $i;
+	}
+}

BIN
static/img/center-bg.png


BIN
static/img/center-code.png


BIN
static/img/center-icon-1.png


BIN
static/img/center-icon-2.png


BIN
static/img/center-icon-3.png


BIN
static/img/client-bg.png


BIN
static/img/client-ico.png


BIN
static/img/defaultavatar.png


BIN
static/img/empty.png


BIN
static/img/icon-user.png


BIN
static/img/icon-warning.png


BIN
static/img/index-nav-1.png


BIN
static/img/index-nav-2.png


BIN
static/img/index-nav-3.png


BIN
static/img/index-nav-4.png


BIN
static/img/integral-ico-y.png


BIN
static/img/integral-ico.png


BIN
static/img/order-banner.png


BIN
static/img/report-bg.png


BIN
static/img/report-ico.png


BIN
static/img/supermarket-top.png


BIN
static/img/tabbar-gift-gray.png


BIN
static/img/tabbar-gift.png


BIN
static/img/tabbar-home-gray.png


BIN
static/img/tabbar-home.png


BIN
static/img/tabbar-my-gray.png


BIN
static/img/tabbar-my.png


BIN
static/img/type-default-ico.png


BIN
static/img/typelist-bg.png


BIN
static/logo.png


BIN
static/qrlogo.png


+ 29 - 0
store/$u.mixin.js

@@ -0,0 +1,29 @@
+// $u.mixin.js
+
+import { mapState } from 'vuex'
+import store from "@/store"
+
+// 尝试将用户在根目录中的store/index.js的vuex的state变量,全部加载到全局变量中
+let $uStoreKey = [];
+try{
+	$uStoreKey = store.state ? Object.keys(store.state) : [];
+}catch(e){
+	
+}
+
+module.exports = {
+	created() {
+		// 将vuex方法挂在到$u中
+		// 使用方法为:如果要修改vuex的state中的user.name变量为"史诗" => this.$u.vuex('user.name', '史诗')
+		// 如果要修改vuex的state的version变量为1.0.1 => this.$u.vuex('version', '1.0.1')
+		this.$u.vuex = (name, value) => {
+			this.$store.commit('$uStore', {
+				name,value
+			})
+		}
+	},
+	computed: {
+		// 将vuex的state中的所有变量,解构到全局混入的mixin中
+		...mapState($uStoreKey)
+	}
+}

+ 87 - 0
store/index.js

@@ -0,0 +1,87 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+Vue.use(Vuex)
+
+let lifeData = {};
+
+try{
+	// 尝试获取本地是否存在lifeData变量,第一次启动APP时是不存在的
+	lifeData = uni.getStorageSync('lifeData');
+}catch(e){
+	
+}
+
+// 需要永久存储,且下次APP启动需要取出的,在state中的变量名
+
+let saveStateKeys = ['vuex_member_info', 'vuex_user_info','vuex_wechatOpenid'];
+
+// 保存变量到本地存储中
+const saveLifeData = function(key, value){
+	// 判断变量名是否在需要存储的数组中
+	if(saveStateKeys.indexOf(key) != -1) {
+		// 获取本地存储的lifeData对象,将变量添加到对象中
+		let tmp = uni.getStorageSync('lifeData');
+		// 第一次打开APP,不存在lifeData变量,故放一个{}空对象
+		tmp = tmp ? tmp : {};
+		tmp[key] = value;
+		// 执行这一步后,所有需要存储的变量,都挂载在本地的lifeData对象中
+		uni.setStorageSync('lifeData', tmp);
+	}
+}
+const store = new Vuex.Store({
+	state: {
+		// 如果上面从本地获取的lifeData对象下有对应的属性,就赋值给state中对应的变量
+		// 加上vuex_前缀,是防止变量名冲突,也让人一目了然
+		vuex_member_info: lifeData.vuex_member_info ? lifeData.vuex_member_info : {},
+		vuex_user_info: lifeData.vuex_user_info ? lifeData.vuex_user_info : {},
+		vuex_wechatOpenid:lifeData.vuex_wechatOpenid ? lifeData.vuex_wechatOpenid : '',
+		cartGoods:[],//购物车商品
+		creditGoods:[],//积分商品
+		buyNowGoods:[],//立即购买商品
+		// vuex_version: '1.0.1',
+	},
+	mutations: {
+		$uStore(state, payload) {
+			// 判断是否多层级调用,state中为对象存在的情况,诸如user.info.score = 1
+			let nameArr = payload.name.split('.');
+			let saveKey = '';
+			let len = nameArr.length;
+			if(len >= 2) {
+				let obj = state[nameArr[0]];
+				for(let i = 1; i < len - 1; i ++) {
+					obj = obj[nameArr[i]];
+				}
+				obj[nameArr[len - 1]] = payload.value;
+				saveKey = nameArr[0];
+			} else {
+				// 单层级变量,在state就是一个普通变量的情况
+				state[payload.name] = payload.value;
+				saveKey = payload.name;
+			}
+			// 保存变量到本地,见顶部函数定义
+			saveLifeData(saveKey, state[saveKey])
+		}
+	},
+	actions: {
+		// 获取用户信息
+		getUserInfo({ commit },backFu) {
+			if(this.state.user_info && this.state.user_info.userId) {
+				Vue.prototype.$u.api.checkToken().then((data) => {
+					if(data && data.code ===200) {
+						backFu(this.state.user_info)
+					}
+				})
+			} else {
+			    Vue.prototype.$u.api.userInfo().then((data) => {
+					console.log(data)
+			    	if(data && data.code ===200) {
+						Vue.prototype.$u.vuex('user_info', data.data)
+			    		backFu(data.data)
+			    	}
+			    })	
+			}
+		},
+	}
+})
+
+export default store

+ 233 - 0
study/studydetails.vue

@@ -0,0 +1,233 @@
+<template>
+	<view class="">
+		<!-- <u-navbar
+			:title="title"
+			:placeholder="true"
+			:autoBack="true"
+			 :safeAreaInsetTop="true"
+		>
+		</u-navbar> -->
+		<view class="page-wrap">
+			<view class="base-info">
+				<view class="title">{{pageData.title}}</view>
+				<view class="info u-flex u-row-between">
+					<view class="time">{{pageData.createTime}}</view>
+					<view class="view-count">{{pageData.studyUserNum}}人已学</view>
+				</view>
+			</view>
+			<view class="content-wrap">
+				<view class="content" v-if="pageData.libType!=2">
+					<u-parse :content="pageData.content"></u-parse>
+				</view>
+				<view class="video-wrap" v-else>
+					<video 
+						id="myVideo" :src="pageData.videoUrl"
+					    @error="videoErrorCallback" 
+						:show-fullscreen-btn="true"
+						@timeupdate="timeupdate"
+						@ended="ended"
+						:initial-time="initial_time"
+						@loadedmetadata="loadedmetadata"
+						:controls="true">
+					</video>
+					<view class="video-tip">{{videoTip}}</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default{
+	data(){
+		return{
+			pageHeight:null,
+			windowTop:null,
+			windowHeight:null,
+			scrollHeight:null,
+			title:'详情',
+			id:null,
+			type:'',
+			pageData:{
+				title:'',
+				content:''
+			},
+			swiperDetails:{},
+			// 视频创建dom
+			videoContext: uni.createVideoContext('myVideo', this),
+			// 视频已经播放时间
+			playedTime: 0,
+			// 视频总时长
+			duration: 0,
+			// 视频实时时间
+			initial_time:0,
+			// 视频实际时间
+			video_real_time: 0,
+			// 视频总时长
+			duration: 0,
+			// 当前视频选中播放
+			currentDuration: 0,
+			// 视频信息
+			videoInfo: {},
+			hasAddView:false,
+			videoTip:'请完整学习视频!(未完成)'
+		}
+	},
+	onLoad(page) {
+		this.id = page.id;
+		this.getDetails(this.id);
+		
+	},
+	onReady(){
+		this.windowTop = uni.getSystemInfoSync().windowTop;
+		this.windowHeight = uni.getSystemInfoSync().windowHeight;
+		this.pageHeight = this.windowTop + this.windowHeight;
+		this.scrollHeight = document.documentElement.scrollHeight;
+		// console.log('this.pageHeight',this.pageHeight);
+		// console.log('this.scrollHeight',this.scrollHeight);
+		if(this.scrollHeight>this.pageHeight){
+			console.log('有滚动条');
+		}else{
+			console.log('没有滚动条');
+		}
+		
+	},
+	// 在 onPageScroll 监听函数中监听滚动事件
+	onPageScroll(e) {
+		// console.log('onPageScroll',e);
+		// console.log('this.pageHeight',this.pageHeight);
+		// console.log('this.scrollHeight',this.scrollHeight);
+		// console.log('document.body.scrollTop',document.body.scrollTop);
+	  // 如果页面滚动到底部,则执行相应的操作
+	  if (e.scrollTop + this.windowHeight >= this.scrollHeight) {
+		  console.log('页面滚动到底部');
+	    // 执行操作
+	  }
+	},
+	beforeDestroy() {
+		if(this.pageData.libType==2){
+			console.log('hasAddView',this.hasAddView);
+		}
+	},
+	methods:{
+		getDetails(id){
+			this.$u.api.courseDetails({id:id}).then(res=>{
+				this.pageData = res.data;
+				
+				if(this.pageData.libType!=2){
+					this.addViewCount(this.id)
+				}
+				
+				// console.log('res',res);
+				}).catch(err=>{
+				console.log('courseDetails',err.data);
+			})
+		},
+		addViewCount(id){
+			this.$u.api.addViewCount({id:id}, { custom: { noload: true } }).then(res=>{
+				this.videoTip = '请完整学习视频!(已完成)';
+				// console.log('res',res);
+				}).catch(err=>{
+				console.log('addViewCount',err.data);
+			})
+		},
+		timeupdate(e){
+			// console.log('timeupdate',e.detail);
+			//播放的总时长
+			let duration = e.detail.duration;
+			//实时播放进度 秒数
+			let jumpTime = parseInt(e.detail.currentTime);
+			//当前视频进度
+			if (jumpTime - this.playedTime > 3) {
+			  // 差别过大,调用seek方法跳转到实际观看时间
+			  this.videoContext.seek(this.playedTime);
+			  wx.showToast({
+			    title: '未完整看完该视频,不能快进',
+			    icon: 'none',
+			    duration: 2000
+			  });
+			} else {
+			  this.video_real_time = parseInt(e.detail.currentTime);
+			  this.currentDuration = e.detail.currentTime;
+			  if (this.video_real_time > this.playedTime) {
+			    this.playedTime = this.video_real_time;
+			  }
+			}
+			// console.log('playedTime',this.playedTime);
+			// console.log('duration',parseInt(duration));
+			// 距离播放完成多少秒算看完,并调用接口
+			if(parseInt(duration)-this.playedTime==5&&this.hasAddView==false){
+				// console.log('看完了');
+				this.hasAddView = true;
+				this.addViewCount(this.id)
+			}
+		},
+		loadedmetadata(data) {
+		  this.duration = data.detail.duration;
+		},
+		ended() {
+		  // 用户把进度条拉到最后,但是实际观看时间不够,跳转回去会自动暂停。
+		  // 这里加个判断。
+		  if (this.videoUrl) {
+		    this.$emit(
+		      'recordDuration',
+		      { playDuration: this.playedTime, duration: this.duration, currentDuration: this.currentDuration },
+		      this.videoIndex,
+		      true
+		    );
+		  }
+		}
+	}
+}
+</script>
+<style>
+page{background-color: #F5F9FC;}
+</style>
+<style lang="scss" scoped>
+.title{
+	font-size: 36rpx;
+	font-weight: 600;
+	color: #333333;
+	line-height: 50rpx;
+	margin-bottom: 20rpx;
+	text-align: center;
+}
+.info{
+	margin-bottom: 50rpx;
+	font-size: 24rpx;
+	font-weight: 400;
+	color: #999999;
+	line-height: 33rpx;
+}
+.page-wrap{
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #666666;
+	line-height: 44rpx;
+	p{
+		text-indent: 2em;
+	}
+	/deep/ .u-image{
+		margin-bottom: 20rpx;
+	}
+}
+.content{
+	font-size: 26rpx;
+	font-weight: 400;
+	color: #666666;
+	line-height: 44rpx;
+	margin: 20rpx 0;
+}
+.u-image{
+	max-width: 100%;
+}
+.video-wrap{
+	video{
+		width:100%;
+	}
+	.video-tip{
+		text-align: center;
+		margin: 20rpx;
+	}
+}
+</style>

+ 184 - 0
study/studylist.vue

@@ -0,0 +1,184 @@
+<template>
+	<view class="pages">
+		<view class="page-wrap">
+			
+				<u-search
+					placeholder="搜索关键词" 
+					:clearabled="true"
+					:showAction="false"
+					height="80rpx"
+					@search="search"
+					@custom="search"
+					@change="titleChange"
+					@clear="reloadList"
+					bgColor="#fff"
+					borderColor="#00A447"
+					v-model="params.title">
+				</u-search>
+		
+		<view class="tabs-wrap" v-if="showTabs">
+			<u-tabs 
+			:list="tabsList" 
+			lineColor="#00A447" 
+			 :activeStyle="{color:'#333','font-weight': '600','font-size':'30rpx'}"
+			 :inactiveStyle="{color:'#999'}"
+			@click="tabsClick"></u-tabs>
+		</view>
+		<mescroll-body class="" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view class="page-wrap" >
+				<view class="course u-flex u-col-top" v-for="(item,index) in dataList" :key="item.id" @click="$u.route('/study/studydetails',{id:item.id})">
+					<view class="pic">
+						<u--image mode="scaleToFill" width="180rpx" height="170rpx" radius="10rpx" :showLoading="true" class="image" :src="item.mainImg"></u--image>
+					</view>
+					<view class="text">
+						<view class="title ellipsis-2">
+							<text class="new" v-if="index<9">最新</text>
+							{{item.title}}
+						</view>
+						<view class="classify">
+							<!-- <text class="classify-item">测试</text> -->
+						</view>
+						<view class="studyUserNum">
+							{{item.studyUserNum}}人已学习
+						</view>
+					</view>
+				</view>
+			</view>
+		</mescroll-body>
+			
+		</view>
+	</view>
+</template>
+
+<script>
+	// 引入mescroll-mixins.js
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		data() {
+			return {
+				downOption: {},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 3, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关学习资料'
+					}
+				},
+				tabsList:[],
+				params:{
+					title:'',
+					classifyId:'',
+				},
+				activeIndex:0,
+				// 列表数据
+				dataList: [],
+				showTabs:true
+			}
+		},
+		onShow() {
+			// this.getclassify();
+		},
+		onLoad() {
+
+		},
+		methods: {
+			/*下拉刷新的回调, 重置列表为第一页 (此处可删,mixins已默认)
+			downCallback(){
+				this.mescroll.resetUpScroll();
+			},
+			/*上拉加载的回调*/
+			upCallback(page) {
+				let that = this;
+				// 此处可以继续请求其他接口
+				// if(page.num == 1){
+				// 	// 请求其他接口...
+				// }
+				
+				if(!this.params.classifyId){
+					this.getclassify();
+					return // 此处return,先获取xx
+				}
+				
+				let pageNum = page.num; // 页码, 默认从1开始
+				let pageSize = page.size; // 页长, 默认每页10条
+				let params = Object.assign(this.params,{pageNum:pageNum,pageSize:pageSize});
+				if(params.title){
+					params.classifyId = '';
+					this.showTabs = false;
+				}else{
+					this.showTabs = true;
+				}
+				// console.log('this.params',this.params);
+				this.$u.api.studyLib(params).then(data => {
+					// console.log('data',JSON.parse(JSON.stringify(data)));
+					// 接口返回的当前页数据列表 (数组)
+					let curPageData = data.data.rows;
+					// console.log('curPageData',JSON.parse(JSON.stringify(curPageData)));
+					// 接口返回的当前页数据长度 (如列表有26个数据,当前页返回8个,则curPageLen=8)
+					let curPageLen = curPageData.length; 
+					// 接口返回的总页数 (如列表有26个数据,每页10条,共3页; 则totalPage=3)
+					// let totalPage =  data.data.data.totalPage; 
+					// 接口返回的总数据量(如列表有26个数据,每页10条,共3页; 则totalSize=26)
+					let totalSize = data.data.total; 
+					// 接口返回的是否有下一页 (true/false)
+					// let hasNext = data.xxx; 
+					// console.log('totalPage',totalPage,'curPageLen',curPageLen);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动置空列表
+					this.dataList = this.dataList.concat(curPageData); //追加新数据
+					// 请求成功,隐藏加载状态
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					// this.mescroll.endByPage(curPageLen, totalPage); 
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					this.mescroll.endBySize(curPageLen, totalSize); 
+				}).catch(err => {
+					this.mescroll.endErr()
+					console.log(err)
+				});	
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList() {
+				this.mescroll.resetUpScroll();
+			},
+			search(e){
+				this.mescroll.resetUpScroll();
+				// this.reloadList();
+				// console.log('search',e)
+			},
+			getclassify(){
+				this.$u.api.getclassify().then(res=>{
+					this.tabsList = res.data.rows.map(item=>{
+						return {name:item.classifyName,id:item.id}
+					});
+					this.params.classifyId = this.tabsList[0].id;
+					this.mescroll.resetUpScroll();
+				}).catch(err=>{
+					this.mescroll.endErr()
+					console.log('getclassify',err);
+				})				
+			},
+			tabsClick(item){
+				// console.log('itemitemitem',item);
+				this.activeIndex = item.index;
+				this.params.classifyId = item.id;
+				this.reloadList()
+			},
+			titleChange(title){
+				let that = this;
+				if(!title){
+					setTimeout(()=>{
+						that.search()
+					},500)
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 24 - 0
template.h5.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+		<title>
+			<%= htmlWebpackPlugin.options.title %>
+		</title>
+		<script>
+			document.addEventListener('DOMContentLoaded', function() {
+				document.documentElement.style.fontSize = document.documentElement.clientWidth / 20 + 'px'
+			})
+		</script>
+		<link rel="stylesheet" href="<%= BASE_URL %>static/index.css" />
+	</head>
+	<body>
+		<noscript>
+			<strong>本站点必须要开启JavaScript才能运行</strong>
+		</noscript>
+		<div id="app"></div>
+	</body>
+</html>

+ 286 - 0
uni.scss

@@ -0,0 +1,286 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+@import '@/uni_modules/uview-ui/theme.scss';
+
+/* 行为相关颜色 */
+$uni-color-primary: #009AEF;
+$uni-color-success: #4cd964;
+$uni-color-warning: #FFB100;
+$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-text-color-red:#FF3C3F;
+
+/* 背景颜色 */
+$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-background-color:#F5F5F5;
+
+/* 边框颜色 */
+$uni-border-color:#eee;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$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;
+$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;
+
+page{
+	padding-top: 0;
+	// &:has(.u-navbar){
+	// 	padding-top: 104px;
+	// }
+}
+// @import url("@/static/css/flex.scss");
+.u-flex {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+}
+
+.u-flex-wrap {
+	flex-wrap: wrap;
+}
+
+.u-flex-nowrap {
+	flex-wrap: nowrap;
+}
+
+.u-col-center {
+	align-items: center;
+}
+
+.u-col-top {
+	align-items: flex-start;
+}
+
+.u-col-bottom {
+	align-items: flex-end;
+}
+
+.u-row-center {
+	justify-content: center;
+}
+
+.u-row-left {
+	justify-content: flex-start;
+}
+
+.u-row-right {
+	justify-content: flex-end;
+}
+
+.u-row-between {
+	justify-content: space-between;
+}
+
+.u-row-around {
+	justify-content: space-around;
+}
+
+.u-text-left {
+	text-align: left;
+}
+
+.u-text-center {
+	text-align: center;
+}
+
+.u-text-right {
+	text-align: right;
+}
+
+.u-flex-col {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: column;
+}
+
+// 定义flex等分
+@for $i from 0 through 12 {
+	.u-flex-#{$i} {
+		flex: $i;
+	}
+}
+
+@mixin multi-ellipsis($line: 1) {
+  @if $line <= 0 {
+      $line: 1;
+  }
+
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: $line;
+  -webkit-box-orient: vertical;
+}
+
+.ellipsis-1 {
+  @include multi-ellipsis(1);
+}
+
+.ellipsis-2 {
+  @include multi-ellipsis(2);
+}
+
+// 课程
+.course{
+	padding-bottom: 40rpx;
+	margin-bottom: 40rpx;
+	&:not(:last-of-type){
+		border-bottom: 1px solid #eee;
+	}
+	.pic{
+		margin-right: 40rpx;
+	}
+	.text{
+		flex: 1;
+		.title{
+			font-size: 32rpx;
+			font-family: PingFangSC-Semibold, PingFang SC;
+			font-weight: 600;
+			color: #333333;
+			line-height: 44rpx;
+			margin-bottom: 20rpx;
+			min-height: 100rpx;
+			.new{
+				padding: 2rpx 8rpx;
+				background: #F4420A;
+				border-radius: 8rpx;
+				font-size: 22rpx;
+				font-family: PingFangSC-Regular, PingFang SC;
+				font-weight: 400;
+				color: #FFFFFF;
+				line-height: 32rpx;
+				margin-right: 18rpx;
+			}
+		}
+		.classify{
+			.classify-item{
+				height: 48rpx;
+				line-height: 48rpx;
+				padding: 0 18rpx;
+				background-color: #eee;
+				border-radius: 24rpx;
+				color: #999999;
+				font-size: 28rpx;
+			}
+		}
+		.studyUserNum{
+			font-size: 24rpx;
+			font-family: PingFangSC-Regular, PingFang SC;
+			font-weight: 400;
+			color: #999999;
+			line-height: 34rpx;
+			text-align: right;
+		}
+	}
+}
+
+.icon-nav{
+	margin-bottom: 45rpx;
+	.u-image{
+		margin-bottom: 15rpx;
+	}
+	.nav-item{
+		text-align: center;
+		font-size: 22rpx;
+		color: #666;
+	}
+}
+
+
+.tabs-wrap{
+	margin-bottom: 30rpx;
+	.more{
+		font-size: 24rpx;
+		color: #999;
+	}
+}
+
+.news{
+	.news-item{
+		padding-bottom: 30rpx;
+		margin-bottom: 30rpx;
+		.text{
+			margin-left: 20rpx;
+			.til{
+				font-size: 30rpx;
+				color: #333;
+				line-height: 1.5;
+				font-weight: 600;
+				margin-bottom: 17rpx;
+			}
+			.time{
+				text-align: left;
+				color: #999;
+				font-size: 24rpx;
+			}
+		}
+	}
+}
+
+.page-bg{
+	position: absolute;
+	z-index: -1;
+	left: 0;
+	top: 0;
+	right: 0;
+	.img{
+		width: 100%;
+	}
+}

+ 6 - 0
uni_modules/mescroll-uni/changelog.md

@@ -0,0 +1,6 @@
+## 1.3.7(2021-04-13)
+1. 新增`mescroll-swiper-sticky.vue`的示例, 轮播吸顶菜单导航  
+2. 新增`mescroll-empty.vue`的示例, 单独使用空布局组件  
+3. 简化tabs在具体项目中的使用,并简化对应的示例  
+4. mescroll-uni 支持动态禁止滚动的属性 disableScroll (注: mescroll-body不支持)  
+-by 小瑾同学

+ 19 - 0
uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.css

@@ -0,0 +1,19 @@
+.mescroll-body {
+	position: relative; /* 下拉刷新区域相对自身定位 */
+	height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
+	overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
+.mescroll-body.mescorll-sticky{
+	overflow: unset !important
+}
+
+/* 适配 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);
+	}
+}

+ 400 - 0
uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.vue

@@ -0,0 +1,400 @@
+<template>
+	<view 
+	class="mescroll-body mescroll-render-touch" 
+	:class="{'mescorll-sticky': sticky}"
+	: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">
+					<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>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from "../mescroll-uni/wxs/renderjs.js";
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from "../mescroll-uni/mescroll-uni.js";
+	// 引入全局配置
+	import GlobalOption from "../mescroll-uni/mescroll-uni-option.js";
+	// 引入国际化工具类
+	import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+	// 引入回到顶部组件
+	import MescrollTop from "../mescroll-uni/components/mescroll-top.vue";
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from "../mescroll-uni/wxs/mixins.js";
+	
+	/**
+	 * mescroll-body 基于page滚动的下拉刷新和上拉加载组件, 支持嵌套原生组件, 性能好
+	 * @property {Object} down 下拉刷新的参数配置
+	 * @property {Object} up 上拉加载的参数配置
+	 * @property {Object} i18n 国际化的参数配置
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+	 * @property {String, Number} height 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+	 * @property {Boolean} sticky 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法隐藏
+	 * @event {Function} init 初始化完成的回调 
+	 * @event {Function} down 下拉刷新的回调
+	 * @event {Function} up 上拉加载的回调 
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+	 * @event {Function} topclick 点击回到顶部的按钮回调
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+	 * @example <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-body>
+	 */
+	export default {
+		name: 'mescroll-body',
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		props: {
+			down: Object,
+			up: Object,
+			i18n: Object,
+			top: [String, Number],
+			topbar: [Boolean, String],
+			bottom: [String, Number],
+			safearea: Boolean,
+			height: [String, Number],
+			bottombar:{
+				type: Boolean,
+				default: true
+			},
+			sticky: Boolean
+		},
+		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 // 状态栏高度
+			};
+		},
+		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.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					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: {
+					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组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4; 
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					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);
+					}
+				}
+			};
+			
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			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为滚动区域
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// 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为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).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;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	};
+</script>
+
+<style>
+	@import "../mescroll-body/mescroll-body.css";
+	@import "../mescroll-uni/components/mescroll-down.css";
+	@import "../mescroll-uni/components/mescroll-up.css";
+</style>

+ 47 - 0
uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.css

@@ -0,0 +1,47 @@
+/*下拉刷新--标语*/
+.mescroll-downwarp .downwarp-slogan{
+	display: block;
+	width: 420rpx;
+	height: 168rpx;
+	margin: auto;
+}
+/*下拉刷新--向下进度动画*/
+.mescroll-downwarp .downwarp-progress{
+	display: inline-block;
+	width: 40rpx;
+	height: 40rpx;
+	border: none;
+	margin: auto;
+	background-size: contain;
+	background-repeat: no-repeat;
+	background-position: center;
+	background-image: url(https://www.mescroll.com/img/beibei/mescroll-progress.png);
+	transition: all 300ms;
+}
+/*下拉刷新--进度条*/
+.mescroll-downwarp .downwarp-loading{
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid #FF8095;
+	border-bottom-color: transparent;
+}
+/*下拉刷新--吉祥物*/
+.mescroll-downwarp .downwarp-mascot{
+	position: absolute;
+	right: 16rpx;
+	bottom: 0;
+	width: 100rpx;
+	height: 100rpx;
+	background-size: contain;
+	background-repeat: no-repeat;
+	animation: animMascot .6s steps(1,end) infinite;
+}
+@keyframes animMascot {
+	0% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
+	25% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb2.png)}
+	50% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb3.png)}
+	75% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb4.png)}
+	100% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
+}

+ 39 - 0
uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.vue

@@ -0,0 +1,39 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+			<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+			<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+			<view class="downwarp-mascot"></view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object , // down的配置项
+		type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 是否在加载中
+		isDownLoading(){
+			return this.type === 3
+		},
+		// 旋转的角度
+		downRotate(){
+			return this.type === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+		}
+	}
+};
+</script>
+
+<style>
+@import "../../../mescroll-uni/components/mescroll-down.css";
+@import "./mescroll-down.css";
+</style>

+ 360 - 0
uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-body.vue

@@ -0,0 +1,360 @@
+<template>
+	<view 
+		class="mescroll-body mescroll-render-touch" 
+		:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+		:class="{'mescorll-sticky': sticky}"
+		@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"></mescroll-down> -->
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+				<view class="downwarp-content">
+					<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+					<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+					<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+					<view class="downwarp-mascot"></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端生效) -->
+		<!-- #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>
+
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				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, // 上拉加载的参数配置
+			i18n: 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
+			},
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+		},
+		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 this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+			}
+		},
+		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: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					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);
+					}
+				}
+			};
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			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为滚动区域
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// 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为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).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;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	};
+</script>
+
+<style>
+	@import "../../mescroll-body/mescroll-body.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+</style>

+ 49 - 0
uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni-option.js

@@ -0,0 +1,49 @@
+// mescroll-uni和mescroll-body 的全局配置
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		offset: uni.upx2px(140), // 在列表顶部,下拉大于140upx,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		offset: 150, // 距底部多远时,触发upCallback
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入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: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+		}
+	},
+	// 国际化配置
+	i18n: {
+		// 中文
+		zh: {
+			up: {
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textNoMore: '-- END --', // 没有更多数据的提示文本
+				empty: {
+					tip: '~ 暂无相关数据 ~' // 空提示
+				}
+			}
+		},
+		// 英文
+		en: {
+			up: {
+				textLoading: 'loading ...',
+				textNoMore: '-- END --',
+				empty: {
+					tip: '~ absolutely empty ~'
+				}
+			}
+		}
+	}
+}
+
+export default GlobalOption

+ 437 - 0
uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni.vue

@@ -0,0 +1,437 @@
+<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-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+			<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"></mescroll-down> -->
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+						<view class="downwarp-content">
+							<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+							<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+							<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+							<view class="downwarp-mascot"></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端生效) -->
+				<!-- #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>
+
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				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 // 状态栏高度
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			i18n: 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
+			},
+			disableScroll: Boolean // 是否禁止滚动
+		},
+		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(){
+				if(this.disableScroll) return false
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+			}
+		},
+		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已渲染
+						this.getClientInfo(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)
+							}
+						})
+					})
+				}
+			},
+			// 获取滚动区域的信息
+			getClientInfo(success){
+				let query = uni.createSelectorQuery();
+				// #ifndef MP-ALIPAY || MP-DINGTALK
+				query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+				// #endif
+				let view = query.select('#' + this.viewId);
+				view.boundingClientRect(data => {
+					success(data)
+				}).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组件时,此行不可删)
+					},
+					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()
+					}
+				}
+			}
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			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
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// 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'){
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+					vm.getClientInfo(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (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
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					})
+					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;
+			}
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	}
+</script>
+
+<style>
+	@import "../../mescroll-uni/mescroll-uni.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+</style>

+ 44 - 0
uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.css

@@ -0,0 +1,44 @@
+/*下拉刷新--上下箭头*/
+.mescroll-downwarp .downwarp-arrow {
+	display: inline-block;
+	width: 20px;
+	height: 20px;
+	margin: 10px;
+	background-image: url(https://www.mescroll.com/img/xinlang/mescroll-arrow.png);
+	background-size: contain;
+	vertical-align: middle;
+	transition: all 300ms;
+}
+
+/*下拉刷新--旋转进度条*/
+.mescroll-downwarp .downwarp-progress{
+	width: 36px;
+	height: 36px;
+	border: none;
+	margin: auto;
+	background-size: contain;
+	animation: progressRotate 0.6s steps(6, start) infinite;
+}
+@keyframes progressRotate {
+	0% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+	16% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
+	}
+	32% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
+	}
+	48% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
+	}
+	64% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
+	}
+	80% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
+	}
+	100% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+}

+ 53 - 0
uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.vue

@@ -0,0 +1,53 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<view v-if="isDownLoading" class="downwarp-progress"></view>
+			<view v-else class="downwarp-arrow" :style="{ 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)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption() {
+			return this.option || {};
+		},
+		// 是否在加载中
+		isDownLoading() {
+			return this.type === 3;
+		},
+		// 旋转的角度
+		downRotate() {
+			return this.type === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+		},
+		// 文本提示
+		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-uni/components/mescroll-down.css';
+@import './mescroll-down.css';
+</style>

+ 32 - 0
uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.css

@@ -0,0 +1,32 @@
+/*上拉加载--旋转进度条*/
+.mescroll-upwarp .upwarp-progress {
+	width: 36px;
+	height: 36px;
+	border: none;
+	margin: auto;
+	background-size: contain;
+	animation: progressRotate 0.6s steps(6, start) infinite;
+}
+@keyframes progressRotate {
+	0% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+	16% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
+	}
+	32% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
+	}
+	48% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
+	}
+	64% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
+	}
+	80% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
+	}
+	100% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+}

+ 40 - 0
uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.vue

@@ -0,0 +1,40 @@
+<!-- 上拉加载区域 -->
+<template>
+	<view class="mescroll-upwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+		<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+		<view v-show="isUpLoading">
+			<view class="upwarp-progress mescroll-rotate"></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(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption() {
+			return this.option || {};
+		},
+		// 加载中
+		isUpLoading() {
+			return this.type === 1;
+		},
+		// 没有更多了
+		isUpNoMore() {
+			return this.type === 2;
+		}
+	}
+};
+</script>
+
+<style>
+@import '../../../mescroll-uni/components/mescroll-up.css';
+@import './mescroll-up.css';
+</style>

+ 380 - 0
uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-body.vue

@@ -0,0 +1,380 @@
+<template>
+	<view 
+		class="mescroll-body mescroll-render-touch" 
+		:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+		:class="{'mescorll-sticky': sticky}"
+		@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"></mescroll-down> -->
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+				<view class="downwarp-content">
+					<view v-if="isDownLoading" class="downwarp-progress"></view>
+					<view v-else class="downwarp-arrow" :style="{ 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 && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+			<view 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"></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端生效) -->
+		<!-- #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>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				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, // 上拉加载的参数配置
+			i18n: 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
+			},
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+		},
+		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 this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+			},
+			// 文本提示
+			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.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					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: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4; 
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					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);
+					}
+				}
+			};
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			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为滚动区域
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// 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为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).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;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	};
+</script>
+
+<style>
+	@import "../../mescroll-uni/mescroll-uni.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+	@import "./components/mescroll-up.css";
+</style>

+ 64 - 0
uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni-option.js

@@ -0,0 +1,64 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入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: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+		}
+	},
+	// 国际化配置
+	i18n: {
+		// 中文
+		zh: {
+			down: {
+				textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+				textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textSuccess: '加载成功', // 加载成功的文本
+				textErr: '加载失败', // 加载失败的文本
+			},
+			up: {
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textNoMore: '-- END --', // 没有更多数据的提示文本
+				empty: {
+					tip: '~ 空空如也 ~' // 空提示
+				}
+			}
+		},
+		// 英文
+		en: {
+			down: {
+				textInOffset: 'drop down refresh',
+				textOutOffset: 'release updates',
+				textLoading: 'loading ...',
+				textSuccess: 'loaded successfully',
+				textErr: 'loading failed'
+			},
+			up: {
+				textLoading: 'loading ...',
+				textNoMore: '-- END --',
+				empty: {
+					tip: '~ absolutely empty ~'
+				}
+			}
+		}
+	}
+}
+
+export default GlobalOption

+ 462 - 0
uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni.vue

@@ -0,0 +1,462 @@
+<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-with-animation="scrollAnim" @scroll="scroll"  :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+			<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"></mescroll-down> -->
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+						<view class="downwarp-content">
+							<view v-if="isDownLoading" class="downwarp-progress"></view>
+							<view v-else class="downwarp-arrow" :style="{ 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 && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+					<view 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"></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端生效) -->
+				<!-- #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>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				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 // 状态栏高度
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			i18n: 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
+			},
+			disableScroll: Boolean // 是否禁止滚动
+		},
+		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(){
+				if(this.disableScroll) return false
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading() {
+				return this.downLoadType === 3;
+			},
+			// 旋转的角度
+			downRotate() {
+				return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+			},
+			// 文本提示
+			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.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					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已渲染
+						this.getClientInfo(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)
+							}
+						})
+					})
+				}
+			},
+			// 获取滚动区域的信息
+			getClientInfo(success){
+				let query = uni.createSelectorQuery();
+				// #ifndef MP-ALIPAY || MP-DINGTALK
+				query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+				// #endif
+				let view = query.select('#' + this.viewId);
+				view.boundingClientRect(data => {
+					success(data)
+				}).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组件时,此行不可删)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4; 
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					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()
+					}
+				}
+			}
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			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
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// 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'){
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+					vm.getClientInfo(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (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
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					})
+					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;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	}
+</script>
+
+<style>
+	@import "../../mescroll-uni/mescroll-uni.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+	@import "./components/mescroll-up.css";
+</style>

+ 116 - 0
uni_modules/mescroll-uni/components/mescroll-empty/mescroll-empty.vue

@@ -0,0 +1,116 @@
+<!--空布局:
+遵循easycom规范, 可作为独立的组件, 不使用mescroll的页面也能使用:
+<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="btnText" class="empty-btn" @click="emptyClick">{{ btnText }}</view>
+	</view>
+</template>
+
+<script>
+// 引入全局配置
+import GlobalOption from '../mescroll-uni/mescroll-uni-option.js';
+// 引入国际化工具类
+import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+export default {
+	props: {
+		// empty的配置项: 默认为GlobalOption.up.empty
+		option: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	// 使用computed获取配置,用于支持option的动态配置
+	computed: {
+		// 图标
+		icon() {
+			if (this.option.icon != null) { // 此处不使用短路求值, 用于支持传空串不显示图标
+				return this.option.icon
+			} else{
+				let i18nType = mescrollI18n.getType() // 国际化配置
+				if (this.option.i18n) {
+					return this.option.i18n[i18nType].icon
+				} else{
+					return GlobalOption.i18n[i18nType].up.empty.icon || GlobalOption.up.empty.icon
+				}
+			}
+		},
+		// 文本提示
+		tip() {
+			if (this.option.tip != null) { // 支持传空串不显示文本提示
+				return this.option.tip
+			} else{
+				let i18nType = mescrollI18n.getType() // 国际化配置
+				if (this.option.i18n) {
+					return this.option.i18n[i18nType].tip
+				} else{
+					return GlobalOption.i18n[i18nType].up.empty.tip || GlobalOption.up.empty.tip
+				}
+			}
+		},
+		// 按钮文本
+		btnText() {
+			if (this.option.i18n) {
+				let i18nType = mescrollI18n.getType() // 国际化配置
+				return this.option.i18n[i18nType].btnText
+			} else{
+				return this.option.btnText
+			}
+		}
+	},
+	methods: {
+		// 点击按钮
+		emptyClick() {
+			this.$emit('emptyclick');
+		}
+	}
+};
+</script>
+
+<style>
+/* 无任何数据的空布局 */
+.mescroll-empty {
+	box-sizing: border-box;
+	width: 100%;
+	padding: 100rpx 50rpx;
+	text-align: center;
+}
+
+.mescroll-empty.empty-fixed {
+	z-index: 99;
+	position: absolute; /*transform会使fixed失效,最终会降级为absolute */
+	top: 100rpx;
+	left: 0;
+}
+
+.mescroll-empty .empty-icon {
+	width: 280rpx;
+	height: 280rpx;
+}
+
+.mescroll-empty .empty-tip {
+	margin-top: 20rpx;
+	font-size: 24rpx;
+	color: gray;
+}
+
+.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>

+ 55 - 0
uni_modules/mescroll-uni/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
uni_modules/mescroll-uni/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>

+ 83 - 0
uni_modules/mescroll-uni/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
uni_modules/mescroll-uni/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
uni_modules/mescroll-uni/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>

+ 15 - 0
uni_modules/mescroll-uni/components/mescroll-uni/mescroll-i18n.js

@@ -0,0 +1,15 @@
+// 国际化工具类
+const mescrollI18n = {
+	// 默认语言
+	def: "zh",
+	// 获取当前语言类型
+	getType(){
+		return uni.getStorageSync("mescroll-i18n") || this.def
+	},
+	// 设置当前语言类型
+	setType(type){
+		uni.setStorageSync("mescroll-i18n", type)
+	}
+}
+
+export default mescrollI18n

Vissa filer visades inte eftersom för många filer har ändrats