lhl 1 year ago
commit
5bc7e5034d
100 changed files with 12114 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 16 0
      .hbuilderx/launch.json
  3. 229 0
      App.vue
  4. 36 0
      README.en.md
  5. 39 0
      README.md
  6. 352 0
      common/html-parser.js
  7. 176 0
      components/an-notice-bar/an-notice-bar.vue
  8. 190 0
      components/authVip.vue
  9. 8 0
      components/circle-progress-bar/changelog.md
  10. 286 0
      components/circle-progress-bar/circle-progress-bar.vue
  11. 85 0
      components/circle-progress-bar/package.json
  12. 28 0
      components/circle-progress-bar/readme.md
  13. 155 0
      components/if-continue/index.vue
  14. 377 0
      components/m-icon/m-icon.css
  15. 41 0
      components/m-icon/m-icon.vue
  16. 131 0
      components/m-input.vue
  17. 55 0
      components/mescroll-uni/components/mescroll-down.css
  18. 47 0
      components/mescroll-uni/components/mescroll-down.vue
  19. 90 0
      components/mescroll-uni/components/mescroll-empty.vue
  20. 83 0
      components/mescroll-uni/components/mescroll-top.vue
  21. 47 0
      components/mescroll-uni/components/mescroll-up.css
  22. 39 0
      components/mescroll-uni/components/mescroll-up.vue
  23. 19 0
      components/mescroll-uni/mescroll-body.css
  24. 344 0
      components/mescroll-uni/mescroll-body.vue
  25. 65 0
      components/mescroll-uni/mescroll-mixins.js
  26. 33 0
      components/mescroll-uni/mescroll-uni-option.js
  27. 36 0
      components/mescroll-uni/mescroll-uni.css
  28. 788 0
      components/mescroll-uni/mescroll-uni.js
  29. 420 0
      components/mescroll-uni/mescroll-uni.vue
  30. 50 0
      components/mescroll-uni/mixins/mescroll-comp.js
  31. 51 0
      components/mescroll-uni/mixins/mescroll-more-item.js
  32. 56 0
      components/mescroll-uni/mixins/mescroll-more.js
  33. 102 0
      components/mescroll-uni/wxs/mixins.js
  34. 92 0
      components/mescroll-uni/wxs/renderjs.js
  35. 268 0
      components/mescroll-uni/wxs/wxs.wxs
  36. 50 0
      components/myAd.vue
  37. 154 0
      components/overtimu/index.vue
  38. 147 0
      components/question-answer-sheet-1/question-answer-sheet-1.vue
  39. 22 0
      components/question-answer-sheet/question-answer-sheet.vue
  40. 116 0
      components/question-answer/question-answer.vue
  41. 79 0
      components/question-fav/question-fav.vue
  42. 78 0
      components/question-jianda/question-jianda.vue
  43. 203 0
      components/question-jiucuo/question-jiucuo.vue
  44. 146 0
      components/question-note/question-note.vue
  45. 216 0
      components/question-option/question-option.vue
  46. 79 0
      components/question-pre-next/question-pre-next.vue
  47. 38 0
      components/question-remove/question-remove.vue
  48. 60 0
      components/question-set/question-set.vue
  49. 82 0
      components/question-tiankong/question-tiankong.vue
  50. 24 0
      components/question-title/question-title.vue
  51. 59 0
      components/question-type/question-type.vue
  52. 28 0
      components/uParse/gaoyia-parse/components/wxParseAudio.vue
  53. 94 0
      components/uParse/gaoyia-parse/components/wxParseImg.vue
  54. 55 0
      components/uParse/gaoyia-parse/components/wxParseTable.vue
  55. 98 0
      components/uParse/gaoyia-parse/components/wxParseTemplate0.vue
  56. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate1.vue
  57. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate10.vue
  58. 86 0
      components/uParse/gaoyia-parse/components/wxParseTemplate11.vue
  59. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate2.vue
  60. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate3.vue
  61. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate4.vue
  62. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate5.vue
  63. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate6.vue
  64. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate7.vue
  65. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate8.vue
  66. 88 0
      components/uParse/gaoyia-parse/components/wxParseTemplate9.vue
  67. 15 0
      components/uParse/gaoyia-parse/components/wxParseVideo.vue
  68. 261 0
      components/uParse/gaoyia-parse/libs/html2json.js
  69. 156 0
      components/uParse/gaoyia-parse/libs/htmlparser.js
  70. 209 0
      components/uParse/gaoyia-parse/libs/wxDiscode.js
  71. 258 0
      components/uParse/gaoyia-parse/parse.css
  72. 227 0
      components/uParse/gaoyia-parse/parse.vue
  73. 27 0
      components/uParse/src/components/wxParseAudio.vue
  74. 90 0
      components/uParse/src/components/wxParseImg.vue
  75. 104 0
      components/uParse/src/components/wxParseTemplate0.vue
  76. 96 0
      components/uParse/src/components/wxParseTemplate1.vue
  77. 94 0
      components/uParse/src/components/wxParseTemplate10.vue
  78. 84 0
      components/uParse/src/components/wxParseTemplate11.vue
  79. 95 0
      components/uParse/src/components/wxParseTemplate2.vue
  80. 95 0
      components/uParse/src/components/wxParseTemplate3.vue
  81. 95 0
      components/uParse/src/components/wxParseTemplate4.vue
  82. 95 0
      components/uParse/src/components/wxParseTemplate5.vue
  83. 95 0
      components/uParse/src/components/wxParseTemplate6.vue
  84. 95 0
      components/uParse/src/components/wxParseTemplate7.vue
  85. 95 0
      components/uParse/src/components/wxParseTemplate8.vue
  86. 95 0
      components/uParse/src/components/wxParseTemplate9.vue
  87. 15 0
      components/uParse/src/components/wxParseVideo.vue
  88. 261 0
      components/uParse/src/libs/html2json.js
  89. 156 0
      components/uParse/src/libs/htmlparser.js
  90. 195 0
      components/uParse/src/libs/wxDiscode.js
  91. 493 0
      components/uParse/src/wxParse.css
  92. 122 0
      components/uParse/src/wxParse.vue
  93. 92 0
      components/uc-parse/uc-parse.vue
  94. 105 0
      components/uni-badge/uni-badge.vue
  95. 170 0
      components/uni-countdown/uni-countdown.vue
  96. 39 0
      components/uni-icon/uni-icon.vue
  97. 199 0
      components/uni-list-item/uni-list-item.vue
  98. 88 0
      components/uni-list/readme.md
  99. 45 0
      components/uni-list/uni-list.vue
  100. 124 0
      components/uni-segmented-control/uni-segmented-control.vue

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/unpackage/

+ 16 - 0
.hbuilderx/launch.json

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

+ 229 - 0
App.vue

@@ -0,0 +1,229 @@
+<script>
+	export default {
+		onLaunch: function() {
+			console.log('App Launch')
+		},
+		onShow: function() {
+			console.log('App Show')
+		},
+		onHide: function() {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style>
+	@import "~@/static/css/tiku.css";
+    /*每个页面公共css */
+    @import "./static/font/iconfont.css";
+    /*占位符*/
+    .lz-status_bar {
+        height: var(--status-bar-height);
+        width: 100%;
+        background-color: #fff;
+    }
+
+    .lz-top_view {
+        height: var(--status-bar-height);
+        width: 100%;
+        position: fixed;
+        background-color: #fff;
+        top: 0;
+        z-index: 999;
+    }
+    /*头部样式*/
+    .kaoshi-head{
+        background: #fff;
+        top: var(--status-bar-height);
+        z-index: 100;
+        border-bottom: solid 1px #f6f7f9;
+    }
+    .kaoshi-head-left .icon-zuojiantou{
+        font-size: 20px;
+    }
+    .kaoshi-head-top{
+        display: flex;
+        font-size: 16px;
+        color: #000;
+        align-items: center;
+        width: 93%;
+        margin: 0 auto;
+        line-height: 50px;
+    }
+    .kaoshi-head .kaoshi-head-left{width: 20%;display: flex;align-items: center;}
+    .kaoshi-head .kaoshi-head-left .iconfont{font-size: 18px;}
+    .kaoshi-head .kaoshi-head-m{
+		width: 60%;
+		text-align: center;
+		font-size: 18px !important;
+		text-overflow: ellipsis;
+		overflow: hidden;
+		display: -webkit-box;
+		-webkit-line-clamp: 1;
+		-webkit-box-orient: vertical;
+		word-break: break-all;
+		font-weight: bold !important;
+	}
+    .kaoshi-head .kaoshi-head-right{width: 20%;text-align: right;}
+    .kaoshi-head-right .iconfont{
+        font-size: 23px;
+    }
+    .pub-flex-1 {
+        flex: 1;
+    }
+    .pub-gray{
+        color: #b4b3b3;
+    }
+    .pub-s-gray {
+        color: #616161;
+    }
+    .pub-orange{
+        color: #ffb500
+    }
+    .pub-green{
+        color: #0ec358;
+    }
+    .pub-red{
+        color: #ff4c4c
+    }
+    .green-background{
+        background: #0ec358;
+        border: solid 1px #0ec358!important;
+        color: #fff;
+    }
+    .orange-background{
+        background: #ffb500;
+        border: solid 1px #ffb500!important;
+        color: #fff;
+    }
+    .red-background{
+        background: #ff4c4c;
+        border: solid 1px #ff4c4c!important;
+        color: #fff;
+    }
+    .dai{
+        border: solid 1px #0081ff!important;
+    }
+    .exam-tan-button {
+        text-align: center;
+    }
+    .white-background{
+        background: #fff
+    }
+	.vip{
+		width: 35px;
+	   color: #ffecd5;
+	   border-radius: 5px;
+	   padding: 4px 6px;
+	   font-size: 11px;
+	   margin-left: 4px;
+	   display: inline-block;
+	   background-image: linear-gradient(to left, #4b444c, #000);
+	}
+	.vip image{
+	    width: 14px;
+	    height: 14px;
+		float: left;
+	    margin-right: 3px;
+	}
+	/* 弹窗 */
+	.lz-modal {
+	    position: fixed;
+	    top: 0;
+	    right: 0;
+	    bottom: 0;
+	    left: 0;
+	    z-index: 999;
+	    opacity: 0;
+	    outline: 0;
+	    text-align: center;
+	    backface-visibility: hidden;
+	    perspective: 1000px;
+	    background: rgba(0,0,0,.4);
+	    transition: all .3s ease-in-out 0s;
+	    pointer-events: none;
+	}
+	.lz-modal .lz-modal-dialog {
+	    position: relative;
+	    display: inline-block;
+	    vertical-align: middle;
+	    margin-left: auto;
+	    margin-right: auto;
+	    width: 81%;
+	    max-width: 100%;
+	    background-color: #fff;
+	    border-radius: 10px;
+	    overflow: hidden;
+	}
+	.lz-modal::before {
+	    content: "\200B";
+	    display: inline-block;
+	    height: 100%;
+	    vertical-align: middle;
+	}
+	.lz-list-header{
+	    display: flex;
+	    flex-direction: row;
+	    width: 89%;
+	    margin: 0 auto;
+	    padding: 14px 0;
+	    align-items: center;
+	    justify-content: space-between;
+		font-size: 17px;
+		    color: #000;
+	}
+	.lz-list-header .iconfont{
+	    color: #c5c5c5;
+	}
+	.lz-list-item .lz-list-line .lz-list-content {
+		flex: 1;
+		color: #000;
+		font-size: 15px;
+		line-height: 1.5;
+		text-align: left;
+		width: auto;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		white-space: nowrap;
+		padding: 10px 0;
+		margin: 0 18px 12px;
+		border: solid 1px #dfdfdf;
+		padding-left: 11px;
+	}
+	.lz-list-item uni-input {
+	    font-size: 14px;
+	}
+	.lz-list-body::before {
+	    border-bottom: none;
+	}
+	.lz-list-body::before {
+	    border-top:none;
+	}
+	.primary {
+		width: 92%;
+		margin: 0 auto;
+		background: #3c7bfc !important;
+		color: #fff !important;
+		border-radius: 6px;
+		font-size: 16px;
+	}
+	.lz-list-header .icon-cuo1{
+	    font-size: 17px;
+	    color: #9e9e9e;
+	}
+	.lz-modal.lz-modal-show {
+	    opacity: 1;
+	    transition-duration: .3s;
+	    transform: scale(1);
+	    overflow-x: hidden;
+	    overflow-y: auto;
+	    pointer-events: auto;
+	}
+	
+	.search-wuc .cur .cur-line{
+		width: 17px !important;
+	}
+	.my-kf .uni-list-cell__container {
+	    padding: 9px 15px !important;
+	}
+</style>

+ 36 - 0
README.en.md

@@ -0,0 +1,36 @@
+# 考试答题系统2020年8月25日
+
+#### Description
+{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
+
+#### Software Architecture
+Software architecture description
+
+#### Installation
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Instructions
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### Contribution
+
+1.  Fork the repository
+2.  Create Feat_xxx branch
+3.  Commit your code
+4.  Create Pull Request
+
+
+#### Gitee Feature
+
+1.  You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
+2.  Gitee blog [blog.gitee.com](https://blog.gitee.com)
+3.  Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
+4.  The most valuable open source project [GVP](https://gitee.com/gvp)
+5.  The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
+6.  The most popular members  [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 39 - 0
README.md

@@ -0,0 +1,39 @@
+# 考试答题系统2020年8月25日
+
+#### 介绍
+{**以下是码云平台说明,您可以替换此简介**
+码云是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台
+无论是个人、团队、或是企业,都能够用码云实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
+
+#### 软件架构
+软件架构说明
+
+
+#### 安装教程
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### 使用说明
+
+1.  xxxx
+2.  xxxx
+3.  xxxx
+
+#### 参与贡献
+
+1.  Fork 本仓库
+2.  新建 Feat_xxx 分支
+3.  提交代码
+4.  新建 Pull Request
+
+
+#### 码云特技
+
+1.  使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
+2.  码云官方博客 [blog.gitee.com](https://blog.gitee.com)
+3.  你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解码云上的优秀开源项目
+4.  [GVP](https://gitee.com/gvp) 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目
+5.  码云官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
+6.  码云封面人物是一档用来展示码云会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 352 - 0
common/html-parser.js

@@ -0,0 +1,352 @@
+/*
+ * HTML5 Parser By Sam Blowes
+ *
+ * Designed for HTML5 documents
+ *
+ * Original code by John Resig (ejohn.org)
+ * http://ejohn.org/blog/pure-javascript-html-parser/
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ *
+ * ----------------------------------------------------------------------------
+ * License
+ * ----------------------------------------------------------------------------
+ *
+ * This code is triple licensed using Apache Software License 2.0,
+ * Mozilla Public License or GNU Public License
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.  You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+ * License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is Simple HTML Parser.
+ *
+ * The Initial Developer of the Original Code is Erik Arvidsson.
+ * Portions created by Erik Arvidssson are Copyright (C) 2004. All Rights
+ * Reserved.
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ * ----------------------------------------------------------------------------
+ * Usage
+ * ----------------------------------------------------------------------------
+ *
+ * // Use like so:
+ * HTMLParser(htmlString, {
+ *     start: function(tag, attrs, unary) {},
+ *     end: function(tag) {},
+ *     chars: function(text) {},
+ *     comment: function(text) {}
+ * });
+ *
+ * // or to get an XML string:
+ * HTMLtoXML(htmlString);
+ *
+ * // or to get an XML DOM Document
+ * HTMLtoDOM(htmlString);
+ *
+ * // or to inject into an existing document/DOM node
+ * HTMLtoDOM(htmlString, document);
+ * HTMLtoDOM(htmlString, document.body);
+ *
+ */
+// Regular Expressions for parsing tags and attributes
+var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
+var endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
+var attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; // Empty Elements - HTML 5
+
+var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr'); // Block Elements - HTML 5
+// fixed by xxx 将 ins 标签从块级名单中移除
+
+var block = makeMap('a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video'); // Inline Elements - HTML 5
+
+var inline = makeMap('abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'); // Elements that you can, intentionally, leave open
+// (and which close themselves)
+
+var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); // Attributes that have their values filled in disabled="disabled"
+
+var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'); // Special Elements (can contain anything)
+
+var special = makeMap('script,style');
+function HTMLParser(html, handler) {
+  var index;
+  var chars;
+  var match;
+  var stack = [];
+  var last = html;
+
+  stack.last = function () {
+    return this[this.length - 1];
+  };
+
+  while (html) {
+    chars = true; // Make sure we're not in a script or style element
+
+    if (!stack.last() || !special[stack.last()]) {
+      // Comment
+      if (html.indexOf('<!--') == 0) {
+        index = html.indexOf('-->');
+
+        if (index >= 0) {
+          if (handler.comment) {
+            handler.comment(html.substring(4, index));
+          }
+
+          html = html.substring(index + 3);
+          chars = false;
+        } // end tag
+
+      } else if (html.indexOf('</') == 0) {
+        match = html.match(endTag);
+
+        if (match) {
+          html = html.substring(match[0].length);
+          match[0].replace(endTag, parseEndTag);
+          chars = false;
+        } // start tag
+
+      } else if (html.indexOf('<') == 0) {
+        match = html.match(startTag);
+
+        if (match) {
+          html = html.substring(match[0].length);
+          match[0].replace(startTag, parseStartTag);
+          chars = false;
+        }
+      }
+
+      if (chars) {
+        index = html.indexOf('<');
+        var text = index < 0 ? html : html.substring(0, index);
+        html = index < 0 ? '' : html.substring(index);
+
+        if (handler.chars) {
+          handler.chars(text);
+        }
+      }
+    } else {
+      html = html.replace(new RegExp('([\\s\\S]*?)<\/' + stack.last() + '[^>]*>'), function (all, text) {
+        text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, '$1$2');
+
+        if (handler.chars) {
+          handler.chars(text);
+        }
+
+        return '';
+      });
+      parseEndTag('', stack.last());
+    }
+
+    if (html == last) {
+      throw 'Parse Error: ' + html;
+    }
+
+    last = html;
+  } // Clean up any remaining tags
+
+
+  parseEndTag();
+
+  function parseStartTag(tag, tagName, rest, unary) {
+    tagName = tagName.toLowerCase();
+
+    if (block[tagName]) {
+      while (stack.last() && inline[stack.last()]) {
+        parseEndTag('', stack.last());
+      }
+    }
+
+    if (closeSelf[tagName] && stack.last() == tagName) {
+      parseEndTag('', tagName);
+    }
+
+    unary = empty[tagName] || !!unary;
+
+    if (!unary) {
+      stack.push(tagName);
+    }
+
+    if (handler.start) {
+      var attrs = [];
+      rest.replace(attr, function (match, name) {
+        var value = arguments[2] ? arguments[2] : arguments[3] ? arguments[3] : arguments[4] ? arguments[4] : fillAttrs[name] ? name : '';
+        attrs.push({
+          name: name,
+          value: value,
+          escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') // "
+
+        });
+      });
+
+      if (handler.start) {
+        handler.start(tagName, attrs, unary);
+      }
+    }
+  }
+
+  function parseEndTag(tag, tagName) {
+    // If no tag name is provided, clean shop
+    if (!tagName) {
+      var pos = 0;
+    } // Find the closest opened tag of the same type
+    else {
+        for (var pos = stack.length - 1; pos >= 0; pos--) {
+          if (stack[pos] == tagName) {
+            break;
+          }
+        }
+      }
+
+    if (pos >= 0) {
+      // Close all the open elements, up the stack
+      for (var i = stack.length - 1; i >= pos; i--) {
+        if (handler.end) {
+          handler.end(stack[i]);
+        }
+      } // Remove the open elements from the stack
+
+
+      stack.length = pos;
+    }
+  }
+}
+
+function makeMap(str) {
+  var obj = {};
+  var items = str.split(',');
+
+  for (var i = 0; i < items.length; i++) {
+    obj[items[i]] = true;
+  }
+
+  return obj;
+}
+
+function removeDOCTYPE(html) {
+  return html.replace(/<\?xml.*\?>\n/, '').replace(/<!doctype.*>\n/, '').replace(/<!DOCTYPE.*>\n/, '');
+}
+
+function parseAttrs(attrs) {
+  return attrs.reduce(function (pre, attr) {
+    var value = attr.value;
+    var name = attr.name;
+
+    if (pre[name]) {
+			pre[name] = pre[name] + " " + value;
+    } else {
+			pre[name] = value;
+    }
+
+    return pre;
+  }, {});
+}
+
+function parseHtml(html) {
+  html = removeDOCTYPE(html);
+  var stacks = [];
+  var results = {
+    node: 'root',
+    children: []
+  };
+  HTMLParser(html, {
+    start: function start(tag, attrs, unary) {
+      var node = {
+        name: tag
+      };
+
+      if (attrs.length !== 0) {
+        node.attrs = parseAttrs(attrs);
+      }
+
+      if (unary) {
+        var parent = stacks[0] || results;
+
+        if (!parent.children) {
+          parent.children = [];
+        }
+
+        parent.children.push(node);
+      } else {
+        stacks.unshift(node);
+      }
+    },
+    end: function end(tag) {
+      var node = stacks.shift();
+      if (node.name !== tag) console.error('invalid state: mismatch end tag');
+
+      if (stacks.length === 0) {
+        results.children.push(node);
+      } else {
+        var parent = stacks[0];
+
+        if (!parent.children) {
+          parent.children = [];
+        }
+
+        parent.children.push(node);
+      }
+    },
+    chars: function chars(text) {
+      var node = {
+        type: 'text',
+        text: text
+      };
+
+      if (stacks.length === 0) {
+        results.children.push(node);
+      } else {
+        var parent = stacks[0];
+
+        if (!parent.children) {
+          parent.children = [];
+        }
+
+        parent.children.push(node);
+      }
+    },
+    comment: function comment(text) {
+      var node = {
+        node: 'comment',
+        text: text
+      };
+      var parent = stacks[0];
+
+      if (!parent.children) {
+        parent.children = [];
+      }
+
+      parent.children.push(node);
+    }
+  });
+  return results.children;
+}
+
+export default parseHtml;

+ 176 - 0
components/an-notice-bar/an-notice-bar.vue

@@ -0,0 +1,176 @@
+<template>
+	<view>
+		<view class="an-notice-box">
+			<view class="an-notice-l">
+				<view class="iconfont icon-tongzhi"></view>
+			</view>
+			<scroll-view class="an-notice-content">
+				<swiper v-if="show" class="swiper" :autoplay="true" :interval="switchTime*1000" :duration="1500"
+					:circular="true" :vertical="true">
+					<swiper-item v-for="(item, index) in list" :key="index" class="an-notice-content-item">
+						<view class="swiper-item">
+							<view class="an-notice-content-item-text" :style="'color: '+color+';'">
+								<view style="width: 85%;" @tap="gotoDetail(item)">{{item.title}}</view>
+							</view>
+						</view>
+					</swiper-item>
+				</swiper>
+			</scroll-view>
+			<view class="an-notice-more" @tap="$openrul('/pages/article/notice')">
+				<view class="iconfont icon-arrow" style="font-size: 12px;"></view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import uniIcons from '../uni-icon/uni-icon.vue'
+	export default {
+		components: {
+			uniIcons
+		},
+		props: {
+			message_list: {},
+			color: {
+				type: String,
+				default: '#000'
+			},
+			bgColor: {
+				type: String,
+				default: '#fff'
+			},
+			switchTime: {
+				type: Number,
+				default: 3
+			},
+			showSerial: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				number: 0,
+				list: this.message_list,
+				copyText: '',
+				show: '',
+			};
+		},
+		mounted() {
+			this.show = true;
+		},
+		watch: {
+			message_list() {
+				this.list = this.message_list
+			}
+		},
+		methods: {
+			more() {
+				this.show = false;
+				this.$emit('more')
+			},
+			gotoDetail(item) {
+				if (item.url == '') {
+					uni.navigateTo({
+						url: '/pages/article/detail?id=' + item.id + '&type=2'
+					})
+				} else {
+					uni.navigateTo({
+						url: '/pages/webview/webview?url=' + item.url + '&title=' + item.title
+					})
+				}
+			},
+		}
+	}
+</script>
+
+<style>
+	.swiper {
+		height: 100%;
+	}
+
+	.an-notice-box {
+		overflow: hidden;
+		display: flex;
+		justify-content: flex-start;
+	}
+
+	.an-notice-box .an-notice-l {
+		display: flex;
+		align-items: center;
+	}
+
+	.an-notice-box .icon-tongzhi {
+		color: #ffc001;
+		font-size: 16px;
+	}
+
+	.an-notice-box .an-notice-l-title {
+		font-size: 17px;
+		color: #fdd100;
+		font-weight: bold;
+		font-style: italic;
+		margin-bottom: 2px;
+	}
+
+	.an-notice-icon {
+		width: 60upx;
+		height: 60upx;
+		line-height: 50upx;
+		text-align: center;
+		position: relative;
+	}
+
+	.an-notice-content {
+		width: calc(100% - 220upx);
+		position: relative;
+		font-size: 15px;
+		padding-left: 6px;
+		flex: 1;
+	}
+
+	.an-notice-content-item {
+		width: 100%;
+		text-align: left;
+	}
+
+	.an-notice-content-item-text {
+		width: 96%;
+		display: block;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		overflow: hidden;
+		font-size: 15px;
+	}
+
+	.an-notice-more {
+		font-size: 13px;
+		/* color: #b7b7b7; */
+		text-align: center;
+		display: flex;
+		align-items: center;
+	}
+
+	.an-notice-more-l {
+		text-decoration: underline;
+		margin-right: 3px;
+	}
+
+	@keyframes anotice {
+		0% {
+			transform: translateY(100%);
+		}
+
+		30% {
+			transform: translateY(0);
+		}
+
+		70% {
+			transform: translateY(0);
+		}
+
+		100% {
+			transform: translateY(-100%);
+		}
+	}
+</style>

+ 190 - 0
components/authVip.vue

@@ -0,0 +1,190 @@
+<template>
+	<view>
+		<view class="lz-modal lz-modal-show">
+			<view class="lz-modal-dialog">
+				<view class="lz-list">
+					<view class="lz-list-header" style="display: flex;flex-direction: row;">
+						<view>温馨提示</view>
+						<view class="iconfont icon-cuo1" @tap="hideAuthVip()"></view>
+					</view>
+					<view class="lz-list-body">
+						<view class="lz-list-item lz-list-item-middle">
+							<view class="lz-list-line">
+								<view class="ktvip">
+									<image src="../static/img/zs.png" mode="" class="ktvip-img"></image>
+									<view class="ktvip-flex"><text>请激活VIP</text>(当前科目:{{subject.subject_name}})</view>
+								</view>
+								<view class="index-bm-title">
+									<input class="uni-input" v-model="vipnum" confirm-type="search"
+										placeholder="请输入激活码" />
+								</view>
+								<view class="ktvip-b">
+									<!-- #ifdef H5 || APP-PLUS -->
+									<button @tap="$openrul('/pages/my/about?id=1')">联系客服</button>
+									<!-- #endif -->
+									<!-- #ifdef MP-WEIXIN -->
+									<button open-type="contact">联系客服</button>
+									<!-- #endif -->
+									<button @tap="authVip()" class="ktvip-b-r">确认激活</button>
+								</view>
+							</view>
+						</view>
+					</view>
+				</view>
+				<view class="lz-margin--b-md"></view>
+			</view>
+		</view>
+
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex';
+	export default {
+		name: "auth-vip",
+		data() {
+			return {
+				vipnum: '',
+				mode: '',
+			}
+		},
+		computed: {
+			...mapState(['subject', 'userinfo', 'subjectVip']),
+		},
+		mounted() {
+			this.mode = this.$myConfig.mode
+		},
+		methods: {
+			hideAuthVip() {
+				this.$emit('hideAuthVip', false)
+			},
+			async authVip() {
+				let res = await this.$myHttp.post({
+					url: this.$myHttp.urlMap.exchangesubject,
+					data: {
+						code: this.vipnum,
+						subject_id: this.subject.id
+					},
+					needLogin: true
+				})
+				if (res.code == 1) {
+					uni.showToast({
+						title: '激活成功',
+						icon: 'none',
+						duration: 1000
+					})
+					this.$myUserLogin.getSubvip(this.subject.id)
+					this.hideAuthVip()
+				} else {
+					uni.showToast({
+						title: res.msg,
+						icon: 'none',
+						duration: 1000
+					})
+				}
+			},
+			//获取获取解密手机号
+			//  async userDecryptData(encryptedData,iv,code) {
+			//   let res = await this.$myHttp.post({
+			//    url: this.$myHttp.urlMap.decryptData,
+			//    data: {
+			//     encryptedData:encryptedData,
+			//     iv:iv,
+			//     code:code,
+			//    },
+			//    needLogin: true
+			//   })
+			//   if (res.code == 1) {
+			//    let jmPnumber = res.data
+			//    let myPhone = jmPnumber.phoneNumber
+			// console.log(myPhone);
+			//   }
+			//  },
+
+
+			//  getPhoneNumber(e) {
+			//   let enc = e.detail.encryptedData
+			//    let iv = e.detail.iv
+			//   this.userDecryptData(enc,iv,this.cod)
+			//  },
+		}
+	}
+</script>
+
+<style>
+	.ktvip {
+		color: #848484;
+		font-size: 15px;
+	}
+
+	.ktvip-flex {
+		width: 84%;
+		margin: 0px auto;
+	}
+
+	.ktvip-flex text {
+		color: #000;
+		font-weight: bold;
+	}
+
+	.ktvip-img {
+		width: 68px;
+		height: 68px;
+		margin-bottom: 3px;
+	}
+
+	.ktvip-b {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 14px;
+		margin: 14px 0;
+	}
+
+	.ktvip-b button {
+		font-size: 14px;
+		margin: 0 8px;
+		border: solid 1px #ebebeb;
+	}
+
+	.ktvip-b-r {
+		font-size: 14px;
+		background: #3c7bfc;
+		border: solid 1px #3c7bfc !important;
+		color: #fff;
+	}
+
+	.ktvip-b-r button::after,
+	.ktvip-b button::after {
+		border: none;
+	}
+
+	.index-bm-title {
+		width: 86%;
+		margin: 12px auto 10px;
+	}
+
+	.index-bm-content {
+		margin-top: 10px;
+	}
+
+	.index-bm-flex {
+		display: flex;
+		font-size: 15px;
+		align-items: center;
+		margin-bottom: 8px;
+	}
+
+	.index-bm-title input {
+		border-radius: 5px;
+		border: none;
+		padding: 10px;
+		flex: 1;
+		font-size: 15px;
+		color: #000;
+		text-align: left;
+		border: 1px #eee solid;
+	}
+</style>

+ 8 - 0
components/circle-progress-bar/changelog.md

@@ -0,0 +1,8 @@
+## 1.1.2(2022-11-18)
+修复了一些bug
+## 1.1.1(2022-10-26)
+添加了颜色透明
+添加了自定义单位
+修复了中心间隔线
+修复了动画流畅度
+取消了进度条首尾圆角

+ 286 - 0
components/circle-progress-bar/circle-progress-bar.vue

@@ -0,0 +1,286 @@
+<template>
+	<view class="circle-progress-bar" 
+	:style="{
+		width: sunit(size),
+		height: sunit(size),
+	}">
+		<view class="circle" :change:prop="animateModule.pro" :prop="cpro"
+		:data-animate="animate"
+		:style="{
+			transform: `rotate(${start * 360 + 45}deg)`,
+			border: `${sunit(border_width)} solid ${border_color}`,
+		}">
+		</view>
+		<view class="bg" v-if="background"
+		:style="{
+			background: background,
+		}"></view>
+		<view class="border-back" v-if="border_back_color"
+		:style="{
+			border: `${sunit(border_width)} solid ${border_back_color}`
+		}"></view>
+		<view class="center">
+			<slot :pro="cpro"></slot>
+		</view>
+	</view>
+</template>
+
+<script module="animateModule" lang="wxs">
+	var Timing = {
+		easeIn: function easeIn(pos) {
+			return Math.pow(pos, 3);
+		},
+		easeOut: function easeOut(pos) {
+			return Math.pow(pos - 1, 3) + 1;
+		},
+		easeInOut: function easeInOut(pos) {
+			if ((pos /= 0.5) < 1) {
+				return 0.5 * Math.pow(pos, 3);
+			} else {
+				return 0.5 * (Math.pow(pos - 2, 3) + 2);
+			}
+		},
+		linear: function linear(pos) {
+			return pos;
+		}
+	};
+
+	//#ifdef MP
+	function setTimeout(t, cb, d) {
+		if (d > 0) {
+			var s = getDate().getTime();
+			var fn = function () {
+				if (getDate().getTime() - s > d) {
+					cb && cb();
+				} else
+					t.requestAnimationFrame(fn);
+			}
+			fn();
+		}
+		else
+			cb && cb();
+	}
+	//#endif
+
+	function Animation(opts) {
+		opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration;
+		opts.timing = opts.timing || 'linear';
+		var delay = 17;
+
+		function createAnimationFrame() {
+			if (typeof setTimeout !== 'undefined') {
+				return function(step, delay) {
+					//#ifndef MP
+					setTimeout(function() {
+						var timeStamp = +new Date();
+						step(timeStamp);
+					}, delay);
+					//#endif
+					//#ifdef MP
+					setTimeout(opts.instance, function () {
+						var timeStamp = getDate()
+						step(timeStamp);
+					}, delay)
+					//#endif
+				};
+			} else if (typeof requestAnimationFrame !== 'undefined') {
+				return requestAnimationFrame;
+			} else {
+				return function(step) {
+					step(null);
+				};
+			}
+		};
+		var animationFrame = createAnimationFrame();
+		var startTimeStamp = null;
+		var _step = function step(timestamp) {
+			if (timestamp === null) {
+				opts.onProcess && opts.onProcess(1);
+				opts.onAnimationFinish && opts.onAnimationFinish();
+				return;
+			}
+			if (startTimeStamp === null) {
+				startTimeStamp = timestamp;
+			}
+			if (timestamp - startTimeStamp < opts.duration) {
+				var process = (timestamp - startTimeStamp) / opts.duration;
+				var timingFunction = Timing[opts.timing];
+				process = timingFunction(process);
+
+				opts.onProcess && opts.onProcess(process);
+				animationFrame(_step, delay);
+			} else {
+				opts.onProcess && opts.onProcess(1);
+				opts.onAnimationFinish && opts.onAnimationFinish();
+			}
+		};
+		animationFrame(_step, delay);
+	}
+
+	function getPath(deg) {
+		var path = '50% 50%'
+		//各个锚点
+		var ps = ['0% 0%', '100% 0%', '100% 100%', '0% 100%']
+		var ps1 = path + ',' + ps[0]
+		var ps2 = ps1 + ',' + ps[1]
+		var ps3 = ps2 + ',' + ps[2]
+		var ps4 = ps3 + ',' + ps[3]
+		var ops = [
+			function(per) { return ps1 + ',' + (per + '% 0%') },
+			function(per) { return ps2 + ',' + ('100% ' + per + '%') },
+			function(per) { return ps3 + ',' + (100 - per) + '% 100%' },
+			function(per) { return ps4 + ',' + '0% ' + (100 - per) + '%' },
+		]
+		if (deg == 0) {
+			return 'polygon(50% 50%, 50% 0%)'
+		}
+		else if (deg % 360 == 0) {
+			return ''
+		}
+		var idx = parseInt(deg / 90) % 4
+		var pdeg = deg % 90
+		var per = pdeg / 90 * 100
+		if(ops[idx]) {
+			return 'polygon(' + ops[idx](per) + ')'
+		}
+		else {
+			return ''
+		}
+	}
+
+	function setDeg(newValue, oldValue, ownerInstance, instance) {
+		var odeg = oldValue * 360
+		var deg = newValue * 360
+		var offset = deg - odeg
+		
+		var ds = instance.getDataset()
+		if(!ds.animate) {
+			var path = getPath(deg)
+			instance.setStyle({
+				'clip-path': path,
+			})
+			return
+		}
+		Animation({
+			instance: ownerInstance,
+			timing: 'easeInOut',
+			duration: 300,
+			onProcess: function onProcess(process) {
+				var pdeg = odeg + process * offset
+				var path = getPath(pdeg)
+				var com = ownerInstance.selectComponent('.circle');
+				com.setStyle({
+					'clip-path': path,
+				})
+			},
+			onAnimationFinish: function onAnimationFinish() {}
+		});
+	}
+	module.exports = {
+		pro: setDeg,
+	}
+</script>
+
+<script>
+	export default {
+		props: {
+			pro: {
+				type: Number,
+				default: 0
+			},
+			//起始位置 0-1
+			start: {
+				type: Number,
+				default: 0,
+			},
+			//圆形大小
+			size: {
+				type: Number,
+				default: 200
+			},
+			//线宽度
+			border_width: {
+				type: Number,
+				default: 20
+			},
+			//线颜色
+			border_color: {
+				type: String,
+				default: '#07C160',
+			},
+			//线背景色
+			border_back_color: {
+				type: String,
+			},
+			//中心内容背景色
+			background: {
+				type: String,
+			},
+			//单位
+			unit: {
+				type: String,
+				default: 'px',
+			},
+			//是否启用动画
+			animate:{
+				type: Boolean,
+				default: true,
+			}
+		},
+		data() {
+			return {
+				cpro: 0,
+			}
+		},
+		watch: {
+			pro(val) {
+				this.cpro = val
+			}
+		},
+		mounted() {
+			this.cpro = this.pro
+		},
+		methods: {
+			sunit(num) {
+				if(typeof num === 'number') {
+					return num + this.unit
+				}
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	.circle-progress-bar{
+		position: relative;
+	}
+	.circle, .bg, .border-back {
+		height: 100%;
+		width: 100%;
+		border-radius: 50%;
+		position: absolute;
+		box-sizing: border-box;
+	}
+	.circle{
+		z-index: 1;
+	}
+	.border-back{
+		height: 100%;
+		width: 100%;
+		left: 50%;
+		top: 50%;
+		transform: translate(-50%, -50%);
+	}
+	.point {
+		position: absolute;
+		border-radius: 50%;
+		z-index: 1;
+	}
+	.center{
+		position: absolute;
+		left: 50%;
+		top: 50%;
+		transform: translate(-50%, -50%);
+		z-index: 2;
+	}
+</style>

+ 85 - 0
components/circle-progress-bar/package.json

@@ -0,0 +1,85 @@
+{
+  "id": "circle-progress-bar",
+  "displayName": "圆形进度条(纯CSS) ",
+  "version": "1.1.2",
+  "description": "圆形进度条(纯CSS) ",
+  "keywords": [
+    "进度条",
+    "圆形进度条",
+    "circle-progress-bar"
+],
+  "repository": "",
+  "engines": {
+    "HBuilderX": "^3.6.7"
+  },
+  "dcloudext": {
+    "type": "component-vue",
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "插件不采集任何数据",
+      "permissions": "无"
+    },
+    "npmurl": ""
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "Vue": {
+          "vue2": "y",
+          "vue3": "y"
+        },
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "n"
+        },
+        "H5-mobile": {
+          "Safari": "u",
+          "Android Browser": "u",
+          "微信浏览器(Android)": "u",
+          "QQ浏览器(Android)": "u"
+        },
+        "H5-pc": {
+          "Chrome": "u",
+          "IE": "u",
+          "Edge": "u",
+          "Firefox": "u",
+          "Safari": "u"
+        },
+        "小程序": {
+          "微信": {
+            "minVersion": "2.6.6"
+        },
+          "阿里": "u",
+          "百度": "u",
+          "字节跳动": "u",
+          "QQ": "u",
+          "钉钉": "u",
+          "快手": "u",
+          "飞书": "u",
+          "京东": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        }
+      }
+    }
+  }
+}

+ 28 - 0
components/circle-progress-bar/readme.md

@@ -0,0 +1,28 @@
+# circle-progress-bar
+### 使用说明
+
+---
+
+注册组件后直接使用,在 `<!-- content -->` 处写中心内容,比如进度百分比。
+
+```html
+<circle-progress-bar :pro="pro">
+	<!-- content -->
+</circle-progress-bar>
+```
+
+### 参数说明
+
+---
+
+| 参数              | 类型    | 默认值  | 描述                |
+| ----------------- | ------- | ------- | -------------------|
+| pro               | Number  | 0       | 进度百分比 0-1      |
+| start             | Number  | 0       | 起始角度 0-1        |
+| size              | Number  | 100     | 组件大小(单位rpx)   |
+| border_width      | Number  | 5       | 进度条宽度(单位rpx) |
+| border_color      | String  | #07C160 | 进度条颜色          |
+| border_back_color | String  | #DDD    | 进度条背景色        |
+| background        | String  | #FFF    | 中心内容背景色      |
+| unit        		| String  | rpx    	| 大小单位			 |
+| animate           | Boolean | true    | 是否启用动画        |

+ 155 - 0
components/if-continue/index.vue

@@ -0,0 +1,155 @@
+<template>
+	<view>
+		<view class="tan">
+			<view class="tan-mask" @tap="tap_handler(0)"></view>
+			<view class="tan-modal">
+				<view class="tan-content">
+					<view class="exam-tan-content">
+						<view>您上次答到第</view>
+						<view class="exam-tan-content-title">{{timu_order-1}}</view>
+						<view>道题</view>
+					</view>
+					<view class="exam-tan-button">
+						<view class="exam-tan-button-r" @tap="tap_handler(4)">重新答题</view>
+						<view class="exam-tan-button-l" @tap="tap_handler(3)">继续上次</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				timu_order: this.timuorder
+			}
+		},
+		props: {
+			timuorder: 0
+		},
+		/*    timuinfo: {
+		        type: Object,
+		        default: {
+		            library_name: '',
+		            total_num: 0,
+		            zhengquelv: 0,
+		            right_num: 0,
+		            error_num: 0,
+		            yida_num:0
+		        }
+		    },*/
+		computed: {},
+		methods: {
+			tap_handler(type) {
+				this.$emit('tap_handler', type)
+			}
+		}
+	}
+</script>
+
+<style>
+	/*弹窗*/
+	.tan-mask {
+		position: fixed;
+		z-index: 999;
+		top: 0;
+		right: 0;
+		left: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0.6);
+	}
+
+	.tan-modal {
+		position: fixed;
+		z-index: 999;
+		width: 80%;
+		max-width: 300px;
+		top: 50%;
+		left: 50%;
+		-webkit-transform: translate(-50%, -50%);
+		transform: translate(-50%, -50%);
+		background-color: #fff;
+		text-align: center;
+		border-radius: 3px;
+		overflow: hidden;
+	}
+
+	.tan-title {
+		padding: 18px 0;
+		border-bottom: solid 1px #f4f4f4;
+	}
+
+	.tan-title view {
+		font-weight: 400;
+		font-size: 17px;
+		word-wrap: break-word;
+		word-break: break-all;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		display: -webkit-box;
+		-webkit-line-clamp: 2;
+		-webkit-box-orient: vertical;
+	}
+
+	.tan-content {
+		width: 90%;
+		margin: 0 auto;
+		font-size: 14px;
+	}
+
+	.exam-tan-content {
+		margin-top: 24px;
+		font-size: 16px;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		line-height: 25px;
+	}
+
+	.exam-tan-content-title {
+		color: #ff7412;
+		font-weight: bold
+	}
+
+	.exam-tan-content-flex {
+		display: flex;
+		width: 91%;
+		margin: 0 auto;
+		padding: 13px 0;
+		border-bottom: solid 1px #f4f4f4;
+	}
+
+	.exam-tan-content-flex span {
+		color: #767676;
+		width: 42%;
+		text-align: left;
+	}
+
+	.exam-tan-button {
+		color: #fff;
+		padding: 12px 0;
+		margin: 10px 0;
+		border-radius: 4px;
+		font-size: 16px;
+		display: flex;
+		justify-content: space-between;
+		width: 100%;
+	}
+
+	.exam-tan-button-l {
+		background: #ffd947;
+		color: #000;
+		width: 48%;
+		padding: 8px 0;
+		border-radius: 2px;
+	}
+
+	.exam-tan-button-r {
+		background: #000;
+		width: 48%;
+		padding: 8px 0;
+		border-radius: 2px;
+	}
+</style>

