فهرست منبع

初始化项目

19124812874 5 سال پیش
کامیت
3a807dd920
73فایلهای تغییر یافته به همراه13203 افزوده شده و 0 حذف شده
  1. 43 0
      App.vue
  2. 221 0
      assets/colorui/app.css
  3. 36 0
      assets/colorui/icon.css
  4. 3908 0
      assets/colorui/main.css
  5. 139 0
      assets/http/api.js
  6. 48 0
      assets/http/service.js
  7. 32 0
      components/addBtn/addBtn.vue
  8. 134 0
      components/feedback/feedback.vue
  9. 55 0
      components/mescroll-body/components/mescroll-down.css
  10. 47 0
      components/mescroll-body/components/mescroll-down.vue
  11. 90 0
      components/mescroll-body/components/mescroll-empty.vue
  12. 83 0
      components/mescroll-body/components/mescroll-top.vue
  13. 47 0
      components/mescroll-body/components/mescroll-up.css
  14. 39 0
      components/mescroll-body/components/mescroll-up.vue
  15. 19 0
      components/mescroll-body/mescroll-body.css
  16. 348 0
      components/mescroll-body/mescroll-body.vue
  17. 65 0
      components/mescroll-body/mescroll-mixins.js
  18. 36 0
      components/mescroll-body/mescroll-uni-option.js
  19. 36 0
      components/mescroll-body/mescroll-uni.css
  20. 799 0
      components/mescroll-body/mescroll-uni.js
  21. 424 0
      components/mescroll-body/mescroll-uni.vue
  22. 48 0
      components/mescroll-body/mixins/mescroll-comp.js
  23. 59 0
      components/mescroll-body/mixins/mescroll-more-item.js
  24. 74 0
      components/mescroll-body/mixins/mescroll-more.js
  25. 109 0
      components/mescroll-body/wxs/mixins.js
  26. 92 0
      components/mescroll-body/wxs/renderjs.js
  27. 268 0
      components/mescroll-body/wxs/wxs.wxs
  28. 126 0
      components/repair/repair.vue
  29. 87 0
      main.js
  30. 78 0
      manifest.json
  31. 26 0
      package-lock.json
  32. 16 0
      package.json
  33. 175 0
      pages.json
  34. 355 0
      pages/building/building.vue
  35. 130 0
      pages/building/card.vue
  36. 22 0
      pages/car/car.vue
  37. 217 0
      pages/index/index.vue
  38. 481 0
      pages/login/login.vue
  39. 22 0
      pages/mine/mine.vue
  40. 22 0
      pages/residential/residential.vue
  41. 310 0
      pages/room/add.vue
  42. 137 0
      pages/room/card.vue
  43. 119 0
      pages/room/detail.vue
  44. 395 0
      pages/room/room.vue
  45. 130 0
      pages/unit/card.vue
  46. 373 0
      pages/unit/unit.vue
  47. 154 0
      pages/user-auth/card.vue
  48. 92 0
      pages/user-auth/detail.vue
  49. 430 0
      pages/user-auth/user-auth.vue
  50. 413 0
      pages/user/add.vue
  51. 146 0
      pages/user/card.vue
  52. 150 0
      pages/user/detail.vue
  53. 395 0
      pages/user/user.vue
  54. BIN
      static/choose-Cade/choose-Cadecc.png
  55. BIN
      static/choose-Cade/choose-Cades.png
  56. BIN
      static/choose-Cade/choose-Cadex.png
  57. 2 0
      static/iconfont.css
  58. BIN
      static/index/face.png
  59. BIN
      static/login/a4.png
  60. BIN
      static/login/psw1.png
  61. BIN
      static/login/psw2.png
  62. BIN
      static/mine/logout.png
  63. BIN
      static/tarbar/index-select.png
  64. BIN
      static/tarbar/index.png
  65. BIN
      static/tarbar/mine-select.png
  66. BIN
      static/tarbar/mine.png
  67. 76 0
      uni.scss
  68. 119 0
      utils/cache.js
  69. 44 0
      utils/dateTime.js
  70. 20 0
      utils/dialog.js
  71. 12 0
      utils/global.js
  72. 415 0
      utils/util.js
  73. 215 0
      utils/verify.js

+ 43 - 0
App.vue

@@ -0,0 +1,43 @@
+<script>
+	export default {
+		onLaunch: function() {
+			//更新版本
+			const updateManager = uni.getUpdateManager();
+			updateManager.onCheckForUpdate(function (res) {
+			  // 请求完新版本信息的回调
+			  console.log("是否有新版本:",res.hasUpdate);
+			});
+			updateManager.onUpdateReady(function (res) {
+			  uni.showModal({
+			    title: '更新提示',
+			    content: '新版本已经准备好,是否重启应用?',
+			    success(res) {
+			      if (res.confirm) {
+			        // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
+			        updateManager.applyUpdate();
+			      }
+			    }
+			  });
+			
+			});
+			updateManager.onUpdateFailed(function (res) {
+			  // 新的版本下载失败
+			});
+		},
+		onShow: function() {
+			console.log('App Show')
+		},
+		onHide: function() {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import "./static/iconfont.css";
+	@import "uview-ui/index.scss";
+	
+	@import "@/assets/colorui/main.css";
+	@import "@/assets/colorui/icon.css";
+	@import "@/assets/colorui/app.css";
+</style>

+ 221 - 0
assets/colorui/app.css

@@ -0,0 +1,221 @@
+.text-base{
+	color: #7fc6ac;
+}
+.line-base{
+	border: 1rpx solid #7fc6ac;
+	color: #7fc6ac;
+}
+.bg-base{
+	background-color: #7fc6ac;
+	color: #FFFFFF;
+}
+/* ===========padding=========== */
+.padding-10,.padding-sm{
+	padding: 10rpx;
+}
+.padding-20,.padding-df{
+	padding: 20rpx;
+}
+.padding-30,.padding-lg{
+	padding: 30rpx;
+}
+.padding-40{
+	padding: 40rpx;
+}
+.padding-50{
+	padding: 50rpx;
+}
+.padding-left-10,.padding-left-sm{
+	padding-left: 10rpx;
+}
+.padding-left-20,.padding-left-df{
+	padding-left: 20rpx;
+}
+.padding-left-30,.padding-left-lg{
+	padding-left: 30rpx;
+}
+.padding-left-40{
+	padding-left: 40rpx;
+}
+.padding-left-50{
+	padding-left: 50rpx;
+}
+
+.padding-right-10,.padding-right-sm{
+	padding-right: 10rpx;
+}
+.padding-right-20,.padding-right-df{
+	padding-right: 20rpx;
+}
+.padding-right-30,.padding-right-lg{
+	padding-right: 30rpx;
+}
+.padding-right-40{
+	padding-right: 40rpx;
+}
+.padding-right-50{
+	padding-right: 50rpx;
+}
+
+.padding-top-10,.padding-top-sm{
+	padding-top: 10rpx;
+}
+.padding-top-20,.padding-top-df{
+	padding-top: 20rpx;
+}
+.padding-top-30,.padding-top-lg{
+	padding-top: 30rpx;
+}
+.padding-top-40{
+	padding-top: 40rpx;
+}
+.padding-top-50{
+	padding-top: 50rpx;
+}
+
+.padding-bottom-10,.padding-bottom-sm{
+	padding-bottom: 10rpx;
+}
+.padding-bottom-20,.padding-bottom-df{
+	padding-bottom: 20rpx;
+}
+.padding-bottom-30,.padding-bottom-lg{
+	padding-bottom: 30rpx;
+}
+.padding-bottom-40{
+	padding-bottom: 40rpx;
+}
+.padding-bottom-50{
+	padding-bottom: 50rpx;
+}
+
+/* ===========margin=========== */
+.margin-10,.margin-sm{
+	margin: 10rpx;
+}
+.margin-20,.margin-df{
+	margin: 20rpx;
+}
+.margin-30,.margin-lg{
+	margin: 30rpx;
+}
+.margin-40{
+	margin: 40rpx;
+}
+.margin-50{
+	margin: 50rpx;
+}
+.margin-left-10,.margin-left-sm{
+	margin-left: 10rpx;
+}
+.margin-left-20,.margin-left-df{
+	margin-left: 20rpx;
+}
+.margin-left-30,.margin-left-lg{
+	margin-left: 30rpx;
+}
+.margin-left-40{
+	margin-left: 40rpx;
+}
+.margin-left-50{
+	margin-left: 50rpx;
+}
+
+.margin-right-10,.margin-right-sm{
+	margin-right: 10rpx;
+}
+.margin-right-20,.margin-right-df{
+	margin-right: 20rpx;
+}
+.margin-right-30,.margin-right-lg{
+	margin-right: 30rpx;
+}
+.margin-right-40{
+	margin-right: 40rpx;
+}
+.margin-right-50{
+	margin-right: 50rpx;
+}
+
+.margin-top-10,.margin-top-sm{
+	margin-top: 10rpx;
+}
+.margin-top-20,.margin-top-df{
+	margin-top: 20rpx;
+}
+.margin-top-30,.margin-top-lg{
+	margin-top: 30rpx;
+}
+.margin-top-40{
+	margin-top: 40rpx;
+}
+.margin-top-50{
+	margin-top: 50rpx;
+}
+
+.margin-bottom-10,.margin-bottom-sm{
+	margin-bottom: 10rpx;
+}
+.margin-bottom-20,.margin-bottom-df{
+	margin-bottom: 20rpx;
+}
+.margin-bottom-30,.margin-bottom-lg{
+	margin-bottom: 30rpx;
+}
+.margin-bottom-40{
+	margin-bottom: 40rpx;
+}
+.margin-bottom-50{
+	margin-bottom: 50rpx;
+}
+
+/* ==========text-cut=========== */
+
+
+
+.text-cut-1 {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-line-clamp: 1;
+	-webkit-box-orient: vertical;
+}
+
+.text-cut-2 {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+}
+
+.text-cut {
+		overflow: hidden;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+		display: block;
+}
+
+.bg-gradual-black {
+	background-image: linear-gradient(45deg, #26272f, #414244);
+	color: #f7ebd2;
+}
+
+.footer-fixed {
+	position: fixed;
+	z-index: 99999;
+	width: 100%;
+	bottom: 0;
+	left: 0;
+}
+
+.bottom-bar{
+	/* position: fixed;
+	top: 88%; */
+	width: 100%;
+	height: 150rpx;
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+	padding: 30rpx;
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 36 - 0
assets/colorui/icon.css


+ 3908 - 0
assets/colorui/main.css

@@ -0,0 +1,3908 @@
+/* ==================
+        初始化
+ ==================== */
+body {
+	background-color: #f1f1f1;
+	font-size: 28upx;
+	color: #333333;
+	font-family: Helvetica Neue, Helvetica, sans-serif;
+}
+
+view,
+scroll-view,
+swiper,
+button,
+input,
+textarea,
+label,
+navigator,
+image {
+	box-sizing: border-box;
+}
+
+.round {
+	border-radius: 5000upx;
+}
+
+.radius {
+	border-radius: 6upx;
+}
+
+/* ==================
+          图片
+ ==================== */
+
+image {
+	max-width: 100%;
+	display: inline-block;
+	position: relative;
+	z-index: 0;
+}
+
+image.loading::before {
+	content: "";
+	background-color: #f5f5f5;
+	display: block;
+	position: absolute;
+	width: 100%;
+	height: 100%;
+	z-index: -2;
+}
+
+image.loading::after {
+	content: "\e7f1";
+	font-family: "cuIcon";
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 32upx;
+	height: 32upx;
+	line-height: 32upx;
+	right: 0;
+	bottom: 0;
+	z-index: -1;
+	font-size: 32upx;
+	margin: auto;
+	color: #ccc;
+	-webkit-animation: cuIcon-spin 2s infinite linear;
+	animation: cuIcon-spin 2s infinite linear;
+	display: block;
+}
+
+.response {
+	width: 100%;
+}
+
+/* ==================
+         开关
+ ==================== */
+
+switch,
+checkbox,
+radio {
+	position: relative;
+}
+
+switch::after,
+switch::before {
+	font-family: "cuIcon";
+	content: "\e645";
+	position: absolute;
+	color: #ffffff !important;
+	top: 0%;
+	left: 0upx;
+	font-size: 26upx;
+	line-height: 26px;
+	width: 50%;
+	text-align: center;
+	pointer-events: none;
+	transform: scale(0, 0);
+	transition: all 0.3s ease-in-out 0s;
+	z-index: 9;
+	bottom: 0;
+	height: 26px;
+	margin: auto;
+}
+
+switch::before {
+	content: "\e646";
+	right: 0;
+	transform: scale(1, 1);
+	left: auto;
+}
+
+switch[checked]::after,
+switch.checked::after {
+	transform: scale(1, 1);
+}
+
+switch[checked]::before,
+switch.checked::before {
+	transform: scale(0, 0);
+}
+
+/* #ifndef MP-ALIPAY */
+radio::before,
+checkbox::before {
+	font-family: "cuIcon";
+	content: "\e645";
+	position: absolute;
+	color: #ffffff !important;
+	top: 50%;
+	margin-top: -8px;
+	right: 5px;
+	font-size: 32upx;
+	line-height: 16px;
+	pointer-events: none;
+	transform: scale(1, 1);
+	transition: all 0.3s ease-in-out 0s;
+	z-index: 9;
+}
+
+radio .wx-radio-input,
+checkbox .wx-checkbox-input,
+radio .uni-radio-input,
+checkbox .uni-checkbox-input {
+	margin: 0;
+	width: 24px;
+	height: 24px;
+}
+
+checkbox.round .wx-checkbox-input,
+checkbox.round .uni-checkbox-input {
+	border-radius: 100upx;
+}
+
+/* #endif */
+
+switch[checked]::before {
+	transform: scale(0, 0);
+}
+
+switch .wx-switch-input,
+switch .uni-switch-input {
+	border: none;
+	padding: 0 24px;
+	width: 48px;
+	height: 26px;
+	margin: 0;
+	border-radius: 100upx;
+}
+
+switch .wx-switch-input:not([class*="bg-"]),
+switch .uni-switch-input:not([class*="bg-"]) {
+	background: #8799a3 !important;
+}
+
+switch .wx-switch-input::after,
+switch .uni-switch-input::after {
+	margin: auto;
+	width: 26px;
+	height: 26px;
+	border-radius: 100upx;
+	left: 0upx;
+	top: 0upx;
+	bottom: 0upx;
+	position: absolute;
+	transform: scale(0.9, 0.9);
+	transition: all 0.1s ease-in-out 0s;
+}
+
+switch .wx-switch-input.wx-switch-input-checked::after,
+switch .uni-switch-input.uni-switch-input-checked::after {
+	margin: auto;
+	left: 22px;
+	box-shadow: none;
+	transform: scale(0.9, 0.9);
+}
+
+radio-group {
+	display: inline-block;
+}
+
+
+
+switch.radius .wx-switch-input::after,
+switch.radius .wx-switch-input,
+switch.radius .wx-switch-input::before,
+switch.radius .uni-switch-input::after,
+switch.radius .uni-switch-input,
+switch.radius .uni-switch-input::before {
+	border-radius: 10upx;
+}
+
+switch .wx-switch-input::before,
+radio.radio::before,
+checkbox .wx-checkbox-input::before,
+radio .wx-radio-input::before,
+switch .uni-switch-input::before,
+radio.radio::before,
+checkbox .uni-checkbox-input::before,
+radio .uni-radio-input::before {
+	display: none;
+}
+
+radio.radio[checked]::after,
+radio.radio .uni-radio-input-checked::after {
+	content: "";
+	background-color: transparent;
+	display: block;
+	position: absolute;
+	width: 8px;
+	height: 8px;
+	z-index: 999;
+	top: 0upx;
+	left: 0upx;
+	right: 0;
+	bottom: 0;
+	margin: auto;
+	border-radius: 200upx;
+	/* #ifndef MP */
+	border: 7px solid #ffffff !important;
+	/* #endif */
+
+	/* #ifdef MP */
+	border: 8px solid #ffffff !important;
+	/* #endif */
+}
+
+.switch-sex::after {
+	content: "\e71c";
+}
+
+.switch-sex::before {
+	content: "\e71a";
+}
+
+.switch-sex .wx-switch-input,
+.switch-sex .uni-switch-input {
+	background: #e54d42 !important;
+	border-color: #e54d42 !important;
+}
+
+.switch-sex[checked] .wx-switch-input,
+.switch-sex.checked .uni-switch-input {
+	background: #0081ff !important;
+	border-color: #0081ff !important;
+}
+
+switch.red[checked] .wx-switch-input.wx-switch-input-checked,
+checkbox.red[checked] .wx-checkbox-input,
+radio.red[checked] .wx-radio-input,
+switch.red.checked .uni-switch-input.uni-switch-input-checked,
+checkbox.red.checked .uni-checkbox-input,
+radio.red.checked .uni-radio-input {
+	background-color: #e54d42 !important;
+	border-color: #e54d42 !important;
+	color: #ffffff !important;
+}
+
+switch.orange[checked] .wx-switch-input,
+checkbox.orange[checked] .wx-checkbox-input,
+radio.orange[checked] .wx-radio-input,
+switch.orange.checked .uni-switch-input,
+checkbox.orange.checked .uni-checkbox-input,
+radio.orange.checked .uni-radio-input {
+	background-color: #f37b1d !important;
+	border-color: #f37b1d !important;
+	color: #ffffff !important;
+}
+
+switch.yellow[checked] .wx-switch-input,
+checkbox.yellow[checked] .wx-checkbox-input,
+radio.yellow[checked] .wx-radio-input,
+switch.yellow.checked .uni-switch-input,
+checkbox.yellow.checked .uni-checkbox-input,
+radio.yellow.checked .uni-radio-input {
+	background-color: #fbbd08 !important;
+	border-color: #fbbd08 !important;
+	color: #333333 !important;
+}
+
+switch.olive[checked] .wx-switch-input,
+checkbox.olive[checked] .wx-checkbox-input,
+radio.olive[checked] .wx-radio-input,
+switch.olive.checked .uni-switch-input,
+checkbox.olive.checked .uni-checkbox-input,
+radio.olive.checked .uni-radio-input {
+	background-color: #8dc63f !important;
+	border-color: #8dc63f !important;
+	color: #ffffff !important;
+}
+
+switch.green[checked] .wx-switch-input,
+switch[checked] .wx-switch-input,
+checkbox.green[checked] .wx-checkbox-input,
+checkbox[checked] .wx-checkbox-input,
+radio.green[checked] .wx-radio-input,
+radio[checked] .wx-radio-input,
+switch.green.checked .uni-switch-input,
+switch.checked .uni-switch-input,
+checkbox.green.checked .uni-checkbox-input,
+checkbox.checked .uni-checkbox-input,
+radio.green.checked .uni-radio-input,
+radio.checked .uni-radio-input {
+	background-color: #39b54a !important;
+	border-color: #39b54a !important;
+	color: #ffffff !important;
+	border-color: #39B54A !important;
+}
+
+switch.cyan[checked] .wx-switch-input,
+checkbox.cyan[checked] .wx-checkbox-input,
+radio.cyan[checked] .wx-radio-input,
+switch.cyan.checked .uni-switch-input,
+checkbox.cyan.checked .uni-checkbox-input,
+radio.cyan.checked .uni-radio-input {
+	background-color: #1cbbb4 !important;
+	border-color: #1cbbb4 !important;
+	color: #ffffff !important;
+}
+
+switch.blue[checked] .wx-switch-input,
+checkbox.blue[checked] .wx-checkbox-input,
+radio.blue[checked] .wx-radio-input,
+switch.blue.checked .uni-switch-input,
+checkbox.blue.checked .uni-checkbox-input,
+radio.blue.checked .uni-radio-input {
+	background-color: #0081ff !important;
+	border-color: #0081ff !important;
+	color: #ffffff !important;
+}
+
+switch.purple[checked] .wx-switch-input,
+checkbox.purple[checked] .wx-checkbox-input,
+radio.purple[checked] .wx-radio-input,
+switch.purple.checked .uni-switch-input,
+checkbox.purple.checked .uni-checkbox-input,
+radio.purple.checked .uni-radio-input {
+	background-color: #6739b6 !important;
+	border-color: #6739b6 !important;
+	color: #ffffff !important;
+}
+
+switch.mauve[checked] .wx-switch-input,
+checkbox.mauve[checked] .wx-checkbox-input,
+radio.mauve[checked] .wx-radio-input,
+switch.mauve.checked .uni-switch-input,
+checkbox.mauve.checked .uni-checkbox-input,
+radio.mauve.checked .uni-radio-input {
+	background-color: #9c26b0 !important;
+	border-color: #9c26b0 !important;
+	color: #ffffff !important;
+}
+
+switch.pink[checked] .wx-switch-input,
+checkbox.pink[checked] .wx-checkbox-input,
+radio.pink[checked] .wx-radio-input,
+switch.pink.checked .uni-switch-input,
+checkbox.pink.checked .uni-checkbox-input,
+radio.pink.checked .uni-radio-input {
+	background-color: #e03997 !important;
+	border-color: #e03997 !important;
+	color: #ffffff !important;
+}
+
+switch.brown[checked] .wx-switch-input,
+checkbox.brown[checked] .wx-checkbox-input,
+radio.brown[checked] .wx-radio-input,
+switch.brown.checked .uni-switch-input,
+checkbox.brown.checked .uni-checkbox-input,
+radio.brown.checked .uni-radio-input {
+	background-color: #a5673f !important;
+	border-color: #a5673f !important;
+	color: #ffffff !important;
+}
+
+switch.grey[checked] .wx-switch-input,
+checkbox.grey[checked] .wx-checkbox-input,
+radio.grey[checked] .wx-radio-input,
+switch.grey.checked .uni-switch-input,
+checkbox.grey.checked .uni-checkbox-input,
+radio.grey.checked .uni-radio-input {
+	background-color: #8799a3 !important;
+	border-color: #8799a3 !important;
+	color: #ffffff !important;
+}
+
+switch.gray[checked] .wx-switch-input,
+checkbox.gray[checked] .wx-checkbox-input,
+radio.gray[checked] .wx-radio-input,
+switch.gray.checked .uni-switch-input,
+checkbox.gray.checked .uni-checkbox-input,
+radio.gray.checked .uni-radio-input {
+	background-color: #f0f0f0 !important;
+	border-color: #f0f0f0 !important;
+	color: #333333 !important;
+}
+
+switch.black[checked] .wx-switch-input,
+checkbox.black[checked] .wx-checkbox-input,
+radio.black[checked] .wx-radio-input,
+switch.black.checked .uni-switch-input,
+checkbox.black.checked .uni-checkbox-input,
+radio.black.checked .uni-radio-input {
+	background-color: #333333 !important;
+	border-color: #333333 !important;
+	color: #ffffff !important;
+}
+
+switch.white[checked] .wx-switch-input,
+checkbox.white[checked] .wx-checkbox-input,
+radio.white[checked] .wx-radio-input,
+switch.white.checked .uni-switch-input,
+checkbox.white.checked .uni-checkbox-input,
+radio.white.checked .uni-radio-input {
+	background-color: #ffffff !important;
+	border-color: #ffffff !important;
+	color: #333333 !important;
+}
+
+/* ==================
+          边框
+ ==================== */
+
+/* -- 实线 -- */
+
+.solid,
+.solid-top,
+.solid-right,
+.solid-bottom,
+.solid-left,
+.solids,
+.solids-top,
+.solids-right,
+.solids-bottom,
+.solids-left,
+.dashed,
+.dashed-top,
+.dashed-right,
+.dashed-bottom,
+.dashed-left {
+	position: relative;
+}
+
+.solid::after,
+.solid-top::after,
+.solid-right::after,
+.solid-bottom::after,
+.solid-left::after,
+.solids::after,
+.solids-top::after,
+.solids-right::after,
+.solids-bottom::after,
+.solids-left::after,
+.dashed::after,
+.dashed-top::after,
+.dashed-right::after,
+.dashed-bottom::after,
+.dashed-left::after {
+	content: " ";
+	width: 200%;
+	height: 200%;
+	position: absolute;
+	top: 0;
+	left: 0;
+	border-radius: inherit;
+	transform: scale(0.5);
+	transform-origin: 0 0;
+	pointer-events: none;
+	box-sizing: border-box;
+}
+
+.solid::after {
+	border: 1upx solid rgba(0, 0, 0, 0.1);
+}
+
+.solid-top::after {
+	border-top: 1upx solid rgba(0, 0, 0, 0.1);
+}
+
+.solid-right::after {
+	border-right: 1upx solid rgba(0, 0, 0, 0.1);
+}
+
+.solid-bottom::after {
+	border-bottom: 1upx solid rgba(0, 0, 0, 0.1);
+}
+
+.solid-left::after {
+	border-left: 1upx solid rgba(0, 0, 0, 0.1);
+}
+
+.solids::after {
+	border: 8upx solid #eee;
+}
+
+.solids-top::after {
+	border-top: 8upx solid #eee;
+}
+
+.solids-right::after {
+	border-right: 8upx solid #eee;
+}
+
+.solids-bottom::after {
+	border-bottom: 8upx solid #eee;
+}
+
+.solids-left::after {
+	border-left: 8upx solid #eee;
+}
+
+/* -- 虚线 -- */
+
+.dashed::after {
+	border: 1upx dashed #ddd;
+}
+
+.dashed-top::after {
+	border-top: 1upx dashed #ddd;
+}
+
+.dashed-right::after {
+	border-right: 1upx dashed #ddd;
+}
+
+.dashed-bottom::after {
+	border-bottom: 1upx dashed #ddd;
+}
+
+.dashed-left::after {
+	border-left: 1upx dashed #ddd;
+}
+
+/* -- 阴影 -- */
+
+.shadow[class*='white'] {
+	--ShadowSize: 0 1upx 6upx;
+}
+
+.shadow-lg {
+	--ShadowSize: 0upx 40upx 100upx 0upx;
+}
+
+.shadow-warp {
+	position: relative;
+	box-shadow: 0 0 10upx rgba(0, 0, 0, 0.1);
+}
+
+.shadow-warp:before,
+.shadow-warp:after {
+	position: absolute;
+	content: "";
+	top: 20upx;
+	bottom: 30upx;
+	left: 20upx;
+	width: 50%;
+	box-shadow: 0 30upx 20upx rgba(0, 0, 0, 0.2);
+	transform: rotate(-3deg);
+	z-index: -1;
+}
+
+.shadow-warp:after {
+	right: 20upx;
+	left: auto;
+	transform: rotate(3deg);
+}
+
+.shadow-blur {
+	position: relative;
+}
+
+.shadow-blur::before {
+	content: "";
+	display: block;
+	background: inherit;
+	filter: blur(10upx);
+	position: absolute;
+	width: 100%;
+	height: 100%;
+	top: 10upx;
+	left: 10upx;
+	z-index: -1;
+	opacity: 0.4;
+	transform-origin: 0 0;
+	border-radius: inherit;
+	transform: scale(1, 1);
+}
+
+/* ==================
+          按钮
+ ==================== */
+
+.cu-btn {
+	position: relative;
+	border: 0upx;
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	box-sizing: border-box;
+	padding: 0 30upx;
+	font-size: 28upx;
+	height: 64upx;
+	line-height: 1;
+	text-align: center;
+	text-decoration: none;
+	overflow: visible;
+	margin-left: initial;
+	transform: translate(0upx, 0upx);
+	margin-right: initial;
+}
+
+.cu-btn::after {
+	display: none;
+}
+
+.cu-btn:not([class*="bg-"]) {
+	background-color: #f0f0f0;
+}
+
+.cu-btn[class*="line"] {
+	background-color: transparent;
+}
+
+.cu-btn[class*="line"]::after {
+	content: " ";
+	display: block;
+	width: 200%;
+	height: 200%;
+	position: absolute;
+	top: 0;
+	left: 0;
+	border: 1upx solid currentColor;
+	transform: scale(0.5);
+	transform-origin: 0 0;
+	box-sizing: border-box;
+	border-radius: 12upx;
+	z-index: 1;
+	pointer-events: none;
+}
+
+.cu-btn.round[class*="line"]::after {
+	border-radius: 1000upx;
+}
+
+.cu-btn[class*="lines"]::after {
+	border: 6upx solid currentColor;
+}
+
+.cu-btn[class*="bg-"]::after {
+	display: none;
+}
+
+.cu-btn.sm {
+	padding: 0 20upx;
+	font-size: 20upx;
+	height: 48upx;
+}
+
+.cu-btn.lg {
+	padding: 0 40upx;
+	font-size: 32upx;
+	height: 80upx;
+}
+
+.cu-btn.cuIcon.sm {
+	width: 48upx;
+	height: 48upx;
+}
+
+.cu-btn.cuIcon {
+	width: 64upx;
+	height: 64upx;
+	border-radius: 500upx;
+	padding: 0;
+}
+
+button.cuIcon.lg {
+	width: 80upx;
+	height: 80upx;
+}
+
+.cu-btn.shadow-blur::before {
+	top: 4upx;
+	left: 4upx;
+	filter: blur(6upx);
+	opacity: 0.6;
+}
+
+.cu-btn.button-hover {
+	transform: translate(1upx, 1upx);
+}
+
+.block {
+	display: block;
+}
+
+.cu-btn.block {
+	display: flex;
+}
+
+.cu-btn[disabled] {
+	opacity: 0.6;
+	color: #ffffff;
+}
+
+/* ==================
+          徽章
+ ==================== */
+
+.cu-tag {
+	font-size: 24upx;
+	vertical-align: middle;
+	position: relative;
+	display: inline-flex;
+	align-items: center;
+	justify-content: center;
+	box-sizing: border-box;
+	padding: 0upx 16upx;
+	height: 48upx;
+	font-family: Helvetica Neue, Helvetica, sans-serif;
+	white-space: nowrap;
+}
+
+.cu-tag:not([class*="bg"]):not([class*="line"]) {
+	background-color: #f1f1f1;
+}
+
+.cu-tag[class*="line-"]::after {
+	content: " ";
+	width: 200%;
+	height: 200%;
+	position: absolute;
+	top: 0;
+	left: 0;
+	border: 1upx solid currentColor;
+	transform: scale(0.5);
+	transform-origin: 0 0;
+	box-sizing: border-box;
+	border-radius: inherit;
+	z-index: 1;
+	pointer-events: none;
+}
+
+.cu-tag.radius[class*="line"]::after {
+	border-radius: 12upx;
+}
+
+.cu-tag.round[class*="line"]::after {
+	border-radius: 1000upx;
+}
+
+.cu-tag[class*="line-"]::after {
+	border-radius: 0;
+}
+
+.cu-tag+.cu-tag {
+	margin-left: 10upx;
+}
+
+.cu-tag.sm {
+	font-size: 20upx;
+	padding: 0upx 12upx;
+	height: 32upx;
+}
+
+.cu-capsule {
+	display: inline-flex;
+	vertical-align: middle;
+}
+
+.cu-capsule+.cu-capsule {
+	margin-left: 10upx;
+}
+
+.cu-capsule .cu-tag {
+	margin: 0;
+}
+
+.cu-capsule .cu-tag[class*="line-"]:last-child::after {
+	border-left: 0upx solid transparent;
+}
+
+.cu-capsule .cu-tag[class*="line-"]:first-child::after {
+	border-right: 0upx solid transparent;
+}
+
+.cu-capsule.radius .cu-tag:first-child {
+	border-top-left-radius: 6upx;
+	border-bottom-left-radius: 6upx;
+}
+
+.cu-capsule.radius .cu-tag:last-child::after,
+.cu-capsule.radius .cu-tag[class*="line-"] {
+	border-top-right-radius: 12upx;
+	border-bottom-right-radius: 12upx;
+}
+
+.cu-capsule.round .cu-tag:first-child {
+	border-top-left-radius: 200upx;
+	border-bottom-left-radius: 200upx;
+	text-indent: 4upx;
+}
+
+.cu-capsule.round .cu-tag:last-child::after,
+.cu-capsule.round .cu-tag:last-child {
+	border-top-right-radius: 200upx;
+	border-bottom-right-radius: 200upx;
+	text-indent: -4upx;
+}
+
+.cu-tag.badge {
+	border-radius: 200upx;
+	position: absolute;
+	top:4upx;
+	right: -10upx;
+	font-size: 20upx;
+	padding: 0upx 10upx;
+	height: 28upx;
+	color: #ffffff;
+}
+
+.cu-tag.badge:not([class*="bg-"]) {
+	background-color: #dd514c;
+}
+
+.cu-tag:empty:not([class*="cuIcon-"]) {
+	padding: 0upx;
+	width: 16upx;
+	height: 16upx;
+	top: -4upx;
+	right: -4upx;
+}
+
+.cu-tag[class*="cuIcon-"] {
+	width: 32upx;
+	height: 32upx;
+	top: -4upx;
+	right: -4upx;
+}
+
+/* ==================
+          头像
+ ==================== */
+
+.cu-avatar {
+	font-variant: small-caps;
+	margin: 0;
+	padding: 0;
+	display: inline-flex;
+	text-align: center;
+	justify-content: center;
+	align-items: center;
+	background-color: #ccc;
+	color: #ffffff;
+	white-space: nowrap;
+	position: relative;
+	width: 64upx;
+	height: 64upx;
+	background-size: cover;
+	background-position: center;
+	vertical-align: middle;
+	font-size: 1.5em;
+}
+
+.cu-avatar.sm {
+	width: 48upx;
+	height: 48upx;
+	font-size: 1em;
+}
+
+.cu-avatar.lg {
+	width: 96upx;
+	height: 96upx;
+	font-size: 2em;
+}
+
+.cu-avatar.xl {
+	width: 128upx;
+	height: 128upx;
+	font-size: 2.5em;
+}
+
+.cu-avatar .avatar-text {
+	font-size: 0.4em;
+}
+
+.cu-avatar-group {
+	direction: rtl;
+	unicode-bidi: bidi-override;
+	padding: 0 10upx 0 40upx;
+	display: inline-block;
+}
+
+.cu-avatar-group .cu-avatar {
+	margin-left: -30upx;
+	border: 4upx solid #f1f1f1;
+	vertical-align: middle;
+}
+
+.cu-avatar-group .cu-avatar.sm {
+	margin-left: -20upx;
+	border: 1upx solid #f1f1f1;
+}
+
+/* ==================
+         进度条
+ ==================== */
+
+.cu-progress {
+	overflow: hidden;
+	height: 28upx;
+	background-color: #ebeef5;
+	display: inline-flex;
+	align-items: center;
+	width: 100%;
+}
+
+.cu-progress+view,
+.cu-progress+text {
+	line-height: 1;
+}
+
+.cu-progress.xs {
+	height: 10upx;
+}
+
+.cu-progress.sm {
+	height: 20upx;
+}
+
+.cu-progress view {
+	width: 0;
+	height: 100%;
+	align-items: center;
+	display: flex;
+	justify-items: flex-end;
+	justify-content: space-around;
+	font-size: 20upx;
+	color: #ffffff;
+	transition: width 0.6s ease;
+}
+
+.cu-progress text {
+	align-items: center;
+	display: flex;
+	font-size: 20upx;
+	color: #333333;
+	text-indent: 10upx;
+}
+
+.cu-progress.text-progress {
+	padding-right: 60upx;
+}
+
+.cu-progress.striped view {
+	background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+	background-size: 72upx 72upx;
+}
+
+.cu-progress.active view {
+	animation: progress-stripes 2s linear infinite;
+}
+
+@keyframes progress-stripes {
+	from {
+		background-position: 72upx 0;
+	}
+
+	to {
+		background-position: 0 0;
+	}
+}
+
+/* ==================
+          加载
+ ==================== */
+
+.cu-load {
+	display: block;
+	line-height: 3em;
+	text-align: center;
+}
+
+.cu-load::before {
+	font-family: "cuIcon";
+	display: inline-block;
+	margin-right: 6upx;
+}
+
+.cu-load.loading::before {
+	content: "\e67a";
+	animation: cuIcon-spin 2s infinite linear;
+}
+
+.cu-load.loading::after {
+	content: "加载中...";
+}
+
+.cu-load.over::before {
+	content: "\e64a";
+}
+
+.cu-load.over::after {
+	content: "没有更多了";
+}
+
+.cu-load.erro::before {
+	content: "\e658";
+}
+
+.cu-load.erro::after {
+	content: "加载失败";
+}
+
+.cu-load.load-cuIcon::before {
+	font-size: 32upx;
+}
+
+.cu-load.load-cuIcon::after {
+	display: none;
+}
+
+.cu-load.load-cuIcon.over {
+	display: none;
+}
+
+.cu-load.load-modal {
+	position: fixed;
+	top: 0;
+	right: 0;
+	bottom: 140upx;
+	left: 0;
+	margin: auto;
+	width: 260upx;
+	height: 260upx;
+	background-color: #ffffff;
+	border-radius: 10upx;
+	box-shadow: 0 0 0upx 2000upx rgba(0, 0, 0, 0.5);
+	display: flex;
+	align-items: center;
+	flex-direction: column;
+	justify-content: center;
+	font-size: 28upx;
+	z-index: 9999;
+	line-height: 2.4em;
+}
+
+.cu-load.load-modal [class*="cuIcon-"] {
+	font-size: 60upx;
+}
+
+.cu-load.load-modal image {
+	width: 70upx;
+	height: 70upx;
+}
+
+.cu-load.load-modal::after {
+	content: "";
+	position: absolute;
+	background-color: #ffffff;
+	border-radius: 50%;
+	width: 200upx;
+	height: 200upx;
+	font-size: 10px;
+	border-top: 6upx solid rgba(0, 0, 0, 0.05);
+	border-right: 6upx solid rgba(0, 0, 0, 0.05);
+	border-bottom: 6upx solid rgba(0, 0, 0, 0.05);
+	border-left: 6upx solid #f37b1d;
+	animation: cuIcon-spin 1s infinite linear;
+	z-index: -1;
+}
+
+.load-progress {
+	pointer-events: none;
+	top: 0;
+	position: fixed;
+	width: 100%;
+	left: 0;
+	z-index: 2000;
+}
+
+.load-progress.hide {
+	display: none;
+}
+
+.load-progress .load-progress-bar {
+	position: relative;
+	width: 100%;
+	height: 4upx;
+	overflow: hidden;
+	transition: all 200ms ease 0s;
+}
+
+.load-progress .load-progress-spinner {
+	position: absolute;
+	top: 10upx;
+	right: 10upx;
+	z-index: 2000;
+	display: block;
+}
+
+.load-progress .load-progress-spinner::after {
+	content: "";
+	display: block;
+	width: 24upx;
+	height: 24upx;
+	-webkit-box-sizing: border-box;
+	box-sizing: border-box;
+	border: solid 4upx transparent;
+	border-top-color: inherit;
+	border-left-color: inherit;
+	border-radius: 50%;
+	-webkit-animation: load-progress-spinner 0.4s linear infinite;
+	animation: load-progress-spinner 0.4s linear infinite;
+}
+
+@-webkit-keyframes load-progress-spinner {
+	0% {
+		-webkit-transform: rotate(0);
+		transform: rotate(0);
+	}
+
+	100% {
+		-webkit-transform: rotate(360deg);
+		transform: rotate(360deg);
+	}
+}
+
+@keyframes load-progress-spinner {
+	0% {
+		-webkit-transform: rotate(0);
+		transform: rotate(0);
+	}
+
+	100% {
+		-webkit-transform: rotate(360deg);
+		transform: rotate(360deg);
+	}
+}
+
+/* ==================
+          列表
+ ==================== */
+.grayscale {
+	filter: grayscale(1);
+}
+
+.cu-list+.cu-list {
+	margin-top: 30upx
+}
+
+.cu-list>.cu-item {
+	transition: all .6s ease-in-out 0s;
+	transform: translateX(0upx)
+}
+
+.cu-list>.cu-item.move-cur {
+	transform: translateX(-260upx)
+}
+
+.cu-list>.cu-item .move {
+	position: absolute;
+	right: 0;
+	display: flex;
+	width: 260upx;
+	height: 100%;
+	transform: translateX(100%)
+}
+
+.cu-list>.cu-item .move view {
+	display: flex;
+	flex: 1;
+	justify-content: center;
+	align-items: center
+}
+
+.cu-list.menu-avatar {
+	overflow: hidden;
+}
+
+.cu-list.menu-avatar>.cu-item {
+	position: relative;
+	display: flex;
+	padding-right: 10upx;
+	height: 140upx;
+	background-color: #ffffff;
+	justify-content: flex-end;
+	align-items: center
+}
+
+.cu-list.menu-avatar>.cu-item>.cu-avatar {
+	position: absolute;
+	left: 30upx
+}
+
+.cu-list.menu-avatar>.cu-item .flex .text-cut {
+	max-width: 510upx
+}
+
+.cu-list.menu-avatar>.cu-item .content {
+	position: absolute;
+	left: 146upx;
+	width: calc(100% - 96upx - 60upx - 120upx - 20upx);
+	line-height: 1.6em;
+}
+
+.cu-list.menu-avatar>.cu-item .content.flex-sub {
+	width: calc(100% - 96upx - 60upx - 20upx);
+}
+
+.cu-list.menu-avatar>.cu-item .content>view:first-child {
+	font-size: 30upx;
+	display: flex;
+	align-items: center
+}
+
+.cu-list.menu-avatar>.cu-item .content .cu-tag.sm {
+	display: inline-block;
+	margin-left: 10upx;
+	height: 28upx;
+	font-size: 16upx;
+	line-height: 32upx
+}
+
+.cu-list.menu-avatar>.cu-item .action {
+	width: 100upx;
+	text-align: center
+}
+
+.cu-list.menu-avatar>.cu-item .action view+view {
+	margin-top: 10upx
+}
+
+.cu-list.menu-avatar.comment>.cu-item .content {
+	position: relative;
+	left: 0;
+	width: auto;
+	flex: 1;
+}
+
+.cu-list.menu-avatar.comment>.cu-item {
+	padding: 30upx 30upx 30upx 120upx;
+	height: auto
+}
+
+.cu-list.menu-avatar.comment .cu-avatar {
+	align-self: flex-start
+}
+
+.cu-list.menu>.cu-item {
+	position: relative;
+	display: flex;
+	padding: 0 30upx;
+	min-height: 100upx;
+	background-color: #ffffff;
+	justify-content: space-between;
+	align-items: center
+}
+
+.cu-list.menu>.cu-item:last-child:after {
+	border: none
+}
+
+.cu-list.menu-avatar>.cu-item:after,
+.cu-list.menu>.cu-item:after {
+	position: absolute;
+	top: 0;
+	left: 0;
+	box-sizing: border-box;
+	width: 200%;
+	height: 200%;
+	border-bottom: 1upx solid #ddd;
+	border-radius: inherit;
+	content: " ";
+	transform: scale(.5);
+	transform-origin: 0 0;
+	pointer-events: none
+}
+
+.cu-list.menu>.cu-item.grayscale {
+	background-color: #f5f5f5
+}
+
+.cu-list.menu>.cu-item.cur {
+	background-color: #fcf7e9
+}
+
+.cu-list.menu>.cu-item.arrow {
+	padding-right: 90upx
+}
+
+.cu-list.menu>.cu-item.arrow:before {
+	position: absolute;
+	top: 0;
+	right: 30upx;
+	bottom: 0;
+	display: block;
+	margin: auto;
+	width: 30upx;
+	height: 30upx;
+	color: #8799a3;
+	content: "\e6a3";
+	text-align: center;
+	font-size: 34upx;
+	font-family: cuIcon;
+	line-height: 30upx
+}
+
+.cu-list.menu>.cu-item button.content {
+	padding: 0;
+	background-color: transparent;
+	justify-content: flex-start
+}
+
+.cu-list.menu>.cu-item button.content:after {
+	display: none
+}
+
+.cu-list.menu>.cu-item .cu-avatar-group .cu-avatar {
+	border-color: #ffffff
+}
+
+.cu-list.menu>.cu-item .content>view:first-child {
+	display: flex;
+	align-items: center
+}
+
+.cu-list.menu>.cu-item .content>text[class*=cuIcon] {
+	display: inline-block;
+	margin-right: 10upx;
+	width: 1.6em;
+	text-align: center
+}
+
+.cu-list.menu>.cu-item .content>image {
+	display: inline-block;
+	margin-right: 10upx;
+	width: 1.6em;
+	height: 1.6em;
+	vertical-align: middle
+}
+
+.cu-list.menu>.cu-item .content {
+	font-size: 30upx;
+	line-height: 1.6em;
+	flex: 1
+}
+
+.cu-list.menu>.cu-item .content .cu-tag.sm {
+	display: inline-block;
+	margin-left: 10upx;
+	height: 28upx;
+	font-size: 16upx;
+	line-height: 32upx
+}
+
+.cu-list.menu>.cu-item .action .cu-tag:empty {
+	right: 10upx
+}
+
+.cu-list.menu {
+	display: block;
+	overflow: hidden
+}
+
+.cu-list.menu.sm-border>.cu-item:after {
+	left: 30upx;
+	width: calc(200% - 120upx)
+}
+
+.cu-list.grid>.cu-item {
+	position: relative;
+	display: flex;
+	padding: 20upx 0 30upx;
+	transition-duration: 0s;
+	flex-direction: column
+}
+
+.cu-list.grid>.cu-item:after {
+	position: absolute;
+	top: 0;
+	left: 0;
+	box-sizing: border-box;
+	width: 200%;
+	height: 200%;
+	border-right: 1px solid rgba(0, 0, 0, .1);
+	border-bottom: 1px solid rgba(0, 0, 0, .1);
+	border-radius: inherit;
+	content: " ";
+	transform: scale(.5);
+	transform-origin: 0 0;
+	pointer-events: none
+}
+
+.cu-list.grid>.cu-item text {
+	display: block;
+	margin-top: 10upx;
+	color: #888;
+	font-size: 26upx;
+	line-height: 40upx
+}
+
+.cu-list.grid>.cu-item [class*=cuIcon] {
+	position: relative;
+	display: block;
+	margin-top: 20upx;
+	width: 100%;
+	font-size: 48upx
+}
+
+.cu-list.grid>.cu-item .cu-tag {
+	right: auto;
+	left: 50%;
+	margin-left: 20upx
+}
+
+.cu-list.grid {
+	background-color: #ffffff;
+	text-align: center
+}
+
+.cu-list.grid.no-border>.cu-item {
+	padding-top: 10upx;
+	padding-bottom: 20upx
+}
+
+.cu-list.grid.no-border>.cu-item:after {
+	border: none
+}
+
+.cu-list.grid.no-border {
+	padding: 20upx 10upx
+}
+
+.cu-list.grid.col-3>.cu-item:nth-child(3n):after,
+.cu-list.grid.col-4>.cu-item:nth-child(4n):after,
+.cu-list.grid.col-5>.cu-item:nth-child(5n):after {
+	border-right-width: 0
+}
+
+.cu-list.card-menu {
+	overflow: hidden;
+	margin-right: 30upx;
+	margin-left: 30upx;
+	border-radius: 20upx
+}
+
+
+/* ==================
+          操作条
+ ==================== */
+
+.cu-bar {
+	display: flex;
+	position: relative;
+	align-items: center;
+	min-height: 100upx;
+	justify-content: space-between;
+}
+
+.cu-bar .action {
+	display: flex;
+	align-items: center;
+	height: 100%;
+	justify-content: center;
+	max-width: 100%;
+}
+
+.cu-bar .action.border-title {
+	position: relative;
+	top: -10upx;
+}
+
+.cu-bar .action.border-title text[class*="bg-"]:last-child {
+	position: absolute;
+	bottom: -0.5rem;
+	min-width: 2rem;
+	height: 6upx;
+	left: 0;
+}
+
+.cu-bar .action.sub-title {
+	position: relative;
+	top: -0.2rem;
+}
+
+.cu-bar .action.sub-title text {
+	position: relative;
+	z-index: 1;
+}
+
+.cu-bar .action.sub-title text[class*="bg-"]:last-child {
+	position: absolute;
+	display: inline-block;
+	bottom: -0.2rem;
+	border-radius: 6upx;
+	width: 100%;
+	height: 0.6rem;
+	left: 0.6rem;
+	opacity: 0.3;
+	z-index: 0;
+}
+
+.cu-bar .action.sub-title text[class*="text-"]:last-child {
+	position: absolute;
+	display: inline-block;
+	bottom: -0.7rem;
+	left: 0.5rem;
+	opacity: 0.2;
+	z-index: 0;
+	text-align: right;
+	font-weight: 900;
+	font-size: 36upx;
+}
+
+.cu-bar.justify-center .action.border-title text:last-child,
+.cu-bar.justify-center .action.sub-title text:last-child {
+	left: 0;
+	right: 0;
+	margin: auto;
+	text-align: center;
+}
+
+.cu-bar .action:first-child {
+	margin-left: 30upx;
+	font-size: 30upx;
+}
+
+.cu-bar .action text.text-cut {
+	text-align: left;
+	width: 100%;
+}
+
+.cu-bar .cu-avatar:first-child {
+	margin-left: 20upx;
+}
+
+.cu-bar .action:first-child>text[class*="cuIcon-"] {
+	margin-left: -0.3em;
+	margin-right: 0.3em;
+}
+
+.cu-bar .action:last-child {
+	margin-right: 30upx;
+}
+
+.cu-bar .action>text[class*="cuIcon-"],
+.cu-bar .action>view[class*="cuIcon-"] {
+	font-size: 36upx;
+}
+
+.cu-bar .action>text[class*="cuIcon-"]+text[class*="cuIcon-"] {
+	margin-left: 0.5em;
+}
+
+.cu-bar .content {
+	position: absolute;
+	text-align: center;
+	width: calc(100% - 340upx);
+	left: 0;
+	right: 0;
+	bottom: 0;
+	top: 0;
+	margin: auto;
+	height: 60upx;
+	font-size: 32upx;
+	line-height: 60upx;
+	cursor: none;
+	pointer-events: none;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	overflow: hidden;
+}
+
+.cu-bar.ios .content {
+	bottom: 7px;
+	height: 30px;
+	font-size: 32upx;
+	line-height: 30px;
+}
+
+.cu-bar.btn-group {
+	justify-content: space-around;
+}
+
+.cu-bar.btn-group button {
+	padding: 20upx 32upx;
+}
+
+.cu-bar.btn-group button {
+	flex: 1;
+	margin: 0 20upx;
+	max-width: 50%;
+}
+
+.cu-bar .search-form {
+	background-color: #f5f5f5;
+	line-height: 64upx;
+	height: 64upx;
+	font-size: 24upx;
+	color: #333333;
+	flex: 1;
+	display: flex;
+	align-items: center;
+	margin: 0 30upx;
+}
+
+.cu-bar .search-form+.action {
+	margin-right: 30upx;
+}
+
+.cu-bar .search-form input {
+	flex: 1;
+	padding-right: 30upx;
+	height: 64upx;
+	line-height: 64upx;
+	font-size: 26upx;
+	background-color: transparent;
+}
+
+.cu-bar .search-form [class*="cuIcon-"] {
+	margin: 0 0.5em 0 0.8em;
+}
+
+.cu-bar .search-form [class*="cuIcon-"]::before {
+	top: 0upx;
+}
+
+.cu-bar.fixed,
+.nav.fixed {
+	position: fixed;
+	width: 100%;
+	top: 0;
+	z-index: 1024;
+	box-shadow: 0 1upx 6upx rgba(0, 0, 0, 0.1);
+}
+
+.cu-bar.foot {
+	position: fixed;
+	width: 100%;
+	bottom: 0;
+	z-index: 1024;
+	box-shadow: 0 -1upx 6upx rgba(0, 0, 0, 0.1);
+}
+
+.cu-bar.tabbar {
+	padding: 0;
+	height: calc(100upx + env(safe-area-inset-bottom) / 2);
+	padding-bottom: calc(env(safe-area-inset-bottom) / 2);
+}
+
+.cu-tabbar-height {
+	min-height: 100upx;
+	height: calc(100upx + env(safe-area-inset-bottom) / 2);
+}
+
+.cu-bar.tabbar.shadow {
+	box-shadow: 0 -1upx 6upx rgba(0, 0, 0, 0.1);
+}
+
+.cu-bar.tabbar .action {
+	font-size: 22upx;
+	position: relative;
+	flex: 1;
+	text-align: center;
+	padding: 0;
+	display: block;
+	height: auto;
+	line-height: 1;
+	margin: 0;
+	background-color: inherit;
+	overflow: initial;
+}
+
+.cu-bar.tabbar.shop .action {
+	width: 140upx;
+	flex: initial;
+}
+
+.cu-bar.tabbar .action.add-action {
+	position: relative;
+	z-index: 2;
+	padding-top: 50upx;
+}
+
+.cu-bar.tabbar .action.add-action [class*="cuIcon-"] {
+	position: absolute;
+	width: 70upx;
+	z-index: 2;
+	height: 70upx;
+	border-radius: 50%;
+	line-height: 70upx;
+	font-size: 50upx;
+	top: -35upx;
+	left: 0;
+	right: 0;
+	margin: auto;
+	padding: 0;
+}
+
+.cu-bar.tabbar .action.add-action::after {
+	content: "";
+	position: absolute;
+	width: 100upx;
+	height: 100upx;
+	top: -50upx;
+	left: 0;
+	right: 0;
+	margin: auto;
+	box-shadow: 0 -3upx 8upx rgba(0, 0, 0, 0.08);
+	border-radius: 50upx;
+	background-color: inherit;
+	z-index: 0;
+}
+
+.cu-bar.tabbar .action.add-action::before {
+	content: "";
+	position: absolute;
+	width: 100upx;
+	height: 30upx;
+	bottom: 30upx;
+	left: 0;
+	right: 0;
+	margin: auto;
+	background-color: inherit;
+	z-index: 1;
+}
+
+.cu-bar.tabbar .btn-group {
+	flex: 1;
+	display: flex;
+	justify-content: space-around;
+	align-items: center;
+	padding: 0 10upx;
+}
+
+.cu-bar.tabbar button.action::after {
+	border: 0;
+}
+
+.cu-bar.tabbar .action [class*="cuIcon-"] {
+	width: 100upx;
+	position: relative;
+	display: block;
+	height: auto;
+	margin: 0 auto 10upx;
+	text-align: center;
+	font-size: 40upx;
+}
+
+.cu-bar.tabbar .action .cuIcon-cu-image {
+	margin: 0 auto;
+}
+
+.cu-bar.tabbar .action .cuIcon-cu-image image {
+	width: 50upx;
+	height: 50upx;
+	display: inline-block;
+}
+
+.cu-bar.tabbar .submit {
+	align-items: center;
+	display: flex;
+	justify-content: center;
+	text-align: center;
+	position: relative;
+	flex: 2;
+	align-self: stretch;
+}
+
+.cu-bar.tabbar .submit:last-child {
+	flex: 2.6;
+}
+
+.cu-bar.tabbar .submit+.submit {
+	flex: 2;
+}
+
+.cu-bar.tabbar.border .action::before {
+	content: " ";
+	width: 200%;
+	height: 200%;
+	position: absolute;
+	top: 0;
+	left: 0;
+	transform: scale(0.5);
+	transform-origin: 0 0;
+	border-right: 1upx solid rgba(0, 0, 0, 0.1);
+	z-index: 3;
+}
+
+.cu-bar.tabbar.border .action:last-child:before {
+	display: none;
+}
+
+.cu-bar.input {
+	padding-right: 20upx;
+	background-color: #ffffff;
+}
+
+.cu-bar.input input {
+	overflow: initial;
+	line-height: 64upx;
+	height: 64upx;
+	min-height: 64upx;
+	flex: 1;
+	font-size: 30upx;
+	margin: 0 20upx;
+}
+
+.cu-bar.input .action {
+	margin-left: 20upx;
+}
+
+.cu-bar.input .action [class*="cuIcon-"] {
+	font-size: 48upx;
+}
+
+.cu-bar.input input+.action {
+	margin-right: 20upx;
+	margin-left: 0upx;
+}
+
+.cu-bar.input .action:first-child [class*="cuIcon-"] {
+	margin-left: 0upx;
+}
+
+.cu-custom {
+	display: block;
+	position: relative;
+}
+
+.cu-custom .cu-bar .content {
+	width: calc(100% - 440upx);
+}
+
+/* #ifdef MP-ALIPAY */
+.cu-custom .cu-bar .action .cuIcon-back {
+	opacity: 0;
+}
+
+/* #endif */
+
+.cu-custom .cu-bar .content image {
+	height: 60upx;
+	width: 240upx;
+}
+
+.cu-custom .cu-bar {
+	min-height: 0px;
+	/* #ifdef MP-WEIXIN */
+	padding-right: 220upx;
+	/* #endif */
+	/* #ifdef MP-ALIPAY */
+	padding-right: 150upx;
+	/* #endif */
+	box-shadow: 0upx 0upx 0upx;
+	z-index: 9999;
+}
+
+.cu-custom .cu-bar .border-custom {
+	position: relative;
+	background: rgba(0, 0, 0, 0.15);
+	border-radius: 1000upx;
+	height: 30px;
+}
+
+.cu-custom .cu-bar .border-custom::after {
+	content: " ";
+	width: 200%;
+	height: 200%;
+	position: absolute;
+	top: 0;
+	left: 0;
+	border-radius: inherit;
+	transform: scale(0.5);
+	transform-origin: 0 0;
+	pointer-events: none;
+	box-sizing: border-box;
+	border: 1upx solid #ffffff;
+	opacity: 0.5;
+}
+
+.cu-custom .cu-bar .border-custom::before {
+	content: " ";
+	width: 1upx;
+	height: 110%;
+	position: absolute;
+	top: 22.5%;
+	left: 0;
+	right: 0;
+	margin: auto;
+	transform: scale(0.5);
+	transform-origin: 0 0;
+	pointer-events: none;
+	box-sizing: border-box;
+	opacity: 0.6;
+	background-color: #ffffff;
+}
+
+.cu-custom .cu-bar .border-custom text {
+	display: block;
+	flex: 1;
+	margin: auto !important;
+	text-align: center;
+	font-size: 34upx;
+}
+
+/* ==================
+         导航栏
+ ==================== */
+
+.nav {
+	white-space: nowrap;
+}
+
+::-webkit-scrollbar {
+	display: none;
+}
+
+.nav .cu-item {
+	height: 90upx;
+	display: inline-block;
+	line-height: 90upx;
+	margin: 0 10upx;
+	padding: 0 20upx;
+}
+
+.nav .cu-item.cur {
+	border-bottom: 4upx solid;
+}
+
+/* ==================
+         时间轴
+ ==================== */
+
+.cu-timeline {
+	display: block;
+	background-color: #ffffff;
+}
+
+.cu-timeline .cu-time {
+	width: 120upx;
+	text-align: center;
+	padding: 20upx 0;
+	font-size: 26upx;
+	color: #888;
+	display: block;
+}
+
+.cu-timeline>.cu-item {
+	padding: 30upx 30upx 30upx 120upx;
+	position: relative;
+	display: block;
+	z-index: 0;
+}
+
+.cu-timeline>.cu-item:not([class*="text-"]) {
+	color: #ccc;
+}
+
+.cu-timeline>.cu-item::after {
+	content: "";
+	display: block;
+	position: absolute;
+	width: 1upx;
+	background-color: #ddd;
+	left: 60upx;
+	height: 100%;
+	top: 0;
+	z-index: 8;
+}
+
+.cu-timeline>.cu-item::before {
+	font-family: "cuIcon";
+	display: block;
+	position: absolute;
+	top: 36upx;
+	z-index: 9;
+	background-color: #ffffff;
+	width: 50upx;
+	height: 50upx;
+	text-align: center;
+	border: none;
+	line-height: 50upx;
+	left: 36upx;
+}
+
+.cu-timeline>.cu-item:not([class*="cuIcon-"])::before {
+	content: "\e763";
+}
+
+.cu-timeline>.cu-item[class*="cuIcon-"]::before {
+	background-color: #ffffff;
+	width: 50upx;
+	height: 50upx;
+	text-align: center;
+	border: none;
+	line-height: 50upx;
+	left: 36upx;
+}
+
+.cu-timeline>.cu-item>.content {
+	padding: 30upx;
+	border-radius: 6upx;
+	display: block;
+	line-height: 1.6;
+}
+
+.cu-timeline>.cu-item>.content:not([class*="bg-"]) {
+	background-color: #f1f1f1;
+	color: #333333;
+}
+
+.cu-timeline>.cu-item>.content+.content {
+	margin-top: 20upx;
+}
+
+/* ==================
+         聊天
+ ==================== */
+
+.cu-chat {
+	display: flex;
+	flex-direction: column;
+}
+
+.cu-chat .cu-item {
+	display: flex;
+	padding: 30upx 30upx 70upx;
+	position: relative;
+}
+
+.cu-chat .cu-item>.cu-avatar {
+	width: 80upx;
+	height: 80upx;
+}
+
+.cu-chat .cu-item>.main {
+	max-width: calc(100% - 260upx);
+	margin: 0 40upx;
+	display: flex;
+	align-items: center;
+}
+
+.cu-chat .cu-item>image {
+	height: 320upx;
+}
+
+.cu-chat .cu-item>.main .content {
+	padding: 20upx;
+	border-radius: 6upx;
+	display: inline-flex;
+	max-width: 100%;
+	align-items: center;
+	font-size: 30upx;
+	position: relative;
+	min-height: 80upx;
+	line-height: 40upx;
+	text-align: left;
+}
+
+.cu-chat .cu-item>.main .content:not([class*="bg-"]) {
+	background-color: #ffffff;
+	color: #333333;
+}
+
+.cu-chat .cu-item .date {
+	position: absolute;
+	font-size: 24upx;
+	color: #8799a3;
+	width: calc(100% - 320upx);
+	bottom: 20upx;
+	left: 160upx;
+}
+
+.cu-chat .cu-item .action {
+	padding: 0 30upx;
+	display: flex;
+	align-items: center;
+}
+
+.cu-chat .cu-item>.main .content::after {
+	content: "";
+	top: 27upx;
+	transform: rotate(45deg);
+	position: absolute;
+	z-index: 100;
+	display: inline-block;
+	overflow: hidden;
+	width: 24upx;
+	height: 24upx;
+	left: -12upx;
+	right: initial;
+	background-color: inherit;
+}
+
+.cu-chat .cu-item.self>.main .content::after {
+	left: auto;
+	right: -12upx;
+}
+
+.cu-chat .cu-item>.main .content::before {
+	content: "";
+	top: 30upx;
+	transform: rotate(45deg);
+	position: absolute;
+	z-index: -1;
+	display: inline-block;
+	overflow: hidden;
+	width: 24upx;
+	height: 24upx;
+	left: -12upx;
+	right: initial;
+	background-color: inherit;
+	filter: blur(5upx);
+	opacity: 0.3;
+}
+
+.cu-chat .cu-item>.main .content:not([class*="bg-"])::before {
+	background-color: #333333;
+	opacity: 0.1;
+}
+
+.cu-chat .cu-item.self>.main .content::before {
+	left: auto;
+	right: -12upx;
+}
+
+.cu-chat .cu-item.self {
+	justify-content: flex-end;
+	text-align: right;
+}
+
+.cu-chat .cu-info {
+	display: inline-block;
+	margin: 20upx auto;
+	font-size: 24upx;
+	padding: 8upx 12upx;
+	background-color: rgba(0, 0, 0, 0.2);
+	border-radius: 6upx;
+	color: #ffffff;
+	max-width: 400upx;
+	line-height: 1.4;
+}
+
+/* ==================
+         卡片
+ ==================== */
+
+.cu-card {
+	display: block;
+	overflow: hidden;
+}
+
+.cu-card>.cu-item {
+	display: block;
+	background-color: #ffffff;
+	overflow: hidden;
+	border-radius: 10upx;
+	margin: 30upx;
+}
+
+.cu-card>.cu-item.shadow-blur {
+	overflow: initial;
+}
+
+.cu-card.no-card>.cu-item {
+	margin: 0upx;
+	border-radius: 0upx;
+}
+
+.cu-card .grid.grid-square {
+	margin-bottom: -20upx;
+}
+
+.cu-card.case .image {
+	position: relative;
+}
+
+.cu-card.case .image image {
+	width: 100%;
+}
+
+.cu-card.case .image .cu-tag {
+	position: absolute;
+	right: 0;
+	top: 0;
+}
+
+.cu-card.case .image .cu-bar {
+	position: absolute;
+	bottom: 0;
+	width: 100%;
+	background-color: transparent;
+	padding: 0upx 30upx;
+}
+
+.cu-card.case.no-card .image {
+	margin: 30upx 30upx 0;
+	overflow: hidden;
+	border-radius: 10upx;
+}
+
+.cu-card.dynamic {
+	display: block;
+}
+
+.cu-card.dynamic>.cu-item {
+	display: block;
+	background-color: #ffffff;
+	overflow: hidden;
+}
+
+.cu-card.dynamic>.cu-item>.text-content {
+	padding: 0 30upx 0;
+	max-height: 6.4em;
+	overflow: hidden;
+	font-size: 30upx;
+	margin-bottom: 20upx;
+}
+
+.cu-card.dynamic>.cu-item .square-img {
+	width: 100%;
+	height: 200upx;
+	border-radius: 6upx;
+}
+
+.cu-card.dynamic>.cu-item .only-img {
+	width: 100%;
+	height: 320upx;
+	border-radius: 6upx;
+}
+
+/* card.dynamic>.cu-item .comment {
+  padding: 20upx;
+  background-color: #f1f1f1;
+  margin: 0 30upx 30upx;
+  border-radius: 6upx;
+} */
+
+.cu-card.article {
+	display: block;
+}
+
+.cu-card.article>.cu-item {
+	padding-bottom: 30upx;
+}
+
+.cu-card.article>.cu-item .title {
+	font-size: 30upx;
+	font-weight: 900;
+	color: #333333;
+	line-height: 100upx;
+	padding: 0 30upx;
+}
+
+.cu-card.article>.cu-item .content {
+	display: flex;
+	padding: 0 30upx;
+}
+
+.cu-card.article>.cu-item .content>image {
+	width: 240upx;
+	height: 6.4em;
+	margin-right: 20upx;
+	border-radius: 6upx;
+}
+
+.cu-card.article>.cu-item .content .desc {
+	flex: 1;
+	display: flex;
+	flex-direction: column;
+	justify-content: space-between;
+}
+
+.cu-card.article>.cu-item .content .text-content {
+	font-size: 28upx;
+	color: #888;
+	height: 4.8em;
+	overflow: hidden;
+}
+
+/* ==================
+         表单
+ ==================== */
+
+.cu-form-group {
+	background-color: #ffffff;
+	padding: 1upx 30upx;
+	display: flex;
+	align-items: center;
+	min-height: 100upx;
+	justify-content: space-between;
+}
+
+.cu-form-group+.cu-form-group {
+	border-top: 1upx solid #eee;
+}
+
+.cu-form-group .title {
+	text-align: justify;
+	padding-right: 30upx;
+	font-size: 30upx;
+	position: relative;
+	height: 60upx;
+	line-height: 60upx;
+}
+
+.cu-form-group input {
+	flex: 1;
+	font-size: 30upx;
+	color: #555;
+	padding-right: 20upx;
+}
+
+.cu-form-group>text[class*="cuIcon-"] {
+	font-size: 36upx;
+	padding: 0;
+	box-sizing: border-box;
+}
+
+.cu-form-group textarea {
+	margin: 32upx 0 30upx;
+	height: 4.6em;
+	width: 100%;
+	line-height: 1.2em;
+	flex: 1;
+	font-size: 28upx;
+	padding: 0;
+}
+
+.cu-form-group.align-start .title {
+	height: 1em;
+	margin-top: 32upx;
+	line-height: 1em;
+}
+
+.cu-form-group picker {
+	flex: 1;
+	padding-right: 40upx;
+	overflow: hidden;
+	position: relative;
+}
+
+.cu-form-group picker .picker {
+	line-height: 100upx;
+	font-size: 28upx;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	overflow: hidden;
+	width: 100%;
+	text-align: right;
+}
+
+.cu-form-group picker::after {
+	font-family: cuIcon;
+	display: block;
+	content: "\e6a3";
+	position: absolute;
+	font-size: 34upx;
+	color: #8799a3;
+	line-height: 100upx;
+	width: 60upx;
+	text-align: center;
+	top: 0;
+	bottom: 0;
+	right: -20upx;
+	margin: auto;
+}
+
+.cu-form-group textarea[disabled],
+.cu-form-group textarea[disabled] .placeholder {
+	color: transparent;
+}
+
+/* ==================
+         模态窗口
+ ==================== */
+
+.cu-modal {
+	position: fixed;
+	top: 0;
+	right: 0;
+	bottom: 0;
+	left: 0;
+	z-index: 1110;
+	opacity: 0;
+	outline: 0;
+	text-align: center;
+	-ms-transform: scale(1.185);
+	transform: scale(1.185);
+	backface-visibility: hidden;
+	perspective: 2000upx;
+	background: rgba(0, 0, 0, 0.6);
+	transition: all 0.3s ease-in-out 0s;
+	pointer-events: none;
+}
+
+.cu-modal::before {
+	content: "\200B";
+	display: inline-block;
+	height: 100%;
+	vertical-align: middle;
+}
+
+.cu-modal.show {
+	opacity: 1;
+	transition-duration: 0.3s;
+	-ms-transform: scale(1);
+	transform: scale(1);
+	overflow-x: hidden;
+	overflow-y: auto;
+	pointer-events: auto;
+}
+
+.cu-dialog {
+	position: relative;
+	display: inline-block;
+	vertical-align: middle;
+	margin-left: auto;
+	margin-right: auto;
+	width: 680upx;
+	max-width: 100%;
+	background-color: #f8f8f8;
+	border-radius: 10upx;
+	overflow: hidden;
+}
+
+.cu-modal.bottom-modal::before {
+	vertical-align: bottom;
+}
+
+.cu-modal.bottom-modal .cu-dialog {
+	width: 100%;
+	border-radius: 0;
+}
+
+.cu-modal.bottom-modal {
+	margin-bottom: -1000upx;
+}
+
+.cu-modal.bottom-modal.show {
+	margin-bottom: 0;
+}
+
+.cu-modal.drawer-modal {
+	transform: scale(1);
+	display: flex;
+}
+
+.cu-modal.drawer-modal .cu-dialog {
+	height: 100%;
+	min-width: 200upx;
+	border-radius: 0;
+	margin: initial;
+	transition-duration: 0.3s;
+}
+
+.cu-modal.drawer-modal.justify-start .cu-dialog {
+	transform: translateX(-100%);
+}
+
+.cu-modal.drawer-modal.justify-end .cu-dialog {
+	transform: translateX(100%);
+}
+
+.cu-modal.drawer-modal.show .cu-dialog {
+	transform: translateX(0%);
+}
+.cu-modal .cu-dialog>.cu-bar:first-child .action{
+  min-width: 100rpx;
+  margin-right: 0;
+  min-height: 100rpx;
+}
+/* ==================
+         轮播
+ ==================== */
+swiper .a-swiper-dot {
+	display: inline-block;
+	width: 16upx;
+	height: 16upx;
+	background: rgba(0, 0, 0, .3);
+	border-radius: 50%;
+	vertical-align: middle;
+}
+
+swiper[class*="-dot"] .wx-swiper-dots,
+swiper[class*="-dot"] .a-swiper-dots,
+swiper[class*="-dot"] .uni-swiper-dots {
+	display: flex;
+	align-items: center;
+	width: 100%;
+	justify-content: center;
+}
+
+swiper.square-dot .wx-swiper-dot,
+swiper.square-dot .a-swiper-dot,
+swiper.square-dot .uni-swiper-dot {
+	background-color: #ffffff;
+	opacity: 0.4;
+	width: 10upx;
+	height: 10upx;
+	border-radius: 20upx;
+	margin: 0 8upx !important;
+}
+
+swiper.square-dot .wx-swiper-dot.wx-swiper-dot-active,
+swiper.square-dot .a-swiper-dot.a-swiper-dot-active,
+swiper.square-dot .uni-swiper-dot.uni-swiper-dot-active {
+	opacity: 1;
+	width: 30upx;
+}
+
+swiper.round-dot .wx-swiper-dot,
+swiper.round-dot .a-swiper-dot,
+swiper.round-dot .uni-swiper-dot {
+	width: 10upx;
+	height: 10upx;
+	position: relative;
+	margin: 4upx 8upx !important;
+}
+
+swiper.round-dot .wx-swiper-dot.wx-swiper-dot-active::after,
+swiper.round-dot .a-swiper-dot.a-swiper-dot-active::after,
+swiper.round-dot .uni-swiper-dot.uni-swiper-dot-active::after {
+	content: "";
+	position: absolute;
+	width: 10upx;
+	height: 10upx;
+	top: 0upx;
+	left: 0upx;
+	right: 0;
+	bottom: 0;
+	margin: auto;
+	background-color: #ffffff;
+	border-radius: 20upx;
+}
+
+swiper.round-dot .wx-swiper-dot.wx-swiper-dot-active,
+swiper.round-dot .a-swiper-dot.a-swiper-dot-active,
+swiper.round-dot .uni-swiper-dot.uni-swiper-dot-active {
+	width: 18upx;
+	height: 18upx;
+}
+
+.screen-swiper {
+	min-height: 375upx;
+}
+
+.screen-swiper image,
+.screen-swiper video,
+.swiper-item image,
+.swiper-item video {
+	width: 100%;
+	display: block;
+	height: 100%;
+	margin: 0;
+	pointer-events: none;
+}
+
+.card-swiper {
+	height: 420upx !important;
+}
+
+.card-swiper swiper-item {
+	width: 610upx !important;
+	left: 70upx;
+	box-sizing: border-box;
+	padding: 40upx 0upx 70upx;
+	overflow: initial;
+}
+
+.card-swiper swiper-item .swiper-item {
+	width: 100%;
+	display: block;
+	height: 100%;
+	border-radius: 10upx;
+	transform: scale(0.9);
+	transition: all 0.2s ease-in 0s;
+	overflow: hidden;
+}
+
+.card-swiper swiper-item.cur .swiper-item {
+	transform: none;
+	transition: all 0.2s ease-in 0s;
+}
+
+
+.tower-swiper {
+	height: 420upx;
+	position: relative;
+	max-width: 750upx;
+	overflow: hidden;
+}
+
+.tower-swiper .tower-item {
+	position: absolute;
+	width: 300upx;
+	height: 380upx;
+	top: 0;
+	bottom: 0;
+	left: 50%;
+	margin: auto;
+	transition: all 0.2s ease-in 0s;
+	opacity: 1;
+}
+
+.tower-swiper .tower-item.none {
+	opacity: 0;
+}
+
+.tower-swiper .tower-item .swiper-item {
+	width: 100%;
+	height: 100%;
+	border-radius: 6upx;
+	overflow: hidden;
+}
+
+/* ==================
+          步骤条
+ ==================== */
+
+.cu-steps {
+	display: flex;
+}
+
+scroll-view.cu-steps {
+	display: block;
+	white-space: nowrap;
+}
+
+scroll-view.cu-steps .cu-item {
+	display: inline-block;
+}
+
+.cu-steps .cu-item {
+	flex: 1;
+	text-align: center;
+	position: relative;
+	min-width: 100upx;
+}
+
+.cu-steps .cu-item:not([class*="text-"]) {
+	color: #8799a3;
+}
+
+.cu-steps .cu-item [class*="cuIcon-"],
+.cu-steps .cu-item .num {
+	display: block;
+	font-size: 40upx;
+	line-height: 80upx;
+}
+
+.cu-steps .cu-item::before,
+.cu-steps .cu-item::after,
+.cu-steps.steps-arrow .cu-item::before,
+.cu-steps.steps-arrow .cu-item::after {
+	content: "";
+	display: block;
+	position: absolute;
+	height: 0px;
+	width: calc(100% - 80upx);
+	border-bottom: 1px solid #ccc;
+	left: calc(0px - (100% - 80upx) / 2);
+	top: 40upx;
+	z-index: 0;
+}
+
+.cu-steps.steps-arrow .cu-item::before,
+.cu-steps.steps-arrow .cu-item::after {
+	content: "\e6a3";
+	font-family: 'cuIcon';
+	height: 30upx;
+	border-bottom-width: 0px;
+	line-height: 30upx;
+	top: 0;
+	bottom: 0;
+	margin: auto;
+	color: #ccc;
+}
+
+.cu-steps.steps-bottom .cu-item::before,
+.cu-steps.steps-bottom .cu-item::after {
+	bottom: 40upx;
+	top: initial;
+}
+
+.cu-steps .cu-item::after {
+	border-bottom: 1px solid currentColor;
+	width: 0px;
+	transition: all 0.3s ease-in-out 0s;
+}
+
+.cu-steps .cu-item[class*="text-"]::after {
+	width: calc(100% - 80upx);
+	color: currentColor;
+}
+
+.cu-steps .cu-item:first-child::before,
+.cu-steps .cu-item:first-child::after {
+	display: none;
+}
+
+.cu-steps .cu-item .num {
+	width: 40upx;
+	height: 40upx;
+	border-radius: 50%;
+	line-height: 40upx;
+	margin: 20upx auto;
+	font-size: 24upx;
+	border: 1px solid currentColor;
+	position: relative;
+	overflow: hidden;
+}
+
+.cu-steps .cu-item[class*="text-"] .num {
+	background-color: currentColor;
+}
+
+.cu-steps .cu-item .num::before,
+.cu-steps .cu-item .num::after {
+	content: attr(data-index);
+	position: absolute;
+	left: 0;
+	right: 0;
+	top: 0;
+	bottom: 0;
+	margin: auto;
+	transition: all 0.3s ease-in-out 0s;
+	transform: translateY(0upx);
+}
+
+.cu-steps .cu-item[class*="text-"] .num::before {
+	transform: translateY(-40upx);
+	color: #ffffff;
+}
+
+.cu-steps .cu-item .num::after {
+	transform: translateY(40upx);
+	color: #ffffff;
+	transition: all 0.3s ease-in-out 0s;
+}
+
+.cu-steps .cu-item[class*="text-"] .num::after {
+	content: "\e645";
+	font-family: 'cuIcon';
+	color: #ffffff;
+	transform: translateY(0upx);
+}
+
+.cu-steps .cu-item[class*="text-"] .num.err::after {
+	content: "\e646";
+}
+
+/* ==================
+          布局
+ ==================== */
+
+/*  -- flex弹性布局 -- */
+
+.flex {
+	display: flex;
+}
+
+.basis-xs {
+	flex-basis: 20%;
+}
+
+.basis-sm {
+	flex-basis: 40%;
+}
+
+.basis-df {
+	flex-basis: 50%;
+}
+
+.basis-lg {
+	flex-basis: 60%;
+}
+
+.basis-xl {
+	flex-basis: 80%;
+}
+
+.flex-sub {
+	flex: 1;
+}
+
+.flex-twice {
+	flex: 2;
+}
+
+.flex-treble {
+	flex: 3;
+}
+
+.flex-direction {
+	flex-direction: column;
+}
+
+.flex-wrap {
+	flex-wrap: wrap;
+}
+
+.align-start {
+	align-items: flex-start;
+}
+
+.align-end {
+	align-items: flex-end;
+}
+
+.align-center {
+	align-items: center;
+}
+
+.align-stretch {
+	align-items: stretch;
+}
+
+.self-start {
+	align-self: flex-start;
+}
+
+.self-center {
+	align-self: flex-center;
+}
+
+.self-end {
+	align-self: flex-end;
+}
+
+.self-stretch {
+	align-self: stretch;
+}
+
+.align-stretch {
+	align-items: stretch;
+}
+
+.justify-start {
+	justify-content: flex-start;
+}
+
+.justify-end {
+	justify-content: flex-end;
+}
+
+.justify-center {
+	justify-content: center;
+}
+
+.justify-between {
+	justify-content: space-between;
+}
+
+.justify-around {
+	justify-content: space-around;
+}
+
+/* grid布局 */
+
+.grid {
+	display: flex;
+	flex-wrap: wrap;
+}
+
+.grid.grid-square {
+	overflow: hidden;
+}
+
+.grid.grid-square .cu-tag {
+	position: absolute;
+	right: 0;
+	top: 0;
+	border-bottom-left-radius: 6upx;
+	padding: 6upx 12upx;
+	height: auto;
+	background-color: rgba(0, 0, 0, 0.5);
+}
+
+.grid.grid-square>view>text[class*="cuIcon-"] {
+	font-size: 52upx;
+	position: absolute;
+	color: #8799a3;
+	margin: auto;
+	top: 0;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	flex-direction: column;
+}
+
+.grid.grid-square>view {
+	margin-right: 20upx;
+	margin-bottom: 20upx;
+	border-radius: 6upx;
+	position: relative;
+	overflow: hidden;
+}
+.grid.grid-square>view.bg-img image {
+	width: 100%;
+	height: 100%;
+	position: absolute;
+}
+.grid.col-1.grid-square>view {
+	padding-bottom: 100%;
+	height: 0;
+	margin-right: 0;
+}
+
+.grid.col-2.grid-square>view {
+	padding-bottom: calc((100% - 20upx)/2);
+	height: 0;
+	width: calc((100% - 20upx)/2);
+}
+
+.grid.col-3.grid-square>view {
+	padding-bottom: calc((100% - 40upx)/3);
+	height: 0;
+	width: calc((100% - 40upx)/3);
+}
+
+.grid.col-4.grid-square>view {
+	padding-bottom: calc((100% - 60upx)/4);
+	height: 0;
+	width: calc((100% - 60upx)/4);
+}
+
+.grid.col-5.grid-square>view {
+	padding-bottom: calc((100% - 80upx)/5);
+	height: 0;
+	width: calc((100% - 80upx)/5);
+}
+
+.grid.col-2.grid-square>view:nth-child(2n),
+.grid.col-3.grid-square>view:nth-child(3n),
+.grid.col-4.grid-square>view:nth-child(4n),
+.grid.col-5.grid-square>view:nth-child(5n) {
+	margin-right: 0;
+}
+
+.grid.col-1>view {
+	width: 100%;
+}
+
+.grid.col-2>view {
+	width: 50%;
+}
+
+.grid.col-3>view {
+	width: 33.33%;
+}
+
+.grid.col-4>view {
+	width: 25%;
+}
+
+.grid.col-5>view {
+	width: 20%;
+}
+
+/*  -- 内外边距 -- */
+
+.margin-0 {
+	margin: 0;
+}
+
+.margin-xs {
+	margin: 10upx;
+}
+
+.margin-sm {
+	margin: 20upx;
+}
+
+.margin {
+	margin: 30upx;
+}
+
+.margin-lg {
+	margin: 40upx;
+}
+
+.margin-xl {
+	margin: 50upx;
+}
+
+.margin-top-xs {
+	margin-top: 10upx;
+}
+
+.margin-top-sm {
+	margin-top: 20upx;
+}
+
+.margin-top {
+	margin-top: 30upx;
+}
+
+.margin-top-lg {
+	margin-top: 40upx;
+}
+
+.margin-top-xl {
+	margin-top: 50upx;
+}
+
+.margin-right-xs {
+	margin-right: 10upx;
+}
+
+.margin-right-sm {
+	margin-right: 20upx;
+}
+
+.margin-right {
+	margin-right: 30upx;
+}
+
+.margin-right-lg {
+	margin-right: 40upx;
+}
+
+.margin-right-xl {
+	margin-right: 50upx;
+}
+
+.margin-bottom-xs {
+	margin-bottom: 10upx;
+}
+
+.margin-bottom-sm {
+	margin-bottom: 20upx;
+}
+
+.margin-bottom {
+	margin-bottom: 30upx;
+}
+
+.margin-bottom-lg {
+	margin-bottom: 40upx;
+}
+
+.margin-bottom-xl {
+	margin-bottom: 50upx;
+}
+
+.margin-left-xs {
+	margin-left: 10upx;
+}
+
+.margin-left-sm {
+	margin-left: 20upx;
+}
+
+.margin-left {
+	margin-left: 30upx;
+}
+
+.margin-left-lg {
+	margin-left: 40upx;
+}
+
+.margin-left-xl {
+	margin-left: 50upx;
+}
+
+.margin-lr-xs {
+	margin-left: 10upx;
+	margin-right: 10upx;
+}
+
+.margin-lr-sm {
+	margin-left: 20upx;
+	margin-right: 20upx;
+}
+
+.margin-lr {
+	margin-left: 30upx;
+	margin-right: 30upx;
+}
+
+.margin-lr-lg {
+	margin-left: 40upx;
+	margin-right: 40upx;
+}
+
+.margin-lr-xl {
+	margin-left: 50upx;
+	margin-right: 50upx;
+}
+
+.margin-tb-xs {
+	margin-top: 10upx;
+	margin-bottom: 10upx;
+}
+
+.margin-tb-sm {
+	margin-top: 20upx;
+	margin-bottom: 20upx;
+}
+
+.margin-tb {
+	margin-top: 30upx;
+	margin-bottom: 30upx;
+}
+
+.margin-tb-lg {
+	margin-top: 40upx;
+	margin-bottom: 40upx;
+}
+
+.margin-tb-xl {
+	margin-top: 50upx;
+	margin-bottom: 50upx;
+}
+
+.padding-0 {
+	padding: 0;
+}
+
+.padding-xs {
+	padding: 10upx;
+}
+
+.padding-sm {
+	padding: 20upx;
+}
+
+.padding {
+	padding: 30upx;
+}
+
+.padding-lg {
+	padding: 40upx;
+}
+
+.padding-xl {
+	padding: 50upx;
+}
+
+.padding-top-xs {
+	padding-top: 10upx;
+}
+
+.padding-top-sm {
+	padding-top: 20upx;
+}
+
+.padding-top {
+	padding-top: 30upx;
+}
+
+.padding-top-lg {
+	padding-top: 40upx;
+}
+
+.padding-top-xl {
+	padding-top: 50upx;
+}
+
+.padding-right-xs {
+	padding-right: 10upx;
+}
+
+.padding-right-sm {
+	padding-right: 20upx;
+}
+
+.padding-right {
+	padding-right: 30upx;
+}
+
+.padding-right-lg {
+	padding-right: 40upx;
+}
+
+.padding-right-xl {
+	padding-right: 50upx;
+}
+
+.padding-bottom-xs {
+	padding-bottom: 10upx;
+}
+
+.padding-bottom-sm {
+	padding-bottom: 20upx;
+}
+
+.padding-bottom {
+	padding-bottom: 30upx;
+}
+
+.padding-bottom-lg {
+	padding-bottom: 40upx;
+}
+
+.padding-bottom-xl {
+	padding-bottom: 50upx;
+}
+
+.padding-left-xs {
+	padding-left: 10upx;
+}
+
+.padding-left-sm {
+	padding-left: 20upx;
+}
+
+.padding-left {
+	padding-left: 30upx;
+}
+
+.padding-left-lg {
+	padding-left: 40upx;
+}
+
+.padding-left-xl {
+	padding-left: 50upx;
+}
+
+.padding-lr-xs {
+	padding-left: 10upx;
+	padding-right: 10upx;
+}
+
+.padding-lr-sm {
+	padding-left: 20upx;
+	padding-right: 20upx;
+}
+
+.padding-lr {
+	padding-left: 30upx;
+	padding-right: 30upx;
+}
+
+.padding-lr-lg {
+	padding-left: 40upx;
+	padding-right: 40upx;
+}
+
+.padding-lr-xl {
+	padding-left: 50upx;
+	padding-right: 50upx;
+}
+
+.padding-tb-xs {
+	padding-top: 10upx;
+	padding-bottom: 10upx;
+}
+
+.padding-tb-sm {
+	padding-top: 20upx;
+	padding-bottom: 20upx;
+}
+
+.padding-tb {
+	padding-top: 30upx;
+	padding-bottom: 30upx;
+}
+
+.padding-tb-lg {
+	padding-top: 40upx;
+	padding-bottom: 40upx;
+}
+
+.padding-tb-xl {
+	padding-top: 50upx;
+	padding-bottom: 50upx;
+}
+
+/* -- 浮动 --  */
+
+.cf::after,
+.cf::before {
+	content: " ";
+	display: table;
+}
+
+.cf::after {
+	clear: both;
+}
+
+.fl {
+	float: left;
+}
+
+.fr {
+	float: right;
+}
+
+/* ==================
+          背景
+ ==================== */
+
+.line-red::after,
+.lines-red::after {
+	border-color: #e54d42;
+}
+
+.line-orange::after,
+.lines-orange::after {
+	border-color: #f37b1d;
+}
+
+.line-yellow::after,
+.lines-yellow::after {
+	border-color: #fbbd08;
+}
+
+.line-olive::after,
+.lines-olive::after {
+	border-color: #8dc63f;
+}
+
+.line-green::after,
+.lines-green::after {
+	border-color: #39b54a;
+}
+
+.line-cyan::after,
+.lines-cyan::after {
+	border-color: #1cbbb4;
+}
+
+.line-blue::after,
+.lines-blue::after {
+	border-color: #0081ff;
+}
+
+.line-purple::after,
+.lines-purple::after {
+	border-color: #6739b6;
+}
+
+.line-mauve::after,
+.lines-mauve::after {
+	border-color: #9c26b0;
+}
+
+.line-pink::after,
+.lines-pink::after {
+	border-color: #e03997;
+}
+
+.line-brown::after,
+.lines-brown::after {
+	border-color: #a5673f;
+}
+
+.line-grey::after,
+.lines-grey::after {
+	border-color: #8799a3;
+}
+
+.line-gray::after,
+.lines-gray::after {
+	border-color: #aaaaaa;
+}
+
+.line-black::after,
+.lines-black::after {
+	border-color: #333333;
+}
+
+.line-white::after,
+.lines-white::after {
+	border-color: #ffffff;
+}
+
+.bg-red {
+	background-color: #e54d42;
+	color: #ffffff;
+}
+
+.bg-orange {
+	background-color: #f37b1d;
+	color: #ffffff;
+}
+
+.bg-yellow {
+	background-color: #fbbd08;
+	color: #333333;
+}
+
+.bg-olive {
+	background-color: #8dc63f;
+	color: #ffffff;
+}
+
+.bg-green {
+	background-color: #39b54a;
+	color: #ffffff;
+}
+
+.bg-cyan {
+	background-color: #1cbbb4;
+	color: #ffffff;
+}
+
+.bg-blue {
+	background-color: #0081ff;
+	color: #ffffff;
+}
+
+.bg-purple {
+	background-color: #6739b6;
+	color: #ffffff;
+}
+
+.bg-mauve {
+	background-color: #9c26b0;
+	color: #ffffff;
+}
+
+.bg-pink {
+	background-color: #e03997;
+	color: #ffffff;
+}
+
+.bg-brown {
+	background-color: #a5673f;
+	color: #ffffff;
+}
+
+.bg-grey {
+	background-color: #8799a3;
+	color: #ffffff;
+}
+
+.bg-gray {
+	background-color: #f0f0f0;
+	color: #333333;
+}
+
+.bg-black {
+	background-color: #333333;
+	color: #ffffff;
+}
+
+.bg-white {
+	background-color: #ffffff;
+	color: #666666;
+}
+
+.bg-shadeTop {
+	background-image: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.01));
+	color: #ffffff;
+}
+
+.bg-shadeBottom {
+	background-image: linear-gradient(rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 1));
+	color: #ffffff;
+}
+
+.bg-red.light {
+	color: #e54d42;
+	background-color: #fadbd9;
+}
+
+.bg-orange.light {
+	color: #f37b1d;
+	background-color: #fde6d2;
+}
+
+.bg-yellow.light {
+	color: #fbbd08;
+	background-color: #fef2ced2;
+}
+
+.bg-olive.light {
+	color: #8dc63f;
+	background-color: #e8f4d9;
+}
+
+.bg-green.light {
+	color: #39b54a;
+	background-color: #d7f0dbff;
+}
+
+.bg-cyan.light {
+	color: #1cbbb4;
+	background-color: #d2f1f0;
+}
+
+.bg-blue.light {
+	color: #0081ff;
+	background-color: #cce6ff;
+}
+
+.bg-purple.light {
+	color: #6739b6;
+	background-color: #e1d7f0;
+}
+
+.bg-mauve.light {
+	color: #9c26b0;
+	background-color: #ebd4ef;
+}
+
+.bg-pink.light {
+	color: #e03997;
+	background-color: #f9d7ea;
+}
+
+.bg-brown.light {
+	color: #a5673f;
+	background-color: #ede1d9;
+}
+
+.bg-grey.light {
+	color: #8799a3;
+	background-color: #e7ebed;
+}
+
+.bg-gradual-red {
+	background-image: linear-gradient(45deg, #f43f3b, #ec008c);
+	color: #ffffff;
+}
+
+.bg-gradual-black {
+	background-image: linear-gradient(45deg, #26272f, #414244);
+	color: #f7ebd2;
+}
+
+.bg-gradual-orange {
+	background-image: linear-gradient(45deg, #ff9700, #ed1c24);
+	color: #ffffff;
+}
+
+.bg-gradual-green {
+	background-image: linear-gradient(45deg, #39b54a, #8dc63f);
+	color: #ffffff;
+}
+
+.bg-gradual-purple {
+	background-image: linear-gradient(45deg, #9000ff, #5e00ff);
+	color: #ffffff;
+}
+
+.bg-gradual-pink {
+	background-image: linear-gradient(45deg, #ec008c, #6739b6);
+	color: #ffffff;
+}
+
+.bg-gradual-blue {
+	background-image: linear-gradient(45deg, #0081ff, #1cbbb4);
+	color: #ffffff;
+}
+
+.shadow[class*="-red"] {
+	box-shadow: 6upx 6upx 8upx rgba(204, 69, 59, 0.2);
+}
+
+.shadow[class*="-orange"] {
+	box-shadow: 6upx 6upx 8upx rgba(217, 109, 26, 0.2);
+}
+
+.shadow[class*="-yellow"] {
+	box-shadow: 6upx 6upx 8upx rgba(224, 170, 7, 0.2);
+}
+
+.shadow[class*="-olive"] {
+	box-shadow: 6upx 6upx 8upx rgba(124, 173, 55, 0.2);
+}
+
+.shadow[class*="-green"] {
+	box-shadow: 6upx 6upx 8upx rgba(48, 156, 63, 0.2);
+}
+
+.shadow[class*="-cyan"] {
+	box-shadow: 6upx 6upx 8upx rgba(28, 187, 180, 0.2);
+}
+
+.shadow[class*="-blue"] {
+	box-shadow: 6upx 6upx 8upx rgba(0, 102, 204, 0.2);
+}
+
+.shadow[class*="-purple"] {
+	box-shadow: 6upx 6upx 8upx rgba(88, 48, 156, 0.2);
+}
+
+.shadow[class*="-mauve"] {
+	box-shadow: 6upx 6upx 8upx rgba(133, 33, 150, 0.2);
+}
+
+.shadow[class*="-pink"] {
+	box-shadow: 6upx 6upx 8upx rgba(199, 50, 134, 0.2);
+}
+
+.shadow[class*="-brown"] {
+	box-shadow: 6upx 6upx 8upx rgba(140, 88, 53, 0.2);
+}
+
+.shadow[class*="-grey"] {
+	box-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2);
+}
+
+.shadow[class*="-gray"] {
+	box-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2);
+}
+
+.shadow[class*="-black"] {
+	box-shadow: 6upx 6upx 8upx rgba(26, 26, 26, 0.2);
+}
+
+.shadow[class*="-white"] {
+	box-shadow: 6upx 6upx 8upx rgba(26, 26, 26, 0.2);
+}
+
+.text-shadow[class*="-red"] {
+	text-shadow: 6upx 6upx 8upx rgba(204, 69, 59, 0.2);
+}
+
+.text-shadow[class*="-orange"] {
+	text-shadow: 6upx 6upx 8upx rgba(217, 109, 26, 0.2);
+}
+
+.text-shadow[class*="-yellow"] {
+	text-shadow: 6upx 6upx 8upx rgba(224, 170, 7, 0.2);
+}
+
+.text-shadow[class*="-olive"] {
+	text-shadow: 6upx 6upx 8upx rgba(124, 173, 55, 0.2);
+}
+
+.text-shadow[class*="-green"] {
+	text-shadow: 6upx 6upx 8upx rgba(48, 156, 63, 0.2);
+}
+
+.text-shadow[class*="-cyan"] {
+	text-shadow: 6upx 6upx 8upx rgba(28, 187, 180, 0.2);
+}
+
+.text-shadow[class*="-blue"] {
+	text-shadow: 6upx 6upx 8upx rgba(0, 102, 204, 0.2);
+}
+
+.text-shadow[class*="-purple"] {
+	text-shadow: 6upx 6upx 8upx rgba(88, 48, 156, 0.2);
+}
+
+.text-shadow[class*="-mauve"] {
+	text-shadow: 6upx 6upx 8upx rgba(133, 33, 150, 0.2);
+}
+
+.text-shadow[class*="-pink"] {
+	text-shadow: 6upx 6upx 8upx rgba(199, 50, 134, 0.2);
+}
+
+.text-shadow[class*="-brown"] {
+	text-shadow: 6upx 6upx 8upx rgba(140, 88, 53, 0.2);
+}
+
+.text-shadow[class*="-grey"] {
+	text-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2);
+}
+
+.text-shadow[class*="-gray"] {
+	text-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2);
+}
+
+.text-shadow[class*="-black"] {
+	text-shadow: 6upx 6upx 8upx rgba(26, 26, 26, 0.2);
+}
+
+.bg-img {
+	background-size: cover;
+	background-position: center;
+	background-repeat: no-repeat;
+}
+
+.bg-mask {
+	background-color: #333333;
+	position: relative;
+}
+
+.bg-mask::after {
+	content: "";
+	border-radius: inherit;
+	width: 100%;
+	height: 100%;
+	display: block;
+	background-color: rgba(0, 0, 0, 0.4);
+	position: absolute;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	top: 0;
+}
+
+.bg-mask view,
+.bg-mask cover-view {
+	z-index: 5;
+	position: relative;
+}
+
+.bg-video {
+	position: relative;
+}
+
+.bg-video video {
+	display: block;
+	height: 100%;
+	width: 100%;
+	-o-object-fit: cover;
+	object-fit: cover;
+	position: absolute;
+	top: 0;
+	z-index: 0;
+	pointer-events: none;
+}
+
+/* ==================
+          文本
+ ==================== */
+
+.text-xs {
+	font-size: 20upx;
+}
+
+.text-sm {
+	font-size: 24upx;
+}
+
+.text-df {
+	font-size: 28upx;
+}
+
+.text-lg {
+	font-size: 32upx;
+}
+
+.text-xl {
+	font-size: 36upx;
+}
+
+.text-xxl {
+	font-size: 44upx;
+}
+
+.text-sl {
+	font-size: 80upx;
+}
+
+.text-xsl {
+	font-size: 120upx;
+}
+
+.text-Abc {
+	text-transform: Capitalize;
+}
+
+.text-ABC {
+	text-transform: Uppercase;
+}
+
+.text-abc {
+	text-transform: Lowercase;
+}
+
+.text-price::before {
+	content: "¥";
+	font-size: 80%;
+	margin-right: 4upx;
+}
+
+.text-cut {
+	text-overflow: ellipsis;
+	white-space: nowrap;
+	overflow: hidden;
+}
+
+.text-bold {
+	font-weight: bold;
+}
+
+.text-center {
+	text-align: center;
+}
+
+.text-content {
+	line-height: 1.6;
+}
+
+.text-left {
+	text-align: left;
+}
+
+.text-right {
+	text-align: right;
+}
+
+.text-red,
+.line-red,
+.lines-red {
+	color: #e54d42;
+}
+
+.text-orange,
+.line-orange,
+.lines-orange {
+	color: #f37b1d;
+}
+
+.text-yellow,
+.line-yellow,
+.lines-yellow {
+	color: #fbbd08;
+}
+
+.text-olive,
+.line-olive,
+.lines-olive {
+	color: #8dc63f;
+}
+
+.text-green,
+.line-green,
+.lines-green {
+	color: #39b54a;
+}
+
+.text-cyan,
+.line-cyan,
+.lines-cyan {
+	color: #1cbbb4;
+}
+
+.text-blue,
+.line-blue,
+.lines-blue {
+	color: #0081ff;
+}
+
+.text-purple,
+.line-purple,
+.lines-purple {
+	color: #6739b6;
+}
+
+.text-mauve,
+.line-mauve,
+.lines-mauve {
+	color: #9c26b0;
+}
+
+.text-pink,
+.line-pink,
+.lines-pink {
+	color: #e03997;
+}
+
+.text-brown,
+.line-brown,
+.lines-brown {
+	color: #a5673f;
+}
+
+.text-grey,
+.line-grey,
+.lines-grey {
+	color: #8799a3;
+}
+
+.text-gray,
+.line-gray,
+.lines-gray {
+	color: #aaaaaa;
+}
+
+.text-black,
+.line-black,
+.lines-black {
+	color: #333333;
+}
+
+.text-white,
+.line-white,
+.lines-white {
+	color: #ffffff;
+}

+ 139 - 0
assets/http/api.js

@@ -0,0 +1,139 @@
+import {http} from './service.js'
+
+
+// 权限管理
+let permissionsUrl={
+	login:'/permissions/authentication/login',
+	menuList: "/permissions/account/getMyPermissionsTreeList",
+	orgList:"/permissions/org/pagingQuery",
+	orgListByName: "/permissions/org/getListByLikeName",
+	getById: "/permissions/org/get/",
+	carList:"/permissions/xq/device/admin/user/pagingQuery",
+	subList:"/permissions/org/getSubListByPid"
+}
+const permissions = {
+	login:(params,header) => http.post(permissionsUrl.login,params,{header: header}),
+	menuList:(params,header) => http.get(permissionsUrl.menuList,{params:params,header: header}),
+	orgList:(params,header) => http.get(permissionsUrl.orgList,{params:params,header: header}),
+	orgListByName:(params,header) => http.get(permissionsUrl.orgListByName,{params:params,header: header}),
+	getById: (params,header) => http.get(permissionsUrl.getById+params,{header: header}),
+	getCarList:(params,header)=>http.get(permissionsUrl.carList,{params:params,header:header}),
+	getSubListByPid:(params)=>http.get(permissionsUrl.subList,{params:params,header:header})
+}
+
+//小区管理
+let residentialUrl={
+	page:"estate/residential/getListByLikeName",
+}
+const residential={
+	page:(params,header) => http.get(residentialUrl.page,{params:params,header: header}),
+}
+
+//楼栋管理
+let buildingUrl={
+	page:'estate/building/showBuildingList',
+	update:'estate/building/updateBuilding',
+	del:'estate/building/deleteBuilding',
+	add:'estate/building/addBuilding'
+}
+const building={
+	page:(params,header) => http.get(buildingUrl.page,{params:params,header: header}),
+	update: (params,header) => http.post(buildingUrl.update,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+	del: (params,header) => http.post(buildingUrl.del,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+	add: (params,header) => http.post(buildingUrl.add,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+}
+
+//单元管理
+let unitUrl={
+	page:"/estate/unit/findListByPosition"
+}
+const unit={
+	page:(params,header) => http.get(unitUrl.page,{params:params,header: header}),
+}
+
+
+//房间管理
+let roomUrl={
+	del:'estate/room/deleteRoom',
+	add:'estate/room/addRoom',
+	page: "/estate/room/showRoomList",
+	pageBycondition: "estate/room/getListByLikeName",
+	update:"estate/room/updateRoom",
+}
+const room = {
+	del: (params,header) => http.post(roomUrl.del,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+	add: (params,header) => http.post(roomUrl.add,params,{header: header}),
+	page:(params,header) => http.get(roomUrl.page,{params:params,header: header}),
+	pageBycondition: (params,header) => http.get(roomUrl.pageBycondition,{params:params,header: header}),
+	update: (params,header) => http.post(roomUrl.update,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+}
+
+//住户管理
+let userUrl={
+	page:'estate/user/showUserList',
+	del:'estate/user/deleteUser',
+	add:'estate/user/addUser',
+	update:'estate/user/updateUser'
+}
+let user={
+	page:(params,header) => http.get(userUrl.page,{params:params,header: header}),
+	del: (params,header) => http.post(userUrl.del,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+	add: (params,header) => http.post(userUrl.add,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+	update: (params,header) => http.post(userUrl.update,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+}
+
+//住户审核
+let userAuthUrl={
+	//审核列表
+	page:'apply/user/showApplyUserList',
+	//认证审核
+	updateCheckState:'apply/user/updateCheckState',
+}
+const userAuth={
+	page: (params,header) => http.post(userAuthUrl.page,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+	updateCheckState: (params,header) => http.post(userAuthUrl.updateCheckState,params,{header: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}),
+}
+
+
+
+//用户反馈
+let userFeedBackUrl={
+	page:'estate/userFeedback/pagingQuery',
+}
+const userFeedBack={
+	page:(params,header) => http.get(userFeedBackUrl.page,{params:params,header: header}),
+}
+
+//物业报修
+let estateRepairUrl={
+	page:'estate/estateRepair/showRepairList',
+	submit:'/estate/estateRepair/addOrEditEstateRepair',//新增修改
+	detail:'/estate/estateRepair/findEstateRepairById/app/'
+}
+const estateRepair={
+	page:(params,header) => http.get(estateRepairUrl.page,{params:params,header: header}),
+	submit: (params,header) => http.post(estateRepairUrl.submit,params,{header: header}),
+	detail:(params,header) => http.get(estateRepairUrl.detail+params),
+}
+
+
+export const api={
+	//权限管理
+	permissions,
+	//小区管理
+	residential,
+	//楼栋管理
+	building,
+	//单元管理
+	unit,
+	//房间管理
+	room,
+	//住户管理
+	user,
+	//住户审核
+	userAuth,
+	//用户反馈
+	userFeedBack,
+	//物业报修
+	estateRepair
+}

+ 48 - 0
assets/http/service.js

@@ -0,0 +1,48 @@
+import Request from 'luch-request'
+// let baseURL='http://139.9.103.171:9091/'
+let baseURL='https://wuye.58fo.com/api/'
+const http = new Request()
+http.setConfig((config) => { /* 设置全局配置 */
+  config.baseURL = baseURL  /* 根域名不同 */
+  config.header = {
+    ...config.header,
+  }
+  return config
+})
+http.interceptors.request.use((config) => { /* 请求之前拦截器。可以使用async await 做异步操作 */
+  config.header = {
+    ...config.header,
+	cookie: "access-token="+uni.getStorageSync("access-token")
+  }
+  return config
+}, (config) => {
+  return Promise.reject(config)
+})
+http.interceptors.response.use(async (response) => { /* 请求之后拦截器。可以使用async await 做异步操作  */
+		if(response.data.code=="401"){
+			uni.showModal({
+				title: "请登录",
+				content: response.data.msg,
+				showCancel: false,
+				success: (res) => {
+					if (res.confirm) {
+						uni.reLaunch({
+							url: "/pages/login/login",
+						})
+					}
+				}
+			})
+			return;
+		}
+      return response.data
+  }, (err) => { // 请求错误做点什么
+      uni.showToast({
+          icon: 'none',
+          position: 'bottom',
+          title: '网络异常'
+      })
+      return err
+  })
+export {
+  http
+}

+ 32 - 0
components/addBtn/addBtn.vue

@@ -0,0 +1,32 @@
+<template>
+	<view>
+		<view class="btn cu-btn cuIcon round bg-base" style="position: ;">
+			<text class="cuIcon-add" style="font-size: 50rpx;"></text>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		components:{
+				
+			},
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.btn{
+		position: fixed;
+		bottom: 20%;
+		right: 20rpx;
+		width: 80rpx;height: 80rpx;text-align: center;line-height: 80rpx;
+	}
+
+</style>

+ 134 - 0
components/feedback/feedback.vue

@@ -0,0 +1,134 @@
+<template>
+	<!-- <view class="">
+		<view class="history">
+			<u-time-line>
+				<u-time-line-item  nodeTop="2" v-for="(item,index) in list" :key="index">
+					<template v-slot:node>
+						<view class="u-node bg-white" >
+							<u-icon name="clock-fill" color="#59a5f0" :size="28"></u-icon>
+						</view>
+					</template>
+					<template v-slot:content>
+						<view>
+							<view >{{item.createDate}}</view>
+							<view style="width: 636rpx;">
+								<view class="content">
+									<view class="flex justify-between">
+										<view class="">
+											<view class="nav-title">
+												<text class="">反馈人:</text>
+												<text class="">{{item.memberName}}</text>
+											</view>
+											<view class="nav-title">
+												<text >反馈类型:</text>
+												<text >功能异常:功能故障或不好用</text>
+											</view>
+											<view class="nav-title">
+												<text >反馈内容:</text>
+												<text>{{item.question}}</text>
+											</view>
+										</view>
+									</view>
+								</view>
+							</view>
+						</view>
+					</template>
+				</u-time-line-item>
+			</u-time-line>
+		</view>
+	</view> -->
+	<view class="">
+		<view @click="goDetail(item)" class="data" v-for="(item, index) in list" :key="index">
+			<view class="item">
+				<view style="padding: 0 30rpx;">
+					<view class="content flex">
+						<view class="padding-left-10">
+							投诉人:{{item.memberName}}
+						</view>
+					</view>
+					<view class="content flex">
+						<view class="padding-left-10">
+							反馈类型:功能异常:功能故障或不好用
+						</view>
+					</view>
+					<view class="content flex">
+						<view class="padding-left-10">
+							反馈时间:{{item.createDate}}
+						</view>
+					</view>
+					<view class="content flex">
+						<view class="padding-left-10">
+							反馈内容:{{item.question}}
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'card',
+	props:{
+		list:{
+			type:Array
+		}
+	},
+	data() {
+		return {
+			
+		};
+	},
+	onLoad() {
+		
+	},
+	methods:{
+		copy(data){
+			uni.setClipboardData({
+				data:data
+			})
+		},
+		goDetail(item){
+			getApp().globalData.dataDetail=item
+			uni.navigateTo({
+				url:"../detail?id="+item.id
+			})
+		}
+	}
+};
+</script>
+
+<style lang="scss">
+// .nav-title {
+// 	font-size: 28upx;
+// 	font-weight: 300;
+// 	padding-top: 20rpx;
+// }
+
+// .history{
+// 	box-sizing: border-box;
+// 	padding: 40rpx 20rpx 20rpx 50rpx;
+// }
+// .content{
+// 	box-shadow: 0 8rpx 10rpx rgba(234, 234, 234, 0.5),8rpx 0rpx 10rpx rgba(234, 234, 234,.5),-8rpx 0rpx 10rpx rgba(234, 234, 234,.5);
+// 	background-color: #FFFFFF;
+// 	padding: 20rpx;
+// 	border-radius: 10rpx;
+// 	margin: 20rpx 0;
+// }
+	
+.data {
+	background-color: #ffffff;
+	margin:10rpx;
+	padding: 10rpx 0;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	font-size: 28rpx;
+	.item {
+		.content {
+			padding: 10rpx 0;
+		}
+	}
+}
+</style>

+ 55 - 0
components/mescroll-body/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
components/mescroll-body/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>

+ 90 - 0
components/mescroll-body/components/mescroll-empty.vue

@@ -0,0 +1,90 @@
+<!--空布局
+
+可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
+import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
+
+-->
+<template>
+	<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
+		<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
+		<view v-if="tip" class="empty-tip">{{ tip }}</view>
+		<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
+	</view>
+</template>
+
+<script>
+// 引入全局配置
+import GlobalOption from './../mescroll-uni-option.js';
+export default {
+	props: {
+		// empty的配置项: 默认为GlobalOption.up.empty
+		option: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	// 使用computed获取配置,用于支持option的动态配置
+	computed: {
+		// 图标
+		icon() {
+			return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
+		},
+		// 文本提示
+		tip() {
+			return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
+		}
+	},
+	methods: {
+		// 点击按钮
+		emptyClick() {
+			this.$emit('emptyclick');
+		}
+	}
+};
+</script>
+
+<style>	
+/* 无任何数据的空布局 */
+.mescroll-empty {
+	box-sizing: border-box;
+	width: 100%;
+	padding: 300rpx 50rpx 0;
+	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>

+ 83 - 0
components/mescroll-body/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
components/mescroll-body/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
components/mescroll-body/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>

+ 19 - 0
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);
+	}
+}

+ 348 - 0
components/mescroll-body/mescroll-body.vue

@@ -0,0 +1,348 @@
+<template>
+	<view 
+	class="mescroll-body mescroll-render-touch" 
+	:class="{'mescorll-sticky': sticky}"
+	:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom,'background-color': isShowEmpty?'#FFFFFF':'#f1f1f1'}" 
+	@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="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				windowHeight: 0, // 可使用窗口的高度
+				windowBottom: 0, // 可使用窗口的底部位置
+				statusBarHeight: 0 // 状态栏高度
+			};
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			},
+			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 '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);
+					}
+				}
+			};
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y为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;
+			}
+		}
+	};
+</script>
+
+<style>
+	@import "./mescroll-body.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 65 - 0
components/mescroll-body/mescroll-mixins.js

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

+ 36 - 0
components/mescroll-body/mescroll-uni-option.js

@@ -0,0 +1,36 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textSuccess: '加载成功', // 加载成功的文本
+		textErr: '加载失败', // 加载失败的文本
+		beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '到底啦', // 没有更多数据的提示文本
+		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: "http://139.9.103.171:1888/miniofile/xlyq/empty/empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+			tip: '数据为空' // 提示
+		}
+	}
+}
+
+export default GlobalOption