+ 377 - 0
components/m-icon/m-icon.css

@@ -0,0 +1,377 @@
+@font-face {
+	font-family: uniicons;
+	font-weight: normal;
+	font-style: normal;
+	src: url('@/static/font/uni.ttf') format('truetype');
+}
+
+.m-icon {
+	font-family: uniicons;
+	font-size: 48upx;
+	font-weight: normal;
+	font-style: normal;
+	line-height: 1;
+	display: inline-block;
+	text-decoration: none;
+	-webkit-font-smoothing: antialiased;
+}
+
+.m-icon.uni-active {
+	color: #007aff;
+}
+
+.m-icon-contact:before {
+	content: '\e100';
+}
+
+.m-icon-person:before {
+	content: '\e101';
+}
+
+.m-icon-personadd:before {
+	content: '\e102';
+}
+
+.m-icon-contact-filled:before {
+	content: '\e130';
+}
+
+.m-icon-person-filled:before {
+	content: '\e131';
+}
+
+.m-icon-personadd-filled:before {
+	content: '\e132';
+}
+
+.m-icon-phone:before {
+	content: '\e200';
+}
+
+.m-icon-email:before {
+	content: '\e201';
+}
+
+.m-icon-chatbubble:before {
+	content: '\e202';
+}
+
+.m-icon-chatboxes:before {
+	content: '\e203';
+}
+
+.m-icon-phone-filled:before {
+	content: '\e230';
+}
+
+.m-icon-email-filled:before {
+	content: '\e231';
+}
+
+.m-icon-chatbubble-filled:before {
+	content: '\e232';
+}
+
+.m-icon-chatboxes-filled:before {
+	content: '\e233';
+}
+
+.m-icon-weibo:before {
+	content: '\e260';
+}
+
+.m-icon-weixin:before {
+	content: '\e261';
+}
+
+.m-icon-pengyouquan:before {
+	content: '\e262';
+}
+
+.m-icon-chat:before {
+	content: '\e263';
+}
+
+.m-icon-qq:before {
+	content: '\e264';
+}
+
+.m-icon-videocam:before {
+	content: '\e300';
+}
+
+.m-icon-camera:before {
+	content: '\e301';
+}
+
+.m-icon-mic:before {
+	content: '\e302';
+}
+
+.m-icon-location:before {
+	content: '\e303';
+}
+
+.m-icon-mic-filled:before,
+.m-icon-speech:before {
+	content: '\e332';
+}
+
+.m-icon-location-filled:before {
+	content: '\e333';
+}
+
+.m-icon-micoff:before {
+	content: '\e360';
+}
+
+.m-icon-image:before {
+	content: '\e363';
+}
+
+.m-icon-map:before {
+	content: '\e364';
+}
+
+.m-icon-compose:before {
+	content: '\e400';
+}
+
+.m-icon-trash:before {
+	content: '\e401';
+}
+
+.m-icon-upload:before {
+	content: '\e402';
+}
+
+.m-icon-download:before {
+	content: '\e403';
+}
+
+.m-icon-close:before {
+	content: '\e404';
+}
+
+.m-icon-redo:before {
+	content: '\e405';
+}
+
+.m-icon-undo:before {
+	content: '\e406';
+}
+
+.m-icon-refresh:before {
+	content: '\e407';
+}
+
+.m-icon-star:before {
+	content: '\e408';
+}
+
+.m-icon-plus:before {
+	content: '\e409';
+}
+
+.m-icon-minus:before {
+	content: '\e410';
+}
+
+.m-icon-circle:before,
+.m-icon-checkbox:before {
+	content: '\e411';
+}
+
+.m-icon-close-filled:before,
+.m-icon-clear:before {
+	content: '\e434';
+}
+
+.m-icon-refresh-filled:before {
+	content: '\e437';
+}
+
+.m-icon-star-filled:before {
+	content: '\e438';
+}
+
+.m-icon-plus-filled:before {
+	content: '\e439';
+}
+
+.m-icon-minus-filled:before {
+	content: '\e440';
+}
+
+.m-icon-circle-filled:before {
+	content: '\e441';
+}
+
+.m-icon-checkbox-filled:before {
+	content: '\e442';
+}
+
+.m-icon-closeempty:before {
+	content: '\e460';
+}
+
+.m-icon-refreshempty:before {
+	content: '\e461';
+}
+
+.m-icon-reload:before {
+	content: '\e462';
+}
+
+.m-icon-starhalf:before {
+	content: '\e463';
+}
+
+.m-icon-spinner:before {
+	content: '\e464';
+}
+
+.m-icon-spinner-cycle:before {
+	content: '\e465';
+}
+
+.m-icon-search:before {
+	content: '\e466';
+}
+
+.m-icon-plusempty:before {
+	content: '\e468';
+}
+
+.m-icon-forward:before {
+	content: '\e470';
+}
+
+.m-icon-back:before,
+.m-icon-left-nav:before {
+	content: '\e471';
+}
+
+.m-icon-checkmarkempty:before {
+	content: '\e472';
+}
+
+.m-icon-home:before {
+	content: '\e500';
+}
+
+.m-icon-navigate:before {
+	content: '\e501';
+}
+
+.m-icon-gear:before {
+	content: '\e502';
+}
+
+.m-icon-paperplane:before {
+	content: '\e503';
+}
+
+.m-icon-info:before {
+	content: '\e504';
+}
+
+.m-icon-help:before {
+	content: '\e505';
+}
+
+.m-icon-locked:before {
+	content: '\e506';
+}
+
+.m-icon-more:before {
+	content: '\e507';
+}
+
+.m-icon-flag:before {
+	content: '\e508';
+}
+
+.m-icon-home-filled:before {
+	content: '\e530';
+}
+
+.m-icon-gear-filled:before {
+	content: '\e532';
+}
+
+.m-icon-info-filled:before {
+	content: '\e534';
+}
+
+.m-icon-help-filled:before {
+	content: '\e535';
+}
+
+.m-icon-more-filled:before {
+	content: '\e537';
+}
+
+.m-icon-settings:before {
+	content: '\e560';
+}
+
+.m-icon-list:before {
+	content: '\e562';
+}
+
+.m-icon-bars:before {
+	content: '\e563';
+}
+
+.m-icon-loop:before {
+	content: '\e565';
+}
+
+.m-icon-paperclip:before {
+	content: '\e567';
+}
+
+.m-icon-eye:before {
+	content: '\e568';
+}
+
+.m-icon-arrowup:before {
+	content: '\e580';
+}
+
+.m-icon-arrowdown:before {
+	content: '\e581';
+}
+
+.m-icon-arrowleft:before {
+	content: '\e582';
+}
+
+.m-icon-arrowright:before {
+	content: '\e583';
+}
+
+.m-icon-arrowthinup:before {
+	content: '\e584';
+}
+
+.m-icon-arrowthindown:before {
+	content: '\e585';
+}
+
+.m-icon-arrowthinleft:before {
+	content: '\e586';
+}
+
+.m-icon-arrowthinright:before {
+	content: '\e587';
+}
+
+.m-icon-pulldown:before {
+	content: '\e588';
+}
+
+.m-icon-scan:before {
+    content: "\e612";
+}

+ 41 - 0
components/m-icon/m-icon.vue

@@ -0,0 +1,41 @@
+<template>
+	<view class="m-icon" :class="['m-icon-'+type]" :style="{color:color,'font-size':fontSize}" @click="onClick()"></view>
+</template>
+
+<script>
+	export default {
+		props: {
+			/**
+			 * 图标类型
+			 */
+			type: String,
+			/**
+			 * 图标颜色
+			 */
+			color: String,
+			/**
+			 * 图标大小
+			 */
+			size: {
+				type: [Number, String],
+				default: 24
+			}
+		},
+		computed: {
+			fontSize() {
+				var size = Number(this.size)
+				size = isNaN(size) ? 24 : size
+				return `${size}px`
+			}
+		},
+		methods: {
+			onClick() {
+				this.$emit('click')
+			}
+		}
+	}
+</script>
+
+<style>
+	@import "./m-icon.css";
+</style>

+ 131 - 0
components/m-input.vue

@@ -0,0 +1,131 @@
+<template>
+	<view class="m-input-view">
+		<input :focus="focus_" :type="inputType" :value="value" @input="onInput" class="m-input-input" :placeholder="placeholder"
+		 :password="type==='password'&&!showPassword" @focus="onFocus" @blur="onBlur" />
+		<!-- 优先显示密码可见按钮 -->
+		<view v-if="clearable_&&!displayable_&&value.length" class="m-input-icon">
+			<m-icon color="#666666" type="clear" size="20" @click="clear"></m-icon>
+		</view>
+		<view v-if="displayable_" class="m-input-icon">
+			<m-icon :color="showPassword?'#666666':'#cccccc'" type="eye" size="20" @click="display"></m-icon>
+		</view>
+	</view>
+</template>
+
+<script>
+	import mIcon from './m-icon/m-icon.vue'
+
+	export default {
+		components: {
+			mIcon
+		},
+		props: {
+			/**
+			 * 输入类型
+			 */
+			type: String,
+			/**
+			 * 值
+			 */
+			value: String,
+			/**
+			 * 占位符
+			 */
+			placeholder: String,
+			/**
+			 * 是否显示清除按钮
+			 */
+			clearable: {
+				type: [Boolean, String],
+				default: false
+			},
+			/**
+			 * 是否显示密码可见按钮
+			 */
+			displayable: {
+				type: [Boolean, String],
+				default: false
+			},
+			/**
+			 * 自动获取焦点
+			 */
+			focus: {
+				type: [Boolean, String],
+				default: false
+			}
+		},
+		model: {
+			prop: 'value',
+			event: 'input'
+		},
+		data() {
+			return {
+				/**
+				 * 显示密码明文
+				 */
+				showPassword: false,
+				/**
+				 * 是否获取焦点
+				 */
+				isFocus: false
+			}
+		},
+		computed: {
+			inputType() {
+				const type = this.type
+				return type === 'password' ? 'text' : type
+			},
+			clearable_() {
+				return String(this.clearable) !== 'false'
+			},
+			displayable_() {
+				return String(this.displayable) !== 'false'
+			},
+			focus_() {
+				return String(this.focus) !== 'false'
+			}
+		},
+		methods: {
+			clear() {
+				this.$emit('input', '')
+			},
+			display() {
+				this.showPassword = !this.showPassword
+			},
+			onFocus() {
+				this.isFocus = true
+			},
+			onBlur() {
+				this.$nextTick(() => {
+					this.isFocus = false
+				})
+			},
+			onInput(e) {
+				this.$emit('input', e.target.value)
+			}
+		}
+	}
+</script>
+
+<style>
+	.m-input-view {
+		display: flex;
+		flex-direction: row;
+		align-items: center;
+		width: 100%;
+		flex: 1;
+		padding: 0 10rpx;
+	}
+
+	.m-input-input {
+		flex: 1;
+		width: 100%;
+		height: 50px;
+		line-height: 50px;
+		font-size: 14px;
+	}
+
+	.m-input-icon {
+		width: 20px;
+	}
+</style>

+ 55 - 0
components/mescroll-uni/components/mescroll-down.css

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

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

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

+ 90 - 0
components/mescroll-uni/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: 100rpx 50rpx;
+	text-align: center;
+}
+
+.mescroll-empty.empty-fixed {
+	z-index: 99;
+	position: absolute; /*transform会使fixed失效,最终会降级为absolute */
+	top: 100rpx;
+	left: 0;
+}
+
+.mescroll-empty .empty-icon {
+	width: 280rpx;
+	height: 280rpx;
+}
+
+.mescroll-empty .empty-tip {
+	margin-top: 20rpx;
+	font-size: 24rpx;
+	color: gray;
+}
+
+.mescroll-empty .empty-btn {
+	display: inline-block;
+	margin-top: 40rpx;
+	min-width: 200rpx;
+	padding: 18rpx;
+	font-size: 28rpx;
+	border: 1rpx solid #e04b28;
+	border-radius: 60rpx;
+	color: #e04b28;
+}
+
+.mescroll-empty .empty-btn:active {
+	opacity: 0.75;
+}
+</style>

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

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

+ 47 - 0
components/mescroll-uni/components/mescroll-up.css

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

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

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

+ 19 - 0
components/mescroll-uni/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);
+	}
+}

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

@@ -0,0 +1,344 @@
+<template>
+	<view 
+	class="mescroll-body mescroll-render-touch" 
+	:class="{'mescorll-sticky': sticky}"
+	:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+	@touchstart="wxsBiz.touchstartEvent" 
+	@touchmove="wxsBiz.touchmoveEvent" 
+	@touchend="wxsBiz.touchendEvent" 
+	@touchcancel="wxsBiz.touchendEvent"
+	:change:prop="wxsBiz.propObserver"
+	:prop="wxsProp"
+	>
+		<!-- 状态栏 -->
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+				<view class="downwarp-content">
+					<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+					<view class="downwarp-tip">{{downText}}</view>
+				</view>
+			</view>
+	
+			<!-- 列表内容 -->
+			<slot></slot>
+
+			<!-- 空布局 -->
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+			<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+			<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+			<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+				<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+				<view v-show="upLoadType===1">
+					<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+					<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+				</view>
+				<!-- 无数据 -->
+				<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+			</view>
+		</view>
+		
+		<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+		<!-- #ifdef H5 -->
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+		<!-- #endif -->
+		
+		<!-- 适配iPhoneX -->
+		<view v-if="safearea" class="mescroll-safearea"></view>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="./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.optDown.textLoading;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num) {
+				if (typeof num === 'string') {
+					if (num.indexOf('px') !== -1) {
+						if (num.indexOf('rpx') !== -1) {
+							// "10rpx"
+							num = num.replace('rpx', '');
+						} else if (num.indexOf('upx') !== -1) {
+							// "10upx"
+							num = num.replace('upx', '');
+						} else {
+							// "10px"
+							return Number(num.replace('px', ''));
+						}
+					} else if (num.indexOf('%') !== -1) {
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace('%', '')) / 100;
+						return this.windowHeight * rate;
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0;
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll);
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
+							if(vm.downLoadType === 4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll);
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+					}
+				}
+			};
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y为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-uni/mescroll-mixins.js

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

+ 33 - 0
components/mescroll-uni/mescroll-uni-option.js

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

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

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

+ 788 - 0
components/mescroll-uni/mescroll-uni.js

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

+ 420 - 0
components/mescroll-uni/mescroll-uni.vue

@@ -0,0 +1,420 @@
+<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">
+			<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.optDown.textLoading;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num){
+				if(typeof num === "string"){
+					if (num.indexOf('px') !== -1) {
+						if(num.indexOf('rpx') !== -1) { // "10rpx"
+							num = num.replace('rpx', '');
+						} else if(num.indexOf('upx') !== -1) { // "10upx"
+							num = num.replace('upx', '');
+						} else { // "10px"
+							return Number(num.replace('px', ''))
+						}
+					}else if (num.indexOf('%') !== -1){
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace("%","")) / 100
+						return this.windowHeight * rate
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0
+			},
+			//注册列表滚动事件,用于下拉刷新和上拉加载
+			scroll(e) {
+				this.mescroll.scroll(e.detail, () => {
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+				})
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll)
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			},
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+			setClientHeight() {
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+					this.isExec = true; // 避免多次获取
+					this.$nextTick(() => { // 确保dom已渲染
+						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组件时,此行不可删)
+					},
+					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>

+ 50 - 0
components/mescroll-uni/mixins/mescroll-comp.js

@@ -0,0 +1,50 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
+ * 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
+ * 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
+ */
+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;

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

@@ -0,0 +1,51 @@
+/**
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
+ */
+const MescrollMoreItemMixin = {
+	// 支付宝小程序不支持props的mixin,需写在具体的页面中
+	// #ifndef MP-ALIPAY || 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: {
+		// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
+			// 自动加载当前tab的数据
+			if(this.i === this.index){
+				this.isInit = true; // 标记为true
+				this.mescroll.triggerDownScroll();
+			}
+		},
+	}
+}
+
+export default MescrollMoreItemMixin;

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

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

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

@@ -0,0 +1,102 @@
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
+const WxsMixin = {
+	data() {
+		return {
+			// 传入wxs视图层的数据 (响应式)
+			wxsProp: {
+				optDown:{}, // 下拉刷新的配置
+				scrollTop:0, // 滚动条的距离
+				bodyHeight:0, // body的高度
+				isDownScrolling:false, // 是否正在下拉刷新中
+				isUpScrolling:false, // 是否正在上拉加载中
+				isScrollBody:true, // 是否为mescroll-body滚动
+				isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 标记调用wxs视图层的方法
+			callProp: {
+				callType: '', // 方法名
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
+			// #ifndef MP-WEIXIN || 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
+			}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的方法 (值改变才触发更新)
+			setTimeout(()=>{
+				if(this.downLoadType === 4 || this.downLoadType === 0){
+					this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+				}
+			},320)
+		}
+		// 初始化wxs的数据
+		this.wxsCall({type: 'setWxsProp'})
+		// #endif
+	}
+}
+
+export default WxsMixin;

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

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

+ 268 - 0
components/mescroll-uni/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
+}

+ 50 - 0
components/myAd.vue

@@ -0,0 +1,50 @@
+<template>
+	<view>
+		<view v-if="id != ''" style="width:100%">
+			<ad :unit-id="id" :ad-type="ad_type" ad-theme="white" grid-count="5"></ad>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		mapState
+	} from 'vuex';
+	
+	export default {
+		name: "myAd",
+		props: {
+			type: {
+				type: String,
+				default: ''
+			}
+		},
+		data(){
+			return {
+				list:[],
+				id:'',
+				ad_type:'banner'
+			}
+		},
+		mounted() {
+			this.getAd();
+		},
+		methods:{
+			async getAd(){
+				let res = await this.$myHttp.post({
+					url:this.$myHttp.urlMap.getAd,
+					data:{},
+					needLogin:false,
+				})
+				if(res.code == 1){
+					this.list = res.data
+					this.id = this.list[this.type-1].advert
+					this.ad_type = this.list[this.type-1].advert_type
+				}
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 154 - 0
components/overtimu/index.vue

@@ -0,0 +1,154 @@
+<template>
+	<view>
+		<view class="tan">
+			<view class="tan-mask" @tap="tap_handler(0)"></view>
+			<view class="tan-modal">
+				<view class="tan-title">
+					<view>是否退出</view>
+				</view>
+				<view class="tan-content">
+					<view class="exam-tan-content">
+						<view class="exam-tan-content-flex">
+							<span>答题数</span>
+							<view>{{timuinfo.yida_num}}题</view>
+						</view>
+						<view class="exam-tan-content-flex">
+							<span>正确率</span>
+							<view>{{timuinfo.zhengquelv}}%</view>
+						</view>
+						<view class="exam-tan-content-flex">
+							<span>正确</span>
+							<view class="pub-green">{{timuinfo.right_num}}</view>
+						</view>
+						<view class="exam-tan-content-flex">
+							<span>错误</span>
+							<view class="pub-red">{{timuinfo.error_num}}</view>
+						</view>
+					</view>
+					<view class="exam-tan-button">
+						<view class="exam-tan-button-l" @tap="tap_handler(0)">继续答题</view>
+						<view class="exam-tan-button-r" @tap="tap_handler(1)">结束答题</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {}
+		},
+		props: {
+			timuinfo: {
+				type: Object,
+				default: {
+					library_name: '',
+					total_num: 0,
+					zhengquelv: 0,
+					right_num: 0,
+					error_num: 0,
+					yida_num: 0
+				}
+			}
+		},
+		computed: {},
+		methods: {
+			tap_handler(type) {
+				this.$emit('tap_handler', type)
+			}
+		}
+	}
+</script>
+
+<style>
+	/*弹窗*/
+	.tan-mask {
+		position: fixed;
+		z-index: 999;
+		top: 0;
+		right: 0;
+		left: 0;
+		bottom: 0;
+		background: rgba(0, 0, 0, 0.6);
+	}
+
+	.tan-modal {
+		position: fixed;
+		z-index: 999;
+		width: 80%;
+		max-width: 300px;
+		top: 50%;
+		left: 50%;
+		-webkit-transform: translate(-50%, -50%);
+		transform: translate(-50%, -50%);
+		background-color: #fff;
+		text-align: center;
+		border-radius: 3px;
+		overflow: hidden;
+	}
+
+	.tan-title {
+		padding: 15px 0;
+		border-bottom: solid 1px #f4f4f4;
+	}
+
+	.tan-title view {
+		font-weight: 400;
+		font-size: 17px;
+		word-wrap: break-word;
+		word-break: break-all;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		display: -webkit-box;
+		-webkit-line-clamp: 2;
+		-webkit-box-orient: vertical;
+	}
+
+	.tan-content {
+		width: 90%;
+		margin: 0 auto;
+		font-size: 14px;
+	}
+
+	.exam-tan-content-flex {
+		display: flex;
+		width: 91%;
+		margin: 0 auto;
+		padding: 13px 0;
+		border-bottom: solid 1px #f4f4f4;
+	}
+
+	.exam-tan-content-flex span {
+		color: #767676;
+		width: 42%;
+		text-align: left;
+	}
+
+	.exam-tan-button {
+		color: #fff;
+		padding: 12px 0;
+		margin: 10px 0;
+		border-radius: 4px;
+		font-size: 16px;
+		display: flex;
+		justify-content: space-between;
+		width: 100%;
+	}
+
+	.exam-tan-button-l {
+		background: #3c7bfc;
+		width: 48%;
+		padding: 8px 0;
+		border-radius: 2px;
+	}
+
+	.exam-tan-button-r {
+		background: #e4e4e4;
+		color: #000;
+		width: 48%;
+		padding: 8px 0;
+		border-radius: 2px;
+	}
+</style>

+ 147 - 0
components/question-answer-sheet-1/question-answer-sheet-1.vue

@@ -0,0 +1,147 @@
+<template>
+	<view>
+		<view class="order-pay-z" @tap="$emit('show_choice_timu_list',false)" style="position: fixed;"></view>
+		<view class="tika" style="position: fixed;">
+			<view class="order-pay1">
+				<view class="questionBankAnswer-dc" v-if="question_type == 'test' && my_res.right_num != undefined">
+					<view class="questionBankAnswer-dc-flex">
+						<view class="iconfont icon-dui"></view>
+						<view>{{ my_res.right_num }}</view>
+					</view>
+					<view class="questionBankAnswer-dc-flex">
+						<view class="iconfont icon-cuo"></view>
+						<view>{{ my_res.error_num }}</view>
+					</view>
+				</view>
+				<view v-else-if="question_type == 'exam'" class="questionBankAnswer-r" style="color: #000;"
+					@tap="jiaojuan1">
+					<view class="iconfont icon-bianjisekuai"></view>
+					<view>交卷</view>
+				</view>
+				<block v-if="show_remove">
+					<question-remove class="questionBankAnswer-bottom" @removeError="removeError"></question-remove>
+				</block>
+				<block v-if="type != '' && show_fav">
+					<question-fav class="questionBankAnswer-bottom" :current_timu="current_timu"
+						@collect_opt="collect_opt" :type="type"></question-fav>
+					<view class="questionBankAnswer-dc">
+					</view>
+				</block>
+				<view v-else></view>
+				<view class="questionBankAnswer-r">
+					<view class="iconfont icon-fenlei"></view>
+					<view class="questionBankAnswer-r-menu">
+						<span>{{ timu_order }}</span>
+						/{{ total_num }}
+					</view>
+				</view>
+			</view>
+			<view class="tika-content">
+				<block v-for="(item, index) in timu_list" :key="index">
+					<!-- 章节练习 -->
+					<block v-if="question_type == 'test'">
+						<view @tap="choice_timu_by_id(index)" class="tika-content-n green-background"
+							v-if="item.state === 2">{{ index+1 }}
+						</view>
+						<view @tap="choice_timu_by_id(index)" class="tika-content-n red-background"
+							v-else-if="item.state === 1">{{ index+1 }}
+						</view>
+						<view class="tika-content-n" v-else-if="item.state === 3" @tap="choice_timu_by_id(index)">
+							{{ index+1 }}
+						</view>
+					</block>
+					<!-- 考试 -->
+					<block v-else>
+						<view @tap="choice_timu_by_id(index)" class="tika-content-n orange-background"
+							v-if="item.state === 1">{{ index + 1 }}
+						</view>
+						<view class="tika-content-n" v-else-if="item.state === 0" @tap="choice_timu_by_id(index)">
+							{{ index + 1 }}
+						</view>
+					</block>
+				</block>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			show_remove: {
+				type: Boolean,
+				default: false,
+			},
+			show_fav: {
+				type: Boolean,
+				default: false,
+			},
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			type: {
+				type: String,
+				default: ''
+			},
+			question_type: {
+				type: String,
+				default: 'test'
+			},
+			my_res: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			timu_list: {
+				type: Array,
+				default: function() {
+					return [];
+				},
+			},
+			timu_order: {
+				type: Number,
+				default: 0,
+			},
+			total_num: {
+				type: Number,
+				default: 0,
+			},
+		},
+		watch: {
+			current_timu: {
+				handler(newVal, oldVal) {
+					// console.log(newVal)
+				},
+				deep: true,
+				immediate: true
+			}
+		},
+		data() {
+			return {
+				show_choice_timu_list: false
+			}
+		},
+		methods: {
+			choice_timu_by_id(index) {
+				this.$emit('choice_timu_by_id', index)
+			},
+			//考试交卷
+			jiaojuan1() {
+				this.$emit('jiaojuan1')
+			},
+			collect_opt() {
+				this.$emit('collect_opt')
+			},
+			removeError() {
+				this.$emit('removeError')
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 22 - 0
components/question-answer-sheet/question-answer-sheet.vue

@@ -0,0 +1,22 @@
+<template>
+	<view>
+		<view class="questionBankAnswer-bottom-flex" @tap="$emit('show_choice_timu_list',true)">
+			<view class="iconfont icon-shiti-tianchong"></view>
+			<view>答题卡</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				show_choice_timu_list: false
+			}
+		},
+		methods: {}
+	}
+</script>
+
+<style>
+</style>

+ 116 - 0
components/question-answer/question-answer.vue

@@ -0,0 +1,116 @@
+<template>
+	<view class="questionBankAnswer-jiexi">
+		<!--如果答对加class=dui-->
+		<view class="questionBankAnswer-jiexi-daan"
+			:class="[answer_type(timu_order-1) == 1  ? 'dui' : 'cuo',current_timu.type == 5 ? 'jianda' : '']">
+			<view class="questionBankAnswer-jiexi-daan-flex">
+				<view class="dui_jd_b">{{current_timu.type == 5 ? '参考答案' : '正确答案'}}</view>
+				<view class="questionBankAnswer-jiexi-daan-flex-dui"
+					v-if="current_timu.right_answer == 1 || current_timu.right_answer == 0">
+					{{ current_timu.right_answer == 1 ? '对' : '错' }}
+				</view>
+				<view v-else-if="current_timu.type != 4" class="questionBankAnswer-jiexi-daan-flex-dui"
+					:style="{'font-size':(current_timu.type > 3 ?'16px':'30px')}">
+					<uc-parse :node="current_timu.right_answer"></uc-parse>
+				</view>
+				<view v-else-if="current_timu.type == 4" class="questionBankAnswer-jiexi-daan-flex-dui"
+					:style="{'font-size':(current_timu.type > 3 ?'16px':'30px')}"
+					v-html="current_timu.right_answer.split('|').join(',')">
+				</view>
+			</view>
+			<view class="questionBankAnswer-jiexi-flex" v-if="myAnswer">
+				<view v-if="current_timu.type != 5" class="questionBankAnswer-jiexi-daan-line"></view>
+				<view class="questionBankAnswer-jiexi-daan-flex">
+					<view>我的答案</view>
+					<view style="word-break: break-all;"
+						:class="[(copy_post_answer_right[current_timu.id] || answer_type(timu_order-1) == 1)?'questionBankAnswer-jiexi-daan-flex-dui':'questionBankAnswer-jiexi-daan-flex-cuo']"
+						:style="{'font-size':(current_timu.type > 3 ?'16px':'30px')}">{{myAnswer}}
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="questionBankAnswer-jiexi-jx" v-if="current_timu.area != '' && current_timu.area != null">
+			<view class="questionBankAnswer-jiexi-jx-title">试题解析</view>
+			<view>
+				<uc-parse :node="current_timu.area"></uc-parse>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			timu_order: {
+				type: Number,
+				default: 0
+			},
+			show_beiti: {
+				type: Boolean,
+				default: true,
+			},
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			timu_list: {
+				type: Array,
+				default: function() {
+					return [];
+				},
+			},
+			copy_post_answer_right: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+		},
+		data() {
+			return {
+				myAnswer: ''
+			}
+		},
+
+		methods: {
+			//我的答案处的背景颜色
+			answer_type(index) {
+				if (this.show_beiti || this.timu_list[index].state == 2) {
+					return 1
+				}
+				return 0
+			},
+			my_answer(copy_user_answer) {
+				let answers = copy_user_answer[this.current_timu.id] || [];
+				if (this.current_timu.type <= 3) {
+					let answer = '';
+					if (answers && answers.length > 0) {
+						for (let i = 0, leng = answers.length; i < leng; i++) {
+							if (answers[i].active) {
+								answer += answers[i].answer_code
+							}
+						}
+					}
+					if (answer === '1') {
+						answer = '对'
+					} else if (answer === '0') {
+						answer = '错'
+					}
+					this.myAnswer = answer
+				} else {
+					if (answers && answers.length == 0) {
+						this.myAnswer = ''
+						// || (this.current_timu.my_answer_arr || []).join(',')
+					} else {
+						this.myAnswer = answers.join(',')
+					}
+				}
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 79 - 0
components/question-fav/question-fav.vue

@@ -0,0 +1,79 @@
+<template>
+	<view class="questionBankAnswer-bottom-flex" :class="{'collect': current_timu.is_fav == 1}" @tap="collect_opt">
+		<view class="iconfont icon-shoucang1"
+			:class="{'': current_timu.is_fav != 1, 'yellow': current_timu.is_fav == 1}"></view>
+		<view>{{ current_timu.is_fav == 1 ? '已收藏' : '收藏'}}</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			type: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+
+			}
+		},
+		watch: {
+			current_timu: {
+				handler(newVal, oldVal) {
+					// console.log(newVal)
+				},
+				deep: true,
+				immediate: true
+			}
+		},
+		methods: {
+			// 收藏
+			async collect_opt() {
+				let url = ''
+				// 1:章节练习  2:历年真题
+				if (this.type == 1) {
+					// 1:章节练习
+					url = this.$myHttp.urlMap.unitshoucangeedit
+				} else if (this.type == 2) {
+					// 2:历年真题
+					url = this.$myHttp.urlMap.zhentishoucangeedit
+				}
+				let res = await this.$myHttp.post({
+					url: url,
+					data: {
+						id: this.current_timu.id
+					},
+					needLogin: true
+				});
+				if (res.code == 1) {
+					if (this.current_timu.is_fav == 1) {
+						this.$emit('collect_opt', 0)
+						// this.$set(this.current_timu, 'is_fav', 0)
+						uni.showToast({
+							icon: 'none',
+							title: '取消成功'
+						});
+					} else {
+						this.$emit('collect_opt', 1)
+						// this.$set(this.current_timu, 'is_fav', 1)
+						uni.showToast({
+							icon: 'none',
+							title: '收藏成功'
+						});
+					}
+				}
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 78 - 0
components/question-jianda/question-jianda.vue

@@ -0,0 +1,78 @@
+<template>
+	<view>
+		<view>
+			<!--====sl修改开始====-->
+			<view class="jdt_style" v-for="(item,index) in current_timu.right_answer_arr" :key="index">
+				<textarea placeholder="请输入答案" :value="current_timu.my_answer_arr[index]"
+					@input="post_answer($event, index)" auto-height />
+			</view>
+			<!--====sl修改结束====-->
+		</view>
+		<view class="tkt_button" :class="is_jianda_sub ? 'duoxuan_checked' : 'duoxuan' "
+			v-if="current_timu.right_answer_arr.length > 0" @tap="post_answer_req()">确定</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			is_jianda_sub: {
+				type: Boolean,
+				default: false,
+			},
+			show_beiti: {
+				type: Boolean,
+				default: true,
+			},
+			copy_user_answer: {
+				type: Array,
+				default: function() {
+					return [];
+				},
+			}
+		},
+		watch: {
+			current_timu: {
+				handler(newVal, oldVal) {},
+				deep: true,
+				immediate: true
+			}
+		},
+		methods: {
+			// 提交答案
+			post_answer(item, index) {
+				if (this.is_jianda_sub) {
+					this.$emit('set_sub_button_style', 'is_jianda_sub', false)
+				}
+				let timu = this.current_timu,
+					my_answer_arr = this.current_timu.my_answer_arr;
+
+				this.$set(my_answer_arr, index, item.detail.value)
+				this.$set(timu, index, 'my_answer_arr', my_answer_arr)
+
+				this.$set(timu, 'show_right_answer', true)
+				if (timu.my_answer_arr.length > 0) {
+					this.$set(timu, 'my_answer', timu.my_answer_arr.join('|'))
+				}
+				this.$emit('set_current_timu', timu)
+				let copy_user_answer_this = this.copy_user_answer;
+				if (copy_user_answer_this[this.current_timu.id]) {
+					copy_user_answer_this[this.current_timu.id] = timu.my_answer_arr
+				}
+				this.$emit('set_copy_user_answer', copy_user_answer_this)
+			},
+			post_answer_req() {
+				this.$emit('post_answer_req', true)
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 203 - 0
components/question-jiucuo/question-jiucuo.vue

@@ -0,0 +1,203 @@
+<template>
+	<view>
+		<view class="questionBankAnswer-bottom-flex" @tap="jiucuo">
+			<view class="iconfont icon-sousuo1" style="font-weight: bold;"></view>
+			<view>纠错</view>
+		</view>
+		<uni-popup ref="popup" type="bottom" radius="12rpx" background-color="#8c8989" color="#5A5B5C" width="90%">
+			<view class="jiucuo">
+			<view class="popup-content">
+				<text class="text">纠错反馈</text>
+				<view class="iconfont icon-cuo1" @tap="hide"></view>
+			</view>
+			<view class="jiucuo-content">
+				<text>为方便我们排查错误,请您详细描述错误,例如:</text>
+				<view class="jiucuo-xuan">
+					<checkbox-group @change="checkboxChange" class="jiucuo-checkbox">
+						<label v-for='(item,index) in list' :key="index">
+							<checkbox :value="item.value" color="#3c7bfc" :checked="item.checked"
+								style="transform:scale(0.6)" />{{item.name}}
+						</label>
+					</checkbox-group>
+				</view>
+				<textarea :value="content" placeholder="其他错误,请详细描述您遇到的问题..." @input="setContent"
+					class="popup-textarea" />
+				<button type="primary" :loading="isSubmit" @tap="submit" class="popup-button">提交</button>
+			</view>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			current_timu: {
+				type: Object,
+				default: {},
+			},
+		},
+		data() {
+			return {
+				isSubmit: false,
+				arr: [],
+				content: '',
+				list: [{
+					value: '1',
+					name: '含有错别字'
+				}, {
+					value: '2',
+					name: '答案不正确'
+				}, {
+					value: '3',
+					name: '答案不完整'
+				}, {
+					value: '4',
+					name: '解析不完整'
+				}, ]
+			}
+		},
+		methods: {
+			async submit() {
+				if (this.content == '' && this.arr.length == 0) return
+				if (this.isSubmit) return
+				this.isSubmit = true
+				let res = await this.$myHttp.post({
+					url: this.$myHttp.urlMap.jiucuo,
+					data: {
+						question_id: this.current_timu.id,
+						type: this.arr.join(',') + (this.arr.length > 0 && this.content != '' ? ',' : '') +
+							this.content
+					},
+					needLogin: true
+				})
+				if (res.code == 1) {
+					this.isSubmit = false
+					uni.showToast({
+						title: '提交成功'
+					})
+					this.$refs.popup.close()
+				} else {
+					uni.showToast({
+						title: res.msg,
+						icon: 'none'
+					})
+					this.isSubmit = false
+				}
+			},
+			show() {
+				this.$refs.popup.open('bottom')
+				this.arr = []
+				this.content = ''
+				var items = this.list;
+				for (var i = 0, lenI = items.length; i < lenI; ++i) {
+					const item = items[i]
+					this.$set(item, 'checked', false)
+				}
+			},
+			hide() {
+				this.$refs.popup.close()
+			},
+			setContent(e) {
+				this.content = e.detail.value
+			},
+			checkboxChange: function(e) {
+				var items = this.list,
+					values = e.detail.value;
+				this.arr = []
+				for (var i = 0, lenI = items.length; i < lenI; ++i) {
+					const item = items[i]
+					if (values.includes(item.value)) {
+						this.$set(item, 'checked', true)
+						this.arr.push(item.name)
+					} else {
+						this.$set(item, 'checked', false)
+					}
+				}
+			},
+			jiucuo() {
+				if (this.current_timu.id != '' && this.current_timu.id != undefined) {
+					this.show()
+				}
+			},
+		}
+	}
+</script>
+
+<style>
+	.popup-content {
+		position: relative;
+		background: #fff;
+		z-index: 111;
+		width: 100%;
+		border-radius: 12px 12px 0 0;
+		height: 42px;
+		border-bottom: solid 1px #f1f1f1;
+		text-align: center;
+		line-height: 42px;
+		color: #313131;
+		font-size: 15px;
+	}
+
+	.jiucuo {
+		position: relative;
+		background: #fff;
+		border-radius: 12rpx 12rpx 0 0;
+		max-height: 80vh;
+		overflow-y: auto;
+	}
+
+	.jiucuo-content {
+		font-size: 13px;
+		padding: 9px 9px 0;
+	}
+
+	.jiucuo-xuan {
+		background: #fff;
+		padding: 7px 2px;
+		margin: 12px 0;
+		border-radius: 4px;
+	}
+
+	.jiucuo-checkbox {
+		width: 100%;
+		display: flex;
+		flex-wrap: wrap;
+		align-items: flex-start;
+	}
+
+	.jiucuo-checkbox label {
+		flex: 0 0 30%;
+		display: flex;
+		align-items: center;
+	}
+
+	.popup-textarea {
+		width: 92%;
+		margin: 0;
+		font-size: 13px;
+		background: #fff;
+		padding: 11px;
+		height: 72px;
+		margin-bottom: 12px;
+		background: #f7f7f7;
+	}
+
+	.popup-button {
+		background: #3c7bfc !important;
+		color: #fff;
+		padding: 2px;
+		margin: 0 auto 11px;
+		border-radius: 43px;
+		font-size: 16px;
+		display: block !important;
+		width: 95%;
+	}
+
+	.icon-cuo1 {
+		position: absolute;
+		color: #ccc;
+		top: 0;
+		right: 12px;
+	}
+</style>

+ 146 - 0
components/question-note/question-note.vue

@@ -0,0 +1,146 @@
+<template>
+	<view>
+		<view class="questionBankAnswer-bottom-flex" @tap="note">
+			<view class="iconfont icon-bianjisekuai"></view>
+			<view>笔记</view>
+		</view>
+		<uni-popup ref="popup" type="bottom" radius="12rpx" background-color="#8c8989" color="#5A5B5C" width="90%">
+			<view class="biji-content">
+				<view class="popup-content">
+					<text class="text">笔记</text>
+					<view class="iconfont icon-cuo1" @tap="hide"></view>
+				</view>
+				<textarea :value="content" placeholder="请输入笔记" @input="setContent" class="popup-textarea" />
+				<button type="primary" :loading="isSubmit" @tap="submit" class="popup-button">提交</button>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+		},
+		data() {
+			return {
+				content: '',
+				isSubmit: false,
+			}
+		},
+		methods: {
+			setContent(e) {
+				this.content = e.detail.value
+			},
+			async submit() {
+				// if (this.content == '') return
+				if (this.isSubmit) return
+				this.isSubmit = true
+				let res = await this.$myHttp.post({
+					url: this.$myHttp.urlMap.addNote,
+					data: {
+						question_id: this.current_timu.id,
+						content: this.content
+					},
+					needLogin: true
+				})
+				if (res.code == 1) {
+					this.isSubmit = false
+					uni.showToast({
+						title: '保存成功'
+					})
+					this.$refs.popup.close()
+				} else {
+					uni.showToast({
+						title: res.msg,
+						icon: 'none'
+					})
+					this.isSubmit = false
+				}
+			},
+			//获取之前的记录
+			async getNote() {
+				let res = await this.$myHttp.post({
+					url: this.$myHttp.urlMap.getNote,
+					data: {
+						question_id: this.current_timu.id,
+					},
+					needLogin: true
+				})
+				if (res.code == 1) {
+					if (res.data == null) {
+						this.content = ''
+					} else {
+						if (this.current_timu.id == res.data.question_id) {
+							this.content = res.data.content
+						}
+					}
+
+				}
+			},
+			note() {
+				if (this.current_timu.id != '' && this.current_timu.id != undefined) {
+					this.$refs.popup.open('bottom')
+					this.getNote()
+				}
+			},
+			hide() {
+				this.$refs.popup.close()
+			}
+		}
+	}
+</script>
+
+<style>
+	.popup-content {
+		position: relative;
+		background: #fff;
+		z-index: 111;
+		width: 100%;
+		border-radius: 12px 12px 0 0;
+		height: 42px;
+		border-bottom: solid 1px #f1f1f1;
+		text-align: center;
+		line-height: 42px;
+		color: #313131;
+		font-size: 15px;
+	}
+
+	.popup-textarea {
+		width: 92%;
+		margin: 12px;
+		height: 125px;
+		font-size: 14px;
+	}
+
+	.biji-content {
+		position: relative;
+		background: #fff;
+		border-radius: 12rpx 12rpx 0 0;
+		max-height: 80vh;
+		overflow-y: auto;
+	}
+
+	.popup-button {
+		background: #3c7bfc !important;
+		color: #fff;
+		padding: 2px;
+		margin: 0 auto 11px;
+		border-radius: 43px;
+		font-size: 16px;
+		display: block !important;
+		width: 95%;
+	}
+
+	.icon-cuo1 {
+		position: absolute;
+		color: #ccc;
+		top: 0;
+		right: 12px;
+	}
+</style>