+ 36 - 0
components/mescroll-body/mescroll-uni.css

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

+ 799 - 0
components/mescroll-body/mescroll-uni.js

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

+ 424 - 0
components/mescroll-body/mescroll-uni.vue

@@ -0,0 +1,424 @@
+<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" :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>
+			</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="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins:[renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				scrollTop: 0, // 滚动条的位置
+				scrollAnim: false, // 是否开启滚动动画
+				windowTop: 0, // 可使用窗口的顶部位置
+				windowBottom: 0, // 可使用窗口的底部位置
+				windowHeight: 0, // 可使用窗口的高度
+				statusBarHeight: 0 // 状态栏高度
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			// 是否使用fixed定位 (当height有值,则不使用)
+			isFixed(){
+				return !this.height && this.fixed
+			},
+			// mescroll的高度
+			scrollHeight(){
+				if (this.isFixed) {
+					return "auto"
+				} else if(this.height){
+					return this.toPx(this.height) + 'px'
+				}else{
+					return "100%"
+				}
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			fixedTop() {
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+			},
+			padTop() {
+				return !this.isFixed ? this.numTop + 'px' : 0
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom)
+			},
+			fixedBottom() {
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+			},
+			padBottom() {
+				return !this.isFixed ? this.numBottom + 'px' : 0
+			},
+			// 是否为重置下拉的状态
+			isDownReset(){
+				return this.downLoadType===3 || this.downLoadType===4
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 列表是否可滑动
+			scrollable(){
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.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组件时,此行不可删)
+						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组件时,此行不可删)
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+							if(vm.downLoadType===4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll)
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) { // 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+						// 更新容器的高度 (多mescroll的情况)
+						vm.setClientHeight()
+					}
+				}
+			}
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+			
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是scrollview,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+				if(typeof y === 'string'){
+					// 小程序不支持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;
+			}
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		}
+	}
+</script>
+
+<style>
+	@import "./mescroll-uni.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 48 - 0
components/mescroll-body/mixins/mescroll-comp.js