+ 216 - 0
components/question-option/question-option.vue

@@ -0,0 +1,216 @@
+<template>
+	<view class="questionBankAnswer-content-menu" @tap="post_answer(item, index)" :class="{'active-bg': answerRightFlag, //// 用户答题完毕,正确答案显示,用户
+		// 错误答案显示  !((copy_post_status[current_timu.id] || show_beiti) && item.right_flg)
+		'kaoshi': question_type == 'exam',
+		'error-bg':answerWrongFlag}">
+		<!-- 用户选择错误答案显示特殊样式 -->
+		<view class="questionBankAnswer-content-num" :class="{'active': answerRightFlag, //// 用户答题完毕,正确答案显示,用户
+		// 错误答案显示
+		'error':answerWrongFlag}">
+			{{ item.answer_code === '1' ? 'A' : item.answer_code === '0' ? 'B' :
+	        item.answer_code }}
+			<!-- 考试不显示 -->
+			<!-- 1、用户提交答案以后显示 -->
+			<!-- 2、背题模式显示 -->
+			<view class="questionBankAnswer-dui">
+				<image v-if="answerRightFlag && question_type != 'exam'" :src="static_media.img01" class="questionBankAnswer-dui-img"></image>
+				<!-- (copy_post_status[current_timu.id] || show_beiti) && item.right_flg -->
+			</view>
+		</view>
+		<view class="questionBankAnswer-content-txt">
+			<uc-parse :node="getAsw(item.answer)"></uc-parse>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			question_type: {
+				type: String,
+				default: 'test'
+			},
+			type: {
+				type: String,
+				default: '-1'
+			},
+			item: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			index: {
+				type: Number,
+				default: -1
+			},
+			show_beiti: {
+				type: Boolean,
+				default: true,
+			},
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			copy_user_answer: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			copy_post_status: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			timu_index: {
+				type: Number,
+				default: -1
+			}
+		},
+		data() {
+			return {
+				answerRightFlag: false,
+				answerWrongFlag: false,
+			}
+		},
+		computed: {
+			static_media() {
+				return {
+					img01: this.$myConfig.localMedia + '/static/img/dui.png',
+				}
+			},
+		},
+		created() {},
+		methods: {
+			getAsw(item) {
+				item = item.replace(/^[A-Z]\./,'')
+				
+				return item
+			},
+			setAnswerStyle(copy_user_answer) {
+				this.answerRightFlag = false
+				this.answerWrongFlag = false
+				this.computedError(this.index, copy_user_answer)
+				this.computedActive(this.item, this.index, copy_user_answer)
+			},
+			//选项的错误状态
+			computedError(index, copy_user_answer) {
+				if (this.question_type == 'exam') {
+					this.answerWrongFlag = false
+					return
+				} else if (copy_user_answer && copy_user_answer[this.current_timu.id] && copy_user_answer[this
+						.current_timu.id][index]) {
+					if (copy_user_answer[this.current_timu.id][index].error_flg && ((this.current_timu.type == 2 &&
+							this.copy_post_status[this.current_timu.id]) || (this.current_timu.type == 1 || this
+							.current_timu.type == 3))) {
+						this.answerWrongFlag = true
+						return
+					}
+					this.answerWrongFlag = false
+					return
+				}
+				this.answerWrongFlag = false
+			},
+			//选项的正确状态
+			computedActive(item, index, copy_user_answer) {
+				if (this.question_type == 'exam') {
+					if (copy_user_answer && copy_user_answer[this.current_timu.id] && copy_user_answer[this.current_timu
+							.id][index].active) {
+						this.answerRightFlag = true
+						return
+					} else {
+						this.answerRightFlag = false
+						return
+					}
+				} else if (this.show_beiti || this.copy_post_status[this.current_timu.id]) {
+					//背题模式  或者答完之后的
+					if (item.right_flg) {
+						this.answerRightFlag = true
+						return
+					}
+					this.answerRightFlag = false
+					return
+				} else if (copy_user_answer && copy_user_answer[this.current_timu.id] && copy_user_answer[
+						this.current_timu.id][index].active) {
+					//答题模式  答完之后的
+					this.answerRightFlag = true
+					return
+				}
+				this.answerRightFlag = false
+			},
+			// 提交答案
+			post_answer(item, index) {
+				let copy_user_answer_this = JSON.parse(JSON.stringify(this.copy_user_answer))
+				// 如果背题模式,不做处理
+				if (this.show_beiti) {
+					return;
+				}
+				// 如果是判断和单选。执行以下处理
+				if (this.current_timu.type == 1 || this.current_timu.type == 3) {
+					this.$nextTick(() => {
+						if (copy_user_answer_this[this.current_timu.id]) {
+							for (let i = 0, leng = copy_user_answer_this[this.current_timu.id].length; i <
+								leng; i++) {
+								copy_user_answer_this[this.current_timu.id].splice(i, 1, {
+									...copy_user_answer_this[this.current_timu.id][i],
+									active: false,
+									error_flg: false
+								})
+							}
+							if (this.current_timu.right_answer.toString().toLowerCase().replace(/\s/g, '').indexOf(
+									item.answer_code.toString().toLowerCase().replace(/\s/g, '')) === -1) {
+								copy_user_answer_this[this.current_timu.id].splice(index, 1, {
+									...copy_user_answer_this[this.current_timu.id][index],
+									active: true,
+									error_flg: true
+								})
+							} else {
+								copy_user_answer_this[this.current_timu.id].splice(index, 1, {
+									...copy_user_answer_this[this.current_timu.id][index],
+									active: true,
+									error_flg: false
+								})
+							}
+						}
+						this.$emit('set_copy_user_answer', copy_user_answer_this)
+						this.$emit('post_answer_req', true)
+					});
+				} else if (this.current_timu.type == 2) {
+					// 如果是多选。执行以下处理
+					if (copy_user_answer_this[this.current_timu.id]) {
+						this.$emit('set_sub_button_style', 'is_duoxuan_sub', false)
+						if (copy_user_answer_this[this.current_timu.id][index]) {
+							if (copy_user_answer_this[this.current_timu.id][index].active) {
+								copy_user_answer_this[this.current_timu.id].splice(index, 1, {
+									...copy_user_answer_this[this.current_timu.id][index],
+									active: false,
+									error_flg: false
+								})
+							} else {
+								if (this.current_timu.right_answer.toString().toLowerCase().replace(/\s/g, '').indexOf(item
+										.answer_code.toString().toLowerCase().replace(/\s/g, '')) === -1) {
+									copy_user_answer_this[this.current_timu.id].splice(index, 1, {
+										...copy_user_answer_this[this.current_timu.id][index],
+										active: true,
+										error_flg: true
+									})
+								} else {
+									copy_user_answer_this[this.current_timu.id].splice(index, 1, {
+										...copy_user_answer_this[this.current_timu.id][index],
+										active: true,
+										error_flg: false
+									})
+								}
+							}
+						}
+					}
+					this.$emit('set_copy_user_answer', copy_user_answer_this)
+				}
+			},
+		}
+	}
+</script>

+ 79 - 0
components/question-pre-next/question-pre-next.vue

@@ -0,0 +1,79 @@
+<template>
+	<view class="questionBankAnswer-contentbtnbox">
+		<view :class="timu_order <= 1 ? 'noprevbtn' : ''" class="questionBankAnswer-contentprevbtn" @tap="prevpage">
+			上一题
+		</view>
+		<view :class="timu_order >= total_num  ? 'nonextbtn' : ''" class="questionBankAnswer-contentnextbtn"
+			@tap="nextpage">
+			下一题
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			timu_list: {
+				type: Array,
+				default: function() {
+					return [];
+				},
+			},
+			timu_order: {
+				type: Number,
+				default: 0
+			},
+			total_num: {
+				type: Number,
+				default: -1
+			},
+			confirmText: {
+				type: String,
+				default: '结束练习'
+			},
+		},
+		data() {
+			return {
+
+			}
+		},
+		methods: {
+			prevpage() {
+				if (this.timu_order <= 1) {
+					this.$myUtils.$prompt.showToast({
+						icon: 'none',
+						title: '没有上一题'
+					});
+				} else {
+					this.$emit('choice_timu_by_id', this.timu_order - 2)
+				}
+			},
+			nextpage() {
+				if (this.timu_order >= this.timu_list.length) {
+					this.$myUtils.$prompt.showToast({
+						icon: 'none',
+						title: '没有下一题'
+					});
+					// uni.showModal({
+					// 	title: '提示',
+					// 	content: '已没有下一题',
+					// 	confirmText: this.confirmText,
+					// 	confirmColor: '#3c7bfc',
+					// 	success: (res) => {
+					// 		if (res.confirm) {
+					// 			this.$emit('tap_handler',1);
+					// 		} else if (res.cancel) {
+					// 			// console.log('用户点击取消');
+					// 		}
+					// 	}
+					// });
+				} else {
+					this.$emit('choice_timu_by_id', this.timu_order)
+				}
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 38 - 0
components/question-remove/question-remove.vue

@@ -0,0 +1,38 @@
+<template>
+	<view class="questionBankAnswer-bottom-flex">
+		<view class="questionBankAnswer-bottom-flex" @tap="removeError">
+			<view class="iconfont icon-a-31Jguanbiyichu"></view>
+			<view class="ycct">移除</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default{
+		data(){
+			return {
+				
+			}
+		},
+		methods:{
+			removeError(){
+				let that = this
+				uni.showModal({
+					title: '提示',
+					content: '是否移除该错题',
+					confirmText:'移除',
+					success: function (res) {
+						if (res.confirm) {
+							that.$emit('removeError')
+						} else if (res.cancel) {
+							
+						}
+					}
+				});
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 60 - 0
components/question-set/question-set.vue

@@ -0,0 +1,60 @@
+<template>
+	<view class="">
+		<view class="order-pay-z" v-if="show_change_moshi" @tap="hide_change_moshi"></view>
+		<view class="tika" v-if="show_change_moshi" :style="{height: (show_empty ? '110px' : '60px')}">
+			<view class="tika-content">
+				<view class="uni-list">
+					<view class="uni-list-cell uni-list-cell-pd" v-if="show_empty">
+						<view class="uni-list-cell-db">清除答题记录</view>
+						<view class="qkbutton" @tap="emptyLog">清空</view>
+					</view>
+					<view class="uni-list-cell uni-list-cell-pd">
+						<view class="uni-list-cell-db">夜间模式</view>
+						<switch color="#3c7bfc" :checked="if_theme_dark" @change="set_if_theme_dark" />
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			show_change_moshi: {
+				type: Boolean,
+				default: false
+			},
+			if_theme_dark: {
+				type: Boolean,
+				default: false
+			},
+			show_empty: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+
+			}
+		},
+		methods: {
+			// 设置是否黑夜模式
+			set_if_theme_dark(e) {
+				// if_theme_dark
+				let if_theme_dark = e.target.value
+				this.$emit('set_if_theme_dark', if_theme_dark)
+			},
+			hide_change_moshi() {
+				this.$emit('hide_change_moshi')
+			},
+			emptyLog() {
+				this.$emit('emptyLog')
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 82 - 0
components/question-tiankong/question-tiankong.vue

@@ -0,0 +1,82 @@
+<template>
+	<view>
+		<view>
+			<view class="tkt_style" v-for="(item,index) in current_timu.right_answer_arr" :key="index">
+				<input type="text" placeholder="请输入答案" :value="current_timu.my_answer_arr[index]"
+					@input="post_answer($event, index)">
+			</view>
+		</view>
+		<view class="tkt_button" :class="is_tiankong_sub ? 'duoxuan_checked' : 'duoxuan' "
+			v-if="current_timu.right_answer_arr.length > 0 && !show_beiti" @tap="post_answer_req()">
+			{{is_tiankong_sub ? '提交成功' : '确认提交'}}
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			is_tiankong_sub: {
+				type: Boolean,
+				default: false,
+			},
+			show_beiti: {
+				type: Boolean,
+				default: true,
+			},
+			copy_user_answer: {
+				type: Array,
+				default: function() {
+					return [];
+				},
+			}
+		},
+		watch: {
+			current_timu: {
+				handler(newVal, oldVal) {},
+				deep: true,
+				immediate: true
+			}
+		},
+		methods: {
+			// 提交答案
+			post_answer(item, index) {
+				this.$emit('post_answer', item, index)
+				if (this.is_tiankong_sub) {
+					this.$emit('set_sub_button_style', 'is_tiankong_sub', false)
+				}
+				let timu = this.current_timu,
+					my_answer_arr = this.current_timu.my_answer_arr;
+				this.$set(my_answer_arr, index, item.detail.value)
+				this.$set(timu, index, 'my_answer_arr', my_answer_arr)
+
+				this.$set(timu, 'show_right_answer', true)
+				if (timu.my_answer_arr.length > 0) {
+					this.$set(timu, 'my_answer', timu.my_answer_arr.join('|'))
+				}
+				// if (timu.my_answer_arr.length > 0) {
+				// 	this.$set(timu, 'user_answer', timu.my_answer_arr.join('|'))
+				// }
+				this.$emit('set_current_timu', timu)
+				// let copy_user_answer_this = this.copy_user_answer;
+				// if (copy_user_answer_this[this.current_timu.id]) {
+				// 	copy_user_answer_this[this.current_timu.id] = timu.my_answer_arr
+				// }
+				// console.log(copy_user_answer_this)
+				// this.$emit('set_copy_user_answer',copy_user_answer_this)
+			},
+			post_answer_req() {
+				this.$emit('post_answer_req', true)
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 24 - 0
components/question-title/question-title.vue

@@ -0,0 +1,24 @@
+<template>
+	<view class="questionBankAnswer-title">
+		<uc-parse :node="node"></uc-parse>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			node: {
+				type: String,
+				default: '',
+			},
+		},
+		data() {
+			return {
+
+			}
+		},
+	}
+</script>
+
+<style>
+</style>

+ 59 - 0
components/question-type/question-type.vue

@@ -0,0 +1,59 @@
+<template>
+	<view class="questionBankAnswer-flex">
+		<view class="questionBankAnswer-top-l">
+			<view class="questionBankAnswer-tx">
+				{{ current_timu.type == 1 ? "单选题" : current_timu.type == 2 ? "多选题" : current_timu.type == 3 ?'判断题' : current_timu.type == 4 ?'填空题' : current_timu.type == 5 ?'简答题' : '' }}
+			</view>
+			<view class="questionBankAnswer-dc djs" v-if="over_time && over_time.second != undefined">
+				倒计时:
+				<view>
+					<uni-countdown :show-day="false" color="#fff" background-color="#3c7bfc" border-color="#3c7bfc"
+						:hour="over_time.hour" @timeup="timeup" :minute="over_time.minute" :second="over_time.second">
+					</uni-countdown>
+				</view>
+			</view>
+			<view class="questionBankAnswer-top">
+				<span>{{ timu_order }}</span>
+				/{{ total_num }}
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			over_time: {
+				type: Object,
+				default: function() {
+					return {};
+				},
+			},
+			current_timu: {
+				type: Object,
+				default: function() {
+					return {}
+				},
+			},
+			timu_order: {
+				type: Number,
+				default: 0
+			},
+			total_num: {
+				type: Number,
+				default: 0
+			},
+		},
+		created() {
+			console.log(this.timu_order)
+		},
+		methods: {
+			timeup() {
+				this.$emit('timeup')
+			},
+		}
+	}
+</script>
+
+<style>
+</style>

+ 28 - 0
components/uParse/gaoyia-parse/components/wxParseAudio.vue

@@ -0,0 +1,28 @@
+<template>
+	<!-- '<audio/>' 组件不再维护,建议使用能力更强的 'uni.createInnerAudioContext' 接口 有时间再改-->
+  <!--增加audio标签支持-->
+  <audio
+    :id="node.attr.id"
+    :class="node.classStr"
+    :style="node.styleStr"
+    :src="node.attr.src"
+    :loop="node.attr.loop"
+    :poster="node.attr.poster"
+    :name="node.attr.name"
+    :author="node.attr.author"
+    controls></audio>
+</template>
+
+<script>
+export default {
+  name: 'wxParseAudio',
+  props: {
+    node: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+};
+</script>

+ 94 - 0
components/uParse/gaoyia-parse/components/wxParseImg.vue

@@ -0,0 +1,94 @@
+<template>
+	<image
+		:mode="node.attr.mode"
+		:lazy-load="node.attr.lazyLoad"
+		:class="node.classStr"
+		:style="newStyleStr || node.styleStr"
+		:data-src="node.attr.src"
+		:src="node.attr.src"
+		@tap="wxParseImgTap"
+		@load="wxParseImgLoad"
+	/>
+</template>
+
+<script>
+export default {
+	name: 'wxParseImg',
+	data() {
+		return {
+			newStyleStr: '',
+			preview: true
+		};
+	},
+	inject: ['parseWidth'],
+	mounted() {},
+	props: {
+		node: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	
+	methods: {
+		wxParseImgTap(e) {
+			if (!this.preview) return;
+			const { src } = e.currentTarget.dataset;
+			if (!src) return;
+			let parent = this.$parent;
+			while (!parent.preview || typeof parent.preview !== 'function') {
+				// TODO 遍历获取父节点执行方法
+				parent = parent.$parent;
+			}
+			parent.preview(src, e);
+		},
+		// 图片视觉宽高计算函数区
+		wxParseImgLoad(e) {
+			const { src } = e.currentTarget.dataset;
+			if (!src) return;
+			let { width, height } = e.mp.detail;
+
+			const recal = this.wxAutoImageCal(width, height);
+
+			const { imageheight, imageWidth } = recal;
+			const { padding, mode } = this.node.attr;//删除padding
+			// const { mode } = this.node.attr;
+
+			const { styleStr } = this.node;
+			const imageHeightStyle = mode === 'widthFix' ? '' : `height: ${imageheight}px;`;
+
+			this.newStyleStr = `${styleStr}; ${imageHeightStyle}; width: ${imageWidth}px; padding: 0 ${+padding}px;`;//删除padding
+			// this.newStyleStr = `${styleStr}; ${imageHeightStyle}; width: ${imageWidth}px;`;
+		},
+		// 计算视觉优先的图片宽高
+		wxAutoImageCal(originalWidth, originalHeight) {
+			// 获取图片的原始长宽
+			const windowWidth = this.parseWidth.value;
+			const results = {};
+
+			if (originalWidth < 60 || originalHeight < 60) {
+				const { src } = this.node.attr;
+				let parent = this.$parent;
+				while (!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.removeImageUrl(src);
+				this.preview = false;
+			}
+
+			// 判断按照那种方式进行缩放
+			if (originalWidth > windowWidth) {
+				// 在图片width大于手机屏幕width时候
+				results.imageWidth = windowWidth;
+				results.imageheight = windowWidth * (originalHeight / originalWidth);
+			} else {
+				// 否则展示原来的数据
+				results.imageWidth = originalWidth;
+				results.imageheight = originalHeight;
+			}
+			return results;
+		}
+	}
+};
+</script>

+ 55 - 0
components/uParse/gaoyia-parse/components/wxParseTable.vue

@@ -0,0 +1,55 @@
+<template>
+	<div class='tablebox'>
+		<rich-text :nodes="nodes" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+	</div>
+</template>
+<script>
+export default {
+	name: 'wxParseTable',
+	props: {
+		node: {
+			type: Object,
+			default() {
+				return {};
+			},
+		},
+	},
+	inject: ['parseSelect'],
+	data() {
+		return {
+			nodes:[]
+		};
+	},
+	mounted() {
+		this.nodes=this.loadNode([this.node]);
+	},
+	methods: {
+		loadNode(node) {
+			let obj = [];
+			for (let children of node) {
+				if (children.node=='element') {
+					let t = {
+						name:children.tag,
+						attrs: {
+							class: children.classStr,
+							// style: children.styleStr,
+						},
+						children: children.nodes?this.loadNode(children.nodes):[]
+					}
+					
+					obj.push(t)
+				} else if(children.node=='text'){
+					obj.push({
+						type: 'text',
+						text: children.text
+					})
+				}
+			}
+			return obj
+		}
+	}
+};
+</script>
+<style>
+	@import url("../parse.css");
+</style>

+ 98 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate0.vue

@@ -0,0 +1,98 @@
+<template>
+	<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node"/>
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text'">{{node.text}}</block>
+</template>
+
+<script>
+	// #ifdef APP-PLUS | H5
+	import wxParseTemplate from './wxParseTemplate0';
+	// #endif
+	// #ifdef MP
+	import wxParseTemplate from './wxParseTemplate1';
+	// #endif
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+
+	export default {
+		// #ifdef APP-PLUS | H5
+		name: 'wxParseTemplate',
+		// #endif
+		// #ifdef MP
+		name: 'wxParseTemplate0',
+		// #endif
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;// TODO currentTarget才有dataset
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {// TODO 遍历获取父节点执行方法
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate1.vue

@@ -0,0 +1,88 @@
+<template>
+	<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text'">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate2';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate1',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate10.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate11';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate10',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 86 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate11.vue

@@ -0,0 +1,86 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<rich-text :nodes="node" :class="node.classStr" :style="'user-select:' + parseSelect"></rich-text>
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate11',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate2.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text'">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate3';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate2',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate3.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate4';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate3',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate4.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate5';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate4',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate5.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate6';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate5',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate6.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate7';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate6',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate7.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate8';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate7',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate8.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate9';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate8',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 88 - 0
components/uParse/gaoyia-parse/components/wxParseTemplate9.vue

@@ -0,0 +1,88 @@
+<template>
+		<!--判断是否是标签节点-->
+	<block v-if="node.node == 'element'">
+		<!--button类型-->
+		<button v-if="node.tag == 'button'" type="default" size="mini" :class="node.classStr" :style="node.styleStr">
+			<wx-parse-template :node="node" />
+		</button>
+		
+		<!--a类型-->
+		<view v-else-if="node.tag == 'a'" @click="wxParseATap(node.attr,$event)" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--li类型-->
+		<view v-else-if="node.tag == 'li'" :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+		
+		<!--table类型-->
+		<wx-parse-table v-else-if="node.tag == 'table'" :class="node.classStr" :style="node.styleStr" :node="node" />
+		
+		<!--br类型-->
+		<!-- #ifndef H5 -->
+			<text v-else-if="node.tag == 'br'">\n</text>
+		<!-- #endif -->
+		<!-- #ifdef H5 -->
+			<br v-else-if="node.tag == 'br'">
+		<!-- #endif -->
+		
+		<!--video类型-->
+		<wx-parse-video :node="node" v-else-if="node.tag == 'video'"/>
+	
+		<!--audio类型-->
+		<wx-parse-audio :node="node" v-else-if="node.tag == 'audio'"/>
+	
+		<!--img类型-->
+		<wx-parse-img :node="node" v-else-if="node.tag == 'img'" :style="node.styleStr"/>
+	
+		<!--其他标签-->
+		<view v-else :class="node.classStr" :style="node.styleStr">
+			<block v-for="(node, index) of node.nodes" :key="index">
+				<wx-parse-template :node="node" />
+			</block>
+		</view>
+	</block>
+	
+	<!--判断是否是文本节点-->
+	<block v-else-if="node.node == 'text' ">{{node.text}}</block>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate10';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+	import wxParseTable from './wxParseTable';
+	
+	export default {
+		name: 'wxParseTemplate9',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+			wxParseTable
+		},
+		methods: {
+			wxParseATap(attr,e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				let parent = this.$parent;
+				while(!parent.preview || typeof parent.preview !== 'function') {
+					parent = parent.$parent;
+				}
+				parent.navigate(href, e, attr);
+			}
+		}
+	};
+</script>

+ 15 - 0
components/uParse/gaoyia-parse/components/wxParseVideo.vue

@@ -0,0 +1,15 @@
+<template>
+  <!--增加video标签支持,并循环添加-->
+  <view :class="node.classStr" :style="node.styleStr">
+    <video :class="node.classStr" :style="node.styleStr" class="video-video" :src="node.attr.src"></video>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'wxParseVideo',
+  props: {
+    node: {},
+  },
+};
+</script>

+ 261 - 0
components/uParse/gaoyia-parse/libs/html2json.js

@@ -0,0 +1,261 @@
+/**
+ * html2Json 改造来自: https://github.com/Jxck/html2json
+ *
+ *
+ * author: Di (微信小程序开发工程师)
+ * organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
+ *               垂直微信小程序开发交流社区
+ *
+ * github地址: https://github.com/icindy/wxParse
+ *
+ * for: 微信小程序富文本解析
+ * detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
+ */
+
+import wxDiscode from './wxDiscode';
+import HTMLParser from './htmlparser';
+
+function makeMap(str) {
+  const obj = {};
+  const items = str.split(',');
+  for (let i = 0; i < items.length; i += 1) obj[items[i]] = true;
+  return obj;
+}
+
+// Block Elements - HTML 5
+const block = makeMap('br,code,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video');
+
+// Inline Elements - HTML 5
+const inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,button,cite,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var');
+
+// Elements that you can, intentionally, leave open
+// (and which close themselves)
+const closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
+
+function removeDOCTYPE(html) {
+  const isDocument = /<body.*>([^]*)<\/body>/.test(html);
+  return isDocument ? RegExp.$1 : html;
+}
+
+function trimHtml(html) {
+  return html
+    .replace(/<!--.*?-->/gi, '')
+    .replace(/\/\*.*?\*\//gi, '')
+    .replace(/[ ]+</gi, '<')
+    .replace(/<script[^]*<\/script>/gi, '')
+    .replace(/<style[^]*<\/style>/gi, '');
+}
+
+function getScreenInfo() {
+  const screen = {};
+  wx.getSystemInfo({
+    success: (res) => {
+      screen.width = res.windowWidth;
+      screen.height = res.windowHeight;
+    },
+  });
+  return screen;
+}
+
+function html2json(html, customHandler, imageProp, host) {
+  // 处理字符串
+  html = removeDOCTYPE(html);
+  html = trimHtml(html);
+  html = wxDiscode.strDiscode(html);
+  // 生成node节点
+  const bufArray = [];
+  const results = {
+    nodes: [],
+    imageUrls: [],
+  };
+
+	const screen = getScreenInfo();
+  function Node(tag) {
+    this.node = 'element';
+    this.tag = tag;
+		
+		this.$screen = screen;
+  }
+
+  HTMLParser(html, {
+    start(tag, attrs, unary) {
+      // node for this element
+      const node = new Node(tag);
+
+      if (bufArray.length !== 0) {
+        const parent = bufArray[0];
+        if (parent.nodes === undefined) {
+          parent.nodes = [];
+        }
+      }
+
+      if (block[tag]) {
+        node.tagType = 'block';
+      } else if (inline[tag]) {
+        node.tagType = 'inline';
+      } else if (closeSelf[tag]) {
+        node.tagType = 'closeSelf';
+      }
+
+      node.attr = attrs.reduce((pre, attr) => {
+        const { name } = attr;
+        let { value } = attr;
+        if (name === 'class') {
+          node.classStr = value;
+        }
+        // has multi attibutes
+        // make it array of attribute
+        if (name === 'style') {
+          node.styleStr = value;
+        }
+        if (value.match(/ /)) {
+          value = value.split(' ');
+        }
+
+        // if attr already exists
+        // merge it
+        if (pre[name]) {
+          if (Array.isArray(pre[name])) {
+            // already array, push to last
+            pre[name].push(value);
+          } else {
+            // single value, make it array
+            pre[name] = [pre[name], value];
+          }
+        } else {
+          // not exist, put it
+          pre[name] = value;
+        }
+
+        return pre;
+      }, {});
+
+      // 优化样式相关属性
+      if (node.classStr) {
+        node.classStr += ` ${node.tag}`;
+      } else {
+        node.classStr = node.tag;
+      }
+      if (node.tagType === 'inline') {
+        node.classStr += ' inline';
+      }
+
+      // 对img添加额外数据
+      if (node.tag === 'img') {
+        let imgUrl = node.attr.src;
+        imgUrl = wxDiscode.urlToHttpUrl(imgUrl, imageProp.domain);
+        Object.assign(node.attr, imageProp, {
+          src: imgUrl || '',
+        });
+        if (imgUrl) {
+          results.imageUrls.push(imgUrl);
+        }
+      }
+
+      // 处理a标签属性
+      if (node.tag === 'a') {
+        node.attr.href = node.attr.href || '';
+      }
+
+      // 处理font标签样式属性
+      if (node.tag === 'font') {
+        const fontSize = [
+          'x-small',
+          'small',
+          'medium',
+          'large',
+          'x-large',
+          'xx-large',
+          '-webkit-xxx-large',
+        ];
+        const styleAttrs = {
+          color: 'color',
+          face: 'font-family',
+          size: 'font-size',
+        };
+        if (!node.styleStr) node.styleStr = '';
+        Object.keys(styleAttrs).forEach((key) => {
+          if (node.attr[key]) {
+            const value = key === 'size' ? fontSize[node.attr[key] - 1] : node.attr[key];
+            node.styleStr += `${styleAttrs[key]}: ${value};`;
+          }
+        });
+      }
+
+      // 临时记录source资源
+      if (node.tag === 'source') {
+        results.source = node.attr.src;
+      }
+
+      if (customHandler.start) {
+        customHandler.start(node, results);
+      }
+
+      if (unary) {
+        // if this tag doesn't have end tag
+        // like <img src="hoge.png"/>
+        // add to parents
+        const parent = bufArray[0] || results;
+        if (parent.nodes === undefined) {
+          parent.nodes = [];
+        }
+        parent.nodes.push(node);
+      } else {
+        bufArray.unshift(node);
+      }
+    },
+    end(tag) {
+      // merge into parent tag
+      const node = bufArray.shift();
+      if (node.tag !== tag) {
+        console.error('invalid state: mismatch end tag');
+      }
+
+      // 当有缓存source资源时于于video补上src资源
+      if (node.tag === 'video' && results.source) {
+        node.attr.src = results.source;
+        delete results.source;
+      }
+
+      if (customHandler.end) {
+        customHandler.end(node, results);
+      }
+
+      if (bufArray.length === 0) {
+        results.nodes.push(node);
+      } else {
+        const parent = bufArray[0];
+        if (!parent.nodes) {
+          parent.nodes = [];
+        }
+        parent.nodes.push(node);
+      }
+    },
+    chars(text) {
+      if (!text.trim()) return;
+
+      const node = {
+        node: 'text',
+        text,
+      };
+
+      if (customHandler.chars) {
+        customHandler.chars(node, results);
+      }
+
+      if (bufArray.length === 0) {
+        results.nodes.push(node);
+      } else {
+        const parent = bufArray[0];
+        if (parent.nodes === undefined) {
+          parent.nodes = [];
+        }
+        parent.nodes.push(node);
+      }
+    },
+  });
+
+  return results;
+}
+
+export default html2json;

+ 156 - 0
components/uParse/gaoyia-parse/libs/htmlparser.js

@@ -0,0 +1,156 @@
+/**
+ *
+ * htmlParser改造自: https://github.com/blowsie/Pure-JavaScript-HTML5-Parser
+ *
+ * author: Di (微信小程序开发工程师)
+ * organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
+ *               垂直微信小程序开发交流社区
+ *
+ * github地址: https://github.com/icindy/wxParse
+ *
+ * for: 微信小程序富文本解析
+ * detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
+ */
+// Regular Expressions for parsing tags and attributes
+
+const startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z0-9_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
+const endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
+const attr = /([a-zA-Z0-9_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
+
+function makeMap(str) {
+  const obj = {};
+  const items = str.split(',');
+  for (let i = 0; i < items.length; i += 1) obj[items[i]] = true;
+  return obj;
+}
+
+// Empty Elements - HTML 5
+const empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr');
+
+// Block Elements - HTML 5
+const block = makeMap('address,code,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video');
+
+// Inline Elements - HTML 5
+const inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var');
+
+// Elements that you can, intentionally, leave open
+// (and which close themselves)
+const closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
+
+// Attributes that have their values filled in disabled="disabled"
+const fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
+
+function HTMLParser(html, handler) {
+  let index;
+  let chars;
+  let match;
+  let last = html;
+  const stack = [];
+
+  stack.last = () => stack[stack.length - 1];
+
+  function parseEndTag(tag, tagName) {
+    // If no tag name is provided, clean shop
+    let pos;
+    if (!tagName) {
+      pos = 0;
+    } else {
+      // Find the closest opened tag of the same type
+      tagName = tagName.toLowerCase();
+      for (pos = stack.length - 1; pos >= 0; pos -= 1) {
+        if (stack[pos] === tagName) break;
+      }
+    }
+    if (pos >= 0) {
+      // Close all the open elements, up the stack
+      for (let i = stack.length - 1; i >= pos; i -= 1) {
+        if (handler.end) handler.end(stack[i]);
+      }
+
+      // Remove the open elements from the stack
+      stack.length = pos;
+    }
+  }
+
+  function parseStartTag(tag, tagName, rest, unary) {
+    tagName = tagName.toLowerCase();
+
+    if (block[tagName]) {
+      while (stack.last() && inline[stack.last()]) {
+        parseEndTag('', stack.last());
+      }
+    }
+
+    if (closeSelf[tagName] && stack.last() === tagName) {
+      parseEndTag('', tagName);
+    }
+
+    unary = empty[tagName] || !!unary;
+
+    if (!unary) stack.push(tagName);
+
+    if (handler.start) {
+      const attrs = [];
+
+      rest.replace(attr, function genAttr(matches, name) {
+        const value = arguments[2] || arguments[3] || arguments[4] || (fillAttrs[name] ? name : '');
+
+        attrs.push({
+          name,
+          value,
+          escaped: value.replace(/(^|[^\\])"/g, '$1\\"'), // "
+        });
+      });
+
+      if (handler.start) {
+        handler.start(tagName, attrs, unary);
+      }
+    }
+  }
+
+  while (html) {
+    chars = true;
+
+    if (html.indexOf('</') === 0) {
+      match = html.match(endTag);
+
+      if (match) {
+        html = html.substring(match[0].length);
+        match[0].replace(endTag, parseEndTag);
+        chars = false;
+      }
+
+      // start tag
+    } else if (html.indexOf('<') === 0) {
+      match = html.match(startTag);
+
+      if (match) {
+        html = html.substring(match[0].length);
+        match[0].replace(startTag, parseStartTag);
+        chars = false;
+      }
+    }
+
+    if (chars) {
+      index = html.indexOf('<');
+      let text = '';
+      while (index === 0) {
+        text += '<';
+        html = html.substring(1);
+        index = html.indexOf('<');
+      }
+      text += index < 0 ? html : html.substring(0, index);
+      html = index < 0 ? '' : html.substring(index);
+
+      if (handler.chars) handler.chars(text);
+    }
+
+    if (html === last) throw new Error(`Parse Error: ${html}`);
+    last = html;
+  }
+
+  // Clean up any remaining tags
+  parseEndTag();
+}
+
+export default HTMLParser;

+ 209 - 0
components/uParse/gaoyia-parse/libs/wxDiscode.js

@@ -0,0 +1,209 @@
+// HTML 支持的数学符号
+function strNumDiscode(str) {
+str = str.replace(/&forall;|&#8704;|&#x2200;/g, '∀');
+str = str.replace(/&part;|&#8706;|&#x2202;/g, '∂');
+str = str.replace(/&exist;|&#8707;|&#x2203;/g, '∃');
+str = str.replace(/&empty;|&#8709;|&#x2205;/g, '∅');
+str = str.replace(/&nabla;|&#8711;|&#x2207;/g, '∇');
+str = str.replace(/&isin;|&#8712;|&#x2208;/g, '∈');
+str = str.replace(/&notin;|&#8713;|&#x2209;/g, '∉');
+str = str.replace(/&ni;|&#8715;|&#x220b;/g, '∋');
+str = str.replace(/&prod;|&#8719;|&#x220f;/g, '∏');
+str = str.replace(/&sum;|&#8721;|&#x2211;/g, '∑');
+str = str.replace(/&minus;|&#8722;|&#x2212;/g, '−');
+str = str.replace(/&lowast;|&#8727;|&#x2217;/g, '∗');
+str = str.replace(/&radic;|&#8730;|&#x221a;/g, '√');
+str = str.replace(/&prop;|&#8733;|&#x221d;/g, '∝');
+str = str.replace(/&infin;|&#8734;|&#x221e;/g, '∞');
+str = str.replace(/&ang;|&#8736;|&#x2220;/g, '∠');
+str = str.replace(/&and;|&#8743;|&#x2227;/g, '∧');
+str = str.replace(/&or;|&#8744;|&#x2228;/g, '∨');
+str = str.replace(/&cap;|&#8745;|&#x2229;/g, '∩');
+str = str.replace(/&cup;|&#8746;|&#x222a;/g, '∪');
+str = str.replace(/&int;|&#8747;|&#x222b;/g, '∫');
+str = str.replace(/&there4;|&#8756;|&#x2234;/g, '∴');
+str = str.replace(/&sim;|&#8764;|&#x223c;/g, '∼');
+str = str.replace(/&cong;|&#8773;|&#x2245;/g, '≅');
+str = str.replace(/&asymp;|&#8776;|&#x2248;/g, '≈');
+str = str.replace(/&ne;|&#8800;|&#x2260;/g, '≠');
+str = str.replace(/&le;|&#8804;|&#x2264;/g, '≤');
+str = str.replace(/&ge;|&#8805;|&#x2265;/g, '≥');
+str = str.replace(/&sub;|&#8834;|&#x2282;/g, '⊂');
+str = str.replace(/&sup;|&#8835;|&#x2283;/g, '⊃');
+str = str.replace(/&nsub;|&#8836;|&#x2284;/g, '⊄');
+str = str.replace(/&sube;|&#8838;|&#x2286;/g, '⊆');
+str = str.replace(/&supe;|&#8839;|&#x2287;/g, '⊇');
+str = str.replace(/&oplus;|&#8853;|&#x2295;/g, '⊕');
+str = str.replace(/&otimes;|&#8855;|&#x2297;/g, '⊗');
+str = str.replace(/&perp;|&#8869;|&#x22a5;/g, '⊥');
+str = str.replace(/&sdot;|&#8901;|&#x22c5;/g, '⋅');
+return str;
+}
+
+// HTML 支持的希腊字母
+function strGreeceDiscode(str) {
+str = str.replace(/&Alpha;|&#913;|&#x391;/g, 'Α');
+str = str.replace(/&Beta;|&#914;|&#x392;/g, 'Β');
+str = str.replace(/&Gamma;|&#915;|&#x393;/g, 'Γ');
+str = str.replace(/&Delta;|&#916;|&#x394;/g, 'Δ');
+str = str.replace(/&Epsilon;|&#917;|&#x395;/g, 'Ε');
+str = str.replace(/&Zeta;|&#918;|&#x396;/g, 'Ζ');
+str = str.replace(/&Eta;|&#919;|&#x397;/g, 'Η');
+str = str.replace(/&Theta;|&#920;|&#x398;/g, 'Θ');
+str = str.replace(/&Iota;|&#921;|&#x399;/g, 'Ι');
+str = str.replace(/&Kappa;|&#922;|&#x39a;/g, 'Κ');
+str = str.replace(/&Lambda;|&#923;|&#x39b;/g, 'Λ');
+str = str.replace(/&Mu;|&#924;|&#x39c;/g, 'Μ');
+str = str.replace(/&Nu;|&#925;|&#x39d;/g, 'Ν');
+str = str.replace(/&Xi;|&#925;|&#x39d;/g, 'Ν');
+str = str.replace(/&Omicron;|&#927;|&#x39f;/g, 'Ο');
+str = str.replace(/&Pi;|&#928;|&#x3a0;/g, 'Π');
+str = str.replace(/&Rho;|&#929;|&#x3a1;/g, 'Ρ');
+str = str.replace(/&Sigma;|&#931;|&#x3a3;/g, 'Σ');
+str = str.replace(/&Tau;|&#932;|&#x3a4;/g, 'Τ');
+str = str.replace(/&Upsilon;|&#933;|&#x3a5;/g, 'Υ');
+str = str.replace(/&Phi;|&#934;|&#x3a6;/g, 'Φ');
+str = str.replace(/&Chi;|&#935;|&#x3a7;/g, 'Χ');
+str = str.replace(/&Psi;|&#936;|&#x3a8;/g, 'Ψ');
+str = str.replace(/&Omega;|&#937;|&#x3a9;/g, 'Ω');
+
+str = str.replace(/&alpha;|&#945;|&#x3b1;/g, 'α');
+str = str.replace(/&beta;|&#946;|&#x3b2;/g, 'β');
+str = str.replace(/&gamma;|&#947;|&#x3b3;/g, 'γ');
+str = str.replace(/&delta;|&#948;|&#x3b4;/g, 'δ');
+str = str.replace(/&epsilon;|&#949;|&#x3b5;/g, 'ε');
+str = str.replace(/&zeta;|&#950;|&#x3b6;/g, 'ζ');
+str = str.replace(/&eta;|&#951;|&#x3b7;/g, 'η');
+str = str.replace(/&theta;|&#952;|&#x3b8;/g, 'θ');
+str = str.replace(/&iota;|&#953;|&#x3b9;/g, 'ι');
+str = str.replace(/&kappa;|&#954;|&#x3ba;/g, 'κ');
+str = str.replace(/&lambda;|&#955;|&#x3bb;/g, 'λ');
+str = str.replace(/&mu;|&#956;|&#x3bc;/g, 'μ');
+str = str.replace(/&nu;|&#957;|&#x3bd;/g, 'ν');
+str = str.replace(/&xi;|&#958;|&#x3be;/g, 'ξ');
+str = str.replace(/&omicron;|&#959;|&#x3bf;/g, 'ο');
+str = str.replace(/&pi;|&#960;|&#x3c0;/g, 'π');
+str = str.replace(/&rho;|&#961;|&#x3c1;/g, 'ρ');
+str = str.replace(/&sigmaf;|&#962;|&#x3c2;/g, 'ς');
+str = str.replace(/&sigma;|&#963;|&#x3c3;/g, 'σ');
+str = str.replace(/&tau;|&#964;|&#x3c4;/g, 'τ');
+str = str.replace(/&upsilon;|&#965;|&#x3c5;/g, 'υ');
+str = str.replace(/&phi;|&#966;|&#x3c6;/g, 'φ');
+str = str.replace(/&chi;|&#967;|&#x3c7;/g, 'χ');
+str = str.replace(/&psi;|&#968;|&#x3c8;/g, 'ψ');
+str = str.replace(/&omega;|&#969;|&#x3c9;/g, 'ω');
+str = str.replace(/&thetasym;|&#977;|&#x3d1;/g, 'ϑ');
+str = str.replace(/&upsih;|&#978;|&#x3d2;/g, 'ϒ');
+str = str.replace(/&piv;|&#982;|&#x3d6;/g, 'ϖ');
+str = str.replace(/&middot;|&#183;|&#xb7;/g, '·');
+return str;
+}
+
+function strcharacterDiscode(str) {
+// 加入常用解析
+
+// str = str.replace(/&nbsp;|&#32;|&#x20;/g, "&nbsp;");
+// str = str.replace(/&ensp;|&#8194;|&#x2002;/g, '&ensp;');
+// str = str.replace(/&#12288;|&#x3000;/g, '<span class=\'spaceshow\'> </span>');
+// str = str.replace(/&emsp;|&#8195;|&#x2003;/g, '&emsp;');
+// str = str.replace(/&quot;|&#34;|&#x22;/g, "\"");
+// str = str.replace(/&apos;|&#39;|&#x27;/g, "&apos;");
+// str = str.replace(/&acute;|&#180;|&#xB4;/g, "´");
+// str = str.replace(/&times;|&#215;|&#xD7;/g, "×");
+// str = str.replace(/&divide;|&#247;|&#xF7;/g, "÷");
+// str = str.replace(/&amp;|&#38;|&#x26;/g, '&amp;');
+// str = str.replace(/&lt;|&#60;|&#x3c;/g, '&lt;');
+// str = str.replace(/&gt;|&#62;|&#x3e;/g, '&gt;');
+
+
+
+
+str = str.replace(/&nbsp;|&#32;|&#x20;/g, "<span class='spaceshow'> </span>");
+str = str.replace(/&ensp;|&#8194;|&#x2002;/g, '<span class=\'spaceshow\'> </span>');
+str = str.replace(/&#12288;|&#x3000;/g, '<span class=\'spaceshow\'> </span>');
+str = str.replace(/&emsp;|&#8195;|&#x2003;/g, '<span class=\'spaceshow\'> </span>');
+str = str.replace(/&quot;|&#34;|&#x22;/g, "\"");
+str = str.replace(/&quot;|&#39;|&#x27;/g, "'");
+str = str.replace(/&acute;|&#180;|&#xB4;/g, "´");
+str = str.replace(/&times;|&#215;|&#xD7;/g, "×");
+str = str.replace(/&divide;|&#247;|&#xF7;/g, "÷");
+str = str.replace(/&amp;|&#38;|&#x26;/g, '&');
+str = str.replace(/&lt;|&#60;|&#x3c;/g, '<');
+str = str.replace(/&gt;|&#62;|&#x3e;/g, '>');
+return str;
+}
+
+// HTML 支持的其他实体
+function strOtherDiscode(str) {
+str = str.replace(/&OElig;|&#338;|&#x152;/g, 'Œ');
+str = str.replace(/&oelig;|&#339;|&#x153;/g, 'œ');
+str = str.replace(/&Scaron;|&#352;|&#x160;/g, 'Š');
+str = str.replace(/&scaron;|&#353;|&#x161;/g, 'š');
+str = str.replace(/&Yuml;|&#376;|&#x178;/g, 'Ÿ');
+str = str.replace(/&fnof;|&#402;|&#x192;/g, 'ƒ');
+str = str.replace(/&circ;|&#710;|&#x2c6;/g, 'ˆ');
+str = str.replace(/&tilde;|&#732;|&#x2dc;/g, '˜');
+str = str.replace(/&thinsp;|$#8201;|&#x2009;/g, '<span class=\'spaceshow\'> </span>');
+str = str.replace(/&zwnj;|&#8204;|&#x200C;/g, '<span class=\'spaceshow\'>‌</span>');
+str = str.replace(/&zwj;|$#8205;|&#x200D;/g, '<span class=\'spaceshow\'>‍</span>');
+str = str.replace(/&lrm;|$#8206;|&#x200E;/g, '<span class=\'spaceshow\'>‎</span>');
+str = str.replace(/&rlm;|&#8207;|&#x200F;/g, '<span class=\'spaceshow\'>‏</span>');
+str = str.replace(/&ndash;|&#8211;|&#x2013;/g, '–');
+str = str.replace(/&mdash;|&#8212;|&#x2014;/g, '—');
+str = str.replace(/&lsquo;|&#8216;|&#x2018;/g, '‘');
+str = str.replace(/&rsquo;|&#8217;|&#x2019;/g, '’');
+str = str.replace(/&sbquo;|&#8218;|&#x201a;/g, '‚');
+str = str.replace(/&ldquo;|&#8220;|&#x201c;/g, '“');
+str = str.replace(/&rdquo;|&#8221;|&#x201d;/g, '”');
+str = str.replace(/&bdquo;|&#8222;|&#x201e;/g, '„');
+str = str.replace(/&dagger;|&#8224;|&#x2020;/g, '†');
+str = str.replace(/&Dagger;|&#8225;|&#x2021;/g, '‡');
+str = str.replace(/&bull;|&#8226;|&#x2022;/g, '•');
+str = str.replace(/&hellip;|&#8230;|&#x2026;/g, '…');
+str = str.replace(/&permil;|&#8240;|&#x2030;/g, '‰');
+str = str.replace(/&prime;|&#8242;|&#x2032;/g, '′');
+str = str.replace(/&Prime;|&#8243;|&#x2033;/g, '″');
+str = str.replace(/&lsaquo;|&#8249;|&#x2039;/g, '‹');
+str = str.replace(/&rsaquo;|&#8250;|&#x203a;/g, '›');
+str = str.replace(/&oline;|&#8254;|&#x203e;/g, '‾');
+str = str.replace(/&euro;|&#8364;|&#x20ac;/g, '€');
+str = str.replace(/&trade;|&#8482;|&#x2122;/g, '™');
+str = str.replace(/&larr;|&#8592;|&#x2190;/g, '←');
+str = str.replace(/&uarr;|&#8593;|&#x2191;/g, '↑');
+str = str.replace(/&rarr;|&#8594;|&#x2192;/g, '→');
+str = str.replace(/&darr;|&#8595;|&#x2193;/g, '↓');
+str = str.replace(/&harr;|&#8596;|&#x2194;/g, '↔');
+str = str.replace(/&crarr;|&#8629;|&#x21b5;/g, '↵');
+str = str.replace(/&lceil;|&#8968;|&#x2308;/g, '⌈');
+str = str.replace(/&rceil;|&#8969;|&#x2309;/g, '⌉');
+str = str.replace(/&lfloor;|&#8970;|&#x230a;/g, '⌊');
+str = str.replace(/&rfloor;|&#8971;|&#x230b;/g, '⌋');
+str = str.replace(/&loz;|&#9674;|&#x25ca;/g, '◊');
+str = str.replace(/&spades;|&#9824;|&#x2660;/g, '♠');
+str = str.replace(/&clubs;|&#9827;|&#x2663;/g, '♣');
+str = str.replace(/&hearts;|&#9829;|&#x2665;/g, '♥');
+str = str.replace(/&diams;|&#9830;|&#x2666;/g, '♦');
+return str;
+}
+
+function strDiscode(str) {
+  str = strNumDiscode(str);
+  str = strGreeceDiscode(str);
+  str = strcharacterDiscode(str);
+  str = strOtherDiscode(str);
+  return str;
+}
+
+function urlToHttpUrl(url, domain) {
+  if (/^\/\//.test(url)) {
+    return `https:${url}`;
+  } else if (/^\//.test(url)) {
+    return `https://${domain}${url}`;
+  }
+  return url;
+}
+
+export default {
+  strDiscode,
+  urlToHttpUrl,
+};

+ 258 - 0
components/uParse/gaoyia-parse/parse.css

@@ -0,0 +1,258 @@
+/**
+ * author: Di (微信小程序开发工程师)
+ * organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
+ *         垂直微信小程序开发交流社区
+ *
+ * github地址: https://github.com/icindy/wxParse
+ *
+ * for: 微信小程序富文本解析
+ * detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
+ */
+/**
+ * 请在全局下引入该文件,@import '/static/wxParse.css';
+ */
+.wxParse {
+	user-select:none;
+	width: 100%;
+	font-family: Helvetica, "PingFangSC", 'Microsoft Yahei', '微软雅黑', Arial, sans-serif;
+	color: #333;
+	line-height: 1.5;
+	font-size: 1em;
+	text-align:justify;/* //左右两端对齐 */
+}
+.wxParse view ,.wxParse uni-view{
+	word-break: break-word;
+}
+.wxParse .p {
+	padding-bottom: 0.5em;
+	clear: both;
+	/* letter-spacing: 0;//字间距 */
+}
+.wxParse .inline {
+  display: inline;
+  margin: 0;
+  padding: 0;
+}
+
+.wxParse .div {
+  margin: 0;
+  padding: 0;
+  display: block;
+}
+
+.wxParse .h1{
+  font-size: 2em;
+  line-height: 1.2em;
+  margin: 0.67em 0;
+}
+.wxParse .h2{
+  font-size: 1.5em;
+  margin: 0.83em 0;
+}
+.wxParse .h3{
+  font-size: 1.17em;
+  margin: 1em 0;
+}
+.wxParse .h4{
+  margin: 1.33em 0;
+}
+.wxParse .h5{
+  font-size: 0.83em;
+  margin: 1.67em 0;
+}
+.wxParse .h6{
+  font-size: 0.83em;
+  margin: 1.67em 0;
+}
+
+.wxParse .h1,
+.wxParse .h2,
+.wxParse .h3,
+.wxParse .h4,
+.wxParse .h5,
+.wxParse .h6,
+.wxParse .b,
+.wxParse .strong{
+  font-weight: bolder;
+}
+
+.wxParse .i,
+.wxParse .cite,
+.wxParse .em,
+.wxParse .var,
+.wxParse .address {
+  font-style: italic;
+}
+.wxParse .spaceshow{
+	  white-space: pre;
+}
+.wxParse .pre,
+.wxParse .tt,
+.wxParse .code,
+.wxParse .kbd,
+.wxParse .samp {
+  font-family: monospace;
+}
+.wxParse .pre {
+  overflow: auto;
+  background: #f5f5f5;
+  padding: 16upx;
+  white-space: pre;
+  margin: 1em 0upx;
+  font-size: 24upx;
+}
+.wxParse .code {
+	overflow: auto;
+	padding: 16upx;
+	white-space: pre;
+	margin: 1em 0upx;
+	background: #f5f5f5;
+	font-size: 24upx;
+}
+
+.wxParse .big {
+  font-size: 1.17em;
+}
+
+.wxParse .small,
+.wxParse .sub,
+.wxParse .sup {
+  font-size: 0.83em;
+}
+
+.wxParse .sub {
+  vertical-align: sub;
+}
+.wxParse .sup {
+  vertical-align: super;
+}
+
+.wxParse .s,
+.wxParse .strike,
+.wxParse .del {
+  text-decoration: line-through;
+}
+
+.wxParse .strong,
+.wxParse .text,
+.wxParse .span,
+.wxParse .s {
+  display: inline;
+}
+
+.wxParse .a {
+  color: deepskyblue;
+}
+
+.wxParse .video {
+  text-align: center;
+  margin: 22upx 0;
+}
+
+.wxParse .video-video {
+  width: 100%;
+}
+.wxParse .uni-image{
+	max-width: 100%;
+}
+.wxParse .img {
+  display: block;
+  max-width: 100%;
+  margin-bottom: 0em;/* //与p标签底部padding同时修改 */
+  overflow: hidden;
+}
+
+.wxParse .blockquote {
+  margin: 10upx 0;
+  padding: 22upx 0 22upx 22upx;
+  font-family: Courier, Calibri, "宋体";
+  background: #f5f5f5;
+  border-left: 6upx solid #dbdbdb;
+}
+.wxParse .blockquote .p {
+  margin: 0;
+}
+.wxParse .ul, .wxParse .ol {
+  display: block;
+  margin: 1em 0;
+  padding-left: 2em;
+}
+.wxParse .ol {
+  list-style-type: disc;
+}
+.wxParse .ol {
+  list-style-type: decimal;
+}
+.wxParse .ol>weixin-parse-template,.wxParse .ul>weixin-parse-template {
+  display: list-item;
+  align-items: baseline;
+  text-align: match-parent;
+}
+
+.wxParse .ol>.li,.wxParse .ul>.li {
+  display: list-item;
+  align-items: baseline;
+  text-align: match-parent;
+}
+.wxParse .ul .ul, .wxParse .ol .ul {
+  list-style-type: circle;
+}
+.wxParse .ol .ol .ul, .wxParse .ol .ul .ul, .wxParse .ul .ol .ul, .wxParse .ul .ul .ul {
+    list-style-type: square;
+}
+
+.wxParse .u {
+  text-decoration: underline;
+}
+.wxParse .hide {
+  display: none;
+}
+.wxParse .del {
+  display: inline;
+}
+.wxParse .figure {
+  overflow: hidden;
+}
+.wxParse .tablebox{
+	overflow: auto;
+	background-color: #f5f5f5;
+	background: #f5f5f5;
+	font-size: 13px;
+	padding: 8px;
+}
+.wxParse .table .table,.wxParse .table{
+	border-collapse:collapse;
+	box-sizing: border-box;
+	/* 内边框 */
+	/* width: 100%; */
+	overflow: auto;
+	white-space: pre;
+}
+.wxParse .tbody{
+	border-collapse:collapse;
+	box-sizing: border-box;
+	/* 内边框 */
+	border: 1px solid #dadada;
+}
+.wxParse .table  .thead, .wxParse  .table .tfoot, .wxParse  .table .th{
+	border-collapse:collapse;
+	box-sizing: border-box;
+	background: #ececec;
+	font-weight: 40;
+}
+.wxParse  .table .tr {
+	border-collapse:collapse;
+	box-sizing: border-box;
+	/* border: 2px solid #F0AD4E; */
+	overflow:auto;
+}
+.wxParse  .table .th,
+.wxParse  .table .td{
+	border-collapse:collapse;
+	box-sizing: border-box;
+	border: 2upx solid #dadada;
+	overflow:auto;
+}
+.wxParse .audio, .wxParse .uni-audio-default{
+	display: block;
+}

+ 227 - 0
components/uParse/gaoyia-parse/parse.vue

@@ -0,0 +1,227 @@
+<!--**
+ * forked from:https://github.com/F-loat/mpvue-wxParse
+ *
+ * github地址: https://github.com/dcloudio/uParse
+ *
+ * for: uni-app框架下 富文本解析
+ * 
+ * 优化 by gaoyia@qq.com  https://github.com/gaoyia/parse
+ */-->
+
+<template>
+	
+	<!--基础元素-->
+	<div class="wxParse" :class="className" :style="'user-select:' + userSelect">
+		<block v-for="(node, index) of nodes" :key="index" v-if="!loading">
+			<wxParseTemplate :node="node" />
+		</block>
+	</div>
+</template>
+
+<script>
+import HtmlToJson from './libs/html2json';
+import wxParseTemplate from './components/wxParseTemplate0';
+
+	
+	export default {
+		name: 'wxParse',
+		props: {
+			// user-select:none;
+			userSelect: {
+				type: String,
+				default: 'text' //none |text| all | element
+			},
+			imgOptions: {
+				type: [Object, Boolean],
+				default: function() {
+					return {
+						loop: false,
+						indicator: 'number',
+						longPressActions: false
+						// longPressActions: {
+						// 	 itemList: ['发送给朋友', '保存图片', '收藏'],
+						// 		success: function (res) {
+						// 			console.log('选中了第' + (res.tapIndex + 1) + '个按钮');
+						// 		},
+						// 		fail: function (res) {
+						// 			console.log(res.errMsg);
+						// 		}    
+						// 	}
+						// }
+					}
+				}
+			},
+			loading: {
+				type: Boolean,
+				default: false
+			},
+			className: {
+				type: String,
+				default: ''
+			},
+			content: {
+				type: String,
+				default: ''
+			},
+			noData: {
+				type: String,
+				default: '<div style="color: white;">数据不能为空</div>'
+			},
+			startHandler: {
+				type: Function,
+				default () {
+					return node => {
+						node.attr.class = null;
+						node.attr.style = null;
+					};
+				}
+			},
+			endHandler: {
+				type: Function,
+				default: null
+			},
+			charsHandler: {
+				type: Function,
+				default: null
+			},
+			imageProp: {
+				type: Object,
+				default () {
+					return {
+						mode: 'aspectFit',
+						padding: 0,
+						lazyLoad: false,
+						domain: ''
+					};
+				}
+			}
+		},
+		components: {
+			wxParseTemplate
+		},
+		data() {
+			return {
+				nodes: {},
+				imageUrls: [],
+				wxParseWidth: {
+					value: 0
+				}
+			};
+		},
+		computed: {},
+		mounted() {
+			this.setHtml()
+		},
+		methods: {
+			setHtml() {
+				this.getWidth().then((data) => {
+					this.wxParseWidth.value = data;
+				})
+				let {
+					content,
+					noData,
+					imageProp,
+					startHandler,
+					endHandler,
+					charsHandler
+				} = this;
+				let parseData = content || noData;
+				let customHandler = {
+					start: startHandler,
+					end: endHandler,
+					chars: charsHandler
+				};
+				let results = HtmlToJson(parseData, customHandler, imageProp, this);
+
+				this.imageUrls = results.imageUrls;
+				// this.nodes = results.nodes;
+				
+				
+				this.nodes = [];
+				results.nodes.forEach((item) => {
+					setTimeout(() => {
+						this.nodes.push(item)
+					}, 0);
+				})
+			},
+			getWidth() {
+				return new Promise((res, rej) => {
+					// #ifndef MP-ALIPAY || MP-BAIDU
+					uni.createSelectorQuery()
+						.in(this)
+						.select('.wxParse')
+						.fields({
+								size: true,
+								scrollOffset: true
+							},
+							data => {
+								res(data.width);
+							}
+						).exec();
+					// #endif
+					// #ifdef MP-BAIDU
+					const query = swan.createSelectorQuery();
+					query.select('.wxParse').boundingClientRect();
+					query.exec(obj => {
+						const rect = obj[0]
+						if (rect) {
+							res(rect.width);
+						}
+					});
+					// #endif
+					// #ifdef MP-ALIPAY
+					my.createSelectorQuery()
+						.select('.wxParse')
+						.boundingClientRect().exec((ret) => {
+							res(ret[0].width);
+						});
+					// #endif
+				});
+			},
+			navigate(href, $event, attr) {
+				this.$emit('navigate', href, $event);
+			},
+			preview(src, $event) {
+				if (!this.imageUrls.length || typeof this.imgOptions === 'boolean') {
+
+				} else {
+					uni.previewImage({
+						current: src,
+						urls: this.imageUrls,
+						loop: this.imgOptions.loop,
+						indicator: this.imgOptions.indicator,
+						longPressActions: this.imgOptions.longPressActions
+					});
+				}
+				this.$emit('preview', src, $event);
+			},
+			removeImageUrl(src) {
+				const {
+					imageUrls
+				} = this;
+				imageUrls.splice(imageUrls.indexOf(src), 1);
+			}
+		},
+		// 父组件中提供
+		provide() {
+			return {
+				parseWidth: this.wxParseWidth,
+				parseSelect: this.userSelect
+				// 提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
+			};
+		},
+		watch: {
+			content(){
+				this.setHtml()
+			}
+			// content: {
+			// 	handler: function(newVal, oldVal) {
+			// 		if (newVal !== oldVal) {
+			// 			
+			// 		}
+			// 	},
+			// 	deep: true
+			// }
+		}
+	};
+</script>

+ 27 - 0
components/uParse/src/components/wxParseAudio.vue

@@ -0,0 +1,27 @@
+<template>
+  <!--增加audio标签支持-->
+  <audio
+    :id="node.attr.id"
+    :class="node.classStr"
+    :style="node.styleStr"
+    :src="node.attr.src"
+    :loop="node.attr.loop"
+    :poster="node.attr.poster"
+    :name="node.attr.name"
+    :author="node.attr.author"
+    controls></audio>
+</template>
+
+<script>
+export default {
+  name: 'wxParseAudio',
+  props: {
+    node: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+};
+</script>

+ 90 - 0
components/uParse/src/components/wxParseImg.vue

@@ -0,0 +1,90 @@
+<template>
+	<image :mode="node.attr.mode" :lazy-load="node.attr.lazyLoad" :class="node.classStr" :style="newStyleStr || node.styleStr"
+	 :data-src="node.attr.src" :src="node.attr.src" @tap="wxParseImgTap" @load="wxParseImgLoad" />
+</template>
+
+<script>
+	export default {
+		name: 'wxParseImg',
+		data() {
+			return {
+				newStyleStr: '',
+				preview: true,
+			};
+		},
+		props: {
+			node: {
+				type: Object,
+				default () {
+					return {};
+				},
+			},
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseImgTap(e) {
+				if (!this.preview) return;
+				const {
+					src
+				} = e.currentTarget.dataset;
+				if (!src) return;
+				this.uparse.preview(src, e);
+			},
+			// 图片视觉宽高计算函数区
+			wxParseImgLoad(e) {
+				const {
+					src
+				} = e.currentTarget.dataset;
+				if (!src) return;
+				const {
+					width,
+					height
+				} = e.mp.detail;
+				const recal = this.wxAutoImageCal(width, height);
+				const {
+					imageheight,
+					imageWidth
+				} = recal;
+				const {
+					padding,
+					mode
+				} = this.node.attr;
+				const {
+					styleStr
+				} = this.node;
+				const imageHeightStyle = mode === 'widthFix' ? '' : `height: ${imageheight}px;`;
+				this.newStyleStr = `${styleStr}; ${imageHeightStyle}; width: ${imageWidth}px; padding: 0 ${+padding}px;`;
+			},
+			// 计算视觉优先的图片宽高
+			wxAutoImageCal(originalWidth, originalHeight) {
+				// 获取图片的原始长宽
+				const {
+					padding
+				} = this.node.attr;
+				const windowWidth = this.node.$screen.width - (2 * padding);
+				const results = {};
+
+				if (originalWidth < 60 || originalHeight < 60) {
+					const {
+						src
+					} = this.node.attr;
+					this.uparse.removeImageUrl(src);
+					this.preview = false;
+				}
+
+				// 判断按照那种方式进行缩放
+				if (originalWidth > windowWidth) {
+					// 在图片width大于手机屏幕width时候
+					results.imageWidth = windowWidth;
+					results.imageheight = windowWidth * (originalHeight / originalWidth);
+				} else {
+					// 否则展示原来的数据
+					results.imageWidth = originalWidth;
+					results.imageheight = originalHeight;
+				}
+
+				return results;
+			}
+		}
+	};
+</script>

+ 104 - 0
components/uParse/src/components/wxParseTemplate0.vue

@@ -0,0 +1,104 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--table类型-->
+			<block v-else-if="node.tag == 'table'">
+				<view :class="node.classStr" class="table" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate1';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate0',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;// TODO currentTarget才有dataset
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 96 - 0
components/uParse/src/components/wxParseTemplate1.vue

@@ -0,0 +1,96 @@
+<template>
+	<view :class="(node.tag == 'li' ? node.classStr : (node.node==='text'?'text':''))">
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<!-- <view :class="node.classStr" :style="node.styleStr"> -->
+				<view :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate2';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate1',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			},
+		},
+	};
+</script>

+ 94 - 0
components/uParse/src/components/wxParseTemplate10.vue

@@ -0,0 +1,94 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate11';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate10',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 84 - 0
components/uParse/src/components/wxParseTemplate11.vue

@@ -0,0 +1,84 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<!--button类型-->
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					{{node.text}}
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					{{node.text}}
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					{{node.text}}
+				</view>
+			</block>
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate11',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate2.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate3';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate2',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate3.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate4';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate3',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate4.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate5';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate4',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate5.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate6';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate5',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate6.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate7';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate6',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate7.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate8';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate7',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate8.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate9';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate8',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 95 - 0
components/uParse/src/components/wxParseTemplate9.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<!--判断是否是标签节点-->
+		<block v-if="node.node == 'element'">
+			<block v-if="node.tag == 'button'">
+				<button type="default" size="mini">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</button>
+			</block>
+
+			<!--li类型-->
+			<block v-else-if="node.tag == 'li'">
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--video类型-->
+			<block v-else-if="node.tag == 'video'">
+				<wx-parse-video :node="node" />
+			</block>
+
+			<!--audio类型-->
+			<block v-else-if="node.tag == 'audio'">
+				<wx-parse-audio :node="node" />
+			</block>
+
+			<!--img类型-->
+			<block v-else-if="node.tag == 'img'">
+				<wx-parse-img :node="node" />
+			</block>
+
+			<!--a类型-->
+			<block v-else-if="node.tag == 'a'">
+				<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+			<!--br类型-->
+			<block v-else-if="node.tag == 'br'">
+				<text>\n</text>
+			</block>
+
+			<!--其他标签-->
+			<block v-else>
+				<view :class="node.classStr" :style="node.styleStr">
+					<block v-for="(node, index) of node.nodes" :key="index">
+						<wx-parse-template :node="node" />
+					</block>
+				</view>
+			</block>
+
+		</block>
+
+		<!--判断是否是文本节点-->
+		<block v-else-if="node.node == 'text'">{{node.text}}</block>
+	</view>
+</template>
+
+<script>
+	import wxParseTemplate from './wxParseTemplate10';
+	import wxParseImg from './wxParseImg';
+	import wxParseVideo from './wxParseVideo';
+	import wxParseAudio from './wxParseAudio';
+
+	export default {
+		name: 'wxParseTemplate9',
+		props: {
+			node: {},
+		},
+		components: {
+			wxParseTemplate,
+			wxParseImg,
+			wxParseVideo,
+			wxParseAudio,
+		},
+		inject: ['uparse'],
+		methods: {
+			wxParseATap(e) {
+				const {
+					href
+				} = e.currentTarget.dataset;
+				if (!href) return;
+				this.uparse.navigate(href, e);
+			}
+		}
+	};
+</script>

+ 15 - 0
components/uParse/src/components/wxParseVideo.vue

@@ -0,0 +1,15 @@
+<template>
+  <!--增加video标签支持,并循环添加-->
+  <view :class="node.classStr" :style="node.styleStr">
+    <video :class="node.classStr" class="video-video" :src="node.attr.src"></video>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'wxParseVideo',
+  props: {
+    node: {},
+  },
+};
+</script>

+ 261 - 0
components/uParse/src/libs/html2json.js

@@ -0,0 +1,261 @@
+/**
+ * html2Json 改造来自: https://github.com/Jxck/html2json
+ *
+ *
+ * author: Di (微信小程序开发工程师)
+ * organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
+ *               垂直微信小程序开发交流社区
+ *
+ * github地址: https://github.com/icindy/wxParse
+ *
+ * for: 微信小程序富文本解析
+ * detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
+ */
+
+import wxDiscode from './wxDiscode';
+import HTMLParser from './htmlparser';
+
+function makeMap(str) {
+  const obj = {};
+  const items = str.split(',');
+  for (let i = 0; i < items.length; i += 1) obj[items[i]] = true;
+  return obj;
+}
+
+// Block Elements - HTML 5
+const block = makeMap('br,code,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video');
+
+// Inline Elements - HTML 5
+const inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,button,cite,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var');
+
+// Elements that you can, intentionally, leave open
+// (and which close themselves)
+const closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
+
+function removeDOCTYPE(html) {
+  const isDocument = /<body.*>([^]*)<\/body>/.test(html);
+  return isDocument ? RegExp.$1 : html;
+}
+
+function trimHtml(html) {
+  return html
+    .replace(/<!--.*?-->/gi, '')
+    .replace(/\/\*.*?\*\//gi, '')
+    .replace(/[ ]+</gi, '<')
+    .replace(/<script[^]*<\/script>/gi, '')
+    .replace(/<style[^]*<\/style>/gi, '');
+}
+
+function getScreenInfo() {
+  const screen = {};
+  wx.getSystemInfo({
+    success: (res) => {
+      screen.width = res.windowWidth;
+      screen.height = res.windowHeight;
+    },
+  });
+  return screen;
+}
+
+function html2json(html, customHandler, imageProp, host) {
+  // 处理字符串
+  html = removeDOCTYPE(html);
+  html = trimHtml(html);
+  html = wxDiscode.strDiscode(html);
+  // 生成node节点
+  const bufArray = [];
+  const results = {
+    nodes: [],
+    imageUrls: [],
+  };
+
+	const screen = getScreenInfo();
+  function Node(tag) {
+    this.node = 'element';
+    this.tag = tag;
+		
+		this.$screen = screen;
+  }
+
+  HTMLParser(html, {
+    start(tag, attrs, unary) {
+      // node for this element
+      const node = new Node(tag);
+
+      if (bufArray.length !== 0) {
+        const parent = bufArray[0];
+        if (parent.nodes === undefined) {
+          parent.nodes = [];
+        }
+      }
+
+      if (block[tag]) {
+        node.tagType = 'block';
+      } else if (inline[tag]) {
+        node.tagType = 'inline';
+      } else if (closeSelf[tag]) {
+        node.tagType = 'closeSelf';
+      }
+
+      node.attr = attrs.reduce((pre, attr) => {
+        const { name } = attr;
+        let { value } = attr;
+        if (name === 'class') {
+          node.classStr = value;
+        }
+        // has multi attibutes
+        // make it array of attribute
+        if (name === 'style') {
+          node.styleStr = value;
+        }
+        if (value.match(/ /)) {
+          value = value.split(' ');
+        }
+
+        // if attr already exists
+        // merge it
+        if (pre[name]) {
+          if (Array.isArray(pre[name])) {
+            // already array, push to last
+            pre[name].push(value);
+          } else {
+            // single value, make it array
+            pre[name] = [pre[name], value];
+          }
+        } else {
+          // not exist, put it
+          pre[name] = value;
+        }
+
+        return pre;
+      }, {});
+
+      // 优化样式相关属性
+      if (node.classStr) {
+        node.classStr += ` ${node.tag}`;
+      } else {
+        node.classStr = node.tag;
+      }
+      if (node.tagType === 'inline') {
+        node.classStr += ' inline';
+      }
+
+      // 对img添加额外数据
+      if (node.tag === 'img') {
+        let imgUrl = node.attr.src;
+        imgUrl = wxDiscode.urlToHttpUrl(imgUrl, imageProp.domain);
+        Object.assign(node.attr, imageProp, {
+          src: imgUrl || '',
+        });
+        if (imgUrl) {
+          results.imageUrls.push(imgUrl);
+        }
+      }
+
+      // 处理a标签属性
+      if (node.tag === 'a') {
+        node.attr.href = node.attr.href || '';
+      }
+
+      // 处理font标签样式属性
+      if (node.tag === 'font') {
+        const fontSize = [
+          'x-small',
+          'small',
+          'medium',
+          'large',
+          'x-large',
+          'xx-large',
+          '-webkit-xxx-large',
+        ];
+        const styleAttrs = {
+          color: 'color',
+          face: 'font-family',
+          size: 'font-size',
+        };
+        if (!node.styleStr) node.styleStr = '';
+        Object.keys(styleAttrs).forEach((key) => {
+          if (node.attr[key]) {
+            const value = key === 'size' ? fontSize[node.attr[key] - 1] : node.attr[key];
+            node.styleStr += `${styleAttrs[key]}: ${value};`;
+          }
+        });
+      }
+
+      // 临时记录source资源
+      if (node.tag === 'source') {
+        results.source = node.attr.src;
+      }
+
+      if (customHandler.start) {
+        customHandler.start(node, results);
+      }
+
+      if (unary) {
+        // if this tag doesn't have end tag
+        // like <img src="hoge.png"/>
+        // add to parents
+        const parent = bufArray[0] || results;
+        if (parent.nodes === undefined) {
+          parent.nodes = [];
+        }
+        parent.nodes.push(node);
+      } else {
+        bufArray.unshift(node);
+      }
+    },
+    end(tag) {
+      // merge into parent tag
+      const node = bufArray.shift();
+      if (node.tag !== tag) {
+        console.error('invalid state: mismatch end tag');
+      }
+
+      // 当有缓存source资源时于于video补上src资源
+      if (node.tag === 'video' && results.source) {
+        node.attr.src = results.source;
+        delete results.source;
+      }
+
+      if (customHandler.end) {
+        customHandler.end(node, results);
+      }
+
+      if (bufArray.length === 0) {
+        results.nodes.push(node);
+      } else {
+        const parent = bufArray[0];
+        if (!parent.nodes) {
+          parent.nodes = [];
+        }
+        parent.nodes.push(node);
+      }
+    },
+    chars(text) {
+      if (!text.trim()) return;
+
+      const node = {
+        node: 'text',
+        text,
+      };
+
+      if (customHandler.chars) {
+        customHandler.chars(node, results);
+      }
+
+      if (bufArray.length === 0) {
+        results.nodes.push(node);
+      } else {
+        const parent = bufArray[0];
+        if (parent.nodes === undefined) {
+          parent.nodes = [];
+        }
+        parent.nodes.push(node);
+      }
+    },
+  });
+
+  return results;
+}
+
+export default html2json;

+ 156 - 0
components/uParse/src/libs/htmlparser.js

@@ -0,0 +1,156 @@
+/**
+ *
+ * htmlParser改造自: https://github.com/blowsie/Pure-JavaScript-HTML5-Parser
+ *
+ * author: Di (微信小程序开发工程师)
+ * organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
+ *               垂直微信小程序开发交流社区
+ *
+ * github地址: https://github.com/icindy/wxParse
+ *
+ * for: 微信小程序富文本解析
+ * detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
+ */
+// Regular Expressions for parsing tags and attributes
+
+const startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z0-9_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
+const endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
+const attr = /([a-zA-Z0-9_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
+
+function makeMap(str) {
+  const obj = {};
+  const items = str.split(',');
+  for (let i = 0; i < items.length; i += 1) obj[items[i]] = true;
+  return obj;
+}
+
+// Empty Elements - HTML 5
+const empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr');
+
+// Block Elements - HTML 5
+const block = makeMap('address,code,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video');
+
+// Inline Elements - HTML 5
+const inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var');
+
+// Elements that you can, intentionally, leave open
+// (and which close themselves)
+const closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
+
+// Attributes that have their values filled in disabled="disabled"
+const fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
+
+function HTMLParser(html, handler) {
+  let index;
+  let chars;
+  let match;
+  let last = html;
+  const stack = [];
+
+  stack.last = () => stack[stack.length - 1];
+
+  function parseEndTag(tag, tagName) {
+    // If no tag name is provided, clean shop
+    let pos;
+    if (!tagName) {
+      pos = 0;
+    } else {
+      // Find the closest opened tag of the same type
+      tagName = tagName.toLowerCase();
+      for (pos = stack.length - 1; pos >= 0; pos -= 1) {
+        if (stack[pos] === tagName) break;
+      }
+    }
+    if (pos >= 0) {
+      // Close all the open elements, up the stack
+      for (let i = stack.length - 1; i >= pos; i -= 1) {
+        if (handler.end) handler.end(stack[i]);
+      }
+
+      // Remove the open elements from the stack
+      stack.length = pos;
+    }
+  }
+
+  function parseStartTag(tag, tagName, rest, unary) {
+    tagName = tagName.toLowerCase();
+
+    if (block[tagName]) {
+      while (stack.last() && inline[stack.last()]) {
+        parseEndTag('', stack.last());
+      }
+    }
+
+    if (closeSelf[tagName] && stack.last() === tagName) {
+      parseEndTag('', tagName);
+    }
+
+    unary = empty[tagName] || !!unary;
+
+    if (!unary) stack.push(tagName);
+
+    if (handler.start) {
+      const attrs = [];
+
+      rest.replace(attr, function genAttr(matches, name) {
+        const value = arguments[2] || arguments[3] || arguments[4] || (fillAttrs[name] ? name : '');
+
+        attrs.push({
+          name,
+          value,
+          escaped: value.replace(/(^|[^\\])"/g, '$1\\"'), // "
+        });
+      });
+
+      if (handler.start) {
+        handler.start(tagName, attrs, unary);
+      }
+    }
+  }
+
+  while (html) {
+    chars = true;
+
+    if (html.indexOf('</') === 0) {
+      match = html.match(endTag);
+
+      if (match) {
+        html = html.substring(match[0].length);
+        match[0].replace(endTag, parseEndTag);
+        chars = false;
+      }
+
+      // start tag
+    } else if (html.indexOf('<') === 0) {
+      match = html.match(startTag);
+
+      if (match) {
+        html = html.substring(match[0].length);
+        match[0].replace(startTag, parseStartTag);
+        chars = false;
+      }
+    }
+
+    if (chars) {
+      index = html.indexOf('<');
+      let text = '';
+      while (index === 0) {
+        text += '<';
+        html = html.substring(1);
+        index = html.indexOf('<');
+      }
+      text += index < 0 ? html : html.substring(0, index);
+      html = index < 0 ? '' : html.substring(index);
+
+      if (handler.chars) handler.chars(text);
+    }
+
+    if (html === last) throw new Error(`Parse Error: ${html}`);
+    last = html;
+  }
+
+  // Clean up any remaining tags
+  parseEndTag();
+}
+
+export default HTMLParser;

+ 195 - 0
components/uParse/src/libs/wxDiscode.js

@@ -0,0 +1,195 @@
+// HTML 支持的数学符号
+function strNumDiscode(str) {
+  str = str.replace(/&forall;/g, '∀');
+  str = str.replace(/&part;/g, '∂');
+  str = str.replace(/&exist;/g, '∃');
+  str = str.replace(/&empty;/g, '∅');
+  str = str.replace(/&nabla;/g, '∇');
+  str = str.replace(/&isin;/g, '∈');
+  str = str.replace(/&notin;/g, '∉');
+  str = str.replace(/&ni;/g, '∋');
+  str = str.replace(/&prod;/g, '∏');
+  str = str.replace(/&sum;/g, '∑');
+  str = str.replace(/&minus;/g, '−');
+  str = str.replace(/&lowast;/g, '∗');
+  str = str.replace(/&radic;/g, '√');
+  str = str.replace(/&prop;/g, '∝');
+  str = str.replace(/&infin;/g, '∞');
+  str = str.replace(/&ang;/g, '∠');
+  str = str.replace(/&and;/g, '∧');
+  str = str.replace(/&or;/g, '∨');
+  str = str.replace(/&cap;/g, '∩');
+  str = str.replace(/&cup;/g, '∪');
+  str = str.replace(/&int;/g, '∫');
+  str = str.replace(/&there4;/g, '∴');
+  str = str.replace(/&sim;/g, '∼');
+  str = str.replace(/&cong;/g, '≅');
+  str = str.replace(/&asymp;/g, '≈');
+  str = str.replace(/&ne;/g, '≠');
+  str = str.replace(/&le;/g, '≤');
+  str = str.replace(/&ge;/g, '≥');
+  str = str.replace(/&sub;/g, '⊂');
+  str = str.replace(/&sup;/g, '⊃');
+  str = str.replace(/&nsub;/g, '⊄');
+  str = str.replace(/&sube;/g, '⊆');
+  str = str.replace(/&supe;/g, '⊇');
+  str = str.replace(/&oplus;/g, '⊕');
+  str = str.replace(/&otimes;/g, '⊗');
+  str = str.replace(/&perp;/g, '⊥');
+  str = str.replace(/&sdot;/g, '⋅');
+  return str;
+}
+
+// HTML 支持的希腊字母
+function strGreeceDiscode(str) {
+  str = str.replace(/&Alpha;/g, 'Α');
+  str = str.replace(/&Beta;/g, 'Β');
+  str = str.replace(/&Gamma;/g, 'Γ');
+  str = str.replace(/&Delta;/g, 'Δ');
+  str = str.replace(/&Epsilon;/g, 'Ε');
+  str = str.replace(/&Zeta;/g, 'Ζ');
+  str = str.replace(/&Eta;/g, 'Η');
+  str = str.replace(/&Theta;/g, 'Θ');
+  str = str.replace(/&Iota;/g, 'Ι');
+  str = str.replace(/&Kappa;/g, 'Κ');
+  str = str.replace(/&Lambda;/g, 'Λ');
+  str = str.replace(/&Mu;/g, 'Μ');
+  str = str.replace(/&Nu;/g, 'Ν');
+  str = str.replace(/&Xi;/g, 'Ν');
+  str = str.replace(/&Omicron;/g, 'Ο');
+  str = str.replace(/&Pi;/g, 'Π');
+  str = str.replace(/&Rho;/g, 'Ρ');
+  str = str.replace(/&Sigma;/g, 'Σ');
+  str = str.replace(/&Tau;/g, 'Τ');
+  str = str.replace(/&Upsilon;/g, 'Υ');
+  str = str.replace(/&Phi;/g, 'Φ');
+  str = str.replace(/&Chi;/g, 'Χ');
+  str = str.replace(/&Psi;/g, 'Ψ');
+  str = str.replace(/&Omega;/g, 'Ω');
+
+  str = str.replace(/&alpha;/g, 'α');
+  str = str.replace(/&beta;/g, 'β');
+  str = str.replace(/&gamma;/g, 'γ');
+  str = str.replace(/&delta;/g, 'δ');
+  str = str.replace(/&epsilon;/g, 'ε');
+  str = str.replace(/&zeta;/g, 'ζ');
+  str = str.replace(/&eta;/g, 'η');
+  str = str.replace(/&theta;/g, 'θ');
+  str = str.replace(/&iota;/g, 'ι');
+  str = str.replace(/&kappa;/g, 'κ');
+  str = str.replace(/&lambda;/g, 'λ');
+  str = str.replace(/&mu;/g, 'μ');
+  str = str.replace(/&nu;/g, 'ν');
+  str = str.replace(/&xi;/g, 'ξ');
+  str = str.replace(/&omicron;/g, 'ο');
+  str = str.replace(/&pi;/g, 'π');
+  str = str.replace(/&rho;/g, 'ρ');
+  str = str.replace(/&sigmaf;/g, 'ς');
+  str = str.replace(/&sigma;/g, 'σ');
+  str = str.replace(/&tau;/g, 'τ');
+  str = str.replace(/&upsilon;/g, 'υ');
+  str = str.replace(/&phi;/g, 'φ');
+  str = str.replace(/&chi;/g, 'χ');
+  str = str.replace(/&psi;/g, 'ψ');
+  str = str.replace(/&omega;/g, 'ω');
+  str = str.replace(/&thetasym;/g, 'ϑ');
+  str = str.replace(/&upsih;/g, 'ϒ');
+  str = str.replace(/&piv;/g, 'ϖ');
+  str = str.replace(/&middot;/g, '·');
+  return str;
+}
+
+function strcharacterDiscode(str) {
+  // 加入常用解析
+  str = str.replace(/&nbsp;/g, ' ');
+  str = str.replace(/&ensp;/g, ' ');
+  str = str.replace(/&emsp;/g, ' ');
+  str = str.replace(/&quot;/g, "'");
+  str = str.replace(/&amp;/g, '&');
+  str = str.replace(/&lt;/g, '<');
+  str = str.replace(/&gt;/g, '>');
+  str = str.replace(/&#8226;/g, '•');
+
+  return str;
+}
+
+// HTML 支持的其他实体
+function strOtherDiscode(str) {
+  str = str.replace(/&OElig;/g, 'Œ');
+  str = str.replace(/&oelig;/g, 'œ');
+  str = str.replace(/&Scaron;/g, 'Š');
+  str = str.replace(/&scaron;/g, 'š');
+  str = str.replace(/&Yuml;/g, 'Ÿ');
+  str = str.replace(/&fnof;/g, 'ƒ');
+  str = str.replace(/&circ;/g, 'ˆ');
+  str = str.replace(/&tilde;/g, '˜');
+  str = str.replace(/&ensp;/g, '');
+  str = str.replace(/&emsp;/g, '');
+  str = str.replace(/&thinsp;/g, '');
+  str = str.replace(/&zwnj;/g, '');
+  str = str.replace(/&zwj;/g, '');
+  str = str.replace(/&lrm;/g, '');
+  str = str.replace(/&rlm;/g, '');
+  str = str.replace(/&ndash;/g, '–');
+  str = str.replace(/&mdash;/g, '—');
+  str = str.replace(/&lsquo;/g, '‘');
+  str = str.replace(/&rsquo;/g, '’');
+  str = str.replace(/&sbquo;/g, '‚');
+  str = str.replace(/&ldquo;/g, '“');
+  str = str.replace(/&rdquo;/g, '”');
+  str = str.replace(/&bdquo;/g, '„');
+  str = str.replace(/&dagger;/g, '†');
+  str = str.replace(/&Dagger;/g, '‡');
+  str = str.replace(/&bull;/g, '•');
+  str = str.replace(/&hellip;/g, '…');
+  str = str.replace(/&permil;/g, '‰');
+  str = str.replace(/&prime;/g, '′');
+  str = str.replace(/&Prime;/g, '″');
+  str = str.replace(/&lsaquo;/g, '‹');
+  str = str.replace(/&rsaquo;/g, '›');
+  str = str.replace(/&oline;/g, '‾');
+  str = str.replace(/&euro;/g, '€');
+  str = str.replace(/&trade;/g, '™');
+
+  str = str.replace(/&larr;/g, '←');
+  str = str.replace(/&uarr;/g, '↑');
+  str = str.replace(/&rarr;/g, '→');
+  str = str.replace(/&darr;/g, '↓');
+  str = str.replace(/&harr;/g, '↔');
+  str = str.replace(/&crarr;/g, '↵');
+  str = str.replace(/&lceil;/g, '⌈');
+  str = str.replace(/&rceil;/g, '⌉');
+
+  str = str.replace(/&lfloor;/g, '⌊');
+  str = str.replace(/&rfloor;/g, '⌋');
+  str = str.replace(/&loz;/g, '◊');
+  str = str.replace(/&spades;/g, '♠');
+  str = str.replace(/&clubs;/g, '♣');
+  str = str.replace(/&hearts;/g, '♥');
+
+  str = str.replace(/&diams;/g, '♦');
+  str = str.replace(/&#39;/g, "'");
+  return str;
+}
+
+function strDiscode(str) {
+  str = strNumDiscode(str);
+  str = strGreeceDiscode(str);
+  str = strcharacterDiscode(str);
+  str = strOtherDiscode(str);
+  return str;
+}
+
+function urlToHttpUrl(url, domain) {
+  if (/^\/\//.test(url)) {
+    return `https:${url}`;
+  } else if (/^\//.test(url)) {
+    return `https://${domain}${url}`;
+  }
+  return url;
+}
+
+export default {
+  strDiscode,
+  urlToHttpUrl,
+};

+ 493 - 0
components/uParse/src/wxParse.css

@@ -0,0 +1,493 @@
+/* #ifndef APP-PLUS-NVUE */
+.wxParse {
+  width: 100%;
+  font-family: Helvetica, sans-serif;
+  font-size: 30upx;
+  color: #666;
+  line-height: 1.8;
+}
+
+.wxParse view {
+  word-break: hyphenate;
+}
+
+.wxParse .inline {
+  display: inline;
+  margin: 0;
+  padding: 0;
+}
+
+.wxParse .div {
+  margin: 0;
+  padding: 0;
+}
+
+.wxParse .h1 .text {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+.wxParse .h2 .text {
+  font-size: 1.5em;
+  margin: 0.83em 0;
+}
+.wxParse .h3 .text {
+  font-size: 1.17em;
+  margin: 1em 0;
+}
+.wxParse .h4 .text {
+  margin: 1.33em 0;
+}
+.wxParse .h5 .text {
+  font-size: 0.83em;
+  margin: 1.67em 0;
+}
+.wxParse .h6 .text {
+  font-size: 0.67em;
+  margin: 2.33em 0;
+}
+
+.wxParse .h1 .text,
+.wxParse .h2 .text,
+.wxParse .h3 .text,
+.wxParse .h4 .text,
+.wxParse .h5 .text,
+.wxParse .h6 .text,
+.wxParse .b,
+.wxParse .strong {
+  font-weight: bolder;
+}
+
+
+.wxParse .p {
+  margin: 1em 0;
+}
+
+.wxParse .i,
+.wxParse .cite,
+.wxParse .em,
+.wxParse .var,
+.wxParse .address {
+  font-style: italic;
+}
+
+.wxParse .pre,
+.wxParse .tt,
+.wxParse .code,
+.wxParse .kbd,
+.wxParse .samp {
+  font-family: monospace;
+}
+.wxParse .pre {
+  overflow: auto;
+  background: #f5f5f5;
+  padding: 16upx;
+  white-space: pre;
+  margin: 1em 0upx;
+}
+.wxParse .code {
+  display: inline;
+  background: #f5f5f5;
+}
+
+.wxParse .big {
+  font-size: 1.17em;
+}
+
+.wxParse .small,
+.wxParse .sub,
+.wxParse .sup {
+  font-size: 0.83em;
+}
+
+.wxParse .sub {
+  vertical-align: sub;
+}
+.wxParse .sup {
+  vertical-align: super;
+}
+
+.wxParse .s,
+.wxParse .strike,
+.wxParse .del {
+  text-decoration: line-through;
+}
+
+.wxParse .strong,
+.wxParse .s {
+  display: inline;
+}
+
+.wxParse .a {
+  color: deepskyblue;
+}
+
+.wxParse .video {
+  text-align: center;
+  margin: 22upx 0;
+}
+
+.wxParse .video-video {
+  width: 100%;
+}
+
+.wxParse .img {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  max-width: 100%;
+  overflow: hidden;
+}
+
+.wxParse .blockquote {
+  margin: 10upx 0;
+  padding: 22upx 0 22upx 22upx;
+  font-family: Courier, Calibri, "宋体";
+  background: #f5f5f5;
+  border-left: 6upx solid #dbdbdb;
+}
+.wxParse .blockquote .p {
+  margin: 0;
+}
+
+.wxParse .ul, .wxParse .ol {
+  display: block;
+  margin: 1em 0;
+  padding-left: 33upx;
+}
+.wxParse .ol {
+  list-style-type: disc;
+}
+.wxParse .ol {
+  list-style-type: decimal;
+}
+.wxParse .ol>weixin-parse-template,.wxParse .ul>weixin-parse-template {
+  display: list-item;
+  align-items: baseline;
+  text-align: match-parent;
+}
+
+.wxParse .ol>.li,.wxParse .ul>.li {
+  display: list-item;
+  align-items: baseline;
+  text-align: match-parent;
+}
+.wxParse .ul .ul, .wxParse .ol .ul {
+  list-style-type: circle;
+}
+.wxParse .ol .ol .ul, .wxParse .ol .ul .ul, .wxParse .ul .ol .ul, .wxParse .ul .ul .ul {
+    list-style-type: square;
+}
+
+.wxParse .u {
+  text-decoration: underline;
+}
+.wxParse .hide {
+  display: none;
+}
+.wxParse .del {
+  display: inline;
+}
+.wxParse .figure {
+  overflow: hidden;
+}
+
+.wxParse .table {
+  width: 100%;
+}
+.wxParse .thead, .wxParse .tfoot, .wxParse .tr {
+  display: flex;
+  flex-direction: row;
+}
+.wxParse .tr {
+  width:100%;
+  display: flex;
+  border-right: 2upx solid #e0e0e0;
+  border-bottom: 2upx solid #e0e0e0;
+}
+.wxParse .th,
+.wxParse .td {
+  display: flex;
+  width: 1276upx;
+  overflow: auto;
+  flex: 1;
+  padding: 11upx;
+  border-left: 2upx solid #e0e0e0;
+}
+.wxParse .td:last {
+  border-top: 2upx solid #e0e0e0;
+}
+.wxParse .th {
+  background: #f0f0f0;
+  border-top: 2upx solid #e0e0e0;
+}
+/* #endif */
+
+/* #ifdef APP-PLUS-NVUE */
+ .wxParse {
+  width: 750upx;
+  font-family: Helvetica, sans-serif;
+  font-size: 32px;
+  color: #666;
+  line-height: 1.8;
+  display: flex;
+  flex-direction: column;
+}
+
+.wxParse view {
+  word-break: hyphenate;
+}
+
+.wxParse .inline {
+  display: inline;
+  margin-top: 0;
+  margin-bottom: 0;
+  margin-left: 0;
+  margin-right: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.wxParse .div {
+  width: 750upx;
+  margin-top: 0;
+  margin-bottom: 0;
+  margin-left: 0;
+  margin-right: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.wxParse .h1 .text {
+  font-size: 32px;
+  margin-top: 0.67em;
+  margin-bottom: 0.67em;
+}
+.wxParse .h2 .text {
+  font-size: 32px;
+  margin-top: 0.83em;
+  margin-bottom: 0.83em;
+}
+.wxParse .h3 .text {
+  font-size: 32px;
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
+.wxParse .h4 .text {
+  margin-top: 1.33em;
+  margin-bottom: 1.33em;
+}
+.wxParse .h5 .text {
+  font-size: 32px;
+  margin-top: 1.67em;
+  margin-bottom: 1.67em;
+}
+.wxParse .h6 .text {
+  font-size: 32px;
+  margin-top: 2.33em;
+  margin-bottom: 2.33em;
+}
+
+.wxParse .h1 .text,
+.wxParse .h2 .text,
+.wxParse .h3 .text,
+.wxParse .h4 .text,
+.wxParse .h5 .text,
+.wxParse .h6 .text,
+.wxParse .b,
+.wxParse .strong {
+  font-weight: bolder;
+}
+
+
+.wxParse .p {
+  width: 750upx;
+  margin: 1em 0;
+}
+.wxParse p {
+  width: 750upx;
+}
+
+.wxParse .i,
+.wxParse .cite,
+.wxParse .em,
+.wxParse .var,
+.wxParse .address {
+  font-style: italic;
+}
+
+.wxParse .pre,
+.wxParse .tt,
+.wxParse .code,
+.wxParse .kbd,
+.wxParse .samp {
+  font-family: monospace;
+}
+.wxParse .pre {
+  overflow: auto;
+  background-color:#f5f5f5;
+  padding-top: 16upx;
+  padding-left: 16upx;
+  padding-right: 16upx;
+  padding-bottom: 16upx;
+  white-space: pre;
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
+.wxParse .code {
+  display: inline;
+  background: #f5f5f5;
+}
+
+.wxParse .big {
+  font-size: 32px;
+}
+
+.wxParse .small,
+.wxParse .sub,
+.wxParse .sup {
+  font-size: 32px;
+}
+
+.wxParse .sub {
+  vertical-align: sub;
+}
+.wxParse .sup {
+  vertical-align: super;
+}
+
+.wxParse .s,
+.wxParse .strike,
+.wxParse .del {
+  text-decoration: line-through;
+}
+
+.wxParse .strong,
+.wxParse .s {
+  display: inline;
+}
+
+.wxParse .a {
+  color: deepskyblue;
+}
+
+.wxParse .video {
+  text-align: center;
+  margin-top: 22upx;
+  margin-bottom: 22upx;
+}
+
+.wxParse .video-video {
+  width: 750upx;
+}
+
+.wxParse .img {
+  display: inline-block;
+  width: 750upx;
+  height: auto;
+  overflow: hidden;
+}
+
+.wxParse .blockquote {
+  margin-top: 10upx;
+  margin-bottom: 10upx;
+
+  padding-top: 22upx;
+  padding-bottom:22upx;
+  padding-left:22upx;
+  font-family: Courier, Calibri, "宋体";
+  background-color: #f5f5f5;
+
+  border-left-width: 6upx;
+  border-left-style: solid;
+  border-left-color: #dbdbdb;
+}
+.wxParse .blockquote .p {
+  margin: 0;
+}
+
+.wxParse .ul, .wxParse .ol {
+  display: block;
+  margin: 1em 0;
+  padding-left: 33upx;
+}
+.wxParse .ol {
+  list-style-type: disc;
+}
+.wxParse .ol {
+  list-style-type: decimal;
+}
+.wxParse .ol>weixin-parse-template,.wxParse .ul>weixin-parse-template {
+  display: list-item;
+  align-items: baseline;
+  text-align: match-parent;
+}
+
+.wxParse .ol>.li,.wxParse .ul>.li {
+  display: list-item;
+  align-items: baseline;
+  text-align: match-parent;
+}
+.wxParse .ul .ul, .wxParse .ol .ul {
+  list-style-type: circle;
+}
+.wxParse .ol .ol .ul, .wxParse .ol .ul .ul, .wxParse .ul .ol .ul, .wxParse .ul .ul .ul {
+  list-style-type: square;
+}
+
+.wxParse .u {
+  text-decoration: underline;
+}
+.wxParse .hide {
+  display: none;
+}
+.wxParse .del {
+  display: inline;
+}
+.wxParse .figure {
+  overflow: hidden;
+}
+
+.wxParse .table {
+  width: 750upx;
+}
+.wxParse .thead, .wxParse .tfoot, .wxParse .tr {
+  display: flex;
+  flex-direction: row;
+}
+.wxParse .tr {
+  width:750upx;
+  flex-direction:row;
+  border-right: 2upx solid #e0e0e0;
+  border-bottom: 2upx solid #e0e0e0;
+}
+.wxParse .th,
+.wxParse .td {
+  flex-direction:row;
+  width: 1276upx;
+  overflow: auto;
+  flex: 1;
+  padding-top: 11upx;
+  padding-left: 11upx;
+  padding-right: 11upx;
+  padding-bottom: 11upx;
+
+  border-top-width: 2upx;
+  border-top-style: solid;
+  border-top-color: #e0e0e0;
+}
+.wxParse .td:last {
+  border-top-width: 2upx;
+  border-top-style: solid;
+  border-top-color: #e0e0e0;
+}
+.wxParse .th {
+  background-color: #f0f0f0;
+  border-top-width: 2upx;
+  border-top-style: solid;
+  border-top-color: #e0e0e0;
+}
+/* #endif */

+ 122 - 0
components/uParse/src/wxParse.vue

@@ -0,0 +1,122 @@
+<!--**
+ * forked from:https://github.com/F-loat/mpvue-wxParse
+ *
+ * github地址: https://github.com/dcloudio/uParse
+ *
+ * for: uni-app框架下 富文本解析
+ */-->
+
+<template>
+<!--基础元素-->
+<div class="wxParse" :class="className" v-if="!loading">
+  <block v-for="(node,index) of nodes" :key="index">
+    <wxParseTemplate :node="node" />
+  </block>
+</div>
+</template>
+
+<script>
+import HtmlToJson from './libs/html2json';
+import wxParseTemplate from './components/wxParseTemplate0';
+
+export default {
+  name: 'wxParse',
+  props: {
+    loading: {
+      type: Boolean,
+      default: false,
+    },
+    className: {
+      type: String,
+      default: '',
+    },
+    content: {
+      type: String,
+      default: '',
+    },
+    noData: {
+      type: String,
+      default: '<div style="color: white;">数据不能为空</div>',
+    },
+    startHandler: {
+      type: Function,
+      default() {
+        return (node) => {
+          node.attr.class = null;
+          node.attr.style = null;
+        };
+      },
+    },
+    endHandler: {
+      type: Function,
+      default: null,
+    },
+    charsHandler: {
+      type: Function,
+      default: null,
+    },
+    imageProp: {
+      type: Object,
+      default() {
+        return {
+          mode: 'aspectFit',
+          padding: 0,
+          lazyLoad: false,
+          domain: '',
+        };
+      },
+    },
+  },
+	provide() {
+		return {
+			uparse: this
+		}
+	},
+  components: {
+    wxParseTemplate,
+  },
+  data() {
+    return {
+      imageUrls: []
+    };
+  },
+  computed: {
+    nodes() {
+      const {
+        content,
+        noData,
+        imageProp,
+        startHandler,
+        endHandler,
+        charsHandler,
+      } = this;
+      const parseData = content || noData;
+      const customHandler = {
+        start: startHandler,
+        end: endHandler,
+        chars: charsHandler,
+      };
+      const results = HtmlToJson(parseData, customHandler, imageProp, this);
+      this.imageUrls = results.imageUrls;
+      return results.nodes;
+    },
+  },
+  methods: {
+    navigate(href, $event) {
+      this.$emit('navigate', href, $event);
+    },
+    preview(src, $event) {
+      if (!this.imageUrls.length) return;
+      wx.previewImage({
+        current: src,
+        urls: this.imageUrls,
+      });
+      this.$emit('preview', src, $event);
+    },
+    removeImageUrl(src) {
+      const { imageUrls } = this;
+      imageUrls.splice(imageUrls.indexOf(src), 1);
+    },
+  },
+};
+</script>

+ 92 - 0
components/uc-parse/uc-parse.vue

@@ -0,0 +1,92 @@
+<template>
+	<view>
+		<view >
+			<rich-text :nodes="getnodes" :preview="true"></rich-text>
+		</view>
+	</view>
+</template>
+
+<script>
+	import htmlParser from '@/common/html-parser';
+	
+	export default {
+		name: 'timuJiexi',
+		props: {
+			node: {
+				type: String,
+				default: '',
+			},
+			from:{
+				type:Number,
+				default:1
+			},
+			display_type:{
+				type:Number,
+				default:1
+			}
+		},
+		data(){
+			return{
+				imageWidth:0,
+				imageHeight:0,
+				myNode:'',
+			}
+		},
+		created() {
+		},
+		computed:{
+			getnodes(){
+				if(this.node != ''){
+					this.myNode = this.node
+					let nodes = [];
+					try{
+						nodes = htmlParser("<p>&nbsp;"+this.myNode.trim()+"&nbsp;</p>");
+					}catch{
+						return ''
+						// 解析错误,请联系管理员!
+					}
+					return this.parseImgs(nodes) || ''
+				}
+			},
+			
+		},
+		methods:{
+			parseImgs(nodes) {
+				nodes.forEach(node => {
+					if (node.name === 'img') {
+						node.attrs.style = `max-width:100%;vertical-align:middle;`
+						// let width = '',that = this;
+						// uni.getSystemInfo({
+						// 	success: function (res) {
+						// 		if(that.from == 1){
+						// 			width = res.windowWidth - 20*2
+						// 		}else{
+						// 			if(that.display_type == 1){
+						// 				width = res.windowWidth - (3+2)*2*0.01*res.windowWidth -30 -16
+						// 			}else{
+						// 				width = res.windowWidth*0.45 -30 -16
+						// 			}
+						// 		}
+						// 	}
+						// });
+					 //    node.attrs.style = `width:${width}px; vertical-align:middle;`
+					}else{
+						node.style = `vertical-align:middle;`
+					}
+					// node.style = `vertical-align:middle;`
+					if (Array.isArray(node.children)) {
+						this.parseImgs(node.children);
+					}
+				});
+				
+				return nodes;
+			}
+		}
+	}
+</script>
+
+<style>
+	img {
+		max-width: 100px;
+	}
+</style>

+ 105 - 0
components/uni-badge/uni-badge.vue

@@ -0,0 +1,105 @@
+<template>
+  <text
+    v-if="text"
+    :class="inverted ? 'uni-badge-' + type + ' uni-badge--' + size + ' uni-badge-inverted' : 'uni-badge-' + type + ' uni-badge--' + size"
+    class="uni-badge"
+    @click="onClick()">{{ text }}</text>
+</template>
+
+<script>
+export default {
+  name: 'UniBadge',
+  props: {
+    type: {
+      type: String,
+      default: 'default'
+    },
+    inverted: {
+      type: Boolean,
+      default: false
+    },
+    text: {
+      type: [String, Number],
+      default: ''
+    },
+    size: { // small.normal
+      type: String,
+      default: 'normal'
+    }
+  },
+  methods: {
+    onClick () {
+      this.$emit('click')
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+	$bage-size:12px;
+	$bage-small:scale(0.8);
+
+	.uni-badge {
+		font-family: 'Helvetica Neue', Helvetica, sans-serif;
+		box-sizing: border-box;
+		font-size: $bage-size;
+		line-height: 1;
+		display: inline-block;
+		padding: 3px 6px;
+		color: $uni-text-color;
+		border-radius: 100px;
+		background-color: $uni-bg-color-hover;
+
+		&.uni-badge-inverted {
+			padding: 0 5px 0 0;
+			color: $uni-text-color-grey;
+			background-color: transparent;
+		}
+
+		&-primary {
+			color: $uni-text-color-inverse;
+			background-color: $uni-color-primary;
+
+			&.uni-badge-inverted {
+				color: $uni-color-primary;
+				background-color: transparent
+			}
+		}
+
+		&-success {
+            color: #5d5d5d;
+            background-color: #fff;
+            font-size: 14px;
+
+			&.uni-badge-inverted {
+				color: $uni-color-success;
+				background-color: transparent
+			}
+		}
+
+		&-warning {
+			color: $uni-text-color-inverse;
+			background-color: $uni-color-warning;
+
+			&.uni-badge-inverted {
+				color: $uni-color-warning;
+				background-color: transparent
+			}
+		}
+
+		&-error {
+			color: $uni-text-color-inverse;
+			background-color: $uni-color-error;
+
+			&.uni-badge-inverted {
+				color: $uni-color-error;
+				background-color: transparent
+			}
+		}
+
+		&--small {
+			transform: $bage-small;
+			transform-origin: center center;
+		}
+	}
+</style>

+ 170 - 0
components/uni-countdown/uni-countdown.vue

@@ -0,0 +1,170 @@
+<template>
+  <view class="uni-countdown">
+    <view
+      v-if="showDay"
+      :style="{borderColor:borderColor, color:color, background:backgroundColor}"
+      class="uni-countdown__number">{{ d }}</view>
+    <view
+      v-if="showDay"
+      :style="{color:splitorColor}"
+      class="uni-countdown__splitor">天</view>
+    <view
+      :style="{borderColor:borderColor, color:color, background:backgroundColor}"
+      class="uni-countdown__number">{{ h }}</view>
+    <view
+      :style="{color:splitorColor}"
+      class="uni-countdown__splitor">{{ showColon ? ':' : '时' }}</view>
+    <view
+      :style="{borderColor:borderColor, color:color, background:backgroundColor}"
+      class="uni-countdown__number">{{ i }}</view>
+    <view
+      :style="{color:splitorColor}"
+      class="uni-countdown__splitor">{{ showColon ? ':' : '分' }}</view>
+    <view
+      :style="{borderColor:borderColor, color:color, background:backgroundColor}"
+      class="uni-countdown__number">{{ s }}</view>
+    <view
+      v-if="!showColon"
+      :style="{color:splitorColor}"
+      class="uni-countdown__splitor">秒</view>
+  </view>
+</template>
+<script>
+export default {
+  name: 'UniCountdown',
+  props: {
+    showDay: {
+      type: Boolean,
+      default: true
+    },
+    showColon: {
+      type: Boolean,
+      default: true
+    },
+    backgroundColor: {
+      type: String,
+      default: '#FFFFFF'
+    },
+    borderColor: {
+      type: String,
+      default: '#000000'
+    },
+    color: {
+      type: String,
+      default: '#000000'
+    },
+    splitorColor: {
+      type: String,
+      default: '#000000'
+    },
+    day: {
+      type: Number,
+      default: 0
+    },
+    hour: {
+      type: Number,
+      default: 0
+    },
+    minute: {
+      type: Number,
+      default: 0
+    },
+    second: {
+      type: Number,
+      default: 0
+    }
+  },
+  data () {
+    return {
+      timer: null,
+      d: '00',
+      h: '00',
+      i: '00',
+      s: '00',
+      leftTime: 0,
+      seconds: 0
+    }
+  },
+  created: function (e) {
+    this.seconds = this.toSeconds(this.day, this.hour, this.minute, this.second)
+    this.countDown()
+    this.timer = setInterval(() => {
+      this.seconds--
+      if (this.seconds < 0) {
+        this.timeUp()
+        return
+      }
+      this.countDown()
+    }, 1000)
+  },
+  beforeDestroy () {
+    clearInterval(this.timer)
+  },
+  methods: {
+    toSeconds (day, hours, minutes, seconds) {
+      return (day * 60 * 60 * 24) + (hours * 60 * 60) + (minutes * 60) + seconds
+    },
+    timeUp () {
+      clearInterval(this.timer)
+      this.$emit('timeup')
+    },
+    countDown () {
+      let seconds = this.seconds
+      let [day, hour, minute, second] = [0, 0, 0, 0]
+      if (seconds > 0) {
+        day = Math.floor(seconds / (60 * 60 * 24))
+        hour = Math.floor(seconds / (60 * 60)) - (day * 24)
+        minute = Math.floor(seconds / 60) - (day * 24 * 60) - (hour * 60)
+        second = Math.floor(seconds) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60)
+      } else {
+        this.timeUp()
+      }
+      if (day < 10) {
+        day = '0' + day
+      }
+      if (hour < 10) {
+        hour = '0' + hour
+      }
+      if (minute < 10) {
+        minute = '0' + minute
+      }
+      if (second < 10) {
+        second = '0' + second
+      }
+      this.d = day
+      this.h = hour
+      this.i = minute
+      this.s = second
+    }
+  }
+}
+</script>
+<style lang="scss">
+	$countdown-height:44upx;
+
+	.uni-countdown {
+		padding: 2upx 0;
+		display: inline-flex;
+		flex-wrap: nowrap;
+		justify-content: center;
+
+		&__splitor {
+			justify-content: center;
+			line-height: $countdown-height;
+			padding: 0 5upx;
+            font-size: $uni-font-size-base;
+		}
+
+		&__number {
+			line-height: $countdown-height;
+			justify-content: center;
+			height: $countdown-height;
+			border-radius: $uni-border-radius-base;
+			margin: 0 5upx;
+			font-size: $uni-font-size-base;
+			border: 1px solid #000000;
+			font-size: $uni-font-size-sm;
+			padding: 0 10upx;
+		}
+	}
+</style>

File diff suppressed because it is too large
+ 39 - 0
components/uni-icon/uni-icon.vue


+ 199 - 0
components/uni-list-item/uni-list-item.vue

@@ -0,0 +1,199 @@
+<template>
+	<view class="uni-list-cell" :class="[disabled === true || disabled === 'true' ? 'uni-list-cell--disabled' : '']"
+	 :hover-class="disabled === true || disabled === 'true' || showSwitch === true || showSwitch === 'true' ? '' : 'uni-list-cell--hover'" @click="onClick">
+		<view class="uni-list-cell__container">
+			<view class="uni-list-cell__icon" v-if="thumb">
+				<image class="uni-list-cell__icon-img" :src="thumb"></image>
+			</view>
+			<view class="uni-list-cell__icon" v-else-if="showExtraIcon === true || showExtraIcon === 'true'">
+				<uni-icon :color="extraIcon.color" :size="extraIcon.size" :type="extraIcon.type"></uni-icon>
+			</view>
+			<view class="uni-list-cell__content">
+				<view class="uni-list-cell__content-title">{{title}}</view>
+				<view class="uni-list-cell__content-note" v-if="note">{{note}}</view>
+			</view>
+			<view class="uni-list-cell__extra" v-if="showBadge === true || showBadge === 'true' || showArrow === true || showArrow === 'true'||showSwitch === true || showSwitch === 'true'">
+				<uni-badge v-if="showBadge === true || showBadge === 'true'" :type="badgeType" :text="badgeText"></uni-badge>
+				<switch v-if="showSwitch === true || showSwitch === 'true'" :disabled='disabled' :checked="switchChecked" @change="onSwitchChange" />
+				<!-- <uni-icon v-if="showArrow === true || showArrow === 'true'" color="#bbb" size="20" type="arrowright"></uni-icon> -->
+				<view v-if="showArrow === true || showArrow === 'true'" class="iconfont icon-arrow" style="font-size:14px;color: #acacac;"></view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import uniIcon from '../uni-icon/uni-icon.vue'
+	import uniBadge from '../uni-badge/uni-badge.vue'
+	export default {
+		name: 'uni-list-item',
+		components: {
+			uniIcon,
+			uniBadge
+		},
+		data() {
+			return {
+
+			};
+		},
+		props: {
+			title: String, //列表标题
+			note: String, //列表描述
+			disabled: { //是否禁用
+				type: [Boolean, String],
+				default: false
+			},
+			showArrow: { //是否显示箭头
+				type: [Boolean, String],
+				default: true
+			},
+			showBadge: { //是否显示数字角标
+				type: [Boolean, String],
+				default: false
+			},
+			showSwitch: { //是否显示Switch
+				type: [Boolean, String],
+				default: false
+			},
+			switchChecked: { //Switch是否被选中
+				type: [Boolean, String],
+				default: false
+			},
+			badgeText: String, //badge内容
+			badgeType: { //badge类型
+				type: String,
+				default: 'success'
+			},
+			thumb: String, //缩略图
+			showExtraIcon: { //是否显示扩展图标
+				type: [Boolean, String],
+				default: false
+			},
+			extraIcon: {
+				type: Object,
+				default () {
+					return {
+						type: "contact",
+						color: "#000000",
+						size: "20"
+					};
+				}
+			}
+		},
+		methods: {
+			onClick() {
+				this.$emit('click')
+			},
+			onSwitchChange(e) {
+				this.$emit('switchChange', e.detail)
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	@mixin list-hover {
+		background-color: $uni-bg-color-hover;
+	}
+
+	@mixin list-disabled {
+		opacity: 0.3;
+	}
+
+	$list-cell-pd:$uni-spacing-col-lg $uni-spacing-row-lg;
+
+	.uni-list-cell {
+		font-size: $uni-font-size-lg;
+		position: relative;
+		display: flex;
+		flex-direction: column;
+		justify-content: space-between;
+		align-items: center;
+
+		&--disabled {
+			@include list-disabled;
+		}
+
+		&--hover {
+			@include list-hover;
+		}
+
+		&__container {
+			padding: 14px 15px;
+			width: 100%;
+			box-sizing: border-box;
+			flex: 1;
+			position: relative;
+			display: flex;
+			flex-direction: row;
+			justify-content: space-between;
+			align-items: center;
+			border-bottom: solid 1px #f1f1f1;
+
+			&:after {
+				position: absolute;
+				z-index: 3;
+				right: 0;
+				bottom: 0;
+				left: 30upx;
+				height: 1px;
+				content: '';
+				-webkit-transform: scaleY(.5);
+				transform: scaleY(.5);
+				//background-color: $uni-border-color;
+			}
+		}
+
+		&__content {
+			flex: 1;
+			overflow: hidden;
+			display: flex;
+			flex-direction: column;
+			text-align: left;
+
+			&-title {
+				font-size: 14px;
+				text-overflow: ellipsis;
+				white-space: nowrap;
+				color: #484848;
+				line-height: 1.5;
+				overflow: hidden;
+			}
+
+			&-note {
+				color: $uni-text-color-grey;
+				font-size: $uni-font-size-base;
+				white-space: normal;
+				display: -webkit-box;
+				-webkit-box-orient: vertical;
+				-webkit-line-clamp: 2;
+				overflow: hidden;
+			}
+		}
+
+		&__extra {
+			width: 60%;
+			display: flex;
+			flex-direction: row;
+			justify-content: flex-end;
+			align-items: center;
+		}
+
+		&__icon {
+			margin-right: 18upx;
+			display: flex;
+			flex-direction: row;
+			justify-content: center;
+			align-items: center;
+
+			&-img {
+				height: 23px;
+				width: 23px;
+			}
+		}
+	}
+
+	.uni-list>.uni-list-cell:last-child .uni-list-cell-container:after {
+		height: 0px;
+	}
+</style>

+ 88 - 0
components/uni-list/readme.md

@@ -0,0 +1,88 @@
+### List 列表
+
+列表组件,组件名:``uni-list``、``uni-list-item``,代码块: uList。
+
+**使用方式:**
+
+在 ``script`` 中引用组件 
+
+```javascript
+import uniList from '@/components/uni-list/uni-list.vue'
+import uniListItem from '@/components/uni-list-item/uni-list-item.vue'
+export default {
+    components: {uniList,uniListItem}
+}
+```
+
+List 一般用法
+
+```html
+<uni-list>
+    <uni-list-item title="标题文字" show-arrow="false"></uni-list-item>
+    <uni-list-item title="标题文字"></uni-list-item>
+    <uni-list-item title="标题文字" show-badge="true" badge-text="12"></uni-list-item>
+    <uni-list-item title="禁用状态" disabled="true" show-badge="true" badge-text="12"></uni-list-item>
+</uni-list>
+```
+
+带描述信息
+
+```html
+<uni-list>
+    <uni-list-item title="标题文字" note="描述信息"></uni-list-item>
+    <uni-list-item title="标题文字" note="描述信息" show-badge="true" badge-text="12"></uni-list-item>
+</uni-list>
+```
+
+包含图片
+
+```html
+<uni-list>
+    <uni-list-item title="标题文字" thumb="http://img-cdn-qiniu.dcloud.net.cn/new-page/hx.png"></uni-list-item>
+</uni-list>
+```
+
+包含图标
+
+```html
+<uni-list>
+    <uni-list-item title="标题文字" 
+        show-extra-icon="true" 
+        :extra-icon="{color: '#4cd964',size: '22',type: 'spinner'}">
+    </uni-list-item>
+</uni-list>
+```
+
+显示Switch
+
+```html
+<uni-list>
+    <uni-list-item title="标题文字" show-switch="true" show-arrow="false"></uni-list-item>
+</uni-list>
+```
+
+实际效果参考:[https://github.com/dcloudio/uni-ui](https://github.com/dcloudio/uni-ui)
+
+**uniListItem 属性说明:**
+
+|属性名|类型|默认值	|说明|
+|---|----|---|---|
+|title|String|-|标题|
+|note|String|-|描述|
+|disabled|Boolean|false|是否禁用|
+|show-arrow|Boolean|true|是否显示箭头图标|
+|show-badge|Boolean|false|是否显示数字角标|
+|badge-text|String|-|数字角标内容|
+|badge-type|String|-|数字角标类型,参考[Badge 数字角标](https://ext.dcloud.net.cn/plugin?id=21)|
+|show-switch|Boolean|false|是否显示Switch|
+|switch-checked|Boolean|false|Switch是否被选中|
+|show-extra-icon|Boolean|false|左侧是否显示扩展图标|
+|extra-icon|Object|-|扩展图标参数,格式为 ``{color: '#4cd964',size: '22',type: 'spinner'}``,参考 [Iocn 图标](https://ext.dcloud.net.cn/plugin?id=28)|
+|thumb|String|-|左侧缩略图,若thumb有值,则不会显示扩展图标|
+
+**uniListItem 事件说明:**
+
+|事件称名|说明|返回参数|
+|---|----|---|
+|click|点击 uniListItem 触发事件|-|
+|switchChange|点击切换 Switch 时触发|{value:checked}|

+ 45 - 0
components/uni-list/uni-list.vue

@@ -0,0 +1,45 @@
+<template>
+	<view class="uni-list">
+		<slot></slot>
+	</view>
+</template>
+<script>
+	export default {
+		name: 'uni-list'
+	}
+</script>
+<style lang="scss">
+	.uni-list {
+		background-color: $uni-bg-color;
+		position: relative;
+		width: 100%;
+		display: flex;
+		flex-direction: column;
+
+		&:after {
+			position: absolute;
+			z-index: 10;
+			right: 0;
+			bottom: 0;
+			left: 0;
+			height: 1px;
+			content: '';
+			-webkit-transform: scaleY(0.5);
+			transform: scaleY(0.5);
+			//background-color: $uni-border-color;
+		}
+
+		&:before {
+			position: absolute;
+			z-index: 10;
+			right: 0;
+			top: 0;
+			left: 0;
+			height: 1px;
+			content: '';
+			-webkit-transform: scaleY(0.5);
+			transform: scaleY(0.5);
+			//background-color: $uni-border-color;
+		}
+	}
+</style>

+ 124 - 0
components/uni-segmented-control/uni-segmented-control.vue

@@ -0,0 +1,124 @@
+<template>
+  <view
+    :class="{ text: styleType === 'text' }"
+    :style="{ borderColor: styleType === 'text' ? '' : activeColor }"
+    class="segmented-control"
+  >
+    <view
+      v-for="(item, index) in values"
+      :class="[{ text: styleType === 'text' }, { active: index === currentIndex }]"
+      :key="index"
+      :style="{
+        color:
+          index === currentIndex
+            ? styleType === 'text'
+              ? activeColor
+              : '#fff'
+            : styleType === 'text'
+              ? '#000'
+              : activeColor,
+        backgroundColor: index === currentIndex && styleType === 'button' ? activeColor : ''
+      }"
+      class="segmented-control-item"
+      @click="_onClick(index)"
+    >
+      {{ item }}
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'UniSegmentedControl',
+  props: {
+    current: {
+      type: Number,
+      default: 0
+    },
+    values: {
+      type: Array,
+      default () {
+        return []
+      }
+    },
+    activeColor: {
+      type: String,
+      default: '#000'
+    },
+    styleType: {
+      type: String,
+      default: 'button'
+    }
+  },
+  data () {
+    return {
+      currentIndex: 0
+    }
+  },
+  watch: {
+    current (val) {
+      if (val !== this.currentIndex) {
+        this.currentIndex = val
+      }
+    }
+  },
+  created () {
+    this.currentIndex = this.current
+  },
+  methods: {
+    _onClick (index) {
+      if (this.currentIndex !== index) {
+        this.currentIndex = index
+        this.$emit('clickItem', index)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.segmented-control {
+	display: flex;
+	flex-direction: row;
+	justify-content: center;
+	width: 75%;
+	font-size: 28upx;
+	box-sizing: border-box;
+	margin: 20px auto;
+	overflow: hidden;
+
+	border: 1px solid;
+	border-radius: 10upx;
+
+	&.text {
+		border: 0;
+		border-radius: 0;
+	}
+}
+
+.segmented-control-item {
+	flex: 1;
+	text-align: center;
+	line-height: 60upx;
+	box-sizing: border-box;
+
+	border-left: 1px solid;
+
+	&.active {
+		color: #fff;
+	}
+
+	&.text {
+		border-left: 0;
+		color: #000;
+
+		&.active {
+			border-bottom-style: solid;
+		}
+	}
+
+	&:first-child {
+		border-left-width: 0;
+	}
+}
+</style>

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