@@ -0,0 +1,48 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollCompMixin = {
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
+	onPageScroll(e) {
+		this.handlePageScroll(e)
+	},
+	onReachBottom() {
+		this.handleReachBottom()
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		this.handlePullDownRefresh()
+	},
+	// mescroll-body写在子子子...组件的情况 (多级)
+	data() {
+		return {
+			mescroll: {
+				onPageScroll: e=>{
+					this.handlePageScroll(e)
+				},
+				onReachBottom: ()=>{
+					this.handleReachBottom()
+				},
+				onPullDownRefresh: ()=>{
+					this.handlePullDownRefresh()
+				}
+			}
+		}
+	},
+	methods:{
+		handlePageScroll(e){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPageScroll(e);
+		},
+		handleReachBottom(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onReachBottom();
+		},
+		handlePullDownRefresh(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPullDownRefresh();
+		}
+	}
+}
+
+export default MescrollCompMixin;

+ 59 - 0
components/mescroll-body/mixins/mescroll-more-item.js

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

+ 74 - 0
components/mescroll-body/mixins/mescroll-more.js

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

+ 109 - 0
components/mescroll-body/wxs/mixins.js

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

+ 92 - 0
components/mescroll-body/wxs/renderjs.js

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

+ 268 - 0
components/mescroll-body/wxs/wxs.wxs

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

+ 126 - 0
components/repair/repair.vue

@@ -0,0 +1,126 @@
+<template>
+	<view class="">
+		<view  @click="goDetail(item)" class="data" v-for="(item, index) in list" :key="index">
+			<view class="top" style="color: #444444;">
+				<view class="left">
+					<view class="title">
+						<text>报修编号:{{item.repairNo}}</text>
+						<text @click.stop="copy(item.repairNo)" class="padding-left-20">复制</text>
+					</view>
+				</view>
+				<view class="right">
+					<text class="text-red" v-if="item.handleStatus==-1">已取消</text>
+					<text class="text-orange" v-if="item.handleStatus==0">待处理</text>
+					<text class="" v-if="item.handleStatus==1">已处理</text>
+				</view>
+			</view>
+			<view class="item">
+				<view style="padding: 0 30rpx;">
+					<view class="content text-bold text-lg">
+						<text style="line-height: 50rpx;">{{item.reportDetail}}</text>
+					</view>
+					<view class="content flex">
+						<u-icon name="map-fill" size="30" color="#dc9c72"></u-icon>
+						<view class="padding-left-10">
+							{{item.residentialName}}  
+							<text class="cuIcon-move" style="padding: 0 4rpx;"></text>
+							{{item.reportPosition}}
+						</view>
+					</view>
+					<view class="content flex">
+						<u-icon name="clock-fill" size="30" color="#fb970b"></u-icon>
+						<view class="padding-left-10">
+							{{item.beginTime}}
+							<text class="cuIcon-move" style="padding: 0 4rpx;"></text>
+							{{item.endTime}}
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="bottom">
+				<!-- 待处理状态 -->
+				<view v-if="item.handleStatus==0" @click.stop="handelRepair(item)"  class="cu-btn  sm round line-base" style="margin: 0 8rpx;padding: 0 20rpx;font-size: 22rpx;">
+					处理工单
+				</view>
+				<view v-else class="cu-btn  sm round line-base" style="margin: 0 8rpx;padding: 0 20rpx;font-size: 22rpx;">
+					查看详情
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'card',
+	props:{
+		list:{
+			type:Array
+		}
+	},
+	data() {
+		return {
+			
+		};
+	},
+	onLoad() {
+		
+	},
+	methods:{
+		copy(data){
+			uni.setClipboardData({
+				data:data
+			})
+		},
+		goDetail(item){
+			uni.navigateTo({
+				url:"/pages/repair/detail?id="+item.id
+			})
+		},
+		handelRepair(item){
+			this.$emit('handelRepair',item)
+		}
+	}
+};
+</script>
+
+<style lang="scss">
+.data {
+	background-color: #ffffff;
+	margin:8rpx;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	padding:0 10rpx;
+	font-size: 28rpx;
+	.top {
+		display: flex;
+		justify-content: space-between;
+		padding: 18rpx 0;
+		font-size: 26rpx;
+		border-bottom: 1rpx dashed #EDEDED;
+		.left {
+			display: flex;
+			align-items: center;
+			.title {
+				margin: 0 10rpx;
+			}
+		}
+		.right{
+			margin-right: 10rpx;
+		}
+	}
+	.item {
+		margin: 4rpx 0 20rpx 0;
+		.content {
+			padding: 8rpx 0;
+		}
+	}
+	.bottom {
+		display: flex;
+		padding-bottom: 15rpx;
+		padding-right: 10rpx;
+		justify-content: flex-end;
+		align-items: center;
+	}
+}
+</style>

+ 87 - 0
main.js

@@ -0,0 +1,87 @@
+import Vue from 'vue'
+import App from './App'
+
+//uview 组件
+import uView from 'uview-ui';
+Vue.use(uView);
+
+import addBtn from '@/components/addBtn/addBtn.vue'
+Vue.component('addBtn',addBtn)
+
+//mescroll
+import MescrollBody from "@/components/mescroll-body/mescroll-body.vue"
+Vue.component('mescroll-body', MescrollBody)
+
+//封装api
+import { api } from 'assets/http/api.js' 
+Vue.prototype.$api = api
+
+//全局变量
+import Global from './utils/global'
+Vue.prototype.$global = Global
+
+//缓存
+import simpleCache from "@/utils/cache.js"
+Vue.prototype.$cache = simpleCache
+
+//校验文件
+import verify from 'utils/verify.js'
+Vue.prototype.$verify = verify
+
+//封装日期时间工具类
+import dateTime from 'utils/dateTime.js'
+Vue.prototype.$dateTime = dateTime
+
+//封装提示工具
+import dialog from 'utils/dialog.js'
+Vue.prototype.$dialog = dialog
+
+//引入工具类
+import util from 'utils/util.js'
+Vue.prototype.$util = util
+
+
+//uview 封装提示框
+Vue.prototype.$showToast=function(title,type,position){
+	this.$refs.uToast.show({
+		title: title,
+		position:position||'top',
+		type: type ||'success'
+	})
+}
+
+/**封装判空函数
+ * @param {Object} value
+ */
+Vue.prototype.$isEmpty=function(value){
+	switch (typeof value) {
+		case 'undefined':
+			return true;
+		case 'string':
+			if(value=='undefined') return true
+			if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
+			break;
+		case 'boolean':
+			if (!value) return true;
+			break;
+		case 'number':
+			if (0 === value || isNaN(value)) return true;
+			break;
+		case 'object':
+			if (null === value || value.length === 0) return true;
+			for (var i in value) {
+				return false;
+			}
+			return true;
+	}
+	return false;
+}
+
+Vue.config.productionTip = false
+
+App.mpType = 'app'
+
+const app = new Vue({
+    ...App
+})
+app.$mount()

+ 78 - 0
manifest.json

@@ -0,0 +1,78 @@
+{
+    "name" : "yinchuan-property-assistant-new",
+    "appid" : "__UNI__362054C",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueCompiler" : "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.READ_CONTACTS\"/>",
+                    "<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.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<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.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wxa05f3aff24dba698",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    }
+}

+ 26 - 0
package-lock.json

@@ -0,0 +1,26 @@
+{
+  "name": "yinchuan-property-assistant-new",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@dcloudio/types": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npm.taobao.org/@dcloudio/types/download/@dcloudio/types-2.0.20.tgz",
+      "integrity": "sha1-KxEpo5i2Wwe32nXZ8zEFbFVVclY="
+    },
+    "luch-request": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npm.taobao.org/luch-request/download/luch-request-3.0.5.tgz",
+      "integrity": "sha1-E/JWYRZrPOfIYfGhHT/GmQvIWyg=",
+      "requires": {
+        "@dcloudio/types": "^2.0.16"
+      }
+    },
+    "uview-ui": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npm.taobao.org/uview-ui/download/uview-ui-1.8.3.tgz",
+      "integrity": "sha1-wmAqa/ez7vYD/WMpWes0UK1KC5s="
+    }
+  }
+}

+ 16 - 0
package.json

@@ -0,0 +1,16 @@
+{
+  "name": "yinchuan-property-assistant-new",
+  "version": "1.0.0",
+  "description": "",
+  "main": "main.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "luch-request": "^3.0.5",
+    "uview-ui": "^1.8.3"
+  }
+}

+ 175 - 0
pages.json

@@ -0,0 +1,175 @@
+{
+	"easycom": {
+		"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
+	 },
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+
+	    {
+            "path" : "pages/login/login",
+            "style" :                                                                                    
+            {
+                "navigationBarTextStyle": "white",
+                "navigationBarTitleText": "物业助手",
+                "navigationBarBackgroundColor": "#7dc7ae",
+                "backgroundColor": "#7dc7ae"
+            }
+            
+        },
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTextStyle": "white",
+				"navigationBarTitleText": "物业助手",
+				"navigationBarBackgroundColor": "#7dc7ae",
+				"backgroundColor": "#ffffff"
+			}
+		}
+		,{
+		    "path" : "pages/mine/mine",
+		    "style" :                                                                                    
+		    {
+		        "navigationBarTextStyle": "white",
+		        "navigationBarTitleText": "",
+		        "navigationBarBackgroundColor": "#7dc7ae",
+		        "backgroundColor": "#7dc7ae",
+		        "enablePullDownRefresh": true
+		    }
+		    
+		}
+        ,{
+            "path" : "pages/residential/residential",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/building/building",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "楼栋管理",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/unit/unit",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/room/room",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "房间管理",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/user/user",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "住户管理",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/user-auth/user-auth",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "住户审核",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/car/car",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+
+        ,{
+            "path" : "pages/room/add",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "添加房间",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/room/detail",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "房间详情",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/user/detail",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "住户详情",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/user/add",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "新增住户",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/user-auth/detail",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "审核详情",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+    ],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "物业助手",
+		"navigationBarBackgroundColor": "#FFFFFF",
+		"backgroundColor": "#FFFFFF"
+	},
+	"tabBar": {
+	    "color":"#a0aabc",
+	    "backgroundColor":"#FFFFFF",
+	    "borderStyle":"white",
+	    "selectedColor":"#7dc7ae",
+	    "list": [
+	   {
+	     "pagePath": "pages/index/index",
+	     "text": "工作台",
+	     "selectedIconPath":"static/tarbar/index-select.png",
+	     "iconPath":"static/tarbar/index.png"
+	   },
+	   {
+	     "pagePath": "pages/mine/mine",
+	     "text": "个人中心",
+	     "selectedIconPath":"static/tarbar/mine-select.png",
+	     "iconPath":"static/tarbar/mine.png"
+	   }
+	    ]
+	}
+}

+ 355 - 0
pages/building/building.vue

@@ -0,0 +1,355 @@
+<template>
+	<view>
+		<!-- 详情 -->
+		<u-popup v-model="popupShow" mode="center" width="650" :closeable="true" border-radius="10">
+			<view style="padding: 60rpx 40rpx 10rpx;">
+				<u-form label-width="150" :model="form">
+					<u-form-item label="楼栋名称" :required="true"><u-input v-model="form.name" /></u-form-item>
+					<u-form-item label="所属小区" :required="true">
+						<u-input type="select" disabled @click="operaType=1;residentialShow=true" v-model="form.residentialName" />
+					</u-form-item>
+					<u-form-item label="负责人" ><u-input v-model="form.personName" /></u-form-item>
+					<u-form-item label="联系电话" ><u-input v-model="form.personTel" /></u-form-item>
+				</u-form>
+				<view @click="submit" class="flex cu-btn bg-blue" style="margin: 60rpx 0 10rpx;padding: 40rpx;">
+					提交
+				</view>
+			</view>
+		</u-popup>
+		
+		<!-- 筛选标签条 -->
+		<view class="cu-bar bg-white flex justify-around" style="z-index: 100;width: 100%;position: fixed;top: -2rpx;">
+			<view class="u-line-1" @click="showDown(index)" v-for="(item,index) in filterMenu" :key="index"  style="width: 24%;text-align: center;height: 100%;">
+				{{data[index]==""? item:data[index]}}
+				<u-icon  name="arrow-down-fill" size="20" style="padding-left: 10rpx;"></u-icon>
+			</view>
+			
+			<view class="u-line-1" @click="filterShow=true"   style="width: 26%;position: relative;text-align: center;height: 100%;">
+				筛选
+				<text class="cuIcon-filter padding-left-10" ></text>
+				<u-badge  :offset="[2, 20]"  size="mini" type="error" :count="filterCount"></u-badge>
+			</view>
+			
+			<view class="u-line-1"   style="text-align: center;height: 100%;">
+				<view @click="popupShow=true" class="cu-btn radius bg-base sm">
+					<text class="cuIcon-add"></text>
+					添加
+				</view>
+			</view>
+		</view>
+		
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view style="margin-top: 110rpx;">
+				<card @delItem="delItem" @edit="edit" :list="list"></card>
+			</view>
+		</mescroll-body>
+		
+		<!-- 地区选择器 -->
+		<u-picker  @confirm="regionConfirm"  mode="region" v-model="regionShow"></u-picker>
+		<!-- 小区 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="residentialShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="keyword" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(residentialList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="residentailConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in residentialList" :key="index">
+					<text>{{item.label}}</text>
+				</view>
+				<u-divider v-if="residentialList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+
+		<!-- 筛选 -->
+		<u-modal :show-cancel-button="true" cancel-text="重置"   @cancel="reset" @confirm="filterConfirm" title="筛选" :mask-close-able="true" v-model="filterShow" >
+			<view class="slot-content" style="margin: 20rpx;">
+				<u-form label-width="150" :model="params" ref="uForm">
+					<u-form-item :border-bottom="false" label="楼栋名"><u-input placeholder="请输入楼栋名" v-model="params.name" /></u-form-item>
+				</u-form>
+			</view>
+		</u-modal>
+		<!-- 通知alert -->
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	import card from "./card.vue"
+	import MescrollMixin from "@/components/mescroll-body/mescroll-mixins.js";
+	let that;
+	export default {
+		components:{
+			card
+		},
+		mixins:[MescrollMixin],
+		data() {
+			return {
+				//新增修改详情弹窗
+				popupShow:false,
+				form:{},
+				operaType:0,
+				
+				//顶部菜单栏
+				filterMenu: ["所属地区","所属小区"],
+				data:['','',''],
+				
+				//地区
+				regionShow:false,
+				areaValue:'',
+				
+				//小区
+				residentialShow:false,
+				residentialList:[],
+				
+				//筛选
+				filterShow:false,
+				filterCount:0,
+				
+				//参数
+				params:{},
+				//小区关键字
+				keyword:'',
+				
+				list: [],
+				downOption: { 
+					use: true,
+					auto: false
+				},
+				upOption: {
+					page: {
+						page: 0,
+						size: 10
+					},
+					noMoreSize: 5, 
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				
+			}
+		},
+		onLoad(){
+			that=this
+			this.getResidentailList({isSelect:true})
+		},
+		watch:{
+			keyword(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.searchList=[]
+					let params = {
+						isSelect: true,
+						regionArea: that.areaValue,
+						name: that.keyword
+					}
+					this.getResidentailList(params)
+				}, 500)
+			}
+			
+		},
+		methods: {
+			edit(item){
+				let {orgId,orgPosition,...form}=item
+				this.form=form
+				this.popupShow=true
+				console.log(this.form);
+			},
+			delItem(item){
+				this.$dialog.showModal('确定要删除此记录吗?').then(res=>{
+					this.$api.building.del({id:item.id}).then(res=>{
+						if (res.data==true) {
+							this.$u.toast('删除成功')
+							this.mescroll.resetUpScroll()
+						}else{
+							this.$u.toast('删除失败')
+						}
+					})
+				})
+			},
+			
+			submit(){
+				if (this.$isEmpty(this.form.name)) {
+					this.$u.toast('请输入楼栋名')
+					return
+				}
+				if (this.$isEmpty(this.form.residentialId) || this.$isEmpty(this.form.residentialName)) {
+					this.$u.toast('请选择所属小区')
+					return
+				}
+				if (!this.$isEmpty(this.form.personTel)) {
+					if (!this.$verify.phone(this.form.personTel)&&!this.$verify.landline(this.form.personTel)) {
+						this.$u.toast('请输入正确的联系方式')
+						return
+					}
+				}
+				console.log(this.form);
+				if (this.$isEmpty(this.form.id)) {
+					console.log('我是新增');
+					this.$api.building.add(this.form).then(res=>{
+						if (res.code==200) {
+							this.popupShow=false
+							this.$u.toast('添加成功')
+							this.mescroll.resetUpScroll()
+						}
+					})
+				}else{
+					let {createAccountId,createDate,floorNumber,roomNumber,status,unitNumber,personNumber,...params}=this.form
+					console.log(params);
+					this.$api.building.update(params).then(res=>{
+						if (res.data==true) {
+							this.popupShow=false
+							this.$u.toast('修改成功')
+							this.mescroll.resetUpScroll()
+						}
+					})
+				}
+			},
+			//获取小区列表
+			getResidentailList(params){
+				let list = [];
+				this.$api.residential.page(params).then(res=>{
+					let list=[]
+					res.list.forEach(item=>{
+						let obj={
+							label:item.name,
+							value:item.id
+						}
+						list.push(obj)
+					})
+					this.residentialList =  list;
+				})
+			},
+			/**
+			 * 获取筛选的条件数
+			 */
+			getFilterCount(){
+				let n=0
+				console.log(this.areaValue);
+				if (!this.$isEmpty(this.areaValue)) {
+					//地区
+					n++
+				}
+				if (!this.$isEmpty(this.params.name)) {
+					//楼栋
+					n++
+				}
+				this.filterCount=n
+			},
+			
+			/**
+			 * 下拉回调
+			 */
+			downCallback(){
+				setTimeout(()=>{
+					this.mescroll.resetUpScroll()
+				},1500)
+			},
+			/**
+			 * 上拉回调
+			 * @param {Object} mescroll
+			 */
+			upCallback(mescroll) {
+				let params=this.params
+				params.current=mescroll.num
+				params.size=mescroll.size
+				this.getFilterCount()
+				try{
+					this.$api.building.page(params).then(res=>{
+						let data=res.data.records
+						let length=data.length
+						let total=res.data.total
+						mescroll.endBySize(length, total);
+						if(mescroll.num == 1) this.list = []; 
+						this.list=this.list.concat(data); 
+					})
+				}catch(e){
+					mescroll.endErr();
+				}
+			},
+			/**
+			 * 重置
+			 */
+			reset(){
+				//查询数据列表的参数
+				this.params={}
+				this.data=['','']
+				//地区
+				this.areaValue=''
+				//小区列表
+				this.residentialList=[]
+				//小区关键字
+				this.keyword=''
+				//数据列表
+				this.list=[],
+				this.getResidentailList()
+				this.mescroll.resetUpScroll()
+			},
+			/**
+			 * 点击选择小区,重新获取楼栋列表
+			 */
+			residentailConfirm(res){
+				if (this.operaType==0) {
+					// 筛选楼栋列表的选择小区
+					this.params.residentialId =res.value
+					this.data[1] = res.label
+					this.residentialShow = false
+					this.mescroll.resetUpScroll()
+				}else if(this.operaType==1){
+					// 添加操作的选择小区
+					this.form.residentialName=res.label
+					this.form.residentialId=res.value
+					this.residentialShow=false
+					this.operaType=0
+				}
+			},
+			/**
+			 * 顶部菜单选择
+			 * @param {Object} index 菜单下标
+			 */
+			showDown(index){
+				if (index==0) {
+					//地区
+					this.regionShow=true
+					return
+				}
+				if(index==1){
+					//小区
+					this.residentialShow = true;
+					return;
+				}
+			},
+			/**
+			 * 确认筛选
+			 */
+			filterConfirm(){
+				this.mescroll.resetUpScroll()
+			},
+			/**
+			 * 地区确认回调
+			 * @param {Object} object
+			 */
+			regionConfirm(object){
+				this.data[0]= object.area.label;
+				this.areaValue = object.area.value;
+				let params = {
+					isSelect: true,
+					regionArea: this.areaValue
+				};
+				this.getFilterCount()
+				this.getResidentailList(params);
+			},
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 130 - 0
pages/building/card.vue

@@ -0,0 +1,130 @@
+<template>
+	<view class="">
+		<view   class="data" v-for="(item, index) in list" :key="index">
+			<view class="top">
+				<view class="left">
+					<view class="title">
+						<u-icon name="fangjian" custom-prefix="custom-icon" size="30" color="#50baca"></u-icon>
+						<text style="padding: 0 10rpx;font-weight: 800;">楼栋:</text>
+						{{item.name}}
+					</view>
+				</view>
+				
+				<view class="right">
+					<text>{{item.residentialName}}</text>
+				</view>
+			</view>
+			<view class="item">
+				<view class="left">
+					<view class="cu-list grid col-3 no-border" >
+						<view class="cu-item" >
+							<text  style="color:  #cb2d5d;font-size: 32rpx;">{{item.unitNumber}}</text>
+							<text class="count ">单元总数</text>
+						</view>
+						
+						<view class="cu-item" >
+							<text style="color:  #cb2d5d;font-size: 32rpx;">{{item.roomNumber}}</text>
+							<text class="count">房间总数</text>
+						</view>
+						
+						<view class="cu-item" >
+							<text style="color:  #cb2d5d;font-size: 32rpx;">{{item.unitNumber}}</text>
+							<text class="count">楼层总数</text>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="bottom">
+				<view @click="edit(item)" class="cuIcon cu-btn round bg-blue light text-bold">
+					<text class="cuIcon-edit"></text>
+				</view>
+				<view @click="delItem(item)" class="cuIcon cu-btn round bg-red light margin-left-20 text-bold">
+					<text class="cuIcon-delete"></text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'card',
+	props:{
+		list:{
+			type:Array,
+			default:()=>{
+				[]
+			}
+		}
+	},
+	data() {
+		return {
+			loading:false
+		};
+	},
+	created() {
+		
+	},
+	methods:{
+		copy(data){
+			uni.setClipboardData({
+				data:data
+			})
+		},
+		edit(item){
+			this.$emit('edit',item)
+		},
+		delItem(item){
+			this.$emit('delItem',item)
+		}
+	}
+};
+</script>
+
+<style lang="scss">
+
+.count{
+	font-weight: 800;
+	padding-top: 10rpx;
+}
+
+
+	
+.data {
+	background-color: #ffffff;
+	margin: 10rpx ;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	padding: 20rpx 10rpx;
+	font-size: 28rpx;
+	.top {
+		display: flex;
+		justify-content: space-between;
+		padding-bottom: 20rpx;
+		border-bottom: 1rpx dashed   #dedede;
+		.left {
+			display: flex;
+			align-items: center;
+			.title {
+				margin: 0 10rpx;
+				font-size: 30rpx;
+			}
+		}
+		.right{
+			margin-right: 10rpx;
+		}
+	}
+	.item {
+		margin: 5rpx 0 10rpx 0;
+		.content {
+			border-bottom: 1rpx dashed #DDDDDD;
+			padding: 10rpx 0;
+		}
+	}
+	.bottom {
+		display: flex;
+		justify-content: flex-end;
+		align-items: center;
+	}
+}
+</style>

+ 22 - 0
pages/car/car.vue

@@ -0,0 +1,22 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 217 - 0
pages/index/index.vue

@@ -0,0 +1,217 @@
+<template>
+	<view class="">
+		<view class="bg-white">
+			<image @error="imgError" style="width: 100%;height: 340rpx;" :src="imgUrl('banner',banner)" ></image>
+		</view>
+		<view style="padding:40rpx 40rpx 10px"  class="  flex bg-white">
+			<u-icon name="xiaoqu" custom-prefix="custom-icon" size="36" color="#7dc7ae"></u-icon>
+			<view style="font-size: 30rpx;" class="flex  justify-center align-center text-bold padding-left-10">
+				<text>社区管理</text>
+			</view>
+		</view>
+		<view style="padding: 30rpx 10rpx 0rpx;" class="cu-list grid col-4 no-border" >
+			<view class="cu-item "  @click="jump(item.title)" v-for="(item,index) in gridList" :key="index">
+				<view class="grid-icon" >
+					<u-icon :name="item.name" custom-prefix="custom-icon" size="80" :color="item.color"></u-icon>
+				</view>
+				<text  style="color: #333333;font-size: 26rpx;padding-top: 6rpx;padding-bottom: 20rpx;" >{{item.title}}</text>
+				<view class="cu-tag  bg-red badge" v-if="item.badge>0">{{item.badge}}</view>
+			</view>
+		</view>	
+		<view style="padding: 20rpx 0  20rpx 40rpx;"  class="  flex bg-white">
+			<u-icon size="40" name="daibanshixiang1" custom-prefix="custom-icon"  color="#7dc7ae"></u-icon>
+			<view style="font-size: 30rpx;" class="flex  justify-center align-center text-bold padding-left-10">
+				<text>物业服务</text>
+			</view>
+		</view>
+		<view class="bg-white " style="padding: 0rpx 0 20rpx 0">
+			<u-tabs  bar-height="8" bar-width="80" active-color="#7dc7ae" :list="list" :is-scroll="false" :current="subsectionCurrent" @change="subsectionChange"></u-tabs>
+		</view>
+		<!-- 住户认证 -->
+		<repair v-if="subsectionCurrent==0" @handelRepair="handelRepair" :list="repairList"></repair>
+		<feedback v-else :list="feedbackList"></feedback>
+		<view class="flex justify-center " style="padding:30rpx 0 ">
+			<view @click="more" class="cu-btn radius sm" style="border: 1rpx solid #7fc6ac;color: #7fc6ac;padding: 10rpx 40rpx;">
+				查看更多
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import repair from "@/components/repair/repair" 
+	import feedback from "@/components/feedback/feedback"
+	export default {
+		components:{
+			repair,feedback
+		},
+		data() {
+			return {
+				banner:'http://139.9.103.171:1888/miniofile/estate/banner.png',
+				isError:false,
+				
+				feedbackList:[],
+				repairList:[],
+				list:[
+					{
+						name:'物业报修'
+					},
+					{
+						name:'用户反馈'
+					},
+				],
+				subsectionCurrent:0,
+				
+				gridList:[
+					{
+						color:'#a1cece',
+						title:"小区管理",
+						badge:0,
+						name:'shequ'
+					},
+					{
+						color:'#3dc159',
+						title:"楼栋管理",
+						badge:0,
+						name:'loudong1'
+					},
+					{
+						color:'#3dc159',
+						title:"单元管理",
+						badge:0,
+						name:'tesecanyin'
+					},
+					{
+						color:'#77b0cb',
+						title:"房间管理",
+						badge:0,
+						name:'fangjianxinxi-'
+					},
+					{
+						color:'#de9964',
+						title:"住户管理",
+						badge:0,
+						name:'zhuhu'
+					},
+					{
+						color:'#d2837d',
+						title:"住户审核",
+						badge:0,
+						name:'zheng'
+					},
+					{
+						color:'#7156bf',
+						title:"车辆管理",
+						badge:0,
+						name:'cheliang1'
+					},
+				]
+			}
+		},
+		onLoad() {
+			
+		},
+		onShow() {
+			this.fetchUserFeedback()
+			this.fetchRepairList()
+		},
+		computed:{
+			imgUrl(){
+				return (id,url)=>{
+					return 	this.$cache.cacheImg(id,url,this.isError)
+				}
+			}
+		},
+		methods: {
+			/**
+			 * 缓存的图片是否出现错误
+			 */
+			imgError(){
+				this.isError=true
+			},
+			/**
+			 * 处理工单
+			 */
+			handelRepair(item){
+				//已完成,待评价
+				item.handleStatus=1
+				item.estimateStatus=0
+				this.$dialog.showModal('确定把该工单置为已处理状态?').then(res=>{
+					this.$api.estateRepair.submit(item).then(res=>{
+						this.$u.toast('操作成功')
+						this.fetchRepairList()
+					})
+				})
+			},
+			more(){
+				if (this.subsectionCurrent==0) {
+					uni.navigateTo({
+						url:'../repair/list'
+					})
+				}else if (this.subsectionCurrent==1) {
+					uni.navigateTo({
+						url:'../feedback/list'
+					})
+				}
+			},
+			fetchUserFeedback(){
+				let params={
+					current:1,
+					size:5
+				}
+				this.$api.userFeedBack.page(params).then(res=>{
+					this.feedbackList=res.data.records
+				})
+			},
+			fetchRepairList(){
+				let params={
+					current:1,
+					size:20
+				}
+				this.$api.estateRepair.page(params).then(res=>{
+					this.repairList=res.data.records
+				})
+			},
+			subsectionChange(index){
+				this.subsectionCurrent=index
+			},
+			jump(title){
+				let url=''
+				if (title=='小区管理') {
+					url='/pages/residential/residential'
+				}else if(title=='楼栋管理'){
+					url='/pages/building/building'
+				}else if (title=='单元管理') {
+					url='/pages/unit/unit'
+				}else if (title=='房间管理') {
+					url='/pages/room/room'
+				}else if (title=='住户管理') {
+					url='/pages/user/user'
+				}else if (title=='住户审核') {
+					url='/pages/user-auth/user-auth'
+				}else if (title=='车辆管理') {
+					url='/pages/car/car'
+				}
+				
+				if (!this.$isEmpty(url)) {
+					uni.navigateTo({
+						url:url,
+					})
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.nav-checked{
+		background-color: #7fc6ac;
+		color: #FFFFFF;
+		font-weight: 800;
+	}
+	.nav-unchecked{
+		background-color: #FFFFFF;
+		color: #7fc6ac;
+	}
+	
+</style>

+ 481 - 0
pages/login/login.vue

@@ -0,0 +1,481 @@
+<template>
+	<view class="container">
+		<view class="bg bg-color-base margin-b20"></view>
+
+		<view class="tab vs-row vs-align-center">
+			<image class="tab-bg" 
+			src="https://6e69-niew6-1302638010.tcb.qcloud.la/denglu/%E7%99%BB%E5%BD%955/banner-icon.png?sign=d63f6b91aed3733b261cc0127d2cf702&t=1604049324"
+			mode=""></image>
+
+			<view class="vs-row vs-align-center">
+				<view class="vs-column vs-align-center margin-r40" @click="cur = 0">
+					<text class="font-50 margin-b20" style="color: #000000;">登录</text>
+				</view>
+			</view>
+		</view>
+
+		<view class="login margin-b80">
+			<view class="input vs-row vs-align-center margin-b40">
+				<image style="color: #88d8bc;width: 42rpx;height: 40rpx;" class="input-icon margin-r20" src="/static/login/a4.png"></image>
+				<input class="vs-flex-item  font-30" v-model="phone" type="text" value=""  placeholder="请输入您的账号"
+				 placeholder-class="input-placeholder" />
+			</view>
+			<view class="input vs-row vs-align-center margin-b40">
+				<image style="width: 40rpx;height: 40rpx;" class="input-icon margin-r20" src="/static/login/psw2.png"
+				 ></image>
+				<input class="vs-flex-item  font-30" v-model="pwd" type="text" password value="" placeholder="请输入您的密码"
+				 placeholder-class="input-placeholder" />
+			</view>
+			<view style="margin-left: 20rpx;">
+				<u-checkbox active-color="#7dc7ae" @change="checkboxChange" v-model="isSave" >记住密码</u-checkbox>
+			</view>
+		</view>
+		<view @click="login" class="button bg-color-base vs-row vs-align-center vs-space-center margin-b20">
+			<text class="color-white font-34">立即登录</text>
+		</view>
+		<u-modal v-model="show" :content="content"></u-modal>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				isSave: false,
+				show: false,
+				content: "",
+				phone: '', //手机号码
+				pwd: '' //密码
+			}
+		},
+		onLoad() {
+			uni.getStorage({
+			    key: 'account',
+			    success: (res)=>{
+			       this.phone = res.data.account;
+				   this.pwd = res.data.pwd;
+				   this.isSave = true;
+			    }
+			});
+		},
+		methods:{
+			checkboxChange(e){
+				this.isSave=e.value
+			},
+			login() {
+				
+				var that = this;
+				if (!that.phone) {
+					that.content="请输入您的账号";
+					that.show = true;
+					return;
+				}
+				
+				if (!that.pwd) {
+					that.content="请输入您的密码";
+					that.show = true;
+					return;
+				}
+				this.$api.permissions.login({
+					loginName: that.phone,
+					loginPwd: that.pwd
+				},{"Content-Type":"application/x-www-form-urlencoded"}).then(res => {
+					if(res.code=="200"){
+						if(this.isSave==true){
+							 uni.setStorageSync('account',{account:that.phone,pwd:that.pwd});					
+						}else{
+							uni.removeStorageSync("account");
+						}
+						 uni.setStorageSync('access-token',res.data);			
+						uni.reLaunch({
+							url:"../index/index"
+						})
+					}else{
+						that.content=res.msg;
+						that.show = true;
+					}
+					console.log(res);
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	page{
+		background-color: #FFFFFF;
+	}
+	.container {
+		position: relative;
+	}
+
+	.bg {
+		position: relative;
+		width: 750rpx;
+		height: 380rpx;
+	}
+
+	.tab {
+		position: absolute;
+		top: 240rpx;
+		left: 20rpx;
+		right: 20rpx;
+		height: 150rpx;
+		padding: 0 50rpx;
+		background-color: #fff;
+		border-top-left-radius: 20rpx;
+		border-top-right-radius: 20rpx;
+
+		&-bg {
+			position: absolute;
+			top: -200rpx;
+			right: -50rpx;
+			width: 440rpx;
+			height: 285rpx;
+		}
+	}
+
+	.line {
+		width: 25rpx;
+		height: 7rpx;
+	}
+
+	.login,
+	.register {
+		padding: 0 60rpx;
+	}
+
+	.input {
+		width: 580rpx;
+		height: 90rpx;
+		padding: 0 30rpx;
+		background-color: rgba(80, 100, 235, 0.1);
+		border-radius: 100rpx;
+
+		&-icon {
+			width: 30rpx;
+			height: 38rpx;
+		}
+
+		&-placeholder {
+			color: #909090;
+		}
+	}
+
+	.button {
+		width: 630rpx;
+		height: 90rpx;
+		margin-left: 60rpx;
+		border-radius: 100rpx;
+	}
+
+	.separator {
+		height: 2rpx;
+		margin: 0 30rpx;
+		background-color: #f5f5f5;
+	}
+	// 下边距
+	.margin-b5 {
+		margin-bottom: 5rpx;
+	}
+
+	.margin-b10 {
+		margin-bottom: 10rpx;
+	}
+
+	.margin-b15 {
+		margin-bottom: 15rpx;
+	}
+
+	.margin-b20 {
+		margin-bottom: 20rpx;
+	}
+
+	.margin-b25 {
+		margin-bottom: 25rpx;
+	}
+
+	.margin-b30 {
+		margin-bottom: 30rpx;
+	}
+
+	.margin-b40 {
+		margin-bottom: 40rpx;
+	}
+
+	.margin-b60 {
+		margin-bottom: 60rpx;
+	}
+
+	.margin-b80 {
+		margin-bottom: 80rpx;
+	}
+
+	.margin-b100 {
+		margin-bottom: 100rpx;
+	}
+
+	// 右边距
+	.margin-r5 {
+		margin-right: 5rpx;
+	}
+
+	.margin-r10 {
+		margin-right: 10rpx;
+	}
+
+	.margin-r15 {
+		margin-right: 15rpx;
+	}
+
+	.margin-r20 {
+		margin-right: 20rpx;
+	}
+
+	.margin-r25 {
+		margin-right: 25rpx;
+	}
+
+	.margin-r30 {
+		margin-right: 30rpx;
+	}
+
+	.margin-r40 {
+		margin-right: 40rpx;
+	}
+
+	.margin-r60 {
+		margin-right: 60rpx;
+	}
+
+	// 字体大小
+	.font-18 {
+		font-style: normal;
+		font-size: 18rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-20 {
+		font-style: normal;
+		font-size: 20rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-22 {
+		font-style: normal;
+		font-size: 22rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-24 {
+		font-style: normal;
+		font-size: 24rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-26 {
+		font-style: normal;
+		font-size: 26rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-28 {
+		font-style: normal;
+		font-size: 28rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-30 {
+		font-style: normal;
+		font-size: 30rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-32 {
+		font-style: normal;
+		font-size: 32rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-34 {
+		font-style: normal;
+		font-size: 34rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-36 {
+		font-style: normal;
+		font-size: 36rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-38 {
+		font-style: normal;
+		font-size: 38rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-40 {
+		font-style: normal;
+		font-size: 40rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-46 {
+		font-style: normal;
+		font-size: 46rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-50 {
+		font-style: normal;
+		font-size: 50rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-60 {
+		font-style: normal;
+		font-size: 60rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	.font-80 {
+		font-style: normal;
+		font-size: 80rpx;
+		font-family: Droid Sans Fallback;
+	}
+
+	// 字体对齐
+	.text-left {
+		text-align: left;
+	}
+
+	.text-center {
+		text-align: center;
+	}
+
+	.text-right {
+		text-align: right;
+	}
+
+	// color相关
+	.color-white {
+		color: #FFFFFF;
+	}
+
+	.color-red {
+		color: #dc0000;
+	}
+
+	// 黑色色阶向下
+	.color-black {
+		color: #000;
+	}
+
+	.color-black-3 {
+		color: #333;
+	}
+
+	.color-black-6 {
+		color: #666;
+	}
+
+	.color-black-9 {
+		color: #999;
+	}
+
+	// 字体宽度
+	.font-weight-400 {
+		font-weight: 400;
+	}
+
+	.font-weight-500 {
+		font-weight: bold;
+	}
+
+	// 间隔
+	.spacing-20 {
+		width: 750rpx;
+		height: 20rpx;
+		background-color: #f8f8f8;
+	}
+
+	// 圆角
+	.radius-10 {
+		border-radius: 10rpx;
+	}
+
+	.radius-20 {
+		border-radius: 20rpx;
+	}
+
+	.radius-30 {
+		border-radius: 30rpx;
+	}
+
+	.radius-circle {
+		border-radius: 50%;
+	}
+
+	.radius-height {
+		border-radius: 10000px;
+	}
+
+	// flex相关
+	.vs-flex-item {
+		flex: 1;
+	}
+
+	.vs-space-between {
+		justify-content: space-between;
+	}
+
+	.vs-space-around {
+		justify-content: space-around;
+	}
+
+	.vs-space-center {
+		justify-content: center;
+	}
+
+	.vs-space-end {
+		justify-content: flex-end;
+	}
+
+	.vs-row {
+		flex-direction: row;
+	}
+
+	.vs-column {
+		flex-direction: column;
+	}
+
+	.vs-align-end {
+		align-items: flex-end;
+	}
+
+	.vs-align-center {
+		display: flex;
+		align-items: center;
+	}
+
+	.vs-align-start {
+		align-items: flex-start;
+	}
+
+	.vs-item-hover {
+		background-color: rgba(0, 0, 0, 0.05);
+	}
+
+	.vs-btn-hover {
+		opacity: 0.8;
+	}
+
+	.color-base {
+		color: #7dc7ae;
+	}
+
+	.bg-color-base {
+		background-color: #7dc7ae;
+	}
+</style>

+ 22 - 0
pages/mine/mine.vue

@@ -0,0 +1,22 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 22 - 0
pages/residential/residential.vue

@@ -0,0 +1,22 @@
+<template>
+	<view>
+		
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 310 - 0
pages/room/add.vue

@@ -0,0 +1,310 @@
+<template>
+	<view class="" >
+		<u-toast ref="uToast"/>
+		<view class="bg-white" style="padding: 10rpx 30rpx;margin: 20rpx;border-radius: 12rpx;">
+			<u-form :label-style="labelStyle" label-width="150" :model="form" ref="uForm">
+				<u-form-item label="房间名称" :required="true"><u-input v-model="form.name" placeholder="请输入房间名称"/></u-form-item>
+				<u-form-item v-if="!$isEmpty(form.id)" label="门牌号" :required="true"><u-input v-model="form.houseNumber" placeholder="请输入门牌号"/></u-form-item>
+				<u-form-item label="所属小区" :required="true">
+					<u-input v-if="$isEmpty(form.id)"  type="select" :select-open="residentialShow" v-model="residentialName"  placeholder="请选择小区" @click="residentialShow=true"></u-input>
+					<u-input v-else   disabled v-model="form.residentialName" ></u-input>
+				</u-form-item>
+				<u-form-item label="所属楼栋" :required="true">
+					<!-- 新增 -->
+					<u-input v-if="$isEmpty(form.id)"  type="select" :select-open="buildingShow" v-model="form.buildingName"  placeholder="请选择小区" @click="showBuilding"></u-input>
+					<!-- 修改 -->
+					<u-input v-else   disabled v-model="form.buildingName" ></u-input>
+				</u-form-item>
+				<u-form-item label="所属单元" :required="true">
+					<u-input v-if="$isEmpty(form.id)"  v-model="unitName" type="select" :select-open="unitShow"  placeholder="请选择单元" @click="showUnit"></u-input>
+					<u-input v-else v-model="form.unitName" disabled ></u-input>
+				</u-form-item>
+				<u-form-item label="面积">
+					<u-input v-model="form.area" type="number" placeholder="请填写房间面积"></u-input>
+				</u-form-item>
+				<u-form-item label="户型">
+					<u-input v-model="form.houseType"  placeholder="请填写房间户型"></u-input>
+				</u-form-item>
+				<u-form-item label="楼层">
+					<u-input v-model="form.floorName"  placeholder="请填写房间楼层"></u-input>
+				</u-form-item>
+				<u-form-item label="装修">
+					<u-input v-model="form.ornament"  placeholder="请填写房间装修"></u-input>
+				</u-form-item>
+				<u-form-item label="朝向">
+					<u-input v-model="form.orientation"  placeholder="请填写房间朝向"></u-input>
+				</u-form-item>
+				<u-form-item  label="序号">
+					<u-number-box :min="1" :max="100" solt="right" v-model="form.serial" ></u-number-box>
+				</u-form-item>
+				<u-form-item label="备注" label-position="top">
+					<u-input type="textarea" height="300" maxlength="300" v-model="form.remark"  placeholder="请输入300字以内的备注介绍"></u-input>
+				</u-form-item>
+			</u-form>
+		</view>
+		
+		<!-- 小区 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="residentialShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="residentialKeyword" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(residentialList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="residentialConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in residentialList" :key="index">
+					<text>{{item.name}}</text>
+				</view>
+				<u-divider v-if="residentialList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+		
+		<!-- 楼栋 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="buildingShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="buildingKeyWord" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(buildingList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="buildingConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in buildingList" :key="index">
+					<text>{{item.name}}</text>
+				</view>
+				<u-divider v-if="buildingList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		<u-picker @confirm="unitConfirm" range-key="name" :range="unitList" v-model="unitShow" mode="selector"></u-picker>
+		
+		<view @click="confirm" class="bottom-bar" >
+			<view class="cu-btn bg-blue radius" style="width: 70%;height: 84rpx;">
+				提交
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: '',
+	data() {
+		return {
+			labelStyle:{
+				"fontWeight":"600"
+			},
+			form:{
+				name:'',
+				residentialId:'',
+				unitId:'',
+				houseType:'',
+				floor:'',
+				orientation:'',
+				serial:1,
+				remark:''
+			},
+			//小区
+			residentialName:'',
+			residentialShow:false,
+			residentialKeyword:'',
+			residentialList:[],
+			//楼栋
+			buildingName:'',
+			buildingShow:false,
+			buildingKeyWord:'',
+			buildingList:'',
+			//单元
+			unitName:'',
+			unitList:[],
+			unitShow:false
+		};
+	},
+	onLoad(options) {
+		//加载小区数据
+		this.fetchResidentialList()
+		if (!this.$isEmpty(options.id)) {
+			this.form.id=options.id
+			this.fetchDataDetail()
+			uni.setNavigationBarTitle({
+				title:"修改房间信息"
+			})
+		}
+	},
+	watch:{
+		//小区
+		residentialKeyword(){
+			let that=this
+			if (this.timer){
+				clearTimeout(this.timer)
+			}
+			this.timer = setTimeout(() => {
+				that.residentialList=[]
+				that.getResidentailList()
+			}, 500)
+		},
+		//楼栋
+		buildingKeyWord(){
+			let that=this
+			if (this.timer){
+				clearTimeout(this.timer)
+			}
+			this.timer = setTimeout(() => {
+				that.buildingList=[]
+				let params={
+					residentialId:that.form.residentialId,
+					name:that.buildingKeyWord
+				}
+				that.fetchBuildingList(params)
+			}, 500)
+		},
+	},
+	methods:{
+		fetchDataDetail(){
+			this.$api.room.pageBycondition({id:this.form.id}).then(res=>{
+				this.form=res.list[0]
+			})
+		},
+		
+		//修改房间时,加载房间的数据
+		/**
+		 * 获取小区数据列表
+		 */
+		fetchResidentialList(){
+			let params={
+				isSelect: true,
+				name:this.residentialKeyword
+			}
+			this.$api.residential.page(params).then(res=>{
+				this.residentialList = res.list
+			})
+		},
+		//点击选择小区时
+		residentialConfirm(item){
+			//赋值给提交的数据
+			this.form.residentialId=item.id
+			//赋值给回显的名称
+			this.residentialName=item.name
+			//加载楼栋信息
+			this.buildingList=[]
+			let params={
+				residentialId:item.id
+			}
+			this.fetchBuildingList(params)
+			this.residentialShow=false
+		},
+		showBuilding(){
+			if (this.$isEmpty(this.residentialName)) {
+				this.$showToast('请先选择小区','warning')
+				return
+			}
+			this.buildingShow=true
+		},
+		showUnit(){
+			if (this.$isEmpty(this.buildingName)) {
+				this.$showToast('请先选择楼栋','warning')
+				return
+			}
+			this.unitShow=true
+		},
+		/**
+		 * 获取楼栋列表
+		 * @param {Object} 
+		 */
+		fetchBuildingList(params){
+			this.$api.building.page(params).then(res=>{
+				this.buildingList=res.data.records
+			})
+		},
+		/**
+		 * 选择楼栋
+		 */
+		buildingConfirm(item){
+			// 赋值给提交的数据
+			this.form.buildingId=item.id
+			//数据回显
+			this.buildingName=item.name
+			//加载单元列表
+			this.fetchUnitList()
+			this.buildingShow=false
+		},
+		/**
+		 * 获取单元列表
+		 */
+		fetchUnitList(){
+			let params={
+				buildingId:this.form.buildingId,
+				residentialId:this.form.residentialId
+			}
+			this.$api.unit.page(params).then(res=>{
+				this.unitList = res.list
+			})
+		},
+		/**
+		 * 点击选择单元
+		 * @param {Object} 
+		 */
+		unitConfirm(e){
+			console.log(e);
+			//赋值给提交的数据
+			this.form.unitId=this.unitList[e[0]].id
+			//赋值给回显的名称
+			this.unitName=this.unitList[e[0]].name
+			this.unitShow=false
+		},
+		confirm(){
+			if (this.$isEmpty(this.form.name)) {
+				this.$u.toast('请输入房间名称')
+				return
+			}
+			if (this.$isEmpty(this.form.residentialId)) {
+				this.$u.toast("请选择小区")
+				return
+			}
+			if (this.$isEmpty(this.form.buildingId)) {
+				this.$u.toast("请选择楼栋")
+				return
+			}
+			if (this.$isEmpty(this.form.unitId)) {
+				this.$u.toast('请选择单元')
+				return
+			}
+			console.log(this.form);
+			if (this.$isEmpty(this.form.id)) {
+				return
+				this.$api.room.add(this.form).then(res=>{
+					if (res.data) {
+						this.$dialog.showModal('操作成功',false).then(res=>{
+							uni.navigateBack()
+						})
+					}else{
+						this.$u.toast('操作失败')
+					}
+				})
+				//新增
+			}else{
+				let {oldId,orgPosition,...params}=this.form
+				//修改
+				this.$api.room.update(params).then(res=>{
+					if (res.data==true) {
+						this.$dialog.showModal('操作成功',false).then(res=>{
+							uni.navigateBack()
+						})
+					}else{
+						this.$u.toast('操作失败')
+					}
+				})
+			}
+		}
+		
+
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+	page{
+		background-color: #FFFFFF;
+	}
+</style>

+ 137 - 0
pages/room/card.vue

@@ -0,0 +1,137 @@
+<template>
+	<view class="">
+		<view @click="goDetail(item)"  class="data" v-for="(item, index) in list" :key="index">
+			<view class="top">
+				<view class="left">
+					<view class="title">
+						<u-icon name="fangjian" custom-prefix="custom-icon" size="30" color="#50baca"></u-icon>
+						<text class="padding-left-20">房间名称:{{item.name}}</text>
+						<!-- <text @click.stop="copy(item.name)" style="text-decoration: underline;" class="text-blue padding-left-20">复制</text> -->
+					</view>
+				</view>
+			</view>
+			<view class="item">
+				<view class="left">
+					<view style="padding: 0 30rpx;">
+						<!-- <view class="content flex">
+							<u-icon name="menpaihao" custom-prefix="custom-icon" size="30" color="#fdb524"></u-icon>
+							<text class="text-bold  padding-left-10">门牌号:</text>
+							<text>{{item.houseNumber}}</text>
+						</view> -->
+						<view  class="content flex">
+							<u-icon name="xiaoqu" custom-prefix="custom-icon" size="30" color="#2fc500"></u-icon>
+							<text class="text-bold padding-left-10">所属小区:</text>
+							<text  v-text="item.residentialName?item.residentialName:'未知'"></text>
+						</view>
+						<view  class="content flex">
+							<u-icon name="zhuhuzongshu" custom-prefix="custom-icon" size="30" color="#d4237a"></u-icon>
+							<text class="text-bold padding-left-10">所属楼栋:</text>
+							<text  v-text="item.buildingName" ></text>
+						</view>
+						<view  class="content flex">
+							<u-icon name="danyuan" custom-prefix="custom-icon" size="30" color="#5ca8f0"></u-icon>
+							<text class="text-bold padding-left-10">所属单元:</text>
+							<text  v-text="item.unitName?item.unitName:'未知'" ></text>
+						</view>
+						<!-- <view  class="content flex">
+							<u-icon name="zhuhuzongshu" custom-prefix="custom-icon" size="30" color="#d4237a"></u-icon>
+							<text class="text-bold padding-left-10">住户总数:</text>
+							<text class="text-bold" v-text="item.userNumber" ></text>
+						</view> -->
+					</view>
+				</view>
+			</view>
+			<view class="bottom flex" >
+				<view @click.stop="edit(item)" class="cu-btn  sm round line-blue" style="margin: 0 10rpx;">
+					修改房间
+				</view>
+				<view @click.stop="delItem(item)" class="cu-btn  sm round bg-red" style="margin: 0 10rpx;">
+					删除房间
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'card',
+	props:{
+		list:{
+			type:Array,
+			default:()=>{
+				[]
+			}
+		}
+	},
+	data() {
+		return {
+			loading:false
+		};
+	},
+	created() {
+		
+	},
+	methods:{
+		copy(data){
+			uni.setClipboardData({
+				data:data
+			})
+		},
+		goDetail(item){
+			uni.navigateTo({
+				url:"/pages/room/detail?id="+item.id
+			})
+		},
+		edit(item){
+			uni.navigateTo({
+				url:"/pages/room/add?id="+item.id
+			})
+		},
+		delItem(item){
+			this.$emit('delItem',item)
+		}
+	}
+};
+</script>
+
+<style lang="scss">
+.data {
+	background-color: #ffffff;
+	margin: 10rpx ;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	padding: 20rpx 10rpx;
+	font-size: 28rpx;
+	.top {
+		display: flex;
+		justify-content: space-between;
+		padding-bottom: 20rpx;
+		border-bottom: 1rpx solid #dedede;
+		.left {
+			display: flex;
+			align-items: center;
+			.title {
+				margin: 0 10rpx;
+				font-size: 30rpx;
+			}
+		}
+		.right{
+			margin-right: 10rpx;
+		}
+	}
+	.item {
+		margin: 5rpx 0 20rpx 0;
+		.content {
+			border-bottom: 1rpx dashed #DDDDDD;
+			padding: 30rpx 0;
+		}
+	}
+	.bottom {
+		display: flex;
+		margin-top: 30rpx;
+		justify-content: flex-end;
+		align-items: center;
+	}
+}
+</style>

+ 119 - 0
pages/room/detail.vue

@@ -0,0 +1,119 @@
+<template>
+	<view class="">
+		<view class="card">
+			<scroll-view  style="margin: 40rpx;">
+				<u-form :label-style="labelStyle" label-width="150" :model="form" ref="uForm">
+					<u-form-item label="房间名称"><u-input v-model="form.name" placeholder="暂无信息" disabled/></u-form-item>
+					<u-form-item label="门牌号"><u-input v-model="form.houseNumber" placeholder="暂无信息" disabled/></u-form-item>
+					<u-form-item label="所属小区">
+						<u-input  v-model="form.residentialName"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="所属楼栋">
+						<u-input  v-model="form.buildingName"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="所属单元">
+						<u-input placeholder="暂无信息" v-model="form.unitName" disabled ></u-input>
+					</u-form-item>
+					<u-form-item label="面积">
+						<u-input v-model="form.area"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="户型">
+						<u-input v-model="form.houseType"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="楼层">
+						<u-input v-model="form.floorName"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="装修">
+						<u-input v-model="form.ornament"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="朝向">
+						<u-input v-model="form.orientation"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="序号">
+						<u-input v-model="form.serial"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+				</u-form>
+			</scroll-view>
+		</view>
+		<view class="" style="height: 150rpx;"></view>
+		<view class="footer-fixed">
+			<u-grid :col="3">
+				<u-grid-item @click="add">
+					<u-icon name="plus-circle-fill" color='#19be6b' :size="50"></u-icon>
+					<view class="grid-text">新增</view>
+				</u-grid-item>
+				<u-grid-item @click="edit">
+					<u-icon name="edit-pen-fill" color="#2979ff" :size="50"></u-icon>
+					<view class="grid-text">修改</view>
+				</u-grid-item>
+				<u-grid-item @click="deleteRoom">
+					<u-icon name="trash-fill" color="#fa3534" :size="50"></u-icon>
+					<view class="grid-text">删除</view>
+				</u-grid-item>
+			</u-grid>
+		</view>
+		<navigator url="../index/index" open-type="switchTab"  class="cuIcon cu-btn round" style="background-color: rgba(0,0,0,.2);color: #FFFFFF;position: fixed;right: 3%;bottom: 20%;width: 80rpx;height: 80rpx;">
+			<text class="cuIcon-home" style="font-size: 40rpx;"></text>
+		</navigator>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				labelStyle:{
+					"fontWeight":"600"
+				},
+				form:{}
+			}
+		},
+		onLoad(options) {
+			this.fetchDetail(options.id)
+		},
+		methods: {
+			//获取详情
+			fetchDetail(id){
+				this.$api.room.pageBycondition({id}).then(res=>{
+					this.form=res.list[0]
+				})
+			},
+			//新增
+			add(){
+				uni.navigateTo({
+					url:"add"
+				})
+			},
+			//修改
+			edit(){
+				uni.navigateTo({
+					url:"./add?id="+this.form.id
+				})
+			},
+			//删除
+			deleteRoom(){
+				let item =this.form
+				this.$dialog.showModal('确定要删除此项吗?').then(res=>{
+					this.$api.room.del({id:this.form.id}).then(res=>{
+						if (res.data) {
+							this.$dialog.showModal('删除',false).then(res=>{
+								uni.navigateBack({
+									delta:1
+								})
+							})
+						}
+					})
+				})
+			}
+			
+		}
+	}
+</script>
+
+<style lang="scss">
+	.card{
+		background-color: #FFFFFF;
+		margin: 30rpx;
+		border-radius: 20rpx;
+	}
+</style>

+ 395 - 0
pages/room/room.vue

@@ -0,0 +1,395 @@
+<template>
+	<view>
+		<!-- 筛选标签条 -->
+		<view class="cu-bar bg-white flex" style="z-index: 100;width: 100%;position: fixed;top: -2rpx;">
+			<view class="u-line-1" @click="showDown(index)" v-for="(item,index) in filterMenu" :key="index" style="width: 20%;text-align: center;height: 100%;">
+				<text v-text="data[index]==''?item:data[index]" :class="data[index]==''?'':'text-red'"></text>
+				<u-icon v-if="index!=4" name="arrow-down-fill" size="20" style="padding-left: 10rpx;"></u-icon>
+				<text class="cuIcon-filter padding-left-10" v-else></text>
+				<u-badge :offset="[10, 8]" v-if="index==4" size="mini" type="error" :count="filterCount"></u-badge>
+			</view>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view style="margin-top: 110rpx;">
+				<card @delItem="delItem" :list="list"></card>
+			</view>
+		</mescroll-body>
+		<!-- 小区 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="popupShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="keyword" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(searchList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="residentailConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in searchList" :key="index">
+					<text>{{item.label}}</text>
+				</view>
+				<u-divider v-if="searchList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		<!-- 楼栋 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="buildingShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="buildingKeyWord" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(buildingList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="buildingConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in buildingList" :key="index">
+					<text>{{item.name}}</text>
+				</view>
+				<u-divider v-if="buildingList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+		
+		
+		<!-- 地区选择器 -->
+		<u-picker  @confirm="regionConfirm"  mode="region" v-model="regionShow"></u-picker>
+		
+		<!-- 筛选 -->
+		<u-modal :show-cancel-button="true" cancel-text="重置"   @cancel="reset" @confirm="filterConfirm" title="筛选" :mask-close-able="true" v-model="filterShow" >
+			<view class="slot-content" style="margin: 20rpx;">
+				<u-form label-width="150" :model="params" ref="uForm">
+					<u-form-item label="房间名"><u-input v-model="params.name" /></u-form-item>
+					<!-- <u-form-item :border-bottom="false" label="门牌号"><u-input v-model="params.houseNumber" /></u-form-item> -->
+				</u-form>
+			</view>
+		</u-modal>
+		
+		<!-- 单元选择器 -->
+		<u-picker @confirm="unitConfirm" range-key="name" :range="unitList" v-model="unitShow" mode="selector"></u-picker>
+		
+		<!-- 添加按钮 -->
+		<add-btn @click.native="add"></add-btn>
+		<!-- 通知alert -->
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	import card from "./card.vue"
+	import MescrollMixin from "@/components/mescroll-body/mescroll-mixins.js";
+	let that;
+	export default {
+		components:{
+			card
+		},
+		mixins:[MescrollMixin],
+		data() {
+			return {
+				
+				//顶部菜单栏
+				filterMenu: ["地区","小区","楼栋","单元","筛选"],
+				data:['','','','',''],
+				
+				//地区
+				regionShow:false,
+				areaValue:'',
+				
+				//底部弹出框
+				popupShow:false,
+				searchList:[],
+				//筛选框
+				filterShow:false,
+				filterCount:0,
+				
+				// 列表数据
+				params:{},
+				keyword:'',
+				
+				//单元
+				unitShow:false,
+				unitList:[],
+				
+				//楼栋
+				buildingShow:false,
+				buildingKeyWord:'',
+				buildingList:[],
+				
+				list: [],
+				downOption: { 
+					use: true,
+					auto: false
+				},
+				upOption: {
+					page: {
+						page: 0,
+						size: 10
+					},
+					noMoreSize: 5, 
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				
+			}
+		},
+		onLoad(){
+			that=this
+			this.getResidentailList({isSelect:true})
+		},
+		onShow() {
+			if (this.canReset) {
+				this.mescroll.resetUpScroll()
+			}
+			this.canReset=true
+		},
+		watch:{
+			keyword(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.searchList=[]
+					let params = {
+						isSelect: true,
+						regionArea: that.areaValue,
+						name: that.keyword
+					}
+					this.getResidentailList(params)
+				}, 500)
+			},
+			buildingKeyWord(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.buildingList=[]
+					let params={
+						residentialId:that.params.residentialId,
+						name:that.buildingKeyWord
+					}
+					that.fetchBuildingList(params)
+				}, 500)
+			},
+		},
+		methods: {
+			/**
+			 * 获取筛选的条件数
+			 */
+			getFilterCount(){
+				let n=0
+				if (!this.$isEmpty(this.params.residentialId)) {
+					//小区
+					n++
+				}
+				if (!this.$isEmpty(this.params.buildingId)) {
+					//楼栋
+					n++
+				}
+				if (!this.$isEmpty(this.params.unitId)) {
+					//单元
+					n++
+				}
+				if (!this.$isEmpty(this.params.name)) {
+					//房间名
+					n++
+				}
+				this.filterCount=n
+			},
+			
+			/**
+			 * 下拉回调
+			 */
+			downCallback(){
+				setTimeout(()=>{
+					this.mescroll.resetUpScroll()
+				},1500)
+			},
+			/**
+			 * 上拉回调
+			 * @param {Object} mescroll
+			 */
+			upCallback(mescroll) {
+				let params=this.params
+				params.current=mescroll.num
+				params.size=mescroll.size
+				this.getFilterCount()
+				try{
+					this.$api.room.page(params).then(res=>{
+						let data=res.data.records
+						let length=data.length
+						let total=res.data.total
+						mescroll.endBySize(length, total);
+						if(mescroll.num == 1) this.list = []; 
+						this.list=this.list.concat(data); 
+					})
+				}catch(e){
+					mescroll.endErr();
+				}
+			},
+			/**
+			 * 重置
+			 */
+			reset(){
+				this.params={}
+				this.data=['','','','','']
+				this.areaValue=''
+				this.searchList=[]
+				this.keyword=''
+				this.unitList=[]
+				this.buildingKeyWord='',
+				this.buildingList=[],
+				this.list=[],
+				this.mescroll.resetUpScroll()
+			},
+			/**
+			 * 点击选择小区,获取楼栋列表
+			 */
+			residentailConfirm(res){
+				//筛选标题
+				this.params.residentialId =res.value
+				this.data[1] = res.label
+				this.popupShow = false
+				//获取楼栋列表
+				let params={
+					residentialId:this.params.residentialId
+				}
+				this.fetchBuildingList(params)
+				this.mescroll.resetUpScroll()
+			},
+			//点击选择楼栋,获取单元列表
+			buildingConfirm(res){
+				this.params.buildingId=res.id
+				this.data[2] = res.name
+				this.buildingShow = false
+				let params={
+					residentialId:this.params.residentialId,
+					buildingId:res.id
+				}
+				this.getUnitByResidentialId(params)
+				this.mescroll.resetUpScroll()
+			},
+			/**
+			 * 获取楼栋列表
+			 * @param {Object} 
+			 */
+			fetchBuildingList(params){
+				this.$api.building.page(params).then(res=>{
+					this.buildingList=res.data.records
+				})
+			},
+			/**
+			 * 根据小区id获取单元列表
+			 */	
+			getUnitByResidentialId(params){
+				this.$api.unit.page(params).then(res=>{
+					this.unitList=res.list
+				})
+			},
+			unitConfirm(index){
+				this.data[3]=this.unitList[index].name
+				this.params.unitId=this.unitList[index].id
+				this.mescroll.resetUpScroll()
+			},
+			/**
+			 * 顶部菜单选择
+			 * @param {Object} index 菜单下标
+			 */
+			showDown(index){
+				if (index==0) {
+					//地区
+					this.regionShow=true
+					return
+				}
+				if(index==1){
+					//小区
+					this.popupShow = true;
+				 return;
+				}else if(index==2){
+					//楼栋
+					if (this.$isEmpty(this.data[1])) {
+						this.$refs.uToast.show({
+							title: '请先选择小区',
+							type: 'warning',
+						})
+						return
+					}
+					this.buildingShow=true
+				}else if (index==3) {
+					//单元
+					if(this.$isEmpty(this.data[2])){
+						this.$refs.uToast.show({
+							title: '请先选择楼栋',
+							type: 'warning',
+						})
+						return
+					}
+					this.unitShow=true
+				}else if (index==4) {
+					//筛选
+					this.filterShow=true
+				}
+			},
+			/**
+			 * 确认筛选
+			 */
+			filterConfirm(){
+				this.mescroll.resetUpScroll()
+			},
+			/**
+			 * 地区确认回调
+			 * @param {Object} object
+			 */
+			regionConfirm(object){
+				this.data[0]= object.area.label;
+				this.areaValue = object.area.value;
+				let params = {
+					isSelect: true,
+					regionArea: this.areaValue
+				};
+				this.getResidentailList(params);
+			},
+			/**
+			 * 获取小区列表
+			 * @param {Object} params
+			 */
+			getResidentailList(params){
+				let list = [];
+				this.$api.residential.page(params).then(res=>{
+					let list=[]
+					res.list.forEach(item=>{
+						let obj={
+							label:item.name,
+							value:item.id
+						}
+						list.push(obj)
+					})
+					this.searchList =  list;
+				})
+			},
+			/**
+			 * 添加房间
+			 */
+			add(){
+				uni.navigateTo({
+					url:"./add"
+				})
+			},
+			delItem(item){
+				this.$dialog.showModal('确定要删除此项吗?').then(res=>{
+					this.$api,room.del({id:item.id}).then(res=>{
+						if (res.data) {
+							this.$showToast('删除成功')
+							that.mescroll.resetUpScroll()
+						}
+					})
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 130 - 0
pages/unit/card.vue

@@ -0,0 +1,130 @@
+<template>
+	<view class="">
+		<view  class="data" v-for="(item, index) in list" :key="index">
+			<view class="top">
+				<view class="left">
+					<view class="title">
+						<u-icon name="fangjian" custom-prefix="custom-icon" size="30" color="#50baca"></u-icon>
+						<text style="padding: 0 10rpx;font-weight: 800;">单元名:</text>
+						{{item.name}}
+					</view>
+				</view>
+				
+				<view class="right">
+					<text>{{item.residentialName}}</text>
+				</view>
+			</view>
+			<view class="item">
+				<view class="left">
+					<view class="cu-list grid col-3 no-border" >
+						<view class="cu-item" >
+							<text  style="color:  #cb2d5d;font-size: 32rpx;">{{item.buildingName}}</text>
+							<text class="count ">所属楼栋</text>
+						</view>
+						
+						<view class="cu-item" >
+							<text style="color:  #cb2d5d;font-size: 32rpx;">{{item.roomNumber}}</text>
+							<text class="count">房间总数</text>
+						</view>
+						
+						<view class="cu-item" >
+							<text style="color:  #cb2d5d;font-size: 32rpx;">{{item.userNumber}}</text>
+							<text class="count">住户总数</text>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="bottom">
+				<view @click="edit(item)" class="cuIcon cu-btn round bg-blue light text-bold">
+					<text class="cuIcon-edit"></text>
+				</view>
+				<view @click="delItem(item)" class="cuIcon cu-btn round bg-red light margin-left-20 text-bold">
+					<text class="cuIcon-delete"></text>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'card',
+	props:{
+		list:{
+			type:Array,
+			default:()=>{
+				[]
+			}
+		}
+	},
+	data() {
+		return {
+			loading:false
+		};
+	},
+	created() {
+		
+	},
+	methods:{
+		copy(data){
+			uni.setClipboardData({
+				data:data
+			})
+		},
+		edit(item){
+			this.$emit('edit',item)
+		},
+		delItem(item){
+			this.$emit('delItem',item)
+		}
+	}
+};
+</script>
+
+<style lang="scss">
+
+.count{
+	font-weight: 800;
+	padding-top: 10rpx;
+}
+
+
+	
+.data {
+	background-color: #ffffff;
+	margin: 10rpx ;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	padding: 20rpx 10rpx;
+	font-size: 28rpx;
+	.top {
+		display: flex;
+		justify-content: space-between;
+		padding-bottom: 20rpx;
+		border-bottom: 1rpx dashed   #dedede;
+		.left {
+			display: flex;
+			align-items: center;
+			.title {
+				margin: 0 10rpx;
+				font-size: 30rpx;
+			}
+		}
+		.right{
+			margin-right: 10rpx;
+		}
+	}
+	.item {
+		margin: 5rpx 0 10rpx 0;
+		.content {
+			border-bottom: 1rpx dashed #DDDDDD;
+			padding: 10rpx 0;
+		}
+	}
+	.bottom {
+		display: flex;
+		justify-content: flex-end;
+		align-items: center;
+	}
+}
+</style>

+ 373 - 0
pages/unit/unit.vue

@@ -0,0 +1,373 @@
+<template>
+	<view>
+		<!-- 详情 -->
+		<u-popup v-model="popupShow" mode="center" width="650" :closeable="true" border-radius="10">
+			<view style="padding: 60rpx 40rpx 10rpx;">
+				<u-form label-width="150" :model="form">
+					<u-form-item label="楼栋名称" :required="true"><u-input v-model="form.name" /></u-form-item>
+					<u-form-item label="所属小区" :required="true">
+						<u-input type="select" disabled @click="operaType=1;residentialShow=true" v-model="form.residentialName" />
+					</u-form-item>
+					<u-form-item label="负责人" ><u-input v-model="form.personName" /></u-form-item>
+					<u-form-item label="联系电话" ><u-input v-model="form.personTel" /></u-form-item>
+				</u-form>
+				<view @click="submit" class="flex cu-btn bg-blue" style="margin: 60rpx 0 10rpx;padding: 40rpx;">
+					提交
+				</view>
+			</view>
+		</u-popup>
+		
+		<!-- 筛选标签条 -->
+		<view class="cu-bar bg-white flex justify-around" style="z-index: 100;width: 100%;position: fixed;top: -2rpx;">
+			<view class="u-line-1" @click="showDown(index)" v-for="(item,index) in filterMenu" :key="index"  style="width: 24%;text-align: center;height: 100%;">
+				{{data[index]==""? item:data[index]}}
+				<u-icon  name="arrow-down-fill" size="20" style="padding-left: 10rpx;"></u-icon>
+			</view>
+			
+			<view class="u-line-1" @click="filterShow=true"   style="width: 26%;position: relative;text-align: center;height: 100%;">
+				筛选
+				<text class="cuIcon-filter padding-left-10" ></text>
+				<u-badge  :offset="[2, 20]"  size="mini" type="error" :count="filterCount"></u-badge>
+			</view>
+			
+			<view class="u-line-1"   style="text-align: center;height: 100%;">
+				<view @click="popupShow=true" class="cu-btn radius bg-base sm">
+					<text class="cuIcon-add"></text>
+					添加
+				</view>
+			</view>
+		</view>
+		
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view style="margin-top: 110rpx;">
+				<card @delItem="delItem" @edit="edit" :list="list"></card>
+			</view>
+		</mescroll-body>
+	
+		<!-- 小区 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="residentialShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="keyword" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(residentialList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="residentailConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in residentialList" :key="index">
+					<text>{{item.label}}</text>
+				</view>
+				<u-divider v-if="residentialList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+		<!-- 楼栋 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="buildingShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="buildingKeyWord" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(buildingList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="buildingConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in buildingList" :key="index">
+					<text>{{item.label}}</text>
+				</view>
+				<u-divider v-if="buildingList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+
+		<!-- 筛选 -->
+		<u-modal :show-cancel-button="true" cancel-text="重置"   @cancel="reset" @confirm="filterConfirm" title="筛选" :mask-close-able="true" v-model="filterShow" >
+			<view class="slot-content" style="margin: 20rpx;">
+				<u-form label-width="150" :model="params" ref="uForm">
+					<u-form-item :border-bottom="false" label="单元名"><u-input placeholder="请输入单元名" v-model="params.name" /></u-form-item>
+				</u-form>
+			</view>
+		</u-modal>
+		<!-- 通知alert -->
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	import card from "./card.vue"
+	import MescrollMixin from "@/components/mescroll-body/mescroll-mixins.js";
+	let that;
+	export default {
+		components:{
+			card
+		},
+		mixins:[MescrollMixin],
+		data() {
+			return {
+				//新增修改详情弹窗
+				popupShow:false,
+				form:{},
+				operaType:0,
+				
+				//顶部菜单栏
+				filterMenu: ["所属小区","所属楼栋"],
+				data:['','',''],
+				
+				//小区
+				residentialShow:false,
+				residentialList:[],
+				
+				//楼栋
+				buildingShow:false,
+				buildingList:[],
+				
+				//筛选
+				filterShow:false,
+				filterCount:0,
+				
+				//参数
+				params:{},
+				//小区关键字
+				keyword:'',
+				
+				list: [],
+				downOption: { 
+					use: true,
+					auto: false
+				},
+				upOption: {
+					page: {
+						page: 0,
+						size: 10
+					},
+					noMoreSize: 5, 
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				
+			}
+		},
+		onLoad(){
+			that=this
+			this.getResidentailList({isSelect:true})
+		},
+		watch:{
+			keyword(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.searchList=[]
+					let params = {
+						isSelect: true,
+						name: that.keyword
+					}
+					this.getResidentailList(params)
+				}, 500)
+			},
+			buildingKeyWord(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.buildingList=[]
+					let params={
+						residentialId:that.params.residentialId,
+						name:that.buildingKeyWord
+					}
+					that.fetchBuildingList(params)
+				}, 500)
+			},
+		},
+		methods: {
+			edit(item){
+				let {orgId,orgPosition,...form}=item
+				this.form=form
+				this.popupShow=true
+				console.log(this.form);
+			},
+			delItem(item){
+				this.$dialog.showModal('确定要删除此记录吗?').then(res=>{
+					this.$api.building.del({id:item.id}).then(res=>{
+						if (res.data==true) {
+							this.$u.toast('删除成功')
+							this.mescroll.resetUpScroll()
+						}else{
+							this.$u.toast('删除失败')
+						}
+					})
+				})
+			},
+			
+			submit(){
+				if (this.$isEmpty(this.form.name)) {
+					this.$u.toast('请输入楼栋名')
+					return
+				}
+				if (this.$isEmpty(this.form.residentialId) || this.$isEmpty(this.form.residentialName)) {
+					this.$u.toast('请选择所属小区')
+					return
+				}
+				if (!this.$isEmpty(this.form.personTel)) {
+					if (!this.$verify.phone(this.form.personTel)&&!this.$verify.landline(this.form.personTel)) {
+						this.$u.toast('请输入正确的联系方式')
+						return
+					}
+				}
+				console.log(this.form);
+				if (this.$isEmpty(this.form.id)) {
+					console.log('我是新增');
+					this.$api.building.add(this.form).then(res=>{
+						if (res.code==200) {
+							this.popupShow=false
+							this.$u.toast('添加成功')
+							this.mescroll.resetUpScroll()
+						}
+					})
+				}else{
+					let {createAccountId,createDate,floorNumber,roomNumber,status,unitNumber,personNumber,...params}=this.form
+					console.log(params);
+					this.$api.building.update(params).then(res=>{
+						if (res.data==true) {
+							this.popupShow=false
+							this.$u.toast('修改成功')
+							this.mescroll.resetUpScroll()
+						}
+					})
+				}
+			},
+			//获取小区列表
+			getResidentailList(params){
+				let list = [];
+				this.$api.residential.page(params).then(res=>{
+					let list=[]
+					res.list.forEach(item=>{
+						let obj={
+							label:item.name,
+							value:item.id
+						}
+						list.push(obj)
+					})
+					this.residentialList =  list;
+				})
+			},
+			/**
+			 * 获取筛选的条件数
+			 */
+			getFilterCount(){
+				let n=0
+				if (!this.$isEmpty(this.params.residentialId)) {
+					//小区
+					n++
+				}
+				if (!this.$isEmpty(this.params.buildingName)) {
+					//楼栋
+					n++
+				}
+				if (!this.$isEmpty(this.params.name)) {
+					//单元名
+					n++
+				}
+				this.filterCount=n
+			},
+			
+			/**
+			 * 下拉回调
+			 */
+			downCallback(){
+				setTimeout(()=>{
+					this.mescroll.resetUpScroll()
+				},1500)
+			},
+			/**
+			 * 上拉回调
+			 * @param {Object} mescroll
+			 */
+			upCallback(mescroll) {
+				let params=this.params
+				params.current=mescroll.num
+				params.size=mescroll.size
+				this.getFilterCount()
+				try{
+					this.$api.building.page(params).then(res=>{
+						let data=res.data.records
+						let length=data.length
+						let total=res.data.total
+						mescroll.endBySize(length, total);
+						if(mescroll.num == 1) this.list = []; 
+						this.list=this.list.concat(data); 
+					})
+				}catch(e){
+					mescroll.endErr();
+				}
+			},
+			/**
+			 * 重置
+			 */
+			reset(){
+				//查询数据列表的参数
+				this.params={}
+				this.data=['','']
+				//小区列表
+				this.residentialList=[]
+				//小区关键字
+				this.keyword=''
+				//楼栋列表
+				this.buildingList=[]
+				this.buildingKeyWord=''
+				//数据列表
+				this.list=[],
+				this.getResidentailList()
+				this.mescroll.resetUpScroll()
+			},
+			/**
+			 * 点击选择小区,重新获取楼栋列表
+			 */
+			residentailConfirm(res){
+				if (this.operaType==0) {
+					// 筛选楼栋列表的选择小区
+					this.params.residentialId =res.value
+					this.data[1] = res.label
+					this.residentialShow = false
+					this.mescroll.resetUpScroll()
+				}else if(this.operaType==1){
+					// 添加操作的选择小区
+					this.form.residentialName=res.label
+					this.form.residentialId=res.value
+					this.residentialShow=false
+					this.operaType=0
+				}
+			},
+			/**
+			 * 顶部菜单选择
+			 * @param {Object} index 菜单下标
+			 */
+			showDown(index){
+				if (index==0) {
+					//小区
+					this.residentialShow = true;
+					return
+				}
+				if(index==1){
+					//楼栋
+					this.buildingShow = true;
+					return;
+				}
+			},
+			/**
+			 * 确认筛选
+			 */
+			filterConfirm(){
+				this.mescroll.resetUpScroll()
+			},
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 154 - 0
pages/user-auth/card.vue

@@ -0,0 +1,154 @@
+<!-- 删除,单元,修改,添加,管理功能皆未实现 -->
+<template>
+	<view class="">
+		<view @click="goDetail(item)"  class="data" v-for="(item, index) in list" :key="index">
+			<view class="top">
+				<view class="left">
+					<view class="title">
+						<u-icon name="man-add-fill"  size="34" color="#50baca"></u-icon>
+						<text class="padding-left-10">姓名:{{item.name}}</text>
+					</view>
+				</view>
+				<view class="right">
+					<text class="text-orange" v-if="item.checkState==0">待审核</text>
+					<text class="text-green " v-else-if="item.checkState==1">审核通过</text>
+					<text class="text-red" 	  v-else="item.checkState==2">审核未通过</text>
+				</view>
+			</view>
+			<view class="item">
+				<view class="left">
+					<view style="padding: 0 30rpx;">
+						<view class="content flex">
+							<u-icon v-if="item.sex==1" name="man"   size="30" color="#fdb524"></u-icon>
+							<u-icon v-else name="woman"   size="30" color="#fdb524"></u-icon>
+							<text class="text-bold  padding-left-10">性别:</text>
+							<text v-text="item.sex==1?'男':'女'"></text>
+						</view>
+						<view  class="content flex">
+							<u-icon name="bookmark"  size="30" color="#5ca8f0"></u-icon>
+							<text class="text-bold padding-left-10">住户类型:</text>
+							<text class="" v-if="item.type==0">业主</text>
+							<text class="" v-else-if="item.type==1">成员</text>
+							<text class="" v-else="item.type==2">租客</text>
+						</view>
+						<view  class="content flex align-center">
+							<u-icon name="xiaoqu" custom-prefix="custom-icon" size="30" color="#2fc500"></u-icon>
+							<text style="width: 25%;" class="text-bold padding-left-10">房屋信息:</text>
+							<text style="line-height: 42rpx;">{{item.residentialName}} - {{item.buildingName}} - {{item.unitName}} - {{item.roomName}}</text>
+						</view>
+						<view  class="content flex">
+							<text class="cuIcon-mobile" style="color: #007AFF;font-size: 32rpx;"></text>
+							<text class="text-bold padding-left-10">联系方式:</text>
+							<text class="text-bold" style="padding-top: 6rpx;" v-text="item.tel" ></text>
+							<text @click="call(item.tel)" class="padding-left-10 text-blue" style="text-decoration: underline;">拨打</text>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="bottom flex" >
+				<!-- 待审核 -->
+				<view @click.stop="pass(item.id)" v-if="item.checkState==0" class="cu-btn  sm round line-blue" style="margin: 0 10rpx;">
+					审核通过
+				</view>
+				<view @click.stop="fail(item.id)" v-if="item.checkState==0" class="cu-btn  sm round bg-base" style="margin: 0 10rpx;">
+					审核不通过
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'card',
+	props:{
+		list:{
+			type:Array,
+			default:()=>{
+				[]
+			}
+		}
+	},
+	data() {
+		return {
+			loading:false
+		};
+	},
+	created() {
+		
+	},
+	methods:{
+		call(phone){
+			this.$util.callPhone(phone)
+		},
+		goDetail(item){
+			getApp().globalData.userAuthData=item
+			uni.navigateTo({
+				url:"detail"
+			})
+		},
+		copy(data){
+			uni.setClipboardData({
+				data:data
+			})
+		},
+		pass(id){
+			let params={
+				id:id,
+				checkState:1
+			}
+			this.$emit('pass',params)
+		},
+		fail(id){
+			let params={
+				id:id,
+				checkState:2
+			}
+			this.$emit('fail',params)
+		}
+		
+	}
+};
+</script>
+
+<style lang="scss">
+.data {
+	width: 710rpx;
+	background-color: #ffffff;
+	margin: 20rpx auto;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	padding: 20rpx 10rpx;
+	font-size: 28rpx;
+	.top {
+		display: flex;
+		justify-content: space-between;
+		padding-bottom: 20rpx;
+		border-bottom: 1rpx solid #dedede;
+		.left {
+			display: flex;
+			align-items: center;
+			.title {
+				margin: 0 10rpx;
+				font-size: 30rpx;
+			}
+		}
+		.right{
+			margin-right: 10rpx;
+		}
+	}
+	.item {
+		margin: 5rpx 0 20rpx 0;
+		.content {
+			border-bottom: 1rpx dashed #DDDDDD;
+			padding: 30rpx 0;
+		}
+	}
+	.bottom {
+		display: flex;
+		margin-top: 30rpx;
+		justify-content: flex-end;
+		align-items: center;
+	}
+}
+</style>

+ 92 - 0
pages/user-auth/detail.vue

@@ -0,0 +1,92 @@
+<template>
+	<view class="">
+		<view class="card">
+			<scroll-view  style="margin: 40rpx;">
+				<u-form :label-style="labelStyle" label-width="180" :model="form" ref="uForm">
+					<u-form-item label="姓名"><u-input v-model="form.name" placeholder="暂无信息" disabled/></u-form-item>
+					<u-form-item label="性别">
+						<text v-text="form.sex==1?'男':'女'"></text>
+					</u-form-item>
+					<u-form-item label="联系方式">
+						<u-input v-model="form.tel"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="住户类型">
+						<view class='cu-tag radius bg-blue' >
+							<text class="" v-if="form.type==0">业主</text>
+							<text class="" v-else-if="form.type==1">成员</text>
+							<text class="" v-else="form.type==2">租客</text>
+						</view>
+					</u-form-item>
+					<u-form-item label="房屋信息">
+						<text >{{form.residentialName}}-{{form.buildingName}}-{{form.unitName}}-{{form.roomName}}</text>
+					</u-form-item>
+					<u-form-item label="证件号码">
+						<u-input v-model="form.idCard"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="创建时间">
+						<u-input v-model="form.createDate"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="审核状态">
+						<view class='cu-tag radius bg-orange' v-if="form.checkState==0">
+							<text >待审核</text>
+						</view>
+						<view class='cu-tag radius bg-green' v-if="form.checkState==1">
+							<text >审核通过</text>
+						</view>
+						<view class='cu-tag radius bg-red' v-if="form.checkState==2">
+							<text >审核未通过</text>
+						</view>
+					</u-form-item>
+					<u-form-item label="审核时间">
+						<u-input v-model="form.checkDate"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="审核人员">
+						<u-input v-model="form.operatorName"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item v-if="!$isEmpty(form.imageUri)" label="人脸照片:" label-position="top">
+						<image  @click="previewImg()" style="width: 300rpx;height: 300rpx;" :src="'http://139.9.103.171:8888/'+form.imageUri" ></image>
+					</u-form-item>
+					<u-form-item v-else label="人脸照片" >
+						<text>暂无</text>
+					</u-form-item>
+				</u-form>
+			</scroll-view>
+		</view>
+		<navigator url="../index/index" open-type="switchTab"  class="cuIcon cu-btn round" style="background-color: rgba(0,0,0,.2);color: #FFFFFF;position: fixed;right: 3%;bottom: 20%;width: 70rpx;height: 70rpx;">
+			<text class="cuIcon-home" style="font-size: 40rpx;"></text>
+		</navigator>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				labelStyle:{
+					"fontWeight":"600"
+				},
+				form:{}
+			}
+		},
+		onShow() {
+			this.form=getApp().globalData.userAuthData
+		},
+		methods: {
+			previewImg(){
+				let url=this.$global.imgBaseUrl+this.form.imageUri
+				let imgArr=[url]
+				uni.previewImage({
+					urls:imgArr
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss">
+	.card{
+		background-color: #FFFFFF;
+		margin: 30rpx;
+		border-radius: 20rpx;
+	}
+</style>

+ 430 - 0
pages/user-auth/user-auth.vue

@@ -0,0 +1,430 @@
+<template>
+	<view>
+		<!-- 筛选标签条 -->
+		<view class="cu-bar bg-white flex" style="padding: 0 10rpx;z-index: 100;width: 100%;position: fixed;top: -2rpx;">
+			<view class="u-line-1" @click="showDown(index)" v-for="(item,index) in filterMenu" :key="index" style="width: 25%;text-align: center;height: 100%;">
+				<text v-text="data[index]==''?item:data[index]" :class="data[index]==''?'':'text-red'"></text>
+				<u-icon v-if="index!=4" name="arrow-down-fill" size="20" style="padding-left: 10rpx;"></u-icon>
+				<text class="cuIcon-filter padding-left-10" v-else></text>
+				<u-badge :offset="[10, 8]" v-if="index==4" size="mini" type="error" :count="filterCount"></u-badge>
+			</view>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view style="margin-top: 110rpx;">
+				<card @pass="pass" @fail="fail" @delItem="delItem" :list="list"></card>
+			</view>
+		</mescroll-body>
+		<!-- 小区 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="residentailShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="residentailKeyWord" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(residentailList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="residentailConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in residentailList" :key="index">
+					<text>{{item.label}}</text>
+				</view>
+				<u-divider v-if="residentailList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+		<!-- 楼栋 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="buildingShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="buildingKeyWord" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(buildingList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="buildingConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in buildingList" :key="index">
+					<text>{{item.name}}</text>
+				</view>
+				<u-divider v-if="buildingList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+		<!-- 单元选择器 -->
+		<u-picker @confirm="unitConfirm" range-key="name" :range="unitList" v-model="unitShow" mode="selector"></u-picker>
+		<!-- 人脸状态 -->
+		<u-picker @confirm="checkStatusConfirm" range-key="name" :range="checkStatusList" v-model="checkStatusShow" mode="selector"></u-picker>
+		<!-- 筛选 -->
+		<u-modal :show-cancel-button="true" cancel-text="重置"   @cancel="reset" @confirm="mescroll.resetUpScroll()" title="筛选" :mask-close-able="true" v-model="filterShow" >
+			<view class="slot-content" style="margin: 20rpx;">
+				<u-form label-width="150" :model="params" ref="uForm">
+					<u-form-item label="姓名"><u-input v-model="params.name" /></u-form-item>
+					<u-form-item :border-bottom="false" label="联系方式"><u-input type="number"  v-model="params.tel" /></u-form-item>
+				</u-form>
+			</view>
+		</u-modal>
+		
+		<u-modal   :show-cancel-button="true"  @confirm="submitAudit" title="审核意见" :mask-close-able="true" v-model="auditShow" >
+			<view class="slot-content" style="margin: 20rpx;">
+				<u-form label-width="150"  ref="uForm">
+					<u-form-item :border-bottom="false"><u-input height="150"  placeholder="请输入审核意见" v-model="auditParams.checkOpinion" /></u-form-item>
+				</u-form>
+			</view>
+		</u-modal>
+		
+		<!-- 通知alert -->
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	import card from "./card.vue"
+	import MescrollMixin from "@/components/mescroll-body/mescroll-mixins.js";
+	let that;
+	export default {
+		components:{
+			card
+		},
+		mixins:[MescrollMixin],
+		data() {
+			return {
+				//顶部菜单栏
+				filterMenu: ["小区","楼栋","单元","审核状态","筛选"],
+				data:['','','','',''],
+				
+				//小区
+				residentailShow:false,
+				residentailList:[],
+				residentailKeyWord:'',
+				
+				//楼栋
+				buildingShow:false,
+				buildingList:[],
+				buildingKeyWord:'',
+				
+				//单元
+				unitShow:false,
+				unitList:[],
+				
+				//人脸状态
+				checkStatusShow:false,
+				checkStatusList:[
+					{
+						'name':'待审核',
+						'value':'0'
+					},
+					{
+						'name':'审核通过',
+						'value':'1'
+					},
+					{
+						'name':'审核未通过',
+						'value':'2'
+					},
+					{
+						'name':'待上传',
+						'value':'-1'
+					},
+				],
+				
+				//筛选框
+				filterShow:false,
+				filterCount:0,
+				
+				//审核不通过的意见
+				auditShow:false,
+				auditParams:{},
+				
+				
+				params:{},
+				list: [],
+				downOption: { 
+					use: true,
+					auto: false
+				},
+				upOption: {
+					page: {
+						page: 0,
+						size: 10
+					},
+					noMoreSize: 5, 
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+			}
+		},
+		onLoad(){
+			that=this
+			this.getResidentailList()
+		},
+		onShow() {
+			if (this.canReset) {
+				this.mescroll.resetUpScroll()
+			}
+			this.canReset=true
+		},
+		watch:{
+			residentailKeyWord(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.residentailList=[]
+					this.getResidentailList()
+				}, 500)
+			},
+			buildingKeyWord(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.buildingList=[]
+					that.fetchBuildingList()
+				}, 500)
+			},
+		},
+		methods: {
+			 //审核通过
+			pass(params){
+				this.$dialog.showModal('确定审核通过该住户?').then(res=>{
+					this.$api.userAuth.updateCheckState(params).then(res=>{
+						if (res.code==200) {
+							this.$u.toast('操作成功')
+							this.mescroll.resetUpScroll()
+						}else{
+							this.$u.toast('操作失败')
+						}
+					})
+				})
+			},
+			//审核不通过,显示审核意见框
+			fail(params){
+				this.auditParams=params
+				this.auditShow=true
+			},
+			//执行审核不通过
+			submitAudit(){
+				if (this.$isEmpty(this.auditParams.checkOpinion)) {
+					this.$u.toast('请输入审核意见')
+					return
+				}
+				this.$api.userAuth.updateCheckState(this.auditParams).then(res=>{
+					if (res.code==200) {
+						this.$u.toast('操作成功')
+						this.mescroll.resetUpScroll()
+					}else{
+						this.$u.toast('操作失败')
+					}
+				})
+			},
+			
+	/* 小区 */
+	
+			//获取小区列表
+			getResidentailList(){
+				let params={
+					isSelect:true,
+					name: that.residentailKeyWord,
+				}
+				let list = [];
+				this.$api.residential.page(params).then(res=>{
+					let list=[]
+					res.list.forEach(item=>{
+						let obj={
+							label:item.name,
+							value:item.id
+						}
+						list.push(obj)
+					})
+					this.residentailList = list;
+				})
+			},
+			//点击选择小区,获取楼栋列表
+			residentailConfirm(res){
+				this.params.residentialId =res.value
+				this.data[0] = res.label
+				this.residentailShow = false
+				//获取楼栋列表
+				this.fetchBuildingList()
+				this.mescroll.resetUpScroll()
+			},
+			
+	/* 楼栋 */		
+			
+			//获取楼栋列表
+			fetchBuildingList(){
+				let params={
+					residentialId:this.params.residentialId,
+					name:that.buildingKeyWord
+				}
+				this.$api.building.page(params).then(res=>{
+					this.buildingList=res.data.records
+				})
+			},
+			//点击选择楼栋,获取单元列表
+			buildingConfirm(res){
+				console.log(res);
+				this.params.buildingId=res.id
+				this.data[1] = res.name
+				this.buildingShow = false
+				
+				this.getUnitByResidentialId()
+				this.mescroll.resetUpScroll()
+			},
+			
+			
+	/* 单元 */		
+			
+			
+			//获取单元列表
+			getUnitByResidentialId(){
+				let params={
+					residentialId:this.params.residentialId,
+					buildingId:this.params.buildingId
+				}
+				this.$api.unit.page(params).then(res=>{
+					this.unitList=res.list
+				})
+			},
+			//点击选择单元
+			unitConfirm(index){
+				this.data[2]=this.unitList[index].name
+				this.params.unitId=this.unitList[index].id
+				this.mescroll.resetUpScroll()
+			},
+			
+	/* 审核状态 */
+			checkStatusConfirm(e){
+				this.data[3]=this.checkStatusList[e].name
+				this.params.checkState=this.checkStatusList[e].value
+				this.mescroll.resetUpScroll()
+			},
+			
+	/* 筛选 */
+			//重置
+			reset(){
+				this.params={}
+				this.data=['','','','','']
+				this.residentailList=[]
+				this.buildingList=[]
+				this.unitList=[]
+				this.residentailKeyWord=''
+				this.buildingKeyWord='',
+				this.list=[],
+				this.getResidentailList()
+				this.mescroll.resetUpScroll()
+			},
+			
+			//获取筛选的条件数
+			getFilterCount(){
+				let n=0
+				if (!this.$isEmpty(this.params.residentialId)) {
+					//小区
+					n++
+				}
+				if (!this.$isEmpty(this.params.buildingId)) {
+					//楼栋
+					n++
+				}
+				if (!this.$isEmpty(this.params.unitId)) {
+					//单元
+					n++
+				}
+				if (!this.$isEmpty(this.params.checkState)) {
+					//审核状态
+					n++
+				}
+				if (!this.$isEmpty(this.params.name)) {
+					//姓名
+					n++
+				}
+				if (!this.$isEmpty(this.params.tel)) {
+					//手机号
+					n++
+				}
+				this.filterCount=n
+			},
+			
+			/**
+			 * 下拉回调
+			 */
+			downCallback(){
+				setTimeout(()=>{
+					this.mescroll.resetUpScroll()
+				},1500)
+			},
+			/**
+			 * 上拉回调
+			 * @param {Object} mescroll
+			 */
+			upCallback(mescroll) {
+				let params=this.params
+				params.current=mescroll.num
+				params.size=mescroll.size
+				this.getFilterCount()
+				try{
+					this.$api.userAuth.page(params).then(res=>{
+						let data=res.data.records
+						let length=data.length
+						let total=res.data.total
+						mescroll.endBySize(length, total);
+						if(mescroll.num == 1) this.list = []; 
+						this.list=this.list.concat(data); 
+					})
+				}catch(e){
+					mescroll.endErr();
+				}
+			},
+			
+			/**
+			 * 顶部菜单选择
+			 * @param {Object} index 菜单下标
+			 */
+			showDown(index){
+				if (index==0) {
+					//小区
+					this.residentailShow=true
+				}else if (index==1) {
+					//楼栋
+					if (this.$isEmpty(this.params.residentialId)) {
+						this.$u.toast('请选择小区')
+					}else{
+						this.buildingShow=true
+					}
+					return
+				}else if (index==2) {
+					//单元
+					if (this.$isEmpty(this.params.buildingId)) {
+						this.$u.toast('请选择楼栋')
+					}else{
+						this.unitShow=true
+					}
+					return
+				}else if (index==3) {
+					//审核状态
+					this.checkStatusShow=true
+				}else if (index==4) {
+					//筛选
+					this.filterShow=true
+				}
+			},
+			delItem(item){
+				this.$dialog.showModal('确定要删除此项吗?').then(res=>{
+					this.$api,room.del({id:item.id}).then(res=>{
+						if (res.data) {
+							this.$showToast('删除成功')
+							that.mescroll.resetUpScroll()
+						}
+					})
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 413 - 0
pages/user/add.vue

@@ -0,0 +1,413 @@
+<template>
+	<view >
+		<u-toast ref="uToast"/>
+		<view class="bg-white" style="padding: 10rpx 30rpx;margin: 20rpx;border-radius: 12rpx;">
+			<u-form :label-style="labelStyle"  label-width="180" :model="form" ref="uForm">
+				<u-form-item  label="住户姓名"><u-input v-model="form.name" placeholder="请输入住户姓名"/></u-form-item>
+				<u-form-item  label="性别">
+					<u-input  type="select" :select-open="sexShow" v-model="sexValue"  placeholder="请选择性别" @click="sexShow=true"></u-input>
+				</u-form-item>
+				
+				<u-form-item label="所属小区" :required="true">
+					<u-input v-if="$isEmpty(form.id)"  type="select" :select-open="residentialShow" v-model="residentialName"  placeholder="请选择小区" @click="residentialShow=true"></u-input>
+					<u-input v-else   disabled v-model="form.residentialName" ></u-input>
+				</u-form-item>
+				<u-form-item label="所属楼栋" :required="true">
+					<!-- 新增 -->
+					<u-input v-if="$isEmpty(form.id)"  type="select" :select-open="buildingShow" v-model="form.buildingName"  placeholder="请选择小区" @click="showBuilding"></u-input>
+					<!-- 修改 -->
+					<u-input v-else   disabled v-model="form.buildingName" ></u-input>
+				</u-form-item>
+				<u-form-item label="所属单元" :required="true">
+					<u-input v-if="$isEmpty(form.id)"  v-model="unitName" type="select" :select-open="unitShow"  placeholder="请选择单元" @click="showUnit"></u-input>
+					<u-input v-else v-model="form.unitName" disabled ></u-input>
+				</u-form-item>
+				<u-form-item label="所属房间">
+						<!-- 新增 -->
+					<u-input v-if="$isEmpty(form.id)"  type="select" :select-open="roomShow" v-model="roomName"  placeholder="请选择房间" @click="tapRoomShow"></u-input>
+						<!-- 修改 -->
+					<u-input v-else v-model="form.roomName" disabled ></u-input>
+				</u-form-item>
+				<u-form-item label="住户类型">
+					<u-input  type="select" :select-open="typeShow" v-model="typeName"  placeholder="请选择房间" @click="typeShow=true"></u-input>
+				</u-form-item>
+				<u-form-item label="手机号">
+					<u-input v-model="form.tel"  placeholder="请填写手机号"></u-input>
+				</u-form-item>
+				<u-form-item label="证件类型">
+					<text>身份证</text>
+				</u-form-item>
+				<u-form-item label="证件号码">
+					<u-input v-model="form.idCard"  placeholder="请填写证件号码" type="idcard"></u-input>
+				</u-form-item>
+				<u-form-item label="籍贯">
+					<u-input v-model="form.birthPlace"  placeholder="请填写籍贯"></u-input>
+				</u-form-item>
+				<u-form-item  label="备注" label-position="top">
+					<u-input type="textarea"  placeholder="请填写备注"  v-model="form.remark" />
+				</u-form-item>
+			</u-form>
+		</view>
+		<!-- 性别 -->
+		<u-select @confirm="sexConfirm" v-model="sexShow" :list="sexList"></u-select>
+		
+		<!-- 选择小区 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="searchShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<input v-model="keyword"  :adjust-position="false" type="text" placeholder="请输入搜索关键字" confirm-type="search"></input>
+				</view>
+				<view @click="search" class="action">
+					<button class="cu-btn bg-blue shadow-blur round">搜索</button>
+				</view>
+			</view>
+			<scroll-view @scrolltolower="scrolltolower" style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="searchConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in searchList" :key="index">
+					<text>{{item.name}}</text>
+				</view>
+			</scroll-view>
+		</u-popup>
+		
+		<!-- 选择单元 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="searchShow1">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<input v-model="keyword1"  :adjust-position="false" type="text" placeholder="请输入搜索关键字" confirm-type="search"></input>
+				</view>
+				<view @click="search1" class="action">
+					<button class="cu-btn bg-blue shadow-blur round">搜索</button>
+				</view>
+			</view>
+			<scroll-view @scrolltolower="scrolltolower1" style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="searchConfirm1(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in searchList1" :key="index">
+					<text>{{item.name}}</text>
+				</view>
+			</scroll-view>
+		</u-popup>
+		
+		<!-- 房间-->
+		<u-select @confirm="roomConfirm" v-model="roomShow" :list="roomList"></u-select>
+		
+		<!-- 住户类型 -->
+		<u-select @confirm="typeConfirm" v-model="typeShow" :list="typeList"></u-select>
+		
+		<view @click="confirm" style="display: flex;justify-content: center;">
+			<view class="cu-btn bg-blue radius" style="width: 70%;height: 84rpx;margin: 20rpx 0;">
+				提交
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: '',
+	data() {
+		return {
+			labelStyle:{
+				"fontWeight":"600"
+			},
+			form:{
+				type:-1
+			},
+			// 性别
+			sexValue:'', //回显
+			sexShow:false,
+			sexList:[
+				{
+					value:'1',
+					label:'男'
+				},
+				{
+					value:'2',
+					label:'女'
+				},
+			],
+			//小区
+			residentialName:'',
+			residentialShow:false,
+			residentialKeyword:'',
+			residentialList:[],
+			//楼栋
+			buildingName:'',
+			buildingShow:false,
+			buildingKeyWord:'',
+			buildingList:'',
+			//单元
+			unitName:'',
+			unitList:[],
+			unitShow:false,
+			
+			//房间
+			roomName:'', //回显
+			roomShow:false,
+			roomList:[],
+			
+			//住户类型
+			typeName:'', //回显
+			typeShow:false,
+			typeList:[
+				{
+					value:'0',
+					label:'业主'
+				},
+				{
+					value:'1',
+					label:'成员'
+				},
+				{
+					value:'2',
+					label:'租户'
+				},
+			],
+			
+		};
+	},
+	onLoad(options) {
+		// this.fetchSearchList()
+		console.log(options.id);
+		if (!this.$isEmpty(options.id)) {
+			this.form.id=options.id
+			uni.setNavigationBarTitle({
+				title:"修改住户信息"
+			})
+		}
+	},
+	onShow() {
+		if (!this.$isEmpty(this.form.id)) {
+			this.form=getApp().globalData.userDetail
+			console.log(this.form);
+			if (this.form.sex==1) {
+				this.sexValue="男"
+			}else if(this.form.sex==2) {
+				this.sexValue="女"
+			}
+			if (this.form.type==0) {
+				this.typeName='业主'
+			}else if (this.form.type==1) {
+				this.typeName='住户'
+			}else if (this.form.type==2) {
+				this.typeName='租户'
+			}
+		}
+	},
+	methods:{
+		// 性别
+		sexConfirm(e){
+			this.sexValue=e[0].label
+			this.form.sex=e[0].value
+		},
+		
+		// 小区
+		/**
+		 * 加载小区数据
+		 */
+		fetchSearchList(){
+			let params={
+				name:this.keyword,
+				current:this.searchCurrent
+			}
+			this.$api.residential.showResidentialList(params).then(res=>{
+				this.searchList = [...this.searchList,...res.data.records]
+			})
+		},
+		/**
+		 * 下拉加载数据
+		 */
+		scrolltolower(e){
+			 if(this.searchList.length<this.searchCurrent*10){
+			   return
+			}else{
+				this.searchCurrent++
+				this.fetchSearchList()
+			}
+		},
+		/**
+		 * 点击选择小区
+		 */
+		searchConfirm(item){
+			//赋值给提交的数据
+			this.form.residentialId=item.id
+			//赋值给回显的名称
+			this.residentialName=item.name
+			//加载单元数据
+			this.searchList1=[]
+			this.fetchSearchList1()
+			this.searchShow=false
+		},
+		/**
+		 * 确定搜索小区数据
+		 */
+		search(){
+			this.searchList=[]
+			this.fetchSearchList()
+		},
+		
+		//单元
+		showUnit(){
+			if (this.$isEmpty(this.form.residentialId)) {
+				this.$showToast('请先选择小区','error')
+				return
+			}
+			this.searchShow1=true
+		},
+		scrolltolower1(e){
+			 if(this.searchList1.length<this.searchCurrent1*10){
+			   return
+			}else{
+				this.searchCurrent1++
+				this.fetchSearchList1()
+			}
+		},
+		fetchSearchList1(){
+			let params={
+				name:this.keyword1,
+				current:this.searchCurrent1,
+				residentialId:this.form.residentialId
+			}
+			this.$api.unit.showUnitList(params).then(res=>{
+				this.searchList1 = [...this.searchList1,...res.data.records]
+			})
+		},
+		searchConfirm1(item){
+			//赋值给提交的数据
+			this.form.unitId=item.id
+			//赋值给回显的名称
+			this.unitName=item.name
+			//加载房间的数据
+			this.fetchRoomList()
+			this.searchShow1=false
+		},
+		search1(){
+			this.searchList1=[]
+			this.fetchSearchList1()
+		},
+		
+		//房间
+		fetchRoomList(){
+			let params={
+				residentialId:this.form.residentialId,
+				unitId:this.form.unitId,
+				isSelect:true
+			}
+			this.$api.room.getListByLikeName(params).then(res=>{
+				this.roomList=[]
+				res.list.forEach(item=>{
+					let list={
+						label:item.name,
+						value:item.id
+					}
+					this.roomList.push(list)
+				})
+			})
+		},
+		roomConfirm(e){
+			this.roomName=e[0].label
+			this.form.roomId=e[0].value
+		},
+		tapRoomShow(){
+			if (this.$isEmpty(this.form.unitId)) {
+				this.$showToast('请先选择单元','error')
+				return
+			}
+			this.roomShow=true
+		},
+		//住户类型
+		typeConfirm(e){
+			this.typeName=e[0].label
+			this.form.type=e[0].value
+		},
+		confirm(){
+			if (this.$isEmpty(this.form.name)) {
+				this.$showToast('请输入姓名','error')
+				return
+			}
+			if (this.$isEmpty(this.form.sex)) {
+				this.$showToast('请选择性别',"error")
+				return
+			}
+			if (this.$isEmpty(this.form.residentialId)) {
+				this.$showToast('请选择小区','error')
+				return
+			}
+			if (this.$isEmpty(this.form.unitId)) {
+				this.$showToast('请选择单元','error')
+				return
+			}
+			if (this.$isEmpty(this.form.roomId)) {
+				this.$showToast('请选择房间','error')
+				return
+			}
+			if (this.form.type==-1) {
+				console.log(this.form.type);
+				this.$showToast('请选择住户类型','error')
+				return
+			}
+			if (this.$isEmpty(this.form.tel)) {
+				this.$showToast('请填写手机号码','error')
+				return
+			}
+			if (!this.$verify.phone(this.form.tel)) {
+				this.$showToast('手机格式不正确','error')
+				return
+			}
+			if (!this.$isEmpty(this.form.idCard)) {
+				if (!this.$verify.idCard(this.form.idCard)) {
+					this.$showToast('身份证号码不正确','error')
+					return
+				}
+			}
+			
+			if (this.$isEmpty(this.form.id)) {
+				//添加住户
+				this.$api.user.add(this.form).then(res=>{
+					if (res.data==true) {
+						this.$dialog.showModal('操作成功',false).then(res=>{
+							uni.navigateBack({
+								delta:1
+							})
+						})
+					}else{
+						this.$showToast(res.msg,'error')
+					}
+				})
+			}else{
+				let params={
+					id:this.form.id,
+					name:this.form.name,
+					roomName:this.form.roomName,
+					roomId:this.form.roomId,
+					type:this.form.type,
+					tel:this.form.tel,
+					sex:this.form.sex,
+					idCard:this.form.idCard || '',
+					birthPlace:this.form.birthPlace || '',
+					remark:this.form.remark || ''
+				}
+				//编辑住户信息
+				console.log(params);
+				this.$api.user.update(params).then(res=>{
+					if (res.data==true) {
+						console.log("1111");
+						this.$dialog.showModal('操作成功',false).then(res=>{
+							uni.navigateBack({
+								delta:1
+							})
+						})
+					}else{
+						this.$showToast('操作失败','error')
+					}
+				})
+			}
+			
+			
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 146 - 0
pages/user/card.vue

@@ -0,0 +1,146 @@
+<!-- 删除,单元,修改,添加,管理功能皆未实现 -->
+<template>
+	<view class="">
+		<view @click="goDetail(item)"  class="data" v-for="(item, index) in list" :key="index">
+			<view class="top">
+				<view class="left">
+					<view class="title">
+						<u-icon name="man-add-fill"  size="34" color="#50baca"></u-icon>
+						<text class="padding-left-10">姓名:{{item.name}}</text>
+					</view>
+				</view>
+				<view class="right flex">
+					<text>人脸审核状态:</text>
+					<text class="text-yellow" v-if="item.checkState==-1">待上传</text>
+					<text class="text-orange" v-else-if="item.checkState==0">待审核</text>
+					<text class="text-green " v-else-if="item.checkState==1">审核通过</text>
+					<text class="text-red" 	  v-else="item.checkState==2">审核未通过</text>
+				</view>
+			</view>
+			<view class="item">
+				<view class="left">
+					<view style="padding: 0 30rpx;">
+						<view class="content flex">
+							<u-icon v-if="item.sex==1" name="man"   size="30" color="#fdb524"></u-icon>
+							<u-icon v-else name="woman"   size="30" color="#fdb524"></u-icon>
+							<text   class="text-bold  padding-left-10">性别:</text>
+							<text   v-text="item.sex==1?'男':'女'"></text>
+						</view>
+						<view  class="content flex">
+							<u-icon name="bookmark"  size="30" color="#5ca8f0"></u-icon>
+							<text class="text-bold padding-left-10">住户类型:</text>
+							<text class="" v-if="item.type==0">业主</text>
+							<text class="" v-else-if="item.type==1">成员</text>
+							<text class="" v-else="item.type==2">租客</text>
+						</view>
+						<view  class="content flex">
+							<u-icon name="xiaoqu" custom-prefix="custom-icon" size="30" color="#2fc500"></u-icon>
+							<text class="text-bold padding-left-10">房屋信息:</text>
+							<text >{{item.residentialName}},{{item.buildingName}},{{item.unitName}},{{item.roomName}}</text>
+						</view>
+						<view  class="content flex">
+							<text class="cuIcon-mobile" style="color: #007AFF;font-size: 32rpx;"></text>
+							<text class="text-bold padding-left-10">联系方式:</text>
+							<text class="text-bold" style="padding-top: 6rpx;" v-text="item.tel" ></text>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="bottom flex" >
+				<view @click.stop="edit(item)" class="cu-btn  sm round line-blue" style="margin: 0 10rpx;">
+					修改信息
+				</view>
+				<view @click.stop="deleteItem(item)" class="cu-btn  sm round bg-red" style="margin: 0 10rpx;">
+					删除记录
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	name: 'card',
+	props:{
+		list:{
+			type:Array,
+			default:()=>{
+				[]
+			}
+		}
+	},
+	data() {
+		return {
+			loading:false
+		};
+	},
+	created() {
+		
+	},
+	methods:{
+		copy(data){
+			uni.setClipboardData({
+				data:data
+			})
+		},
+		deleteItem(item){
+			this.$emit('deleteItem',item)
+		},
+		edit(item){
+			getApp().globalData.userDetail=item
+			uni.navigateTo({
+				url:"add?id="+item.id
+			})
+		},
+		goDetail(item){
+			getApp().globalData.userDetail=item
+			uni.navigateTo({
+				url:"./detail?id="+item.id
+			})
+		}
+		
+	}
+};
+</script>
+
+<style lang="scss">
+.data {
+	width: 710rpx;
+	background-color: #ffffff;
+	margin: 20rpx auto;
+	border-radius: 6rpx;
+	box-sizing: border-box;
+	padding: 20rpx 10rpx;
+	font-size: 28rpx;
+	.top {
+		display: flex;
+		justify-content: space-between;
+		padding-bottom: 20rpx;
+		border-bottom: 1rpx solid #dedede;
+		.left {
+			display: flex;
+			align-items: center;
+			.title {
+				margin: 0 10rpx;
+				font-size: 30rpx;
+			}
+		}
+		.right{
+			margin-right: 10rpx;
+		}
+	}
+	.item {
+		margin: 5rpx 0 20rpx 0;
+		.content {
+			border-bottom: 1rpx dashed #DDDDDD;
+			padding: 30rpx 0;
+		}
+	}
+	.bottom {
+		display: flex;
+		margin-top: 30rpx;
+		justify-content: flex-end;
+		align-items: center;
+	}
+}
+</style>

+ 150 - 0
pages/user/detail.vue

@@ -0,0 +1,150 @@
+<template>
+	<view class="">
+		<view class="card">
+			<scroll-view  style="margin: 40rpx;">
+				<u-form :label-style="labelStyle" label-width="220" :model="form" ref="uForm">
+					<u-form-item label="姓名:"><u-input v-model="form.name" placeholder="暂无信息" disabled/></u-form-item>
+					<u-form-item label="性别:"><text v-text="form.sex==1?'男':'女'"></text></u-form-item>
+					<u-form-item label="联系方式:">
+						<u-input v-model="form.tel"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="证件号码:">
+						<u-input v-model="form.idCard"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item label="人脸审核状态:">
+						<view class='cu-tag radius bg-red' v-if="form.checkState==-1">
+							<text>待上传</text>
+						</view>
+						<view class='cu-tag radius bg-orange' v-if="form.checkState==0">
+							<text>待审核</text>
+						</view>
+						<view class='cu-tag radius bg-green' v-if="form.checkState==1">
+							<text>审核通过</text>
+						</view>
+						<view class='cu-tag radius bg-red' v-if="form.checkState==2">
+							<text>审核通过</text>
+						</view>
+					</u-form-item>
+					<u-form-item label="住户类型:">
+						<view class='cu-tag radius bg-blue' >
+							<text class="" v-if="form.type==0">业主</text>
+							<text class="" v-else-if="form.type==1">成员</text>
+							<text class="" v-else-if="form.type==2">租客</text>
+						</view>
+					</u-form-item>
+					<u-form-item label-width="160" label="房屋信息:">
+						<text >{{form.residentialName}}-{{form.buildingName}}-{{form.unitName}}-{{form.roomName}}</text>
+					</u-form-item>
+					<u-form-item label-width="160" label="创建时间:">
+						<u-input v-model="form.createDate"  placeholder="暂无信息" disabled></u-input>
+					</u-form-item>
+					<u-form-item v-if="!$isEmpty(form.imageUri)" label="人脸照片:" label-position="top">
+						<image  @click="previewImg" style="width: 300rpx;height: 300rpx;" :src="form.fastDfsServer+form.imageUri" ></image>
+					</u-form-item>
+					<u-form-item v-else label="人脸照片" >
+						<text>暂无</text>
+					</u-form-item>
+				</u-form>
+			</scroll-view>
+		</view>
+		<view class="" style="height: 150rpx;"></view>
+		<view class="footer-fixed">
+			<u-grid :col="3">
+				<u-grid-item @click="toItem(1)">
+					<u-icon name="plus-circle-fill" color='#19be6b' :size="50"></u-icon>
+					<view class="grid-text">新增</view>
+				</u-grid-item>
+				<u-grid-item @click="toItem(2)">
+					<u-icon name="edit-pen-fill" color="#2979ff" :size="50"></u-icon>
+					<view class="grid-text">修改</view>
+				</u-grid-item>
+				<u-grid-item @click="deleteRoom">
+					<u-icon name="trash-fill" color="#fa3534" :size="50"></u-icon>
+					<view class="grid-text">删除</view>
+				</u-grid-item>
+			</u-grid>
+		</view>
+		<navigator url="../index/index" open-type="switchTab"  class="cuIcon cu-btn round" style="background-color: rgba(0,0,0,.2);color: #FFFFFF;position: fixed;right: 3%;bottom: 20%;width: 80rpx;height: 80rpx;">
+			<text class="cuIcon-home" style="font-size: 40rpx;"></text>
+		</navigator>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				labelStyle:{
+					"fontWeight":"600"
+				},
+				form:{}
+			}
+		},
+		onLoad(options) {
+			
+		},
+		onShow() {
+			this.form=getApp().globalData.userDetail
+		},
+		methods: {
+			previewImg(){
+				let url=this.form.fastDfsServer+this.form.imageUri
+				let imgArr=[url]
+				uni.previewImage({
+					urls:imgArr
+				})
+			},
+			toItem(index){
+				let url=''
+				switch (index){
+					case 1:
+					//新增
+						this.$u.toast('暂未开放')
+						// uni.navigateTo({
+						// 	url:"add"
+						// })
+						break;
+					case 2:
+					//修改
+						uni.navigateTo({
+							url:"add?id="+this.form.id
+						})
+						break;
+					default:
+						break;
+				}
+				if (!this.$isEmpty(url)) {
+					uni.navigateTo({
+						url:url
+					})
+				}
+			},
+			deleteRoom(){
+				let item =this.form
+				this.$dialog.showModal('确定要删除此项吗?').then(res=>{
+					this.$api.user.del({id:item.id}).then(res=>{
+						if (res.data==true) {
+							this.$dialog.showModal('删除成功',false).then(res=>{
+								uni.navigateBack({
+									delta:1
+								})
+							})
+						}else{
+							this.$u.toast('删除失败')
+						}
+					})
+					
+				})
+			}
+			
+		}
+	}
+</script>
+
+<style lang="scss">
+	.card{
+		background-color: #FFFFFF;
+		margin: 30rpx;
+		border-radius: 20rpx;
+	}
+</style>

+ 395 - 0
pages/user/user.vue

@@ -0,0 +1,395 @@
+<template>
+	<view>
+		<!-- 筛选标签条 -->
+		<view class="cu-bar bg-white flex" style="padding: 0 10rpx;z-index: 100;width: 100%;position: fixed;top: -2rpx;">
+			<view class="u-line-1" @click="showDown(index)" v-for="(item,index) in filterMenu" :key="index" style="width: 25%;text-align: center;height: 100%;">
+				<text v-text="data[index]==''?item:data[index]" :class="data[index]==''?'':'text-red'"></text>
+				<u-icon v-if="index!=4" name="arrow-down-fill" size="20" style="padding-left: 10rpx;"></u-icon>
+				<text class="cuIcon-filter padding-left-10" v-else></text>
+				<u-badge :offset="[10, 8]" v-if="index==4" size="mini" type="error" :count="filterCount"></u-badge>
+			</view>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view style="margin-top: 110rpx;">
+				<card @deleteItem="delItem" :list="list"></card>
+			</view>
+		</mescroll-body>
+		<!-- 小区 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="residentailShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="residentailKeyWord" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(residentailList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="residentailConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in residentailList" :key="index">
+					<text>{{item.label}}</text>
+				</view>
+				<u-divider v-if="residentailList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+		<!-- 楼栋 -->
+		<u-popup  border-radius="60" height="60%"  mode="bottom" v-model="buildingShow">
+			<view class="fixed cu-bar search bg-white">
+				<view class="search-form round">
+					<text class="cuIcon-search"></text>
+					<u-input style="width: 90%;" v-model="buildingKeyWord" type="text" :adjust-position="false" placeholder="请输入关键字搜索" confirm-type="search"/>
+				</view>
+			</view>
+			<scroll-view v-if="!$isEmpty(buildingList)"  style="padding-top: 110rpx;height: 100%;" :scroll-y="true" >
+				<view @click="buildingConfirm(item)" hover-class="hoverClass" class="text-center padding-30 solid-bottom"  v-for="(item,index) in buildingList" :key="index">
+					<text>{{item.name}}</text>
+				</view>
+				<u-divider v-if="buildingList.length>=10" height="80">只显示十条数据</u-divider>
+			</scroll-view>
+			<u-empty v-else name="search"></u-empty>
+		</u-popup>
+		
+		<!-- 单元选择器 -->
+		<u-picker @confirm="unitConfirm" range-key="name" :range="unitList" v-model="unitShow" mode="selector"></u-picker>
+		<!-- 人脸状态 -->
+		<u-picker @confirm="faceStatusConfirm" range-key="name" :range="faceStatusList" v-model="faceShow" mode="selector"></u-picker>
+		<!-- 筛选 -->
+		<u-modal :show-cancel-button="true" cancel-text="重置"   @cancel="reset" @confirm="mescroll.resetUpScroll()" title="筛选" :mask-close-able="true" v-model="filterShow" >
+			<view class="slot-content" style="margin: 20rpx;">
+				<u-form label-width="150" :model="params" ref="uForm">
+					<u-form-item label="姓名"><u-input v-model="params.name" /></u-form-item>
+					<u-form-item :border-bottom="false" label="手机号"><u-input type="number"  v-model="params.tel" /></u-form-item>
+				</u-form>
+			</view>
+		</u-modal>
+		
+		
+		<!-- 添加按钮 -->
+		<!-- <add-btn @click.native="add"></add-btn> -->
+		<!-- 通知alert -->
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	import card from "./card.vue"
+	import MescrollMixin from "@/components/mescroll-body/mescroll-mixins.js";
+	let that;
+	export default {
+		components:{
+			card
+		},
+		mixins:[MescrollMixin],
+		data() {
+			return {
+				//顶部菜单栏
+				filterMenu: ["小区","楼栋","单元","人脸状态","筛选"],
+				data:['','','','',''],
+				
+				//小区
+				residentailShow:false,
+				residentailList:[],
+				residentailKeyWord:'',
+				
+				//楼栋
+				buildingShow:false,
+				buildingList:[],
+				buildingKeyWord:'',
+				
+				//单元
+				unitShow:false,
+				unitList:[],
+				
+				//人脸状态
+				faceShow:false,
+				faceStatusList:[
+					{
+						'name':'待审核',
+						'value':'0'
+					},
+					{
+						'name':'审核通过',
+						'value':'1'
+					},
+					{
+						'name':'审核未通过',
+						'value':'2'
+					},
+					{
+						'name':'待上传',
+						'value':'-1'
+					},
+				],
+				
+				//筛选框
+				filterShow:false,
+				filterCount:0,
+				
+				params:{},
+				list: [],
+				downOption: { 
+					use: true,
+					auto: false
+				},
+				upOption: {
+					page: {
+						page: 0,
+						size: 10
+					},
+					noMoreSize: 5, 
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+			}
+		},
+		onLoad(){
+			that=this
+			this.getResidentailList()
+		},
+		onShow() {
+			if (this.canReset) {
+				this.mescroll.resetUpScroll()
+			}
+			this.canReset=true
+		},
+		watch:{
+			residentailKeyWord(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.residentailList=[]
+					this.getResidentailList()
+				}, 500)
+			},
+			buildingKeyWord(){
+				let that=this
+				//节流函数
+				if (this.timer){
+					clearTimeout(this.timer)//阻止setTimeout函数的执行
+				}
+				this.timer = setTimeout(() => {
+					that.buildingList=[]
+					that.fetchBuildingList()
+				}, 500)
+			},
+		},
+		methods: {
+			
+	/* 小区 */
+	
+			//获取小区列表
+			getResidentailList(){
+				let params={
+					isSelect:true,
+					name: that.residentailKeyWord,
+				}
+				let list = [];
+				this.$api.residential.page(params).then(res=>{
+					let list=[]
+					res.list.forEach(item=>{
+						let obj={
+							label:item.name,
+							value:item.id
+						}
+						list.push(obj)
+					})
+					this.residentailList = list;
+				})
+			},
+			//点击选择小区,获取楼栋列表
+			residentailConfirm(res){
+				this.params.residentialId =res.value
+				this.data[0] = res.label
+				this.residentailShow = false
+				//获取楼栋列表
+				this.fetchBuildingList()
+				this.mescroll.resetUpScroll()
+			},
+			
+	/* 楼栋 */		
+			
+			//获取楼栋列表
+			fetchBuildingList(){
+				let params={
+					residentialId:this.params.residentialId,
+					name:that.buildingKeyWord
+				}
+				this.$api.building.page(params).then(res=>{
+					this.buildingList=res.data.records
+				})
+			},
+			//点击选择楼栋,获取单元列表
+			buildingConfirm(res){
+				console.log(res);
+				this.params.buildingId=res.id
+				this.data[1] = res.name
+				this.buildingShow = false
+				
+				this.getUnitByResidentialId()
+				this.mescroll.resetUpScroll()
+			},
+			
+			
+	/* 单元 */		
+			
+			
+			//获取单元列表
+			getUnitByResidentialId(){
+				let params={
+					residentialId:this.params.residentialId,
+					buildingId:this.params.buildingId
+				}
+				this.$api.unit.page(params).then(res=>{
+					this.unitList=res.list
+				})
+			},
+			//点击选择单元
+			unitConfirm(index){
+				this.data[2]=this.unitList[index].name
+				this.params.unitId=this.unitList[index].id
+				this.mescroll.resetUpScroll()
+			},
+			
+	/* 人脸状态 */
+	
+			faceStatusConfirm(e){
+				this.data[3]=this.faceStatusList[e].name
+				this.params.checkState=this.faceStatusList[e].value
+				this.mescroll.resetUpScroll()
+			},
+			
+	/* 筛选 */
+			//重置
+			reset(){
+				this.params={}
+				this.data=['','','','','']
+				this.residentailList=[]
+				this.buildingList=[]
+				this.unitList=[]
+				this.residentailKeyWord=''
+				this.buildingKeyWord='',
+				this.list=[],
+				this.getResidentailList()
+				this.mescroll.resetUpScroll()
+			},
+			
+			//获取筛选的条件数
+			getFilterCount(){
+				let n=0
+				if (!this.$isEmpty(this.params.residentialId)) {
+					//小区
+					n++
+				}
+				if (!this.$isEmpty(this.params.buildingId)) {
+					//楼栋
+					n++
+				}
+				if (!this.$isEmpty(this.params.unitId)) {
+					//单元
+					n++
+				}
+				if (!this.$isEmpty(this.params.checkState)) {
+					//人脸状态
+					n++
+				}
+				if (!this.$isEmpty(this.params.name)) {
+					//姓名
+					n++
+				}
+				if (!this.$isEmpty(this.params.tel)) {
+					//手机号
+					n++
+				}
+				this.filterCount=n
+			},
+			
+			/**
+			 * 下拉回调
+			 */
+			downCallback(){
+				setTimeout(()=>{
+					this.mescroll.resetUpScroll()
+				},1500)
+			},
+			/**
+			 * 上拉回调
+			 * @param {Object} mescroll
+			 */
+			upCallback(mescroll) {
+				let params=this.params
+				params.current=mescroll.num
+				params.size=mescroll.size
+				this.getFilterCount()
+				try{
+					this.$api.user.page(params).then(res=>{
+						let data=res.data.records
+						let length=data.length
+						let total=res.data.total
+						mescroll.endBySize(length, total);
+						if(mescroll.num == 1) this.list = []; 
+						this.list=this.list.concat(data); 
+					})
+				}catch(e){
+					mescroll.endErr();
+				}
+			},
+			
+			/**
+			 * 顶部菜单选择
+			 * @param {Object} index 菜单下标
+			 */
+			showDown(index){
+				if (index==0) {
+					//小区
+					this.residentailShow=true
+				}else if (index==1) {
+					//楼栋
+					if (this.$isEmpty(this.params.residentialId)) {
+						this.$u.toast('请选择小区')
+					}else{
+						this.buildingShow=true
+					}
+					return
+				}else if (index==2) {
+					//单元
+					if (this.$isEmpty(this.params.buildingId)) {
+						this.$u.toast('请选择楼栋')
+					}else{
+						this.unitShow=true
+					}
+					return
+				}else if (index==3) {
+					//人脸状态
+					this.faceShow=true
+				}else if (index==4) {
+					//筛选
+					this.filterShow=true
+				}
+			},
+			//添加
+			add(){
+				uni.navigateTo({
+					url:"./add"
+				})
+			},
+			//删除
+			delItem(item){
+				this.$dialog.showModal('确定要删除此项吗?').then(res=>{
+					this.$api.user.del({id:item.id}).then(res=>{
+						if (res.data==true) {
+							this.$u.toast('删除成功')
+							that.mescroll.resetUpScroll()
+						}
+					})
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

BIN
static/choose-Cade/choose-Cadecc.png


BIN
static/choose-Cade/choose-Cades.png


BIN
static/choose-Cade/choose-Cadex.png


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 0
static/iconfont.css


BIN
static/index/face.png


BIN
static/login/a4.png


BIN
static/login/psw1.png


BIN
static/login/psw2.png


BIN
static/mine/logout.png


BIN
static/tarbar/index-select.png


BIN
static/tarbar/index.png


BIN
static/tarbar/mine-select.png


BIN
static/tarbar/mine.png


+ 76 - 0
uni.scss

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

+ 119 - 0
utils/cache.js

@@ -0,0 +1,119 @@
+const postfix = '_cache_time'; // 缓存前缀
+
+const simpleCache = {
+	postfix: postfix,
+}
+
+
+/**
+ * 写入缓存
+ * @param {String} key 本地缓存中的指定的key
+ * @param {Any} data 需要存储的内容,只支持原生类型、及能够通过 JSON.stringify 序列化的对象
+ * @param {Number} time 保存的时间, 数字类型,单位秒 为空则永久有效.
+ */
+simpleCache.put = function(key, data, time) {
+	uni.setStorageSync(key, data)
+	var seconds = parseInt(time);
+	if (seconds > 0) {
+		var timestamp = Date.parse(new Date());
+		timestamp = timestamp / 1000 + seconds;
+		uni.setStorageSync(key + postfix, timestamp + "")
+	} else {
+		uni.removeStorageSync(key + postfix)
+	}
+}
+
+/**
+ * 从本地缓存中同步获取指定 key 对应的内容。
+ * @param {String} key 本地缓存中的指定的 key
+ * @param def 获取失败时的默认内容,可以为空.
+ */
+simpleCache.get = function(key, def) {
+	var deadtime = parseInt(uni.getStorageSync(key + postfix))
+	if (deadtime) {
+		if (parseInt(deadtime) < Date.parse(new Date()) / 1000) {
+			//已过期
+			if (def) {
+				return def;
+			} else {
+				//清除过期时间
+				uni.removeStorageSync(key + postfix)
+				//移除缓存
+				uni.removeStorageSync(key)
+				return false;
+			}
+		}
+	}
+	var res = uni.getStorageSync(key);
+	if (res) {
+		return res;
+	} else {
+		if (def == undefined || def == "") {
+			def = false;
+			uni.removeStorageSync(key)
+		}
+		return def;
+	}
+}
+
+/**
+ * 从本地缓存中同步移除指定 key。
+ * @param {String} key 本地缓存中的指定的 key
+ */
+simpleCache.remove = function(key) {
+	uni.removeStorageSync(key);
+	uni.removeStorageSync(key + postfix);
+}
+
+/**
+ * 同步清理本地数据缓存。
+ */
+simpleCache.clear = function() {
+	// 这里面其实可以扩展你不想清理的缓存,比如你保存的用户登录状态. 在清理之前先获取一次.
+	// var user = simpleCache.get("user_info"); 
+	uni.clearStorageSync();
+	// simpleCache.put("user_info",user); // 等 其它的自行扩展.
+}
+
+/**
+ * id_key:存储的key
+ * image_url:图片路径
+ * isError:是否加载错误
+ * time:缓存时间
+ */
+simpleCache.image_cache=function(id_key,image_url,isError,time=3*24*60*60) {
+	let imgTemp= simpleCache.get(id_key)
+	if(!imgTemp){
+		//本地没有缓存,需要下载
+		uni.downloadFile({
+		    url: image_url, 
+		    success: (res) => {
+		        if (res.statusCode === 200) {
+					let temp=res.tempFilePath
+					simpleCache.put(id_key,temp,time)
+		        }
+		    }
+		});
+		return image_url
+	}else{
+		if(isError){
+			console.log("缓存的图片发生错误了,从网络中获取",image_url)
+			return image_url
+		}else{
+			console.log("缓存的图片没有发生错误,从缓存中获取")
+			return imgTemp
+		}
+	}
+}
+
+
+
+// export default simpleCache
+
+module.exports = {
+	put:simpleCache.put,
+	get:simpleCache.get,
+	remove:simpleCache.remove,
+	clear:simpleCache.clear,
+	cacheImg:simpleCache.image_cache
+}

+ 44 - 0
utils/dateTime.js

@@ -0,0 +1,44 @@
+/**
+ * 日期时间工具类,后期慢慢收集累积
+ * createDate 2021-11-02
+ */
+
+let dateTime = {}
+
+/**
+ * 获取当前时间
+ * @return {String} 2021-11-02 19:53:22
+ */
+dateTime.now=()=>{
+		var mydate = new Date();
+		var str = "" + mydate.getFullYear() + "-";
+		if(mydate.getMonth()<10){
+			str +="0"+ (mydate.getMonth() + 1) + "-";
+		}else{
+			str += (mydate.getMonth() + 1) + "-";
+		}
+		if(mydate.getDate()<10){
+			str += "0"+mydate.getDate() + " ";
+		}else{
+			str += mydate.getDate() + " ";
+		}
+		if(mydate.getHours()<10){
+			str += "0"+mydate.getHours() + ":";
+		}else{
+			str += mydate.getHours() + ":";
+		}
+		if(mydate.getMinutes()<10){
+			str += "0"+mydate.getMinutes() + ":";
+		}else{
+			str += mydate.getMinutes() + ":";
+		}
+		if(mydate.getSeconds()<10){
+			str += "0"+mydate.getSeconds();
+		}else{
+			str += mydate.getSeconds();
+		}
+		return str;	
+}
+
+
+export default dateTime

+ 20 - 0
utils/dialog.js

@@ -0,0 +1,20 @@
+let dialog = {};
+/**
+ * 封装模态框
+ */
+dialog.showModal = (content,isShowCancel=true,title='提示')=>{
+    return new Promise((resolve,reject)=>{
+        uni.showModal({
+			title: title,
+			content: content,
+			showCancel:isShowCancel,
+			success: (res)=>{
+				if (res.confirm) {
+					resolve(res)
+				} 
+			}
+		});
+	})
+}
+
+export default dialog

+ 12 - 0
utils/global.js

@@ -0,0 +1,12 @@
+let global = {
+
+  imgBaseUrl:'http://139.9.103.171:8888/',
+
+  wxParams:{
+	  appid:'wxa05f3aff24dba698'
+  },
+  
+
+}
+
+export default global 

+ 415 - 0
utils/util.js

@@ -0,0 +1,415 @@
+//工具类
+let util = {}
+
+
+//复制
+util.copy=content=>{
+	uni.setClipboardData({ data: content });
+}
+
+
+// 图片缓存
+
+
+//拨打电话
+util.callPhone=phone=>{
+	uni.showModal({
+	    title: '提示',
+	    content: `确定要拨打电话:${phone}吗?`,
+	    success: function (res) {
+	        if (res.confirm) {
+				uni.makePhoneCall({
+				    phoneNumber: phone //仅为示例
+				});
+	        } 
+	    }
+	});
+}
+util.showModal=(content,title='提示')=>{
+    return new Promise((resolve,reject)=>{
+        uni.showModal({
+            title: title,
+            content: content,
+            success: (res)=>{
+                resolve(res)
+            }
+        });
+    })
+}
+//是否为手机号码
+util.isPhone = phone => {
+  let pat = /^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\d{8}$/
+  return pat.test(phone)
+}
+
+//去除两边空格
+util.trim = value => {
+  if (value) {
+    return value.replace(/^\s+|\s+$/gm, '')
+  }
+  return ''
+}
+
+//根据QueryString参数名称获取值
+util.getQueryStringByName = (url, name) => {
+  var result = url.match(new RegExp('[?&]' + name + '=([^&]+)', 'i'))
+  if (result == null || result.length < 1) {
+    return ''
+  }
+  return result[1]
+}
+
+//四舍五入保留2位小数(不够位数,则用0替补)
+util.keepTwoDecimalFull = num => {
+  var result = parseFloat(num)
+  if (isNaN(result)) {
+    return false
+  }
+  result = Math.round(num * 100) / 100
+  var s_x = result.toString()
+  var pos_decimal = s_x.indexOf('.')
+  if (pos_decimal < 0) {
+    pos_decimal = s_x.length
+    s_x += '.'
+  }
+  while (s_x.length <= pos_decimal + 2) {
+    s_x += '0'
+  }
+  return s_x
+}
+
+/**
+ * 数组去重
+ * @param {Array} arr 源数组
+ * 
+ * @return {Array} newArr 去重后的数组
+ */
+util.uniqueArray = (arr)=>{
+  let newArr = []
+  arr.forEach(item => {
+    return newArr.includes(item) ? '' : newArr.push(item)
+  })
+  return newArr
+}
+
+util.checkMobile = phoneStr => {
+  if (phoneStr.length < 11) {
+    return false
+  } else if (!/^1[3|4|5|6|7|8][0-9]\d{4,8}$/.test(phoneStr)) {
+    return false
+  } else {
+    return true
+  }
+}
+
+/**
+ * 序列化对象
+ * @param {Object} data 标准JS对象 { key:value,... }
+ */
+util.serialize = (data, isEncode = false) => {
+  if (!data) return ''
+  var pairs = []
+  for (var name in data) {
+    if (!data.hasOwnProperty(name)) continue
+    if (typeof data[name] === 'function') continue
+    var value =
+      data[name] != null && typeof data[name] != 'undefined'
+        ? data[name].toString()
+        : ''
+    if (isEncode) {
+      name = encodeURIComponent(name)
+      value = encodeURIComponent(value)
+    }
+    pairs.push(name + '=' + value)
+  }
+  return pairs.join('&')
+}
+
+
+
+//获取某一个页面上下文 context
+util.getPageCtx = (idx = 0) => {
+  let pages = getCurrentPages()
+  if (pages.length > 0) {
+    return pages[pages.length - 1 - idx] || {}
+  }
+  return {}
+}
+
+//获取当前页面路由
+util.getCurrentRoute = () => {
+  let page = util.getPageCtx()
+  if (page.route) {
+    return page.route
+  }
+  return ''
+}
+
+
+/* ======================== 时间日期 begin ========================== */
+
+
+//日期转字符串
+util.dateToString = (date)=>{
+	var year = date.getFullYear();
+	var month =(date.getMonth() + 1).toString(); 
+	var day = (date.getDate()).toString();  
+	if (month.length == 1) { 
+	    month = "0" + month; 
+	} 
+	if (day.length == 1) { 
+	    day = "0" + day; 
+	}
+	var dateTime = year + "-" + month + "-" + day;
+	return dateTime; 
+}
+//字符串转日期
+util.stringToDate = (dateStr,separator)=>{
+	if(!separator){
+	       separator="-";
+	}
+	var dateArr = dateStr.split(separator);
+	var year = parseInt(dateArr[0]);
+	var month;                     
+	if(dateArr[1].indexOf("0") == 0){
+	    month = parseInt(dateArr[1].substring(1));
+	}else{
+	     month = parseInt(dateArr[1]);
+	}
+	var day = parseInt(dateArr[2]);
+	var date = new Date(year,month -1,day);
+	return date;
+}
+
+/**
+ * 天数加减
+ * date传入你需要的日期,格式"xxxx-xx-xx"。addDays传要加减的日期数,往前传正数,往后传负数
+ */
+util.addDay=(date,addDays)=>{
+	var Dates = new Date(date);
+	Dates.setDate(Dates.getDate() + addDays);
+	var mon = Dates.getMonth() + 1,
+	    day = Dates.getDate();
+	if(mon < 10){
+	    mon = "0" + mon;//月份小于10,在前面补充0
+	}
+	if(day < 10){
+	    day = "0" + day;//日小于10,在前面补充0
+	}
+	return Dates.getFullYear() + "-" + mon + "-" +day;
+}
+
+
+// 时间函数-tian=0(获取当前时间)-accuracy=1(获取年月日)
+util.getTime = (tian, accuracy) => {
+  //获取当前时间戳
+  var timestamp = Date.parse(new Date())
+
+  timestamp = timestamp / 1000
+  var tomorrow_timetamp = timestamp - tian * 24 * 60 * 60
+
+  var date = new Date(tomorrow_timetamp * 1000)
+  //年
+  var Y = date.getFullYear()
+  //月
+  var M =
+    date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
+  //日
+  var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
+  //时
+  var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
+
+  //分
+  var m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
+
+  //秒
+  var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
+
+  var Time = Y + '-' + M + '-' + D
+  var Time2 = Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s
+  if (accuracy == '1') {
+    return Time
+  } else {
+    return Time2
+  }
+}
+
+// 格式化日期 2019-01-01 TT(WW)
+util.dateFormat = (
+  value = new Date(),
+  format = 'yyyy-MM-dd',
+  isInit = false
+) => {
+  if (value) {
+    var date = typeof value == 'string' ? value.replace(/\-/g, '/') : value
+    var d = new Date(date)
+    var week = ''
+    var label = ''
+    if (isInit) {
+      var cdt = new Date()
+      var cur = new Date(
+        cdt.getFullYear() + '/' + (cdt.getMonth() + 1) + '/' + cdt.getDate()
+      )
+      var diffTime = d.getTime() - cur.getTime()
+      var oneDayTime = 24 * 60 * 60 * 1000
+      var twoDayTime = 48 * 60 * 60 * 1000
+      var threeDayTime = 72 * 60 * 60 * 1000
+      let weeks = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
+      if (diffTime > 0) {
+        if (diffTime <= oneDayTime) {
+          label = '今天'
+        } else if (diffTime <= twoDayTime) {
+          label = '明天'
+        } else if (diffTime <= threeDayTime) {
+          label = '后天'
+        }
+      }
+      week = weeks[d.getDay()]
+    }
+
+    var o = {
+      'T+': label,
+      'W+': week,
+      'M+': d.getMonth() + 1, //month
+      'd+': d.getDate(), //day
+      'h+': d.getHours(), //hour
+      'm+': d.getMinutes(), //minute
+      's+': d.getSeconds(), //second
+      'q+': Math.floor((d.getMonth() + 3) / 3), //quarter
+      S: d.getMilliseconds() //millisecond
+    }
+
+    if (/(y+)/.test(format)) {
+      format = format.replace(
+        RegExp.$1,
+        (d.getFullYear() + '').substr(4 - RegExp.$1.length)
+      )
+    }
+    for (var k in o) {
+      if (k == 'T+' && !label) continue
+
+      if (new RegExp('(' + k + ')').test(format)) {
+        format = format.replace(
+          RegExp.$1,
+          RegExp.$1.length == 1
+            ? o[k]
+            : ('00' + o[k]).substr(('' + o[k]).length)
+        )
+      }
+    }
+    return format
+  }
+  return value
+}
+
+// 活动倒计时  需要在外面调用 如:setInterval("countDown()",1000);
+util.countDown = (day, hour, minute, second) => {
+  var flag = false
+  second -= 1
+  if (second == 0) {
+    minute = minute - 1
+    second = 60
+    if (minute < 0) {
+      hour = hour - 1
+      minute = 59
+      if (hour < 0) {
+        day -= 1
+        hour = 23
+        if (day < 0) {
+          flag = true
+        }
+      }
+    }
+  }
+  if (flag) {
+    return '活动结束'
+  } else {
+    return day + '天' + hour + '小时' + minute + '分' + second + '秒'
+  }
+}
+// 两个时间相差 天 小时 分钟 秒 date1、date2格式:'2019-06-29 14:36:10'或'2019/06/29 14:36:10'
+util.timeComparison = (date1, date2) => {
+  // var date1 = '2019-06-29 14:36:10';  //开始时间
+  // var date2 = new Date();    //结束时间
+  var date3 = date2.getTime() - date1.getTime() //时间差的毫秒数
+
+  //------------------------------
+
+  //计算出相差天数
+  var days = Math.floor(date3 / (24 * 3600 * 1000))
+
+  //计算出小时数
+  var leave1 = date3 % (24 * 3600 * 1000) //计算天数后剩余的毫秒数
+  var hours = Math.floor(leave1 / (3600 * 1000))
+  //计算相差分钟数
+  var leave2 = leave1 % (3600 * 1000) //计算小时数后剩余的毫秒数
+  var minutes = Math.floor(leave2 / (60 * 1000))
+  //计算相差秒数
+  var leave3 = leave2 % (60 * 1000) //计算分钟数后剩余的毫秒数
+  var seconds = (leave3 / 1000).toFixed(1)
+
+  return (
+    ' 相差 ' +
+    days +
+    '天 ' +
+    hours +
+    '小时 ' +
+    minutes +
+    ' 分钟' +
+    seconds +
+    ' 秒'
+  )
+}
+util.timeComparisonActive = (date1, date2, isNeedMillSecond = true) => {
+  // var date1 = '2019-06-29 14:36:10';  //开始时间
+  // var date2 = new Date();    //结束时间
+  var date3 = date2.getTime() - date1.getTime() //时间差的毫秒数
+  if (parseInt(date3) <= 0) {
+    date3 = 0
+  }
+  //------------------------------
+
+  //计算出相差天数
+  var days = Math.floor(date3 / (24 * 3600 * 1000))
+
+  //计算出小时数
+  var leave1 = date3 % (24 * 3600 * 1000) //计算天数后剩余的毫秒数
+  var hours = Math.floor(leave1 / (3600 * 1000))
+  //计算相差分钟数
+  var leave2 = leave1 % (3600 * 1000) //计算小时数后剩余的毫秒数
+  var minutes = Math.floor(leave2 / (60 * 1000))
+  //计算相差秒数
+  var leave3 = leave2 % (60 * 1000) //计算分钟数后剩余的毫秒数
+  if (isNeedMillSecond) {
+    var seconds = (leave3 / 1000).toFixed(1)
+  } else {
+    var seconds = Math.floor(leave3 / 1000)
+  }
+  var allSeconds =
+    days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds
+  hours = days * 24 + hours < 10 ? '0' + (days * 24 + hours) : days * 24 + hours
+  minutes = minutes < 10 ? '0' + minutes : minutes
+  seconds = seconds < 10 ? '0' + seconds : seconds
+  days = days < 10 ? '0' + days : days
+
+  var isEnd = allSeconds > 0 ? false : true
+
+  return { days, hours, minutes, seconds, isEnd }
+}
+
+
+/**
+ * 生成日期对象
+ * @param {String,Number} value 日期 字符串 或者 毫秒数字
+ * @return {Date} [date = new Date()] 日期对象
+ */ 
+util.createDate = (value)=>{
+  let date = typeof (value) == 'string' ? value.replace(/\-/g, '/') : value
+  return new Date(date)
+} 
+/* ======================== 时间日期 end ========================== */
+
+
+
+
+export default util

+ 215 - 0
utils/verify.js

@@ -0,0 +1,215 @@
+
+let verify = {}
+/**
+ * 验证电子邮箱格式
+ */
+verify.email=value=>{
+	return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value);
+}
+/**
+ * 验证手机格式
+ */
+verify.phone=value=>{
+	return /^1[23456789]\d{9}$/.test(value)
+}
+
+/**
+ * 是否固定电话
+ */
+verify.landline=value=>{
+	let reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/;
+	return reg.test(value);
+}
+
+/**
+ * 验证URL格式
+ */
+verify.isUrl=value=>{
+	return /^((https|http|ftp|rtsp|mms):\/\/)(([0-9a-zA-Z_!~*'().&=+$%-]+: )?[0-9a-zA-Z_!~*'().&=+$%-]+@)?(([0-9]{1,3}.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(:[0-9]{1,4})?((\/?)|(\/[0-9a-zA-Z_!~*'().;?:@&=+$,%#-]+)+\/?)$/
+		.test(value)
+}
+
+/**
+ * 验证日期格式
+ */
+verify.isDate=value=>{
+	return !/Invalid|NaN/.test(new Date(value).toString())
+}
+
+
+/**
+ * 验证ISO类型的日期格式
+ */
+verify.isDateISO=value=>{
+	return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value)
+}
+
+/**
+ * 验证十进制数字
+ */
+verify.isNumber=value=>{
+	return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)
+}
+
+/**
+ * 验证整数
+ */
+verify.isDigits=value=>{
+	return /^1[23456789]\d{9}$/.test(value)
+}
+
+/**
+ * 验证身份证号码
+ */
+verify.idCard=value=>{
+	return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
+		value)
+}
+
+/**
+ * 是否车牌号
+ */
+verify.isCarNo=value=>{
+	// 新能源车牌
+	const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
+	// 旧车牌
+	const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
+	if (value.length === 7) {
+		return creg.test(value);
+	} else if (value.length === 8) {
+		return xreg.test(value);
+	} else {
+		return false;
+	}
+}
+
+/**
+ * 金额,只允许2位小数
+ */
+verify.isAmount=value=>{
+	//金额,只允许保留两位小数
+	return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value);
+}
+
+/**
+ * 中文
+ */
+verify.isChinese=value=>{
+	let reg = /^[\u4e00-\u9fa5]+$/gi;
+	return reg.test(value);
+}
+
+/**
+ * 只能输入字母
+ */
+verify.isLetter=value=>{
+	return /^[a-zA-Z]*$/.test(value);
+}
+
+
+/**
+ * 只能是字母或者数字
+ */
+verify.isEnOrNum=value=>{
+	//英文或者数字
+	let reg = /^[0-9a-zA-Z]*$/g;
+	return reg.test(value);
+}
+
+/**
+ * 验证是否包含某个值
+ */
+verify.isContains=value=>{
+	return value.indexOf(param) >= 0
+}
+
+/**
+ * 验证一个值范围[min, max]
+ */
+verify.isRange=value=>{
+	return value >= param[0] && value <= param[1]
+}
+
+
+/**
+ * 验证一个长度范围[min, max]
+ */
+verify.isRangeLength=value=>{
+	return value.length >= param[0] && value.length <= param[1]
+}
+
+
+
+
+/**
+ * 判断是否为空
+ */
+verify.isEmpty=value=>{
+	switch (typeof value) {
+		case 'undefined':
+			return true;
+		case 'string':
+			if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
+			break;
+		case 'boolean':
+			if (!value) return true;
+			break;
+		case 'number':
+			if (0 === value || isNaN(value)) return true;
+			break;
+		case 'object':
+			if (null === value || value.length === 0) return true;
+			for (var i in value) {
+				return false;
+			}
+			return true;
+	}
+	return false;
+}
+
+/**
+ * 是否json字符串
+ */
+verify.isJsonString=value=>{
+	if (typeof value == 'string') {
+		try {
+			var obj = JSON.parse(value);
+			if (typeof obj == 'object' && obj) {
+				return true;
+			} else {
+				return false;
+			}
+		} catch (e) {
+			return false;
+		}
+	}
+	return false;
+}
+
+
+/**
+ * 是否数组
+ */
+verify.isArray=value=>{
+	if (typeof Array.isArray === "function") {
+		return Array.isArray(value);
+	} else {
+		return Object.prototype.toString.call(value) === "[object Array]";
+	}
+}
+
+/**
+ * 是否对象
+ */
+verify.isObject=value=>{
+	return Object.prototype.toString.call(value) === '[object Object]';
+	return /^1[23456789]\d{9}$/.test(value)
+}
+/**
+ * 是否短信验证码
+ */
+verify.isCode=(value,len=6)=>{
+	return new RegExp(`^\\d{${len}}$`).test(value);
+}
+
+export default verify

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است