lizhaocai 3 ヶ月 前
コミット
eae7feaa20
100 ファイル変更24301 行追加0 行削除
  1. 16 0
      .hbuilderx/launch.json
  2. 91 0
      App.vue
  3. 115 0
      README.md
  4. 831 0
      circlePages/addShare.vue
  5. 674 0
      circlePages/circle.vue
  6. 141 0
      components/basic-table/basic-table.scss
  7. 363 0
      components/basic-table/basic-table.vue
  8. 108 0
      components/w-select/readme.md
  9. 562 0
      components/w-select/w-select.vue
  10. 25 0
      config.js
  11. 20 0
      index.html
  12. 94 0
      libs/components/demo-title.vue
  13. 689 0
      libs/components/dynamic-demo-template.vue
  14. 147 0
      libs/components/multiple-options-demo.vue
  15. 169 0
      libs/components/nav-index-button.vue
  16. 52 0
      libs/mixin/dynamic_demo_mixin.js
  17. 60 0
      libs/mixin/template_page_mixin.js
  18. 330 0
      libs/navigation/navigation.js
  19. 28 0
      main.js
  20. 66 0
      manifest.json
  21. 419 0
      minePages/set.vue
  22. 101 0
      node_modules/js-md5/CHANGELOG.md
  23. 20 0
      node_modules/js-md5/LICENSE.txt
  24. 76 0
      node_modules/js-md5/README.md
  25. 10 0
      node_modules/js-md5/build/md5.min.js
  26. 73 0
      node_modules/js-md5/package.json
  27. 683 0
      node_modules/js-md5/src/md5.js
  28. 23 0
      package.json
  29. 142 0
      pages.json
  30. 1228 0
      pages/comm/comm.vue
  31. 83 0
      pages/comm/search.vue
  32. 54 0
      pages/discovery/discovery.vue
  33. 1342 0
      pages/home/home.vue
  34. 112 0
      pages/index/auth.vue
  35. 266 0
      pages/index/index.vue
  36. 81 0
      pages/login/info.vue
  37. 384 0
      pages/login/login.vue
  38. 90 0
      pages/mine/about.vue
  39. 279 0
      pages/mine/addFeed.vue
  40. 504 0
      pages/mine/coll.vue
  41. 593 0
      pages/mine/feedback.vue
  42. 600 0
      pages/mine/mine.vue
  43. 468 0
      pages/mine/need.vue
  44. 515 0
      pages/mine/share.vue
  45. 22 0
      pages/webview/index.vue
  46. 46 0
      pages/webview/web-view.vue
  47. 55 0
      plugins/README.md
  48. 65 0
      plugins/cancel.js
  49. 141 0
      plugins/uni_request.js
  50. BIN
      static/author.jpg
  51. BIN
      static/bg4.png
  52. BIN
      static/callus.png
  53. BIN
      static/logo.png
  54. BIN
      static/me2.png
  55. 28 0
      store/$t.mixin.js
  56. 138 0
      store/index.js
  57. 38 0
      template.h5.html
  58. 4 0
      tuniao-ui/README.md
  59. 202 0
      tuniao-ui/components/tn-action-sheet/tn-action-sheet.vue
  60. 103 0
      tuniao-ui/components/tn-avatar-group/tn-avatar-group.vue
  61. 298 0
      tuniao-ui/components/tn-avatar/tn-avatar.vue
  62. 173 0
      tuniao-ui/components/tn-badge/tn-badge.vue
  63. 302 0
      tuniao-ui/components/tn-button/tn-button.vue
  64. 707 0
      tuniao-ui/components/tn-calendar/tn-calendar.vue
  65. 320 0
      tuniao-ui/components/tn-car-keyboard/tn-car-keyboard.vue
  66. 654 0
      tuniao-ui/components/tn-cascade-selection/tn-cascade-selection.vue
  67. 134 0
      tuniao-ui/components/tn-checkbox-group/tn-checkbox-group.vue
  68. 328 0
      tuniao-ui/components/tn-checkbox/tn-checkbox.vue
  69. 223 0
      tuniao-ui/components/tn-circle-progress/tn-circle-progress.vue
  70. 236 0
      tuniao-ui/components/tn-collapse-item/tn-collapse-item.vue
  71. 98 0
      tuniao-ui/components/tn-collapse/tn-collapse.vue
  72. 318 0
      tuniao-ui/components/tn-color-icon/tn-color-icon.vue
  73. 251 0
      tuniao-ui/components/tn-column-notice/tn-column-notice.vue
  74. 314 0
      tuniao-ui/components/tn-count-down/tn-count-down.vue
  75. 171 0
      tuniao-ui/components/tn-count-scroll/tn-count-scroll.vue
  76. 231 0
      tuniao-ui/components/tn-count-to/tn-count-to.vue
  77. 288 0
      tuniao-ui/components/tn-custom-swiper-item/index.wxs
  78. 277 0
      tuniao-ui/components/tn-custom-swiper-item/tn-custom-swiper-item.vue
  79. 535 0
      tuniao-ui/components/tn-custom-swiper/tn-custom-swiper.vue
  80. 265 0
      tuniao-ui/components/tn-drag/index.wxs
  81. 278 0
      tuniao-ui/components/tn-drag/tn-drag.vue
  82. 190 0
      tuniao-ui/components/tn-empty/tn-empty.vue
  83. 523 0
      tuniao-ui/components/tn-fab/tn-fab.vue
  84. 457 0
      tuniao-ui/components/tn-form-item/tn-form-item.vue
  85. 139 0
      tuniao-ui/components/tn-form/tn-form.vue
  86. 382 0
      tuniao-ui/components/tn-goods-nav/tn-goods-nav.vue
  87. 114 0
      tuniao-ui/components/tn-grid-item/tn-grid-item.vue
  88. 111 0
      tuniao-ui/components/tn-grid/tn-grid.vue
  89. 90 0
      tuniao-ui/components/tn-index-anchor/tn-index-anchor.vue
  90. 361 0
      tuniao-ui/components/tn-index-list/tn-index-list.vue
  91. 427 0
      tuniao-ui/components/tn-input/tn-input.vue
  92. 220 0
      tuniao-ui/components/tn-keyboard/tn-keyboard.vue
  93. 225 0
      tuniao-ui/components/tn-landscape/tn-landscape.vue
  94. 254 0
      tuniao-ui/components/tn-lazy-load/tn-lazy-load.vue
  95. 143 0
      tuniao-ui/components/tn-line-progress/tn-line-progress.vue
  96. 209 0
      tuniao-ui/components/tn-list-cell/tn-list-cell.vue
  97. 184 0
      tuniao-ui/components/tn-list-view/tn-list-view.vue
  98. 188 0
      tuniao-ui/components/tn-load-more/tn-load-more.vue
  99. 114 0
      tuniao-ui/components/tn-loading/tn-loading.vue
  100. 0 0
      tuniao-ui/components/tn-modal/tn-modal.vue

+ 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"
+     }
+    ]
+}

+ 91 - 0
App.vue

@@ -0,0 +1,91 @@
+<script>
+  import Vue from 'vue'
+  import store from './store/index.js'
+  import updateCustomBarInfo from './tuniao-ui/libs/function/updateCustomBarInfo.js'
+  
+	export default {
+		onLaunch: function() {
+			uni.getSystemInfo({
+			  success: function(e) {
+			    // #ifndef H5
+			    // 获取手机系统版本
+			    const system = e.system.toLowerCase()
+			    const platform = e.platform.toLowerCase()
+			    // 判断是否为ios设备
+			    if (platform.indexOf('ios') != -1 && (system.indexOf('ios') != -1 || system.indexOf('macos') != -1)) {
+			      Vue.prototype.SystemPlatform = 'apple'
+			    } else if (platform.indexOf('android') != -1 && (system.indexOf('android') != -1)) {
+			      Vue.prototype.SystemPlatform = 'android'
+			    } else {
+			      Vue.prototype.SystemPlatform = 'devtools'
+			    }
+			    // #endif
+			  }
+			})
+      
+      // 获取设备的状态栏信息和自定义顶栏信息
+      // store.dispatch('updateCustomBarInfo')
+      updateCustomBarInfo().then((res) => {
+        store.commit('$tStore', {
+          name: 'vuex_status_bar_height',
+          value: res.statusBarHeight
+        })
+        store.commit('$tStore', {
+          name: 'vuex_custom_bar_height',
+          value: res.customBarHeight
+        })
+      })
+			
+			// #ifdef MP-WEIXIN
+			//更新检测
+			if (wx.canIUse('getUpdateManager')) {
+			  const updateManager = wx.getUpdateManager();
+			  updateManager && updateManager.onCheckForUpdate((res) => {
+			    if (res.hasUpdate) {
+			      updateManager.onUpdateReady(() => {
+			        uni.showModal({
+			          title: '更新提示',
+			          content: '新版本已经准备就绪,是否需要重新启动应用?',
+			          success: (res) => {
+			            if (res.confirm) {
+			              uni.clearStorageSync() // 更新完成后刷新storage的数据
+			              updateManager.applyUpdate()
+			            }
+			          }
+			        })
+			      })
+			
+			      updateManager.onUpdateFailed(() => {
+			        uni.showModal({
+			          title: '已有新版本上线',
+			          content: '小程序自动更新失败,请删除该小程序后重新搜索打开哟~~~',
+                showCancel: false
+			        })
+			      })
+			    } else {
+			      //没有更新
+			    }
+			  })
+			} else {
+			  uni.showModal({
+			    title: '提示',
+			    content: '当前微信版本过低,无法使用该功能,请更新到最新的微信后再重试。',
+          showCancel: false
+			  })
+			}
+      // #endif
+		},
+		onShow: function() {
+			// console.log('App Show')
+		},
+		onHide: function() {
+			// console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="scss">
+  /* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
+  @import './tuniao-ui/index.scss';
+  @import './tuniao-ui/iconfont.css';
+</style>

+ 115 - 0
README.md

@@ -0,0 +1,115 @@
+
+
+## 组件说明
+
+basic-table是基于uniapp开发的列表,参考element-ui设计api和参数,内置斑马纹,border,自定义列,宽度自动计算,合计等功能,现决定免费开源,如果对你有帮助,可给五星好评,如果需要作者帮助,可以给作者买一杯咖啡然后私聊作者。
+
+
+## 参数说明
+**basic-table组件**
+
+| 参数              | 说明                                 | 类型     | 可选值 | 默认值  |
+|-----------------|------------------------------------|--------|-----|------|
+| data | 显示的数据                                | array  | -   | -   |
+| columns | 表头数据                                | array  | -   | -   |
+| align          | 列表对齐方式                             | string | left/center/right   | left   |
+| height          | talbe的高度                             | string | -   | -   |
+| max-height          | 最大高度                             | string | -   | -   |
+| stripe          | 是否为斑马纹 table                             | boolean | true/false   | false   |
+| border          | 是否需要外边框                             | boolean | true/false   | false   |
+| show-header          | 是否显示表头                            | boolean | true/false   | true   |
+| row-class-name          | 行的 className 的回调方法,也可以使用字符串为所有行设置一个固定的 className。   | function({ row, rowIndex }) / string | -   | -   |
+| row-style          | 行的 style 的回调方法,也可以使用一个固定的 Object 为所有行设置一样的 Style。   | function({ row, rowIndex }) / object | -   | -   |
+| header-row-class-name          | 表头的class,传字符串   | string | -   | -   |
+| header-row-style          | 表头的style,传object   | object | -   | -   |
+| empty-text          | 空数据时显示的文本内容, 也可以通过 #empty 设置   | string | -   | 暂无数据   |
+| show-footer          | 是否显示表尾合计项                            | boolean | true/false   | false   |
+| footer-text          | 显示摘要行第一列的文本                            | string | -   | 合计   |
+| footer-method          | 自定义的合计计算方法                        | function({ columns, data }) | -   | -   |
+| index-show          | 是否显示序号                            | boolean | true/false   | false   |
+| index-method          | 序号的回调方法                            | function(index) |-   | -   |
+| index-width          | 序号列的宽度                           | string |-   | 60px  |
+| min-item-width          | 每列的最小宽度                           | string |-   | 80px  |
+
+**basic-table columns[]**
+
+| key             | 说明                                 | 
+|-----------------|------------------------------------|
+| fieldName             | 对应的value字段                                 | 
+| fieldDesc             | 对应的label字段(表头显示的label)                                 | 
+| fieldType             | 默认为空,若设置为slot,即自定义列 | 
+| width             | 默认自适应 | 
+| fixed             | 固定列,可选值left/right | 
+
+
+**basic-table 事件**
+
+| 事件名             | 说明                                 |  回调参数  |
+|-----------------|------------------------------------|--------|-----|------|
+| header-click             | 当某一列的表头被点击时会触发该事件                                 |  -  |
+| row-click             | 当某一行被点击时会触发该事件                                 |  scope, index  |
+| cell-click             | 当某一单元格被点击时会触发该事件                                 |  scope,column, index  |
+
+**插槽**
+| 插槽名              | 说明                                 | 
+|-----------------|------------------------------------     |
+| item | 某列数据item,需设置fieldType为slot                                      | 
+| empty          | 列表为空时的内容                              | 
+
+
+# 使用示例
+
+
+```
+index.vue
+
+<template>
+   <basic-table :columns="columns" :data="tableData">
+      <template #item="{column,scope,index}">
+	     <view v-if="column.fieldName==='name'">
+		    {{scope.name}}
+		 </view>
+		 <view v-else-if="column.fieldName==='age'">
+		 	{{scope.age}}
+		 </view>
+	  </template>
+   </basic-table>
+</template>
+
+<script>
+import BasicTable from '@/pages/components/basic-table/basic-table.vue';
+export default {
+	components: { BasicTable },
+	data() {
+		return {
+			tableData: [{ name: '张三', age: 18 }, { name: '李四', age: 22 }],
+			columns: [
+				{
+					fieldName: 'name',
+					fieldDesc: '姓名',
+					fieldType:'slot'
+				},
+				{
+					fieldName: 'age',
+					fieldDesc: '年龄',
+					fieldType:'slot'
+				}
+			]
+		};
+	},
+};
+</script>
+
+<style lang="scss"></style>
+
+```
+
+## 打赏
+
+如果你觉得本插件,解决了你的问题,可以请作者喝杯咖啡
+
+<div>
+<img src="https://i.328888.xyz/2023/02/28/zV27Q.jpeg" alt="alipay" width="250"><img src="https://i.328888.xyz/2023/02/28/zVw2H.jpeg" alt="wechat" width="250">
+</div>
+
+

+ 831 - 0
circlePages/addShare.vue

@@ -0,0 +1,831 @@
+<template>
+	<view class="template-edit tn-safe-area-inset-bottom">
+		<!-- 顶部自定义导航 -->
+		<tn-nav-bar fixed alpha customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack">
+				<text class='icon tn-icon-left'></text>
+			</view>
+			<view slot="default" v-if="(selectValue=='个人'&&stepIndex==2)||(selectValue=='公司'&&stepIndex==3)" style="display: flex;">
+				<view style="flex:1;margin-left:25px">
+					<text></text>
+				</view>
+				<view>
+					<text style="margin-right: 4px;padding: 6px 15px;background-color:#00000026;border-radius: 30px;color: #3D7EFF;" @click="saveForm(1)">暂存</text>
+				</view>
+			</view>
+		</tn-nav-bar>
+
+		<view class="tn-safe-area-inset-bottom" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+
+			<tn-steps style="pointer-events:none" :list="selectValue=='个人'?stepList:stepList2" :current="stepIndex" mode="dotIcon"></tn-steps>
+			<view v-if="stepIndex==1">
+
+				<view style="padding: 16px">
+					<uni-data-select v-model="selectValue" :localdata="selectList" @change="changeSelect"
+						:clear="false"></uni-data-select>
+				</view>
+				<view v-if="selectValue=='个人'" style="padding: 16px">
+					<uni-forms :modelValue="formData" label-width="0">
+
+						<text v-if="formInfo.contactNickName">用户昵称:</text>
+						<uni-forms-item label="用户昵称" name="realName" label-width="0">
+							<uni-easyinput type="text" disabled v-model="formInfo.contactNickName" placeholder="请输入您的昵称" />
+						</uni-forms-item>
+						<text v-if="formInfo.userRealName">真实姓名:</text>
+						<uni-forms-item label="真实姓名" name="contactMethod" v-if="formInfo.userRealName">
+							<uni-easyinput type="text" disabled="" v-model="formInfo.userRealName" placeholder="请输入真实姓名" />
+						</uni-forms-item>
+						<text v-if="formInfo.contactMethod">联系方式:</text>
+						<uni-forms-item label="联系方式" name="phone">
+							<uni-easyinput type="text" disabled v-model="formInfo.contactMethod" placeholder="请输入联系手机/微信/邮箱" />
+						</uni-forms-item>
+
+					</uni-forms>
+				</view>
+
+				<view v-if="selectValue=='公司'" style="padding: 16px">
+					<uni-forms :modelValue="formData" label-width="0">
+						<view style="margin-bottom:16px">
+							<text v-if="searchValue">公司名称:</text>
+							<w-select style="width: 100%;" v-model='searchValue' :list='items' valueName='name'
+								keyName="regNumber" @change='selectChange' :filterable="true">
+							</w-select>
+						</view>
+						<!-- <uni-forms-item label="公司名称" name="name">
+					<uni-easyinput type="text" v-model="userInfo.company" placeholder="请输入所在公司名称" />
+				</uni-forms-item> -->
+						<text v-if="formInfo.jobTitle">您的职称:</text>
+						<uni-forms-item label="" name="realName" label-width="0">
+							<uni-easyinput type="text" :clearable="false" v-model="formInfo.jobTitle" placeholder="请输入您的职称" />
+						</uni-forms-item>
+						<text v-if="formInfo.contactPerson">联系姓名:</text>
+						<uni-forms-item label="联系人姓名" name="contactMethod">
+							<uni-easyinput type="text" :clearable="false" v-model="formInfo.contactPerson" placeholder="请输入联系人姓名" />
+						</uni-forms-item>
+						<text v-if="formInfo.contactMethod">联系方式:</text>
+						<uni-forms-item label="联系方式" name="phone">
+							<uni-easyinput type="text" :clearable="false" v-model="formInfo.contactMethod" placeholder="请输入联系手机/微信/邮箱" />
+						</uni-forms-item>
+
+						<uni-forms-item label="11" required>
+							<uni-data-checkbox v-model="formInfo.agree"
+								localdata="[{text: '同意平台核查所填信息的真实性',value: '是'}]" />
+						</uni-forms-item>
+					</uni-forms>
+				</view>
+
+
+
+
+
+				<!-- <view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-top-xl tn-margin">
+        <view class="tn-flex justify-content-item">
+          <view class="tn-bg-black tn-color-white tn-text-center" style="border-radius: 100rpx;margin-right: 8rpx;width: 45rpx;height: 45rpx;line-height: 45rpx;">
+            <text class="tn-icon-tag" style="font-size: 30rpx;"></text>
+          </view>
+          <view class="tn-text-lg tn-padding-right-xs tn-text-bold">话题标签</view>
+        </view>
+        <view class="justify-content-item tn-text-df tn-color-grey">
+          <text class="tn-padding-xs">选择</text>
+          <text class="tn-icon-right"></text>
+        </view>
+      </view> -->
+
+				
+
+				<!-- 悬浮按钮-->
+				<view class="tn-flex tn-footerfixed">
+					<view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+						<tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold
+							@click="nextStep()">
+							<!-- <text class="tn-icon-light tn-padding-right-xs tn-color-black"></text> -->
+							<text class="tn-color-white">下一步</text>
+							<!-- <text class="tn-icon-camera tn-padding-left-xs tn-color-black"></text> -->
+						</tn-button>
+					</view>
+				</view>
+			</view>
+			<view v-if="stepIndex!=1">
+
+				<view style="padding: 16px" v-if="(selectValue=='公司'&&stepIndex==2)||(selectValue=='个人'&&stepIndex==2)">
+					<uni-data-select v-model="selectValue2" :localdata="selectList2" @change="changeSelect2"
+						:clear="false"></uni-data-select>
+				</view>
+
+				<view v-if="selectValue2=='产品'" >
+					<view style="padding: 16px" v-if="(selectValue=='公司'&&stepIndex==2)||(selectValue=='个人'&&stepIndex==2)">
+						<text v-if="selectValue4">产品种类:</text>
+						<uni-data-select v-model="selectValue4" :localdata="selectList4" @change="changeSelect4"
+							placeholder="产品种类" :clear="false" style="margin-bottom:16px"></uni-data-select>
+						<text v-if="brand">产品品牌:</text>
+						<view>
+						   <uni-easyinput type="text"  required v-model="brand" placeholder="请输入产品品牌" />
+						</view>
+						<view style="margin-top: 16px;line-height: 30px;margin-bottom: 16px;">
+							发布的产品是否属于医疗器械?
+							<uni-data-checkbox :multiple="false" v-model="isMedical"
+								:localdata="[{text: '是',value: '1'},{text: '否',value: '0'}]" />
+						</view>
+
+						<view v-for="item,itemIndex in extList">
+							
+							<uni-card >
+											<template v-slot:title>
+												<uni-list>
+													<uni-list-item style="align-items: center;">
+															<template v-slot:header>
+																 产品信息{{itemIndex+1}}
+															</template>
+															<template v-slot:footer>
+																 <tn-button fontColor="tn-color-white" shape="round" backgroundColor="#3668FC" v-if="extList.length==1" @click="newItem">+新增产品</tn-button>
+																 <tn-button fontColor="tn-color-white" shape="round" backgroundColor="#3668FC" v-if="extList.length>1&&extList.length<6&&itemIndex==extList.length-1" @click="newItem">+新增产品</tn-button>
+																 <tn-button fontColor="tn-color-white" shape="round" backgroundColor="#FF000C" v-if="extList.length>1&&itemIndex!==extList.length-1" @click="delItem(itemIndex)">-删除产品</tn-button>
+															</template>
+														</uni-list-item>
+												</uni-list>
+												
+											</template>
+										<uni-forms :modelValue="formData" label-width="0">
+											 
+											<uni-forms-item label="" name="prodName" label-width="0">
+												<uni-easyinput maxlength="50" type="text" v-model="item.prodName"
+													placeholder="*请输入产品名称" />
+											</uni-forms-item>
+											<uni-forms-item label="" name="contactMethod">
+												<uni-easyinput maxlength="100" type="text" v-model="item.prodSpec"
+													placeholder="*请输入产品型号" />
+											</uni-forms-item>
+											<uni-forms-item label="" name="phone">
+												<uni-easyinput maxlength="500" type="textarea" v-model="item.prodDesc" placeholder="请输入产品介绍" />
+											</uni-forms-item>
+										
+										
+										</uni-forms>
+										<template v-slot:actions v-if="itemIndex==extList.length-1" >
+											<view style="margin: 12px;margin-top: -12px;">
+												<text style="color:#999">为保证排版整洁,最多一次上传六个产品哦</text>
+											</view>
+										</template>
+							</uni-card>
+							<!-- <view>
+								
+								<uni-forms :modelValue="formData" label-width="0">
+									<view style="display: flex;justify-content: space-between;">
+										<view><text class="tn-icon-p" style="font-size: 30rpx;"></text>产品{{itemIndex+1}}</view>
+										  
+										<view><tn-button v-if="extList.length==1" @click="newItem">+新增产品</tn-button>
+										<tn-button v-if="extList.length>1&&extList.length<6&&itemIndex==extList.length-1" @click="newItem">+新增产品</tn-button>
+										<tn-button v-if="extList.length>1&&itemIndex!==extList.length-1" @click="delItem(itemIndex)">-删除产品</tn-button></view>
+										
+									</view>
+									<uni-forms-item label="" name="prodName" label-width="0">
+										<uni-easyinput type="text" required v-model="item.prodName"
+											placeholder="*请输入产品名称" />
+									</uni-forms-item>
+									<uni-forms-item label="" name="contactMethod">
+										<uni-easyinput type="text" required v-model="item.prodSpec"
+											placeholder="*请输入产品型号" />
+									</uni-forms-item>
+									<uni-forms-item label="" name="phone">
+										<uni-easyinput type="text" v-model="item.prodDesc" placeholder="请输入产品介绍" />
+									</uni-forms-item>
+
+
+								</uni-forms>
+							</view> -->
+
+						</view>
+
+
+					</view>
+
+				</view>
+				<view v-if="selectValue2=='服务'">
+					<view style="padding: 16px">
+						<uni-data-select v-model="selectValue3" :localdata="selectList3" @change="changeSelect3"
+							:clear="false"></uni-data-select>
+					</view>
+				</view>
+
+
+				<view>
+
+
+					<!-- <view class="tn-margin tn-bg-gray--light" style="border-radius: 10rpx;padding: 20rpx 30rpx;">
+					 	<input placeholder="写下一句简短的标题" name="input" placeholder-style="color:#AAAAAA" ></input>
+					 </view> -->
+					<view v-if="selectValue2=='服务'" class="tn-margin tn-bg-gray--light tn-padding"
+						style="border-radius: 10rpx;">
+						<textarea maxlength="500" v-model="content" placeholder="请输入服务介绍"
+							placeholder-style="color:#AAAAAA"></textarea>
+					</view>
+
+					<view class="tn-flex tn-flex-row-between tn-flex-col-center   tn-margin"  v-if="(selectValue=='公司'&&stepIndex==3)||(selectValue=='个人'&&stepIndex==2)">
+						<view class="tn-flex justify-content-item">
+							<view class="tn-text-center"
+								style="border-radius: 100rpx;margin-right: 8rpx;width: 45rpx;height: 45rpx;line-height: 45rpx;">
+								<text class="tn-icon-image" style="font-size: 30rpx;"></text>
+							</view>
+							<view class="tn-text-lg tn-padding-right-xs tn-text-bold">上传其他图片(若有)</view>
+						</view>
+						<!-- <view class="justify-content-item tn-text-df tn-color-grey" @tap="clear">
+					 					<text class="tn-padding-xs">清空上传</text>
+					 					<text class="tn-icon-delete"></text>
+					 				</view> -->
+					</view>
+
+
+
+
+
+
+					<view class="tn-margin-left tn-padding-top-xs"  v-if="(selectValue=='公司'&&stepIndex==3)||(selectValue=='个人'&&stepIndex==2)">
+						<uni-file-picker v-model="imgList" :limit="6" :auto-upload="false" @select="select"
+							@success="success" @delete="deleteFile">
+
+
+						</uni-file-picker>
+						<!-- <tn-image-upload-drag ref="imageUpload" :action="action" :width="236" :height="236" :formData="formData"
+					 					:fileList="fileList" :disabled="disabled" :autoUpload="autoUpload" :maxCount="maxCount"
+					 					:showUploadList="showUploadList" :showProgress="showProgress" :deleteable="deleteable"
+					 					:customBtn="customBtn" @sort-list="onSortList" /> -->
+
+					</view>
+
+					<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-top-xl tn-margin"  v-if="(selectValue=='公司'&&stepIndex==3)||(selectValue=='个人'&&stepIndex==2)">
+						<view class="tn-flex justify-content-item">
+							<view class="tn-text-center"
+								style="border-radius: 100rpx;margin-right: 8rpx;width: 45rpx;height: 45rpx;line-height: 45rpx;">
+								<text class="tn-icon-image" style="font-size: 30rpx;"></text>
+							</view>
+							<view class="tn-text-lg tn-padding-right-xs tn-text-bold">上传相关文件(若有)</view>
+						</view>
+						<!-- <view class="justify-content-item tn-text-df tn-color-grey" @tap="clear">
+					 					<text class="tn-padding-xs">清空上传</text>
+					 					<text class="tn-icon-delete"></text>
+					 				</view> -->
+					</view>
+
+					<view class="tn-margin-left tn-padding-top-xs"  v-if="(selectValue=='公司'&&stepIndex==3)||(selectValue=='个人'&&stepIndex==2)">
+						<uni-file-picker v-model="fileList" :limit="3" mode="grid" file-mediatype="all"
+							file-extname="pdf,docx,doc" :auto-upload="false" @select="select" @success="success" @delete="deleteFile">
+							<tn-button shadow shape="round" fontColor="tn-color-white" size="lg"
+								backgroundColor="tn-bg-blue" :fontSize="24" height="auto"
+								padding="20rpx 36rpx">上传文件</tn-button>
+							<view style="margin-top:20px"><text class="tn-color-grey">支持格式 pdf .doc,不超过5MB。</text>
+							</view>
+							<view style="margin-top:20px" v-if="selectValue2=='产品'"><text class="tn-color-grey">如若产品属于医疗器械,请上传相关资质证明。</text>
+							</view>
+						</uni-file-picker>
+						<!-- <tn-image-upload-drag ref="imageUpload" :action="action" :width="236" :height="236" :formData="formData"
+					 					:fileList="fileList2" :disabled="disabled" :autoUpload="autoUpload" :maxCount="maxCount"
+					 					:showUploadList="showUploadList" :showProgress="showProgress" :deleteable="deleteable"
+					 					:customBtn="customBtn" @sort-list="onSortList" /> -->
+
+					</view>
+				</view>
+
+				<view label="11" name="check" style="padding:16px"  v-if="(selectValue=='公司'&&stepIndex==3)||(selectValue=='个人'&&stepIndex==2)">
+					<uni-data-checkbox :multiple="true" v-model="formInfo.agree"
+						:localdata="[{text: '同意平台核查所填信息的真实性',value: '是'}]" />
+				</view>
+				
+				<!-- 悬浮按钮-->
+				<view class="tn-flex tn-footerfixed"  v-if="selectValue=='公司'">
+					<view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+						<tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold
+							@click="nextStep()">
+							<!-- <text class="tn-icon-light tn-padding-right-xs tn-color-black"></text> -->
+							<text class="tn-color-white">下一步</text>
+							<!-- <text class="tn-icon-camera tn-padding-left-xs tn-color-black"></text> -->
+						</tn-button>
+					</view>
+				</view>
+
+				<view class="tn-flex tn-footerfixed"  v-if="(stepIndex==3)||(selectValue=='个人'&&stepIndex==2)">
+					<view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+						<tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold
+							@click="saveForm()">
+							<!-- <text class="tn-icon-light tn-padding-right-xs tn-color-black"></text> -->
+							<text class="tn-color-white">提交审核</text>
+							<!-- <text class="tn-icon-camera tn-padding-left-xs tn-color-black"></text> -->
+						</tn-button>
+					</view>
+				</view>
+			</view>
+		</view>
+
+
+
+		<view class='tn-tabbar-height'></view>
+
+	</view>
+</template>
+
+<script>
+	import template_page_mixin from '@/libs/mixin/template_page_mixin.js';
+	import request from '../utils/request';
+	export default {
+		name: 'TemplateEdit',
+		mixins: [template_page_mixin],
+		data() {
+			return {
+				canSave:true,
+				isMedical: '0',
+				content: '',
+				selectValue: '个人',
+				selectList: [{
+						value: '个人',
+						text: '个人'
+					},
+					{
+						value: '公司',
+						text: '公司'
+					}
+				],
+				selectValue2: '产品',
+				selectList2: [{
+						value: '产品',
+						text: '产品'
+					},
+					{
+						value: '服务',
+						text: '服务'
+					}
+				],
+				selectValue3: '维修维保',
+				brand: '',
+				// 维修维保/改造升级/验证/搬迁/厂房建设/其他
+				selectList3: [{
+						value: '维修维保',
+						text: '维修维保'
+					},
+					{
+						value: '改造升级',
+						text: '改造升级'
+					},
+					{
+						value: '验证',
+						text: '验证'
+					},
+					{
+						value: '搬迁',
+						text: '搬迁'
+					},
+					{
+						value: '厂房建设',
+						text: '厂房建设'
+					},
+					{
+						value: '其他',
+						text: '其他'
+					}
+				],
+				selectValue4: '机械五金',
+				selectList4: [{
+						value: '机械五金',
+						text: '机械五金'
+					},
+					{
+						value: '仪器仪表',
+						text: '仪器仪表'
+					},
+					{
+						value: '耗材',
+						text: '耗材'
+					},
+					{
+						value: '其他',
+						text: '其他'
+					}
+				],
+				extList: [{
+					prodDesc: "",
+					prodName: "",
+					prodSpec: "",
+				}],
+				// 机械五金/仪器仪表/耗材/其他
+				formInfo: {
+					jobTitle: '',
+					userRealName:JSON.parse(uni.getStorageSync('userInfo')).userRealName,
+					// contactPerson:JSON.parse(uni.getStorageSync('userInfo')).contactNickName?JSON.parse(uni.getStorageSync('userInfo')).contactNickName:'用户'+JSON.parse(uni.getStorageInfoSync('userInfo')).userName.splice(-4),
+					agree: ['是'],
+					contactNickName: JSON.parse(uni.getStorageSync('userInfo')).contactNickName||'用户'+JSON.parse(uni.getStorageSync('userInfo')).userName.slice(-4),
+					contactMethod: JSON.parse(uni.getStorageSync('userInfo')).contactMethod || JSON.parse(uni
+						.getStorageSync('userInfo')).userName
+				},
+				imgList: [],
+				fileDetailList: [],
+				stepIndex: 1,
+				stepList: [{
+						name: '填写个人信息',
+						icon: 'circle',
+						selectIcon: 'circle-fill'
+					},
+					{
+						name: '填写供应信息',
+						icon: 'trusty',
+						selectIcon: 'trusty-fill'
+					}
+				],
+				stepList2 : [{
+						name: '填写公司信息',
+						icon: 'circle',
+						selectIcon: 'circle-fill'
+					},
+					{
+						name: '填写供应信息',
+						icon: 'trusty',
+						selectIcon: 'trusty-fill'
+					},
+					{
+						name: '上传附件',
+						icon: 'vip',
+						selectIcon: 'vip-fill'
+					}
+				],
+				action: 'https://www.hualigs.cn/api/upload',
+				// action: '',
+				formData: {
+					apiType: 'this,ali',
+					token: 'dffc1e06e636cff0fdf7d877b6ae6a2e',
+					image: null
+				},
+				fileList: [],
+				showUploadList: true,
+				customBtn: false,
+				autoUpload: true,
+				showProgress: false,
+				deleteable: true,
+				customStyle: false,
+				maxCount: 9,
+				disabled: false,
+				searchValue: '',
+				items: [],
+				org: {},
+			}
+		},
+		watch: {
+			searchValue(val, oldval) {
+				console.error(val, this.org.name);
+				if (val !== this.org.name) {
+					this.current = null;
+				}
+				if(this.selectValue!='个人'){
+					this.search(val)
+				}
+				
+			}
+		},
+		onLoad() {
+			this.getCompany();
+		},
+		methods: {
+			nextStep() {
+				//todo 检查
+				 let that = this;
+					if(this.stepIndex==2&&that.selectValue2=='产品'){
+						for(let i=0;i<this.extList.length;i++){
+							if(!this.extList[i].prodName||!this.extList[i].prodSpec){
+								uni.showToast({
+									title: !this.extList[i].prodName?'产品'+(i+1)+'的产品名称必填':'产品'+(i+1)+'的产品型号必填',
+									duration: 2000,
+									icon:'none'
+								});
+								return false;
+							}
+						}
+					}
+				 
+				this.stepIndex = this.stepIndex+1;
+				
+				console.error(this.stepIndex);
+			},
+			getCompany(){
+				let that = this;
+				
+				 
+					request.post('/slbUserCompanyRel/show/my', {
+						userNo: uni.getStorageSync('userNo')
+					}).then(res => {
+						if (res.success) {
+							let list = res.list || [];
+							for(let i=0;i<list.length;i++){
+								list[i].name = list[i].company;
+								list[i].regNumber= list[i].company;
+								if(list[i].isDefault=='1'){
+									that.searchValue = list[i].name
+									that.current = list[i].name;
+									that.formInfo.jobTitle = list[i].jobTitle;
+									that.formInfo.contactPerson = list[i].contactPerson;
+									that.formInfo.contactMethod = list[i].contactMethod;
+									that.org = list[i];
+								}
+								 
+							}
+							that.items = list;
+							console.warn(that.items);
+						}  
+					})
+				
+				 
+				 
+			},
+			changeSelect(e) {
+				this.selectValue = e;
+			},
+			changeSelect2(e) {
+				this.selectValue2 = e;
+			},
+			changeSelect3(e) {
+				this.selectValue3 = e;
+			},
+			changeSelect4(e) {
+				this.selectValue4 = e;
+			},
+			newItem(){
+				this.extList.push({
+					prodDesc: "",
+					prodName: "",
+					prodSpec: "",
+				})
+			},
+			delItem(index){
+				this.extList.splice(index,1);
+			},
+			saveForm(status) {
+				if(!this.canSave){
+					return false;
+				}
+				let that = this;
+				if(this.formInfo.agree.length<1){
+					uni.showToast({
+						title: '请勾选同意平台核查所填信息的真实性',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				if(that.selectValue2=='服务'){
+					if(!this.content&&this.fileDetailList.length<1){
+						uni.showToast({
+							title: '请输入服务介绍或上传图片/文件',
+							duration: 2000,
+							icon:'none'
+						});
+						return false;
+					}
+				}
+				
+				if(that.selectValue2=='产品'){
+					 
+					for(let i=0;i<this.extList.length;i++){
+						if(!this.extList[i].prodName||!this.extList[i].prodSpec){
+							uni.showToast({
+								title: !this.extList[i].prodName?'产品'+(i+1)+'的产品名称必填':'产品'+(i+1)+'的产品型号必填',
+								duration: 2000,
+								icon:'none'
+							});
+							return false;
+						}
+					}
+				}
+				
+				let params = {
+
+				};
+				let postData = {
+						type: that.selectValue2=='服务'?'2':'1',
+						secType: that.selectValue3,
+						company: that.selectValue=='公司'?this.org.name:'',
+						jobTitle: this.formInfo.jobTitle,
+						contactPerson: this.formInfo.contactPerson,
+						contactMethod: this.formInfo.contactMethod,
+						contactNickName: this.formInfo.contactNickName,
+						content: that.content,
+						userNo: uni.getStorageSync('userNo'),
+						status: status==1?status:undefined
+					
+				}
+				if(postData.type=='1'){
+					postData.brand = that.brand;
+					postData.secType = that.selectValue4;
+					postData.isMedical = that.isMedical;
+					postData.content = '';
+					postData.shareExt = that.extList;
+				}
+
+				params.slbResourceShare = JSON.stringify(postData);
+				 
+
+				params.fileDetailList = JSON.stringify(this.fileDetailList);
+				uni.showToast({
+					title: '提交中...',
+					icon:'none'
+				});
+				that.canSave = false;
+				request.post('/slbResourceShare/add', params).then(res => {
+					that.canSave = true;
+					if (res.success) {
+						uni.showToast({
+							title: status==1?'暂存成功':'发布已提交,可在我的需求里查看审核进度',
+							icon: 'none',
+							success: () => {
+								setTimeout(() => {
+									uni.redirectTo({
+										url: "/pages/mine/share"
+									});
+								}, 2500)
+							}
+						})
+					} else {
+						uni.showToast({
+							title: res.msg,
+							icon: 'none'
+						})
+					}
+					console.warn(res);
+				})
+			},
+			// 跳转
+			tn(e) {
+				uni.navigateTo({
+					url: e,
+				});
+			},
+			// 手动上传文件
+			upload() {
+				console.warn(121212);
+			},
+			// 手动清空列表
+			clear() {
+				this.$refs.imageUpload.clear()
+			},
+			// 图片拖拽重新排序
+			onSortList(list) {
+				console.log(list);
+			},
+			select(e) {
+				console.log('选择文件:', e)
+				let tempFiles = e.tempFiles;
+				for (let i in tempFiles) {
+					this.upfile(tempFiles[i])
+				}
+			},
+			upfile(file) {
+				let that = this;
+				console.warn(file);
+				uni.uploadFile({
+					url: 'http://slb-m.dev.ml1993.com/oss/upload/userFeedback', //仅为示例,非真实的接口地址
+					filePath: file.url,
+					name: 'file',
+					success: (uploadFileRes) => {
+						console.warn(JSON.parse(uploadFileRes.data));
+						let resultMap = JSON.parse(uploadFileRes.data).resultMap;
+						that.fileDetailList.push({
+							name: file.name,
+							fileName: file.name, // 原始文件名
+							ftpUrl: resultMap.uploadUrl, // 文件访问url
+							path: file.path
+						})
+					}
+				});
+			},
+			// 上传成功
+			success(e) {
+				console.log('上传成功')
+			},
+			deleteFile(e, index) {
+				for(let i=0;i<this.fileDetailList.length;i++){
+					if(e.tempFile.path===this.fileDetailList[i].path){
+						this.fileDetailList.splice(i, 1);
+					}
+				}
+				console.error(this.fileDetailList);
+			},
+			selectChange(e) {
+				this.searchValue = e.name
+				this.current = e.regNumber;
+				if(e.jobTitle){
+					this.formInfo.jobTitle = e.jobTitle;
+					this.formInfo.contactPerson = this[i].contactPerson;
+					this.formInfo.contactMethod = this[i].contactMethod;
+				}
+				this.org = e;
+			},
+			search: function(val) {
+				let that = this;
+
+				if (val && val.length > 3) {
+					request.post('/member/searchCompys', {
+						keyWord: val
+					}).then(res => {
+						if (res.success) {
+							let list = res.resultMap.data || [];
+							that.items = list;
+						} else {
+							uni.showToast({
+								title: res.msg,
+								icon: 'none'
+							})
+						}
+					})
+
+				} else {
+					that.items = [];
+
+				}
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.template-edit {}
+
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+
+	/* 底部悬浮按钮 start*/
+	.tn-tabbar-height {
+		min-height: 100rpx;
+		height: calc(120rpx + env(safe-area-inset-bottom) / 2);
+	}
+
+	.tn-footerfixed {
+		position: fixed;
+		width: 100%;
+		bottom: calc(env(safe-area-inset-bottom));
+		z-index: 1024;
+		box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0);
+		background: #fff;
+	}
+
+	/* 底部悬浮按钮 end*/
+
+	/* 标签内容 start*/
+	.tn-tag-content {
+		&__item {
+			display: inline-block;
+			line-height: 45rpx;
+			padding: 10rpx 30rpx;
+			margin: 20rpx 20rpx 5rpx 0rpx;
+
+			&--prefix {
+				padding-right: 10rpx;
+			}
+		}
+	}
+
+	/deep/ .uni-forms-item__label {
+		display: none;
+	}
+	
+	/deep/ .uni-list-item__container {
+		align-items: center;
+	}
+	/deep/.uni-card--shadow {
+		margin:0 !important;
+	}
+
+
+
+	/* 标签内容 end*/
+</style>

+ 674 - 0
circlePages/circle.vue

@@ -0,0 +1,674 @@
+<template>
+	<view class="template-edit tn-safe-area-inset-bottom">
+		<!-- 顶部自定义导航 -->
+		<tn-nav-bar fixed alpha customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack">
+				<text class='icon tn-icon-left'></text>
+				<!-- <text class='icon tn-icon-home-capsule-fill'></text> -->
+			</view>
+			
+			<view slot="default" v-if="stepIndex==2" style="display: flex;">
+				<view style="flex:1;margin-left:25px">
+					<text></text>
+				</view>
+				<view>
+					<text style="margin-right: 4px;padding: 6px 15px;background-color:#00000026;border-radius: 30px;color: #3D7EFF;" @click="saveForm(1)">暂存</text>
+				</view>
+			</view>
+		</tn-nav-bar>
+
+		<view class="tn-safe-area-inset-bottom" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+
+			<tn-steps style="pointer-events:none" :list="stepList" :current="stepIndex" mode="dotIcon"></tn-steps>
+			<!-- <uni-steps :options="stepList" :active="stepIndex" /> -->
+			
+			<view v-if="stepIndex==1">
+			<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-top tn-margin">
+				<view class="tn-flex justify-content-item">
+					<!-- <view class="tn-bg-black tn-color-white tn-text-center"
+						style="border-radius: 100rpx;margin-right: 8rpx;width: 45rpx;height: 45rpx;line-height: 45rpx;">
+						<text class="tn-icon-topics" style="font-size: 30rpx;"></text>
+					</view> -->
+					<view class="tn-text-lg tn-padding-right-xs tn-text-bold" style="font-size: 15px;">以下三种方式可任选其一,若有可都输入</view>
+				</view>
+				<!-- <view class="justify-content-item tn-text-df tn-color-grey">
+					<text class="tn-padding-xs">500字内</text>
+					<text class="tn-icon-keyboard-circle"></text>
+				</view> -->
+			</view>
+
+			<!-- <view class="tn-margin tn-bg-gray--light" style="border-radius: 10rpx;padding: 20rpx 30rpx;">
+      	<input placeholder="写下一句简短的标题" name="input" placeholder-style="color:#AAAAAA" ></input>
+      </view> -->
+			<view class="tn-margin tn-bg-gray--light tn-padding" style="border-radius: 10rpx;">
+				<textarea maxlength="500" v-model="content" placeholder="请描述您的需求..." placeholder-style="color:#AAAAAA"></textarea>
+			</view>
+
+			<view class="tn-flex tn-flex-row-between tn-flex-col-center   tn-margin">
+				<view class="tn-flex justify-content-item">
+					<view class=" tn-text-center"
+						style="border-radius: 100rpx;margin-right: 8rpx;width: 45rpx;height: 45rpx;line-height: 45rpx;">
+						<text class="tn-icon-image" style="font-size: 30rpx;"></text>
+					</view>
+					<view class="tn-text-lg tn-padding-right-xs tn-text-bold">图片</view>
+				</view>
+				<!-- <view class="justify-content-item tn-text-df tn-color-grey" @tap="clear">
+					<text class="tn-padding-xs">清空上传</text>
+					<text class="tn-icon-delete"></text>
+				</view> -->
+			</view>
+			
+			
+
+
+
+
+			<view class="tn-margin-left tn-padding-top-xs">
+				<uni-file-picker
+					v-model="imgList" :limit="6" @delete="deleteFile"  :auto-upload="false" @select="select" @success="success">
+					 
+				   
+				</uni-file-picker>
+				<!-- <tn-image-upload-drag ref="imageUpload" :action="action" :width="236" :height="236" :formData="formData"
+					:fileList="fileList" :disabled="disabled" :autoUpload="autoUpload" :maxCount="maxCount"
+					:showUploadList="showUploadList" :showProgress="showProgress" :deleteable="deleteable"
+					:customBtn="customBtn" @sort-list="onSortList" /> -->
+
+			</view>
+			
+			<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-top-xl tn-margin">
+				<view class="tn-flex justify-content-item">
+					<view class=" tn-text-center"
+						style="border-radius: 100rpx;margin-right: 8rpx;width: 45rpx;height: 45rpx;line-height: 45rpx;">
+						<text class="tn-icon-link" style="font-size: 30rpx;"></text>
+					</view>
+					<view class="tn-text-lg tn-padding-right-xs tn-text-bold">文件</view>
+				</view>
+				<!-- <view class="justify-content-item tn-text-df tn-color-grey" @tap="clear">
+					<text class="tn-padding-xs">清空上传</text>
+					<text class="tn-icon-delete"></text>
+				</view> -->
+			</view>
+			
+			<view class="tn-margin-left tn-padding-top-xs">
+				<uni-file-picker
+					v-model="fileList" :limit="3" mode="grid" @delete="deleteFile"  file-mediatype="all" file-extname="pdf,docx,doc" :auto-upload="false" @select="select" @success="success">
+				<!-- <button size="default">上传文件</button> -->
+				<tn-button shadow shape="round" fontColor="tn-color-white" size="lg" backgroundColor="tn-bg-blue" :fontSize="24" height="auto" padding="20rpx 36rpx">上传文件</tn-button>
+				 
+				</uni-file-picker>
+				<text class="tn-color-grey">支持格式 pdf .doc,不超过5MB。</text>
+				<!-- <tn-image-upload-drag ref="imageUpload" :action="action" :width="236" :height="236" :formData="formData"
+					:fileList="fileList2" :disabled="disabled" :autoUpload="autoUpload" :maxCount="maxCount"
+					:showUploadList="showUploadList" :showProgress="showProgress" :deleteable="deleteable"
+					:customBtn="customBtn" @sort-list="onSortList" /> -->
+			
+			</view>
+			
+			<view style="padding: 16px">
+				<text v-if="selectValue">需求有效期:</text>
+				<uni-data-select
+				    v-model="selectValue"
+				    :localdata="selectList"
+				    @change="changeSelect"
+					placement="top"
+					placeholder="需求有效期"
+				  ></uni-data-select>
+			</view>
+
+			<!-- <view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-top-xl tn-margin">
+        <view class="tn-flex justify-content-item">
+          <view class="tn-bg-black tn-color-white tn-text-center" style="border-radius: 100rpx;margin-right: 8rpx;width: 45rpx;height: 45rpx;line-height: 45rpx;">
+            <text class="tn-icon-tag" style="font-size: 30rpx;"></text>
+          </view>
+          <view class="tn-text-lg tn-padding-right-xs tn-text-bold">话题标签</view>
+        </view>
+        <view class="justify-content-item tn-text-df tn-color-grey">
+          <text class="tn-padding-xs">选择</text>
+          <text class="tn-icon-right"></text>
+        </view>
+      </view> -->
+
+			<!-- <view class="tn-tag-content tn-margin tn-text-justify tn-padding-bottom">
+        <view v-for="(item, index) in tagList" :key="index" class="tn-tag-content__item tn-margin-right tn-round tn-text-sm tn-text-bold" :class="[`tn-bg-${item.color}--light tn-color-${item.color}`]">
+          <text class="tn-tag-content__item--prefix">#</text> {{ item.title }}
+        </view>
+      </view>  -->
+
+			<!-- 悬浮按钮-->
+			<view class="tn-flex tn-footerfixed">
+				<view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+					<tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold @click="nextStep()">
+						<!-- <text class="tn-icon-light tn-padding-right-xs tn-color-black"></text> -->
+						<text class="tn-color-white">下一步</text>
+						<!-- <text class="tn-icon-camera tn-padding-left-xs tn-color-black"></text> -->
+					</tn-button>
+				</view>
+			</view>
+</view>
+<view v-if="stepIndex==2">
+	
+	<view style="padding:16px">
+		<uni-forms :modelValue="formData" label-width="0">
+			<view style="margin:16px 0">
+				<text v-if="searchValue">公司名称:</text>
+			<w-select
+			     style="width: 100%;" 
+			     v-model='searchValue' 
+			     :list='items'
+			     valueName='name' 
+			     keyName="regNumber"
+			     @change='selectChange'
+				 :filterable="true"
+			   >
+			   </w-select>
+			</view>
+			<!-- <uni-forms-item label="公司名称" name="name">
+				<uni-easyinput type="text" v-model="userInfo.company" placeholder="请输入所在公司名称" />
+			</uni-forms-item> -->
+			<text v-if="formInfo.jobTitle">您的职称:</text>
+			<uni-forms-item label="" name="realName" label-width="0">
+				 <uni-easyinput type="text" v-model="formInfo.jobTitle" :clearable="false" placeholder="请输入您的职称" />
+			</uni-forms-item>
+			<text v-if="formInfo.contactPerson">联系姓名:</text>
+			<uni-forms-item label="联系人姓名" name="contactMethod">
+				 <uni-easyinput type="text" v-model="formInfo.contactPerson" :clearable="false" placeholder="请输入联系人姓名" />
+			</uni-forms-item>
+			<text v-if="formInfo.contactMethod">联系方式:</text>
+			<uni-forms-item label="联系方式" name="phone">
+				 <uni-easyinput type="text"  v-model="formInfo.contactMethod" :clearable="false" placeholder="请输入联系手机/微信/邮箱" />
+			</uni-forms-item>
+			
+			<uni-forms-item label="11" name="check">
+				<uni-data-checkbox :multiple="true" v-model="formInfo.agree" :localdata="[{text: '同意平台核查所填信息的真实性',value: '是'}]" />
+			</uni-forms-item>
+		</uni-forms>
+	</view>
+	
+	<view class="tn-flex tn-footerfixed">
+		<view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+			<tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold @click="preStep()">
+				<!-- <text class="tn-icon-light tn-padding-right-xs tn-color-black"></text> -->
+				<text class="tn-color-white">上一步</text>
+				<!-- <text class="tn-icon-camera tn-padding-left-xs tn-color-black"></text> -->
+			</tn-button>
+		</view>
+		<view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+			<tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold @click="saveForm()">
+				<!-- <text class="tn-icon-light tn-padding-right-xs tn-color-black"></text> -->
+				<text class="tn-color-white">提交审核</text>
+				<!-- <text class="tn-icon-camera tn-padding-left-xs tn-color-black"></text> -->
+			</tn-button>
+		</view>
+	</view>
+</view>
+		</view>
+		
+		
+
+		<view class='tn-tabbar-height'></view>
+
+	</view>
+</template>
+
+<script>
+	import template_page_mixin from '@/libs/mixin/template_page_mixin.js';
+	import request from '../utils/request';
+	export default {
+		name: 'TemplateEdit',
+		mixins: [template_page_mixin],
+		data() {
+			return {
+				canSave: true,
+				selectValue: '',
+				//非常紧急/两周/一月/长期
+				selectList: [{
+						value: '非常紧急',
+						text: '非常紧急'
+					},
+					{
+						value: '两周',
+						text: '两周'
+					},
+					{
+						value: '一月',
+						text: '一月'
+					},
+					{
+						value: '长期',
+						text: '长期'
+					}
+				],
+				content:'',
+				formInfo:{
+					jobTitle:'',
+					// contactPerson:JSON.parse(uni.getStorageSync('userInfo')).contactNickName?JSON.parse(uni.getStorageSync('userInfo')).contactNickName:'用户'+JSON.parse(uni.getStorageInfoSync('userInfo')).userName.splice(-4),
+					agree:['是'],
+					contactPerson:JSON.parse(uni.getStorageSync('userInfo')).contactNickName,
+					contactMethod:JSON.parse(uni.getStorageSync('userInfo')).contactMethod||JSON.parse(uni.getStorageSync('userInfo')).userName
+				},
+				imgList:[],
+				fileDetailList:[],
+				stepIndex:1,
+				// stepList:[{title:'填写需求'},{title: '填写联系方式'}],
+				stepList: [{
+						name: '填写需求',
+						icon: 'circle',
+						selectIcon: 'circle-fill'
+					},
+					{
+						name: '填写联系方式',
+						icon: 'trusty',
+						selectIcon: 'trusty-fill'
+					}
+				],
+				tagList: [{
+						color: 'red',
+						title: "元神",
+					},
+					{
+						color: 'cyan',
+						title: "LOL",
+					},
+					{
+						color: 'blue',
+						title: "速立保",
+					},
+					{
+						color: 'green',
+						title: "科技",
+					},
+					{
+						color: 'orange',
+						title: "免费",
+					},
+					{
+						color: 'purplered',
+						title: "前端",
+					},
+					{
+						color: 'purple',
+						title: "后端",
+					},
+					{
+						color: 'brown',
+						title: "UI设计",
+					},
+					{
+						color: 'yellowgreen',
+						title: "求助",
+					},
+					{
+						color: 'grey',
+						title: "吃货",
+					},
+					{
+						color: 'orangered',
+						title: "萌宠",
+					}
+				],
+				action: 'https://www.hualigs.cn/api/upload',
+				// action: '',
+				formData: {
+					apiType: 'this,ali',
+					token: 'dffc1e06e636cff0fdf7d877b6ae6a2e',
+					image: null
+				},
+				fileList: [],
+				showUploadList: true,
+				customBtn: false,
+				autoUpload: true,
+				showProgress: false,
+				deleteable: true,
+				customStyle: false,
+				maxCount: 9,
+				disabled: false,
+				searchValue: '',
+				items: [],
+				org:{
+					name:'',
+					regNumber:''
+				},
+			}
+		},
+		watch: {
+			searchValue(val, oldval) {
+				console.error(val,this.org.name);
+				if(val!==this.org.name){
+					this.current = null;
+				}
+				if(this.stepIndex==2){
+					this.search(val)
+				}
+				// if(this.org.name!=this.org.regNumber){
+					
+				// }
+				
+			}
+		},
+		onLoad() {
+		    this.getCompany();
+		},
+		methods: {
+			 getCompany(){
+			 	let that = this;
+			 		request.post('/slbUserCompanyRel/show/my', {
+			 			userNo: uni.getStorageSync('userNo')
+			 		}).then(res => {
+			 			if (res.success) {
+			 				let list = res.list || [];
+			 				for(let i=0;i<list.length;i++){
+			 					list[i].name = list[i].company;
+			 					list[i].regNumber= list[i].company;
+								if(list[i].isDefault=='1'){
+									that.searchValue = list[i].name
+									that.current = list[i].name;
+									that.org = list[i];
+									if(list[i].jobTitle){
+										that.formInfo.jobTitle = list[i].jobTitle
+									}
+								}
+			 					 
+			 				}
+			 				that.items = list;
+			 			}  
+			 		})
+			},
+			preStep(){
+				this.stepIndex = 1;
+			},
+			nextStep(){
+				
+				if(this.content==''&&this.fileDetailList.length===0){
+					 uni.showToast({
+						title:'请填写需求或上传图片/文件',
+						icon:'none'
+					 })
+					 this.stepIndex = 1;
+					 
+					 return false;
+				}
+				 
+				this.stepIndex = 2;
+			},
+			 changeSelect(e) {
+			        this.selectValue = e;
+			      },
+			saveForm(status){
+				if(!this.canSave){
+					return false;
+				}
+				let that = this;
+				let params = {
+					
+				};
+				
+				if(!this.org.name){
+					uni.showToast({
+						title: '请检查公司名称是否准确',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				
+				
+				
+				if(this.formInfo.jobTitle.length<1){
+					uni.showToast({
+						title: '请输入您的职称',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				if(this.formInfo.contactPerson.length<1){
+					uni.showToast({
+						title: '请输入联系人姓名',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				if(this.formInfo.contactMethod.length<1){
+					uni.showToast({
+						title: '请输入联系手机/微信/邮箱',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				
+				if(this.formInfo.agree.length<1){
+					uni.showToast({
+						title: '请勾选同意平台核查所填信息的真实性',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				uni.showToast({
+					title: '提交中...',
+					icon:'none'
+				});
+				
+				that.canSave = false;
+				
+				params.slbResourceDemand = JSON.stringify({
+					type:'3',
+					company:this.org.name,
+ 					jobTitle:this.formInfo.jobTitle,
+					contactPerson:this.formInfo.contactPerson,
+					contactMethod:this.formInfo.contactMethod,
+					content:that.content,
+					userNo:uni.getStorageSync('userNo'),
+					validDate:this.selectValue,
+					status: status==1?status:undefined
+				});
+				// params.slbUserCompanyRel = JSON.stringify({
+				// 	jobTitle:this.formInfo.jobTitle,
+				// 	contactPerson:this.formInfo.contactPerson,
+				// 	contactMethod:this.formInfo.contactMethod,
+				// 	userNo:uni.getStorageSync('userNo'),
+				// });
+				
+				params.fileDetailList = JSON.stringify(this.fileDetailList);
+				request.post('/slbResourceDemand/add', params).then(res => {
+				    that.canSave = true;
+					if(res.success){
+						uni.showToast({
+							title:status?'暂存成功':'发布已提交,可在我的需求里查看审核进度',
+							icon:'none',
+							success:()=>{
+								setTimeout(()=>{
+									uni.redirectTo({
+									 url: "/pages/mine/need"
+									});
+								},1500)
+								
+							}
+						})
+						
+					}else{
+						uni.showToast({
+							title:res.msg,
+							icon:'none'
+						})
+					}
+					console.warn(res);
+				})
+			},
+			// 跳转
+			tn(e) {
+				uni.navigateTo({
+					url: e,
+				});
+			},
+			// 手动上传文件
+			upload() {
+				console.warn(121212);
+			},
+			// 手动清空列表
+			clear() {
+				this.$refs.imageUpload.clear()
+			},
+			// 图片拖拽重新排序
+			onSortList(list) {
+				console.log(list);
+			},
+			select(e) {
+				console.log('选择文件:', e)
+				let tempFiles = e.tempFiles;
+				for (let i in tempFiles) {
+					this.upfile(tempFiles[i])
+				}
+			},
+			deleteFile(e, index) {
+				for(let i=0;i<this.fileDetailList.length;i++){
+					if(e.tempFile.path===this.fileDetailList[i].path){
+						this.fileDetailList.splice(i, 1);
+					}
+				}
+			},
+			upfile(file) {
+				let that = this;
+				console.warn(file);
+				uni.uploadFile({
+					url: 'http://slb-m.dev.ml1993.com/oss/upload/userFeedback', //仅为示例,非真实的接口地址
+					filePath: file.url,
+					name: 'file',
+					success: (uploadFileRes) => {
+						console.warn(JSON.parse(uploadFileRes.data));
+						let resultMap = JSON.parse(uploadFileRes.data).resultMap;
+						that.fileDetailList.push({
+							name: file.name,
+							fileName: file.name, // 原始文件名
+							ftpUrl: resultMap.uploadUrl, // 文件访问url
+							path:file.path
+						})
+					}
+				});
+			},
+			// 上传成功
+			success(e) {
+				console.log('上传成功')
+			},
+			selectChange(e){
+				console.error(e);
+				this.searchValue = e.name
+				this.current = e.regNumber;
+				if(e.jobTitle){
+					this.formInfo.jobTitle = e.jobTitle;
+				}
+				this.org = e;
+			},
+			search: function(val) {
+				let that = this;
+				
+				if (val && val.length > 3) {
+					request.post('/member/searchCompys', {
+						keyWord:  val
+					}).then(res => {
+						if(res.success){
+							let list = res.resultMap.data || [];
+							that.items = list;
+							// that.setData({
+							// 	items: list
+							// })
+						}else{
+							uni.showToast({
+								title: res.msg,
+								icon: 'none'
+							})
+						}
+					})
+					 
+				}else{
+					that.items = [];
+					 
+				}
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.template-edit {}
+
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+
+	/* 底部悬浮按钮 start*/
+	.tn-tabbar-height {
+		min-height: 100rpx;
+		height: calc(120rpx + env(safe-area-inset-bottom) / 2);
+	}
+
+	.tn-footerfixed {
+		position: fixed;
+		width: 100%;
+		bottom: calc(env(safe-area-inset-bottom));
+		z-index: 1024;
+		box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0);
+		background: #fff;
+	}
+
+	/* 底部悬浮按钮 end*/
+
+	/* 标签内容 start*/
+	.tn-tag-content {
+		&__item {
+			display: inline-block;
+			line-height: 45rpx;
+			padding: 10rpx 30rpx;
+			margin: 20rpx 20rpx 5rpx 0rpx;
+
+			&--prefix {
+				padding-right: 10rpx;
+			}
+		}
+	}
+	
+	/deep/ .uni-forms-item__label {
+		display: none;
+	}
+
+	/* 标签内容 end*/
+</style>

+ 141 - 0
components/basic-table/basic-table.scss

@@ -0,0 +1,141 @@
+$bg-base-color: #ffffff;
+$header-color: #909399;
+$footer-bg-color: #f5f7fa;
+$table-border: #edeeee;
+$table-stripe-color: #fafafa;
+
+.base-table {
+	overflow: auto;
+	box-sizing: content-box;
+	&.is-border {
+		border: 1px solid $table-border;
+		border-bottom: none;
+		.b-th,
+		.b-td {
+			border-right: 1px solid $table-border;
+			&:last-of-type {
+				border-right: none;
+			}
+		}
+	}
+	&.no-data {
+		.base-table-body {
+			border-bottom: 1px solid $table-border;
+		}
+	}
+	.base-table-inner {
+		display: flex;
+		height: 100%;
+		flex-direction: column;
+		.base-table-header,
+		.base-table-footer {
+			width: 100%;
+			flex-shrink: 0;
+			position: sticky;
+			z-index: 3;
+		}
+		.base-table-header {
+			top: 0;
+			.b-td {
+				background-color: $bg-base-color;
+			}
+		}
+		.base-table-footer {
+			bottom: 0;
+			.b-tr {
+				background-color: $footer-bg-color !important;
+			}
+			.b-td {
+				border-top: 1px solid $table-border;
+				background-color: $footer-bg-color !important;
+			}
+		}
+		.base-table-body {
+			position: relative;
+			flex: 1;
+		}
+
+		.b-table {
+			table-layout: fixed;
+			display: table;
+			.b-thead {
+				color: $header-color;
+				table-layout: fixed;
+				display: table-header-group;
+				vertical-align: middle;
+				font-weight: 700;
+			}
+			.b-tbody {
+				display: table-row-group;
+				vertical-align: middle;
+				table-layout: fixed;
+			}
+			.b-tr {
+				background-color: $bg-base-color;
+				display: table-row;
+				&.is-stripe {
+					.b-td {
+						background-color: $table-stripe-color;
+					}
+				}
+			}
+			.b-th,
+			.b-td {
+				font-size: 28rpx;
+				display: table-cell;
+				border-bottom: 1px solid $table-border;
+				padding: 8px 0;
+				box-sizing: border-box;
+				text-overflow: ellipsis;
+				position: relative;
+				vertical-align: middle;
+				text-align: left;
+				z-index: 1;
+				.b-cell {
+					box-sizing: border-box;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					white-space: normal;
+					word-break: break-all;
+					line-height: 23px;
+					padding: 0 12px;
+				}
+				&.fixed {
+					position: sticky !important;
+					z-index: 2;
+					background: $bg-base-color;
+					border-right: 0;
+					&::before {
+						content: '';
+						position: absolute;
+						top: 0px;
+						width: 10px;
+						bottom: -1px;
+					}
+				}
+				&.fixed-left {
+					left: 0;
+					&::before {
+						right: -10px;
+						box-shadow: inset 10px 0 10px -10px rgba(0, 0, 0, 0.15);
+					}
+				}
+				&.fixed-right {
+					right: 0;
+					&::before {
+						left: -10px;
+						box-shadow: inset -10px 0 10px -10px rgba(0, 0, 0, 0.15);
+					}
+				}
+			}
+			.base-table-empty {
+				min-height: 60px;
+				line-height: 60px;
+				width: 100%;
+				text-align: center;
+				color: $header-color;
+				font-size: 24rpx;
+			}
+		}
+	}
+}

+ 363 - 0
components/basic-table/basic-table.vue

@@ -0,0 +1,363 @@
+<template>
+	<view class="base-table" :style="[getTableStyle]" :class="{ 'is-border': border, 'no-data': data.length === 0 }">
+		<view class="base-table-inner">
+			<view class="base-table-header" v-if="showHeader">
+				<view class="b-table" :style="[tableBodyStyle]">
+					<view class="b-thead">
+						<view class="b-tr" :class="getHeaderClass" :style="getHeaderStyle" @click="handleHeaderClick">
+							<view class="b-th" v-if="indexShow" :style="[getIndexColStyle]"><view class="b-cell">序号</view></view>
+							<view class="b-th" v-for="item in columns" :key="item.fieldName" :class="[getCellProps(item).class]" :style="[getCellProps(item).style]">
+								<view class="b-cell">{{ item.fieldDesc }}</view>
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view class="base-table-body">
+				<view class="b-table" :style="[tableBodyStyle]">
+					<view class="b-tbody" v-if="data.length > 0">
+						<view
+							class="b-tr"
+							v-for="(scope, index) in data"
+							:key="index"
+							:class="[getBodyClass(scope, index)]"
+							:style="[getBodyStyle(scope, index)]"
+							@click="handleRowClick(scope, index)"
+						>
+							<view class="b-td" v-if="indexShow" :style="[getIndexColStyle]">
+								<view class="b-cell">{{ getIndexMethod(index) }}</view>
+							</view>
+							<view class="b-td" v-for="column in columns" :key="column.fieldName" :class="[getCellProps(column).class]" :style="[getCellProps(column).style]">
+								<view class="b-cell" @click.stop="handleCellClick(scope, column, index)">
+									<slot name="item" :scope="scope" :column="column" v-if="column.fieldType === 'slot'"></slot>
+									<view v-else>{{ scope[column.fieldName] }}</view>
+								</view>
+							</view>
+						</view>
+					</view>
+					<view class="base-table-empty" v-else>
+						<view class="mt20" v-if="!$slots.empty">{{ emptyText }}</view>
+						<slot name="empty"></slot>
+					</view>
+				</view>
+			</view>
+			<view class="base-table-footer" v-if="showFooter">
+				<view class="b-table" :style="[tableBodyStyle]">
+					<view class="b-tbody">
+						<view class="b-tr">
+							<view class="b-td" v-if="indexShow" :style="[getIndexColStyle]">
+								<view class="b-cell">{{ footerText }}</view>
+							</view>
+							<view
+								class="b-td"
+								v-for="(item, index) in sumList"
+								:key="index"
+								:class="[getCellProps(columns[index]).class]"
+								:style="[getCellProps(columns[index]).style]"
+							>
+								<view class="b-cell">{{ item }}</view>
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		columns: {
+			type: Array,
+			default: () => []
+		},
+		data: {
+			type: Array,
+			default: () => []
+		},
+		align: {
+			type: String,
+			default: 'left'
+		},
+		height: {
+			type: String
+		},
+		maxHeight: {
+			type: String
+		},
+		width: {
+			type: String,
+			default: '100%'
+		},
+		emptyText: {
+			type: String,
+			default: '暂无数据'
+		},
+		border: {
+			type: Boolean,
+			default: false
+		},
+		stripe: {
+			type: Boolean,
+			default: false
+		},
+		showHeader: {
+			type: Boolean,
+			default: true
+		},
+		showFooter: {
+			type: Boolean,
+			default: false
+		},
+		footerMethod: {
+			type: Function
+		},
+		footerText: {
+			type: String,
+			default: '合计'
+		},
+		indexShow: {
+			type: Boolean,
+			default: false
+		},
+		minItemWidth: {
+			type: Number,
+			default: 80
+		},
+		rowClassName: {
+			type: [Function, String]
+		},
+		rowStyle: {
+			type: [Function, Object]
+		},
+		indexMethod: {
+			type: Function
+		},
+		headerRowClassName: {
+			type: String
+		},
+		headerRowStyle: {
+			type: Object
+		},
+		indexWidth: {
+			type: String,
+			default: '60px'
+		}
+	},
+	data() {
+		return {
+			sumList: [],
+			tableWidth: 0
+		};
+	},
+	mounted() {
+		const query = uni.createSelectorQuery().in(this).select('.base-table');
+		query.boundingClientRect(data => {
+				this.tableWidth = data.width;
+		}).exec();
+	},
+	computed: {
+		getTableStyle() {
+			const { width, height, maxHeight } = this;
+			const styleObj = {};
+			if (width) {
+				styleObj.width = width;
+			}
+			if (height) {
+				styleObj.height = height;
+			}
+			if (maxHeight) {
+				styleObj.maxHeight = maxHeight;
+			}
+			return styleObj;
+		},
+		tableBodyStyle() {
+			if (!this.tableWidth) return {};
+			const clienWidth = this.tableWidth;
+			const flexColumn = this.columns.filter(item => !item.width);
+			//set min width
+			const minWidth = this.minItemWidth;
+
+			let bodyMinWidth = this.columns.reduce((t, c) => {
+				c.width = c.width || minWidth;
+				return t + parseFloat(c.width);
+			}, 0);
+			if(this.indexShow){
+				bodyMinWidth+=parseFloat(this.indexWidth)
+			}
+			if (flexColumn.length > 0 && bodyMinWidth < clienWidth) {
+				const flexWidth = clienWidth - bodyMinWidth;
+				if (flexColumn.length === 1) {
+					flexColumn[0].width = minWidth + flexWidth;
+				} else {
+					const scaleWidth = flexWidth / flexColumn.length;
+					flexColumn.forEach(item => {
+						item.width = minWidth + Math.floor(scaleWidth);
+					});
+				}
+			}
+			bodyMinWidth = Math.max(bodyMinWidth, clienWidth);
+			return {
+				width: `${bodyMinWidth}px`
+			};
+		},
+		showXScroll() {
+			const clienWidth = this.tableWidth;
+			return clienWidth < parseFloat(this.tableBodyStyle?.width || 0);
+		},
+		isEmpty() {
+			return this.data.length === 0;
+		},
+		getHeaderClass() {
+			const headerClass = [];
+			if (this.headerRowClassName) {
+				headerClass.push(this.headerRowClassName);
+			}
+			return headerClass;
+		},
+		getHeaderStyle() {
+			const headerStyle = [];
+			if (typeof this.headerRowStyle === 'object') {
+				if (this.headerRowStyle) {
+					headerStyle.push(this.headerRowStyle);
+				}
+			}
+			return headerStyle;
+		},
+		getIndexColStyle() {
+			return {
+				textAlign: this.align,
+				width: this.indexWidth
+			};
+		}
+	},
+	methods: {
+		init() {
+			this.sumList = [];
+			if (this.showFooter && this.data.length > 0) {
+				const { columns, data, footerText } = this;
+				if (typeof this.footerMethod === 'function') {
+					this.sumList = this.footerMethod({ columns, data });
+				} else {
+					columns.forEach((column, index) => {
+						if (!this.indexShow && index === 0) {
+							this.sumList[index] = footerText;
+							return;
+						}
+						const values = data.map(item => Number(item[column.fieldName]));
+						const precisions = [];
+						let notNumber = true;
+						values.forEach(value => {
+							if (!Number.isNaN(+value)) {
+								notNumber = false;
+								const decimal = `${value}`.split('.')[1];
+								precisions.push(decimal ? decimal.length : 0);
+							}
+						});
+						const precision = Math.max.apply(null, precisions);
+						if (!notNumber) {
+							this.sumList[index] = values.reduce((prev, curr) => {
+								const value = Number(curr);
+								if (!Number.isNaN(+value)) {
+									return Number.parseFloat((prev + curr).toFixed(Math.min(precision, 20)));
+								} else {
+									return prev;
+								}
+							}, 0);
+						} else {
+							this.sumList[index] = '';
+						}
+					});
+				}
+			}
+		},
+
+		getBodyClass(scope, index) {
+			const bodyClass = [];
+			if (this.stripe) {
+				bodyClass.push({ 'is-stripe': index % 2 === 1 });
+			}
+			if (typeof this.rowClassName === 'function') {
+				const rowClass = this.rowClassName?.(scope, index);
+				if (rowClass) {
+					bodyClass.push(rowClass);
+				}
+			} else if (typeof this.rowClassName === 'string') {
+				if (this.rowClassName) {
+					bodyClass.push(this.rowClassName);
+				}
+			}
+			return bodyClass;
+		},
+
+		getBodyStyle(scope, index) {
+			const bodyStyle = [];
+			if (typeof this.rowStyle === 'function') {
+				const rowStyle = this.rowStyle?.(scope, index);
+				if (rowStyle) {
+					bodyStyle.push(rowStyle);
+				}
+			} else if (typeof this.rowStyle === 'object') {
+				if (this.rowStyle) {
+					bodyStyle.push(this.rowStyle);
+				}
+			}
+			return bodyStyle;
+		},
+
+		getIndexMethod(index) {
+			let curIndex = index + 1;
+			if (typeof this.indexMethod === 'function') {
+				curIndex = this.indexMethod?.(index);
+			}
+			return curIndex;
+		},
+
+		getCellProps(row) {
+			const classList = [];
+			if (this.showXScroll && row.fixed) {
+				classList.push('fixed');
+				if (row.fixed === 'left') {
+					classList.push('fixed-left');
+				} else {
+					classList.push('fixed-right');
+				}
+			}
+			return {
+				class: classList,
+				style: {
+					width: `${row.width}px`,
+					textAlign: this.align,
+					minWidth: `${this.minItemWidth}px`
+				}
+			};
+		},
+
+		handleHeaderClick() {
+			this.$emit('header-click');
+		},
+
+		handleRowClick(scope, index) {
+			this.$emit('row-click', scope, index);
+		},
+
+		handleCellClick(scope, column, index) {
+			this.$emit('cell-click', { scope, column, index });
+		},
+	},
+	watch: {
+		data: {
+			handler() {
+				this.init();
+			},
+			immediate: true,
+			deep: true
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import './basic-table.scss';
+</style>

+ 108 - 0
components/w-select/readme.md

@@ -0,0 +1,108 @@
+#### props
+
+| 名称         | 类型    | 默认值   | 说明                                                   |
+| ------------ | ------- | -------- | ------------------------------------------------------ |
+| width        | string  | '200px'  | 选择框宽度                                             |
+| height       | string  | '30px'   | 选择框高度                                             |
+| bgColor      | string  | '#fff'   | 选择框背景颜色                                         |
+| defaultValue | string  | '请选择' | 默认显示的名称                                         |
+| valueName    | string  | 'label'  | 显示的内容字段名                                       |
+| keyName      | string  | 'value'  | 绑定的内容字段名                                       |
+| list         | array   | []       | 展示的内容列表                                         |
+| showClose    | boolean | true     | 是否显示删除按钮                                       |
+| multiple     | boolean | false    | 是否开启多选                                           |
+| filterable   | boolean | false    | 是否开启搜索功能,开启后直接输入值不选择也可以保存内容 |
+
+该组件默认下拉选择器是从底部弹出,当检测到底部高度不足时则会在上面弹出
+
+#### events
+
+| 事件名 | 说明                                         |
+| ------ | -------------------------------------------- |
+| change | 选择的内容改变时触发,返回的参数为列表的item |
+
+#### 基本使用
+
+绑定的值通过`v-model`绑定,如下面的`chooseValue`,需要获取item的值可以监听`@change`事件
+
+```vue
+<template>
+  <view class="login">
+    <w-select 
+      style="margin-left: 20rpx;" 
+      v-model='chooseValue' 
+      :list='list'
+      valueName='content' 
+      keyName="id"
+      @change='change'
+    >
+    </w-select>
+  </view>
+</template>
+<script>
+  export default {
+    data() {
+      return {
+        chooseValue: "",
+        list: [{
+          id: 1,
+          content: '张三'
+        }, {
+          id: 2,
+          content: '李四'
+        }, {
+          id: 3,
+          content: '王五'
+        }],
+      };
+    },
+    methods: {
+      change(e) {
+        console.log('chooseValue', this.chooseValue)
+      }
+    },
+  }
+</script>
+```
+
+#### 多选
+
+多选开启`multiple`属性,双向绑定的值必须为数组类型,在change事件中根据自己需求进行处理。
+
+```vue
+<template>
+    <w-select 
+      v-model='chooseValue' 
+      :list='list'
+      multiple
+      valueName='content' 
+      keyName="id"
+      @change='change'
+    >
+    </w-select>
+</template>
+<script>
+  export default {
+    data() {
+      return {
+        chooseValue: [],
+        list: [{
+          id: 1,
+          content: '张三'
+        }, {
+          id: 2,
+          content: '李四'
+        }, {
+          id: 3,
+          content: '王五'
+        }],
+      };
+    },
+    methods: {
+      change(e) {
+        console.log('chooseValue', this.chooseValue)
+      }
+    },
+  }
+</script>
+```

ファイルの差分が大きいため隠しています
+ 562 - 0
components/w-select/w-select.vue


+ 25 - 0
config.js

@@ -0,0 +1,25 @@
+// api请求地址
+// export const VUE_APP_URL = 'https://devmp.lx-device.com/back'; //生产体验版本 连接开发前管控制器
+// export const REACT_URL = 'https://devmp.lx-device.com';//生产体验版
+
+// export const VUE_APP_URL = 'https://api.lx-device.com'; //生产版本 连接生产前管控制器
+// export const REACT_URL = 'https://xuncai.lx-device.com';//生产版
+
+
+// export const VUE_APP_URL = 'http://local.lx-device.com:8032';
+export const VUE_APP_URL = 'http://slb-m.dev.ml1993.com/lx-api'; //开发版体验 连接开发前管控制器
+export const REACT_URL = 'http://slb-m.dev.ml1993.com/lx-api'; //开发版体验 
+
+
+// export const VUE_APP_URL = 'http://192.168.11.67:8032'; //开发版体验 连接开发前管控制器
+// export const REACT_URL = 'http://192.168.1.52'; //开发版体验 
+
+
+// export const VUE_APP_URL =                                                                                                                                                                                                                     ://stg-xuncai.lx-device.com';//测试环境
+
+
+   // export const REACT_URL = 'http://localhost:3034';
+// 公众号中是否使用公众号授权登录 true公众号授权->auth界面下一步操作  false直接跳转login登录界面
+export const WECHAT_LOGIN = true;
+// 授权之后返回原有地址
+export const WECHAT_AUTH_BACK_URL = 'WECHAT_AUTH_BACK_URL';

+ 20 - 0
index.html

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

+ 94 - 0
libs/components/demo-title.vue

@@ -0,0 +1,94 @@
+<template>
+  <view class="demo-title">
+    <view>
+      <view v-if="type === 'first'" class="main_title">
+        <view v-if="leftIcon" class="main_title__icon main_title__icon--left" :class="[`tn-icon-${leftIcon}`]"></view>
+        <view class="main_title__content">{{ title }}</view>
+        <view v-if="rightIcon" class="main_title__icon main_title__icon--right" :class="[`tn-icon-${rightIcon}`]"></view>
+      </view>
+      <view v-if="type === 'second'" class="second_title">
+        <view class="second_title__content">{{ title }}</view>
+      </view>
+    </view>
+    <view class="content" :class="[{
+      'content--padding': contentPadding
+    }]">
+      <slot></slot>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'demo-title',
+    options: {
+    	// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
+    	virtualHost: true
+    },
+    props: {
+      // 标题类型
+      type: {
+        type: String,
+        default: 'first'
+      },
+      // 标题
+      title: {
+        type: String,
+        default: ''
+      },
+      // 左图标
+      leftIcon: {
+        type: String,
+        default: 'star'
+      },
+      // 右图标
+      rightIcon: {
+        type: String,
+        default: 'star'
+      },
+      // 内容容器是否有两边边距
+      contentPadding: {
+        type: Boolean,
+        default: true
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .main_title {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 50rpx;
+    font-size: 36rpx;
+    font-weight: bold;
+    
+    &__content {
+      padding: 0 18rpx;
+    }
+    
+    &__icon {
+      font-size: 34rpx;
+    }
+  }
+  
+  .second_title {
+    margin: 24rpx 0;
+    margin-left: 30rpx;
+    
+    &__content {
+      font-size: 32rpx;
+      font-weight: bold;
+    }
+  }
+  
+  .content {
+    margin-top: 30rpx;
+    
+    &--padding {
+      margin-left: 30rpx;
+      margin-right: 30rpx;
+    }
+  }
+</style>

+ 689 - 0
libs/components/dynamic-demo-template.vue

@@ -0,0 +1,689 @@
+<template>
+  <view class="dynamic-demo">
+
+    <!-- 效果预览窗口 -->
+    <view v-if="!noDemo" class="demo-container" :class="{'demo-container--full': full}">
+      <view class="demo">
+        <slot></slot>
+      </view>
+      <!-- 提示信息 -->
+      <view v-if="haveTips">
+        <view class="demo__tips__icon" @click="demoTipsClick">
+          <view class="icon tn-icon-help"></view>
+        </view>
+        <view class="demo__tips__content"
+          :class="[showContentTips ? 'demo__tips__content--show' : 'demo__tips__content--hide']">
+          <view v-for="(item,index) in tipsData" :key="index" class="demo__tips__content--item">{{ item }}</view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 模式切换 -->
+    <view v-if="multiMode" class="mode-switch">
+      <view class="mode-switch__container">
+        <view v-for="(item, index) in sectionModeListInfos" :key="index" class="mode-switch__item"
+          :class="[`mode-switch-item-${index}`,{'mode-switch__item--active': modeIndex === index}]"
+          @click="switchMode(index)">{{ item.name }}</view>
+
+        <!-- 滑块样式 -->
+        <view class="mode-switch__slider" :style="[modeSwitchSliderStyle]"></view>
+      </view>
+    </view>
+
+    <!-- 组件对应可选项容器 -->
+    <view class="section-container">
+      <scroll-view
+        class="section__scroll-view"
+        :class="{'section__scroll-view--auto': sectionScrollViewStyle.height === 'auto'}"
+        :style="[sectionScrollViewStyle]"
+        :scroll-y="sectionScrollViewStyle.height !== 'auto'"
+      >
+        <block v-for="(item,index) in btnsList" :key="index">
+          <view class="section__content" :class="{'section__content--visible': item.show}">
+            <view class="section__content__title">
+              <view class="section__content__title__left-line" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]"></view>
+              <view class="section__content__title--text tn-text-ellipsis" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]">{{ item.title }}</view>
+              <view class="section__content__title__right-line" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]"></view>
+            </view>
+            <view class="section__content__btns">
+              <view v-for="(section_btn,section_index) in item.optional" :key="section_index"
+                class="section__content__btns__item" :class="[`tn-main-gradient-${tuniaoColorList[index]}--light`]" @click="sectionBtnClick(index, section_index)">
+                <view class="section__content__btns__item__bg"
+                  :class="[`tn-main-gradient-${tuniaoColorList[index]}`, {'section__content__btns__item__bg--active':sectionIndex[modeIndex][index]['value'] === section_index}]"></view>
+                <view class="section__content__btns__item--text tn-text-ellipsis"
+                  :class="[sectionIndex[modeIndex][index]['value'] === section_index ? 'section__content__btns__item--text--active' : `tn-color-${tuniaoColorList[index]}`]">{{ section_btn }}</view>
+              </view>
+            </view>
+          </view>
+        </block>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'dynamic-demo-template',
+    props: {
+      // 可选项列表数据
+      sectionList: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 提示信息
+      tips: {
+        type: [String, Array],
+        default: ''
+      },
+      // 演示框的内容是否为铺满
+      full: {
+        type: Boolean,
+        default: false
+      },
+      // 是否使用了自定义顶部导航栏
+      customBar: {
+        type: Boolean,
+        default: true
+      },
+      // 是否全屏滚动
+      fullWindowsScroll: {
+        type: Boolean,
+        default: false
+      },
+      // 没有演示内容
+      noDemo: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      tipsData() {
+        if (typeof this.tips === 'string') {
+          return [this.tips]
+        }
+        return this.tips
+      },
+      haveTips() {
+        return this.tips && this.tips.length > 0
+      },
+      multiMode() {
+        return this.sectionList.length > 1
+      },
+      sectionModeList() {
+        return this.sectionList.map((item) => {
+          return item.name
+        })
+      }
+    },
+    data() {
+      return {
+        // 速立保颜色列表
+        tuniaoColorList: this.$t.color.getTuniaoColorList(),
+        // 保存选项列表信息(由于prop中的数据时不能被修改的)
+        _sectionList: [],
+        // 模式列表信息
+        sectionModeListInfos: [],
+        // 所选模式的序号
+        modeIndex: 0,
+        // 模式选择滑块样式
+        modeSwitchSliderStyle: {
+          width: 0,
+          left: 0
+        },
+        // 显示组件相关提示信息
+        showContentTips: false,
+        // 可选项滚动容器样式
+        sectionScrollViewStyle: {
+          height: 0
+        },
+        // 按钮列表信息
+        btnsList: [],
+        // 标记当前所选按钮
+        sectionIndex: [],
+        // 标记选项按钮是否可以滑动(使用scroll-view进行包裹)
+        sectionScrollFlag: true
+      }
+    },
+    watch: {
+      sectionList: {
+        handler(value) {
+          // 如果sectionList发生改变,重新初始化选项列表信息
+          this.initSectionBtns()
+        },
+        deep: true
+      },
+      sectionScrollFlag(value) {
+        if (!value) {
+          this.sectionScrollViewStyle.height = 'auto'
+        }
+      },
+      fullWindowsScroll: {
+        handler(value) {
+          if (value) {
+            this.sectionScrollViewStyle.height = 'auto'
+          }
+        },
+        immediate: true
+      }
+    },
+    created() {
+      // 初始化可选项模式列表
+      this.sectionModeListInfos = this.sectionModeList.map((item) => {
+        return {
+          name: item
+        }
+      })
+      // 初始化选项按钮默认信息
+      this.initSectionBtns()
+    },
+    mounted() {
+      // 等待加载组件完成
+      // setTimeout(() => {
+      //   // 计算出底部scroll-view的高度
+      //   this.initSectionScrollView()
+
+      //   if (this.multiMode) {
+      //     // 获取模式切换标签的信息
+      //     this.getModeTabsInfo()
+      //   }
+      // }, 10)
+      this.$nextTick(() => {
+        // 计算出底部scroll-view的高度
+        this.initSectionScrollView()
+        
+        if (this.multiMode) {
+          // 获取模式切换标签的信息
+          this.getModeTabsInfo()
+        }
+      })
+    },
+    methods: {
+      // 初始化选项滑动窗口的高度
+      initSectionScrollView() {
+        // 全屏滚动时不进行任何的操作
+        if (this.fullWindowsScroll) {
+          return
+        }
+        // 获取屏幕的高度
+        uni.getSystemInfo({
+          success: (systemInfo) => {
+            // 通过当前屏幕的安全高度减去上一个元素的底部和距离上一个元素的外边距,然后减获取到的值减去标题栏的高度即可
+            const navBarHeight = this.customBar ? 0 : this.vuex_custom_bar_height
+            if (this.multiMode) {
+              uni.createSelectorQuery().in(this).select('.mode-switch').boundingClientRect(data => {
+                if (data.bottom >= systemInfo.safeArea.height) {
+                  this.sectionScrollFlag = false
+                } else {
+                  this.sectionScrollFlag = true
+                  const containerBaseHeight = systemInfo.safeArea.height - data.bottom
+                  this.sectionScrollViewStyle.height = (containerBaseHeight - navBarHeight) + systemInfo.statusBarHeight - uni.upx2px(75) + 'px'
+                }
+              }).exec()
+            } else {
+              if (!this.noDemo) {
+                uni.createSelectorQuery().in(this).select('.demo-container').boundingClientRect(data => {
+                  if (data.bottom >= systemInfo.safeArea.height) {
+                    this.sectionScrollFlag = false
+                  } else {
+                    this.sectionScrollFlag = true
+                    const containerBaseHeight = systemInfo.safeArea.height - data.bottom
+                    this.sectionScrollViewStyle.height = (containerBaseHeight - navBarHeight) + systemInfo.statusBarHeight - uni.upx2px(75) + 'px'
+                  }
+                }).exec()
+              } else {
+                this.sectionScrollFlag = false
+              }
+            }
+            
+          }
+        })
+      },
+      // 更新选项滑动容器的高度
+      updateSectionScrollView() {
+        this.$nextTick(() => {
+          this.initSectionScrollView()
+        })
+      },
+      // 获取各个模式tab的节点信息
+      getModeTabsInfo() {
+        let view = uni.createSelectorQuery().in(this)
+        for (let i = 0; i < this.sectionModeListInfos.length; i++) {
+          view.select('.mode-switch-item-' + i).boundingClientRect()
+        }
+        view.exec(res => {
+          // 如果没有获取到,则重新获取
+          if (!res.length) {
+            setTimeout(() => {
+              this.getModeTabsInfo()
+            }, 10)
+            return
+          }
+          // 将每个模式的宽度放入list中
+          res.map((item, index) => {
+            this.sectionModeListInfos[index].width = item.width
+          })
+          // 初始化滑块的宽度
+          this.modeSwitchSliderStyle.width = this.sectionModeListInfos[0].width + 'px'
+
+          // 初始化滑块的位置
+          this.modeSliderPosition()
+        })
+      },
+      // 设置模式滑块的位置
+      modeSliderPosition() {
+        let left = 0
+        // 计算当前所选模式选项到组件左边的距离
+        this.sectionModeListInfos.map((item, index) => {
+          if (index < this.modeIndex) left += item.width
+        })
+
+        this.modeSwitchSliderStyle.left = left + 'px'
+      },
+      // 切换模式
+      switchMode(index) {
+        // 不允许点击当前激活的选项
+        if (index === this.modeIndex) return
+        this.modeIndex = index
+        this.modeSliderPosition()
+        this.updateSectionBtns()
+        this.$emit('modeClick', {
+          index: index
+        })
+      },
+      // 点击内容提示信息
+      demoTipsClick() {
+        this.showContentTips = !this.showContentTips
+      },
+      // 初始化被选中选项按钮
+      initSectionBtns() {
+        this.sectionIndex = []
+        this.sectionIndex = this.sectionList.map((item) => {
+          if (item.hasOwnProperty('section') && item.section.length > 0) {
+            return Array(item.section.length).fill({
+              value: 0,
+              change: false
+            })
+          } else {
+            return []
+          }
+        })
+        
+        this._sectionList = this.$t.deepClone(this.sectionList)
+        // 给本地选项按钮列表给默认show属性
+        this._sectionList.map((item) => {
+          const section = item.section.map((section_item) => {
+            if (!section_item.hasOwnProperty('show')) {
+              section_item.show = true
+            }
+            return section_item
+          })
+          item.section = section
+          return item
+        })
+        
+        // 更新按钮信息
+        this.updateSectionBtns()
+      },
+      // 跟新选项按钮信息
+      updateSectionBtns(sectionIndex = -1, showState = true) {
+        let sectionOptional = this._sectionList[this.modeIndex]['section']
+        this.btnsList = sectionOptional.map((item, index) => {
+          // 判断是否已经修改了对应的值
+          let changeValue = this.sectionIndex[this.modeIndex][index]['change'] || false
+          let currentSectionIndexValue = this.sectionIndex[this.modeIndex][index]['value'] || 0
+          // 取出默认值(如果是已经修改过的选项,则使用之前的选项信息)
+          let indexValue = changeValue ? currentSectionIndexValue : item.hasOwnProperty('current') ? item.current : 0
+          // 取出是否显示当前选项
+          let show = (sectionIndex !== -1 && sectionIndex === index) ? showState : item.hasOwnProperty('show') ? item.show : true
+          // 处理最大最小值
+          if (indexValue < 0) {
+            indexValue = 0
+          }
+          if (indexValue >= item.optional.length) {
+            indexValue = item.optional.length
+          }
+          // this.sectionIndex[this.modeIndex][index]['value'] = indexValue
+          this.$set(this.sectionIndex[this.modeIndex], index, {value: indexValue, change: changeValue})
+          item.show = show
+          return item
+        })
+      },
+      // 更新选项按钮状态信息
+      updateSectionBtnsState(sectionIndex = -1, showState = true) {
+        // 判断sectionIndex是否为数组
+        if (this.$t.array.isArray(sectionIndex)) {
+          if (sectionIndex.length === 0) {
+            return
+          }
+          sectionIndex = sectionIndex.filter((item) => item >= 0 && item < this.sectionList[this.modeIndex]['section'].length)
+          sectionIndex.map((item) => {
+            this.btnsList[item]['show'] = showState
+            this._sectionList[this.modeIndex]['section'][item]['show'] = showState
+          })
+        } else {
+          if (sectionIndex < 0 || sectionIndex >= this.sectionList[this.modeIndex]['section'].length) {
+            return
+          }
+          // 将按键的对应显示状态设置为对应的状态
+          this.btnsList[sectionIndex]['show'] = showState
+          this._sectionList[this.modeIndex]['section'][sectionIndex]['show'] = showState
+        }
+        
+      },
+      // 更新选项按钮选中信息
+      updateSectionBtnsValue(modeIndex = 0, sectionIndex = -1, value = 0) {
+        if (sectionIndex < 0 || sectionIndex >= this.sectionList[modeIndex]['section'].length) {
+          return
+        }
+        // 如果showState为false则移除对应的选项按钮,否则往对应的位置添加上对应的选项按钮
+        this.sectionIndex[modeIndex][sectionIndex] = {
+          value,
+          change: true
+        }
+      },
+      // 选项按钮点击事件
+      sectionBtnClick(index, sectionIndex) {
+        // if (this.sectionIndex[this.modeIndex][index] === sectionIndex) {
+        //   return
+        // }
+        this.$set(this.sectionIndex[this.modeIndex], index, {value: sectionIndex, change: true})
+        this.$emit('click', {
+          methods: this.btnsList[index]['methods'],
+          index: sectionIndex,
+          name: this.btnsList[index]['optional'][sectionIndex]
+        })
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .dynamic-demo {
+    padding-top: 78rpx;
+
+    /* 顶部模式切换start */
+    .mode-switch {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-top: 75rpx;
+      padding: 0 30rpx;
+
+      &__container {
+        position: relative;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        width: 476rpx;
+        height: 62rpx;
+        background-color: #FFFFFF;
+        box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
+        border-radius: 31rpx;
+      }
+
+      &__item {
+        flex: 1;
+        height: 62rpx;
+        width: 100%;
+        line-height: 62rpx;
+        text-align: center;
+        font-size: 28rpx;
+        color: $tn-font-sub-color;
+        z-index: 2;
+        transition: all 0.3s;
+
+        &--active {
+          color: #FFFFFF;
+          font-weight: bold;
+        }
+      }
+
+      &__slider {
+        position: absolute;
+        height: 62rpx;
+        border-radius: 31rpx;
+        // background-image: linear-gradient(-86deg, #FF8359 0%, #FFDF40 100%);
+        background-image: linear-gradient(-86deg, #00C3FF 0%, #58FFF5 100%);
+        box-shadow: 1rpx 10rpx 24rpx 0rpx #00C3FF77;
+        z-index: 1;
+        transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+      }
+    }
+
+    /* 顶部模式切换end */
+
+    /* 演示内容展示start */
+    .demo-container {
+      min-height: 327rpx;
+      width: calc(100% - 60rpx);
+      background-color: #FFFFFF;
+      box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
+      margin: 0 30rpx 5rpx 30rpx;
+      border-radius: 20rpx;
+      position: relative;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      &--full {
+        display: inline-block;
+        padding-bottom: 20rpx;
+        min-height: 0rpx;
+        padding: 10rpx 20rpx 30rpx;
+      }
+
+      .demo {
+        padding-top: 70rpx;
+
+        &__tips {
+          &__icon {
+            position: absolute;
+            top: 20rpx;
+            right: 16rpx;
+            width: 39rpx;
+            height: 39rpx;
+            line-height: 39rpx;
+            font-size: 39rpx;
+
+            .icon {
+              background: linear-gradient(-45deg, #FF8359 0%, #FFDF40 100%);
+              -webkit-background-clip: text;
+              color: transparent;
+              text-shadow: 0rpx 10rpx 10rpx rgba(255, 156, 82, 0.2);
+            }
+          }
+
+          &__content {
+            position: absolute;
+            top: 65rpx;
+            right: 16rpx;
+            font-size: 20rpx;
+            margin-left: 20rpx;
+            word-wrap: normal;
+            display: flex;
+            flex-direction: column;
+            background-color: #E6E6E6;
+            padding: 20rpx;
+            border-radius: 10rpx;
+            transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1);
+            transform-origin: 0 0;
+            z-index: 999999;
+
+            &--hide {
+              transform: scaleY(0);
+            }
+
+            &--show {
+              transform: scaleY(100%);
+
+              &::after {
+                content: "";
+                width: 0px;
+                height: 0px;
+                border-width: 4px;
+                border-style: solid;
+                border-color: transparent transparent rgba(149, 149, 149, 0.1) transparent;
+                position: absolute;
+                top: -8px;
+                right: 6px;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    /* 演示内容展示end */
+
+    /* 可选项start */
+    .section-container {
+      width: 100%;
+      height: auto;
+      margin-top: 70rpx;
+
+      .section {
+        &__content {
+          margin-top: 70rpx;
+          display: none;
+          
+          &--visible {
+            display: block;
+            
+            &:last-child {
+              padding-bottom: calc(70rpx + env(safe-area-inset-bottom));
+            }
+          }
+
+          &:nth-child(1) {
+            margin-top: 0rpx;
+          }
+
+          &__title {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            margin: 0 30rpx;
+            text-align: center;
+
+            &__left-line,
+            &__right-line {
+
+              width: 100rpx;
+              height: 2rpx;
+              position: relative;
+            }
+
+            &__left-line {
+              &::after {
+                content: '';
+                background: inherit;
+                width: 12rpx;
+                height: 12rpx;
+                position: absolute;
+                top: -12rpx;
+                right: 0rpx;
+                border-radius: 50%;
+                transform: translateY(50%);
+              }
+            }
+
+            &__right-line {
+              &::after {
+                content: '';
+                background: inherit;
+                width: 12rpx;
+                height: 12rpx;
+                position: absolute;
+                top: -12rpx;
+                left: 0rpx;
+                border-radius: 50%;
+                transform: translateY(50%);
+              }
+            }
+
+            &--text {
+              -webkit-background-clip: text;
+              color: transparent;
+              min-width: 124rpx;
+              height: 30rpx;
+              font-size: 32rpx;
+              line-height: 1;
+              margin: 0 35rpx;
+            }
+          }
+
+          &__btns {
+            width: calc(100% - 60rpx);
+            margin: 0 30rpx;
+            margin-top: 29rpx;
+            padding: 50rpx 30rpx 0rpx 0rpx;
+            background-color: #FFFFFF;
+            box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
+            border-radius: 20rpx;
+            display: flex;
+            flex-direction: row;
+            align-items: center;
+            justify-content: flex-start;
+            flex-wrap: wrap;
+
+            &__item {
+              max-width: 30%;
+              padding: 17rpx 36rpx;
+              border-radius: 10rpx;
+              margin-bottom: 40rpx;
+              margin-left: 40rpx;
+              position: relative;
+              z-index: 1;
+
+              // &::before {
+              //   content: " ";
+              //   position: absolute;
+              //   top: 10rpx;
+              //   left: 1rpx;
+              //   width: 100%;
+              //   height: 100%;
+              //   background: inherit;
+              //   filter: blur(24rpx);
+              //   opacity: 1;
+              //   z-index: -1;
+              // }
+
+              &__bg {
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                border-radius: inherit;
+                z-index: -1;
+                opacity: 0;
+                transform: scale(0);
+                transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+                &--active {
+                  opacity: 1;
+                  transform: scale(1);
+                }
+              }
+
+              &--text {
+                font-size: 24rpx;
+                line-height: 1.2em;
+                transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+                
+                &--active {
+                  color: #FFFFFF;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    /* 可选项end */
+  }
+</style>

+ 147 - 0
libs/components/multiple-options-demo.vue

@@ -0,0 +1,147 @@
+<template>
+  <view class="multiple-options">
+    <view class="list">
+      <block v-for="(item, index) in listData" :key="index">
+        <view
+          class="list__item"
+          :class="[`tn-main-gradient-${tuniaoColorList[item.bgColorIndex]}--light`]"
+          @tap="navOptionsPage(item.url)"
+        >
+          <view class="list__content">
+            <view class="list__content__title">{{ item.title }}</view>
+            <view class="list__content__desc">{{ item.desc }}</view>
+          </view>
+          <view class="list__icon">
+            <view class="list__icon__main" :class="[`tn-icon-${item.mainIcon}`, `tn-main-gradient-${tuniaoColorList[item.bgColorIndex]}`]"></view>
+            <view class="list__icon__sub" :class="[`tn-icon-${item.subIcon}`, `tn-main-gradient-${tuniaoColorList[item.bgColorIndex]}`]"></view>
+          </view>
+        </view>
+      </block>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'multiple-options-demo',
+    props: {
+      // 显示的列表数据
+      list: {
+        type: Array,
+        default() {
+          return []
+        }
+      }
+    },
+    data() {
+      return {
+        // 图鸟颜色列表
+        tuniaoColorList: [
+          'red',
+          'purplered',
+          'purple',
+          'bluepurple',
+          'aquablue',
+          'blue',
+          'indigo',
+          'cyan',
+          'teal',
+          'green',
+          'orange',
+          'orangered'
+        ],
+        listData: []
+      }
+    },
+    watch: {
+      list(val) {
+        this.initList()
+      }
+    },
+    created() {
+      this.initList()
+    },
+    methods: {
+      // 初始化列表数据
+      initList() {
+        // 给列表添加背景颜色数据
+        this.listData = this.list.map((item, index) => {
+          item.bgColorIndex = this.getBgNum()
+          item.mainIcon = item?.mainIcon || 'computer-fill'
+          item.subIcon = item?.subIcon || 'share'
+          return item
+        })
+      },
+      // 跳转到对应的选项页面
+      navOptionsPage(url) {
+        uni.navigateTo({
+          url: url
+        })
+      },
+      // 获取酷炫背景随机数
+      getBgNum() {
+        return Math.floor((Math.random() * this.tuniaoColorList.length))
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .list {
+    
+    &__item {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: space-between;
+      width: calc(100% - 60rpx);
+      margin: 108rpx 30rpx 0rpx 30rpx;
+      box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
+      border-radius: 20rpx;
+    }
+    
+    &__content {
+      flex: 1;
+      // color: $tn-font-color;
+      margin: 34rpx 0rpx 27rpx 37rpx;
+      
+      &__title {
+        font-size: 36rpx;
+        font-weight: bold;
+        margin-bottom: 12rpx;
+      }
+      &__desc {
+        font-size: 28rpx;
+      }
+    }
+    
+    &__icon {
+      flex: 1;
+      margin-right: 26rpx;
+      position: relative;
+      
+      &__main, &__sub {
+        -webkit-background-clip: text;
+        color: transparent;
+        position: absolute;
+        transition: transform 0.25s ease;
+      }
+      
+      &__main {
+        font-size: 200rpx;
+        width: 190rpx;
+        line-height: 200rpx;
+        top: 0;
+        right: 0rpx;
+        transform: translateY(-60%);
+      }
+      &__sub {
+        font-size: 70rpx;
+        top: 0;
+        right: 175rpx;
+        transform: translateY(-5rpx);
+      }
+    }
+  }
+</style>

+ 169 - 0
libs/components/nav-index-button.vue

@@ -0,0 +1,169 @@
+<template>
+  <view class="nav-index-button" :style="{bottom: `${bottom}rpx`, right: `${right}rpx`}" @tap.stop="navIndex">
+    <view class="nav-index-button__content">
+        <view class="nav-index-button__content--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur tn-cool-bg-color-5">
+          <view class="tn-icon-home-fill"></view>
+        </view>  
+    </view>
+  
+    <view class="nav-index-button__meteor">
+      <view class="nav-index-button__meteor__wrapper">
+        <view v-for="(item,index) in 6" :key="index" class="nav-index-button__meteor__item" :style="{transform: `rotateX(${-60 + (30 * index)}deg) rotateZ(${-60 + (30 * index)}deg)`}">
+          <view class="nav-index-button__meteor__item--pic"></view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'nav-index-button',
+    props: {
+      // 距离底部的距离
+      bottom: {
+        type: [Number, String],
+        default: 300
+      },
+      // 距离右边的距离
+      right: {
+        type: [Number, String],
+        default: 75
+      },
+      // 首页地址
+      indexPath: {
+        type: String,
+        default: '/pages/index'
+      }
+    },
+    methods: {
+      // 跳转回首页
+      navIndex() {
+        // 通过判断当前页面的页面栈信息,是否有上一页进行返回,如果没有则跳转到首页
+        const pages = getCurrentPages()
+        if (pages && pages.length > 0) {
+          const indexPath = this.indexPath || '/pages/index'
+          const firstPage = pages[0]
+          if (pages.length == 1 && (!firstPage.route || firstPage.route != indexPath.substring(1, indexPath.length))) {
+            uni.reLaunch({
+              url: indexPath
+            })
+          } else {
+            uni.navigateBack({
+              delta: 1
+            })
+          }
+        } else {
+          uni.reLaunch({
+            url: indexPath
+          })
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .nav-index-button {
+    position: fixed;
+    animation: suspension 3s ease-in-out infinite;
+    z-index: 999999;
+    
+    &__content {
+      position: absolute;
+      width: 100rpx;
+      height: 100rpx;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      
+      &--icon {
+        width: 100rpx;
+        height: 100rpx;
+        font-size: 60rpx;
+        border-radius: 50%;
+        margin-bottom: 18rpx;
+        position: relative;
+        z-index: 1;
+        transform: scale(0.85);
+        
+        &::after {
+          content: " ";
+          position: absolute;
+          z-index: -1;
+          width: 100%;
+          height: 100%;
+          left: 0;
+          bottom: 0;
+          border-radius: inherit;
+          opacity: 1;
+          transform: scale(1, 1);
+          background-size: 100% 100%;
+          background-image: url(https://resource.tuniaokj.com/images/cool_bg_image/icon_bg6.png);
+        }
+      }
+    }
+    
+    &__meteor {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      width: 100rpx;
+      height: 100rpx;
+      transform-style: preserve-3d;
+      transform: translate(-50%, -50%) rotateY(75deg) rotateZ(10deg);
+      
+      &__wrapper {
+        width: 100rpx;
+        height: 100rpx;
+        transform-style: preserve-3d;
+        animation: spin 20s linear infinite;
+      }
+      
+      &__item {
+        position: absolute;
+        width: 100rpx;
+        height: 100rpx;
+        border-radius: 1000rpx;
+        left: 0;
+        top: 0;
+        
+        &--pic {
+          display: block;
+          width: 100%;
+          height: 100%;
+          background: url(https://resource.tuniaokj.com/images/cool_bg_image/arc3.png) no-repeat center center;
+          background-size: 100% 100%;
+          animation: arc 4s linear infinite;
+        }
+      }
+    }
+  }
+  
+  
+  @keyframes suspension {
+    0%, 100% {
+      transform: translateY(0);
+    }
+    50% {
+      transform: translateY(-0.8rem);
+    }
+  }
+  
+  @keyframes spin {
+    0% {
+      transform: rotateX(0deg);
+    }
+  
+    100% {
+      transform: rotateX(-360deg);
+    }
+  }
+  
+  @keyframes arc {
+    to {
+      transform: rotate(360deg);
+    }
+  }
+</style>

+ 52 - 0
libs/mixin/dynamic_demo_mixin.js

@@ -0,0 +1,52 @@
+/**
+ * 动态参数演示mixin
+ */
+module.exports = {
+  data() {
+    return {
+      // 效果显示框top的值
+      contentContainerTop: '0px',
+      contentContainerIsTop: false,
+      
+      // 参数显示框top的值
+      sectionContainerTop: '0px'
+    }
+  },
+  onReady() {
+    this.updateSectionContainerTop()
+  },
+  methods: {
+    // 处理演示效果框的位置
+    async _handleContentConatinerPosition() {
+      // 获取效果演示框的节点信息
+      const contentContainer = await this._tGetRect('#content_container')
+      // 获取参数框的节点信息
+      this._tGetRect('#section_container').then((res) => {
+        // 判断参数框是否在移动,如果是则更新效果框的位置
+        // 如果效果框的顶部已经触控到顶部导航栏就停止跟随
+        if (res.top - contentContainer.bottom != 15) {
+          const newTop = res.top - (contentContainer.height + uni.upx2px(20))
+          const minTop = this.vuex_custom_bar_height + 1
+          if (newTop < minTop) {
+            this.contentContainerTop = minTop + 'px'
+            this.contentContainerIsTop = true
+          } else {
+            this.contentContainerTop = newTop + 'px'
+            this.contentContainerIsTop = false
+          }
+        }
+      })
+    },
+    // 更新状态切换栏位置信息
+    updateSectionContainerTop() {
+      this._tGetRect('#content_container').then((res) => {
+        this.contentContainerTop = (this.vuex_custom_bar_height + 148) + 'px'
+        this.sectionContainerTop = (res.height + 20) + 'px'
+      })
+    }
+  },
+  // 监听页面滚动
+  onPageScroll() {
+    this._handleContentConatinerPosition()
+  }
+}

+ 60 - 0
libs/mixin/template_page_mixin.js

@@ -0,0 +1,60 @@
+/**
+ * 演示页面mixin
+ */
+module.exports = {
+  data() {
+    return {
+      
+    }
+  },
+  onLoad() {
+    // 更新顶部导航栏信息
+    this.updateCustomBarInfo()
+  },
+  methods: {
+    // 点击左上角返回按钮时触发事件
+    goBack() {
+      // 通过判断当前页面的页面栈信息,是否有上一页进行返回,如果没有则跳转到首页
+      const pages = getCurrentPages()
+      if (pages && pages.length > 0) {
+        const firstPage = pages[0]
+        if (pages.length == 1 && (!firstPage.route || firstPage.route != 'pages/index')) {
+          uni.reLaunch({
+            url: '/pages/index'
+          })
+        } else {
+          uni.navigateBack({
+            delta: 1
+          })
+        }
+      } else {
+        uni.reLaunch({
+          url: '/pages/index'
+        })
+      }
+    },
+    // 更新顶部导航栏信息
+    async updateCustomBarInfo() {
+      // 获取vuex中的自定义顶栏的高度
+      let customBarHeight = this.vuex_custom_bar_height
+      let statusBarHeight = this.vuex_status_bar_height
+      // 如果获取失败则重新获取
+      if (!customBarHeight) {
+        try {
+          const navBarInfo = await this.$t.updateCustomBar()
+          customBarHeight = navBarInfo.customBarHeight
+          statusBarHeight = navBarInfo.statusBarHeight
+        } catch(e) {
+          setTimeout(() => {
+            this.updateCustomBarInfo()
+          }, 10)
+          return
+        }
+      }
+      
+      // 更新vuex中的导航栏信息
+      this.$t.vuex('vuex_status_bar_height', statusBarHeight)
+      this.$t.vuex('vuex_custom_bar_height', customBarHeight)
+    }
+  }
+}

+ 330 - 0
libs/navigation/navigation.js

@@ -0,0 +1,330 @@
+/**
+ * 页面展示列表数据
+ */
+export default {
+  data: [{
+      title: '速立保首页',
+      backgroundColor: 'tn-cool-bg-color-1',
+      list: [{
+          icon: 'code',
+          title: '关于我们',
+          url: '/homePages/about',
+          author: '速立保'
+        },{
+          icon: 'code',
+          title: '全局搜索',
+          url: '/homePages/search',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '今日热榜',
+          url: '/homePages/hot',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '前端业务',
+          url: '/homePages/profession',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '加载效果',
+          url: '/homePages/loading',
+          author: '速立保'
+        }
+      ]
+    },
+    {
+      title: '酷炫圈子',
+      backgroundColor: 'tn-cool-bg-color-1',
+      list: [{
+          icon: 'code',
+          title: '博主_Me',
+          url: '/circlePages/blogger',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '博主_Ta',
+          url: '/circlePages/blogger_other',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '编辑发布',
+          url: '/circlePages/edit',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '广告页',
+          url: '/circlePages/advertise',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '资讯详情',
+          url: '/circlePages/news',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '名片王者',
+          url: '/circlePages/king',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '智能名片',
+          url: '/circlePages/business',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '精选圈子',
+          url: '/circlePages/group',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '积分排行',
+          url: '/circlePages/ranking',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '圈子详情',
+          url: '/circlePages/details',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '预约接龙',
+          url: '/circlePages/reserve',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '活动创建',
+          url: '/circlePages/create',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '打造圈子',
+          url: '/circlePages/build',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '一起群聊',
+          url: '/circlePages/chat',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '对话聊天',
+          url: '/circlePages/chatting',
+          author: '速立保'
+        }
+      ]
+    },
+    {
+      title: '活动广场',
+      backgroundColor: 'tn-cool-bg-color-1',
+      list: [{
+          icon: 'code',
+          title: '地图打卡',
+          url: '/activityPages/map',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '快速答题',
+          url: '/activityPages/topic',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '课程学习',
+          url: '/activityPages/study',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '开源项目',
+          url: '/activityPages/project',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '活动星球',
+          url: '/activityPages/planet',
+          author: '速立保'
+        }
+      ]
+    },
+    {
+      title: '商品优选',
+      backgroundColor: 'tn-cool-bg-color-1',
+      list: [{
+          icon: 'code',
+          title: '优质商家',
+          url: '/preferredPages/shop',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '商品详情',
+          url: '/preferredPages/product',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '历史订单',
+          url: '/preferredPages/order',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '商品分类',
+          url: '/preferredPages/classify',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '商家相册',
+          url: '/preferredPages/photo',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '品牌官网',
+          url: '/preferredPages/website',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '积分兑换',
+          url: '/preferredPages/redeem',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '免单活动',
+          url: '/preferredPages/award',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '免单获取',
+          url: '/preferredPages/awardget',
+          author: '速立保'
+        }
+      ]
+    },
+    {
+      title: '关于我的',
+      backgroundColor: 'tn-cool-bg-color-1',
+      list: [{
+          icon: 'code',
+          title: '使用协议',
+          url: '/minePages/protocol',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '授权登录',
+          url: '/minePages/login',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '消息通知',
+          url: '/minePages/message',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '全局设置',
+          url: '/minePages/set',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '立即体验',
+          url: '/minePages/start',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '感谢名单',
+          url: '/minePages/thanks',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '版本更新',
+          url: '/minePages/version',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '帮助中心',
+          url: '/minePages/help',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '头像上传',
+          url: '/minePages/avatar',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '积分明细',
+          url: '/minePages/integral',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '积分签到',
+          url: '/minePages/signed',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '好物收藏',
+          url: '/minePages/collect',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '账号安全',
+          url: '/minePages/safety',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '赞赏支持',
+          url: '/minePages/reward',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '缺省页',
+          url: '/minePages/default',
+          author: '速立保'
+        },
+        {
+          icon: 'code',
+          title: '富文本',
+          url: '/minePages/content',
+          author: '速立保'
+        }
+      ]
+    }
+  ]
+}

+ 28 - 0
main.js

@@ -0,0 +1,28 @@
+import App from './App'
+import store from './store'
+import storage from './utils/storage.js'
+import Vue from 'vue'
+Vue.config.productionTip = false
+Vue.prototype.$storage = storage;
+
+App.mpType = 'app'
+
+// 引入全局TuniaoUI
+import TuniaoUI from 'tuniao-ui'
+Vue.use(TuniaoUI)
+
+// 引入TuniaoUI提供的vuex简写方法
+let vuexStore = require('@/store/$t.mixin.js')
+Vue.mixin(vuexStore)
+ 
+
+// 引入TuniaoUI对小程序分享的mixin封装
+let mpShare = require('tuniao-ui/libs/mixin/mpShare.js')
+Vue.mixin(mpShare)
+
+const app = new Vue({
+  store,
+  ...App
+})
+
+app.$mount()

+ 66 - 0
manifest.json

@@ -0,0 +1,66 @@
+{
+    "name" : "速立保",
+    "appid" : "__UNI__1EF8770",
+    "description" : "生物制药供需平台",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        "modules" : {},
+        "distribute" : {
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            "ios" : {},
+            "sdkConfigs" : {}
+        }
+    },
+    "quickapp" : {},
+    "mp-weixin" : {
+        "appid" : "wxbec0825602c34690",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true,
+        "requiredPrivateInfos" : [ "chooseAddress" ],
+		 "libVersion" : "latest"
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "2"
+}

+ 419 - 0
minePages/set.vue

@@ -0,0 +1,419 @@
+<template>
+  <view class="template-set">
+    <!-- 顶部自定义导航 -->
+    <tn-nav-bar fixed alpha customBack>
+      <view slot="back" class='tn-custom-nav-bar__back'
+        @click="goBack">
+        <text class='icon tn-icon-left'></text>
+      </view>
+    </tn-nav-bar>
+
+    <view class="tn-margin-top" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+      
+      <view style="text-align: center;">		  
+		  <uni-file-picker 
+		  	v-model="value" mode="list" :auto-upload="false" @select="select" @success="success">
+		  	<image v-if="!userInfo.profilePhotoUrl" src="../static/me2.png" style="width: 100px;height: 100px;"></image>
+		  	<image v-if="userInfo.profilePhotoUrl" :src="userInfo.profilePhotoUrl" style="width: 100px;height: 100px;border-radius: 50%;"></image>
+		  </uni-file-picker>
+	  </view>
+      <!-- <view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding" @click="tn('/minePages/avatar')">
+        <view class="justify-content-item">
+          <view class="tn-text-bold tn-text-lg">
+            用户头像
+          </view>
+          <view class="tn-color-gray tn-padding-top-xs">
+            有趣的头像,百里挑一
+          </view>
+        </view>
+        <view class="justify-content-item tn-text-lg tn-color-grey">
+          <view class="logo-pic tn-shadow">
+            <view class="logo-image">
+              <view class="tn-shadow-blur" style="background-image:url('https://cdn.nlark.com/yuque/0/2022/jpeg/280373/1664005699053-assets/web-upload/8645ea3a-e0a9-4422-8364-cc5ede305c9f.jpeg');width: 80rpx;height: 80rpx;background-size: cover;">
+              </view>
+            </view>
+          </view>
+        </view>
+      </view> -->
+	  
+	  <view class="" style="padding: 16px;
+    margin-top: 16px;">
+	  		<uni-forms :modelValue="formData" label-width="100px">
+	  			<uni-forms-item label="用户昵称" name="name">
+	  				<uni-easyinput type="text" v-model="userInfo.contactNickName" placeholder="请输入昵称" />
+	  			</uni-forms-item>
+	  			<uni-forms-item label="真实姓名" name="realName">
+	  				 <uni-easyinput type="text" :clearable="false" v-model="userInfo.userRealName" placeholder="请输入姓名" />
+	  			</uni-forms-item>
+				<uni-forms-item label="联系方式" name="contactMethod">
+					 <uni-easyinput type="text" v-model="userInfo.contactMethod" placeholder="请输入邮箱/微信" />
+				</uni-forms-item>
+				<uni-forms-item label="手机号" name="phone">
+					 <uni-easyinput type="text" disabled  v-model="userInfo.userName" placeholder=" " />
+				</uni-forms-item>
+	  			 
+	  		</uni-forms>
+	  		<button shape="round" style="background-color: #1d60b1;border-radius: 30px;" type="primary" @click="saveForm">保存修改</button>
+	  	</view>
+      
+      <!-- <view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding" @click="showModal1">
+        <view class="justify-content-item">
+          <view class="tn-text-bold tn-text-lg">
+            用户名
+          </view>
+          <view class="tn-color-gray tn-padding-top-xs">
+            --
+          </view>
+        </view>
+        <view class="justify-content-item tn-text-lg tn-color-grey">
+          
+		  <view class="tn-icon-right tn-padding-top"></view>
+        </view>
+      </view>
+      
+      <view class="tn-flex tn-flex-row-between tn-strip-bottom tn-padding" @click="showModal2">
+        <view class="justify-content-item">
+          <view class="tn-text-bold tn-text-lg">
+            手机号
+          </view>
+          <view class="tn-color-gray tn-padding-top-xs">
+            13911111193
+          </view>
+        </view>
+        <view class="justify-content-item tn-text-lg tn-color-grey">
+          <view class="tn-icon-right tn-padding-top"></view>
+        </view>
+      </view>
+      
+      <view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding" @click="showModal3">
+        <view class="justify-content-item">
+          <view class="tn-text-bold tn-text-lg">
+            联系方式
+          </view>
+          <view class="tn-color-gray tn-padding-top-xs">
+            未填写
+          </view>
+        </view>
+        <view class="justify-content-item tn-text-lg tn-color-grey">
+          <view class="tn-icon-right tn-padding-top"></view>
+        </view>
+      </view>
+       
+       
+      <picker @change="bindPickerChange1" :value="index1" :range="array1">
+        <view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding">
+          <view class="justify-content-item">
+            <view class="tn-text-bold tn-text-lg">
+              所属公司
+            </view>
+            <view class="tn-color-gray tn-padding-top-xs">
+              {{array1[index1]}}
+            </view>
+          </view>
+          <view class="justify-content-item tn-text-lg tn-color-grey">
+            <view class="tn-icon-right tn-padding-top"></view>
+          </view>
+        </view>
+      </picker> -->
+      <tn-modal v-model="show1" :custom="true" :showCloseBtn="true">
+        <view class="custom-modal-content">
+          <view class="">
+            <view class="tn-text-lg tn-text-bold tn-color-purplered tn-text-center tn-padding">修改昵称</view>
+            <view class="tn-bg-gray--light" style="border-radius: 10rpx;padding: 20rpx 30rpx;margin: 50rpx 0 60rpx 0;">
+            	<input placeholder="==" name="input" placeholder-style="color:#AAAAAA" maxlength="20"></input>
+            </view>
+          </view>
+          <view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+            <tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold>
+              <text class="tn-color-white">保 存</text>
+            </tn-button>
+          </view>
+        </view>
+      </tn-modal>
+      
+      <tn-modal v-model="show2" :custom="true" :showCloseBtn="true">
+        <view class="custom-modal-content">
+          <view class="">
+            <view class="tn-text-lg tn-text-bold tn-color-purplered tn-text-center tn-padding">变更手机号码</view>
+            <view class="tn-bg-gray--light tn-color-gray" style="border-radius: 10rpx;padding: 20rpx 30rpx;margin: 50rpx 0 60rpx 0;">
+              13911111193
+            </view>
+          </view>
+          <view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+            <tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold>
+              <text class="tn-color-white">获取手机号</text>
+            </tn-button>
+            <!-- <tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold open-type="getPhoneNumber">
+              <text class="tn-color-white">获取手机号</text>
+            </tn-button> -->
+            <view class="tn-padding-top-sm">因为获取手机号api收费了,所以这里注释掉,需要的自行展示出来即可</view>
+          </view>
+        </view>
+      </tn-modal>
+      
+      <tn-modal v-model="show3" :custom="true" :showCloseBtn="true">
+        <view class="custom-modal-content">
+          <view class="">
+            <view class="tn-text-lg tn-text-bold tn-color-purplered tn-text-center tn-padding">请输入真实姓名</view>
+            <view class="tn-bg-gray--light" style="border-radius: 10rpx;padding: 20rpx 30rpx;margin: 50rpx 0 60rpx 0;">
+              <input placeholder="请填写姓名" name="input" placeholder-style="color:#AAAAAA" maxlength="20"></input>
+            </view>
+          </view>
+          <view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+            <tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold @click="saveForm()">
+              <text class="tn-color-white">保 存</text>
+            </tn-button>
+          </view>
+        </view>
+      </tn-modal>
+      
+      
+      
+      
+    </view>
+    
+    
+  </view>
+</template>
+
+<script>
+  import template_page_mixin from '@/libs/mixin/template_page_mixin.js'
+  import request from '../utils/request';
+  export default {
+    name: 'TemplateSet',
+    mixins: [template_page_mixin],
+    data(){
+      return {
+        show1: false,
+        show2: false,
+        show3: false,
+        index: 0,
+        array: ['女', '男', '保密'],
+        date: '2000-01-29',
+        index1: 1,
+        array1: ['计算机/电子', '广告/媒体', '会计/金融', '政府/非盈利组织/其他'],
+		userInfo:{
+			profilePhotoUrl:uni.getStorageSync('userInfo')?JSON.parse(uni.getStorageSync('userInfo')).profilePhotoUrl:'',
+			contactNickName:uni.getStorageSync('userInfo')?JSON.parse(uni.getStorageSync('userInfo')).contactNickName:'',
+			contactMethod:uni.getStorageSync('userInfo')?JSON.parse(uni.getStorageSync('userInfo')).contactMethod:'',
+			userName:uni.getStorageSync('userInfo')?JSON.parse(uni.getStorageSync('userInfo')).userName:''
+		},
+      }
+    },
+    computed: {
+        startDate() {
+            return this.getDate('start');
+        },
+        endDate() {
+            return this.getDate('end');
+        }
+    },
+	onLoad(){
+		this.getInfo();
+	},
+    methods: {
+      // 跳转
+      tn(e) {
+      	uni.navigateTo({
+      		url: e,
+      	});
+      },
+      
+      // 弹出模态框
+      showModal1(event) {
+        this.openModal1()
+      },
+      // 打开模态框
+      openModal1() {
+        this.show1 = true
+      },
+      
+      // 弹出模态框
+      showModal2(event) {
+        this.openModal2()
+      },
+      // 打开模态框
+      openModal2() {
+        this.show2 = true
+      },
+      
+      // 弹出模态框
+      showModal3(event) {
+        this.openModal3()
+      },
+      // 打开模态框
+      openModal3() {
+        this.show3 = true
+      },
+      
+      bindPickerChange: function(e) {
+        this.index = e.detail.value
+      },
+      
+      bindPickerChange1: function(e) {
+        this.index1 = e.detail.value
+      },
+      
+      bindDateChange: function(e) {
+          this.date = e.detail.value
+      },
+      
+      getDate(type) {
+          const date = new Date();
+          let year = date.getFullYear();
+          let month = date.getMonth() + 1;
+          let day = date.getDate();
+      
+          if (type === 'start') {
+              year = year - 60;
+          } else if (type === 'end') {
+              year = year + 2;
+          }
+          month = month > 9 ? month : '0' + month;
+          day = day > 9 ? day : '0' + day;
+          return `${year}-${month}-${day}`;
+      },
+	  getInfo(){
+		request.post('/slbWxma/getPersonlInfo',{}).then(res=>{
+			if(res&&res.success){
+				this.userInfo = res.resultMap.userInfo||{}
+			}
+		})  
+	  },
+	  saveForm(){
+		  const that = this;
+		  let params= this.userInfo;
+		  request.post('/slbWxma/changePersonlInfo',{userInfo:JSON.stringify(params)},{
+			  headers: {
+			  	'Content-Type': 'application/json', // 默认值
+			  },
+		  }).then(res=>{
+			  if(res.success){
+				  uni.showToast({
+				  	title:'修改成功'
+				  })
+				  uni.navigateBack();
+			  }
+		  })
+	  },
+	  // 获取上传状态
+	  select(e) {
+	  	console.log('选择文件:', e)
+	  	let tempFiles = e.tempFiles;
+	  	for (let i in tempFiles) {
+	  		this.upfile(tempFiles[i])
+	  	}
+	  },
+	  upfile(file) {
+	  	let that = this;
+	  	console.warn(file);
+	  	uni.uploadFile({
+	  		url: 'http://slb-m.dev.ml1993.com/oss/upload/userFeedback', //仅为示例,非真实的接口地址
+	  		filePath: file.url,
+	  		name: 'file',
+	  		success: (uploadFileRes) => {
+	  			console.warn(JSON.parse(uploadFileRes.data));
+	  			let resultMap = JSON.parse(uploadFileRes.data).resultMap;
+				that.userInfo.profilePhotoUrl= resultMap.uploadUrl;
+				 
+				 
+	  			
+	  		}
+	  	});
+	  },
+	  // 上传成功
+	  success(e) {
+	  	console.log('上传成功')
+	  },
+        
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+    /* 胶囊*/
+    .tn-custom-nav-bar__back {
+      width: 60%;
+      height: 100%;
+      position: relative;
+      display: flex;
+      justify-content: space-evenly;
+      align-items: center;
+      box-sizing: border-box;
+      // background-color: rgba(0, 0, 0, 0.15);
+      border-radius: 1000rpx;
+      border: 1rpx solid rgba(255, 255, 255, 0.5);
+      // color: #FFFFFF;
+      font-size: 18px;
+      
+      .icon {
+        display: block;
+        flex: 1;
+        margin: auto;
+        text-align: center;
+      }
+      
+    }
+
+
+    /* 间隔线 start*/
+    .tn-strip-bottom-min {
+      width: 100%;
+      border-bottom: 1rpx solid #F8F9FB;
+    }
+    
+    .tn-strip-bottom {
+     width: 100%;
+     border-bottom: 20rpx solid rgba(241, 241, 241, 0.8);
+    }
+     /* 间隔线 end*/
+
+
+  /* 用户头像 start */
+  .logo-image {
+    width: 80rpx;
+    height: 80rpx;
+    position: relative;
+  }
+
+  .logo-pic {
+    background-size: cover;
+    background-repeat: no-repeat;
+    // background-attachment:fixed;
+    background-position: top;
+    border: 2rpx solid rgba(255,255,255,0.05);
+    box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.15);
+    border-radius: 50%;
+    overflow: hidden;
+    // background-color: #FFFFFF;
+  }
+
+
+  /* 底部悬浮按钮 start*/
+  .tn-tabbar-height {
+  	min-height: 100rpx;
+  	height: calc(120rpx + env(safe-area-inset-bottom) / 2);
+  }
+  .tn-footerfixed {
+    position: fixed;
+    width: 100%;
+    bottom: calc(30rpx + env(safe-area-inset-bottom));
+    z-index: 1024;
+    box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0);
+    
+  }
+  /deep/ .uni-file-picker__container {
+	  justify-content: center;
+  }
+  /deep/ .file-picker__box-content {
+    border: none !important;
+  }
+  /deep/ .uni-file-picker__lists{
+	display: none;
+  }
+  /deep/ .uni-forms-item__label {
+  	font-size: 16px;
+  }
+  /* 底部悬浮按钮 end*/
+  
+</style>

+ 101 - 0
node_modules/js-md5/CHANGELOG.md

@@ -0,0 +1,101 @@
+# Change Log
+
+## v0.7.3 / 2017-12-18
+### Fixed
+- incorrect result when first bit is 1 of bytes. #18
+
+## v0.7.2 / 2017-10-31
+### Improved
+- performance of hBytes increment.
+
+## v0.7.1 / 2017-10-29
+### Fixed
+- incorrect result when file size >= 4G.
+
+## v0.7.0 / 2017-10-29
+### Fixed
+- incorrect result when file size >= 512M.
+
+## v0.6.1 / 2017-10-07
+### Fixed
+- ArrayBuffer.isView issue in IE10.
+
+### Improved
+- performance of input check.
+
+## v0.6.0 / 2017-07-28
+### Added
+- support base64 string output.
+
+## v0.5.0 / 2017-07-14
+### Added
+- support for web worker. #11
+
+### Changed
+- throw error if input type is incorrect.
+- prevent webpack to require dependencies.
+
+## v0.4.2 / 2017-01-18
+### Fixed
+- `root` is undefined in some special environment. #7
+
+## v0.4.1 / 2016-03-31
+### Removed
+- length detection in node.js.
+### Deprecated
+- `buffer` and replace by `arrayBuffer`.
+
+## v0.4.0 / 2015-12-28
+### Added
+- support for update hash.
+- support for bytes array output.
+- support for ArrayBuffer output.
+- support for AMD.
+
+## v0.3.0 / 2015-03-07
+### Added
+- support byte Array, Uint8Array and ArrayBuffer input.
+
+## v0.2.2 / 2015-02-01
+### Fixed
+- bug when special length.
+### Improve
+- performance for node.js.
+
+## v0.2.1 / 2015-01-13
+### Improve
+- performance.
+
+## v0.2.0 / 2015-01-12
+### Removed
+- ascii parameter.
+### Improve
+- performance.
+
+## v0.1.4 / 2015-01-11
+### Improve
+- performance.
+### Added
+- test cases.
+
+## v0.1.3 / 2015-01-05
+### Added
+- bower package.
+- travis.
+- coveralls.
+### Improved
+- performance.
+### Fixed
+- JSHint warnings.
+
+## v0.1.2 / 2014-07-27
+### Fixed
+- accents bug
+
+## v0.1.1 / 2014-01-05
+### Changed
+- license
+
+## v0.1.0 / 2014-01-04
+### Added
+- initial release

+ 20 - 0
node_modules/js-md5/LICENSE.txt

@@ -0,0 +1,20 @@
+Copyright 2014-2017 Chen, Yi-Cyuan
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 76 - 0
node_modules/js-md5/README.md

@@ -0,0 +1,76 @@
+# js-md5
+[![Build Status](https://travis-ci.org/emn178/js-md5.svg?branch=master)](https://travis-ci.org/emn178/js-md5)
+[![Coverage Status](https://coveralls.io/repos/emn178/js-md5/badge.svg?branch=master)](https://coveralls.io/r/emn178/js-md5?branch=master)  
+[![NPM](https://nodei.co/npm/js-md5.png?stars&downloads)](https://nodei.co/npm/js-md5/)
+
+A simple MD5 hash function for JavaScript supports UTF-8 encoding.
+
+## Demo
+[MD5 Online](http://emn178.github.io/online-tools/md5.html)  
+[MD5 File Checksum Online](http://emn178.github.io/online-tools/md5_checksum.html)
+
+## Download
+[Compress](https://raw.github.com/emn178/js-md5/master/build/md5.min.js)  
+[Uncompress](https://raw.github.com/emn178/js-md5/master/src/md5.js)
+
+## Installation
+You can also install js-md5 by using Bower.
+
+    bower install md5
+
+For node.js, you can use this command to install:
+
+    npm install js-md5
+
+## Notice
+`buffer` method is deprecated. This maybe confuse with Buffer in node.js. Please use `arrayBuffer` instead.
+
+## Usage
+You could use like this:
+```JavaScript
+md5('Message to hash');
+var hash = md5.create();
+hash.update('Message to hash');
+hash.hex();
+```
+If you use node.js, you should require the module first:
+```JavaScript
+md5 = require('js-md5');
+```
+It supports AMD:
+```JavaScript
+require(['your/path/md5.js'], function(md5) {
+// ...
+});
+```
+[See document](https://emn178.github.com/js-md5/doc/)
+
+## Example
+```JavaScript
+md5(''); // d41d8cd98f00b204e9800998ecf8427e
+md5('The quick brown fox jumps over the lazy dog'); // 9e107d9d372bb6826bd81d3542a419d6
+md5('The quick brown fox jumps over the lazy dog.'); // e4d909c290d0fb1ca068ffaddf22cbd0
+
+// It also supports UTF-8 encoding
+md5('中文'); // a7bac2239fcdcb3a067903d8077c4a07
+
+// It also supports byte `Array`, `Uint8Array`, `ArrayBuffer`
+md5([]); // d41d8cd98f00b204e9800998ecf8427e
+md5(new Uint8Array([])); // d41d8cd98f00b204e9800998ecf8427e
+
+// Different output
+md5(''); // d41d8cd98f00b204e9800998ecf8427e
+md5.hex(''); // d41d8cd98f00b204e9800998ecf8427e
+md5.array(''); // [212, 29, 140, 217, 143, 0, 178, 4, 233, 128, 9, 152, 236, 248, 66, 126]
+md5.digest(''); // [212, 29, 140, 217, 143, 0, 178, 4, 233, 128, 9, 152, 236, 248, 66, 126]
+md5.arrayBuffer(''); // ArrayBuffer
+md5.buffer(''); // ArrayBuffer, deprecated, This maybe confuse with Buffer in node.js. Please use arrayBuffer instead.
+md5.base64(''); // 1B2M2Y8AsgTpgAmY7PhCfg==
+```
+
+## License
+The project is released under the [MIT license](http://www.opensource.org/licenses/MIT).
+
+## Contact
+The project's website is located at https://github.com/emn178/js-md5  
+Author: Chen, Yi-Cyuan (emn178@gmail.com)

ファイルの差分が大きいため隠しています
+ 10 - 0
node_modules/js-md5/build/md5.min.js


+ 73 - 0
node_modules/js-md5/package.json

@@ -0,0 +1,73 @@
+{
+  "_from": "js-md5@^0.7.3",
+  "_id": "js-md5@0.7.3",
+  "_inBundle": false,
+  "_integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==",
+  "_location": "/js-md5",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "range",
+    "registry": true,
+    "raw": "js-md5@^0.7.3",
+    "name": "js-md5",
+    "escapedName": "js-md5",
+    "rawSpec": "^0.7.3",
+    "saveSpec": null,
+    "fetchSpec": "^0.7.3"
+  },
+  "_requiredBy": [
+    "/"
+  ],
+  "_resolved": "https://registry.npmmirror.com/js-md5/-/js-md5-0.7.3.tgz",
+  "_shasum": "b4f2fbb0b327455f598d6727e38ec272cd09c3f2",
+  "_spec": "js-md5@^0.7.3",
+  "_where": "E:\\exhibition\\lixiang-mp",
+  "author": {
+    "name": "Chen, Yi-Cyuan",
+    "email": "emn178@gmail.com"
+  },
+  "bugs": {
+    "url": "https://github.com/emn178/js-md5/issues"
+  },
+  "bundleDependencies": false,
+  "deprecated": false,
+  "description": "A simple MD5 hash function for JavaScript supports UTF-8 encoding.",
+  "devDependencies": {
+    "expect.js": "~0.3.1",
+    "jsdoc": "^3.4.0",
+    "mocha": "~2.3.4",
+    "nyc": "^11.3.0",
+    "requirejs": "^2.1.22",
+    "uglify-js": "^3.1.9",
+    "webworker-threads": "^0.7.11"
+  },
+  "homepage": "https://github.com/emn178/js-md5",
+  "keywords": [
+    "md5",
+    "hash",
+    "encryption",
+    "cryptography",
+    "HMAC"
+  ],
+  "license": "MIT",
+  "main": "src/md5.js",
+  "name": "js-md5",
+  "nyc": {
+    "exclude": [
+      "tests"
+    ]
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/emn178/js-md5.git"
+  },
+  "scripts": {
+    "build": "npm run-script compress;npm run-script doc",
+    "compress": "uglifyjs src/md5.js -c -m eval --comments --output build/md5.min.js",
+    "coveralls": "nyc report --reporter=text-lcov | coveralls",
+    "doc": "rm -rf doc;jsdoc src README.md -d doc",
+    "report": "nyc --reporter=html --reporter=text mocha tests/node-test.js",
+    "test": "nyc mocha tests/node-test.js"
+  },
+  "version": "0.7.3"
+}

+ 683 - 0
node_modules/js-md5/src/md5.js

@@ -0,0 +1,683 @@
+/**
+ * [js-md5]{@link https://github.com/emn178/js-md5}
+ *
+ * @namespace md5
+ * @version 0.7.3
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2017
+ * @license MIT
+ */
+(function () {
+  'use strict';
+
+  var ERROR = 'input is invalid type';
+  var WINDOW = typeof window === 'object';
+  var root = WINDOW ? window : {};
+  if (root.JS_MD5_NO_WINDOW) {
+    WINDOW = false;
+  }
+  var WEB_WORKER = !WINDOW && typeof self === 'object';
+  var NODE_JS = !root.JS_MD5_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
+  if (NODE_JS) {
+    root = global;
+  } else if (WEB_WORKER) {
+    root = self;
+  }
+  var COMMON_JS = !root.JS_MD5_NO_COMMON_JS && typeof module === 'object' && module.exports;
+  var AMD = typeof define === 'function' && define.amd;
+  var ARRAY_BUFFER = !root.JS_MD5_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined';
+  var HEX_CHARS = '0123456789abcdef'.split('');
+  var EXTRA = [128, 32768, 8388608, -2147483648];
+  var SHIFT = [0, 8, 16, 24];
+  var OUTPUT_TYPES = ['hex', 'array', 'digest', 'buffer', 'arrayBuffer', 'base64'];
+  var BASE64_ENCODE_CHAR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
+
+  var blocks = [], buffer8;
+  if (ARRAY_BUFFER) {
+    var buffer = new ArrayBuffer(68);
+    buffer8 = new Uint8Array(buffer);
+    blocks = new Uint32Array(buffer);
+  }
+
+  if (root.JS_MD5_NO_NODE_JS || !Array.isArray) {
+    Array.isArray = function (obj) {
+      return Object.prototype.toString.call(obj) === '[object Array]';
+    };
+  }
+
+  if (ARRAY_BUFFER && (root.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) {
+    ArrayBuffer.isView = function (obj) {
+      return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer;
+    };
+  }
+
+  /**
+   * @method hex
+   * @memberof md5
+   * @description Output hash as hex string
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {String} Hex string
+   * @example
+   * md5.hex('The quick brown fox jumps over the lazy dog');
+   * // equal to
+   * md5('The quick brown fox jumps over the lazy dog');
+   */
+  /**
+   * @method digest
+   * @memberof md5
+   * @description Output hash as bytes array
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {Array} Bytes array
+   * @example
+   * md5.digest('The quick brown fox jumps over the lazy dog');
+   */
+  /**
+   * @method array
+   * @memberof md5
+   * @description Output hash as bytes array
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {Array} Bytes array
+   * @example
+   * md5.array('The quick brown fox jumps over the lazy dog');
+   */
+  /**
+   * @method arrayBuffer
+   * @memberof md5
+   * @description Output hash as ArrayBuffer
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {ArrayBuffer} ArrayBuffer
+   * @example
+   * md5.arrayBuffer('The quick brown fox jumps over the lazy dog');
+   */
+  /**
+   * @method buffer
+   * @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead.
+   * @memberof md5
+   * @description Output hash as ArrayBuffer
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {ArrayBuffer} ArrayBuffer
+   * @example
+   * md5.buffer('The quick brown fox jumps over the lazy dog');
+   */
+  /**
+   * @method base64
+   * @memberof md5
+   * @description Output hash as base64 string
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {String} base64 string
+   * @example
+   * md5.base64('The quick brown fox jumps over the lazy dog');
+   */
+  var createOutputMethod = function (outputType) {
+    return function (message) {
+      return new Md5(true).update(message)[outputType]();
+    };
+  };
+
+  /**
+   * @method create
+   * @memberof md5
+   * @description Create Md5 object
+   * @returns {Md5} Md5 object.
+   * @example
+   * var hash = md5.create();
+   */
+  /**
+   * @method update
+   * @memberof md5
+   * @description Create and update Md5 object
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {Md5} Md5 object.
+   * @example
+   * var hash = md5.update('The quick brown fox jumps over the lazy dog');
+   * // equal to
+   * var hash = md5.create();
+   * hash.update('The quick brown fox jumps over the lazy dog');
+   */
+  var createMethod = function () {
+    var method = createOutputMethod('hex');
+    if (NODE_JS) {
+      method = nodeWrap(method);
+    }
+    method.create = function () {
+      return new Md5();
+    };
+    method.update = function (message) {
+      return method.create().update(message);
+    };
+    for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+      var type = OUTPUT_TYPES[i];
+      method[type] = createOutputMethod(type);
+    }
+    return method;
+  };
+
+  var nodeWrap = function (method) {
+    var crypto = eval("require('crypto')");
+    var Buffer = eval("require('buffer').Buffer");
+    var nodeMethod = function (message) {
+      if (typeof message === 'string') {
+        return crypto.createHash('md5').update(message, 'utf8').digest('hex');
+      } else {
+        if (message === null || message === undefined) {
+          throw ERROR;
+        } else if (message.constructor === ArrayBuffer) {
+          message = new Uint8Array(message);
+        }
+      }
+      if (Array.isArray(message) || ArrayBuffer.isView(message) ||
+        message.constructor === Buffer) {
+        return crypto.createHash('md5').update(new Buffer(message)).digest('hex');
+      } else {
+        return method(message);
+      }
+    };
+    return nodeMethod;
+  };
+
+  /**
+   * Md5 class
+   * @class Md5
+   * @description This is internal class.
+   * @see {@link md5.create}
+   */
+  function Md5(sharedMemory) {
+    if (sharedMemory) {
+      blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+      blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+      blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+      blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+      this.blocks = blocks;
+      this.buffer8 = buffer8;
+    } else {
+      if (ARRAY_BUFFER) {
+        var buffer = new ArrayBuffer(68);
+        this.buffer8 = new Uint8Array(buffer);
+        this.blocks = new Uint32Array(buffer);
+      } else {
+        this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+      }
+    }
+    this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0;
+    this.finalized = this.hashed = false;
+    this.first = true;
+  }
+
+  /**
+   * @method update
+   * @memberof Md5
+   * @instance
+   * @description Update hash
+   * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+   * @returns {Md5} Md5 object.
+   * @see {@link md5.update}
+   */
+  Md5.prototype.update = function (message) {
+    if (this.finalized) {
+      return;
+    }
+
+    var notString, type = typeof message;
+    if (type !== 'string') {
+      if (type === 'object') {
+        if (message === null) {
+          throw ERROR;
+        } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
+          message = new Uint8Array(message);
+        } else if (!Array.isArray(message)) {
+          if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
+            throw ERROR;
+          }
+        }
+      } else {
+        throw ERROR;
+      }
+      notString = true;
+    }
+    var code, index = 0, i, length = message.length, blocks = this.blocks;
+    var buffer8 = this.buffer8;
+
+    while (index < length) {
+      if (this.hashed) {
+        this.hashed = false;
+        blocks[0] = blocks[16];
+        blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+        blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+        blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+        blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+      }
+
+      if (notString) {
+        if (ARRAY_BUFFER) {
+          for (i = this.start; index < length && i < 64; ++index) {
+            buffer8[i++] = message[index];
+          }
+        } else {
+          for (i = this.start; index < length && i < 64; ++index) {
+            blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
+          }
+        }
+      } else {
+        if (ARRAY_BUFFER) {
+          for (i = this.start; index < length && i < 64; ++index) {
+            code = message.charCodeAt(index);
+            if (code < 0x80) {
+              buffer8[i++] = code;
+            } else if (code < 0x800) {
+              buffer8[i++] = 0xc0 | (code >> 6);
+              buffer8[i++] = 0x80 | (code & 0x3f);
+            } else if (code < 0xd800 || code >= 0xe000) {
+              buffer8[i++] = 0xe0 | (code >> 12);
+              buffer8[i++] = 0x80 | ((code >> 6) & 0x3f);
+              buffer8[i++] = 0x80 | (code & 0x3f);
+            } else {
+              code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
+              buffer8[i++] = 0xf0 | (code >> 18);
+              buffer8[i++] = 0x80 | ((code >> 12) & 0x3f);
+              buffer8[i++] = 0x80 | ((code >> 6) & 0x3f);
+              buffer8[i++] = 0x80 | (code & 0x3f);
+            }
+          }
+        } else {
+          for (i = this.start; index < length && i < 64; ++index) {
+            code = message.charCodeAt(index);
+            if (code < 0x80) {
+              blocks[i >> 2] |= code << SHIFT[i++ & 3];
+            } else if (code < 0x800) {
+              blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+            } else if (code < 0xd800 || code >= 0xe000) {
+              blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+            } else {
+              code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
+              blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+            }
+          }
+        }
+      }
+      this.lastByteIndex = i;
+      this.bytes += i - this.start;
+      if (i >= 64) {
+        this.start = i - 64;
+        this.hash();
+        this.hashed = true;
+      } else {
+        this.start = i;
+      }
+    }
+    if (this.bytes > 4294967295) {
+      this.hBytes += this.bytes / 4294967296 << 0;
+      this.bytes = this.bytes % 4294967296;
+    }
+    return this;
+  };
+
+  Md5.prototype.finalize = function () {
+    if (this.finalized) {
+      return;
+    }
+    this.finalized = true;
+    var blocks = this.blocks, i = this.lastByteIndex;
+    blocks[i >> 2] |= EXTRA[i & 3];
+    if (i >= 56) {
+      if (!this.hashed) {
+        this.hash();
+      }
+      blocks[0] = blocks[16];
+      blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+      blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+      blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+      blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+    }
+    blocks[14] = this.bytes << 3;
+    blocks[15] = this.hBytes << 3 | this.bytes >>> 29;
+    this.hash();
+  };
+
+  Md5.prototype.hash = function () {
+    var a, b, c, d, bc, da, blocks = this.blocks;
+
+    if (this.first) {
+      a = blocks[0] - 680876937;
+      a = (a << 7 | a >>> 25) - 271733879 << 0;
+      d = (-1732584194 ^ a & 2004318071) + blocks[1] - 117830708;
+      d = (d << 12 | d >>> 20) + a << 0;
+      c = (-271733879 ^ (d & (a ^ -271733879))) + blocks[2] - 1126478375;
+      c = (c << 17 | c >>> 15) + d << 0;
+      b = (a ^ (c & (d ^ a))) + blocks[3] - 1316259209;
+      b = (b << 22 | b >>> 10) + c << 0;
+    } else {
+      a = this.h0;
+      b = this.h1;
+      c = this.h2;
+      d = this.h3;
+      a += (d ^ (b & (c ^ d))) + blocks[0] - 680876936;
+      a = (a << 7 | a >>> 25) + b << 0;
+      d += (c ^ (a & (b ^ c))) + blocks[1] - 389564586;
+      d = (d << 12 | d >>> 20) + a << 0;
+      c += (b ^ (d & (a ^ b))) + blocks[2] + 606105819;
+      c = (c << 17 | c >>> 15) + d << 0;
+      b += (a ^ (c & (d ^ a))) + blocks[3] - 1044525330;
+      b = (b << 22 | b >>> 10) + c << 0;
+    }
+
+    a += (d ^ (b & (c ^ d))) + blocks[4] - 176418897;
+    a = (a << 7 | a >>> 25) + b << 0;
+    d += (c ^ (a & (b ^ c))) + blocks[5] + 1200080426;
+    d = (d << 12 | d >>> 20) + a << 0;
+    c += (b ^ (d & (a ^ b))) + blocks[6] - 1473231341;
+    c = (c << 17 | c >>> 15) + d << 0;
+    b += (a ^ (c & (d ^ a))) + blocks[7] - 45705983;
+    b = (b << 22 | b >>> 10) + c << 0;
+    a += (d ^ (b & (c ^ d))) + blocks[8] + 1770035416;
+    a = (a << 7 | a >>> 25) + b << 0;
+    d += (c ^ (a & (b ^ c))) + blocks[9] - 1958414417;
+    d = (d << 12 | d >>> 20) + a << 0;
+    c += (b ^ (d & (a ^ b))) + blocks[10] - 42063;
+    c = (c << 17 | c >>> 15) + d << 0;
+    b += (a ^ (c & (d ^ a))) + blocks[11] - 1990404162;
+    b = (b << 22 | b >>> 10) + c << 0;
+    a += (d ^ (b & (c ^ d))) + blocks[12] + 1804603682;
+    a = (a << 7 | a >>> 25) + b << 0;
+    d += (c ^ (a & (b ^ c))) + blocks[13] - 40341101;
+    d = (d << 12 | d >>> 20) + a << 0;
+    c += (b ^ (d & (a ^ b))) + blocks[14] - 1502002290;
+    c = (c << 17 | c >>> 15) + d << 0;
+    b += (a ^ (c & (d ^ a))) + blocks[15] + 1236535329;
+    b = (b << 22 | b >>> 10) + c << 0;
+    a += (c ^ (d & (b ^ c))) + blocks[1] - 165796510;
+    a = (a << 5 | a >>> 27) + b << 0;
+    d += (b ^ (c & (a ^ b))) + blocks[6] - 1069501632;
+    d = (d << 9 | d >>> 23) + a << 0;
+    c += (a ^ (b & (d ^ a))) + blocks[11] + 643717713;
+    c = (c << 14 | c >>> 18) + d << 0;
+    b += (d ^ (a & (c ^ d))) + blocks[0] - 373897302;
+    b = (b << 20 | b >>> 12) + c << 0;
+    a += (c ^ (d & (b ^ c))) + blocks[5] - 701558691;
+    a = (a << 5 | a >>> 27) + b << 0;
+    d += (b ^ (c & (a ^ b))) + blocks[10] + 38016083;
+    d = (d << 9 | d >>> 23) + a << 0;
+    c += (a ^ (b & (d ^ a))) + blocks[15] - 660478335;
+    c = (c << 14 | c >>> 18) + d << 0;
+    b += (d ^ (a & (c ^ d))) + blocks[4] - 405537848;
+    b = (b << 20 | b >>> 12) + c << 0;
+    a += (c ^ (d & (b ^ c))) + blocks[9] + 568446438;
+    a = (a << 5 | a >>> 27) + b << 0;
+    d += (b ^ (c & (a ^ b))) + blocks[14] - 1019803690;
+    d = (d << 9 | d >>> 23) + a << 0;
+    c += (a ^ (b & (d ^ a))) + blocks[3] - 187363961;
+    c = (c << 14 | c >>> 18) + d << 0;
+    b += (d ^ (a & (c ^ d))) + blocks[8] + 1163531501;
+    b = (b << 20 | b >>> 12) + c << 0;
+    a += (c ^ (d & (b ^ c))) + blocks[13] - 1444681467;
+    a = (a << 5 | a >>> 27) + b << 0;
+    d += (b ^ (c & (a ^ b))) + blocks[2] - 51403784;
+    d = (d << 9 | d >>> 23) + a << 0;
+    c += (a ^ (b & (d ^ a))) + blocks[7] + 1735328473;
+    c = (c << 14 | c >>> 18) + d << 0;
+    b += (d ^ (a & (c ^ d))) + blocks[12] - 1926607734;
+    b = (b << 20 | b >>> 12) + c << 0;
+    bc = b ^ c;
+    a += (bc ^ d) + blocks[5] - 378558;
+    a = (a << 4 | a >>> 28) + b << 0;
+    d += (bc ^ a) + blocks[8] - 2022574463;
+    d = (d << 11 | d >>> 21) + a << 0;
+    da = d ^ a;
+    c += (da ^ b) + blocks[11] + 1839030562;
+    c = (c << 16 | c >>> 16) + d << 0;
+    b += (da ^ c) + blocks[14] - 35309556;
+    b = (b << 23 | b >>> 9) + c << 0;
+    bc = b ^ c;
+    a += (bc ^ d) + blocks[1] - 1530992060;
+    a = (a << 4 | a >>> 28) + b << 0;
+    d += (bc ^ a) + blocks[4] + 1272893353;
+    d = (d << 11 | d >>> 21) + a << 0;
+    da = d ^ a;
+    c += (da ^ b) + blocks[7] - 155497632;
+    c = (c << 16 | c >>> 16) + d << 0;
+    b += (da ^ c) + blocks[10] - 1094730640;
+    b = (b << 23 | b >>> 9) + c << 0;
+    bc = b ^ c;
+    a += (bc ^ d) + blocks[13] + 681279174;
+    a = (a << 4 | a >>> 28) + b << 0;
+    d += (bc ^ a) + blocks[0] - 358537222;
+    d = (d << 11 | d >>> 21) + a << 0;
+    da = d ^ a;
+    c += (da ^ b) + blocks[3] - 722521979;
+    c = (c << 16 | c >>> 16) + d << 0;
+    b += (da ^ c) + blocks[6] + 76029189;
+    b = (b << 23 | b >>> 9) + c << 0;
+    bc = b ^ c;
+    a += (bc ^ d) + blocks[9] - 640364487;
+    a = (a << 4 | a >>> 28) + b << 0;
+    d += (bc ^ a) + blocks[12] - 421815835;
+    d = (d << 11 | d >>> 21) + a << 0;
+    da = d ^ a;
+    c += (da ^ b) + blocks[15] + 530742520;
+    c = (c << 16 | c >>> 16) + d << 0;
+    b += (da ^ c) + blocks[2] - 995338651;
+    b = (b << 23 | b >>> 9) + c << 0;
+    a += (c ^ (b | ~d)) + blocks[0] - 198630844;
+    a = (a << 6 | a >>> 26) + b << 0;
+    d += (b ^ (a | ~c)) + blocks[7] + 1126891415;
+    d = (d << 10 | d >>> 22) + a << 0;
+    c += (a ^ (d | ~b)) + blocks[14] - 1416354905;
+    c = (c << 15 | c >>> 17) + d << 0;
+    b += (d ^ (c | ~a)) + blocks[5] - 57434055;
+    b = (b << 21 | b >>> 11) + c << 0;
+    a += (c ^ (b | ~d)) + blocks[12] + 1700485571;
+    a = (a << 6 | a >>> 26) + b << 0;
+    d += (b ^ (a | ~c)) + blocks[3] - 1894986606;
+    d = (d << 10 | d >>> 22) + a << 0;
+    c += (a ^ (d | ~b)) + blocks[10] - 1051523;
+    c = (c << 15 | c >>> 17) + d << 0;
+    b += (d ^ (c | ~a)) + blocks[1] - 2054922799;
+    b = (b << 21 | b >>> 11) + c << 0;
+    a += (c ^ (b | ~d)) + blocks[8] + 1873313359;
+    a = (a << 6 | a >>> 26) + b << 0;
+    d += (b ^ (a | ~c)) + blocks[15] - 30611744;
+    d = (d << 10 | d >>> 22) + a << 0;
+    c += (a ^ (d | ~b)) + blocks[6] - 1560198380;
+    c = (c << 15 | c >>> 17) + d << 0;
+    b += (d ^ (c | ~a)) + blocks[13] + 1309151649;
+    b = (b << 21 | b >>> 11) + c << 0;
+    a += (c ^ (b | ~d)) + blocks[4] - 145523070;
+    a = (a << 6 | a >>> 26) + b << 0;
+    d += (b ^ (a | ~c)) + blocks[11] - 1120210379;
+    d = (d << 10 | d >>> 22) + a << 0;
+    c += (a ^ (d | ~b)) + blocks[2] + 718787259;
+    c = (c << 15 | c >>> 17) + d << 0;
+    b += (d ^ (c | ~a)) + blocks[9] - 343485551;
+    b = (b << 21 | b >>> 11) + c << 0;
+
+    if (this.first) {
+      this.h0 = a + 1732584193 << 0;
+      this.h1 = b - 271733879 << 0;
+      this.h2 = c - 1732584194 << 0;
+      this.h3 = d + 271733878 << 0;
+      this.first = false;
+    } else {
+      this.h0 = this.h0 + a << 0;
+      this.h1 = this.h1 + b << 0;
+      this.h2 = this.h2 + c << 0;
+      this.h3 = this.h3 + d << 0;
+    }
+  };
+
+  /**
+   * @method hex
+   * @memberof Md5
+   * @instance
+   * @description Output hash as hex string
+   * @returns {String} Hex string
+   * @see {@link md5.hex}
+   * @example
+   * hash.hex();
+   */
+  Md5.prototype.hex = function () {
+    this.finalize();
+
+    var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3;
+
+    return HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
+      HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] +
+      HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] +
+      HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] +
+      HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
+      HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] +
+      HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] +
+      HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] +
+      HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
+      HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] +
+      HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] +
+      HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] +
+      HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
+      HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] +
+      HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] +
+      HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F];
+  };
+
+  /**
+   * @method toString
+   * @memberof Md5
+   * @instance
+   * @description Output hash as hex string
+   * @returns {String} Hex string
+   * @see {@link md5.hex}
+   * @example
+   * hash.toString();
+   */
+  Md5.prototype.toString = Md5.prototype.hex;
+
+  /**
+   * @method digest
+   * @memberof Md5
+   * @instance
+   * @description Output hash as bytes array
+   * @returns {Array} Bytes array
+   * @see {@link md5.digest}
+   * @example
+   * hash.digest();
+   */
+  Md5.prototype.digest = function () {
+    this.finalize();
+
+    var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3;
+    return [
+      h0 & 0xFF, (h0 >> 8) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 24) & 0xFF,
+      h1 & 0xFF, (h1 >> 8) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 24) & 0xFF,
+      h2 & 0xFF, (h2 >> 8) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 24) & 0xFF,
+      h3 & 0xFF, (h3 >> 8) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 24) & 0xFF
+    ];
+  };
+
+  /**
+   * @method array
+   * @memberof Md5
+   * @instance
+   * @description Output hash as bytes array
+   * @returns {Array} Bytes array
+   * @see {@link md5.array}
+   * @example
+   * hash.array();
+   */
+  Md5.prototype.array = Md5.prototype.digest;
+
+  /**
+   * @method arrayBuffer
+   * @memberof Md5
+   * @instance
+   * @description Output hash as ArrayBuffer
+   * @returns {ArrayBuffer} ArrayBuffer
+   * @see {@link md5.arrayBuffer}
+   * @example
+   * hash.arrayBuffer();
+   */
+  Md5.prototype.arrayBuffer = function () {
+    this.finalize();
+
+    var buffer = new ArrayBuffer(16);
+    var blocks = new Uint32Array(buffer);
+    blocks[0] = this.h0;
+    blocks[1] = this.h1;
+    blocks[2] = this.h2;
+    blocks[3] = this.h3;
+    return buffer;
+  };
+
+  /**
+   * @method buffer
+   * @deprecated This maybe confuse with Buffer in node.js. Please use arrayBuffer instead.
+   * @memberof Md5
+   * @instance
+   * @description Output hash as ArrayBuffer
+   * @returns {ArrayBuffer} ArrayBuffer
+   * @see {@link md5.buffer}
+   * @example
+   * hash.buffer();
+   */
+  Md5.prototype.buffer = Md5.prototype.arrayBuffer;
+
+  /**
+   * @method base64
+   * @memberof Md5
+   * @instance
+   * @description Output hash as base64 string
+   * @returns {String} base64 string
+   * @see {@link md5.base64}
+   * @example
+   * hash.base64();
+   */
+  Md5.prototype.base64 = function () {
+    var v1, v2, v3, base64Str = '', bytes = this.array();
+    for (var i = 0; i < 15;) {
+      v1 = bytes[i++];
+      v2 = bytes[i++];
+      v3 = bytes[i++];
+      base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] +
+        BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] +
+        BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] +
+        BASE64_ENCODE_CHAR[v3 & 63];
+    }
+    v1 = bytes[i];
+    base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] +
+      BASE64_ENCODE_CHAR[(v1 << 4) & 63] +
+      '==';
+    return base64Str;
+  };
+
+  var exports = createMethod();
+
+  if (COMMON_JS) {
+    module.exports = exports;
+  } else {
+    /**
+     * @method md5
+     * @description Md5 hash function, export to global in browsers.
+     * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash
+     * @returns {String} md5 hashes
+     * @example
+     * md5(''); // d41d8cd98f00b204e9800998ecf8427e
+     * md5('The quick brown fox jumps over the lazy dog'); // 9e107d9d372bb6826bd81d3542a419d6
+     * md5('The quick brown fox jumps over the lazy dog.'); // e4d909c290d0fb1ca068ffaddf22cbd0
+     *
+     * // It also supports UTF-8 encoding
+     * md5('中文'); // a7bac2239fcdcb3a067903d8077c4a07
+     *
+     * // It also supports byte `Array`, `Uint8Array`, `ArrayBuffer`
+     * md5([]); // d41d8cd98f00b204e9800998ecf8427e
+     * md5(new Uint8Array([])); // d41d8cd98f00b204e9800998ecf8427e
+     */
+    root.md5 = exports;
+    if (AMD) {
+      define(function () {
+        return exports;
+      });
+    }
+  }
+})();

+ 23 - 0
package.json

@@ -0,0 +1,23 @@
+{
+    "id": "tuniao-simple-circle",
+    "name": "速立保",
+    "displayName": "速立保",
+    "version": "1.3.1",
+    "description": "支持微信小程序、APP和H5。",
+    "keywords": [
+        "速立保",
+        "模板",
+        "酷炫",
+        "简约",
+        "前端模板"
+    ],
+    "dcloudext": {
+        "category": [
+            "uni-app前端模板",
+            "前端页面模板"
+        ]
+    },
+	"dependencies": {
+	  "js-md5": "^0.7.3"
+	}
+}

+ 142 - 0
pages.json

@@ -0,0 +1,142 @@
+{
+  "easycom": {
+    "^tn-(.*)": "@/tuniao-ui/components/tn-$1/tn-$1.vue"
+  },
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		
+		{
+			"path" : "pages/index/auth",
+			"style" : 
+			{
+				"navigationBarTitleText" : ""
+			}
+		},
+		{
+		  "path": "pages/index/index",
+		  "style": {
+		    "mp-weixin": {
+		      "disableScroll": true
+		    },
+		    "app-plus": {
+		      "bounce": "none"
+		    },
+		    "mp-alipay": {
+		      "allowsBounceVertical": "NO"
+		    },
+		    "mp-baidu": {
+		      "disableScroll": true
+		    }
+		  }
+		},
+		{
+			"path" : "pages/login/login",
+			"style" : 
+			{
+				"navigationBarTitleText" : "登录"
+			}
+		},
+		{
+			"path" : "pages/login/info",
+			"style" : 
+			{
+				"navigationBarTitleText" : "用户协议"
+			}
+		},
+		{
+			"path" : "pages/mine/need",
+			"style" : 
+			{
+				"navigationBarTitleText" : "我的需求"
+			}
+		},
+		{
+			"path" : "pages/mine/share",
+			"style" : 
+			{
+				"navigationBarTitleText" : "我的共享"
+			}
+		},
+		{
+			"path" : "pages/webview/web-view",
+			"style" : 
+			{
+				"navigationBarTitleText" : ""
+			}
+		},
+		{
+			"path" : "pages/mine/coll",
+			"style" : 
+			{
+				"navigationBarTitleText" : "我的收藏"
+			}
+		},
+		{
+			"path" : "pages/comm/search",
+			"style" : 
+			{
+				"navigationBarTitleText" : "搜索"
+			}
+		},
+		{
+			"path" : "pages/mine/feedback",
+			"style" : 
+			{
+				"navigationBarTitleText" : "问题反馈",
+				"enablePullDownRefresh": true
+			}
+		},
+		{
+			"path" : "pages/mine/addFeed",
+			 
+			"style" : 
+			{
+				"navigationBarTitleText" : "问题反馈"
+			}
+		},
+		{
+			"path" : "pages/mine/about",
+			"style" : 
+			{
+				"navigationBarTitleText" : "了解速立保"
+			}
+		}
+		 
+	],
+  "subPackages": [{
+    "root":"homePages",
+    "pages": []
+  }, {
+    "root":"circlePages",
+    "pages": [{
+        "path": "circle",
+        "style": {
+          "navigationBarTitleText": "晒帖子",
+          "enablePullDownRefresh": false
+        }
+      },
+      {
+      	"path" : "addShare",
+      	"style" : 
+      	{
+      		"navigationBarTitleText" : "发布共享"
+      	}
+      }]
+  }, {
+    "root":"minePages",
+    "pages": [{
+        "path": "set",
+        "style": {
+          "navigationBarTitleText": "设置",
+          "enablePullDownRefresh": false
+        }
+      }
+    ]
+  }],
+	"globalStyle": {
+	  "navigationStyle": "custom",
+	  "navigationBarTextStyle": "black",
+	  "navigationBarTitleText": "速立保",
+	  "navigationBarBackgroundColor": "#F8F8F8",
+	  "backgroundColor": "#F8F8F8"
+	}
+}

ファイルの差分が大きいため隠しています
+ 1228 - 0
pages/comm/comm.vue


+ 83 - 0
pages/comm/search.vue

@@ -0,0 +1,83 @@
+<template>
+	<view>
+		<tn-nav-bar fixed alpha customBack>
+		  <view slot="back" class='tn-custom-nav-bar__back'
+		    @click="goBack">
+		    <text class='icon tn-icon-left'></text>
+		  </view>
+		  <view slot="default">
+		  	<view>
+		  		<text>搜索</text>
+		  	</view>
+		  	 
+		  </view>
+		</tn-nav-bar>
+		
+		<view style="width: 100%;padding:16px"  :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+			<uni-search-bar radius="30" :focus="true" v-model="prodName"  placeholder="请输入您感兴趣的产品名称" cancelButton="none">
+				<template v-slot:searchIcon>
+						 
+					</template>
+			</uni-search-bar>
+			<uni-search-bar radius="30"  v-model="brand"  placeholder="请输入您感兴趣的产品品牌" cancelButton="none">
+				<template v-slot:searchIcon>
+						 
+					</template>
+			</uni-search-bar>
+			<uni-search-bar radius="30" v-model="prodSpec" placeholder="请输入您感兴趣的产品型号" cancelButton="none">
+				<template v-slot:searchIcon>
+						 
+					</template>
+			</uni-search-bar>
+			
+			<button type="primary" style="border-radius:30px;background-color: #3a96d7;" radius @click="searchProd">搜索</button>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				brand: '',
+				prodName:'',
+				prodSpec: '',	
+			}
+		},
+		methods: {
+			goBack(){
+				uni.navigateBack();
+			},
+			searchProd(){
+				uni.setStorageSync('searchProdValue',this.prodName+'/'+this.brand+'/'+this.prodSpec)
+				uni.navigateBack();
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+    /* 胶囊*/
+    .tn-custom-nav-bar__back {
+      width: 60%;
+      height: 100%;
+      position: relative;
+      display: flex;
+      justify-content: space-evenly;
+      align-items: center;
+      box-sizing: border-box;
+      // background-color: rgba(0, 0, 0, 0.15);
+      border-radius: 1000rpx;
+      border: 1rpx solid rgba(255, 255, 255, 0.5);
+      // color: #333;
+      font-size: 18px;
+      
+      .icon {
+        display: block;
+        flex: 1;
+        margin: auto;
+        text-align: center;
+      }
+      
+    }
+</style>

+ 54 - 0
pages/discovery/discovery.vue

@@ -0,0 +1,54 @@
+<template>
+	<view style="text-align: center;" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+		<image style="width: 180px;height: 150px" src="../../static/logo.png"></image>
+		
+		<view style="margin-top:24px"><text style="font-size:30px;">生物制药产业</text></view>
+		<view><text style="font-size:30px">国际产品展示中心</text></view>
+		<view style="margin-top:24px"><text style="font-size:20px;">生物制药产业一站式产品资源供需平台</text></view>
+		 
+		<view style="display: flex;margin-top:32px">
+			<view style="flex: 1;padding: 0 16px;"><tn-button size="lg" width="100%"  backgroundColor="#3a96d7" fontColor="#ffffff" @click="showAdd">我要什么</tn-button></view>
+			<view style="flex: 1;padding: 0 16px;"><tn-button size="lg" width="100%"  backgroundColor="#1d60b1" fontColor="#ffffff" @click="showAdd2">我有什么</tn-button></view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			showAdd(){
+				if(uni.getStorageSync('userNo')){
+					uni.navigateTo({
+						url:'/circlePages/circle'
+					})
+				}else{
+					uni.navigateTo({
+						url:'/pages/login/login'
+					})
+				}
+				
+			},
+			showAdd2(){
+				if(uni.getStorageSync('userNo')){
+					uni.navigateTo({
+						url:'/circlePages/addShare'
+					})
+				}else{
+					uni.navigateTo({
+						url:'/pages/login/login'
+					})
+				}
+				
+			}
+		}
+	}
+</script>
+
+<style>
+
+</style>

ファイルの差分が大きいため隠しています
+ 1342 - 0
pages/home/home.vue


+ 112 - 0
pages/index/auth.vue

@@ -0,0 +1,112 @@
+<template>
+	<view style="display: flex;align-items: center;justify-content: center;height: 100vh;">
+		<!-- <image src="../../static/logo.png" style="width: 90px;height:80px"></image> -->
+		 <tn-loading mode="circle" size="60"></tn-loading>
+
+	</view>
+	
+</template>
+
+<script>
+	import request from '../../utils/request'
+	
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		onLoad(options) {
+			 
+			let that = this;
+			uni.login({
+				success(res) {
+					 console.error(res);
+					 that.loginByCode(res.code);
+				},
+				fail(res) {
+					 console.error(res);
+					uni.hideLoading();
+				}
+			});
+		
+		},
+		methods: {
+			loginByCode(frontId) {
+				const that = this;
+				uni.setStorageSync('loginStatus', 'false');
+				wx.getUserInfo({
+				  success: function(res) {
+					console.error(res);
+					request.post("/slbMpAutoLogin", {
+					 		code:frontId, 
+					 	    appType:'ma',
+							encryptedData:res.encryptedData,
+							iv: res.iv
+					 	}, {
+						login: false,
+						warn:false,
+						loading:false
+					}).then(res2=>{
+						console.error(res2);
+						if(res2.success){
+							//登录成功
+							uni.setStorageSync('loginStatus', 'true');
+							uni.setStorageSync('userMap', JSON.stringify(res2.resultMap));
+							uni.setStorageSync('userNo', res2.resultMap.accountName);
+							that.getUserInfo();
+						}else{
+							console.error(12345);
+							uni.setStorageSync('loginStatus', 'false');
+							uni.removeStorageSync('userMap');
+							uni.removeStorageSync('userNo');
+							uni.removeStorageSync('userInfo');
+							uni.redirectTo({
+								url:'/pages/index/index'
+							})
+							//登录失败,
+							// uni.login({
+							// 	success(res) {
+							// 		 console.error(res);
+							// 		 that.getOpenId(res.code);
+							// 	},
+							// 	fail(res) {
+							// 		 console.error(res);
+							// 		uni.hideLoading();
+							// 	}
+							// });
+							 
+						}
+						console.error(res2);
+					});
+					 
+					 
+				  }
+				})
+				 
+				
+			},
+			getUserInfo() {
+					  let that = this;
+					  request.post('/slbWxma/getPersonlInfo', {
+					  	 
+					  }).then(res => {
+							  console.warn(res);
+							  if(res.success){
+								  that.personInfo = res.resultMap.userInfo||{};
+								  uni.setStorageSync('userInfo', JSON.stringify(res.resultMap.userInfo));
+								  uni.redirectTo({
+								  	url:'/pages/index/index'
+								  })
+							  }
+					  	console.warn(res);
+					  })
+			   
+			},
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 266 - 0
pages/index/index.vue

@@ -0,0 +1,266 @@
+<template>
+	<view class="start-index">
+		<view v-if="tabberPageLoadFlag[0]" :style="{display: currentIndex === 0 ? '' : 'none'}">
+			<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
+				<Home ref="home"></Home>
+			</scroll-view>
+		</view>
+		<view v-if="tabberPageLoadFlag[1]" :style="{display: currentIndex === 1 ? '' : 'none'}">
+			<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
+				<Comm ref="comm"></Comm>
+			</scroll-view>
+		</view>
+		<view v-if="tabberPageLoadFlag[2]" :style="{display: currentIndex === 2 ? '' : 'none'}">
+			<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
+				<Discovery ref="discovery"></Discovery>
+			</scroll-view>
+		</view>
+		<!-- <view v-if="tabberPageLoadFlag[3]" :style="{display: currentIndex === 3 ? '' : 'none'}">
+      <scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
+        <Message ref="message"></Message>
+      </scroll-view>
+    </view> -->
+		<view v-if="tabberPageLoadFlag[3]" :style="{display: currentIndex === 3 ? '' : 'none'}">
+			<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
+				<Mine ref="mine"></Mine>
+			</scroll-view>
+		</view>
+
+		<tn-tabbar v-model="currentIndex" :list="tabbarList" activeColor="#1d60b1" inactiveColor="#AAAAAA"
+			activeIconColor="#1d60b1" :animation="true" :safeAreaInsetBottom="true" @change="switchTabbar"></tn-tabbar>
+	</view>
+</template>
+
+<script>
+	import Home from '../home/home.vue'
+	import Comm from '../comm/comm.vue'
+	import Discovery from '../discovery/discovery.vue'
+	import Mine from '../mine/mine.vue'
+    import request from '../../utils/request'
+	export default {
+		components: {
+			Home,
+			Comm,
+			Discovery,
+			Mine
+		},
+		data() {
+			return {
+				// 底部tabbar菜单数据
+				tabbarList: [{
+						title: '我要什么',
+						activeIcon: 'home-smile-fill',
+						inactiveIcon: 'home-smile'
+					},
+					{
+						title: '我有什么',
+						activeIcon: 'shop-fill',
+						inactiveIcon: 'shop'
+					},
+					// {
+					//   title: '发现',
+					//   activeIcon: 'rocket',
+					//   inactiveIcon: 'cube',
+					//   activeIconColor: '#FFFFFF',
+					//   inactiveIconColor: '#FFFFFF',
+					//   iconSize: 50,
+					//   out: true
+					// },
+					{
+						title: '供需发布平台',
+						activeIcon: 'add-fill',
+						inactiveIcon: 'add-circle',
+						// count: 12
+					},
+					{
+						title: '我的',
+						activeIcon: 'my-fill',
+						inactiveIcon: 'my'
+					}
+				],
+				// tabbar当前被选中的序号
+				currentIndex: 0,
+				// 自定义底栏对应页面的加载情况
+				tabberPageLoadFlag: []
+			}
+		},
+		onLoad(options) {
+			const index = Number(options.index || 0)
+			// 根据底部tabbar菜单列表设置对应页面的加载情况
+			this.tabberPageLoadFlag = this.tabbarList.map((item, tabbar_index) => {
+				return index === tabbar_index
+			})
+			this.switchTabbar(index);
+			let that = this;
+			 
+			// uni.login({
+			// 	success(res) {
+			// 		 console.error(res);
+			// 		 that.loginByCode(res.code);
+			// 	},
+			// 	fail(res) {
+			// 		 console.error(res);
+			// 		uni.hideLoading();
+			// 	}
+			// });
+
+		},
+		onShow(){
+			if (this.currentIndex === 3&&this.$refs.mine) {
+				this.$refs.mine.getContentRectInfo();
+			}
+			if (this.currentIndex === 1&&this.$refs.comm) {
+				this.$refs.comm.fetchData();
+			}
+		},
+		methods: {
+			// 切换导航
+			switchTabbar(index) {
+				this._switchTabbarPage(index)
+				if (index !== 1) {
+					this.$refs?.commRef?.stopAllVideo()
+				}
+			},
+			//获取openId,unionid
+			getOpenId(code) {
+				const that = this;
+				request.post('/wxma/code2Session',{
+						code: code,
+						platType: "slb",
+						mpType: "engineer",
+					}).then(res=>{
+						console.error(res);
+						if(res.success){
+							//登录成功
+							uni.setStorageSync('userMap', JSON.stringify(res.resultMap));
+						}
+					
+					})
+				// wx.request({
+				// 	method: 'post',
+				// 	url: 'http://slb-m.dev.ml1993.com/lx-api/wxma/code2Session', //仅为示例,并非真实的接口地址
+				// 	data: {
+				// 		code: code,
+				// 		platType: "slb",
+				// 		mpType: "engineer",
+				// 	},
+				// 	header: {
+				// 		'content-type': 'application/json' ,// 默认值
+				// 		platType: "slb",
+				// 		mpType: "engineer",
+				// 	},
+				// 	success(res) {
+				// 		console.warn(res);
+				// 		//unionid存到本地
+				// 		wx.setStorage({
+				// 			key: "frontlixiangsid",
+				// 			data: res.data.resultMap.frontlixiangsid
+				// 		}, {
+				// 			key: "userId",
+				// 			data: res.data.resultMap.unionid
+				// 		})
+
+						
+				// 	}
+				// })
+			},
+
+			loginByCode(frontId) {
+				const that = this;
+				uni.setStorageSync('loginStatus', 'false');
+				wx.getUserInfo({
+				  success: function(res) {
+					console.error(res);
+					request.post("/slbMpAutoLogin", {
+					 		code:frontId, 
+					 	    appType:'ma',
+							encryptedData:res.encryptedData,
+							iv: res.iv
+					 	}, {
+						login: false,
+						warn:false,
+						loading:false
+					}).then(res2=>{
+						console.error(res2);
+						if(res2.success){
+							//登录成功
+							uni.setStorageSync('loginStatus', 'true');
+							uni.setStorageSync('userMap', JSON.stringify(res2.resultMap));
+							uni.setStorageSync('userNo', res2.resultMap.accountName);
+						}else{
+							console.error(12345);
+							uni.setStorageSync('loginStatus', 'false');
+							
+							//登录失败,
+							uni.login({
+								success(res) {
+									 console.error(res);
+									 that.getOpenId(res.code);
+								},
+								fail(res) {
+									 console.error(res);
+									uni.hideLoading();
+								}
+							});
+							 
+						}
+						console.error(res2);
+					});
+					 // uni.request({
+					 // 	method: 'post',
+					 // 	url: 'http://slb-m.dev.ml1993.com/lx-api/slbMpAutoLogin', //仅为示例,并非真实的接口地址
+					 // 	data: {
+					 // 		code:frontId, 
+					 // 	    appType:'ma',
+						// 	encryptedData:res.encryptedData,
+						// 	iv: res.iv
+					 // 	},
+					 // 	header: {
+					 // 		'content-type': 'application/json', // 默认值
+					 		 
+					 // 		platType: "slb",
+					 // 		mpType: "engineer",
+					 // 	},
+					 // 	success(res2) {
+					 // 		console.error(res2);
+					 // 		//unionid存到本地
+					 		 
+					 // 	}
+					 // })
+					 
+				  }
+				})
+				 
+				
+			},
+
+			// 瀑布流导航页面滚动到底部
+			tabbarPageScrollLower(e) {
+				if (this.currentIndex === 0) {
+					this.$refs.home.loadMore();
+				}
+				if (this.currentIndex === 1) {
+					this.$refs.comm.loadMore();
+				}
+				if (this.currentIndex === 2) {
+					this.$refs.discovery.getRandomData && this.$refs.discovery.getRandomData()
+				}
+			},
+
+			// 切换导航页面
+			_switchTabbarPage(index) {
+				const selectPageFlag = this.tabberPageLoadFlag[index]
+				if (selectPageFlag === undefined) {
+					return
+				}
+				if (selectPageFlag === false) {
+					this.tabberPageLoadFlag[index] = true
+				}
+				this.currentIndex = index
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 81 - 0
pages/login/info.vue

@@ -0,0 +1,81 @@
+<template>
+	<view class="index tn-safe-area-inset-bottom">
+	
+		<tn-nav-bar customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack()">
+				<text class='icon tn-icon-left'></text>
+			</view>
+	
+			<view slot="default" style="display: flex;">
+				<view style="flex:1;margin-left:25px">
+					<text>服务协议</text>
+				</view>
+				
+				 
+	
+			</view>
+		</tn-nav-bar>
+	
+		<view :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+			协议内容
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			goBack() {
+				uni.navigateBack();
+			},
+		}
+	}
+</script>
+
+
+<style lang="scss" scoped>
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+</style>

+ 384 - 0
pages/login/login.vue

@@ -0,0 +1,384 @@
+<template>
+	<view style="padding: 16px;font-size: 14px;">
+		<tn-nav-bar fixed alpha customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack">
+				<text class='icon tn-icon-left'></text>
+
+			</view>
+		</tn-nav-bar>
+		<view style="text-align: center;padding: 80px 0">
+			<image src="../../static/logo.png" style="width: 110px; height: 100px;"></image>
+			<view style="margin: 8px 0;"><text style="font-size: 22px;font-weight: bold;">速立保</text></view>
+		</view>
+
+		<view :class="!agreeValue?'':'isHidden'" style="background: #00000088;
+    display: inline;
+    padding: 4px 10px;
+    border-radius: 12px;
+    border-bottom-left-radius: 0;
+    color: #fff;font-size: 12px;margin-left: 12px;">
+			请先阅读并同意协议
+		</view>
+
+		<view style="margin-top:4px">
+			<tn-checkbox v-model="agreeValue" activeColor="#1d60b1" name="选项1">
+				阅读并同意
+			</tn-checkbox>
+			<view style="display: inline;font-size: 15px;" @click="showInfo()">
+				<view style="color: #1d60b1;display: inline;">《用户服务协议》</view>
+				和
+				<view style="display: inline;color: #1d60b1;" @click="showInfo2()">《隐私政策》</view>
+			</view>
+
+		</view>
+
+		<view class="" hover-class="button-hover" style="margin-top: 24px;" v-if="agreeValue">
+			<button :disabled="!canSave" style="border-radius: 50rpx;width: 100%;background-color: #1d60b1;" type="primary" open-type="getPhoneNumber"
+				@getphonenumber="getPhoneNumber">手机号快捷登录</button>
+		</view>
+		<view class=""  hover-class="button-hover" style="margin-top: 24px;" v-if="!agreeValue">
+			<button :disabled="!canSave" style="border-radius: 50rpx;width: 100%;background-color: #1d60b1;" type="primary" @click="showToast">手机号快捷登录</button>
+		</view>
+
+
+	</view>
+</template>
+
+<script>
+	import request from '../../utils/request';
+	export default {
+
+		data() {
+			return {
+				agreeValue: false,
+				value: [],
+				range: [{
+					"value": 0,
+					"text": ""
+				}],
+				lxSessionKey: '',
+				openId: '',
+				unionid: '',
+				canSave:true,
+			}
+		},
+		onLoad() {
+
+			// this.getLxSessionKey()
+		},
+		methods: {
+			goBack() {
+				uni.navigateBack();
+			},
+			showToast() {
+				uni.showToast({
+					title: '请先阅读并同意协议',
+					icon: 'none'
+				})
+				wx.vibrateShort();
+			},
+			showInfo() {
+				uni.navigateTo({
+					url:'/pages/webview/web-view?url='+'https://test-oss.lx-device.com/userFeedback/1732866523422nfH.docx',
+				})
+			},
+			showInfo2() {
+				uni.navigateTo({
+					url:'/pages/webview/web-view?url='+'https://test-oss.lx-device.com/userFeedback/1732866629261TEn.docx',
+				})
+			},
+			change(e) {
+				this.value = e.detail.data.length == 0 ? [] : [0];
+				console.warn(this.value);
+				console.log('e:', e);
+			},
+			getLxSessionKey() {
+				const that = this;
+				uni.login({
+					success(res) {
+						console.error(res);
+						that.getOpenId(res.code);
+					},
+					fail(res) {
+						console.error(res);
+						uni.hideLoading();
+					}
+				});
+			},
+			getOpenId(code) {
+				const that = this;
+				request.post('/wxma/code2Session', {
+					code: code,
+					platType: "slb",
+					mpType: "engineer",
+				}).then(res => {
+					console.error(res);
+					if (res.success) {
+						that.lxSessionKey = res.resultMap.lxSessionKey;
+						that.openId = res.resultMap.openId;
+						that.unionid = res.resultMap.unionid;
+					}
+
+				})
+
+			},
+			getPhoneNumber(e) {
+				if (!e.detail.errMsg || e.detail.errMsg != "getPhoneNumber:ok") {
+					// wx.showModal({
+					// 	title: '提示',
+					// 	content: e.detail.errMsg,
+					// 	showCancel: false
+					// })
+					console.error(e)
+					return;
+				}
+				this.getLxSessionKey();
+				setTimeout(() => {
+					this._getPhoneNumber(e)
+				}, 1000)
+
+			},
+
+
+
+
+			_getPhoneNumber(e) {
+				console.warn(e);
+				let that = this;
+
+
+				if (e.detail.errMsg === 'getPhoneNumber:ok') {
+					wx.getUserInfo({
+						success: function(res) {
+							console.error(res);
+							res.encryptedData = encodeURIComponent(e.detail.encryptedData);
+							res.iv = e.detail.iv;
+							res.lxSessionKey = that.lxSessionKey;
+							that.getPhone(res)
+
+
+						}
+					})
+				} else {
+					uni.showToast({
+						icon: 'none',
+						title: e.detail.code ? '获取成功' : '拒绝了使用微信手机号'
+					});
+				}
+
+
+				// if (res.code == 0) {
+				// 	uni.showToast({
+				// 		title: '绑定成功',
+				// 		icon: 'success',
+				// 		duration: 2000
+				// 	})
+				// 	this.$u.vuex('mobile', res.data)
+				// 	this.form.mobile = res.data
+				// } else {
+				// 	uni.showModal({
+				// 		title: '提示',
+				// 		content: res.msg,
+				// 		showCancel: false
+				// 	})
+				// }
+			},
+
+			getPhone(prarms) {
+				let newParams = {};
+				newParams.signature = prarms.signature;
+				newParams.rawData = prarms.rawData;
+				newParams.encryptedData = prarms.encryptedData;
+				newParams.iv = prarms.iv;
+				newParams.lxSessionKey = prarms.lxSessionKey;
+
+				let that = this;
+
+				request.post('/wxma/getWaUserPhone',
+					newParams
+
+				).then(res => {
+					console.error(res);
+					if (res.success) {
+						this.ZhuceByPhone(res.resultMap.waUserPhoneInfo.phoneNumber);
+					} else {
+						uni.showToast({
+							title: res.msg,
+							icon: 'none'
+						})
+					}
+				});
+
+
+				console.warn(res);
+			},
+			async ZhuceByPhone(phone) {
+				const that = this;
+				let params = {
+					phone: phone,
+					openid: that.openId,
+					unionid: that.unionid,
+
+				};
+				that.canSave = false;
+				const res = await request.post('/wxma/register',
+					params, {
+						header: {
+							'content-type': 'application/x-www-form-urlencoded',
+							platType: "slb",
+							mpType: "engineer",
+						}
+					}
+				);
+
+				if (res.success) {
+					//当前页直接登录
+					// uni.navigateTo({
+					//     url:'/pages/index/auth'
+					// })
+					this.loginAgain();
+				} else {
+					uni.showToast({
+						title: res.msg,
+						icon: 'none'
+					})
+				}
+
+				console.warn(res);
+			},
+			loginAgain() {
+				let that = this;
+				uni.login({
+					success(res) {
+						that.loginByCode(res.code);
+					},
+					fail(res) {
+
+						uni.hideLoading();
+					}
+				});
+			},
+			loginByCode(frontId) {
+				const that = this;
+				uni.setStorageSync('loginStatus', 'false');
+				wx.getUserInfo({
+					success: function(res) {
+						console.error(res);
+						request.post("/slbMpAutoLogin", {
+							code: frontId,
+							appType: 'ma',
+							encryptedData: res.encryptedData,
+							iv: res.iv
+						}, {
+							login: false,
+							warn: false,
+							loading: false
+						}).then(res2 => {
+							console.error(res2);
+							if (res2.success) {
+								//登录成功
+								uni.setStorageSync('loginStatus', 'true');
+								uni.setStorageSync('userMap', JSON.stringify(res2.resultMap));
+								uni.setStorageSync('userNo', res2.resultMap.accountName);
+								that.getUserInfo();
+								
+							} else {
+								uni.showToast({
+									title: res2.msg,
+									icon: 'none'
+								})
+								that.canSave = true;
+								//登录失败,
+
+
+							}
+							console.error(res2);
+						});
+
+
+					}
+				})
+			},
+			getUserInfo() {
+				let that = this;
+				request.post('/slbWxma/getPersonlInfo', {
+
+				}).then(res => {
+					that.canSave = true;
+					if (res.success) {
+						that.personInfo = res.resultMap.userInfo || {};
+						uni.setStorageSync('userInfo', JSON.stringify(res.resultMap.userInfo));
+						uni.navigateBack();
+					}
+					console.warn(res);
+				})
+
+			},
+			}
+		}
+</script>
+
+<style lang="scss" scoped>
+	.isHidden {
+		visibility: hidden;
+	}
+
+	.template-edit {}
+
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+
+	/* 底部悬浮按钮 start*/
+	.tn-tabbar-height {
+		min-height: 100rpx;
+		height: calc(120rpx + env(safe-area-inset-bottom) / 2);
+	}
+
+	.tn-footerfixed {
+		position: fixed;
+		width: 100%;
+		bottom: calc(30rpx + env(safe-area-inset-bottom));
+		z-index: 1024;
+		box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0);
+
+	}
+
+	/* 底部悬浮按钮 end*/
+</style>

+ 90 - 0
pages/mine/about.vue

@@ -0,0 +1,90 @@
+<template>
+	<view class="index tn-safe-area-inset-bottom">
+	
+		<tn-nav-bar customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack()">
+				<text class='icon tn-icon-left'></text>
+			</view>
+	
+			<view slot="default" style="display: flex;">
+				<view style="flex:1;margin-left:25px">
+					<text>了解速立保</text>
+				</view>
+				
+				 
+	
+			</view>
+		</tn-nav-bar>
+	
+		<view :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+		 <view style="text-align: center;" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+		 	<image style="width: 180px;height: 150px" src="../../static/logo.png"></image>
+		 	
+		 	<view style="margin-top:24px"><text style="font-size:30px;">生物制药产业</text></view>
+		 	<view><text style="font-size:30px">国际产品展示中心</text></view>
+		 	<view style="margin-top:24px"><text style="font-size:20px;">生物制药产业一站式产品资源供需平台</text></view>
+		 	 
+		 	 <image style="width: 180px; margin-top: 80px;" src="../../static/callus.png"></image>
+		 	 
+		 </view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			goBack() {
+				uni.navigateBack();
+			},
+		}
+	}
+</script>
+
+
+<style lang="scss" scoped>
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+</style>

+ 279 - 0
pages/mine/addFeed.vue

@@ -0,0 +1,279 @@
+<template>
+	<view class="index tn-safe-area-inset-bottom">
+
+		<tn-nav-bar customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack()">
+				<text class='icon tn-icon-left'></text>
+			</view>
+
+			<view slot="default" style="display: flex;">
+				<view style="flex:1;margin-left:25px">
+					<text>问题反馈</text>
+				</view>
+				
+				<view @click="showHis">
+					<text class='tn-icon-time'></text>
+					历史反馈
+				</view>
+
+			</view>
+		</tn-nav-bar>
+
+		<view :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+
+			<view style="display: flex;line-height: 37px;padding: 16px;">
+				<view style="margin-right: 8px;">问题类型</view>
+				<view style="flex:1"><uni-data-select :clear="false" v-model="typeValue" :localdata="range" @change="change"></uni-data-select></view>
+				
+
+			</view>
+
+			 
+				 
+				 <view class="tn-margin tn-bg-gray--light tn-padding" style="border-radius: 10rpx;">
+				 	<textarea maxlength="500" v-model="contentValue" placeholder="请描述您遇到的问题..." placeholder-style="color:#AAAAAA"></textarea>
+				 </view>
+				 
+				 <view class="tn-margin-left tn-padding-top-xs">
+				 	<uni-file-picker
+				 		v-model="imgList" :limit="6"  :auto-upload="false" @select="select" @success="success">
+				 		 
+				 	   
+				 	</uni-file-picker>
+			    </view>
+			<view style="display: flex;line-height: 37px;padding: 16px;">
+				<view style="margin-right: 8px;">联系方式</view>
+				<view style="flex:1">
+					<uni-easyinput  v-model="contactMethod" placeholder="请输入内容"></uni-easyinput>
+					
+				</view>
+				
+
+			</view>
+
+		</view>
+		
+		<view class="tn-flex tn-footerfixed">
+			 
+			<view class="tn-flex-1 justify-content-item tn-margin-sm tn-text-center">
+				<tn-button backgroundColor="#3668FC" padding="40rpx 0" width="60%" shadow fontBold @click="saveForm()">
+					<!-- <text class="tn-icon-light tn-padding-right-xs tn-color-black"></text> -->
+					<text class="tn-color-white">提交</text>
+					<!-- <text class="tn-icon-camera tn-padding-left-xs tn-color-black"></text> -->
+				</tn-button>
+			</view>
+		</view>
+
+
+	</view>
+</template>
+
+<script>
+	import request from '../../utils/request'
+
+	export default {
+		data() {
+			return {
+				imgList:[],
+				fileDetailList:[],
+				showHistory: false,
+				content: [],
+				contentValue:'',
+				contactMethod:uni.getStorageSync('userInfo')?JSON.parse(uni.getStorageSync('userInfo')).userName:'',
+				typeValue:1,
+				range: [{
+						value: 1,
+						text: "汇报系统故障"
+					},
+					{
+						value: 2,
+						text: "平台问题"
+					},
+					{
+						value: 3,
+						text: "投诉"
+					},
+				],
+				showEmpty: false,
+			}
+		},
+		filters: {
+			formatDate(value) {
+				if (!value) return '';
+				const date = new Date(value);
+				const today = new Date();
+				const yesterday = new Date(today); // 昨天的日期
+				yesterday.setDate(yesterday.getDate() - 1); // 将昨天的日期设置为前一天
+
+				if (date.getFullYear() == today.getFullYear() && date.getMonth() == today.getMonth() && date.getDate() ==
+					today.getDate()) {
+					return '今天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				if (date.getFullYear() == yesterday.getFullYear() && date.getMonth() == yesterday.getMonth() && date
+					.getDate() == yesterday.getDate()) {
+					return '昨天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				return date.toLocaleDateString() + ' ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+					.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+			},
+
+
+		},
+		created() {
+			// uni.navigateTo({
+			// 	url:'/pages/mine/addFeed'
+			// })
+		},
+		methods: {
+			 showHis(){
+				uni.navigateTo({
+					url:'/pages/mine/feedback'
+				}) 
+			 },
+			 
+			goBack() {
+				uni.navigateBack();
+			},
+			select(e) {
+				console.log('选择文件:', e)
+				let tempFiles = e.tempFiles;
+				for (let i in tempFiles) {
+					this.upfile(tempFiles[i])
+				}
+			},
+			upfile(file) {
+				let that = this;
+				console.warn(file);
+				uni.uploadFile({
+					url: 'http://slb-m.dev.ml1993.com/oss/upload/userFeedback', //仅为示例,非真实的接口地址
+					filePath: file.url,
+					name: 'file',
+					success: (uploadFileRes) => {
+						console.warn(JSON.parse(uploadFileRes.data));
+						let resultMap = JSON.parse(uploadFileRes.data).resultMap;
+						that.fileDetailList.push({
+							name: file.name,
+							fileName: file.name, // 原始文件名
+							ftpUrl: resultMap.uploadUrl, // 文件访问url
+						})
+					}
+				});
+			},
+			// 上传成功
+			success(e) {
+				console.log('上传成功')
+			},
+			saveForm(){
+				let that = this;
+				let params = {
+					
+				};
+				
+				if(!this.contentValue){
+					uni.showToast({
+						title: '请输入您遇到的问题',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				
+				
+				
+				if(!this.contactMethod){
+					uni.showToast({
+						title: '请输入您的联系方式',
+						duration: 2000,
+						icon:'none'
+					});
+					return false;
+				}
+				 
+				uni.showToast({
+					title: '提交中...',
+					icon:'none'
+				});
+				
+				params.slbFeedback = JSON.stringify({
+					type:this.typeValue,
+					content:this.contentValue,
+					contactMethod:this.contactMethod,
+					userNo:uni.getStorageSync('userNo'),
+				});
+				 
+				
+				params.fileDetailList = JSON.stringify(this.fileDetailList);
+				request.post('/slbFeedback/add', params).then(res => {
+					if(res.success){
+						uni.showToast({
+							title:'已提交',
+							icon:'none',
+							success:()=>{
+								setTimeout(()=>{
+									uni.redirectTo({
+										url:'/pages/mine/feedback'
+									});
+								},2000)
+								
+							}
+						})
+						
+					}else{
+						uni.showToast({
+							title:res.msg,
+							icon:'none'
+						})
+					}
+					console.warn(res);
+				})
+			}
+		},
+		onLoad() {
+			 
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+</style>

+ 504 - 0
pages/mine/coll.vue

@@ -0,0 +1,504 @@
+<template>
+	<view>
+		<tn-nav-bar fixed customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack">
+				<text class='icon tn-icon-left'></text>
+
+			</view>
+			<view slot="default">
+				<text>我的收藏</text>
+			</view>
+		</tn-nav-bar>
+
+		<view :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+			<uv-sticky  :offsetTop="vuex_custom_bar_height + 'px'" bgColor="#ffffff">
+			<tn-tabs-swiper :list="list" :isScroll="false" :current="current" name="tab-name" @change="change" style="border-bottom: 1rpx solid #f1f1f1cc;"></tn-tabs-swiper>
+			</uv-sticky>
+		
+
+			<view>
+				<!-- 图文信息 -->
+				<block v-for="(item,index) in content" :key="index">
+					<view class="blogger__item">
+						<view class="blogger__author tn-flex tn-flex-row-between tn-flex-col-center">
+							<view class="justify__author__info" @click="tn('')">
+								<view class="tn-flex tn-flex-row-center">
+									<view class="tn-flex tn-flex-row-center tn-flex-col-center">
+										<!-- <view class="">
+													<tn-avatar class="" shape="circle" :src="item.userAvatar" size="lg">
+													</tn-avatar>
+												</view> -->
+										<view class="tn-padding-right tn-text-ellipsis">
+											<view class="tn-padding-right tn-text-bold tn-text-lg">
+												{{ item.company }}
+											</view>
+
+										</view>
+									</view>
+								</view>
+							</view>
+							<view v-if="item.validDate"
+								class="blogger__author__btn justify-content-item tn-flex-col-center tn-flex-row-center">
+								<text class="" style="background: #3F51B542;font-size: 12px;
+				padding: 8px;
+				color: #0000FF;
+				border-radius: 24px;
+				border-top-right-radius: 0;">{{item.validDate}}</text>
+							</view>
+						</view>
+
+						<view
+							class="blogger__desc tn-margin-top-sm tn-margin-bottom-sm tn-text-justify tn-flex-col-center tn-flex-row-left"
+							@click="tn('')">
+							<!-- <view v-for="(label_item,label_index) in item.label" :key="label_index"
+										class="blogger__desc__label tn-float-left tn-margin-right">
+										<text class="blogger__desc__label--prefix tn-icon-topics-fill"></text>
+										<text class="tn-text-df">{{ label_item }}</text>
+									</view> -->
+							<!-- 不用限制长度了,因为发布的时候限制长度了-->
+							<tn-tag margin="-4px 4px 0 0" backgroundColor="#3a96d733" v-if="item.brand" fontColor="#3a96d7" shape="circle">{{ item.brand }}</tn-tag>
+							 				
+							<text
+								class="blogger__desc__content tn-flex-1 tn-text-justify tn-text-df">{{ item.content }}</text>
+						</view>
+
+						<!-- 内容太多疲劳了-->
+						<!-- <view
+				      v-if="item.content"
+				      class="blogger__content"
+				      :id="`blogger__content--${index}`"
+				    >
+				      <view
+				        class="blogger__content__data clamp-text-2">
+				        {{ item.content }}
+				      </view>
+				    </view> -->
+
+						<block v-if="item.imgList">
+							<view v-if="[1,2,4].indexOf(item.imgList.length) != -1" class="tn-padding-top-xs"
+								@click="tn('')">
+								<image v-for="(image_item,image_index) in item.imgList" :key="image_index"
+									class="blogger__main-image" :class="{
+				            'blogger__main-image--1 tn-margin-bottom-sm': item.imgList.length === 1,
+				            'blogger__main-image--2 tn-margin-right-sm tn-margin-bottom-sm': item.imgList.length === 2 || item.imgList.length === 4
+				          }" :src="image_item.ftpUrl" mode="scaleToFill" @click="showImg(item.imgList,image_index)"></image>
+							</view>
+							<view v-else class="tn-padding-top-xs" @click="tn('')">
+								<tn-grid hoverClass="none" :col="3">
+									<block v-for="(image_item,image_index) in item.imgList" :key="image_index">
+										<!-- #ifndef MP-WEIXIN -->
+										<tn-grid-item style="width: 30%;margin: 10rpx;">
+											<image class="blogger__main-image blogger__main-image--3"
+												:src="image_item.ftpUrl" mode="scaleToFill" @click="showImg(item.imgList,image_index)"></image>
+										</tn-grid-item>
+										<!-- #endif-->
+										<!-- #ifdef MP-WEIXIN -->
+										<tn-grid-item style="width: 30%;margin: 10rpx;">
+											<image class="blogger__main-image blogger__main-image--3"
+scaleToFill		:src="image_item.ftpUrl" mode="aspectFill" @click="showImg(item.imgList,image_index)"></image>
+										</tn-grid-item>
+										<!-- #endif-->
+									</block>
+								</tn-grid>
+							</view>
+						</block>
+						<!-- 内容太多疲劳了-->
+						<view
+						  v-if="item.shareExt&&item.shareExt.length>0"
+						  class="blogger__content"
+						  :id="`blogger__content--${index}`"
+						>
+						  <uni-table border stripe emptyText="暂无更多数据" >
+						  	<!-- 表头行 -->
+						  	<uni-tr>
+						  		<uni-th align="center">产品名称1</uni-th>
+						  		<uni-th align="center">规格型号</uni-th>
+						  		<uni-th align="left">产品介绍</uni-th>
+						  	</uni-tr>
+						  	<!-- 表格数据行 -->
+						  	<uni-tr v-for="extItem in item.shareExt">
+						  		<uni-td>{{extItem.prodName}}</uni-td>
+								<uni-td>{{extItem.prodSpec}}</uni-td>
+								<uni-td>{{extItem.prodDesc}}</uni-td>
+						  	</uni-tr>
+						  </uni-table>
+						</view>
+						
+						<view v-for="file in item.fileDetailList" v-if="!isImage(file.fileName)">
+							<view>
+								<text class="tn-icon-link"></text>
+								<view style="display: inline-block;margin-left:8px" @click="clickLink(file.ftpUrl)">
+									{{file.fileName}}
+								</view>
+
+							</view>
+						</view>
+
+
+						<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xs">
+							<view class="justify-content-item tn-color-gray tn-text-center">
+								<view class="tn-padding-right   tn-padding-top-xs tn-color-gray">
+									{{ item.createTime2|formatDate }}
+								</view>
+
+							</view>
+							<view class="justify-content-item tn-flex tn-flex-col-center">
+								<text class="tn-icon-delete tn-color-gray tn-text-bold tn-text-xxl"
+									@click="delColl(item)"></text>
+							</view>
+						</view>
+					</view>
+
+					<!-- 边距间隔 -->
+					<view class="tn-strip-bottom" v-if="index != content.length - 1"></view>
+				</block>
+			</view>
+
+			<view v-if="showEmpty" style="margin-top: 32vh;">
+				<tn-empty mode="data"></tn-empty>
+			</view>
+
+		</view>
+
+
+		<!-- 提示窗示例 -->
+		<uni-popup ref="alertDialog" type="dialog">
+			<uni-popup-dialog type="info" cancelText="关闭" confirmText="确认" title="提示" content="确定取消收藏吗?" @confirm="dialogConfirm"
+				@close="dialogClose"></uni-popup-dialog>
+		</uni-popup>
+	 
+
+
+	</view>
+</template>
+
+<script>
+	import request from '../../utils/request'
+
+	export default {
+		data() {
+			return {
+				list: [{
+					'tab-name': '需求'
+				}, {
+					'tab-name': '共享'
+				}],
+				current: 0,
+				showEmpty: false,
+				content: [],
+				curItem:{}
+
+			}
+		},
+		filters: {
+			formatDate(value) {
+				if (!value) return '';
+				const date = new Date(value);
+				const today = new Date();
+				const yesterday = new Date(today); // 昨天的日期
+				yesterday.setDate(yesterday.getDate() - 1); // 将昨天的日期设置为前一天
+
+				if (date.getFullYear() == today.getFullYear() && date.getMonth() == today.getMonth() && date.getDate() ==
+					today.getDate()) {
+					return '今天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				if (date.getFullYear() == yesterday.getFullYear() && date.getMonth() == yesterday.getMonth() && date
+					.getDate() == yesterday.getDate()) {
+					return '昨天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+
+				return date.toLocaleDateString() + ' ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+					.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+			},
+
+
+		},
+		onShow() {
+			this.loadData();
+		},
+		methods: {
+			goBack() {
+				uni.navigateBack();
+			},
+			change(index) {
+				this.current = index;
+				this.loadData();
+			},
+			isImage(fileName) {
+				const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico']
+				const extension = fileName.split('.').pop().toLowerCase();
+				return imageExtensions.includes(extension);
+			},
+			loadData() {
+				let that = this;
+				that.content = [];
+				request.post('/slbCollect/show/my', {
+					userNo: uni.getStorageSync('userNo'),
+					bisType: that.current == 1 ? '2' : '1',
+
+				}).then(res => {
+					let newList = res.list || [];
+					if (res.success) {
+						let cList = [];
+						for (let i = 0; i < newList.length; i++) {
+							if (newList[i].fkBisMap) {
+								newList[i].fkBisMap.collId = newList[i].id;
+								newList[i].fkBisMap.createTime2 = newList[i].createTime;
+								newList[i].fkBisMap.imgList = [];
+								for (let j = 0; j < newList[i].fkBisMap.fileDetailList.length; j++) {
+									if (that.isImage(newList[i].fkBisMap.fileDetailList[j].fileName)) {
+										newList[i].fkBisMap.imgList.push(newList[i].fkBisMap.fileDetailList[j]);
+									}
+								}
+
+								cList.push(newList[i].fkBisMap);
+							}
+						}
+						that.content = cList;
+					}
+
+					if (newList.length == 0) {
+						that.showEmpty = true;
+					} else {
+						that.showEmpty = false;
+					}
+
+
+				})
+			},
+			
+			dialogConfirm(){
+				let item = this.curItem;
+				let that = this;
+				request.post('/slbCollect/del', {
+					id: item.collId,
+					userNo: uni.getStorageSync('userNo'),
+				}).then(res => {
+					if (res.success) {
+						uni.showToast({
+							title: '取消成功'
+						})
+						that.loadData();
+					} else {
+						uni.showToast({
+							title: res.msg,
+							icon: 'none'
+						})
+					}
+				})
+			},
+			
+			delColl(item) {
+				this.curItem = item;
+				this.$refs.alertDialog.open()
+				
+			},
+			showImg(items, index) {
+				let urls = [];
+				for (let i = 0; i < items.length; i++) {
+					urls.push(items[i].ftpUrl);
+				}
+
+				// 预览图片
+				uni.previewImage({
+					urls: urls,
+					current: index,
+
+				});
+			},
+			clickLink(url){
+				uni.navigateTo({
+					url:'/pages/webview/web-view?url='+url,
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+
+
+	/* 文章内容 start*/
+	.blogger {
+		&__item {
+			padding: 30rpx;
+		}
+
+		&__author {
+			&__btn {
+				margin-right: -12rpx;
+				opacity: 0.5;
+			}
+		}
+
+		&__desc {
+			line-height: 30rpx;
+
+			&__label {
+
+				color: #1D2541;
+				background-color: #F3F2F7;
+				border-radius: 10rpx;
+				font-size: 22rpx;
+
+				padding: 5rpx 15rpx;
+				margin: 5rpx 18rpx 0 0;
+
+				&--prefix {
+					font-size: 24rpx;
+					color: #1D2541;
+					padding-right: 10rpx;
+				}
+			}
+
+			&__content {
+				line-height: 50rpx;
+			}
+		}
+
+		&__content {
+			margin-top: 18rpx;
+			padding-right: 18rpx;
+
+			&__data {
+				line-height: 46rpx;
+				text-align: justify;
+				overflow: hidden;
+				transition: all 0.25s ease-in-out;
+
+			}
+
+			&__status {
+				margin-top: 10rpx;
+				font-size: 26rpx;
+				color: #82B2FF;
+			}
+		}
+
+		&__main-image {
+			border: 1rpx solid #F8F7F8;
+			border-radius: 16rpx;
+
+			&--1 {
+				max-width: 80%;
+				max-height: 300rpx;
+			}
+
+			&--2 {
+				max-width: 260rpx;
+				max-height: 260rpx;
+			}
+
+			&--3 {
+				height: 212rpx;
+				width: 100%;
+			}
+		}
+
+		&__count-icon {
+			font-size: 40rpx;
+			padding-right: 5rpx;
+		}
+
+		&__ad {
+			width: 100%;
+			height: 500rpx;
+			transform: translate3d(0px, 0px, 0px) !important;
+
+			::v-deep .uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			.uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			&__item {
+				position: absolute;
+				width: 100%;
+				height: 100%;
+				transform-origin: left center;
+				transform: translate3d(100%, 0px, 0px) scale(1) !important;
+				transition: transform 0.25s ease-in-out;
+				z-index: 1;
+
+				&--0 {
+					transform: translate3d(0%, 0px, 0px) scale(1) !important;
+					z-index: 4;
+				}
+
+				&--1 {
+					transform: translate3d(13%, 0px, 0px) scale(0.9) !important;
+					z-index: 3;
+				}
+
+				&--2 {
+					transform: translate3d(26%, 0px, 0px) scale(0.8) !important;
+					z-index: 2;
+				}
+			}
+
+			&__content {
+				border-radius: 40rpx;
+				width: 640rpx;
+				height: 500rpx;
+				overflow: hidden;
+			}
+
+			&__image {
+				width: 100%;
+				height: 100%;
+			}
+		}
+	}
+
+	/* 文章内容 end*/
+	/* 间隔线 start*/
+	.tn-strip-bottom {
+		width: 100%;
+		border-bottom: 20rpx solid rgba(241, 241, 241, 0.8);
+	}
+	
+	/* 间隔线 end*/
+</style>

+ 593 - 0
pages/mine/feedback.vue

@@ -0,0 +1,593 @@
+<template>
+	<view class="index tn-safe-area-inset-bottom">
+
+		<tn-nav-bar customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack()">
+				<text class='icon tn-icon-left'></text>
+			</view>
+
+			<view slot="default" style="display: flex;">
+				<view style="flex:1;margin-left:25px">
+					<text>我的反馈</text>
+				</view>
+
+			</view>
+		</tn-nav-bar>
+
+		<view :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+
+
+
+			<!-- 图文信息 -->
+			<block v-for="(item,index) in content" :key="index">
+				<view class="blogger__item">
+					<view class="blogger__author tn-flex tn-flex-row-between tn-flex-col-center">
+						<view class="justify__author__info">
+							<view class="tn-flex tn-flex-row-center">
+								<view class="tn-flex tn-flex-row-center tn-flex-col-center">
+									<!-- <view class="">
+												<tn-avatar class="" shape="circle" :src="item.userAvatar" size="lg">
+												</tn-avatar>
+											</view> -->
+									<view class="tn-padding-right tn-text-ellipsis">
+										<view class="tn-padding-right tn-text-bold tn-text-lg">
+											{{ item.type==1?'【汇报系统故障】':item.type==2?'【平台问题】':item.type==3?'【投诉】':'【错误】' }}
+										</view>
+									</view>
+								</view>
+							</view>
+						</view>
+						<view v-if="item.status=='4'&&item.showFlag=='否'"
+							class="blogger__author__btn justify-content-item tn-flex-col-center tn-flex-row-center">
+							<text class="" style="background: #3F51B542;font-size: 12px;
+			padding: 8px;
+			color: #333333;
+			border-radius: 24px;
+			 ">需求已结束</text>
+						</view>
+					</view>
+
+					<view
+						class="blogger__desc tn-margin-top-sm tn-margin-bottom-sm tn-text-justify tn-flex-col-center tn-flex-row-left"
+						@click="tn('')">
+						<!-- <view v-for="(label_item,label_index) in item.label" :key="label_index"
+									class="blogger__desc__label tn-float-left tn-margin-right">
+									<text class="blogger__desc__label--prefix tn-icon-topics-fill"></text>
+									<text class="tn-text-df">{{ label_item }}</text>
+								</view> -->
+						<!-- 不用限制长度了,因为发布的时候限制长度了-->
+						<text
+							class="blogger__desc__content tn-flex-1 tn-text-justify tn-text-df">{{ item.content }}</text>
+					</view>
+
+					<!-- 内容太多疲劳了-->
+					<!-- <view
+			      v-if="item.content"
+			      class="blogger__content"
+			      :id="`blogger__content--${index}`"
+			    >
+			      <view
+			        class="blogger__content__data clamp-text-2">
+			        {{ item.content }}
+			      </view>
+			    </view> -->
+
+					<block v-if="item.imgList">
+						<view v-if="[1,2,4].indexOf(item.imgList.length) != -1" class="tn-padding-top-xs"
+							@click="tn('')">
+							<image v-for="(image_item,image_index) in item.imgList" :key="image_index"
+								class="blogger__main-image" :class="{
+			            'blogger__main-image--1 tn-margin-bottom-sm': item.imgList.length === 1,
+			            'blogger__main-image--2 tn-margin-right-sm tn-margin-bottom-sm': item.imgList.length === 2 || item.imgList.length === 4
+			          }" :src="image_item.ftpUrl" mode="aspectFill" @click="showImg(item.imgList,image_index)"></image>
+						</view>
+						<view v-else class="tn-padding-top-xs">
+							<tn-grid hoverClass="none" :col="3">
+								<block v-for="(image_item,image_index) in item.imgList" :key="image_index">
+									<!-- #ifndef MP-WEIXIN -->
+									<tn-grid-item style="width: 30%;margin: 10rpx;">
+										<image class="blogger__main-image blogger__main-image--3"
+											@click="showImg(item.imgList,image_index)" :src="image_item.ftpUrl"
+											mode="aspectFill"></image>
+									</tn-grid-item>
+									<!-- #endif-->
+									<!-- #ifdef MP-WEIXIN -->
+									<tn-grid-item style="width: 30%;margin: 10rpx;">
+										<image class="blogger__main-image blogger__main-image--3"
+											@click="showImg(item.imgList,image_index)" :src="image_item.ftpUrl"
+											mode="aspectFill"></image>
+									</tn-grid-item>
+									<!-- #endif-->
+								</block>
+							</tn-grid>
+						</view>
+					</block>
+
+
+
+					<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xs">
+						<view class="justify-content-item tn-color-gray tn-text-center">
+							<view class="tn-padding-right   tn-padding-top-xs tn-color-gray">
+								{{ item.createTime|formatDate }}
+							</view>
+
+						</view>
+
+					</view>
+				</view>
+
+				<!-- 边距间隔 -->
+				<view class="tn-strip-bottom" v-if="index != content.length - 1"></view>
+			</block>
+
+			<view v-if="showEmpty" style="margin-top: 32vh;">
+				<tn-empty mode="data"></tn-empty>
+			</view>
+		</view>
+
+		<view class="edit tnxuanfu" @tap="showLandscape">
+			<view class="bg0 pa">
+				<view class="bg1">
+					<text class='icon tn-icon-edit-write-fill'></text>
+
+				</view>
+			</view>
+			<view class="hx-box pa">
+				<view class="pr">
+					<view class="hx-k1 pa0">
+						<view class="span"></view>
+					</view>
+					<view class="hx-k2 pa0">
+						<view class="span"></view>
+					</view>
+					<view class="hx-k3 pa0">
+						<view class="span"></view>
+					</view>
+					<view class="hx-k4 pa0">
+						<view class="span"></view>
+					</view>
+					<view class="hx-k5 pa0">
+						<view class="span"></view>
+					</view>
+					<view class="hx-k6 pa0">
+						<view class="span"></view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import request from '../../utils/request'
+
+	export default {
+		data() {
+			return {
+				showHistory: false,
+				content: [],
+				showEmpty: false,
+			}
+		},
+		filters: {
+			formatDate(value) {
+				if (!value) return '';
+				const date = new Date(value);
+				const today = new Date();
+				const yesterday = new Date(today); // 昨天的日期
+				yesterday.setDate(yesterday.getDate() - 1); // 将昨天的日期设置为前一天
+
+				if (date.getFullYear() == today.getFullYear() && date.getMonth() == today.getMonth() && date.getDate() ==
+					today.getDate()) {
+					return '今天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				if (date.getFullYear() == yesterday.getFullYear() && date.getMonth() == yesterday.getMonth() && date
+					.getDate() == yesterday.getDate()) {
+					return '昨天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				return date.toLocaleDateString() + ' ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+					.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+			},
+
+
+		},
+		created() {
+			
+		},
+		methods: {
+			onPullDownRefresh() {
+				this.loadData();
+			},
+			showLandscape() {
+				uni.navigateTo({
+					url: '/pages/mine/addFeed'
+				})
+			},
+			goBack() {
+					uni.navigateBack();
+			},
+			isImage(fileName) {
+				const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico']
+				const extension = fileName.split('.').pop().toLowerCase();
+				return imageExtensions.includes(extension);
+			},
+			loadData() {
+				
+				let that = this;
+				that.showEmpty = false;
+				request.post('/slbFeedback/query', {
+					userNo: uni.getStorageSync('userNo'),
+				}).then(res => {
+					console.warn(res);
+					if (res && res.success) {
+						let newList = res.list || [];
+						for (let i = 0; i < newList.length; i++) {
+							newList[i].imgList = [];
+							for (let j = 0; j < newList[i].fileDetailList.length; j++) {
+								if (that.isImage(newList[i].fileDetailList[j].fileName)) {
+									newList[i].imgList.push(newList[i].fileDetailList[j]);
+								}
+							}
+						}
+						that.content = newList;
+						if (newList.length == 0) {
+							that.showEmpty = true;
+						} else {
+							that.showEmpty = false;
+						}
+					}
+					uni.stopPullDownRefresh();
+				})
+			},
+			showImg(items, index) {
+				let urls = [];
+				for (let i = 0; i < items.length; i++) {
+					urls.push(items[i].ftpUrl);
+				}
+
+				// 预览图片
+				uni.previewImage({
+					urls: urls,
+					current: index,
+
+				});
+			},
+		},
+		onLoad() {
+			this.loadData();
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		// border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+
+
+	/* 文章内容 start*/
+	.blogger {
+		&__item {
+			padding: 30rpx;
+		}
+
+		&__author {
+			&__btn {
+				margin-right: -12rpx;
+				opacity: 0.5;
+			}
+		}
+
+		&__desc {
+			line-height: 30rpx;
+
+			&__label {
+
+				color: #1D2541;
+				background-color: #F3F2F7;
+				border-radius: 10rpx;
+				font-size: 22rpx;
+
+				padding: 5rpx 15rpx;
+				margin: 5rpx 18rpx 0 0;
+
+				&--prefix {
+					font-size: 24rpx;
+					color: #1D2541;
+					padding-right: 10rpx;
+				}
+			}
+
+			&__content {
+				line-height: 50rpx;
+			}
+		}
+
+		&__content {
+			margin-top: 18rpx;
+			padding-right: 18rpx;
+
+			&__data {
+				line-height: 46rpx;
+				text-align: justify;
+				overflow: hidden;
+				transition: all 0.25s ease-in-out;
+
+			}
+
+			&__status {
+				margin-top: 10rpx;
+				font-size: 26rpx;
+				color: #82B2FF;
+			}
+		}
+
+		&__main-image {
+			border: 1rpx solid #F8F7F8;
+			border-radius: 16rpx;
+
+			&--1 {
+				max-width: 80%;
+				max-height: 300rpx;
+			}
+
+			&--2 {
+				max-width: 260rpx;
+				max-height: 260rpx;
+			}
+
+			&--3 {
+				height: 212rpx;
+				width: 100%;
+			}
+		}
+
+		&__count-icon {
+			font-size: 40rpx;
+			padding-right: 5rpx;
+		}
+
+		&__ad {
+			width: 100%;
+			height: 500rpx;
+			transform: translate3d(0px, 0px, 0px) !important;
+
+			::v-deep .uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			.uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			&__item {
+				position: absolute;
+				width: 100%;
+				height: 100%;
+				transform-origin: left center;
+				transform: translate3d(100%, 0px, 0px) scale(1) !important;
+				transition: transform 0.25s ease-in-out;
+				z-index: 1;
+
+				&--0 {
+					transform: translate3d(0%, 0px, 0px) scale(1) !important;
+					z-index: 4;
+				}
+
+				&--1 {
+					transform: translate3d(13%, 0px, 0px) scale(0.9) !important;
+					z-index: 3;
+				}
+
+				&--2 {
+					transform: translate3d(26%, 0px, 0px) scale(0.8) !important;
+					z-index: 2;
+				}
+			}
+
+			&__content {
+				border-radius: 40rpx;
+				width: 640rpx;
+				height: 500rpx;
+				overflow: hidden;
+			}
+
+			&__image {
+				width: 100%;
+				height: 100%;
+			}
+		}
+	}
+
+	/* 文章内容 end*/
+
+	/* 间隔线 start*/
+	.tn-strip-bottom {
+		width: 100%;
+		border-bottom: 20rpx solid rgba(241, 241, 241, 0.8);
+	}
+
+	/* 间隔线 end*/
+
+
+	/* 悬浮 */
+	.tnxuanfu {
+		animation: suspension 3s ease-in-out infinite;
+	}
+
+	@keyframes suspension {
+
+		0%,
+		100% {
+			transform: translateY(0);
+		}
+
+		50% {
+			transform: translateY(-0.8rem);
+		}
+	}
+
+	/* 悬浮按钮 */
+	.button-shop {
+		width: 90rpx;
+		height: 90rpx;
+		display: flex;
+		flex-direction: row;
+		position: fixed;
+		/* bottom:200rpx;
+	    right: 20rpx; */
+		left: 5rpx;
+		top: 5rpx;
+		z-index: 1001;
+		border-radius: 100px;
+		opacity: 0.9;
+	}
+
+
+	/* 按钮 */
+	.edit {
+		bottom: 300rpx;
+		right: 75rpx;
+		position: fixed;
+		z-index: 9999;
+	}
+
+
+	.pa,
+	.pa0 {
+		position: absolute
+	}
+
+	.pa0 {
+		left: 0;
+		top: 0
+	}
+
+
+	.bg0 {
+		width: 100rpx;
+		height: 100rpx;
+		top: 50%;
+		left: 50%;
+		transform: translate(-50%, -50%);
+		background: #2196F3;
+		border-radius: 50%;
+		font-size: 32px;
+		color: #fff;
+		text-align: center;
+		line-height: 50px;
+	}
+
+	.bg1 {
+		width: 100%;
+		height: 100%;
+	}
+
+
+
+
+	.hx-box {
+		top: 50%;
+		left: 50%;
+		width: 100rpx;
+		height: 100rpx;
+		transform-style: preserve-3d;
+		transform: translate(-50%, -50%) rotateY(75deg) rotateZ(10deg);
+	}
+
+	.hx-box .pr {
+		width: 100rpx;
+		height: 100rpx;
+		transform-style: preserve-3d;
+		animation: hxz 20s linear infinite;
+	}
+
+	@keyframes hxz {
+		0% {
+			transform: rotateX(0deg);
+		}
+
+		100% {
+			transform: rotateX(-360deg);
+		}
+	}
+
+
+
+	.hx-box .pr .pa0 {
+		width: 100rpx;
+		height: 100rpx;
+		/* border: 4px solid #5ec0ff; */
+		border-radius: 1000px;
+	}
+
+
+
+	@keyframes hx {
+		to {
+			transform: rotate(360deg);
+		}
+	}
+
+	.hx-k1 {
+		transform: rotateX(-60deg) rotateZ(-60deg)
+	}
+
+	.hx-k2 {
+		transform: rotateX(-30deg) rotateZ(-30deg)
+	}
+
+	.hx-k3 {
+		transform: rotateX(0deg) rotateZ(0deg)
+	}
+
+	.hx-k4 {
+		transform: rotateX(30deg) rotateZ(30deg)
+	}
+
+	.hx-k5 {
+		transform: rotateX(60deg) rotateZ(60deg)
+	}
+
+	.hx-k6 {
+		transform: rotateX(90deg) rotateZ(90deg)
+	}
+</style>

+ 600 - 0
pages/mine/mine.vue

@@ -0,0 +1,600 @@
+<template>
+  <view class="mine tn-safe-area-inset-bottom">
+
+    <!-- 顶部自定义导航 -->
+    
+    
+    <view class="top-backgroup">
+      <image src='../../static/bg4.png' mode='widthFix' class='backgroud-image'></image>
+    </view>
+    
+    <view class="about__wrap" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+      <!-- 图标logo/头像 -->
+      <view class="tn-flex   tn-flex-col-center tn-margin-bottom" @click="tn('/minePages/set')" style="margin-top: -450rpx;justify-content: center;">
+        <view class="justify-content-item" >
+          <view class="tn-flex tn-flex-col-center tn-flex-row-left">
+            <view class="logo-pic tn-shadow">
+				
+              <view class="logo-image">
+				  <!-- <tn-avatar src="../../static/me2.png" :badge="true" size="xl" badgeIcon="star"></tn-avatar> -->
+
+				 <image v-if="personInfo.profilePhotoUrl" class="tn-shadow-blur" style="width: 140rpx;height: 140rpx;background-size: cover;" :src="personInfo.profilePhotoUrl"></image>
+                 <image v-if="!personInfo.profilePhotoUrl" class="tn-shadow-blur" style="width: 140rpx;height: 140rpx;background-size: cover;" src="../../static/me2.png"></image>
+			   <!-- <view class="tn-shadow-blur" style="background-image:url('https://cdn.nlark.com/yuque/0/2022/jpeg/280373/1664005699053-assets/web-upload/8645ea3a-e0a9-4422-8364-cc5ede305c9f.jpeg');width: 110rpx;height: 110rpx;background-size: cover;">
+                </view> -->
+              </view>
+            </view>
+			 
+
+          </view>
+        </view>
+        
+      </view>
+      
+      <!-- 没有授权,则显示这个授权按钮-->
+      <view class="tn-flex tn-flex-row-between" @click="tn('/pages/login/login')" v-if="showLogin">
+        <view class="tn-flex-1 justify-content-item tn-margin-xs tn-text-center">
+          <tn-button shape="round" backgroundColor="#1d60b1" fontColor="#ffffff" padding="20rpx 0" width="40%" shadow>
+            <text class="tn-icon-wechat tn-padding-right-xs tn-text-xl"></text>
+            <text class="">立即登录</text>
+          </tn-button>
+        </view>
+      </view>
+	  
+	  <view class="about-shadow tn-margin-top-lg tn-padding-top-sm tn-padding-bottom-sm tn-bg-white" v-if="!showLogin">
+	    
+		<view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding" @click="showModal1">
+		  <view class="justify-content-item">
+		    <view class="tn-text-bold tn-text-lg">
+		      用户名
+		    </view>
+		    
+		  </view>
+		  <view class="justify-content-item tn-text-lg tn-color-grey">
+		    <view class="tn-color-gray tn-padding-top-xs">
+		      {{personInfo.accountName||(personInfo.userName?'用户'+personInfo.userName.slice(-4):'')||'未登录'}}
+		    </view>
+ 
+		  </view>
+		</view>
+		<view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding" @click="showModal1">
+		  <view class="justify-content-item">
+		    <view class="tn-text-bold tn-text-lg">
+		      联系方式
+		    </view>
+		    
+		  </view>
+		  <view class="justify-content-item tn-text-lg tn-color-grey">
+		    <view class="tn-color-gray tn-padding-top-xs">
+		       {{personInfo.contactMethod||personInfo.userName||''}}
+		    </view>
+		 
+		  </view>
+		</view>
+		
+		<view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding" @click="showModal1">
+		  <view class="justify-content-item">
+		    <view class="tn-text-bold tn-text-lg">
+		      手机号
+		    </view>
+		    
+		  </view>
+		  <view class="justify-content-item tn-text-lg tn-color-grey">
+		    <view class="tn-color-gray tn-padding-top-xs">
+		     {{personInfo.userName||'未登录'}}
+		    </view>
+		 
+		  </view>
+		</view>
+		
+		<!-- <view class="tn-flex tn-flex-row-between tn-strip-bottom-min tn-padding" @click="showModal1">
+		  <view class="justify-content-item">
+		    <view class="tn-text-bold tn-text-lg">
+		      所属公司
+		    </view>
+		    
+		  </view>
+		  <view class="justify-content-item tn-text-lg tn-color-grey">
+		    <view class="tn-color-gray tn-padding-top-xs">
+		      未绑定
+		    </view>
+		 
+		  </view>
+		</view> -->
+		
+		
+		 
+	  </view>
+      
+       
+      
+      <!-- 方式15 start-->
+      <view class="tn-flex tn-flex-row-between tn-bg-white about-shadow tn-margin-top-xl">
+        <view class="tn-padding-sm tn-margin-xs" @click="tn('/pages/mine/need')">
+          <view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center tn-margin-left">
+            <view class="icon15__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur" style="background-color: #F3F2F7;color: #7C8191;">
+              <view class="tn-icon-like"></view>
+            </view>  
+            <view class="tn-text-center">
+              <text class="tn-text-ellipsis">我的需求</text>
+            </view>
+          </view>
+        </view>
+        <view class="tn-padding-sm tn-margin-xs" @click="tn('/pages/mine/share')">
+          <view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
+            <view class="icon15__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur" style="background-color: #F3F2F7;color: #7C8191;">
+              <view class="tn-icon-share-triangle"></view>
+            </view>  
+            <view class="tn-text-center">
+              <text class="tn-text-ellipsis">我的共享</text>
+            </view>
+          </view>
+        </view>
+        <view class="tn-padding-sm tn-margin-xs" @click="tn('/pages/mine/coll')">
+          <view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center tn-margin-right">
+            <view class="icon15__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur" style="background-color: #F3F2F7;color: #7C8191;">
+              <view class="tn-icon-star"></view>
+            </view>  
+            <view class="tn-text-center">
+              <text class="tn-text-ellipsis">我的收藏</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <!-- 方式15 end-->
+      
+      
+       
+      
+      <view class="about-shadow tn-margin-top-lg tn-margin-bottom-lg tn-padding-top-sm tn-padding-bottom-sm">
+        
+		 
+		
+        <tn-list-cell :hover="true" :unlined="true" :radius="true" :fontSize="30">
+          <button class="tn-flex tn-flex-col-center tn-button--clear-style" @click="showFeedback">
+            <view
+              class="icon1__item--icon tn-flex tn-flex-row-center tn-flex-col-center" style="color: #7C8191;">
+              <view class="tn-icon-message-fill"></view>
+            </view>
+            <view class="tn-flex tn-flex-row-between" style="width: 100%;">
+              <view class="tn-margin-left-sm">我有问题</view>
+              <view class="tn-color-gray tn-icon-right"></view>
+            </view>
+          </button>
+        </tn-list-cell>
+        <!-- <tn-list-cell :hover="true" :unlined="true" :radius="true" :fontSize="30" @click="callPhoneNumber" data-number="18266666666">
+          <view class="tn-flex tn-flex-col-center">
+            <view
+              class="icon1__item--icon tn-flex tn-flex-row-center tn-flex-col-center" style="color: #7C8191;">
+              <view class="tn-icon-tel-circle-fill"></view>
+            </view>
+            <view class="tn-margin-left-sm tn-flex-1">技术支持</view>
+            <view
+              class="tn-margin-left-sm tn-color-cat tn-text-sm tn-padding-left-xs tn-padding-right-xs tn-bg-gray--light tn-round">
+              158****8888</view>
+          </view>
+        </tn-list-cell> -->
+		<tn-list-cell :hover="true" :unlined="true" :radius="true" :fontSize="30" @click="showInfo()">
+		  <view class="tn-flex tn-flex-col-center">
+		    <view
+		      class="icon1__item--icon tn-flex tn-flex-row-center tn-flex-col-center" style="color: #7C8191;">
+		      <view class="tn-icon-safe-fill"></view>
+		    </view>
+		    <view class="tn-margin-left-sm tn-flex-1">协议展示</view>
+		    <view class="tn-color-gray tn-icon-right"></view>
+		  </view>
+		</tn-list-cell>
+		<tn-list-cell :hover="true" :unlined="true" :radius="true" :fontSize="30" @click=showAbout()>
+		  <view class="tn-flex tn-flex-col-center">
+		    <view
+		      class="icon1__item--icon tn-flex tn-flex-row-center tn-flex-col-center" style="color: #7C8191;">
+		      <view class="tn-icon-help"></view>
+		    </view>
+		    <view class="tn-margin-left-sm tn-flex-1">了解速立保</view>
+		    <view class="tn-color-gray tn-icon-right"></view>
+		  </view>
+		</tn-list-cell>
+      </view>
+
+    </view>
+
+    <view class='tn-tabbar-height'></view>
+
+  </view>
+</template>
+
+<script>
+  import request from '../../utils/request'
+  export default {
+    name: 'Mine',
+    data() {
+      return {
+		  personInfo: uni.getStorageSync('userInfo')?JSON.parse(uni.getStorageSync('userInfo')):{},
+		  showLogin: false
+      }
+    },
+	onReady() {
+		 
+	  this.$nextTick(() => {
+		  if(!uni.getStorageSync('userNo')){
+		  	this.showLogin = true;
+		  }
+		  console.error(JSON.parse(uni.getStorageSync('userInfo')))
+	    this.getContentRectInfo()
+	  })
+	  
+	},
+	 
+    methods: {
+       // 获取内容容器的信息
+       getContentRectInfo() {
+       		  let that = this;
+       		  request.post('/slbWxma/getPersonlInfo', {
+       		  	 
+       		  }).then(res => {
+				  console.warn(res);
+				  if(res&&res.success){
+					  that.personInfo = res.resultMap.userInfo||{};
+					  uni.setStorageSync('userInfo', JSON.stringify(res.resultMap.userInfo));
+					  that.showLogin = false;
+				  }
+       		  	console.warn(res);
+       		  })
+          
+       },
+      // 跳转到速立保官网
+      navTuniaoWebsite() {
+        uni.navigateToMiniProgram({
+          appId: 'wxa698b1eee960632f'
+        })
+      },
+      // 跳转到速立保UI
+      navTuniaoUI() {
+        uni.navigateToMiniProgram({
+          appId: 'wxf3d81a452b88ff4b'
+        })
+      },
+      // 跳转
+      tn(e) {
+		if(!uni.getStorageSync('userNo')){
+			uni.navigateTo({
+			  url: '/pages/login/login',
+			});
+			return false;
+		}
+        uni.navigateTo({
+          url: e,
+        });
+      },
+	  showFeedback(){
+		  uni.navigateTo({
+		    url: '/pages/mine/addFeed',
+		  });
+	  },
+      // 收货地址
+      navAddress() {
+        uni.chooseAddress({
+        })
+      },
+      // 震动跳转
+      navThanks(e) {
+        wx.vibrateShort();
+        uni.navigateTo({
+          url: '/pages/login/login'
+        })
+      },
+      //拨打固定电话
+      callPhoneNumber() {
+		  uni.navigateTo({
+		    url: '/pages/login/login'
+		  })
+        // uni.makePhoneCall({
+        //   phoneNumber: "18219128888",
+        // });
+      },
+      // 复制开源地址
+      copySource() {
+        uni.setClipboardData({
+          data: "等待上传插件市场",
+        })
+      },
+	  showInfo(){
+		 uni.navigateTo({
+		 	url:'/pages/webview/web-view?url='+'https://test-oss.lx-device.com/userFeedback/1732866523422nfH.docx',
+		 })
+	  },
+	  showAbout(){
+		  uni.navigateTo({
+		    url: '/pages/mine/about'
+		  })
+	  }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .mine{
+    max-height: 100vh;
+  }
+  /* 底部安全边距 start*/
+  .tn-tabbar-height {
+  	min-height: 120rpx;
+  	height: calc(140rpx + env(safe-area-inset-bottom) / 2);
+    height: calc(140rpx + constant(safe-area-inset-bottom));
+  }
+  
+  .tn-color-cat{
+    color: #1D2541;
+  }
+  .tn-bg-cat{
+    background-color: #1D2541;
+  }
+
+  
+  /* 自定义导航栏内容 start */
+  .custom-nav {
+    height: 100%;
+    
+    &__back {
+      margin: auto 5rpx;
+      font-size: 40rpx;
+      margin-right: 10rpx;
+      flex-basis: 5%;
+      width: 100rpx;
+      position: absolute;
+    }
+  }
+  /* 自定义导航栏内容 end */
+
+
+  /* 顶部背景图 end */
+  
+
+  /* 用户头像 start */
+  .logo-image {
+    width: 140rpx;
+    height: 140rpx;
+    position: relative;
+    overflow: hidden;
+    border-radius: 50%;
+  }
+
+  .logo-pic {
+    background-size: cover;
+    background-repeat: no-repeat;
+    // background-attachment:fixed;
+    background-position: top;
+    border: 8rpx solid rgba(255,255,255,0.05);
+    box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.15);
+    border-radius: 50%;
+    overflow: hidden;
+    // background-color: #FFFFFF;
+  }
+
+  /* 页面 start*/
+  .about-shadow {
+    border-radius: 15rpx;
+    box-shadow: 0rpx 0rpx 50rpx 0rpx rgba(0, 0, 0, 0.07);
+  }
+
+  .about {
+
+    &__wrap {
+      position: relative;
+      z-index: 1;
+      margin: 20rpx 30rpx;
+    }
+  }
+
+  /* 页面 end*/
+  
+  /* 图标容器15 start */
+  .icon15 {
+    &__item {
+      width: 30%;
+      background-color: #FFFFFF;
+      border-radius: 10rpx;
+      padding: 30rpx;
+      margin: 20rpx 10rpx;
+      transform: scale(1);
+      transition: transform 0.3s linear;
+      transform-origin: center center;
+      
+      &--icon {
+        width: 100rpx;
+        height: 100rpx;
+        font-size: 60rpx;
+        border-radius: 50%;
+        margin-bottom: 18rpx;
+        position: relative;
+        z-index: 1;
+        
+        &::after {
+          content: " ";
+          position: absolute;
+          z-index: -1;
+          width: 100%;
+          height: 100%;
+          left: 0;
+          bottom: 0;
+          border-radius: inherit;
+          opacity: 1;
+          transform: scale(1, 1);
+          background-size: 100% 100%;
+  
+            
+        }
+      }
+    }
+  }
+  
+  /* 图标容器12 start */
+  .tn-three{
+      position: absolute;
+      top: 50%;
+      right: 50%;
+      bottom: 50%;
+      left: 50%;
+      transform: translate(-38rpx, -16rpx) rotateX(30deg) rotateY(20deg) rotateZ(-30deg);
+      text-shadow: -1rpx 2rpx 0 #f0f0f0, -2rpx 4rpx 0 #f0f0f0, -10rpx 20rpx 30rpx rgba(0, 0, 0, 0.2);
+  }
+  .icon20 {
+    &__item {
+      width: 30%;
+      background-color: #FFFFFF;
+      border-radius: 10rpx;
+      padding: 30rpx;
+      margin: 20rpx 10rpx;
+      transform: scale(1);
+      transition: transform 0.3s linear;
+      transform-origin: center center;
+      
+      &--icon {
+        width: 100rpx;
+        height: 100rpx;
+        font-size: 60rpx;
+        border-radius: 50%;
+        margin-bottom: 18rpx;
+        position: relative;
+        z-index: 1;
+        
+        &::after {
+          content: " ";
+          position: absolute;
+          z-index: -1;
+          width: 100%;
+          height: 100%;
+          left: 0;
+          bottom: 0;
+          border-radius: inherit;
+          opacity: 1;
+          transform: scale(1, 1);
+          background-size: 100% 100%;
+          background-image: url(https://resource.tuniaokj.com/images/cool_bg_image/icon_bg.png);
+        }
+      }
+    }
+  }
+  
+
+
+  .button-vip {
+    width: 100%;
+    height: 150rpx;
+    border-radius: 15rpx;
+    position: relative;
+    z-index: 1;
+    
+    &::after {
+      content: " ";
+      position: absolute;
+      z-index: -1;
+      width: 100%;
+      height: 100%;
+      left: 0;
+      bottom: 0;
+      border-radius: inherit;
+      opacity: 1;
+      transform: scale(1, 1);
+      background-size: 100% 100%;
+      background-image: url(https://resource.tuniaokj.com/images/cool_bg_image/icon_bg.png);
+    }    
+  }
+  
+
+  /* 图标容器12 start */
+  .icon12 {
+    &__item {
+      width: 30%;
+      background-color: #FFFFFF;
+      border-radius: 10rpx;
+      padding: 30rpx;
+      margin: 20rpx 10rpx;
+      transform: scale(1);
+      transition: transform 0.3s linear;
+      transform-origin: center center;
+
+      &--icon {
+        width: 15rpx;
+        height: 15rpx;
+        font-size: 50rpx;
+        border-radius: 50%;
+        margin-bottom: 38rpx;
+        position: relative;
+        z-index: 1;
+        
+        &::after {
+          content: " ";
+          position: absolute;
+          z-index: -1;
+          width: 100%;
+          height: 100%;
+          left: 0;
+          bottom: 0;
+          border-radius: inherit;
+          opacity: 1;
+          transform: scale(1, 1);
+          background-size: 100% 100%;
+            
+        }
+      }
+    }
+  }
+  
+  /* 图标容器1 start */
+  .icon1 {
+    &__item {
+      // width: 30%;
+      background-color: #FFFFFF;
+      border-radius: 10rpx;
+      padding: 30rpx;
+      margin: 20rpx 10rpx;
+      transform: scale(1);
+      transition: transform 0.3s linear;
+      transform-origin: center center;
+  
+      &--icon {
+        width: 40rpx;
+        height: 40rpx;
+        font-size: 40rpx;
+        border-radius: 50%;
+        position: relative;
+        z-index: 1;
+  
+        &::after {
+          content: " ";
+          position: absolute;
+          z-index: -1;
+          width: 100%;
+          height: 100%;
+          left: 0;
+          bottom: 0;
+          border-radius: inherit;
+          opacity: 1;
+          transform: scale(1, 1);
+          background-size: 100% 100%;
+          background-image: url(https://resource.tuniaokj.com/images/cool_bg_image/icon_bg.png);
+        }
+      }
+    }
+  }
+  
+  /* 图标容器1 end */
+  
+  
+  /* 顶部背景图 start */
+  .top-backgroup {
+    height: 450rpx;
+    z-index: -1;
+  
+    .backgroud-image {
+      width: 100%;
+      height: 450rpx;
+      // z-index: -1;
+    }
+  }
+  
+  /* 顶部背景图 end */
+
+
+</style>

+ 468 - 0
pages/mine/need.vue

@@ -0,0 +1,468 @@
+<template>
+	<view>
+		<tn-nav-bar fixed customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack">
+				<text class='icon tn-icon-left'></text>
+
+			</view>
+			<view slot="default">
+				<text>我的需求</text>
+			</view>
+		</tn-nav-bar>
+		<view :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+			<uv-sticky  :offsetTop="vuex_custom_bar_height + 'px'" bgColor="#ffffff">
+			<tn-tabs-swiper :list="list" :isScroll="false" :current="current" name="tab-name"
+				@change="tabChange" style="border-bottom: 1rpx solid #f1f1f1cc;"></tn-tabs-swiper>
+				</uv-sticky>
+		
+
+		<!-- 图文信息 -->
+		<block v-for="(item,index) in content" :key="index">
+			<view class="blogger__item">
+				<view class="blogger__author tn-flex tn-flex-row-between tn-flex-col-center">
+					<view class="justify__author__info">
+						<view class="tn-flex tn-flex-row-center">
+							<view class="tn-flex tn-flex-row-center tn-flex-col-center">
+								<!-- <view class="">
+											<tn-avatar class="" shape="circle" :src="item.userAvatar" size="lg">
+											</tn-avatar>
+										</view> -->
+								<view class="tn-padding-right tn-text-ellipsis">
+									<view class="tn-padding-right tn-text-bold tn-text-lg">
+										{{ item.company }}
+									</view>
+
+								</view>
+							</view>
+						</view>
+					</view>
+					<view v-if="item.status=='4'&&item.showFlag=='否'"
+						class="blogger__author__btn justify-content-item tn-flex-col-center tn-flex-row-center">
+						<text class="" style="background: #3F51B542;font-size: 12px;
+		padding: 8px;
+		color: #333333;
+		border-radius: 24px;
+		 ">需求已结束</text>
+					</view>
+				</view>
+
+				<view
+					class="blogger__desc tn-margin-top-sm tn-margin-bottom-sm tn-text-justify tn-flex-col-center tn-flex-row-left"
+					@click="tn('')">
+					<!-- <view v-for="(label_item,label_index) in item.label" :key="label_index"
+								class="blogger__desc__label tn-float-left tn-margin-right">
+								<text class="blogger__desc__label--prefix tn-icon-topics-fill"></text>
+								<text class="tn-text-df">{{ label_item }}</text>
+							</view> -->
+					<!-- 不用限制长度了,因为发布的时候限制长度了-->
+					<text class="blogger__desc__content tn-flex-1 tn-text-justify tn-text-df">{{ item.content }}</text>
+				</view>
+
+				<!-- 内容太多疲劳了-->
+				<!-- <view
+		      v-if="item.content"
+		      class="blogger__content"
+		      :id="`blogger__content--${index}`"
+		    >
+		      <view
+		        class="blogger__content__data clamp-text-2">
+		        {{ item.content }}
+		      </view>
+		    </view> -->
+
+				<block v-if="item.imgList">
+					<view v-if="[1,2,4].indexOf(item.imgList.length) != -1" class="tn-padding-top-xs" @click="tn('')">
+						<image v-for="(image_item,image_index) in item.imgList" :key="image_index"
+							class="blogger__main-image" :class="{
+		            'blogger__main-image--1 tn-margin-bottom-sm': item.imgList.length === 1,
+		            'blogger__main-image--2 tn-margin-right-sm tn-margin-bottom-sm': item.imgList.length === 2 || item.imgList.length === 4
+		          }" :src="image_item.ftpUrl" mode="aspectFill" @click="showImg(item.imgList,image_index)"></image>
+					</view>
+					<view v-else class="tn-padding-top-xs">
+						<tn-grid hoverClass="none" :col="3">
+							<block v-for="(image_item,image_index) in item.imgList" :key="image_index">
+								<!-- #ifndef MP-WEIXIN -->
+								<tn-grid-item style="width: 30%;margin: 10rpx;">
+									<image class="blogger__main-image blogger__main-image--3" @click="showImg(item.imgList,image_index)" :src="image_item.ftpUrl"
+										mode="aspectFill"></image>
+								</tn-grid-item>
+								<!-- #endif-->
+								<!-- #ifdef MP-WEIXIN -->
+								<tn-grid-item style="width: 30%;margin: 10rpx;">
+									<image class="blogger__main-image blogger__main-image--3" @click="showImg(item.imgList,image_index)" :src="image_item.ftpUrl"
+										mode="aspectFill"></image>
+								</tn-grid-item>
+								<!-- #endif-->
+							</block>
+						</tn-grid>
+					</view>
+				</block>
+				<view v-for="file in item.fileDetailList" v-if="!isImage(file.fileName)">
+					<view>
+						<text class="tn-icon-link"></text>
+						<view style="display: inline-block;margin-left:8px" @click="clickLink(file.ftpUrl)">
+							{{file.fileName}}</view>
+
+					</view>
+				</view>
+
+
+				<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xs">
+					<view class="justify-content-item tn-color-gray tn-text-center">
+						<view class="tn-padding-right   tn-padding-top-xs tn-color-gray">
+							{{ item.createTime|formatDate }}
+						</view>
+
+					</view>
+					<view class="justify-content-item tn-flex tn-flex-col-center" v-if="item.showFlag!='否'">
+						<tn-button shadow shape="round" fontColor="tn-color-white" backgroundColor="tn-bg-blue" :fontSize="24" height="auto" padding="10rpx 18rpx" @click="finishItem(item)">结束需求</tn-button>
+						 
+					</view>
+				</view>
+			</view>
+
+			<!-- 边距间隔 -->
+			<view class="tn-strip-bottom" v-if="index != content.length - 1"></view>
+		</block>
+		
+		<view v-if="showEmpty" style="margin-top: 32vh;">
+			<tn-empty mode="data"></tn-empty>
+		</view>
+		</view>
+		<uni-popup ref="alertDialog" type="dialog">
+			<uni-popup-dialog type="info" cancelText="关闭" confirmText="确定" title="结束确认" content="确定结束需求吗" @confirm="dialogConfirm"
+				 ></uni-popup-dialog>
+		</uni-popup>
+	</view>
+
+
+
+
+	 
+</template>
+
+<script>
+	import request from '../../utils/request'
+
+	export default {
+		data() {
+			return {
+				list: [{
+					'tab-name': '已通过'
+				}, {
+					'tab-name': '审核中'
+				}, {
+					'tab-name': '暂存'
+				}, {
+					'tab-name': '已拒绝'
+				}],
+				current: 0,
+				content: [],
+				showEmpty:false,
+				curItem:{}
+			}
+		},
+		filters: {
+			formatDate(value) {
+				if (!value) return '';
+				const date = new Date(value);
+				const today = new Date();
+				const yesterday = new Date(today); // 昨天的日期
+				  yesterday.setDate(yesterday.getDate() - 1); // 将昨天的日期设置为前一天
+				 
+				if(date.getFullYear()==today.getFullYear()&&date.getMonth()==today.getMonth()&&date.getDate()==today.getDate()){
+					return  '今天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				if(date.getFullYear()==yesterday.getFullYear()&&date.getMonth()==yesterday.getMonth()&&date.getDate()==yesterday.getDate()){
+					return  '昨天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				return date.toLocaleDateString() + ' ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+					.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+			},
+
+
+		},
+		onShow() {
+			this.loadData();
+		},
+		methods: {
+			goBack() {
+				uni.navigateBack();
+			},
+			tabChange(index) {
+				this.current = index;
+				this.loadData();
+			},
+			isImage(fileName) {
+				const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico']
+				const extension = fileName.split('.').pop().toLowerCase();
+				return imageExtensions.includes(extension);
+			},
+			loadData() {
+				let that = this;
+				that.content = [];
+				request.post('/slbResourceDemand/show/my', {
+					userNo: uni.getStorageSync('userNo'),
+					// 状态(1:暂存,2:待处理,3:审核中,4:已通过,9:已拒绝,10:已取消)
+					status: that.current == 1 ? '3' : that.current == 2 ? '1' : that.current == 3 ? '9' : '4',
+					limit: 1000,
+					index:1
+				}).then(res => {
+					if (res&&res.success) {
+						let newList = res.list || [];
+						for (let i = 0; i < newList.length; i++) {
+							newList[i].imgList = [];
+							for (let j = 0; j < newList[i].fileDetailList.length; j++) {
+								if (that.isImage(newList[i].fileDetailList[j].fileName)) {
+									newList[i].imgList.push(newList[i].fileDetailList[j]);
+								}
+							}
+						}
+						that.content = newList;
+						if (newList.length == 0) {
+							that.showEmpty = true;
+						} else {
+							that.showEmpty = false;
+						}
+					}
+
+					console.warn(res);
+				})
+			},
+			dialogConfirm(){
+				let item = this.curItem;
+				let that = this;
+				request.post('/slbResourceDemand/offShelf', {
+					id: item.id,
+					userNo: uni.getStorageSync('userNo'),
+				}).then(res => {
+					if (res.success) {
+						uni.showToast({
+							title: '结束成功'
+						})
+						that.loadData();
+					} else {
+						uni.showToast({
+							title: res.msg,
+							icon: 'none'
+						})
+					}
+				})
+			},
+			finishItem(item){
+				this.curItem = item;
+				this.$refs.alertDialog.open()
+				
+				
+			},
+			showImg(items, index) {
+				let urls = [];
+				for (let i = 0; i < items.length; i++) {
+					urls.push(items[i].ftpUrl);
+				}
+
+				// 预览图片
+				uni.previewImage({
+					urls: urls,
+					current: index,
+
+				});
+			},
+			clickLink(url){
+				uni.navigateTo({
+					url:'/pages/webview/web-view?url='+url,
+				})
+			}
+
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		// border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+
+	/* 文章内容 start*/
+	.blogger {
+		&__item {
+			padding: 30rpx;
+		}
+
+		&__author {
+			&__btn {
+				margin-right: -12rpx;
+				opacity: 0.5;
+			}
+		}
+
+		&__desc {
+			line-height: 30rpx;
+
+			&__label {
+
+				color: #1D2541;
+				background-color: #F3F2F7;
+				border-radius: 10rpx;
+				font-size: 22rpx;
+
+				padding: 5rpx 15rpx;
+				margin: 5rpx 18rpx 0 0;
+
+				&--prefix {
+					font-size: 24rpx;
+					color: #1D2541;
+					padding-right: 10rpx;
+				}
+			}
+
+			&__content {
+				line-height: 50rpx;
+			}
+		}
+
+		&__content {
+			margin-top: 18rpx;
+			padding-right: 18rpx;
+
+			&__data {
+				line-height: 46rpx;
+				text-align: justify;
+				overflow: hidden;
+				transition: all 0.25s ease-in-out;
+
+			}
+
+			&__status {
+				margin-top: 10rpx;
+				font-size: 26rpx;
+				color: #82B2FF;
+			}
+		}
+
+		&__main-image {
+			border: 1rpx solid #F8F7F8;
+			border-radius: 16rpx;
+
+			&--1 {
+				max-width: 80%;
+				max-height: 300rpx;
+			}
+
+			&--2 {
+				max-width: 260rpx;
+				max-height: 260rpx;
+			}
+
+			&--3 {
+				height: 212rpx;
+				width: 100%;
+			}
+		}
+
+		&__count-icon {
+			font-size: 40rpx;
+			padding-right: 5rpx;
+		}
+
+		&__ad {
+			width: 100%;
+			height: 500rpx;
+			transform: translate3d(0px, 0px, 0px) !important;
+
+			::v-deep .uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			.uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			&__item {
+				position: absolute;
+				width: 100%;
+				height: 100%;
+				transform-origin: left center;
+				transform: translate3d(100%, 0px, 0px) scale(1) !important;
+				transition: transform 0.25s ease-in-out;
+				z-index: 1;
+
+				&--0 {
+					transform: translate3d(0%, 0px, 0px) scale(1) !important;
+					z-index: 4;
+				}
+
+				&--1 {
+					transform: translate3d(13%, 0px, 0px) scale(0.9) !important;
+					z-index: 3;
+				}
+
+				&--2 {
+					transform: translate3d(26%, 0px, 0px) scale(0.8) !important;
+					z-index: 2;
+				}
+			}
+
+			&__content {
+				border-radius: 40rpx;
+				width: 640rpx;
+				height: 500rpx;
+				overflow: hidden;
+			}
+
+			&__image {
+				width: 100%;
+				height: 100%;
+			}
+		}
+	}
+
+	/* 文章内容 end*/
+
+	/* 间隔线 start*/
+	.tn-strip-bottom {
+		width: 100%;
+		border-bottom: 20rpx solid rgba(241, 241, 241, 0.8);
+	}
+	
+	/* 间隔线 end*/
+</style>

+ 515 - 0
pages/mine/share.vue

@@ -0,0 +1,515 @@
+<template>
+	<view>
+		<tn-nav-bar fixed customBack>
+			<view slot="back" class='tn-custom-nav-bar__back' @click="goBack">
+				<text class='icon tn-icon-left'></text>
+
+			</view>
+			<view slot="default">
+				<text>我的分享</text>
+			</view>
+		</tn-nav-bar>
+		<view :style="{paddingTop: vuex_custom_bar_height + 'px'}">
+			<uv-sticky  :offsetTop="vuex_custom_bar_height + 'px'" bgColor="#ffffff">
+			<tn-tabs-swiper :list="list" :isScroll="false" :current="current" name="tab-name"
+				@change="change"  style="border-bottom: 1rpx solid #f1f1f1cc;"></tn-tabs-swiper>
+		</uv-sticky>
+		<view class="tn-flex tn-flex-direction-column  tn-margin-top-sm tn-margin-bottom">
+
+			<!-- 图文信息 -->
+			<block v-for="(item,index) in content">
+				<view class="blogger__item" :key="index">
+					<view class="blogger__author tn-flex tn-flex-row-between tn-flex-col-center">
+						<view class="justify__author__info" @click="tn('')">
+							<view class="tn-flex tn-flex-row-center">
+								<view class="tn-flex tn-flex-row-center tn-flex-col-center">
+									<!-- <view class="">
+		                <tn-avatar
+		                  class=""
+		                  shape="circle"
+		                  :src="item.userAvatar"
+		                  size="lg">
+		                </tn-avatar>
+		              </view> -->
+									<view class="tn-padding-right tn-text-ellipsis">
+										<view class="tn-padding-right  tn-text-bold tn-text-lg">
+											{{ item.company||'个人/'+(item.contactNickName||item.contactPerson)}} </view>
+										<!-- <view class="tn-padding-right tn-padding-left-sm tn-padding-top-xs tn-color-gray">{{ item.date }}</view> -->
+									</view>
+								</view>
+							</view>
+						</view>
+						<view v-if="item.status=='4'&&item.showFlag=='否'"
+										class="blogger__author__btn justify-content-item tn-flex-col-center tn-flex-row-center">
+										<text class="" style="background: #3F51B542;font-size: 12px;
+						padding: 8px;
+						color: #333333;
+						border-radius: 24px;
+						 ">已下架</text>
+									</view>
+								 
+						<!-- <view class="blogger__author__btn justify-content-item tn-flex-col-center tn-flex-row-center">
+		          <text class="tn-icon-more-vertical tn-color-gray tn-text-bold tn-text-xxl"></text>
+		        </view> -->
+					</view>
+
+					<view
+						class="blogger__desc tn-margin-top-sm tn-margin-bottom-sm tn-text-justify tn-flex-col-center tn-flex-row-left"
+						@click="tn('')">
+						<!-- <view v-for="(label_item,label_index) in item.label" :key="label_index" class="blogger__desc__label tn-float-left tn-margin-right">
+		          <text class="blogger__desc__label--prefix tn-icon-topics-fill"></text> 
+		          <text class="tn-text-df">{{ label_item }}</text>
+		        </view> -->
+				      <tn-tag margin="-4px 4px 0 0" backgroundColor="#3a96d733" v-if="item.brand" fontColor="#3a96d7" shape="circle">{{ item.brand }}</tn-tag>
+				       
+						<!-- 不用限制长度了,因为发布的时候限制长度了-->
+						<text v-if="item.content"
+							class="blogger__desc__content tn-flex-1 tn-text-justify tn-text-df">{{ item.content }}</text>
+					</view>
+
+					<!-- 内容太多疲劳了-->
+					<view v-if="item.shareExt&&item.shareExt.length>0" class="blogger__content" :id="`blogger__content--${index}`">
+						<uni-table border stripe emptyText="暂无更多数据">
+							<!-- 表头行 -->
+							<uni-tr>
+								<uni-th align="center">产品名称</uni-th>
+								<uni-th align="center">规格型号</uni-th>
+								<uni-th align="left">产品介绍</uni-th>
+							</uni-tr>
+							<!-- 表格数据行 -->
+							<uni-tr v-for="extItem in item.shareExt">
+								<uni-td>{{extItem.prodName}}</uni-td>
+								<uni-td>{{extItem.prodSpec}}</uni-td>
+								<uni-td>{{extItem.prodDesc}}</uni-td>
+							</uni-tr>
+						</uni-table>
+					</view>
+
+					<block v-if="item.imgList">
+						<view v-if="[1,2,4].indexOf(item.imgList.length) != -1" class="tn-padding-top-xs"
+							@click="tn('')">
+							<image v-for="(image_item,image_index) in item.imgList" :key="image_index"
+								class="blogger__main-image" :class="{
+		        'blogger__main-image--1 tn-margin-bottom-sm': item.imgList.length === 1,
+		        'blogger__main-image--2 tn-margin-right-sm tn-margin-bottom-sm': item.imgList.length === 2 || item.imgList.length === 4
+		      }" :src="image_item.ftpUrl" mode="scaleToFill" @click="showImg(item.imgList,image_index)"></image>
+						</view>
+						<view v-else class="tn-padding-top-xs">
+							<tn-grid hoverClass="none" :col="3">
+								<block v-for="(image_item,image_index) in item.imgList" :key="image_index">
+									<!-- #ifndef MP-WEIXIN -->
+									<tn-grid-item style="width: 30%;margin: 10rpx;">
+										<image class="blogger__main-image blogger__main-image--3"
+											:src="image_item.ftpUrl" mode="scaleToFill" @click="showImg(item.imgList,image_index)"></image>
+									</tn-grid-item>
+									<!-- #endif-->
+									<!-- #ifdef MP-WEIXIN -->
+									<tn-grid-item style="width: 30%;margin: 10rpx;">
+										<image class="blogger__main-image blogger__main-image--3"
+											:src="image_item.ftpUrl" mode="scaleToFill" @click="showImg(item.imgList,image_index)"></image>
+									</tn-grid-item>
+									<!-- #endif-->
+								</block>
+							</tn-grid>
+						</view>
+					</block>
+					<view v-for="file in item.fileDetailList" v-if="!isImage(file.fileName)">
+						<view>
+							<text class="tn-icon-link"></text>
+							<view style="display: inline-block;margin-left:8px" @click="clickLink(file.ftpUrl)">
+								{{file.fileName}}</view>
+
+						</view>
+					</view>
+
+					<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xs">
+						<view class="justify-content-item tn-color-gray tn-text-center">
+							<view class="tn-padding-right   tn-padding-top-xs tn-color-gray">
+								{{ item.createTime|formatDate }}
+							</view>
+
+						</view>
+						<view class="justify-content-item tn-flex tn-flex-col-center">
+							<view class="justify-content-item tn-flex tn-flex-col-center" v-if="item.showFlag!='否'">
+								<tn-button shadow shape="round" fontColor="tn-color-white" backgroundColor="tn-bg-blue" :fontSize="24" height="auto" padding="10rpx 18rpx" @click="finishItem(item)">下架</tn-button>
+								 
+							</view>
+						</view>
+					</view>
+				</view>
+
+				<!-- 边距间隔 -->
+				<view class="tn-strip-bottom" v-if="index != content.length - 1"></view>
+			</block>
+
+			<!-- 边距间隔 -->
+			<view class="tn-strip-bottom"></view>
+
+			<!-- 广告 -->
+
+
+		</view>
+		</view>
+		 <tn-modal v-model="showModel" @click="clickModel" :title="titleModel" :content="contentModel" :button="buttonModel"></tn-modal>
+
+		<view class='tn-tabbar-height'></view>
+
+
+		<view v-if="showEmpty" style="margin-top: 32vh;">
+			<tn-empty mode="data"></tn-empty>
+		</view>
+
+
+
+	</view>
+</template>
+
+<script>
+	import request from '../../utils/request'
+
+	export default {
+		data() {
+			return {
+				showModel:false,
+				titleModel:'提示',
+				contentModel:'下架之后内容不再展示在共享页面里, 是否继续?',
+				buttonModel:[{
+				text: '取消',
+			 
+				plain: true,
+				shape: 'round'
+			  },
+			  {
+				text: '继续',
+				backgroundColor: 'tn-bg-indigo',
+				fontColor: '#FFFFFF'
+			  }],
+				list: [{
+					'tab-name': '已通过'
+				}, {
+					'tab-name': '审核中'
+				}, {
+					'tab-name': '暂存'
+				}, {
+					'tab-name': '已拒绝'
+				}],
+				current: 0,
+				showEmpty: false,
+				content:[],
+				curItem:{},
+			}
+		},
+		filters: {
+			formatDate(value) {
+				if (!value) return '';
+				const date = new Date(value);
+				const today = new Date();
+				const yesterday = new Date(today); // 昨天的日期
+				yesterday.setDate(yesterday.getDate() - 1); // 将昨天的日期设置为前一天
+
+				if (date.getFullYear() == today.getFullYear() && date.getMonth() == today.getMonth() && date.getDate() ==
+					today.getDate()) {
+					return '今天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				if (date.getFullYear() == yesterday.getFullYear() && date.getMonth() == yesterday.getMonth() && date
+					.getDate() == yesterday.getDate()) {
+					return '昨天 ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+						.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+				}
+				return date.toLocaleDateString() + ' ' + (date.getHours() > 9 ? '' : '0') + date.getHours() + ':' + (date
+					.getMinutes() > 9 ? '' : '0') + date.getMinutes(); // 根据需要格式化日期
+			},
+
+
+		},
+		onShow() {
+			this.loadData();
+		},
+		methods: {
+			goBack() {
+				uni.navigateBack();
+			},
+			change(index) {
+				this.current = index;
+				this.loadData();
+			},
+			isImage(fileName) {
+				const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico']
+				const extension = fileName.split('.').pop().toLowerCase();
+				return imageExtensions.includes(extension);
+			},
+			loadData() {
+
+				let that = this;
+				that.content = [];
+				request.post('/slbResourceShare/show/my', {
+					userNo: uni.getStorageSync('userNo'),
+					// 状态(1:暂存,2:待处理,3:审核中,4:已通过,9:已拒绝,10:已取消)
+					status: that.current == 1 ? '3' : that.current == 2 ? '1' : that.current == 3 ? '9' : '4',
+
+				}).then(res => {
+					let newList = res.list || [];
+
+					if (res.success) {
+						let newList = res.list || [];
+						for (let i = 0; i < newList.length; i++) {
+							newList[i].imgList = [];
+							for (let j = 0; j < newList[i].fileDetailList.length; j++) {
+								if (that.isImage(newList[i].fileDetailList[j].fileName)) {
+									newList[i].imgList.push(newList[i].fileDetailList[j]);
+								}
+							}
+						}
+						that.content = newList;
+						if (newList.length == 0) {
+							that.showEmpty = true;
+						} else {
+							that.showEmpty = false;
+						}
+					}
+
+
+					console.warn(res);
+				})
+			},
+			showImg(items, index) {
+				let urls = [];
+				for (let i = 0; i < items.length; i++) {
+					urls.push(items[i].ftpUrl);
+				}
+			
+				// 预览图片
+				uni.previewImage({
+					urls: urls,
+					current: index,
+			
+				});
+			},
+			clickLink(url){
+				uni.navigateTo({
+					url:'/pages/webview/web-view?url='+url,
+				})
+			},
+			finishItem(item){
+				this.curItem = item;
+				this.showModel = true;
+				return false;
+			},
+			clickModel(e){
+				if(e.index=1){
+					this.showModel = false;
+					this.finishNext(this.curItem)
+				}else{
+					this.showModel = false;
+					
+				}
+			},
+			finishNext(item){
+				let that = this;
+				request.post('/slbResourceShare/offShelf', {
+					id: item.id,
+					userNo: uni.getStorageSync('userNo'),
+				}).then(res => {
+					if (res.success) {
+						uni.showToast({
+							title: '下架成功'
+						})
+						that.loadData();
+					} else {
+						uni.showToast({
+							title: res.msg,
+							icon: 'none'
+						})
+					}
+				})
+				
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	/* 胶囊*/
+	.tn-custom-nav-bar__back {
+		width: 60%;
+		height: 100%;
+		position: relative;
+		display: flex;
+		justify-content: space-evenly;
+		align-items: center;
+		box-sizing: border-box;
+		// background-color: rgba(0, 0, 0, 0.15);
+		border-radius: 1000rpx;
+		// border: 1rpx solid rgba(255, 255, 255, 0.5);
+		// color: #FFFFFF;
+		font-size: 18px;
+
+		.icon {
+			display: block;
+			flex: 1;
+			margin: auto;
+			text-align: center;
+		}
+
+		&:before {
+			content: " ";
+			width: 1rpx;
+			height: 110%;
+			position: absolute;
+			top: 22.5%;
+			left: 0;
+			right: 0;
+			margin: auto;
+			transform: scale(0.5);
+			transform-origin: 0 0;
+			pointer-events: none;
+			box-sizing: border-box;
+			opacity: 0.7;
+			background-color: #FFFFFF;
+		}
+	}
+
+	/* 文章内容 start*/
+	.blogger {
+		&__item {
+			padding: 30rpx;
+		}
+
+		&__author {
+			&__btn {
+				margin-right: -12rpx;
+				opacity: 0.5;
+			}
+		}
+
+		&__desc {
+			line-height: 30rpx;
+
+			&__label {
+
+				color: #1D2541;
+				background-color: #F3F2F7;
+				border-radius: 10rpx;
+				font-size: 22rpx;
+
+				padding: 5rpx 15rpx;
+				margin: 5rpx 18rpx 0 0;
+
+				&--prefix {
+					font-size: 24rpx;
+					color: #1D2541;
+					padding-right: 10rpx;
+				}
+			}
+
+			&__content {
+				line-height: 50rpx;
+			}
+		}
+
+		&__content {
+			margin-top: 18rpx;
+			padding-right: 18rpx;
+
+			&__data {
+				line-height: 46rpx;
+				text-align: justify;
+				overflow: hidden;
+				transition: all 0.25s ease-in-out;
+
+			}
+
+			&__status {
+				margin-top: 10rpx;
+				font-size: 26rpx;
+				color: #82B2FF;
+			}
+		}
+
+		&__main-image {
+			border: 1rpx solid #F8F7F8;
+			border-radius: 16rpx;
+
+			&--1 {
+				max-width: 80%;
+				max-height: 300rpx;
+			}
+
+			&--2 {
+				max-width: 260rpx;
+				max-height: 260rpx;
+			}
+
+			&--3 {
+				height: 212rpx;
+				width: 100%;
+			}
+		}
+
+		&__count-icon {
+			font-size: 40rpx;
+			padding-right: 5rpx;
+		}
+
+		&__ad {
+			width: 100%;
+			height: 500rpx;
+			transform: translate3d(0px, 0px, 0px) !important;
+
+			::v-deep .uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			.uni-swiper-slide-frame {
+				transform: translate3d(0px, 0px, 0px) !important;
+			}
+
+			&__item {
+				position: absolute;
+				width: 100%;
+				height: 100%;
+				transform-origin: left center;
+				transform: translate3d(100%, 0px, 0px) scale(1) !important;
+				transition: transform 0.25s ease-in-out;
+				z-index: 1;
+
+				&--0 {
+					transform: translate3d(0%, 0px, 0px) scale(1) !important;
+					z-index: 4;
+				}
+
+				&--1 {
+					transform: translate3d(13%, 0px, 0px) scale(0.9) !important;
+					z-index: 3;
+				}
+
+				&--2 {
+					transform: translate3d(26%, 0px, 0px) scale(0.8) !important;
+					z-index: 2;
+				}
+			}
+
+			&__content {
+				border-radius: 40rpx;
+				width: 640rpx;
+				height: 500rpx;
+				overflow: hidden;
+			}
+
+			&__image {
+				width: 100%;
+				height: 100%;
+			}
+		}
+	}
+
+	/* 文章内容 end*/
+	/* 间隔线 start*/
+	.tn-strip-bottom {
+		width: 100%;
+		border-bottom: 20rpx solid rgba(241, 241, 241, 0.8);
+	}
+	
+	/* 间隔线 end*/
+</style>

+ 22 - 0
pages/webview/index.vue

@@ -0,0 +1,22 @@
+<template>
+	<view>
+		<web-view src="https://mp.weixin.qq.com/mp/wapreportwxadevlog?action=get_page&appid=wxbec0825602c34690#wechat_redirect"></web-view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			
+		}
+	}
+</script>
+
+<style>
+
+</style>

+ 46 - 0
pages/webview/web-view.vue

@@ -0,0 +1,46 @@
+<template>
+	<view>
+		<web-view :src="web_url"></web-view>
+
+		<!-- 公共 -->
+
+	</view>
+</template>
+<script>
+	const app = getApp();
+	export default {
+		data() {
+			return {
+				web_url: null
+			};
+		},
+		onLoad(params) {
+			// 调用公共事件方法
+			// url处理
+			var url = decodeURIComponent(params.url) || null;
+			// if (url != null) {
+			//     // token处理
+			//     if (url.indexOf('{token}') >= 0) {
+			//         var user = app.globalData.get_user_cache_info();
+			//         var token = user == null ? null : (user.token || null);
+			//         if (token != null) {
+			//             url = url.replace(/{token}/ig, token);
+			//         }
+			//     }
+			// }
+
+			this.web_url = url;
+
+
+		},
+
+		onShow() {
+			// 调用公共事件方法
+
+			// 公共onshow事件
+
+		},
+
+		methods: {}
+	};
+</script>

+ 55 - 0
plugins/README.md

@@ -0,0 +1,55 @@
+uni.request 进行封装,允许自定义拦截请求 用法与axios基本相同,引入axios 中 cancel文件,允许请求时取消请求
+### 初始化请求
+let instance = unirequest.create({
+	baseURL: '',
+	timeout: 10000
+})
+### 拦截器配置
+// 请求拦截器
+instance.interceptors.request.use(
+	config => {
+		config.header['Content-Type']= 'application/x-www-form-urlencoded';
+		config.cancelToken = new unirequest.CancelToken(function executor(c) {
+			setCancel = c; //记录当前请求
+		});
+		
+		return config
+	})
+// 响应拦截器
+instance.interceptors.response.use(
+	response => {
+		// console.log('响应成功', response)
+		return response
+	})
+### 请求主体  
+* config={url,method,data}
+instance.request(config).then(res => {
+		if (res.statusCode) {
+			if (res.statusCode === 200) {
+				return Promise.resolve(res.data);
+			} else {
+				return Promise.reject(res.data);
+			}
+		} else {
+			return Promise.reject(res.data);
+		}
+	}).catch(err => {
+		return Promise.reject(err);
+	})
+	
+***
+new Proxy(target, handler)   //可以理解为目标对象之前做一层拦截,所有访问的对象必须先通过这层拦截
+Proxy支持拦截的操作,一共有13种:
+get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
+set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
+has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
+deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
+ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
+getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
+defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
+preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
+getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
+isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
+setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
+apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(object,...)。
+construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

+ 65 - 0
plugins/cancel.js

@@ -0,0 +1,65 @@
+function Cancel(message) {
+  this.message = message;
+	console.log('1 cancel')
+}
+
+Cancel.prototype.toString = function toString() {
+  return 'Cancel' + (this.message ? ': ' + this.message : '');
+};
+
+Cancel.prototype.__CANCEL__ = true;
+
+function CancelToken(executor) {
+  if (typeof executor !== 'function') {
+    throw new TypeError('executor must be a function.');
+  }
+	//console.log('2 CancelToken')
+  var resolvePromise;
+  this.promise = new Promise(function promiseExecutor(resolve) {
+    resolvePromise = resolve;
+		//console.log('3 CancelToken')
+  });
+
+  var token = this;
+  executor(function cancel(message) {
+		//console.log('4 CancelToken')
+    if (token.reason) {
+      // Cancellation has already been requested
+      return;
+    }
+		//console.log('5 CancelToken')
+    token.reason = new Cancel(message);
+    resolvePromise(token.reason);
+  });
+}
+
+/**
+ * Throws a `Cancel` if cancellation has been requested.
+ */
+CancelToken.prototype.throwIfRequested = function throwIfRequested() {
+	//console.log('6 CancelToken')
+  if (this.reason) {
+    throw this.reason;
+  }
+};
+
+/**
+ * Returns an object that contains a new `CancelToken` and a function that, when called,
+ * cancels the `CancelToken`.
+ */
+CancelToken.source = function source() {
+  var cancel;
+	console.log('7 CancelToken')
+  var token = new CancelToken(function executor(c) {
+    cancel = c;
+  });
+  return {
+    token: token,
+    cancel: cancel
+  };
+};
+function isCancel(value) {
+	console.log('8 isCancel')
+  return !!(value && value.__CANCEL__);
+};
+export {CancelToken,Cancel,isCancel}

+ 141 - 0
plugins/uni_request.js

@@ -0,0 +1,141 @@
+import {CancelToken,Cancel,isCancel} from './cancel.js'
+class Request {
+	constructor(args) {
+		this.defaults = {headers:{'Accept': 'application/json',
+			'Content-Type': 'application/x-www-form-urlencoded'},debug:false,...args} || {};
+		this.timer = null;
+		this.requestTask = null;
+		this.aborted = false;
+		this.timeoutCancel = false;
+		this.Cancel = Cancel;
+		this.CancelToken = CancelToken;
+		this.isCancel = isCancel;
+	}
+	interceptors = { // 拦截器
+		request: {
+			interceptors: [],
+			use(fn) {
+				this.interceptors.push(fn)		
+			},
+			async intercept(config) {
+				for (let i = 0; i < this.interceptors.length; i++) {
+					config = await this.interceptors[i](config)
+				}		
+				return config
+			}
+		},
+		response: {
+			interceptors: [],
+			use(fn) {
+				this.interceptors.push(fn)
+			},
+			async intercept(resolve, response) {
+				for (let i = 0; i < this.interceptors.length; i++) {
+					response = await this.interceptors[i](response)
+				}
+				return resolve(response)
+
+			}
+		}
+	}
+	abort = () => {
+		this.aborted = true;
+		this.requestTask ? this.requestTask.abort() : ''
+	}
+
+	onerror = async (options={...args}) => {
+		let obj = {
+			url: options.url,
+			method: options.method,
+			mes: options.str
+		};
+		if(options.cancel){
+			obj.mes=options.cancel.message || '取消请求'
+		}
+		if(this.defaults.debug){
+			console.log(obj.mes, options)
+		}
+		return obj;
+	}
+	request(options) {
+		const _this = this;
+		let {method, url, data,cancelToken} = options;
+		let config = {
+			url: this.defaults.baseURL + url,
+			method: method.toUpperCase() || 'GET',
+			header: this.defaults.headers || {},
+			data: data || {},
+		}
+		return new Proxy(new Promise((resolve, reject) => {
+			_this.interceptors.request.intercept(config).then(async (res) => {
+				if (_this.aborted) { // 如果请求已被取消,停止执行,返回 reject
+					const str = '主动中断请求';
+					await _this.onerror({config,str})
+					return reject(str)
+				}
+				if (config.cancelToken) {
+				  // Handle cancellation
+				  config.cancelToken.promise.then(async function onCanceled(cancel) {
+						clearTimeout(_this.timer)
+						_this.requestTask = null;
+						await _this.onerror({config, cancel})
+				    return reject(cancel);
+				  });
+				}
+				_this.requestTask = uni.request({
+					...config,
+					success: async (res) => {
+						clearTimeout(_this.timer) // 清除检测超时定时器
+						_this.interceptors.response.intercept(resolve, res) // 执行响应拦截器
+					},
+					fail: async (error) => {
+						console.log(error)
+						clearTimeout(_this.timer) // 清除检测超时定时器
+						let failTimer = setTimeout(async () => {
+							// 区分失败原因为超时或其它原因
+							const str = '网络异常或URL无效'
+							if (!_this.timeoutCancel) {
+								await _this.onerror({config, str})
+								clearTimeout(failTimer)
+								reject(str)
+							}
+						}, 300)
+
+					}
+				});
+				_this.timer = setTimeout(async () => { // 请求超时执行方法
+				if (config.cancelToken) {return}
+					_this.requestTask.abort(); // 执行取消请求方法
+					_this.timeoutCancel = true;
+					const str = `网络请求时间超时,当前设置响应时间为${_this.defaults.timeout}`
+					await _this.onerror({config, str})
+					reject(str)
+				}, _this.defaults.timeout || 12345) // 设定检测超时定时器
+			})
+
+
+		}), {
+			get: function(target, key, receiver) {
+				// console.log(`getting ${key}!`);
+				return key === 'abort' ? _this.abort : Reflect.get(target, key, receiver).bind(target); //传入target 为异步函数需要 .bind(target)
+			},
+			set: function(target, key, value, receiver) {
+				// console.log(`setting ${key}!`);
+				return Reflect.set(target, key, value, receiver);
+			}
+		})
+	}
+}
+
+// class CreateInstance extends Request {
+// 	constructor(args) {
+// 		super();
+// 		super.defaults = args;
+// 	}
+// }
+let unirequest = new Request();
+unirequest.create = function(args) {
+	return new Request(args)
+}
+
+export default unirequest;

BIN
static/author.jpg


BIN
static/bg4.png


BIN
static/callus.png


BIN
static/logo.png


BIN
static/me2.png


+ 28 - 0
store/$t.mixin.js

@@ -0,0 +1,28 @@
+import { mapState } from 'vuex'
+import store from '@/store'
+
+// 尝试将用户在根目录中的store/index.js的vuex的state变量加载到全局变量中
+let $tStoreKey = []
+try {
+  $tStoreKey = store.state ? Object.keys(store.state) : []
+} catch(e) {
+  
+}
+
+module.exports = {
+  beforeCreate() {
+    // 将vuex方法挂在在$t中
+    // 使用方法: 
+    // 修改vuex的state中的user.name变量为速立保小菜 => this.$t.vuex('user.name', '速立保小菜')
+    // 修改vuexde state中的version变量为1.0.1 => this.$t.vuex('version', 1.0.1)
+    this.$t.vuex = (name, value) => {
+      this.$store.commit('$tStore', {
+        name, value
+      })
+    }
+  },
+  computed: {
+    // 将vuex的state中的变量结构到全局混入mixin中
+    ...mapState($tStoreKey)
+  }
+}

+ 138 - 0
store/index.js

@@ -0,0 +1,138 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import storage from '@/utils/storage.js'
+Vue.use(Vuex)
+
+let lifeData = {}
+
+// 尝试获取本地是否存在lifeData变量,第一次启动时不存在
+try {
+	lifeData = uni.getStorageSync('lifeData')
+} catch (e) {
+
+}
+
+// 标记需要永久存储的变量,在每次启动时取出,在state中的变量名
+let saveStateKeys = ['vuex_user']
+
+// 保存变量到本地存储
+const saveLifeData = function(key, value) {
+	// 判断变量是否在存储数组中
+	if (saveStateKeys.indexOf(key) != -1) {
+		// 获取本地存储的lifeData对象,将变量添加到对象中
+		let tmpLifeData = uni.getStorageSync('lifeData')
+		// 第一次启动时不存在,则放一个空对象
+		tmpLifeData = tmpLifeData ? tmpLifeData : {},
+			tmpLifeData[key] = value
+		// 将变量再次放回本地存储中
+		uni.setStorageSync('lifeData', tmpLifeData)
+	}
+}
+
+const store = new Vuex.Store({
+	state: {
+		// 如果上面从本地获取的lifeData对象下有对应的属性,就赋值给state中对应的变量
+		// 加上vuex_前缀,是防止变量名冲突,也让人一目了然
+		vuex_user: lifeData.vuex_user ? lifeData.vuex_user : {
+			name: '速立保'
+		},
+
+		// 如果vuex_version无需保存到本地永久存储,无需lifeData.vuex_version方式
+		// app版本
+		vuex_version: "1.0.0",
+		// 是否使用自定义导航栏
+		vuex_custom_nav_bar: true,
+		// 状态栏高度
+		vuex_status_bar_height: 0,
+		// 自定义导航栏的高度
+		vuex_custom_bar_height: 0,
+		token: null,
+		openId: null,
+		userInfo: null,
+		expires_time: null,
+		isGOAuth: false, //是否已跳转至登录界面,防止路由重复注入
+		isFirstLoad: true, //是否第一次加载 第一次加载未认证不需要弹框 直接跳转认证
+		authPopupShow: false, //x-authorize 弹窗是否显示
+		engAdmin: false
+	},
+	mutations: {
+		LOGOUT(state) {
+			state.token = null;
+			//state.expires_time = null;
+			state.userInfo = null;
+			storage.remove('openId');
+			storage.remove('userInfo');
+			storage.remove('token');
+			storage.remove('memberNo');
+			storage.remove('myUsername');
+			storage.remove('accountName');
+			//storage.remove('expires_time')
+		},
+		LOGIN(state, {
+			token,
+			openId,
+			expires_time
+		}) {
+			state.token = token;
+			state.openId = openId;
+			state.expires_time = expires_time;
+			storage.set('token', token)
+			storage.set('openId', openId)
+			storage.set('expires_time', expires_time)
+		},
+		UPDATE_USERINFO(state, userInfo) {
+			state.userInfo = userInfo;
+			storage.set('userInfo', userInfo);
+			if (userInfo) {
+				storage.set('memberNo', userInfo.member_no);
+				storage.set('accountName', userInfo.account_name);
+				uni.setStorageSync("myUsername", userInfo.account_name);
+				// uni.setStorageSync("myUsername",userInfo.account_name+"m"+userInfo.member_no);
+				state.engAdmin = false;
+				if (userInfo.engAdmin) {
+					state.engAdmin = userInfo.engAdmin;
+				}
+			}
+
+		},
+		SET_GO_AUTH(state, self) {
+			state.isGOAuth = self;
+		},
+		SHOW_AUTH_POPUP_SHOW(state) {
+			state.authPopupShow = true;
+		},
+		HIDE_AUTH_POPUP_SHOW(state) {
+			state.authPopupShow = false;
+		},
+		$tStore(state, payload) {
+			// 判断是否多层调用,state中为对象存在的情况,例如user.info.score = 1
+			let nameArr = payload.name.split('.')
+			let saveKey = ''
+			let len = nameArr.length
+			if (len >= 2) {
+				let obj = state[nameArr[0]]
+				for (let i = 1; i < len - 1; i++) {
+					obj = obj[nameArr[i]]
+				}
+				obj[nameArr[len - 1]] = payload.value
+				saveKey = nameArr[0]
+			} else {
+				// 单层级变量
+				state[payload.name] = payload.value
+				saveKey = payload.name
+			}
+
+			// 保存变量到本地中
+			saveLifeData(saveKey, state[saveKey])
+		}
+	},
+	actions: {},
+	getters: {
+		token: state => state.token,
+		userInfo: state => state.userInfo || {},
+		expires_time: state => state.expires_time,
+		authPopupShow: state => state.authPopupShow
+	}
+})
+
+export default store

+ 38 - 0
template.h5.html

@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <link rel="shortcut icon" type="image/x-icon" href="./static/favicon.ico">
+    <meta name="viewport"
+      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+    <title>
+      <%= htmlWebpackPlugin.options.title %>
+    </title>
+    <style>
+      ::-webkit-scrollbar {
+        display: none;
+      }
+    </style>
+    <!-- 正式发布的时候使用,开发期间不启用。↑ -->
+    <script>
+      document.addEventListener('DOMContentLoaded', function() {
+        document.documentElement.style.fontSize = document.documentElement.clientWidth / 20 + 'px'
+      })
+    </script>
+    <link rel="stylesheet" href="<%= BASE_URL %>static/index.css" />
+  </head>
+  <body>
+    <!-- 该文件为 H5 平台的模板 HTML,并非应用入口。 -->
+    <!-- 请勿在此文件编写页面代码或直接运行此文件。 -->
+    <!-- 详见文档:https://uniapp.dcloud.io/collocation/manifest?id=h5-template -->
+    <noscript>
+      <strong>本站点必须要开启JavaScript才能运行</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+    <script>
+      /*BAIDU_STAT*/
+    </script>
+  </body>
+</html>

+ 4 - 0
tuniao-ui/README.md

@@ -0,0 +1,4 @@
+TuniaoUi for uniApp v1.0.0 | by 速立保 2021-09-01
+仅供开发,如作它用所承受的法律责任一概与作者无关
+
+*使用TuniaoUi开发扩展与插件时,请注明基于tuniao字眼

+ 202 - 0
tuniao-ui/components/tn-action-sheet/tn-action-sheet.vue

@@ -0,0 +1,202 @@
+<template>
+  <view v-if="value" class="tn-action-sheet-class tn-action-sheet">
+    <tn-popup
+      v-model="value"
+      mode="bottom"
+      length="auto"
+      :popup="false"
+      :borderRadius="borderRadius"
+      :maskCloseable="maskCloseable"
+      :safeAreaInsetBottom="safeAreaInsetBottom"
+      :zIndex="elZIndex"
+      @close="close"
+    >
+      <!-- 提示信息 -->
+      <view
+        v-if="tips.text"
+        class="tn-action-sheet__tips tn-border-solid-bottom"
+        :style="[tipsStyle]"
+      >
+        {{tips.text}}
+      </view>
+      <!-- 按钮列表 -->
+      <block v-for="(item, index) in list" :key="index">
+        <view
+          class="tn-action-sheet__item tn-text-ellipsis"
+          :class="[ index < list.length - 1 ? 'tn-border-solid-bottom' : '']"
+          :style="[itemStyle(index)]"
+          hover-class="tn-hover-class"
+          :hover-stay-time="150"
+          @tap="itemClick(index)"
+          @touchmove.stop.prevent
+        >
+          <text>{{item.text}}</text>
+          <text v-if="item.subText" class="tn-action-sheet__item__subtext tn-text-ellipsis">{{item.subText}}</text>
+        </view>
+      </block>
+      
+      <!-- 取消按钮 -->
+      <block v-if="cancelBtn">
+        <view class="tn-action-sheet__cancel--gab"></view>
+        <view
+          class="tn-action-sheet__cancel tn-action-sheet__item"
+          hover-class="tn-hover-class"
+          :hover-stay-time="150"
+          @tap="close"
+        >{{cancelText}}</view>
+      </block>
+      
+    </tn-popup>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-action-sheet',
+    props: {
+      // 通过v-model控制弹出和收起
+      value: {
+        type: Boolean,
+        default: false
+      },
+      // 按钮文字数组,可以自定义颜色和字体大小
+      // return [{
+      // 	text: '确定',
+      //  subText: '这是一个确定按钮',
+      // 	color: '',
+      // 	fontSize: '',
+      //  disabled: true
+      // }]
+      list: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 顶部提示文字
+      tips: {
+        type: Object,
+        default() {
+          return {
+            text: '',
+            color: '',
+            fontSize: 26
+          }
+        }
+      },
+      // 弹出的顶部圆角值
+      borderRadius: {
+        type: Number,
+        default: 0
+      },
+      // 点击遮罩可以关闭
+      maskCloseable: {
+        type: Boolean,
+        default: true
+      },
+      // 底部取消按钮
+      cancelBtn: {
+        type: Boolean,
+        default: true
+      },
+      // 底部取消按钮的文字
+      cancelText: {
+        type: String,
+        default: '取消'
+      },
+      // 开启底部安全区域
+      // 在iPhoneX机型底部添加一定的内边距
+      safeAreaInsetBottom: {
+        type: Boolean,
+        default: false
+      },
+      // z-index值
+      zIndex: {
+        type: Number,
+        default: 0
+      }
+    },
+    computed: {
+      // 顶部提示样式
+      tipsStyle() {
+        let style = {}
+        if (this.tips.color) style.color = this.tips.color
+        if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx'
+        
+        return style
+      },
+      // 操作项目的样式
+      itemStyle() {
+        return (index) => {
+          let style = {}
+          if (this.list[index].color) style.color = this.list[index].color
+          if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx'
+          
+          // 选项被禁用的样式
+          if (this.list[index].disabled) style.color = '#AAAAAA'
+          
+          return style
+        }
+      },
+      elZIndex() {
+        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
+      }
+    },
+    methods: {
+      // 点击取消按钮
+      close() {
+        // 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
+        this.popupClose();
+        this.$emit('close');
+      },
+      // 关闭弹窗
+      popupClose() {
+        this.$emit('input', false)
+      },
+      // 点击对应的item
+      itemClick(index) {
+        // 如果是禁用项则不进行操作
+        if (this.list[index].disabled) return
+        this.$emit('click', index)
+        this.popupClose()
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-action-sheet {
+    &__tips {
+      font-size: 26rpx;
+      text-align: center;
+      padding: 34rpx 0;
+      line-height: 1;
+      color: $tn-content-color;
+    }
+    
+    &__item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      font-size: 32rpx;
+      padding: 34rpx 0;
+      
+      &__subtext {
+        font-size: 24rpx;
+        color: $tn-content-color;
+        margin-top: 20rpx;
+      }
+    }
+    
+    &__cancel {
+      color: $tn-font-color;
+      
+      &--gab {
+        height: 12rpx;
+        background-color: #eaeaec;
+      }
+    }
+  }
+</style>

+ 103 - 0
tuniao-ui/components/tn-avatar-group/tn-avatar-group.vue

@@ -0,0 +1,103 @@
+<template>
+  <view class="tn-avatar-group-class tn-avatar-group">
+    <view v-for="(item, index) in lists" :key="index" class="tn-avatar-group__item" :style="[itemStyle(index)]">
+      <tn-avatar
+        :src="item.src || ''"
+        :text="item.text || ''"
+        :icon="item.icon || ''"
+        :size="size"
+        :shape="shape"
+        :imgMode="imgMode"
+        :border="true"
+        :borderSize="4"
+      ></tn-avatar>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-avatar-group',
+    props: {
+      // 头像列表
+      lists: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 头像类型
+      // square 带圆角正方形 circle 圆形
+      shape: {
+        type: String,
+        default: 'circle'
+      },
+      // 大小
+      // sm 小头像 lg 大头像 xl 加大头像
+      // 如果为其他则认为是直接设置大小
+      size: {
+        type: [Number, String],
+        default: ''
+      },
+      // 当设置为显示头像信息时,
+      // 图片的裁剪模式
+      imgMode: {
+        type: String,
+        default: 'aspectFill'
+      },
+      // 头像之间的遮挡比例
+      // 0.4 代表 40%
+      gap: {
+        type: Number,
+        default: 0.4
+      }
+    },
+    computed: {
+      itemStyle() {
+        return (index) => {
+          let style = {}
+          if (this._checkSizeIsInline()) {
+            switch(this.size) {
+              case 'sm':
+                style.marginLeft = index != 0 ? `${-48 * this.gap}rpx` : ''
+                break
+              case 'lg':
+                style.marginLeft = index != 0 ? `${-96 * this.gap}rpx` : ''
+                break
+              case 'xl':
+                style.marginLeft = index != 0 ? `${-128 * this.gap}rpx` : ''
+                break
+            }
+          } else {
+            const size = Number(this.size.replace(/(px|rpx)/g, '')) || 64
+            style.marginLeft = index != 0 ? `-${size * this.gap}rpx` : ''
+          }
+          return style
+        }
+      }
+    },
+    data() {
+      return {
+        
+      }
+    },
+    methods: {
+      // 检查是否使用内置的大小进行设置
+      _checkSizeIsInline() {
+        if (/(xs|sm|md|lg|xl|xxl)/.test(this.size)) return true
+        else return false
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-avatar-group {
+    display: flex;
+    flex-direction: row;
+    
+    &__item {
+      position: relative;
+    }
+  }
+</style>

+ 298 - 0
tuniao-ui/components/tn-avatar/tn-avatar.vue

@@ -0,0 +1,298 @@
+<template>
+  <view
+    class="tn-avatar-class tn-avatar"
+    :class="[backgroundColorClass,avatarClass]"
+    :style="[avatarStyle]"
+    @tap="click"
+  >
+    <image
+      v-if="showImg"
+      class="tn-avatar__img"
+      :class="[imgClass]"
+      :src="src"
+      :mode="imgMode || 'aspectFill'"
+      @error="loadImageError"
+    ></image>
+    <view v-else class="tn-avatar__text" >
+      <view v-if="text">{{ text }}</view>
+      <view v-else :class="[`tn-icon-${icon}`]"></view>
+    </view>
+    
+    <!-- 角标 -->
+    <tn-badge
+      v-if="badge && (badgeIcon || badgeText)"
+      :radius="badgeSize"
+      :backgroundColor="badgeBgColor"
+      :fontColor="badgeColor"
+      :fontSize="badgeSize - 8"
+      :absolute="true"
+      :top="badgePosition[0]"
+      :right="badgePosition[1]"
+    >
+      <view v-if="badgeIcon && badgeText === ''">
+        <view :class="[`tn-icon-${badgeIcon}`]"></view>
+      </view>
+      <view v-else>
+        {{ badgeText }}
+      </view>
+    </tn-badge>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    mixins: [componentsColorMixin],
+    name: 'tn-avatar',
+    props: {
+      // 序号
+      index: {
+        type: [Number, String],
+        default: 0
+      },
+      // 头像类型
+      // square 带圆角正方形 circle 圆形
+      shape: {
+        type: String,
+        default: 'circle'
+      },
+      // 大小 
+      // sm 小头像 lg 大头像 xl 加大头像
+      // 如果为其他则认为是直接设置大小
+      size: {
+        type: [Number, String],
+        default: ''
+      },
+      // 是否显示阴影
+      shadow: {
+        type: Boolean,
+        default: false
+      },
+      // 是否显示边框
+      border: {
+        type: Boolean,
+        default: false
+      },
+      // 边框颜色
+      borderColor: {
+        type: String,
+        default: 'rgba(0, 0, 0, 0.1)'
+      },
+      // 边框大小, rpx
+      borderSize: {
+        type: Number,
+        default: 2
+      },
+      // 头像路径
+      src: {
+        type: String,
+        default: ''
+      },
+      // 文字
+      text: {
+        type: String,
+        default: ''
+      },
+      // 图标
+      icon: {
+        type: String,
+        default: ''
+      },
+      // 当设置为显示头像信息时,
+      // 图片的裁剪模式
+      imgMode: {
+        type: String,
+        default: 'aspectFill'
+      },
+      // 是否显示角标
+      badge: {
+        type: Boolean,
+        default: false
+      },
+      // 设置显示角标后,角标大小
+      badgeSize: {
+        type: Number,
+        default: 0
+      },
+      // 角标背景颜色
+      badgeBgColor: {
+        type: String,
+        default: '#AAAAAA'
+      },
+      // 角标字体颜色
+      badgeColor: {
+        type: String,
+        default: '#FFFFFF'
+      },
+      // 角标图标
+      badgeIcon: {
+        type: String,
+        default: ''
+      },
+      // 角标文字,优先级比icon高
+      badgeText: {
+        type: String,
+        default: ''
+      },
+      // 角标坐标
+      // [top, right]
+      badgePosition: {
+        type: Array,
+        default() {
+          return [0, 0]
+        }
+      }
+    },
+    data() {
+      return {
+        // 图片显示是否发生错误
+        imgLoadError: false
+      }
+    },
+    computed: {
+      showImg() {
+        // 如果设置了图片地址,则为显示图片,否则为显示文本
+        return this.text === '' && this.icon === ''
+      },
+      avatarClass() {
+        let clazz = ''
+        clazz += ` tn-avatar--${this.shape}`
+        
+        if (this._checkSizeIsInline()) {
+          clazz += ` tn-avatar--${this.size}`
+        }
+        
+        if (this.shadow) {
+          clazz += ' tn-avatar--shadow'
+        }
+        
+        return clazz
+      },
+      avatarStyle() {
+        let style = {}
+        
+        if (this.backgroundColorStyle) {
+          style.background = this.backgroundColorStyle
+        } else if (this.shadow && this.showImg) {
+          style.backgroundImage = `url(${this.src})`
+        }
+        
+        if (this.border) {
+          style.border = `${this.borderSize}rpx solid ${this.borderColor}`
+        }
+        
+        if (!this._checkSizeIsInline()) {
+          style.width = this.size
+          style.height = this.size
+        }
+        
+        return style
+      },
+      imgClass() {
+        let clazz = ''
+        clazz += ` tn-avatar__img--${this.shape}`
+        
+        return clazz
+      }
+    },
+    methods: {
+      // 加载图片失败
+      loadImageError() {
+        this.imgLoadError = true
+      },
+      // 点击事件
+      click() {
+        this.$emit("click", this.index)
+      },
+      
+      // 检查是否使用内置的大小进行设置
+      _checkSizeIsInline() {
+        if (/^(xs|sm|md|lg|xl|xxl)$/.test(this.size)) return true
+        else return false
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-avatar {
+    /* #ifndef APP-NVUE */
+    display: inline-flex;
+    /* #endif */
+    margin: 0;
+    padding: 0;
+    text-align: center;
+    align-items: center;
+    justify-content: center;
+    background-color: $tn-font-holder-color;
+    // color: #FFFFFF;
+    white-space: nowrap;
+    position: relative;
+    width: 64rpx;
+    height: 64rpx;
+    z-index: 1;
+    
+    &--sm {
+      width: 48rpx;
+      height: 48rpx;
+    }
+    &--lg {
+      width: 96rpx;
+      height: 96rpx;
+    }
+    &--xl {
+      width: 128rpx;
+      height: 128rpx;
+    }
+    
+    &--square {
+      border-radius: 10rpx;
+    }
+    
+    &--circle {
+      border-radius: 5000rpx;
+    }
+    
+    &--shadow {
+      position: relative;
+      
+      &::after {
+        content: " ";
+        display: block;
+        background: inherit;
+        filter: blur(10rpx);
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        top: 10rpx;
+        left: 10rpx;
+        z-index: -1;
+        opacity: 0.4;
+        transform-origin: 0 0;
+        border-radius: inherit;
+        transform: scale(1, 1);
+      }
+    }
+    
+    &__img {
+      width: 100%;
+      height: 100%;
+      
+      &--square {
+        border-radius: 10rpx;
+      }
+      
+      &--circle {
+        border-radius: 5000rpx;
+      }
+    }
+    
+    &__text {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+</style>

+ 173 - 0
tuniao-ui/components/tn-badge/tn-badge.vue

@@ -0,0 +1,173 @@
+<template>
+  <view
+    class="tn-badge-class tn-badge"
+    :class="[
+      backgroundColorClass,
+      fontColorClass,
+      badgeClass
+    ]"
+    :style="[badgeStyle]"
+    @click="handleClick"
+  >
+    <slot v-if="!dot"></slot>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    mixins: [componentsColorMixin],
+    name: 'tn-badge',
+    props: {
+      // 序号
+      index: {
+        type: [Number, String],
+        default: '0'
+      },
+      // 徽章的大小 rpx
+      radius: {
+        type: Number,
+        default: 0
+      },
+      // 内边距
+      padding: {
+        type: String,
+        default: ''
+      },
+      // 外边距
+      margin: {
+        type: String,
+        default: ''
+      },
+      // 是否为一个点
+      dot: {
+        type: Boolean,
+        default: false
+      },
+      // 是否使用绝对定位
+      absolute: {
+        type: Boolean,
+        default: false
+      },
+      // top
+      top: {
+        type: [String, Number],
+        default: ''
+      },
+      // right
+      right: {
+        type: [String, Number],
+        default: ''
+      },
+      // 居中 对齐右上角
+      translateCenter: {
+        type: Boolean,
+        default: true
+      }
+    },
+    computed: {
+      badgeClass() {
+        let clazz = ''
+        if (this.dot) {
+          clazz += ' tn-badge--dot'
+        }
+        if (this.absolute) {
+          clazz += ' tn-badge--absolute'
+          
+          if (this.translateCenter) {
+            clazz += ' tn-badge--center-position'
+          }
+        }
+        
+        return clazz
+      },
+      badgeStyle() {
+        let style = {}
+        
+        if (this.radius !== 0) {
+          style.width = this.radius + 'rpx'
+          style.height = this.radius + 'rpx'
+          style.lineHeight = this.radius + 'rpx'
+          
+          // style.borderRadius = (this.radius * 8) + 'rpx'
+        }
+        
+        if (this.padding) {
+          style.padding = this.padding
+        }
+        if (this.margin) {
+          style.margin = this.margin
+        }
+        if (this.fontColorStyle) {
+          style.color = this.fontColorStyle
+        }
+        if (this.fontSize) {
+          style.fontSize = this.fontSize + this.fontUnit
+        }
+        
+        if (this.backgroundColorStyle) {
+          style.backgroundColor = this.backgroundColorStyle
+        }
+        
+        if (this.top) {
+          style.top = this.$t.string.getLengthUnitValue(this.top)
+        }
+        if (this.right) {
+          style.right = this.$t.string.getLengthUnitValue(this.right)
+        }
+        
+        return style
+      },
+      
+    },
+    data() {
+      return {
+        
+      }
+    },
+    methods: {
+      // 处理点击事件
+      handleClick() {
+        this.$emit('click', {
+          index: Number(this.index)
+        })
+        this.$emit('tap', {
+          index: Number(this.index)
+        })
+      },
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-badge {
+    width: auto;
+    height: auto;
+    box-sizing: border-box;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 10;
+    font-size: 20rpx;
+    background-color: #FFFFFF;
+    // color: #FFFFFF;
+    border-radius: 100rpx;
+    padding: 4rpx 8rpx;
+    line-height: initial;
+    
+    &--dot {
+      width: 8rpx;
+      height: 8rpx;
+      border-radius: 50%;
+      padding: 0;
+    }
+    &--absolute {
+      position: absolute;
+      top: 0;
+      right: 0;
+    }
+    &--center-position {
+      transform: translate(50%, -50%);
+    }
+  }
+</style>

+ 302 - 0
tuniao-ui/components/tn-button/tn-button.vue

@@ -0,0 +1,302 @@
+<template>
+  <button
+    class="tn-btn-class tn-btn"
+    :class="[
+      buttonClass,
+      backgroundColorClass,
+      fontColorClass
+    ]"
+    :style="[buttonStyle]"
+    hover-class="tn-hover"
+    :loading="loading"
+    :disabled="disabled"
+    :form-type="formType"
+    :open-type="openType"
+    @getuserinfo="handleGetUserInfo"
+    @getphonenumber="handleGetPhoneNumber"
+    @contact="handleContact"
+    @error="handleError"
+    @tap="handleClick"
+  >
+    <slot></slot>
+  </button>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    mixins: [componentsColorMixin],
+    name: "tn-button",
+    // 解决再微信小程序种,自定义按钮无法触发bindsubmit
+    behaviors: ['wx://form-field-button'],
+    props: {
+      // 按钮索引,用于区分多个按钮
+      index: {
+        type: [Number, String],
+        default: 0
+      },
+      // 按钮形状 default 默认 round 圆角 icon 图标按钮
+      shape: {
+        type: String,
+        default: 'default'
+      },
+      // 是否加阴影
+      shadow: {
+        type: Boolean,
+        default: false
+      },
+      // 宽度 rpx或%
+      width: {
+        type: String,
+        default: 'auto'
+      },
+      // 高度 rpx或%
+      height: {
+        type: String,
+        default: ''
+      },
+      // 按钮的尺寸 sm lg
+      size: {
+        type: String,
+        default: ''
+      },
+      // 字体是否加粗
+      fontBold: {
+        type: Boolean,
+        default: false
+      },
+      padding: {
+        type: String,
+        default: '0 30rpx'
+      },
+      // 外边距 与css的margin参数用法相同
+      margin: {
+        type: String,
+        default: ''
+      },
+      // 是否镂空
+      plain: {
+        type: Boolean,
+        default: false
+      },
+      // 当plain=true时,是否显示边框
+      border: {
+        type: Boolean,
+        default: true
+      },
+      // 当plain=true时,是否加粗显示边框
+      borderBold: {
+        type: Boolean,
+        default: false
+      },
+      // 是否禁用
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      // 是否显示加载图标
+      loading: {
+        type: Boolean,
+        default: false
+      },
+      // 触发form表单的事件类型
+      formType: {
+        type: String,
+        default: ''
+      },
+      // 开放能力
+      openType: {
+        type: String,
+        default: ''
+      },
+      // 是否阻止重复点击(默认间隔是200ms)
+      blockRepeatClick: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      // 根据不同的参数动态生成class
+      buttonClass() {
+        let clazz = ''
+        // 按钮形状
+        switch (this.shape) {
+          case 'icon':
+          case 'round':
+            clazz += ' tn-round'
+            break
+        }
+        
+        // 阴影
+        if (this.shadow) {
+          if (this.backgroundColorClass !== '' && this.backgroundColorClass.indexOf('tn-bg') != -1) {
+            const color = this.backgroundColor.slice(this.backgroundColor.lastIndexOf('-') + 1)
+            clazz += ` tn-shadow-${color}`
+          } else {
+            clazz += ' tn-shadow-blur'
+          }
+        }
+        
+        // 字体加粗
+        if (this.fontBold) {
+          clazz += ' tn-text-bold'
+        }
+        
+        // 设置为镂空并且设置镂空便可才进行设置
+        if (this.plain) {
+          clazz += ' tn-btn--plain'
+          if (this.border) {
+            clazz += ' tn-border-solid'
+            if (this.borderBold) {
+              clazz += ' tn-bold-border'
+            }
+            if (this.backgroundColor !== '' && this.backgroundColor.includes('tn-bg')) {
+              const color = this.backgroundColor.slice(this.backgroundColor.lastIndexOf('-') + 1)
+              clazz += ` tn-border-${color}`
+            }
+          }
+        }
+        
+        return clazz
+      },
+      // 按钮的样式
+      buttonStyle() {
+        let style = {}
+        switch(this.size) {
+          case 'sm':
+            style.padding = '0 20rpx'
+            style.fontSize = '22rpx'
+            style.height = this.height || '48rpx'
+            break
+          case 'lg':
+            style.padding = '0 40rpx'
+            style.fontSize = '32rpx'
+            style.height = this.height || '80rpx'
+            break
+          default :
+            style.padding = '0 30rpx'
+            style.fontSize = '28rpx'
+            style.height = this.height || '64rpx'
+        }
+        
+        // 是否手动设置了内边距
+        if (this.padding) {
+          style.padding = this.padding
+        }
+        
+        // 是否手动设置外边距
+        if (this.margin) {
+          style.margin = this.margin
+        }
+        
+        // 是否手动设置了字体大小
+        if (this.fontSize) {
+          style.fontSize = this.fontSize + this.fontUnit
+        }
+        style.width = this.shape === 'icon' ? style.height : this.width
+        style.padding = this.shape === 'icon' ? '0' : style.padding
+        
+        if (this.fontColorStyle) {
+          style.color = this.fontColorStyle
+        }
+        
+        if (!this.backgroundColorClass) {
+          if (this.plain) {
+            style.borderColor = this.backgroundColorStyle || '#080808'
+          } else {
+            style.backgroundColor = this.backgroundColorStyle || '#FFFFFF'
+          }
+        }
+        
+        // 设置阴影
+        if (this.shadow && !this.backgroundColorClass) {
+          if (this.backgroundColorStyle.indexOf('#') != -1) {
+            style.boxShadow = `6rpx 6rpx 8rpx ${(this.backgroundColorStyle || '#000000')}10`
+          } else if (this.backgroundColorStyle.indexOf('rgb') != -1 || this.backgroundColorStyle.indexOf('rgba') != -1 || !this.backgroundColorStyle) {
+            style.boxShadow = `6rpx 6rpx 8rpx ${(this.backgroundColorStyle || 'rgba(0, 0, 0, 0.1)')}`
+          }
+          
+        }
+        
+        return style
+      },
+    },
+    data() {
+      return {
+        // 上次点击的时间
+        clickTime: 0,
+        // 两次点击防抖的间隔时间
+        clickIntervalTime: 200
+      }
+    },
+    methods: {
+      // 按钮点击事件
+      handleClick() {
+        if (this.disabled) {
+          return
+        }
+        if (this.blockRepeatClick) {
+          const nowTime = new Date().getTime()
+          if (nowTime - this.clickTime <= this.clickIntervalTime) {
+            return
+          }
+          this.clickTime = nowTime
+          setTimeout(() => {
+            this.clickTime = 0
+          }, this.clickIntervalTime)
+        }
+        this.$emit('click', {
+          index: Number(this.index)
+        })
+        // 兼容tap事件
+        this.$emit('tap', {
+          index: Number(this.index)
+        })
+      },
+      handleGetUserInfo({ detail = {} } = {}) {
+      	this.$emit('getuserinfo', detail);
+      },
+      handleContact({ detail = {} } = {}) {
+      	this.$emit('contact', detail);
+      },
+      handleGetPhoneNumber({ detail = {} } = {}) {
+      	this.$emit('getphonenumber', detail);
+      },
+      handleError({ detail = {} } = {}) {
+      	this.$emit('error', detail);
+      },
+      
+      
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-btn {
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    box-sizing: border-box;
+    line-height: 1;
+    text-align: center;
+    text-decoration: none;
+    overflow: visible;
+    transform: translate(0rpx, 0rpx);
+    // background-color: $tn-mai
+    border-radius: 12rpx;
+    // color: $tn-font-color;
+    margin: 0;
+    
+    &--plain {
+      background-color: transparent !important;
+      background-image: none;
+      
+      &.tn-round {
+        border-radius: 1000rpx !important;
+      }
+    }
+  }
+  
+</style>

+ 707 - 0
tuniao-ui/components/tn-calendar/tn-calendar.vue

@@ -0,0 +1,707 @@
+<template>
+  <tn-popup
+    v-model="value"
+    mode="bottom"
+    :popup="false"
+    length="auto"
+    :borderRadius="borderRadius"
+    :safeAreaInsetBottom="safeAreaInsetBottom"
+    :maskCloseable="maskCloseable"
+    :closeBtn="closeBtn"
+    :zIndex="elIndex"
+    @close="close"
+  >
+    <view class="tn-calendar-class tn-calendar">
+      <!-- 头部 -->
+      <view class="tn-calendar__header">
+        <view v-if="!$slots.tooltip || !$slots.$tooltip" class="tn-calendar__header__text">
+          {{ toolTips }}
+        </view>
+        <view v-else>
+          <slot name="tooltip"></slot>
+        </view>
+      </view>
+      
+      <!-- 操作提示信息 -->
+      <view class="tn-calendar__action">
+        <view v-if="changeYear" class="tn-calendar__action__icon" :style="{backgroundColor: yearArrowColor}" @tap.stop="changeYearHandler(false)">
+          <view><text class="tn-icon-left"></text></view>
+        </view>
+        <view v-if="changeMonth" class="tn-calendar__action__icon" :style="{backgroundColor: monthArrowColor}" @tap.stop="changeMonthHandler(false)">
+          <view><text class="tn-icon-left"></text></view>
+        </view>
+        <view class="tn-calendar__action__text">{{ dateTitle }}</view>
+        <view v-if="changeMonth" class="tn-calendar__action__icon" :style="{backgroundColor: monthArrowColor}" @tap.stop="changeMonthHandler(true)">
+          <view><text class="tn-icon-right"></text></view>
+        </view>
+        <view v-if="changeYear" class="tn-calendar__action__icon" :style="{backgroundColor: yearArrowColor}" @tap.stop="changeYearHandler(true)">
+          <view><text class="tn-icon-right"></text></view>
+        </view>
+      </view>
+      
+      <!-- 星期中文标识 -->
+      <view class="tn-calendar__week-day-zh">
+        <view v-for="(item,index) in weekDayZh" :key="index" class="tn-calendar__week-day-zh__text">{{ item }}</view>
+      </view>
+      
+      <!-- 日历主体 -->
+      <view class="tn-calendar__content">
+        <!-- 前置空白部分 -->
+        <block v-for="(item, index) in weekdayArr" :key="index">
+          <view class="tn-calendar__content__item"></view>
+        </block>
+        <view
+          v-for="(item, index) in daysArr"
+          :key="index"
+          class="tn-calendar__content__item"
+          :class="{
+            'tn-hover': disabledChoose(year, month, index + 1),
+            'tn-calendar__content--start-date': (mode === 'range' && startDate == `${year}-${month}-${index+1}`) || mode === 'date',
+            'tn-calendar__content--end-date': (mode === 'range' && endDate == `${year}-${month}-${index+1}`) || mode === 'date'
+          }"
+          :style="{
+            backgroundColor: colorValue(index, 'bg')
+          }"
+          @tap.stop="dateClick(index)"
+        >
+          <view class="tn-calendar__content__item__text" :style="{color: colorValue(index, 'text')}">
+            <view>{{ item.day }}</view>
+          </view>
+          <view class="tn-calendar__content__item__tips" :style="{color: item.color}">
+            {{ item.bottomInfo }}
+          </view>
+        </view>
+        
+        <view class="tn-calendar__content__month--bg">{{ month }}</view>
+      </view>
+      
+      <!-- 底部 -->
+      <view class="tn-calendar__bottom">
+        <view class="tn-calendar__bottom__choose">
+          <text>{{ mode === 'date' ? activeDate : startDate }}</text>
+          <text v-if="endDate">至{{ endDate }}</text>
+        </view>
+        <view class="tn-calendar__bottom__btn" :style="{backgroundColor: btnColor}" @click="handleBtnClick(false)">
+          <view class="tn-calendar__bottom__btn--text">确定</view>
+        </view>
+      </view>
+    </view>
+  </tn-popup>
+</template>
+
+<script>
+  import Calendar from '../../libs/utils/calendar.js'
+  
+  export default {
+    name: 'tn-calendar',
+    props: {
+      // 双向绑定控制组件弹出与收起
+      value: {
+        type: Boolean,
+        default: false
+      },
+      // 模式
+      // date -> 单日期 range -> 日期范围
+      mode: {
+        type: String,
+        default: 'date'
+      },
+      // 是否允许切换年份
+      changeYear: {
+        type: Boolean,
+        default: true
+      },
+      // 是否允许切换月份
+      changeMonth: {
+        type: Boolean,
+        default: true
+      },
+      // 可切换的最大年份
+      maxYear: {
+        type: [Number, String],
+        default: 2100
+      },
+      // 可切换的最小年份
+      minYear: {
+        type: [Number, String],
+        default: 1970
+      },
+      // 最小日期(不在范围被不允许选择)
+      minDate: {
+        type: String,
+        default: '1970-01-01'
+      },
+      // 最大日期,如果为空则默认为今天
+      maxDate: {
+        type: String,
+        default: ''
+      },
+      // 切换月份按钮的颜色
+      monthArrowColor: {
+        type: String,
+        default: '#AAAAAA'
+      },
+      // 切换年份按钮的颜色
+      yearArrowColor: {
+        type: String,
+        default: '#C8C8C8'
+      },
+      // 默认字体颜色
+      color: {
+        type: String,
+        default: '#080808'
+      },
+      // 选中|起始结束日期背景颜色
+      activeBgColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // 选中|起始结束日期文字颜色
+      activeColor: {
+        type: String,
+        default: '#FFFFFF'
+      },
+      // 范围日期内的背景颜色
+      rangeBgColor: {
+        type: String,
+        default: '#E6E6E655'
+      },
+      // 范围日期内的文字颜色
+      rangeColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // 起始日期显示的文字,mode=range时生效
+      startText: {
+        type: String,
+        default: '开始'
+      },
+      // 结束日期显示的文字,mode=range时生效
+      endText: {
+        type: String,
+        default: '结束'
+      },
+      // 按钮背景颜色
+      btnColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // 农历文字的颜色
+      lunarColor: {
+        type: String,
+        default: '#AAAAAA'
+      },
+      // 选中日期是否有选中效果
+      isActiveCurrent: {
+        type: Boolean,
+        default: true
+      },
+      // 切换年月是否触发事件,mode=date时生效
+      isChange: {
+        type: Boolean,
+        default: false
+      },
+      // 是否显示农历
+      showLunar: {
+        type: Boolean,
+        default: true
+      },
+      // 顶部提示文字
+      toolTips: {
+        type: String,
+        default: '请选择日期'
+      },
+      // 显示圆角的大小
+      borderRadius: {
+        type: Number,
+        default: 8
+      },
+      // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+      safeAreaInsetBottom: {
+      	type: Boolean,
+      	default: false
+      },
+      // 是否可以通过点击遮罩进行关闭
+      maskCloseable: {
+      	type: Boolean,
+      	default: true
+      },
+      // zIndex
+      zIndex: {
+        type: Number,
+        default: 0
+      },
+      // 是否显示关闭按钮
+      closeBtn: {
+        type: Boolean,
+        default: false
+      },
+    },
+    computed: {
+      dateChange() {
+        return `${this.mode}-${this.minDate}-${this.maxDate}`
+      },
+      elIndex() {
+        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
+      },
+      colorValue() {
+        return (index, type) => {
+          let color = type === 'bg' ? '' : this.color
+          let day = index + 1
+          let date = `${this.year}-${this.month}-${day}`
+          let timestamp = new Date(date.replace(/\-/g,'/')).getTime()
+          let start = this.startDate.replace(/\-/g,'/')
+          let end = this.endDate.replace(/\-/g,'/')
+          if ((this.mode === 'date' && this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
+            color = type === 'bg' ? this.activeBgColor : this.activeColor
+          } else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
+            color = type === 'bg' ? this.rangeBgColor : this.rangeColor
+          }
+          return color
+        }
+      }
+    },
+    data() {
+      return {
+        // 星期几,1-7
+        weekday: 1,
+        weekdayArr: [],
+        // 星期对应的中文
+        weekDayZh: ['日','一','二','三','四','五','六'],
+        // 当前月有多少天
+        days: 0,
+        daysArr: [],
+        year: 2021,
+        month: 0,
+        day: 0,
+        startYear: 0,
+        startMonth: 0,
+        startDay: 0,
+        endYear: 0,
+        endMonth: 0,
+        endDay: 0,
+        today: '',
+        activeDate: '',
+        startDate: '',
+        endDate: '',
+        min: null,
+        max: null,
+        // 日期标题
+        dateTitle: '',
+        // 标记是否已经选择了开始日期
+        chooseStart: false
+      }
+    },
+    watch: {
+      dateChange() {
+        this.init()
+      }
+    },
+    created() {
+      this.init()
+    },
+    methods: {
+      // 初始化
+      init() {
+        let now = new Date()
+        this.year = now.getFullYear()
+        this.month = now.getMonth() + 1
+        this.day = now.getDate()
+        this.today = `${this.year}-${this.month}-${this.day}`
+        this.activeDate = this.today
+        this.min = this.initDate(this.minDate)
+        this.max = this.initDate(this.maxDate || this.today)
+        this.startDate = ''
+        this.startYear = 0
+        this.startMonth = 0
+        this.startDay = 0
+        this.endDate = ''
+        this.endYear = 0
+        this.endMonth = 0
+        this.endDay = 0
+        this.chooseStart = false
+        this.changeData()
+      },
+      // 切换月份
+      changeMonthHandler(add) {
+        if (add) {
+          let month = this.month + 1
+          let year = month > 12 ? this.year + 1 : this.year
+          if (!this.checkRange(year)) {
+            this.month = month > 12 ? 1 : month
+            this.year = year
+            this.changeData()
+          }
+        } else {
+          let month = this.month - 1
+          let year = month < 1 ? this.year - 1 : this.year
+          if (!this.checkRange(year)) {
+            this.month = month < 1 ? 12 : month
+            this.year = year
+            this.changeData()
+          }
+        }
+      },
+      // 切换年份
+      changeYearHandler(add) {
+        let year = add ? this.year + 1 : this.year - 1
+        if (!this.checkRange(year)) {
+          this.year = year
+          this.changeData()
+        }
+      },
+      // 日期点击事件
+      dateClick(day) {
+        day += 1
+        if (!this.disabledChoose(this.year, this.month, day)) {
+          this.day = day
+          let date = `${this.year}-${this.month}-${day}`
+          if (this.mode === 'date') {
+            this.activeDate = date
+          } else {
+            let startTimeCompare = new Date(date.replace(/\-/g,'/')).getTime() < new Date(this.startDate.replace(/\-/g,'/')).getTime()
+            if (!this.chooseStart || startTimeCompare) {
+              this.startDate = date
+              this.startYear = this.year
+              this.startMonth = this.month
+              this.startDay = this.day
+              this.endYear = 0
+              this.endMonth = 0
+              this.endDay = 0
+              this.endDate = ''
+              this.activeDate = ''
+              this.chooseStart = true
+            } else {
+              this.endDate = date
+              this.endYear = this.year
+              this.endMonth = this.month
+              this.endDay = this.day
+              this.chooseStart = false
+            }
+          }
+          this.daysArr = this.handleDaysArr()
+        }
+      },
+      // 修改日期数据
+      changeData() {
+        this.days = this.getMonthDay(this.year, this.month)
+        this.daysArr = this.handleDaysArr()
+        this.weekday = this.getMonthFirstWeekDay(this.year, this.month)
+        this.weekdayArr = this.generateArray(1, this.weekday)
+        this.dateTitle = `${this.year}年${this.month}月`
+        if (this.isChange && this.mode === 'date') {
+          this.handleBtnClick(true)
+        }
+      },
+      // 处理按钮点击
+      handleBtnClick(show) {
+        if (!show) {
+          this.close()
+        }
+        if (this.mode === 'date') {
+          let arr = this.activeDate.split('-')
+          let year = this.isChange ? this.year : Number(arr[0])
+          let month = this.isChange ? this.month : Number(arr[1])
+          let day = this.isChange ? this.day : Number(arr[2])
+          let days = this.getMonthDay(year, month)
+          let result = `${year}-${this.formatNumber(month)}-${this.formatNumber(day)}`
+          let weekText = this.getWeekText(result)
+          let isToday = false
+          if (`${year}-${month}-${day}` === this.today) {
+            isToday = true
+          }
+          this.$emit('change', {
+            year,
+            month,
+            day,
+            days,
+            week: weekText,
+            isToday,
+            date: result,
+            // 是否为切换年月操作
+            switch: show
+          })
+        } else {
+          if (!this.startDate || !this.endDate) return
+          
+          let startMonth = this.formatNumber(this.startMonth)
+          let startDay = this.formatNumber(this.startDay)
+          let startDate = `${this.startYear}-${startMonth}-${startDay}`
+          let startWeek = this.getWeekText(startDate)
+          
+          let endMonth = this.formatNumber(this.endMonth)
+          let endDay = this.formatNumber(this.endDay)
+          let endDate = `${this.endYear}-${endMonth}-${endDay}`
+          let endWeek = this.getWeekText(endDate)
+          
+          this.$emit('change', {
+            startYear: this.startYear,
+            startMonth: this.startMonth,
+            startDay: this.startDay,
+            startDate,
+            startWeek,
+            endYear: this.endYear,
+            endMonth: this.endMonth,
+            endDay: this.endDay,
+            endDate,
+            endWeek
+          })
+        }
+      },
+      // 判断是否允许选择
+      disabledChoose(year, month, day) {
+        let flag = true
+        let date = `${year}/${month}/${day}`
+        let min = `${this.min.year}/${this.min.month}/${this.min.day}`
+        let max = `${this.max.year}/${this.max.month}/${this.max.day}`
+        let timestamp = new Date(date).getTime()
+        if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
+          flag = false
+        }
+        return flag
+      },
+      // 检查是否在日期范围内
+      checkRange(year) {
+        let overstep = false
+        if (year < this.minYear || year > this.maxYear) {
+          uni.showToast({
+            title: '所选日期超出范围',
+            icon: 'none'
+          })
+          overstep = true
+        }
+        return overstep
+      },
+      // 处理日期
+      initDate(date) {
+        let fdate = date.split('-')
+        return {
+          year: Number(fdate[0] || 1970),
+          month: Number(fdate[1] || 1),
+          day: Number(fdate[2] || 1)
+        }
+      },
+      // 处理日期数组
+      handleDaysArr() {
+        let days = this.generateArray(1, this.days)
+        let daysArr = days.map((item) => {
+          let bottomInfo = this.showLunar ? Calendar.solar2lunar(this.year, this.month, item).IDayCn : ''
+          let color = this.showLunar ? this.lunarColor : this.activeColor
+          if (
+            (this.mode === 'date' && this.day == item) || 
+            (this.mode === 'range' && (this.startDay == item || this.endDay == item))
+          ) {
+            color = this.activeColor
+          }
+          if (this.mode === 'range') {
+            if (this.startDay == item && this.startDay != this.endDay) {
+              bottomInfo = this.startText
+            }
+            if (this.endDay == item) {
+              bottomInfo = this.endText
+            }
+          }
+          
+          return {
+            day: item,
+            color: color,
+            bottomInfo: bottomInfo
+          }
+        })
+        return daysArr
+      },
+      // 获取对应月有多少天
+      getMonthDay(year, month) {
+        return new Date(year, month, 0).getDate()
+      },
+      // 获取对应月的第一天时星期几
+      getMonthFirstWeekDay(year, month) {
+        return new Date(`${year}/${month}/01 00:00:00`).getDay()
+      },
+      // 获取对应星期的文本
+      getWeekText(date) {
+        date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`)
+        let week = date.getDay()
+        return '星期' + this.weekDayZh[week]
+      },
+      // 生成日期天数数组
+      generateArray(start, end) {
+        return Array.from(new Array(end + 1).keys()).slice(start)
+      },
+      // 格式化数字
+      formatNumber(num) {
+        return num < 10 ? '0' + num : num + ''
+      },
+      // 关闭窗口
+      close() {
+        this.$emit('input', false)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-calendar {
+    color: $tn-font-color;
+    
+    &__header {
+      width: 100%;
+      box-sizing: border-box;
+      font-size: 30rpx;
+      background-color: #FFFFFF;
+      color: $tn-main-color;
+      
+      &__text {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: center;
+        margin-top: 30rpx;
+        padding: 0 60rpx;
+      }
+    }
+    
+    &__action {
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+      align-items: center;
+      padding: 40rpx 0 40rpx 0;
+      
+      &__icon {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin: 0 16rpx;
+        width: 32rpx;
+        height: 32rpx;
+        font-size: 20rpx;
+        // line-height: 32rpx;
+        border-radius: 50%;
+        color: #FFFFFF;
+      }
+      
+      &__text {
+        padding: 0 16rpx;
+        color: $tn-font-color;
+        font-size: 32rpx;
+        font-weight: bold;
+      }
+    }
+    
+    &__week-day-zh {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      padding: 12rpx 0;
+      overflow: hidden;
+      box-shadow: 16rpx 6rpx 8rpx 0 #E6E6E6;
+      margin-bottom: 2rpx;
+      
+      &__text {
+        flex: 1;
+        text-align: center;
+      }
+    }
+    
+    &__content {
+      display: flex;
+      flex-direction: row;
+      flex-wrap: wrap;
+      width: 100%;
+      padding: 12rpx 0;
+      box-sizing: border-box;
+      background-color: #F7F7F7;
+      position: relative;
+      
+      &__item {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        width: 14.2857%;
+        padding: 12rpx 0;
+        margin: 6rpx 0;
+        overflow: hidden;
+        position: relative;
+        z-index: 2;
+        // box-shadow: inset 0rpx 0rpx 22rpx 4rpx rgba(255,255,255, 0.52);
+        
+        &__text {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          height: 80rpx;
+          font-size: 32rpx;
+          position: relative;
+        }
+        
+        &__tips {
+          position: absolute;
+          width: 100%;
+          line-height: 24rpx;
+          left: 0;
+          bottom: 8rpx;
+          text-align: center;
+          z-index: 2;
+          transform-origin: center center;
+          transform: scale(0.8);
+        }
+      }
+      
+      &--start-date {
+        border-top-left-radius: 8rpx;
+        border-bottom-left-radius: 8rpx;
+      }
+      
+      &--end-date {
+        border-top-right-radius: 8rpx;
+        border-bottom-right-radius: 8rpx;
+      }
+      
+      &__month {
+        &--bg {
+          position: absolute;
+          font-size: 200rpx;
+          line-height: 200rpx;
+          left: 50%;
+          top: 50%;
+          transform: translate(-50%, -50%);
+          color: $tn-font-holder-color;
+          z-index: 1;
+        }
+      }
+    }
+    
+    &__bottom {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      background-color: #F7F7F7;
+      padding: 0 40rpx 30rpx;
+      box-sizing: border-box;
+      font-size: 24rpx;
+      color: $tn-font-sub-color;
+      
+      &__choose {
+        height: 50rpx;
+      }
+      
+      &__btn {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 60rpx;
+        border-radius: 40rpx;
+        color: #FFFFFF;
+        font-size: 28rpx;
+      }
+    }
+  }
+</style>

+ 320 - 0
tuniao-ui/components/tn-car-keyboard/tn-car-keyboard.vue

@@ -0,0 +1,320 @@
+<template>
+  <view class="tn-car-keyboard-class tn-car-keyboard" @touchmove.stop.prevent="() => {}">
+    <view class="tn-car-keyboard__grids">
+      
+      <view
+        v-for="(data, index) in inputCarNumber ? endKeyBoardList : areaList"
+        :key="index"
+        class="tn-car-keyboard__grids__item"
+      >
+        <view
+          v-for="(sub_data, sub_index) in data"
+          :key="sub_index"
+          class="tn-car-keyboard__grids__btn"
+          :class="{'tn-car-keyboard__grids__btn--disabled': sub_data === 'I'}"
+          :hover-class="sub_data !== 'I' ? 'tn-car-keyboard--hover' : ''"
+          :hover-stay-time="100"
+          @tap="click(index, sub_index)"
+        >
+          {{ sub_data }}
+        </view>
+      </view>
+      
+      <view
+        class="tn-car-keyboard__back"
+        hover-class="tn-hover-class"
+        :hover-stay-time="150"
+        @touchstart.stop="backspaceClick"
+        @touchend="clearTimer"
+      >
+        <view class="tn-icon-left-arrow tn-car-keyboard__back__icon"></view>
+      </view>
+      
+      <view
+        class="tn-car-keyboard__change"
+        hover-class="tn-car-keyboard--hover"
+        :hover-stay-time="150"
+        @tap="changeMode"
+      >
+        <text class="tn-car-keyboard__mode--zh" :class="[`tn-car-keyboard__mode--${!inputCarNumber ? 'active' : 'inactive'}`]">中</text>
+        /
+        <text class="tn-car-keyboard__mode--en" :class="[`tn-car-keyboard__mode--${inputCarNumber ? 'active' : 'inactive'}`]">英</text>
+      </view>
+      
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-car-keyboard',
+    props: {
+      // 是否打乱键盘顺序
+      randomEnabled: {
+        type: Boolean,
+        default: false
+      },
+      // 切换中英文输入
+      switchEnMode: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      areaList() {
+        let data = [
+          '京',
+          '沪',
+          '粤',
+          '津',
+          '冀',
+          '豫',
+          '云',
+          '辽',
+          '黑',
+          '湘',
+          '皖',
+          '鲁',
+          '苏',
+          '浙',
+          '赣',
+          '鄂',
+          '桂',
+          '甘',
+          '晋',
+          '陕',
+          '蒙',
+          '吉',
+          '闽',
+          '贵',
+          '渝',
+          '川',
+          '青',
+          '琼',
+          '宁',
+          '藏',
+          '港',
+          '澳',
+          '新',
+          '使',
+          '学',
+          '临',
+          '警'
+        ]
+        // 打乱顺序
+        if (this.randomEnabled) data = this.$t.array.random(data)
+        // 切割二维数组
+        let showData = []
+        showData[0] = data.slice(0, 10)
+        showData[1] = data.slice(10, 20)
+        showData[2] = data.slice(20, 30)
+        showData[3] = data.slice(30, 37)
+        return showData
+      },
+      endKeyBoardList() {
+        let data = [
+          1,
+          2,
+          3,
+          4,
+          5,
+          6,
+          7,
+          8,
+          9,
+          0,
+          'Q',
+          'W',
+          'E',
+          'R',
+          'T',
+          'Y',
+          'U',
+          'I',
+          'O',
+          'P',
+          'A',
+          'S',
+          'D',
+          'F',
+          'G',
+          'H',
+          'J',
+          'K',
+          'L',
+          'Z',
+          'X',
+          'C',
+          'V',
+          'B',
+          'N',
+          'M'
+        ]
+        // 打乱顺序
+        if (this.randomEnabled) data = this.$t.array.random(data)
+        // 切割二维数组
+        let showData = []
+        showData[0] = data.slice(0, 10)
+        showData[1] = data.slice(10, 20)
+        showData[2] = data.slice(20, 29)
+        showData[3] = data.slice(29, 36)
+        return showData
+      }
+    },
+    data() {
+      return {
+        // 标记是否输入车牌号码
+        inputCarNumber: false,
+        // 长按多次删除事件监听
+        longPressDeleteTimer: null
+      }
+    },
+    watch:{
+      switchEnMode: {
+        handler(value) {
+          if (value) {
+            this.inputCarNumber = true
+          } else {
+            this.inputCarNumber = false
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+      // 点击键盘按钮
+      click(i, j) {
+        let value = ''
+        // 根据不同模式获取不同数组的值
+        if (this.inputCarNumber) value = this.endKeyBoardList[i][j]
+        else value = this.areaList[i][j]
+        
+        // 车牌里不包含I
+        if (value === 'I') return
+        
+        this.$emit('change', value)
+      },
+      // 修改输入模式
+      // 中文/英文
+      changeMode() {
+        this.inputCarNumber = !this.inputCarNumber
+      },
+      // 点击退格
+      backspaceClick() {
+        this.$emit('backspace')
+        this.clearTimer()
+        this.longPressDeleteTimer = setInterval(() => {
+          this.$emit('backspace')
+        }, 250)
+      },
+      // 清空定时器
+      clearTimer() {
+        if (this.longPressDeleteTimer) {
+          clearInterval(this.longPressDeleteTimer)
+          this.longPressDeleteTimer = null
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-car-keyboard {
+    position: relative;
+    padding: 24rpx 0;
+    background-color: #E6E6E6;
+    
+    &__grids {
+      
+      &__item {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: center;
+      }
+      
+      &__btn {
+        display: inline-flex;
+        justify-content: center;
+        flex: 0 0 64rpx;
+        width: 62rpx;
+        height: 80rpx;
+        font-size: 38rpx;
+        line-height: 80rpx;
+        font-weight: 500;
+        text-decoration: none;
+        text-align: center;
+        background-color: #FFFFFF;
+        margin: 8rpx 5rpx;
+        border-radius: 8rpx;
+        box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
+        
+        &--disabled {
+          opacity: 0.6;
+        }
+      }
+    }
+    
+    &__back {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      position: absolute;
+      width: 96rpx;
+      height: 80rpx;
+      right: 22rpx;
+      bottom: 32rpx;
+      background-color: #E6E6E6;
+      border-radius: 8rpx;
+      box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
+    }
+    
+    &__change {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      position: absolute;
+      width: 96rpx;
+      height: 80rpx;
+      left: 22rpx;
+      bottom: 32rpx;
+      line-height: 1;
+      background-color: #FFFFFF;
+      border-radius: 8rpx;
+      box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
+    }
+    
+    &__mode {
+      &--zh {
+        transform: translateY(-10rpx);
+      }
+      &--en {
+        transform: translateY(10rpx);
+      }
+      
+      &--active {
+        color: $tn-main-color;
+        font-size: 30rpx;
+      }
+      
+      &--inactive {
+        &.tn-car-keyboard__mode--zh {
+          transform: scale(0.85) translateY(-10rpx);
+        }
+      }
+      
+      &--inactive {
+        &.tn-car-keyboard__mode--en {
+          transform: scale(0.85) translateY(10rpx);
+        }
+      }
+    }
+    
+    &--hover {
+      background-color: #E6E6E6 !important;
+    }
+  }
+</style>

+ 654 - 0
tuniao-ui/components/tn-cascade-selection/tn-cascade-selection.vue

@@ -0,0 +1,654 @@
+<template>
+  <view class="tn-cascade-selection tn-cascade-selection-class">
+    <scroll-view
+      class="selection__scroll-view"
+      :class="[{'tn-border-solid-bottom': headerLine}]"
+      :style="[scrollViewStyle]"
+      scroll-x
+      scroll-with-animation
+      :scroll-into-view="scrollViewId"
+    >
+      <view class="selection__header" :class="[backgroundColorClass]" :style="[headerStyle]">
+        <view
+          v-for="(item, index) in selectedArr"
+          :key="index"
+          :id="`select__${index}`"
+          class="selection__header__item"
+          :class="[headerItemClass(index)]"
+          :style="[headerItemStyle(index)]"
+          @tap.stop="clickNav(index)"
+        >
+          {{ item.text }}
+          <view
+            v-if="index===currentTab && showActiveLine"
+            class="selection__header__line"
+            :style="{backgroundColor: activeLineColor}"
+          ></view>
+        </view>
+      </view>
+    </scroll-view>
+    
+    <swiper
+      class="selection__list"
+      :class="[backgroundColorClass]"
+      :style="[listStyle]"
+      :current="currentTab"
+      :duration="300"
+      @change="switchTab"
+    >
+      <swiper-item
+        v-for="(item, index) in selectedArr"
+        :key="index"
+      >
+        <scroll-view
+          class="selection__list__item"
+          :style="{height: selectionContainerHeight + 'rpx'}"
+          scroll-y
+          :scroll-into-view="item.scrollViewId"
+        >
+          <view class="selection__list__item--first"></view>
+          <view
+            v-for="(subItem, subIndex) in item.list"
+            :key="subIndex"
+            :id="`select__${subIndex}`"
+            class="selection__list__item__cell"
+            :style="[itemStyle]"
+            @tap="change(index, subIndex, subItem)"
+          >
+            <view
+              v-if="item.index === subIndex"
+              class="selection__list__item__icon tn-icon-success"
+              :style="[itemIconStyle]"
+            ></view>
+            <image
+              v-if="subItem.src"
+              class="selection__list__item__image"
+              :style="[itemImageStyle]"
+              :src="subItem.src"
+            ></image>
+            <view
+              class="selection__list__item__title"
+              :class="[{'tn-text-bold': item.index === subIndex && itemActiveBold}]"
+              :style="[itemTitleStyle(index, subIndex)]"
+            >
+              {{ subItem.text }}
+            </view>
+            <view
+              v-if="subItem.subText"
+              class="selection__list__item__title--sub"
+              :style="[itemSubTitleStyle]"
+            >
+              {{ subItem.subText }}
+            </view>
+          </view>
+        </scroll-view>
+      </swiper-item>
+    </swiper>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    name: 'tn-cascade-selection',
+    mixins: [ componentsColorMixin ],
+    props: {
+      // 如果下一级是请求返回,则为第一级数据,否则为所有数据
+      /* {
+        text: '', // 标题
+        subText: '', // 子标题
+        src: '', // 图片地址
+        value: 0, // 选中的值
+        children: [
+          {
+            text: '',
+            subText: '',
+            value: 0,
+            children: []
+          }
+        ]
+      } */
+      list: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 默认选中值
+      // ['value1','value2','value3']
+      defaultValue: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 子集数据通过请求来获取
+      request: {
+        type: Boolean,
+        default: false
+      },
+      // request为true时生效, 获取到的子集数据
+      receiveData: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 显示header底部细线
+      headerLine: {
+        type: Boolean,
+        default: true
+      },
+      // header背景颜色
+      headerBgColor: {
+        type: String,
+        default: ''
+      },
+      // 顶部标签栏高度,单位rpx
+      tabsHeight: {
+        type: Number,
+        default: 88
+      },
+      // 默认显示文字
+      text: {
+        type: String,
+        default: '请选择'
+      },
+      // 选中的颜色
+      activeColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // 选中后加粗
+      activeBold: {
+        type: Boolean,
+        default: true
+      },
+      // 选中显示底部线条
+      showActiveLine: {
+        type: Boolean,
+        default: true
+      },
+      // 线条颜色
+      activeLineColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // icon大小,单位rpx
+      activeIconSize: {
+        type: Number,
+        default: 0
+      },
+      // icon颜色
+      activeIconColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // item图片宽度, 单位rpx
+      itemImgWidth: {
+        type: Number,
+        default: 0
+      },
+      // item图片高度, 单位rpx
+      itemImgHeight: {
+        type: Number,
+        default: 0
+      },
+      // item图片圆角
+      itemImgRadius: {
+        type: String,
+        default: '50%'
+      },
+      // item text颜色
+      itemTextColor: {
+        type: String,
+        default: ''
+      },
+      // item text选中颜色
+      itemActiveTextColor: {
+        type: String,
+        default: ''
+      },
+      // item text选中加粗
+      itemActiveBold: {
+        type: Boolean,
+        default: true
+      },
+      // item text文字大小, 单位rpx
+      itemTextSize: {
+        type: Number,
+        default: 0
+      },
+      // item subText颜色
+      itemSubTextColor: {
+        type: String,
+        default: ''
+      },
+      // item subText字体大小, 单位rpx
+      itemSubTextSize: {
+        type: Number,
+        default: 0
+      },
+      // item样式
+      itemStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // selection选项容器高度, 单位rpx
+      selectionContainerHeight: {
+        type: Number,
+        default: 300
+      }
+    },
+    computed: {
+      scrollViewStyle() {
+        let style = {}
+        if (this.headerBgColor) {
+          style.backgroundColor = this.headerBgColor
+        }
+        return style
+      },
+      headerStyle() {
+        let style = {}
+        style.height = `${this.tabsHeight}rpx`
+        if (this.backgroundColorStyle) {
+          style.backgroundColor = this.backgroundColorStyle
+        }
+        return style
+      },
+      headerItemClass() {
+        return (index) => {
+          let clazz = ''
+          if (index !== this.currentTab) {
+            clazz += ` ${this.fontColorClass}`
+          } else {
+            if (this.activeBold) {
+              clazz += ' tn-text-bold'
+            }
+          }
+          return clazz
+        }
+      },
+      headerItemStyle() {
+        return (index) => {
+          let style = {}
+          style.color = index === this.currentTab ? this.activeColor : (this.fontColorStyle ? this.fontColorStyle : '')
+          if (this.fontSizeStyle) {
+            style.fontSize = this.fontSizeStyle
+          }
+          return style
+        }
+      },
+      listStyle() {
+        let style = {}
+        style.height = `${this.selectionContainerHeight}rpx`
+        if (this.backgroundColorStyle) {
+          style.color = this.backgroundColorStyle
+        }
+        return style
+      },
+      itemIconStyle() {
+        let style = {}
+        if (this.activeIconColor) {
+          style.color = this.activeIconColor
+        }
+        if (this.activeIconSize) {
+          style.fontSize = this.activeIconSize + 'rpx'
+        }
+        return style
+      },
+      itemImageStyle() {
+        let style = {}
+        if (this.itemImgWidth) {
+          style.width = this.itemImgWidth + 'rpx'
+        }
+        if (this.itemImgHeight) {
+          style.height = this.itemImgHeight + 'rpx'
+        }
+        if (this.itemImgRadius) {
+          style.borderRadius = this.itemImgRadius
+        }
+        return style
+      },
+      itemTitleStyle() {
+        return (index, subIndex) => {
+          let style = {}
+          if (index === subIndex) {
+            if (this.itemActiveTextColor) {
+              style.color = this.itemActiveTextColor
+            }
+          } else {
+            if (this.itemTextColor) {
+              style.color = this.itemTextColor
+            }
+          }
+          if (this.itemTextSize) {
+            style.fontSize = this.itemTextSize + 'rpx'
+          }
+          return style
+        }
+      },
+      itemSubTitleStyle() {
+        let style = {}
+        if (this.itemSubTextColor) {
+          style.color = this.itemSubTextColor
+        }
+        if (this.itemSubTextSize) {
+          style.fontSize = this.itemSubTextSize + 'rpx'
+        }
+        return {}
+      }
+    },
+    watch: {
+      list(val) {
+        this.initData(val, -1)
+      },
+      defaultValue(val) {
+        this.setDefaultValue(val)
+      },
+      receiveData(val) {
+        this.addSubData(val, this.currentTab)
+      },
+    },
+    data() {
+      return {
+        // 当前选中的子集
+        currentTab: 0,
+        // tabs栏scrollView滚动的位置
+        scrollViewId: 'select__0',
+        // 选项数组
+        selectedArr: []
+      }
+    },
+    created() {
+      this.setDefaultValue(this.defaultValue)
+    },
+    methods: {
+      // 初始化数据
+      initData(data, index) {
+        if (!data || data.length === 0) return
+        if (this.request) {
+          // 第一级数据
+          this.addSubData(data, index)
+        } else {
+          this.addSubData(this.getItemList(index, -1), index)
+        }
+      },
+      // 重置数据
+      reset() {
+        this.initData(this.list, -1)
+      },
+      // 滚动切换
+      switchTab(e) {
+        this.currentTab = e.detail.current
+        this.checkSelectPosition()
+      },
+      // 点击标题切换
+      clickNav(index) {
+        if (this.currentTab !== index) {
+          this.currentTab = index
+        }
+      },
+      // 列表数据发生改变
+      change(index, subIndex, subItem) {
+        let item = this.selectedArr[index]
+        if (item.index === subIndex) return
+        item.index = subIndex
+        item.text = subItem.text
+        item.subText = subItem.subText || ''
+        item.value = subItem.value
+        item.src = subItem.src || ''
+        this.$emit('change', {
+          index: index,
+          subIndex: subIndex,
+          ...subItem
+        })
+        
+        // 如果不是异步加载,则取出对应的数据
+        if (!this.request) {
+          let data = this.getItemList(index, subIndex)
+          this.addSubData(data, index)
+        }
+      },
+      // 设置默认的数据
+      setDefaultValue(val) {
+        let defaultValues = val || []
+        if (defaultValues.length > 0) {
+          this.selectedArr = this.getItemListWithValues(JSON.parse(JSON.stringify(this.list)), defaultValues)
+          if (!this.selectedArr) return
+          this.currentTab = this.selectedArr.length - 1
+          this.$nextTick(() => {
+            this.checkSelectPosition()
+          })
+          // defaultItemList.map((item) => {
+          //   item.scrollViewId = `select__${item.index}`
+          // })
+          // this.selectedArr = defaultItemList
+          // this.currentTab = defaultItemList.length - 1
+          // this.$nextTick(() => {
+          //   this.checkSelectPosition()
+          // })
+        } else {
+          this.initData(this.list, -1)
+        }
+      },
+      // 获取对应选项的item数据
+      getItemList(index, subIndex) {
+        let list = []
+        let arr = JSON.parse(JSON.stringify(this.list))
+        // 初始化数据
+        if (index === -1) {
+          list = this.removeChildren(arr)
+        } else {
+          // 判断第一项是否已经选择
+          let value = this.selectedArr[0].index
+          value = value === -1 ? subIndex : value
+          list = arr[value].children || []
+          if (index > 0) {
+            for (let i = 1; i < index + 1; i++) {
+              // 获取当前数据选中的序号
+              let val = index === i ? subIndex : this.selectedArr[i].index
+              list = list[val].children || []
+              if (list.length === 0) break
+            }
+          }
+          list = this.removeChildren(list)
+        }
+        return list
+      },
+      // 根据数组中的值获取对应的item数据
+      getItemListWithValues(data, values) {
+        const defaultValues = JSON.parse(JSON.stringify(values))
+        if (!defaultValues || defaultValues.length === 0) return
+        // 取出第一个值所对应的item
+        const itemIndex = data.findIndex((item) => {
+          return item.value === defaultValues[0]
+        })
+        if (itemIndex === -1) return
+        const item = data[itemIndex]
+        item.index = itemIndex
+        item.scrollViewId = `select__${itemIndex}`
+        item.list = this.removeChildren(JSON.parse(JSON.stringify(data)))
+        // 判断是否只有1个值
+        if (defaultValues.length === 1 || (!item.hasOwnProperty('children') || item.children.length === 0)) {
+          return this.removeChildren([item])
+        } else {
+          let selectItemList = []
+          const children = item.children
+          selectItemList.push(item)
+          // 移除已经获取的值
+          defaultValues.splice(0, 1)
+          const childrenValue = this.getItemListWithValues(children, defaultValues)
+          selectItemList = selectItemList.concat(childrenValue)
+          
+          return this.removeChildren(selectItemList)
+        }
+      },
+      // 删除子元素
+      removeChildren(data) {
+        let list = data.map((item) => {
+          if (item.hasOwnProperty('children')) {
+            delete item['children']
+          }
+          return item
+        })
+        return list
+      },
+      // 新增子集数据时处理
+      addSubData(data, index) {
+        // 判断是否已经完成选择数据或者为初始化数据
+        if (!data || data.length === 0) {
+          if (index == -1) return
+          // 完成选择
+          let arr = this.selectedArr
+          // 如果当前选中项的序号比已选数据的长度小,则表示当前重新选择了数据
+          if (index < arr.length - 1) {
+            let newArr = arr.slice(0, index + 1)
+            this.selectedArr = newArr
+          }
+          let result = JSON.parse(JSON.stringify(this.selectedArr))
+          let lastItem = result[result.length - 1] || {}
+          let text = ''
+          result.map(item => {
+            text += item.text
+            delete item['list']
+            delete item['scrollViewId']
+            return item
+          })
+          this.$emit('complete', {
+            result: result,
+            value: lastItem.value,
+            text: text,
+            subText: lastItem.subText,
+            src: lastItem.src
+          })
+        } else {
+          // 重置数据
+          let item = [{
+            text: this.text,
+            subText: '',
+            value: '',
+            src: '',
+            index: -1,
+            scrollViewId: 'select__0',
+            list: data
+          }]
+          // 初始化数据
+          if (index === -1) {
+            this.selectedArr = item
+          } else {
+            // 拼接新旧数据并且判断是否为重新选择了数据(如果为重新选择了数据则重置之后的选项数据)
+            let retainArr = this.selectedArr.slice(0, index + 1)
+            this.selectedArr = retainArr.concat(item)
+          }
+          this.$nextTick(() => {
+            this.currentTab = this.selectedArr.length - 1
+          })
+        }
+      },
+      // 检查当前选中项,并将选项设置位置信息
+      checkSelectPosition() {
+        let item = this.selectedArr[this.currentTab]
+        item.scrollViewId = 'select__0'
+        this.$nextTick(() => {
+          setTimeout(() => {
+            // 设置当前数据滚动到的位置
+            let val = item.index < 2 ? 0 : Number(item.index - 2)
+            item.scrollViewId = `select__${val}`
+          }, 10)
+        })
+        
+        // 设置选项滚动到所在的位置
+        if (this.currentTab > 1) {
+          this.scrollViewId = `select__${this.currentTab - 1}`
+        } else {
+          this.scrollViewId = `select__0`
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-cascade-selection {
+    width: 100%;
+  }
+  
+  .selection {
+    &__scroll-view {
+      background-color: #FFFFFF;
+    }
+    
+    &__header {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      position: relative;
+      
+      &__item {
+        max-width: 240rpx;
+        padding: 15rpx 30rpx;
+        flex-shrink: 0;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        position: relative;
+      }
+      
+      &__line {
+        width: 60rpx;
+        height: 6rpx;
+        border-radius: 4rpx;
+        position: absolute;
+        bottom: 0;
+        right: 0;
+        left: 50%;
+        transform: translateX(-50%);
+      }
+    }
+    
+    &__list {
+      background-color: #FFFFFF;
+      &__item {
+        &--first {
+          width: 100%;
+          height: 20rpx;
+        }
+        
+        &__cell {
+          width: 100%;
+          display: flex;
+          align-items: center;
+          padding: 20rpx 30rpx;
+        }
+        
+        &__icon {
+          margin-right: 12rpx;
+          font-size: 24rpx;
+        }
+        
+        &__image {
+          width: 40rpx;
+          height: 40rpx;
+          margin-right: 12rpx;
+          flex-shrink: 0;
+        }
+        
+        &__title {
+          word-break: break-all;
+          color: #333333;
+          font-size: 28rpx;
+          
+          &--sub {
+            margin-left: 20rpx;
+            word-break: break-all;
+            color: $tn-font-sub-color;
+            font-size: 24rpx;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 134 - 0
tuniao-ui/components/tn-checkbox-group/tn-checkbox-group.vue

@@ -0,0 +1,134 @@
+<template>
+  <view class="tn-checkbox-group-class tn-checkbox-group">
+    <slot></slot>
+  </view>
+</template>
+
+<script>
+  import Emitter from '../../libs/utils/emitter.js'
+  
+  export default {
+    mixins: [ Emitter ],
+    name: 'tn-checkbox-group',
+    props: {
+      value: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 可以选中多少个checkbox
+      max: {
+        type: Number,
+        default: 999
+      },
+      // 表单提交时的标识符
+      name: {
+        type: String,
+        default: ''
+      },
+      // 禁用选择
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      // 禁用点击标签进行选择
+      disabledLabel: {
+        type: Boolean,
+        default: false
+      },
+      // 选择框的形状 square 方形 circle 圆形
+      shape: {
+        type: String,
+        default: 'square'
+      },
+      // 选中时的颜色
+      activeColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // 组件大小
+      size: {
+        type: Number,
+        default: 34
+      },
+      // 每个checkbox占的宽度
+      width: {
+        type: String,
+        default: 'auto'
+      },
+      // 是否换行
+      wrap: {
+        type: Boolean,
+        default: false
+      },
+      // 图标大小
+      iconSize: {
+        type: Number,
+        default: 20
+      }
+    },
+    computed: {
+      // 这里computed的变量,都是子组件tn-checkbox需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
+      // 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(tn-checkbox-group)
+      // 拉取父组件新的变化后的参数
+      parentData() {
+        return [this.value, this.disabled, this.disabledLabel, this.shape, this.activeColor, this.size, this.width, this.wrap, this.iconSize]
+      }
+    },
+    data() {
+      return {
+        
+      }
+    },
+    watch: {
+      // 当父组件中的子组件需要共享的参数发生了变化,手动通知子组件
+      parentData() {
+        if (this.children.length) {
+          this.children.map(child => {
+            // 判断子组件(tn-checkbox)如果有updateParentData方法的话,子组件重新从父组件拉取了最新的值
+            typeof(child.updateParentData) === 'function' && child.updateParentData()
+          })
+        }
+      }
+    },
+    created() {
+      this.children = []
+    },
+    methods: {
+      initValue(values) {
+        this.$emit('input', values)
+      },
+      // 触发事件
+      emitEvent() {
+        let values = []
+        this.children.map(child => {
+          if (child.checkValue) values.push(child.name)
+        })
+        this.$emit('change', values)
+        this.$emit('input', values)
+        // 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
+        // 由于头条小程序执行迟钝,故需要用几十毫秒的延时
+        setTimeout(() => {
+        	// 将当前的值发送到 tn-form-item 进行校验
+        	this.dispatch('tn-form-item', 'on-form-change', values)
+        }, 60)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-checkbox-group {
+    /* #ifndef MP || APP-NVUE */
+    display: inline-flex;
+    flex-wrap: wrap;
+    /* #endif */
+    &::after {
+      content: " ";
+      display: table;
+      clear: both;
+    }
+  }
+</style>

+ 328 - 0
tuniao-ui/components/tn-checkbox/tn-checkbox.vue

@@ -0,0 +1,328 @@
+<template>
+  <view class="tn-checkbox-class tn-checkbox" :style="[checkboxStyle]">
+    <view
+      class="tn-checkbox__icon-wrap"
+      :class="[iconClass]"
+      :style="[iconStyle]"
+      @tap="toggle"
+    >
+      <view class="tn-checkbox__icon-wrap__icon" :class="[`tn-icon-${iconName}`]"></view>
+    </view>
+    
+    <view
+      class="tn-checkbox__label"
+      :class="[labelClass]"
+      :style="{
+        fontSize: labelSize ? labelSize + 'rpx' : ''
+      }"
+      @tap="onClickLabel"
+    >
+      <slot></slot>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-checkbox',
+    props: {
+      // checkbox名称
+      name: {
+        type: [String, Number],
+        default: ''
+      },
+      // 是否为选中状态
+      value: {
+        type: Boolean,
+        default: false
+      },
+      // 禁用选择
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      // 禁用点击标签进行选择
+      disabledLabel: {
+        type: Boolean,
+        default: false
+      },
+      // 选择框的形状 square 方形 circle 圆形
+      shape: {
+        type: String,
+        default: ''
+      },
+      // 选中时的颜色
+      activeColor: {
+        type: String,
+        default: ''
+      },
+      // 组件大小
+      size: {
+        type: Number,
+        default: 0
+      },
+      // 图标名称
+      iconName: {
+        type: String,
+        default: 'success'
+      },
+      // 图标大小
+      iconSize: {
+        type: Number,
+        default: 0
+      },
+      // label的字体大小
+      labelSize: {
+        type: Number,
+        default: 0
+      }
+    },
+    computed: {
+      // 是否禁用选中,父组件的禁用会覆盖当前的禁用状态
+      isDisabled() {
+        return this.disabled ? this.disabled : (this.parent ? this.parentData.disabled : false)
+      },
+      // 是否禁用点击label选中,父组件的禁用会覆盖当前的禁用状态
+      isDisabledLabel() {
+        return this.disabledLabel ? this.disabledLabel : (this.parent ? this.parentData.disabledLabel : false)
+      },
+      // 尺寸
+      checkboxSize() {
+        return this.size ? this.size : (this.parent ? this.parentData.size : 34)
+      },
+      // 激活时的颜色
+      elAvtiveColor() {
+        return this.activeColor ? this.activeColor : (this.parent ? this.parentData.activeColor : '#01BEFF')
+      },
+      // 形状
+      elShape() {
+        return this.shape ? this.shape : (this.parent ? this.parentData.shape : 'square')
+      },
+      iconClass() {
+        let clazz = ''
+        clazz += (' tn-checkbox__icon-wrap--' + this.elShape)
+        
+        if (this.checkValue) clazz += ' tn-checkbox__icon-wrap--checked'
+        if (this.isDisabled) clazz += ' tn-checkbox__icon-wrap--disabled'
+        if (this.value && this.isDisabled) clazz += ' tn-checkbox__icon-wrap--disabled--checked'
+        
+        return clazz
+      },
+      iconStyle() {
+        let style = {}
+        // 判断是否用户手动禁用和传递的值
+        if (this.elAvtiveColor && this.checkValue && !this.isDisabled) {
+          style.borderColor = this.elAvtiveColor
+          style.backgroundColor = this.elAvtiveColor
+        }
+        
+        // checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
+        style.color = this.checkValue ? '#FFFFFF' : 'transparent'
+        
+        style.width = this.checkboxSize + 'rpx'
+        style.height = style.width
+        
+        style.fontSize = (this.iconSize ? this.iconSize : (this.parent ? this.parentData.iconSize : 20)) + 'rpx'
+        
+        return style
+      },
+      checkboxStyle() {
+        let style = {}
+        if (this.parent && this.parentData.width) {
+          // #ifdef MP
+          // 各家小程序因为它们特殊的编译结构,使用float布局
+          style.float = 'left';
+          // #endif
+          // #ifndef MP
+          // H5和APP使用flex布局
+          style.flex = `0 0 ${this.parentData.width}`;
+          // #endif
+        }
+        if(this.parent && this.parentData.wrap) {
+        	style.width = '100%';
+        	// #ifndef MP
+        	// H5和APP使用flex布局,将宽度设置100%,即可自动换行
+        	style.flex = '0 0 100%';
+        	// #endif
+        }
+        
+        return style
+      },
+      labelClass() {
+        let clazz = ''
+        if (this.isDisabled) {
+          clazz += ' tn-checkbox__label--disabled'
+        }
+        return clazz
+      }
+    },
+    data() {
+      return {
+        // 当前checkbox的value值
+        checkValue: false,
+        parentData: {
+          value: null,
+          max: null,
+          disabled: null,
+          disabledLabel: null,
+          shape: null,
+          activeColor: null,
+          size: null,
+          width: null,
+          wrap: null,
+          iconSize: null
+        }
+      }
+    },
+    watch: {
+      value(val) {
+        this.checkValue = val
+      }
+    },
+    created() {
+      // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+      // this.parent = this.$t.$parent.call(this, 'tn-checkbox-group')
+      // // 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
+      // this.parent && this.parent.children.push(this)
+      // // 初始化父组件的value值
+      // this.parent && this.parent.emitEvent()
+      this.updateParentData()
+      this.parent && this.parent.children.push(this)
+    },
+    methods: {
+      updateCheckValue() {
+        // 更新当前checkbox的选中状态
+        this.checkValue = (this.parent && this.parentData.value.includes(this.name)) || this.value === true
+        if (this.parent) {
+          if (this.value && !this.parentData.value.includes(this.name)) {
+            this.parentData.value.push(this.name)
+            this.parent.initValue(this.parentData.value)
+          }
+        }
+      },
+      updateParentData() {
+        this.getParentData('tn-checkbox-group')
+        this.updateCheckValue()
+      },
+      onClickLabel() {
+        if (!this.isDisabled && !this.isDisabledLabel) {
+          this.setValue()
+        }
+      },
+      toggle() {
+        if (!this.isDisabled) {
+          this.setValue()
+        }
+      },
+      emitEvent() {
+        this.$emit('change', {
+          name: this.name,
+          value: !this.checkValue
+        })
+        if (this.parent) {
+          this.checkValue = !this.checkValue
+          // 执行父组件tn-checkbox-group的事件方法
+          // 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
+          setTimeout(() => {
+            if(this.parent.emitEvent) this.parent.emitEvent();
+          }, 80)
+        }
+      },
+      // 设置input的值,通过v-modal绑定组件的值
+      setValue() {
+        // 判断是否为可选项组
+        if (this.parent) {
+          // 反转状态
+          if (this.checkValue === true) {
+            this.emitEvent()
+            // this.$emit('input', !this.checkValue)
+          } else {
+            // 超出最大可选项,弹出提示
+            if (this.parentData.value.length >= this.parentData.max) {
+              return this.$t.message.toast(`最多可选${this.parent.max}项`)
+            }
+            // 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
+            this.emitEvent();
+            // this.$emit('input', !this.checkValue);
+          }
+        } else {
+          // 只有一个可选项
+          this.emitEvent()
+          this.$emit('input', !this.checkValue)
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-checkbox {
+    /* #ifndef APP-NVUE */
+    display: inline-flex;
+    /* #endif */
+    align-items: center;
+    overflow: hidden;
+    user-select: none;
+    line-height: 1.8;
+    
+    &__icon-wrap {
+      color: $tn-font-color;
+      flex: none;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      box-sizing: border-box;
+      width: 42rpx;
+      height: 42rpx;
+      color: transparent;
+      text-align: center;
+      transition-property: color, border-color, background-color;
+      border: 1px solid $tn-font-sub-color;
+      transition-duration: 0.2s;
+      
+      /* #ifdef MP-TOUTIAO */
+      // 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
+      &__icon {
+      	line-height: 0;
+      }
+      /* #endif */
+      
+      &--circle {
+        border-radius: 100%;
+      }
+      
+      &--square {
+        border-radius: 6rpx;
+      }
+      
+      &--checked {
+        color: #FFFFFF;
+        background-color: $tn-main-color;
+        border-color: $tn-main-color;
+      }
+      
+      &--disabled {
+        background-color: $tn-font-holder-color;
+        border-color: $tn-font-sub-color;
+      }
+      
+      &--disabled--checked {
+        color: $tn-font-sub-color !important;
+      }
+    }
+    
+    &__label {
+      word-wrap: break-word;
+      margin-left: 10rpx;
+      margin-right: 24rpx;
+      color: $tn-font-color;
+      font-size: 30rpx;
+      
+      &--disabled {
+        color: $tn-font-sub-color;
+      }
+    }
+  }
+</style>

+ 223 - 0
tuniao-ui/components/tn-circle-progress/tn-circle-progress.vue

@@ -0,0 +1,223 @@
+<template>
+  <view
+    class="tn-circle-progress-class tn-circle-progress"
+    :style="{
+      width: widthPx + 'px',
+      height: widthPx + 'px'
+    }"
+  >
+    <!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
+    <!-- 默认圆环 -->
+    <canvas
+      class="tn-circle-progress__canvas-bg"
+      :canvas-id="elBgId"
+      :id="elBgId"
+      :style="{
+        width: widthPx + 'px',
+        height: widthPx + 'px'
+      }"
+    ></canvas>
+    <!-- 进度圆环 -->
+    <canvas
+      class="tn-circle-progress__canvas"
+      :canvas-id="elId"
+      :id="elId"
+      :style="{
+        width: widthPx + 'px',
+        height: widthPx + 'px'
+      }"
+    ></canvas>
+    <view class="tn-circle-progress__content">
+      <slot v-if="$slots.default || $slots.$default"></slot>
+      <view v-else-if="showPercent" class="tn-circle-progress__content__percent">{{ percent + '%' }}</view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-circle-progress',
+    props: {
+      // 进度(百分比)
+      percent: {
+        type: Number,
+        default: 0,
+        validator: val => {
+          return val >= 0 && val <= 100
+        }
+      },
+      // 圆环线宽
+      borderWidth: {
+        type: Number,
+        default: 14
+      },
+      // 整体圆的宽度
+      width: {
+        type: Number,
+        default: 200
+      },
+      // 是否显示条纹
+      striped: {
+        type: Boolean,
+        default: false
+      },
+      // 条纹是否运动
+      stripedActive: {
+        type: Boolean,
+        default: true
+      },
+      // 激活部分颜色
+      activeColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // 非激活部分颜色
+      inactiveColor: {
+        type: String,
+        default: '#f0f0f0'
+      },
+      // 是否显示进度条内部百分比值
+      showPercent: {
+        type: Boolean,
+        default: false
+      },
+      // 圆环执行动画的时间,ms
+      duration: {
+        type: Number,
+        default: 1500
+      }
+    },
+    data() {
+      return {
+        // 微信小程序中不能使用this.$t.uuid()形式动态生成id值,否则会报错
+        // #ifdef MP-WEIXIN
+        elBgId: 'tCircleProgressBgId',
+        elId: 'tCircleProgressElId',
+        // #endif
+        // #ifndef MP-WEIXIN
+        elBgId: this.$t.uuid(),
+        elId: this.$t.uuid(),
+        // #endif
+        // 活动圆上下文
+        progressContext: null,
+        // 转换成px为单位的背景宽度
+        widthPx: uni.upx2px(this.width || 200),
+        // 转换成px为单位的圆环宽度
+        borderWidthPx: uni.upx2px(this.borderWidth || 14),
+        // canvas画圆的起始角度,默认为-90度,顺时针
+        startAngle: -90 * Math.PI / 180,
+        // 动态修改进度值的时候,保存进度值的变化前后值
+        newPercent: 0,
+        oldPercent: 0
+      }
+    },
+    watch: {
+      percent(newVal, oldVal = 0) {
+        if (newVal > 100) newVal = 100
+        if (oldVal < 0) oldVal = 0
+        
+        this.newPercent = newVal
+        this.oldPercent = oldVal
+        setTimeout(() => {
+        	// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
+        	// 将此值减少或者新增到新的百分比值
+        	this.drawCircleByProgress(oldVal)
+        }, 50)
+      }
+    },
+    created() {
+      // 赋值,用于加载后第一个画圆使用
+      this.newPercent = this.percent;
+      this.oldPercent = 0;
+    },
+    mounted() {
+      setTimeout(() => {
+      	this.drawProgressBg()
+      	this.drawCircleByProgress(this.oldPercent)
+      }, 50)
+    },
+    methods: {
+      // 绘制进度条背景
+      drawProgressBg() {
+        let ctx = uni.createCanvasContext(this.elBgId, this)
+        // 设置线宽
+        ctx.setLineWidth(this.borderWidthPx)
+        // 设置颜色
+        ctx.setStrokeStyle(this.inactiveColor)
+        ctx.beginPath()
+        let radius = this.widthPx / 2
+        ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 360 * Math.PI / 180, false)
+        ctx.stroke()
+        ctx.draw()
+      },
+      // 绘制圆弧的进度
+      drawCircleByProgress(progress) {
+        // 如果已经存在则拿来使用
+        let ctx = this.progressContext
+        if (!ctx) {
+          ctx =uni.createCanvasContext(this.elId, this)
+          this.progressContext = ctx
+        }
+        ctx.setLineCap('round')
+        // 设置线条宽度和颜色
+        ctx.setLineWidth(this.borderWidthPx)
+        ctx.setStrokeStyle(this.activeColor)
+        // 将总过渡时间除以100,得出每修改百分之一进度所需的时间
+        let preSecondTime = Math.floor(this.duration / 100)
+        // 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
+        let endAngle = ((360 * Math.PI / 180) / 100) * progress + this.startAngle
+        let radius = this.widthPx / 2
+        ctx.beginPath()
+        ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false)
+        ctx.stroke()
+        ctx.draw()
+        
+        // 如果变更后新值大于旧值,意味着增大了百分比
+        if (this.newPercent > this.oldPercent) {
+          // 每次递增百分之一
+          progress++
+          // 如果新增后的值,大于需要设置的值百分比值,停止继续增加
+          if (progress > this.newPercent) return
+        } else {
+          progress--
+          if (progress < this.newPercent) return
+        }
+        setTimeout(() => {
+          // 定时器,每次操作间隔为time值,为了让进度条有动画效果
+          this.drawCircleByProgress(progress)
+        }, preSecondTime)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-circle-progress {
+    position: relative;
+    /* #ifndef APP-NVUE */
+    display: inline-flex;		
+    /* #endif */
+    align-items: center;
+    justify-content: center;
+    background-color: transparent;
+    
+    &__canvas {
+      position: absolute;
+      
+      &-bg {
+        position: absolute;
+      }
+    }
+    
+    &__content {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      
+      &__percent {
+        font-size: 28rpx;
+      }
+    }
+  }
+</style>

+ 236 - 0
tuniao-ui/components/tn-collapse-item/tn-collapse-item.vue

@@ -0,0 +1,236 @@
+<template>
+  <view class="tn-collapse-item-class tn-collapse-item" :style="[itemStyle]">
+    <!-- 头部 -->
+    <view
+      class="tn-collapse-item__head"
+      :style="[headStyle]"
+      :hover-stay-time="200"
+      :hover-class="hoverClass"
+      @tap.stop="headClick"
+    >
+      <block v-if="!$slots['title-all'] || !$slots['$title-all']">
+        <view
+          v-if="!$slots.title || !$slots.$title"
+          class="tn-collapse-item__head__title tn-text-ellipsis"
+          :style="[
+            { textAlign: align ? align : 'left'},
+            isShow && activeStyle && !arrow ? activeStyle : ''
+          ]"
+        >{{ title }}</view>
+        <view v-else>
+          <slot name="title"></slot>
+        </view>
+        <view class="tn-collapse-item__head__icon__wrap">
+          <view
+            v-if="arrow"
+            class="tn-icon-down tn-collapse-item__head__icon__arrow"
+            :class="{'tn-collapse-item__head__icon__arrow--active': isShow}"
+            :style="[arrowIconStyle]"
+          ></view>
+        </view>
+      </block>
+      <view v-else>
+        <slot name="title-all"></slot>
+      </view>
+    </view>
+    <!-- 内容 -->
+    <view
+      class="tn-collapse-item__body"
+      :style="[{
+        height: isShow ? height + 'px' : '0'
+      }]"
+    >
+      <view class="tn-collapse-item__body__content" :id="elId" :style="[bodyStyle]">
+        <slot></slot>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-collapse-item',
+    props: {
+      // 展开
+      open: {
+        type: Boolean,
+        default: false
+      },
+      // 唯一标识
+      name: {
+        type: String,
+        default: ''
+      },
+      // 标题
+      title: {
+        type: String,
+        default: ''
+      },
+      // 标题对齐方式
+      align: {
+        type: String,
+        default: 'left'
+      },
+      // 点击不收起
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      // 活动时样式
+      activeStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 标识
+      index: {
+        type: [Number, String],
+        default: ''
+      }
+    },
+    computed: {
+      arrowIconStyle() {
+        let style = {}
+        if (this.arrowColor) {
+          style.color = this.arrowColor
+        }
+        return style
+      }
+    },
+    data() {
+      return {
+        isShow: false,
+        elId: this.$t.uuid(),
+        // body高度
+        height: 0,
+        // 头部样式
+        headStyle: {},
+        // 主体样式
+        bodyStyle: {},
+        // item样式
+        itemStyle: {},
+        // 显示右边箭头
+        arrow: true,
+        // 箭头颜色
+        arrowColor: '',
+        // 点击头部时的效果样式
+        hoverClass: ''
+      }
+    },
+    watch: {
+      open(value) {
+        this.isShow = value
+      }
+    },
+    created() {
+      this.parent = false
+      this.isShow = this.open
+    },
+    mounted() {
+      this.init()
+    },
+    methods: {
+      // 异步获取内容或者修改了内容时重新获取内容的信息
+      init() {
+        this.parent = this.$t.$parent.call(this, 'tn-collapse')
+        if (this.parent) {
+          this.nameSync = this.name ? this.name : this.parent.childrens.length
+          // 不存在才添加对应实例
+          !this.parent.childrens.includes(this) && this.parent.childrens.push(this)
+          this.headStyle = this.parent.headStyle
+          this.bodyStyle = this.parent.bodyStyle
+          this.itemStyle = this.parent.itemStyle
+          this.arrow = this.parent.arrow
+          this.arrowColor = this.parent.arrowColor
+          this.hoverClass = this.parent.hoverClass
+        }
+        this.$nextTick(() => {
+          this.queryRect()
+        })
+      },
+      // 点击头部
+      headClick() {
+        if (this.disabled) return
+        if (this.parent && this.parent.accordion) {
+          this.parent.childrens.map(child => {
+            // 如果是手风琴模式,将其他的item关闭
+            if (this !== child) {
+              child.isShow = false
+            }
+          })
+        }
+        
+        this.isShow = !this.isShow
+        // 触发修改事件
+        this.$emit('change', {
+          index: this.index,
+          show: this.isShow
+        })
+        // 只有在打开时才触发父元素的change
+        if (this.isShow) this.parent && this.parent.onChange()
+        this.$forceUpdate()
+      },
+      // 查询内容高度
+      queryRect() {
+        this._tGetRect('#'+this.elId).then(res => {
+          this.height = res.height
+        })
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-collapse-item {
+    
+    &__head {
+      position: relative;
+      display: flex;
+      flex-direction: row;
+      justify-content: space-around;
+      align-items: center;
+      color: $tn-font-color;
+      font-size: 30rpx;
+      line-height: 1;
+      padding: 24rpx 0;
+      padding-left: 24rpx;
+      text-align: left;
+      background-color: #FFFFFF;
+      
+      &__title {
+        flex: 1;
+        overflow: hidden;
+      }
+      
+      &__icon {
+        &__arrow {
+          transition: all 0.3s;
+          margin-right: 20rpx;
+          margin-left: 14rpx;
+          font-size: inherit;
+          
+          &--active {
+            transform: rotate(180deg);
+            transform-origin: center center;
+          }
+        }
+      }
+    }
+    
+    &__body {
+      transition: all 0.3s;
+      overflow: hidden;
+      
+      &__content {
+        overflow: hidden;
+        font-size: 28rpx;
+        color: $tn-font-color;
+        text-align: left;
+        background-color: #FFFFFF;
+        padding-left: 24rpx;
+      }
+    }
+  }
+</style>

+ 98 - 0
tuniao-ui/components/tn-collapse/tn-collapse.vue

@@ -0,0 +1,98 @@
+<template>
+  <view class="tn-collapse-class tn-collapse">
+    <slot></slot>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-collapse',
+    props: {
+      // 是否为手风琴
+      accordion: {
+        type: Boolean,
+        default: true
+      },
+      // 头部样式
+      headStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 主题样式
+      bodyStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 每一个item的样式
+      itemStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 显示箭头
+      arrow: {
+        type: Boolean,
+        default: true
+      },
+      // 箭头颜色
+      arrowColor: {
+        type: String,
+        default: '#AAAAAA'
+      },
+      // 点击标题栏时的按压样式
+      hoverClass: {
+        type: String,
+        default: 'tn-hover'
+      }
+    },
+    computed: {
+      parentData() {
+        return [this.headStyle, this.bodyStyle, this.itemStyle, this.arrow, this.arrowColor, this.hoverClass]
+      }
+    },
+    data() {
+      return {
+        
+      }
+    },
+    watch: {
+      parentData() {
+        // 如果父组件的参数发生变化重新初始化子组件的信息
+        if (this.childrens.length > 0) {
+          this.init()
+        }
+      }
+    },
+    created() {
+      this.childrens = []
+    },
+    methods: {
+      // 重新初始化内部所有子元素计算高度,异步获取数据时重新渲染
+      init() {
+        this.childrens.forEach((child, index) => {
+          child.init()
+        })
+      },
+      // collapseItem被点击时由collapseItem调用父组件
+      onChange() {
+        let activeItem = []
+        this.childrens.forEach((child, index) => {
+          if (child.isShow) {
+            activeItem.push(child.nameSync)
+          }
+        })
+        // 如果时手风琴模式,只有一个匹配结果,即activeItem长度为1
+        if (this.accordion) activeItem = activeItem.join(',')
+        this.$emit('change', activeItem)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 318 - 0
tuniao-ui/components/tn-color-icon/tn-color-icon.vue

@@ -0,0 +1,318 @@
+<template>
+  <text
+    class="tn-color-icon-class tn-color-icon"
+    :class="[
+      'tn-color-icon-' + name
+    ]"
+    :style="{
+      fontSize: size + unit,
+      margin: margin
+    }"
+    @tap="handleClick"
+  ></text>
+</template>
+
+<script>
+  export default {
+    name: 'tn-color-icon',
+    props: {
+      // 索引
+      index: {
+        type: [Number, String],
+        default: '0'
+      },
+      // 图标名称
+      name: {
+        type: String,
+        default: ''
+      },
+      // 图标大小
+      size: {
+        type: Number,
+        default:32
+      },
+      // 大小单位
+      unit: {
+        type: String,
+        default: 'px'
+      },
+      // 外边距
+      margin: {
+        type: String,
+        default: '0'
+      }
+    },
+    computed: {
+      
+    },
+    data() {
+      return {
+        
+      }
+    },
+    methods: {
+      // 处理点击事件
+      handleClick() {
+        this.$emit("click", {
+          index: Number(this.index)
+        })
+        this.$emit("tap", {
+          index: Number(this.index)
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+  @charset "UTF-8";
+  
+  @font-face {
+    font-family: "tuniaoColorFont"; /* Project id 2445412 */
+    /* Color fonts */
+    src: url('iconfont.woff2?t=1632654518618') format('woff2');
+  }
+  
+  .tn-color-icon {
+    font-family: "tuniaoColorFont" !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    text-align: center;
+    text-decoration: none;
+  }
+  
+  .tn-color-icon-logo-github:before {
+    content: "\e601";
+  }
+  
+  .tn-color-icon-logo-qq:before {
+    content: "\e602";
+  }
+  
+  .tn-color-icon-logo-weixin:before {
+    content: "\e603";
+  }
+  
+  .tn-color-icon-logo-alipay:before {
+    content: "\e604";
+  }
+  
+  .tn-color-icon-logo-weibo:before {
+    content: "\e605";
+  }
+  
+  .tn-color-icon-logo-dingtalk:before {
+    content: "\e606";
+  }
+  
+  .tn-color-icon-safe:before {
+    content: "\e607";
+  }
+  
+  .tn-color-icon-wifi:before {
+    content: "\e608";
+  }
+  
+  .tn-color-icon-help:before {
+    content: "\e609";
+  }
+  
+  .tn-color-icon-tag:before {
+    content: "\e60a";
+  }
+  
+  .tn-color-icon-play:before {
+    content: "\e60b";
+  }
+  
+  .tn-color-icon-stopwatch:before {
+    content: "\e60c";
+  }
+  
+  .tn-color-icon-home:before {
+    content: "\e60d";
+  }
+  
+  .tn-color-icon-map:before {
+    content: "\e60e";
+  }
+  
+  .tn-color-icon-book:before {
+    content: "\e60f";
+  }
+  
+  .tn-color-icon-qrcode:before {
+    content: "\e610";
+  }
+  
+  .tn-color-icon-discover:before {
+    content: "\e611";
+  }
+  
+  .tn-color-icon-visitor:before {
+    content: "\e612";
+  }
+  
+  .tn-color-icon-menu:before {
+    content: "\e613";
+  }
+  
+  .tn-color-icon-renew:before {
+    content: "\e614";
+  }
+  
+  .tn-color-icon-business:before {
+    content: "\e615";
+  }
+  
+  .tn-color-icon-telephone:before {
+    content: "\e616";
+  }
+  
+  .tn-color-icon-medicine:before {
+    content: "\e617";
+  }
+  
+  .tn-color-icon-chicken:before {
+    content: "\e618";
+  }
+  
+  .tn-color-icon-clock:before {
+    content: "\e619";
+  }
+  
+  .tn-color-icon-download:before {
+    content: "\e61a";
+  }
+  
+  .tn-color-icon-lamp:before {
+    content: "\e61b";
+  }
+  
+  .tn-color-icon-hourglass:before {
+    content: "\e61c";
+  }
+  
+  .tn-color-icon-calendar:before {
+    content: "\e61d";
+  }
+  
+  .tn-color-icon-bluetooth:before {
+    content: "\e61e";
+  }
+  
+  .tn-color-icon-fish:before {
+    content: "\e61f";
+  }
+  
+  .tn-color-icon-seal:before {
+    content: "\e620";
+  }
+  
+  .tn-color-icon-remind:before {
+    content: "\e621";
+  }
+  
+  .tn-color-icon-music:before {
+    content: "\e622";
+  }
+  
+  .tn-color-icon-email:before {
+    content: "\e623";
+  }
+  
+  .tn-color-icon-medal:before {
+    content: "\e624";
+  }
+  
+  .tn-color-icon-image:before {
+    content: "\e625";
+  }
+  
+  .tn-color-icon-network:before {
+    content: "\e626";
+  }
+  
+  .tn-color-icon-wallet:before {
+    content: "\e627";
+  }
+  
+  .tn-color-icon-program:before {
+    content: "\e628";
+  }
+  
+  .tn-color-icon-shrimp:before {
+    content: "\e629";
+  }
+  
+  .tn-color-icon-collect:before {
+    content: "\e62a";
+  }
+  
+  .tn-color-icon-screw:before {
+    content: "\e62b";
+  }
+  
+  .tn-color-icon-set:before {
+    content: "\e62c";
+  }
+  
+  .tn-color-icon-userfavorite:before {
+    content: "\e62d";
+  }
+  
+  .tn-color-icon-useradd:before {
+    content: "\e62e";
+  }
+  
+  .tn-color-icon-honor:before {
+    content: "\e62f";
+  }
+  
+  .tn-color-icon-shop:before {
+    content: "\e630";
+  }
+  
+  .tn-color-icon-usercard:before {
+    content: "\e631";
+  }
+  
+  .tn-color-icon-school:before {
+    content: "\e632";
+  }
+  
+  .tn-color-icon-user:before {
+    content: "\e633";
+  }
+  
+  .tn-color-icon-internet:before {
+    content: "\e634";
+  }
+  
+  .tn-color-icon-time:before {
+    content: "\e635";
+  }
+  
+  .tn-color-icon-topic:before {
+    content: "\e636";
+  }
+  
+  .tn-color-icon-phone:before {
+    content: "\e637";
+  }
+  
+  .tn-color-icon-usertable:before {
+    content: "\e638";
+  }
+  
+  .tn-color-icon-userset:before {
+    content: "\e639";
+  }
+  
+  .tn-color-icon-game:before {
+    content: "\e63a";
+  }
+  
+</style>

+ 251 - 0
tuniao-ui/components/tn-column-notice/tn-column-notice.vue

@@ -0,0 +1,251 @@
+<template>
+  <view
+    class="tn-column-notice-class tn-column-notice"
+    :class="[backgroundColorClass]"
+    :style="[noticeStyle]"
+  >
+    <!-- 左图标 -->
+    <view class="tn-column-notice__icon">
+      <view
+        v-if="leftIcon"
+        class="tn-column-notice__icon--left" 
+        :class="[`tn-icon-${leftIconName}`,fontColorClass]"
+        :style="[fontStyle('leftIcon')]"
+        @tap="clickLeftIcon"></view>
+    </view>
+    
+    <!-- 滚动显示内容 -->
+    <swiper class="tn-column-notice__swiper" :style="[swiperStyle]" :vertical="vertical" circular :autoplay="autoplay && playStatus === 'play'" :interval="duration" @change="change">
+      <swiper-item v-for="(item, index) in list" :key="index" class="tn-column-notice__swiper--item">
+        <view
+          class="tn-column-notice__swiper--content tn-text-ellipsis"
+          :class="[fontColorClass]"
+          :style="[fontStyle()]"
+          @tap="click(index)"
+        >{{ item }}</view>
+      </swiper-item>
+    </swiper>
+    
+    <!-- 右图标 -->
+    <view class="tn-column-notice__icon">
+      <view
+        v-if="rightIcon"
+        class="tn-column-notice__icon--right" 
+        :class="[`tn-icon-${rightIconName}`,fontColorClass]"
+        :style="[fontStyle('rightIcon')]"
+        @tap="clickRightIcon"></view>
+      <view
+        v-if="closeBtn"
+        class="tn-column-notice__icon--right" 
+        :class="[`tn-icon-close`,fontColorClass]"
+        :style="[fontStyle('close')]"
+        @tap="close"></view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    name: 'tn-column-notice',
+    mixins: [componentsColorMixin],
+    props: {
+      // 显示的内容
+      list: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 是否显示
+      show: {
+        type: Boolean,
+        default: true
+      },
+      // 播放状态
+      // play -> 播放 paused -> 暂停
+      playStatus: {
+        type: String,
+        default: 'play'
+      },
+      // 滚动方向
+      // horizontal -> 水平滚动 vertical -> 垂直滚动
+      mode: {
+        type: String,
+        default: 'horizontal'
+      },
+      // 是否显示左边图标
+      leftIcon: {
+        type: Boolean,
+        default: true
+      },
+      // 左边图标的名称
+      leftIconName: {
+        type: String,
+        default: 'sound'
+      },
+      // 左边图标的大小
+      leftIconSize: {
+        type: Number,
+        default: 34
+      },
+      // 是否显示右边的图标
+      rightIcon: {
+        type: Boolean,
+        default: false
+      },
+      // 右边图标的名称
+      rightIconName: {
+        type: String,
+        default: 'right'
+      },
+      // 右边图标的大小
+      rightIconSize: {
+        type: Number,
+        default: 26
+      },
+      // 是否显示关闭按钮
+      closeBtn: {
+        type: Boolean,
+        default: false
+      },
+      // 圆角
+      radius: {
+        type: Number,
+        default: 0
+      },
+      // 内边距
+      padding: {
+        type: String,
+        default: '18rpx 24rpx'
+      },
+      // 自动播放
+      autoplay: {
+        type: Boolean,
+        default: true
+      },
+      // 滚动周期
+      duration: {
+        type: Number,
+        default: 2000
+      }
+    },
+    computed: {
+      fontStyle() {
+        return (type) => {
+          let style = {}
+          style.color = this.fontColorStyle ? this.fontColorStyle : ''
+          style.fontSize = this.fontSizeStyle ? this.fontSizeStyle : ''
+          if (type === 'leftIcon' && this.leftIconSize) {
+            style.fontSize = this.leftIconSize + 'rpx'
+          }
+          if (type === 'rightIcon' && this.rightIconSize) {
+            style.fontSize = this.rightIconSize + 'rpx'
+          }
+          if (type === 'close') {
+            style.fontSize = '24rpx'
+          }
+          
+          return style
+        }
+      },
+      noticeStyle() {
+        let style = {}
+        style.backgroundColor = this.backgroundColorStyle ? this.backgroundColorStyle : 'transparent'
+        if (this.padding) style.padding = this.padding
+        return style
+      },
+      swiperStyle() {
+        let style = {}
+        style.height = this.fontSize ? this.fontSize + 6 + this.fontUnit : '32rpx'
+        style.lineHeight = style.height
+        
+        return style
+      },
+      // 标记是否为垂直
+      vertical() {
+        if (this.mode === 'horizontal') return false
+        else return true
+      }
+    },
+    data() {
+      return {
+        
+      }
+    },
+    watch: {
+      
+    },
+    methods: {
+      // 点击了通知栏
+      click(index) {
+        this.$emit('click', index)
+      },
+      // 点击了关闭按钮
+      close() {
+        this.$emit('close')
+      },
+      // 点击了左边图标
+      clickLeftIcon() {
+        this.$emit('clickLeft')
+      },
+      // 点击了右边图标
+      clickRightIcon() {
+        this.$emit('clickRight')
+      },
+      // 切换消息时间
+      change(event) {
+        let index = event.detail.current
+        if (index === this.list.length - 1) {
+          this.$emit('end')
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-column-notice {
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: center;
+    flex-wrap: nowrap;
+    overflow: hidden;
+    
+    &__swiper {
+      height: auto;
+      flex: 1;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      margin-left: 12rpx;
+      
+      &--item {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        overflow: hidden;
+      }
+      
+      &--content {
+        overflow: hidden;
+      }
+    }
+    
+    &__icon {
+      &--left {
+        display: inline-flex;
+        align-items: center;
+      }
+      
+      &--right {
+        margin-left: 12rpx;
+        display: inline-flex;
+        align-items: center;
+      }
+    }
+  }
+</style>

+ 314 - 0
tuniao-ui/components/tn-count-down/tn-count-down.vue

@@ -0,0 +1,314 @@
+<template>
+  <view class="tn-countdown-class tn-countdown">
+    <view
+      v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
+      class="tn-countdown__item"
+      :class="[backgroundColorClass]"
+      :style="[itemStyle]"
+    >
+      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
+        {{ d }}
+      </view>
+    </view>
+    <view 
+      v-if="showHours && (hideZeroDay || (!hideZeroDay && d != '00'))"
+      class="tn-countdown__separator"
+      :style="{
+        fontSize: separatorSize + 'rpx',
+        color: separatorColor,
+        paddingBottom: separator === 'en' ? '4rpx' : 0
+      }"
+    >
+      {{ separator === 'en' ? ':' : '天'}}
+    </view>
+    
+    <view
+      v-if="showHours"
+      class="tn-countdown__item"
+      :class="[backgroundColorClass]"
+      :style="[itemStyle]"
+    >
+      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
+        {{ h }}
+      </view>
+    </view>
+    <view 
+      v-if="showMinutes"
+      class="tn-countdown__separator"
+      :style="{
+        fontSize: separatorSize + 'rpx',
+        color: separatorColor,
+        paddingBottom: separator === 'en' ? '4rpx' : 0
+      }"
+    >
+      {{ separator === 'en' ? ':' : '时'}}
+    </view>
+    
+    <view
+      v-if="showMinutes"
+      class="tn-countdown__item"
+      :class="[backgroundColorClass]"
+      :style="[itemStyle]"
+    >
+      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
+        {{ m }}
+      </view>
+    </view>
+    <view 
+      v-if="showSeconds"
+      class="tn-countdown__separator"
+      :style="{
+        fontSize: separatorSize + 'rpx',
+        color: separatorColor,
+        paddingBottom: separator === 'en' ? '4rpx' : 0
+      }"
+    >
+      {{ separator === 'en' ? ':' : '分'}}
+    </view>
+    
+    <view
+      v-if="showSeconds"
+      class="tn-countdown__item"
+      :class="[backgroundColorClass]"
+      :style="[itemStyle]"
+    >
+      <view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
+        {{ s }}
+      </view>
+    </view>
+    <view 
+      v-if="showSeconds && separator === 'cn'"
+      class="tn-countdown__separator"
+      :style="{
+        fontSize: separatorSize + 'rpx',
+        color: separatorColor,
+        paddingBottom: separator === 'en' ? '4rpx' : 0
+      }"
+    >
+      秒
+    </view>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    name: 'tn-count-down',
+    mixins: [componentsColorMixin],
+    props: {
+      // 倒计时时间,秒作为单位
+      timestamp: {
+        type: Number,
+        default: 0
+      },
+      // 是否自动开始
+      autoplay: {
+        type: Boolean,
+        default: true
+      },
+      // 数字框高度
+      height: {
+        type: [String, Number],
+        default: 'auto'
+      },
+      // 分隔符类型
+      // en -> 使用英文的冒号 cn -> 使用中文进行分割
+      separator: {
+        type: String,
+        default: 'en'
+      },
+      // 分割符大小
+      separatorSize: {
+        type: Number,
+        default: 30
+      },
+      // 分隔符颜色
+      separatorColor: {
+        type: String,
+        default: '#080808'
+      },
+      // 是否显示边框
+      showBorder: {
+        type: Boolean,
+        default: false
+      },
+      // 边框颜色
+      borderColor: {
+        type: String,
+        default: '#080808'
+      },
+      // 是否显示秒
+      showSeconds: {
+        type: Boolean,
+        default: true
+      },
+      // 是否显示分
+      showMinutes: {
+        type: Boolean,
+        default: true
+      },
+      // 是否显示时
+      showHours: {
+        type: Boolean,
+        default: true
+      },
+      // 是否显示天
+      showDays: {
+        type: Boolean,
+        default: true
+      },
+      // 如果当天的部分为0时,是否隐藏不显示
+      hideZeroDay: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      // 倒计时item的样式
+      itemStyle() {
+        let style = {}
+        if (this.height) {
+          style.height = this.$t.string.getLengthUnitValue(this.height)
+          style.width = style.height
+        }
+        if (this.showBorder) {
+          style.borderStyle = 'solid'
+          style.borderColor = this.borderColor
+          style.borderWidth = '1rpx'
+        }
+        style.backgroundColor = this.backgroundColorStyle || '#FFFFFF'
+        return style
+      },
+      // 倒计时数字样式
+      letterStyle() {
+        let style = {}
+        style.fontSize = this.fontSizeStyle || '30rpx'
+        style.color = this.fontColorStyle || '#080808'
+        return style
+      }
+    },
+    data() {
+      return {
+        d: '00',
+        h: '00',
+        m: '00',
+        s: '00',
+        // 定时器
+        timer: null,
+        // 记录倒计过程中变化的秒数
+        seconds: 0
+      }
+    },
+    watch: {
+      // 监听时间戳变化
+      timestamp(value) {
+        this.clearTimer()
+        this.start()
+      }
+    },
+    mounted() {
+      // 如果时自动倒计时,加载完成开始计时
+      this.autoplay && this.timestamp && this.start()
+    },
+    beforeDestroy() {
+      this.clearTimer()
+    },
+    methods: {
+      // 开始倒计时
+      start() {
+        // 避免可能出现的倒计时重叠情况
+        this.clearTimer()
+        if (this.timestamp <= 0) return
+        this.seconds = Number(this.timestamp)
+        this.formatTime(this.seconds)
+        this.timer = setInterval(() => {
+          this.seconds--
+          // 发出change事件
+          this.$emit('change', this.seconds)
+          if (this.seconds < 0) {
+            return this.end()
+          }
+          this.formatTime(this.seconds)
+        }, 1000)
+      },
+      // 格式化时间
+      formatTime(seconds) {
+        // 小于等于0的话,结束倒计时
+        seconds <= 0 && this.end()
+        let [day, hour, minute, second] = [0, 0, 0, 0]
+        day = Math.floor(seconds / (60 * 60 * 24))
+        // 如果不显示天,则将天对应的小时计入到小时中
+        // 先把当前的hour计算出来供分和秒使用
+        hour = Math.floor(seconds / (60 * 60)) - (day * 24)
+        let showHour = null
+        if (this.showDays) {
+          showHour = hour
+        } else {
+          // 将天数对应的小时加入到时中进行显示
+          showHour = Math.floor(seconds / (60 * 60))
+        }
+        minute = Math.floor(seconds / 60) - (hour * 60) - (day * 24 * 60)
+        second = Math.floor(seconds) - (minute * 60) - (hour * 60 * 60) - (day * 24 * 60 * 60)
+        // 如果小于0在前面进行补0操作
+        showHour = this.$t.number.formatNumberAddZero(showHour)
+        minute = this.$t.number.formatNumberAddZero(minute)
+        second = this.$t.number.formatNumberAddZero(second)
+        day = this.$t.number.formatNumberAddZero(day)
+        
+        this.d = day
+        this.h = showHour
+        this.m = minute
+        this.s = second
+      },
+      // 倒计时结束
+      end() {
+        this.clearTimer()
+        this.$emit('end')
+      },
+      // 清除倒计时
+      clearTimer() {
+        if (this.timer !== null) {
+          clearInterval(this.timer)
+          this.timer = null
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-countdown {
+    /* #ifndef APP-NVUE */
+    display: inline-flex;
+    /* #endif */
+    align-items: center;
+    
+    &__item {
+      box-sizing: content-box;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      padding: 2rpx;
+      border-radius: 6rpx;
+      white-space: nowrap;
+      transform: translateZ(0);
+      
+      &__time {
+        margin: 0;
+        padding: 0;
+        line-height: 1;
+      }
+    }
+    
+    &__separator {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      padding: 0 5rpx;
+      line-height: 1;
+    }
+  }
+</style>

+ 171 - 0
tuniao-ui/components/tn-count-scroll/tn-count-scroll.vue

@@ -0,0 +1,171 @@
+<template>
+  <view class="tn-count-scroll-class tn-count-scroll">
+    <view
+      v-for="(item, index) in columns"
+      :key="index"
+      class="tn-count-scroll__box"
+      :style="{
+        width: $t.string.getLengthUnitValue(width),
+        height: heightPxValue + 'px'
+      }"
+    >
+      <view
+        class="tn-count-scroll__column"
+        :style="{
+          transform: `translate3d(0, -${keys[index] * heightPxValue}px, 0)`,
+          transitionDuration: `${duration}s`
+        }"
+      >
+        <view
+          v-for="(value, value_index) in item"
+          :key="value_index"
+          class="tn-count-scroll__column__item"
+          :class="[fontColorClass]"
+          :style="{
+            height: heightPxValue + 'px',
+            lineHeight: heightPxValue + 'px',
+            fontSize: fontSizeStyle || '32rpx',
+            fontWeight: bold ? 'bold': 'normal',
+            color: fontColorStyle || '#080808'
+          }"
+        >
+          {{ value }}
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    name: 'tn-count-scroll',
+    mixins: [componentsColorMixin],
+    props: {
+      value: {
+        type: Number,
+        default: 0
+      },
+      // 行高
+      height: {
+        type: Number,
+        default: 32
+      },
+      // 单个字的宽度
+      width: {
+        type: [String, Number],
+        default: 'auto'
+      },
+      // 是否加粗
+      bold: {
+        type: Boolean,
+        default: false
+      },
+      // 持续时间
+      duration: {
+        type: Number,
+        default: 1.2
+      },
+      // 十分位分割符
+      decimalSeparator: {
+        type: String,
+        default: '.'
+      },
+      // 千分位分割符
+      thousandthsSeparator: {
+        type: String,
+        default: ''
+      }
+    },
+    computed: {
+      heightPxValue() {
+        return uni.upx2px(this.height || 0)
+      }
+    },
+    data() {
+      return {
+        // 每列的数据
+        columns: [],
+        // 每列对应值所在的滚动位置
+        keys: []
+      }
+    },
+    watch: {
+      value(val) {
+        this.initColumn(val)
+      }
+    },
+    created() {
+      // 为了达到一进入就有滚动效果,延迟执行初始化
+      this.initColumn()
+      setTimeout(() => {
+        this.initColumn(this.value)
+      }, 20)
+    },
+    methods: {
+      // 初始化每一列的数据
+      initColumn(val) {
+        val = val + ''
+        let digit = val.length,
+            columnArray = [],
+            rows = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
+        for (let i = 0; i < digit; i++) {
+          if (val[i] === this.decimalSeparator || val[i] === this.thousandthsSeparator) {
+            columnArray.push(val[i])
+          } else {
+            columnArray.push(rows)
+          }
+        }
+        this.columns = columnArray
+        this.roll(val)
+      },
+      // 滚动处理
+      roll(value) {
+        let valueArray = value.toString().split(''),
+            lengths = this.columns.length,
+            indexs = [];
+        
+        while (valueArray.length) {
+          let figure = valueArray.pop()
+          if (figure === this.decimalSeparator || figure === this.thousandthsSeparator) {
+            indexs.unshift(0)
+          } else {
+            indexs.unshift(Number(figure))
+          }
+        }
+        while(indexs.length < lengths) {
+          indexs.unshift(0)
+        }
+        this.keys = indexs
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-count-scroll {
+    display: inline-flex;
+    align-items: center;
+    justify-content: space-between;
+    
+    &__box {
+      overflow: hidden;
+    }
+    
+    &__column {
+      transform: translate3d(0, 0, 0);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-direction: column;
+      transition-timing-function: cubic-bezier(0, 1, 0, 1);
+      
+      &__item {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+  }
+</style>

+ 231 - 0
tuniao-ui/components/tn-count-to/tn-count-to.vue

@@ -0,0 +1,231 @@
+<template>
+  <view
+    class="tn-count-num-class tn-count-num"
+    :class="[fontColorClass]"
+    :style="{
+      fontSize: fontSizeStyle || '50rpx',
+      fontWeight: bold ? 'bold' : 'normal',
+      color: fontColorStyle || '#080808'
+    }"
+  >
+    {{ displayValue }}
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    name: 'tn-count-to',
+    mixins: [componentsColorMixin],
+    props: {
+      // 开始的数值,默认为0
+      startVal: {
+        type: Number,
+        default: 0
+      },
+      // 结束目标数值
+      endVal: {
+        type: Number,
+        default: 0,
+        required: true
+      },
+      // 是否自动开始
+      autoplay: {
+        type: Boolean,
+        default: true
+      },
+      // 滚动到目标值的持续时间,单位为毫秒
+      duration: {
+        type: Number,
+        default: 2000
+      },
+      // 是否在即将结束的时候使用缓慢滚动的效果
+      useEasing: {
+        type: Boolean,
+        default: true
+      },
+      // 显示的小数位数
+      decimals: {
+        type: Number,
+        default: 0
+      },
+      // 十进制的分割符
+      decimalSeparator: {
+        type: String,
+        default: '.'
+      },
+      // 千分位的分隔符
+      // 类似金额的分割(¥23,321.05中的",")
+      thousandthsSeparator: {
+        type: String,
+        default: ''
+      },
+      // 是否显示加粗字体
+      bold: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      countDown() {
+        return this.startVal > this.endVal
+      }
+    },
+    data() {
+      return {
+        localStartVal: this.startVal,
+        localDuration: this.duration,
+        // 显示的数值
+        displayValue: this.formatNumber(this.startVal),
+        // 打印的数值
+        printValue: null,
+        // 是否暂停
+        paused: false,
+        // 开始时间戳
+        startTime: null,
+        // 停留时间戳
+        remainingTime: null,
+        // 当前时间戳
+        timestamp: null,
+        // 上一次的时间戳
+        lastTime: 0,
+        rAF: null
+      }
+    },
+    watch: {
+      startVal() {
+        this.autoplay && this.start()
+      },
+      endVal() {
+        this.autoplay && this.start()
+      }
+    },
+    mounted() {
+      this.autoplay && this.start()
+    },
+    methods: {
+      // 开始滚动
+      start() {
+        this.localStartVal = this.startVal
+        this.startTime = null
+        this.localDuration = this.duration
+        this.paused = false
+        this.rAF = this.requestAnimationFrame(this.count)
+      },
+      // 重新开始
+      reStart() {
+        if (this.paused) {
+          this.resume()
+          this.paused = false
+        } else {
+          this.stop()
+          this.paused = true
+        }
+      },
+      // 停止
+      stop() {
+        this.cancelAnimationFrame(this.rAF)
+      },
+      // 恢复
+      resume() {
+        this.startTime = null
+        this.localDuration = this.remainingTime
+        this.localStartVal = this.printValue
+        this.requestAnimationFrame(this.count)
+      },
+      // 重置
+      reset() {
+        this.startTime = null
+        this.cnacelAnimationFrame(this.rAF)
+        this.displayValue = this.formatNumber(this.startVal)
+      },
+      // 销毁组件
+      destroyed() {
+        this.cancelAnimationFrame(this.rAF)
+      },
+      // 累加时间
+      count(timestamp) {
+        if (!this.startTime) this.startTime = timestamp
+        this.timestamp = timestamp
+        const progress = timestamp - this.startTime
+        this.remainingTime = this.localDuration - progress
+        if (this.useEasing) {
+          if (this.countDown) {
+            this.printValue = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration)
+          } {
+            this.printValue = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration)
+          }
+        } else {
+          if (this.countDown) {
+            this.printValue = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration)
+          } else {
+            this.printValue = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration)
+          }
+        }
+        if (this.countDown) {
+          this.printValue = this.printValue < this.endVal ? this.endVal : this.printValue
+        } else {
+          this.printValue = this.printValue > this.endVal ? this.endVal : this.printValue
+        }
+        
+        this.displayValue = this.formatNumber(this.printValue)
+        if (progress < this.localDuration) {
+          this.rAF = this.requestAnimationFrame(this.count)
+        } else {
+          this.$emit('end')
+        }
+      },
+      // 缓动时间计算
+      easingFn(t, b, c, d) {
+        return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
+      },
+      // 请求帧动画
+      requestAnimationFrame(cb) {
+        const currentTime = new Date().getTime()
+        // 为了使setTimteout的尽可能的接近每秒60帧的效果
+        const timeToCall = Math.max(0, 16 - (currentTime - this.lastTime))
+        const timerId = setTimeout(() => {
+          cb && cb(currentTime + timeToCall)
+        }, timeToCall)
+        this.lastTime = currentTime + timeToCall
+        return timerId
+      },
+      // 清除帧动画
+      clearAnimationFrame(timerId) {
+        clearTimeout(timerId)
+      },
+      // 格式化数值
+      formatNumber(number) {
+        const reg = /(\d+)(\d{3})/
+        number = Number(number)
+        number = number.toFixed(Number(this.decimals))
+        number += ''
+        const numberArray = number.split('.')
+        let num1 = numberArray[0]
+        const num2 = numberArray.length > 1 ? this.decimalSeparator + numberArray[1] : ''
+        
+        if (this.thousandthsSeparator && !this.isNumber(this.thousandthsSeparator)) {
+          while(reg.test(num1)) {
+            num1 = num1.replace(reg, '$1' + this.thousandthsSeparator + '$2')
+          }
+        }
+        return num1 + num2
+      },
+      // 判断是否为数字
+      isNumber(val) {
+        return !isNaN(parseFloat(val))
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-count-num {
+    /* #ifndef APP-NVUE */
+    display: inline-flex;
+    /* #endif */
+    text-align: center;
+    line-height: 1;
+  }
+</style>

+ 288 - 0
tuniao-ui/components/tn-custom-swiper-item/index.wxs

@@ -0,0 +1,288 @@
+
+function setTimeout(instance, cb, time) {
+  if (time > 0) {
+    var s = getDate().getTime()
+    var fn = function () {
+        if (getDate().getTime() - s > time) {
+            cb && cb()
+        } else
+            instance.requestAnimationFrame(fn)
+    }
+    fn()
+  }
+  else
+    cb && cb()
+}
+
+// 判断触摸的移动方向
+function decideSwiperDirection(startTouches, currentTouches, vertical) {
+  // 震动偏移容差
+  var toleranceShake = 150
+  // 移动容差
+  var toleranceTranslate = 10
+  
+  if (!vertical) {
+    // 水平方向移动
+    if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
+      // console.log(currentTouches.x, startTouches.x);
+      if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
+        if (currentTouches.x - startTouches.x > 0) {
+          return 'right'
+        } else if (currentTouches.x - startTouches.x < 0) {
+          return 'left'
+        }
+      }
+    }
+  } else {
+    // 垂直方向移动
+    if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
+      // console.log(currentTouches.x, startTouches.x);
+      if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
+        if (currentTouches.y - startTouches.y > 0) {
+          return 'down'
+        } else if (currentTouches.y - startTouches.y < 0) {
+          return 'up'
+        }
+      }
+    }
+  }
+  return ''
+}
+
+// swiperItem参数数据更新
+var itemDataObserver = function(newVal, oldVal, ownerInstance, instance) {
+  if (!newVal || newVal === 'undefined') return
+  var state = ownerInstance.getState()
+  state.itemData = newVal
+}
+
+// swiperIndex数据更新
+var currentIndexObserver = function(newVal, oldVal, ownerInstance, instance) {
+  if ((!newVal && newVal != 0) || newVal === 'undefined') return
+  var state = ownerInstance.getState()
+  state.currentIndex = newVal
+}
+
+// containerData数据更新
+var containerDataObserver = function(newVal, oldVal, ownerInstance, instance) {
+  if (!newVal || newVal === 'undefined') return
+  var state = ownerInstance.getState()
+  state.containerData = newVal
+}
+
+// 开始触摸
+var touchStart = function(event, ownerInstance) {
+  console.log('touchStart');
+  var instance = event.instance
+  var dataset = instance.getDataset()
+  var state = ownerInstance.getState()
+  var itemData = state.itemData
+  var containerData = state.containerData
+  
+  // 由于当前SwiperIndex初始为0,可能会导致swiperIndex数据没有更新
+  if (!state.currentIndex || state.currentIndex === 'undefined') {
+    state.currentIndex = 0
+  }
+  
+  if (!containerData || containerData.circular === 'undefined') {
+    containerData.circular = false
+  }
+  state.containerData = containerData
+  
+  // 如果当前切换动画还没执行结束,再次触摸会重新加载对应的swiperContainer的信息
+  // console.log(containerData.animationFinish);
+  if (!containerData.animationFinish) {
+    ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
+      status: 'reload'
+    })
+  }
+
+  // 判断是否为为当前显示的SwiperItem
+  if (itemData.index != state.currentIndex) return
+  
+  var touches = event.changedTouches[0]
+  if (!touches) return
+  
+  // 标记滑动开始时间
+  state.touchStartTime = getDate().getTime()
+  
+  // 记录当前滑动开始的x,y坐标
+  state.touchRelactive = {
+    x: touches.pageX,
+    y: touches.pageY
+  }
+  // 记录触摸id,用于处理多指的情况
+  state.touchId = touches.identifier
+  
+  // 标记开始触摸
+  state.touching = true
+  ownerInstance.callMethod('updateTouchingStatus', {
+    status: true
+  })
+}
+
+// 正在移动
+var touchMove = function(event, ownerInstance) {
+  console.log('touchMove');
+  var instance = event.instance
+  var dataset = instance.getDataset()
+  var state = ownerInstance.getState()
+  var itemData = state.itemData
+  var containerData = state.containerData
+  
+  // 判断是否为为当前显示的SwiperItem
+  if (itemData.index != state.currentIndex) return
+  
+  // 判断是否开始触摸
+  if (!state.touching) return
+  
+  var touches = event.changedTouches[0]
+  if (!touches) return
+  // 判断是否为同一个触摸点
+  if (state.touchId != touches.identifier) return
+  
+  var currentTouchRelactive = {
+    x: touches.pageX,
+    y: touches.pageY
+  }
+  
+  // 计算相对位移比例
+  if (containerData.vertical) {
+    var touchDistance = currentTouchRelactive.y - state.touchRelactive.y
+    var itemHeight = itemData.itemHeight
+    var distanceRate = touchDistance / itemHeight
+    // console.log(currentTouchRelactive.y, touchDistance, itemHeight, distanceRate);
+    
+    // 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
+    if (!containerData.circular &&
+      ((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
+    ) {
+      return
+    }
+    
+    // 如果超出了距离则不进行操作
+    if((Math.abs(touchDistance) > (itemData.itemTop + itemData.itemHeight))) {
+      ownerInstance.callMethod('updateParentSwiperContainerStyle', {
+        value: distanceRate < 0 ? -1 : 1
+      })
+      return
+    }
+  } else {
+    var touchDistance = currentTouchRelactive.x - state.touchRelactive.x
+    var itemWidth = itemData.itemWidth
+    var distanceRate = touchDistance / itemWidth
+    // console.log(currentTouchRelactive.x, touchDistance, itemWidth, distanceRate);
+    
+    // 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
+    if (!containerData.circular &&
+      ((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
+    ) {
+      return
+    }
+    
+    // 如果超出了距离则不进行操作
+    if((Math.abs(touchDistance) > (itemData.itemLeft + itemData.itemWidth))) {
+      ownerInstance.callMethod('updateParentSwiperContainerStyle', {
+        value: distanceRate < 0 ? -1 : 1
+      })
+      return
+    }
+  }
+  
+  ownerInstance.callMethod('updateParentSwiperContainerStyle', {
+    value: distanceRate
+  })
+}
+
+// 移动结束
+var touchEnd = function(event, ownerInstance) {
+  console.log('touchEnd');
+  var instance = event.instance
+  var dataset = instance.getDataset()
+  var state = ownerInstance.getState()
+  var itemData = state.itemData
+  var containerData = state.containerData
+  
+  // 判断是否为为当前显示的SwiperItem
+  if (itemData.index != state.currentIndex) return
+  
+  // 判断是否开始触摸
+  if (!state.touching) return
+  
+  var touches = event.changedTouches[0]
+  if (!touches) return
+  // 判断是否为同一个触摸点
+  if (state.touchId != touches.identifier) return
+  
+  
+  var currentTime = getDate().getTime()
+  var currentTouchRelactive = {
+    x: touches.pageX,
+    y: touches.pageY
+  }
+  
+  if (containerData.vertical) {
+    // 判断触摸移动方向
+    var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, true)
+    // 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
+    if (containerData.circular ||
+      !((state.currentIndex === 0 && direction === 'down') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'up'))
+    ) {
+      // 判断触摸的时间和移动的距离是否超过了当前itemHeight的一半,如果是则执行切换操作
+      // console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.y - state.touchRelactive.y));
+      if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.y - state.touchRelactive.y) < itemData.itemHeight / 2) {
+        ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
+          status: 'reset'
+        })
+      } else {
+        // console.log(direction, state.touchRelactive.y, currentTouchRelactive.y);
+        
+        ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
+          direction: direction
+        })
+      }
+    }
+  } else {
+    // 判断触摸移动方向
+    var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, false)
+    // 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
+    if (containerData.circular ||
+      !((state.currentIndex === 0 && direction === 'right') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'left'))
+    ) {
+      // 判断触摸的时间和移动的距离是否超过了当前itemWidth的一半,如果是则执行切换操作
+      // console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.x - state.touchRelactive.x));
+      if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.x - state.touchRelactive.x) < itemData.itemWidth / 2) {
+        ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
+          status: 'reset'
+        })
+      } else {
+        // console.log(direction, state.touchRelactive.x, currentTouchRelactive.x);
+        
+        ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
+          direction: direction
+        })
+      }
+    }
+  }
+  
+  // 清除标记
+  state.touchId = null
+  state.touchRelactive = null
+  state.touchStartTime = 0
+  
+  
+  // 标记停止触摸
+  state.touching = true
+  ownerInstance.callMethod('updateTouchingStatus', {
+    status: false
+  })
+}
+
+module.exports = {
+  itemDataObserver: itemDataObserver,
+  currentIndexObserver: currentIndexObserver,
+  containerDataObserver: containerDataObserver,
+  touchStart: touchStart,
+  touchMove: touchMove,
+  touchEnd: touchEnd
+}

+ 277 - 0
tuniao-ui/components/tn-custom-swiper-item/tn-custom-swiper-item.vue

@@ -0,0 +1,277 @@
+<template>
+  <!-- #ifdef MP-WEIXIN -->
+  <view
+    class="tn-c-swiper-item"
+    :style="[swiperStyle]"
+    :itemData="itemData"
+    :currentIndex="currentIndex"
+    :containerData="containerData"
+    :change:itemData="wxs.itemDataObserver"
+    :change:currentIndex="wxs.currentIndexObserver"
+    :change:containerData="wxs.containerDataObserver"
+    @touchstart="wxs.touchStart"
+    :catch:touchmove="touching?wxs.touchMove:''"
+    :catch:touchend="touching?wxs.touchEnd:''"
+  >
+    <view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
+      <slot></slot>
+    </view>
+  </view>
+  <!-- #endif -->
+  <!-- #ifndef MP-WEIXIN -->
+  <view
+    class="tn-c-swiper-item"
+    :style="[swiperStyle]"
+    :itemData="itemData"
+    :currentIndex="currentIndex"
+    :containerData="containerData"
+    :change:itemData="wxs.itemDataObserver"
+    :change:currentIndex="wxs.currentIndexObserver"
+    :change:containerData="wxs.containerDataObserver"
+    @touchstart="wxs.touchStart"
+    @touchmove="wxs.touchMove"
+    @touchend="wxs.touchEnd"
+  >
+    <view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
+      <slot></slot>
+    </view>
+  </view>
+  <!-- #endif -->
+</template>
+
+<script src="./index.wxs" lang="wxs" module="wxs"></script>
+<script>
+  export default {
+    name: 'tn-custom-swiper-item',
+    props: {
+      
+    },
+    computed: {
+      // swiperItem公共数据
+      itemData() {
+        return {
+          index: this.index,
+          itemWidth: this.itemWidth,
+          itemHeight: this.itemHeight,
+          itemTop: this.itemTop,
+          itemLeft: this.itemLeft
+        }
+      },
+      currentIndex() {
+        return this.parentData.currentIndex
+      },
+      containerData() {
+        return {
+          duration: this.parentData.duration,
+          animationFinish: this.parentData.swiperContainerAnimationFinish,
+          circular: this.parentData.circular,
+          swiperItemLength: this.swiperItemLength,
+          vertical: this.parentData.vertical
+        }
+      },
+      swiperStyle() {
+        let style = {}
+        style.transform = `translate3d(${this.translateX}%, ${this.translateY}%, 0px)`
+        return style
+      },
+      containerStyle() {
+        let style = {}
+        if (this.parentData.customSwiperStyle && Object.keys(this.parentData.customSwiperStyle).length > 0) {
+          style = this.parentData.customSwiperStyle
+        }
+        if ((this.currentIndex === 0 && this.index === this.swiperItemLength - 1) || (this.index === this.currentIndex - 1) && 
+          (this.parentData.prevSwiperStyle && Object.keys(this.parentData.prevSwiperStyle).length > 0)
+        ) {
+          // 前一个swiperItem
+          const copyStyle = JSON.parse(JSON.stringify(style))
+          style = Object.assign(copyStyle, this.parentData.prevSwiperStyle)
+        } 
+        if ((this.currentIndex === this.swiperItemLength - 1 && this.index === 0) || (this.index === this.currentIndex + 1) &&
+          (this.parentData.nextSwiperStyle && Object.keys(this.parentData.nextSwiperStyle).length > 0)
+        ) {
+          // 后一个swiperItem
+          const copyStyle = JSON.parse(JSON.stringify(style))
+          style = Object.assign(copyStyle, this.parentData.nextSwiperStyle)
+        }
+        return style
+      }
+    },
+    data() {
+      return {
+        // 父组件参数
+        parentData: {
+          duration: 500,
+          currentIndex: 0,
+          swiperContainerAnimationFinish: false,
+          circular: false,
+          vertical: false,
+          prevSwiperStyle: {},
+          customSwiperStyle: {},
+          nextSwiperStyle: {}
+        },
+        // 标记当前是否正在触摸
+        touching: true,
+        // 当前swiperItem的偏移位置
+        translateX: 0,
+        translateY: 0,
+        // 当前swiperItem的宽高
+        itemWidth: 0,
+        itemHeight: 0,
+        // 当前swiperItem的位置信息
+        itemTop: 0,
+        itemLeft: 0,
+        // 当前swiperItem的状态 prev current next
+        status: 'current',
+        // 当前swiperItem的index序号
+        index: 0,
+        // swiperItem的的数量
+        swiperItemLength: 0
+      }
+    },
+    created() {
+      this.parent = false
+      this.updateParentData()
+      // 获取当前父组件children的数量作为当前swiperItem的序号
+      this.index = this.parent.children.length
+      this.parent && this.parent.children.push(this)
+    },
+    mounted() {
+      this.$nextTick(() => {
+        this.initSwiperItem()
+      })
+    },
+    methods: {
+      // 初始化swiperItem
+      initSwiperItem() {
+        this.getSwiperItemRect(() => {
+          this.parent.updateAllSwiperItemStyle()
+          this.parentData.swiperContainerAnimationFinish = true
+        })
+      },
+      // 获取swiperItem的信息
+      async getSwiperItemRect(callback) {
+        const swiperItemRes = await this._tGetRect('.tn-c-swiper-item')
+        if (!swiperItemRes.height || !swiperItemRes.width) {
+          setTimeout(() => {
+            this.getSwiperItemRect()
+          }, 30)
+          return
+        }
+        
+        this.itemWidth = swiperItemRes.width
+        this.itemHeight = swiperItemRes.height
+        this.itemTop = swiperItemRes.top
+        this.itemLeft = swiperItemRes.left
+        callback && callback()
+      },
+      // 更新swiperItem样式
+      updateSwiperItemStyle(swiperItemLength, currentIndex = undefined) {
+        currentIndex = currentIndex != undefined ? currentIndex : this.parentData.currentIndex
+        this.swiperItemLength = swiperItemLength
+        // 根据当前swiperItem的序号设置偏移位置
+        // 判断当前swiperItem是否为第一个,如果是则将最后的swiperItem移动到当前的前一个位置(即最前面)
+        if (currentIndex === 0 && this.index === swiperItemLength - 1) {
+          if (this.parentData.vertical) {
+            this.translateX = 0
+            this.translateY = -100
+          } else {
+            this.translateX = -100
+            this.translateY = 0
+          }
+        } 
+        // 判断当前swiperItem是否为最后一个,如果是则将最前的swiperItem移动到当前的后一个位置(即最后面)
+        else if (currentIndex === swiperItemLength - 1 && this.index === 0) {
+          if (this.parentData.vertical) {
+            this.translateX = 0
+            this.translateY = swiperItemLength * 100
+          } else {
+            this.translateX = swiperItemLength * 100
+            this.translateY = 0
+          }
+        }
+        // 正常情况
+        else {
+          if (this.parentData.vertical) {
+            this.translateX = 0
+            this.translateY = this.index * 100
+          } else {
+            this.translateX = this.index * 100
+            this.translateY = 0
+          }
+        }
+      },
+      // 更新父组件的偏移位置信息
+      updateParentSwiperContainerStyle(e) {
+        this.parent.updateSwiperContainerStyleWithValue(e.value)
+      },
+      // 根据方向更新父组件的偏移位置信息
+      updateParentSwiperContainerStyleWithDirection(e) {
+        this.parent.updateSwiperContainerStyleWithDirection(e.direction)
+      },
+      // 修改父组件的偏移位置的状态
+      changeParentSwiperContainerStyleStatus(e) {
+        // reset -> 重置 reload -> 重载
+        this.parent.updateSwiperContainerStyleWithDirection(e.status)
+      },
+      // 更新父组件信息
+      updateParentData() {
+        this.getParentData('tn-custom-swiper')
+      },
+      // 更新触摸状态
+      updateTouchingStatus(e) {
+        this.touching = e.status
+        if (e.status) {
+          this.parent.stopAutoPlay()
+        } else {
+          this.parent.startAutoPlay()
+        }
+      },
+      // 提取对应用户自定义样式
+      extractCustomStyle(customStyle) {
+        let data = {
+          transform: {},
+          style: {}
+        }
+        if (!customStyle) return data
+        // 允许设置的transform参数
+        const allowTransformProps = ['scale','scaleX','scaleY','scaleZ','rotate','rotateX','rotateY','rotateZ']
+        for (let prop in customStyle) {
+          if (prop.startsWith('transformProp')) {
+            // transform里面的样式
+            let transformProp = prop.substring('transformProp'.length)
+            const index = allowTransformProps.findIndex((item) => {
+              return item.toLowerCase() === transformProp.toLowerCase()
+            })
+            if (index !== -1) {
+              transformProp = allowTransformProps[index]
+              data.transform[transformProp] = customStyle[prop]
+            }
+          } else {
+            // 普通样式
+            data.style[prop] = customStyle[prop]
+          }
+        }
+        return data
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-c-swiper-item {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    display: block;
+    will-change: transform;
+    cursor: none;
+    transform: translate3d(0px, 0px, 0px);
+    
+    .item__container {
+      width: 100%;
+      height: 100%;
+      display: block;
+      position: absolute;
+    }
+  }
+</style>

+ 535 - 0
tuniao-ui/components/tn-custom-swiper/tn-custom-swiper.vue

@@ -0,0 +1,535 @@
+<template>
+  <view
+    class="tn-c-swiper-class tn-c-swiper"
+  >
+    <!-- 轮播item容器-->
+    <view class="tn-swiper__container" :style="[swiperContainerStyle]" :animation="containerAnimation">
+      <slot></slot>
+    </view>
+    
+    <!-- 轮播指示器-->
+    <view v-if="indicator" class="tn-swiper__indicator" :class="[`tn-swiper__indicator--${vertical ? 'vertical' : 'horizontal'}`]" :style="[indicatorStyle]">
+      <!-- 方形 -->
+      <block v-if="indicatorType === 'rect'">
+        <view
+          v-for="(item, index) in children.length"
+          :key="index"
+          class="tn-swiper__indicator__rect"
+          :class="[
+            `tn-swiper__indicator__rect--${vertical ? 'vertical' : 'horizontal'}`, 
+            currentIndex === index ? `tn-swiper__indicator__rect--active tn-swiper__indicator__rect--active--${vertical ? 'vertical' : 'horizontal'}` : ''
+          ]"
+          :style="[indicatorPointStyle(index)]"
+        ></view>
+      </block>
+      <!-- 点 -->
+      <block v-if="indicatorType === 'dot'">
+        <view
+          v-for="(item, index) in children.length"
+          :key="index"
+          class="tn-swiper__indicator__dot"
+          :class="[
+            `tn-swiper__indicator__dot--${vertical ? 'vertical' : 'horizontal'}`,
+            currentIndex === index ? `tn-swiper__indicator__dot--active tn-swiper__indicator__dot--active--${vertical ? 'vertical' : 'horizontal'}` : ''
+          ]"
+          :style="[indicatorPointStyle(index)]"
+        ></view>
+      </block>
+      <!-- 圆角方形 -->
+      <block v-if="indicatorType === 'round'">
+        <view
+          v-for="(item, index) in children.length"
+          :key="index"
+          class="tn-swiper__indicator__round"
+          :class="[
+            `tn-swiper__indicator__round--${vertical ? 'vertical' : 'horizontal'}`,
+            currentIndex === index ? `tn-swiper__indicator__round--active tn-swiper__indicator__round--active--${vertical ? 'vertical' : 'horizontal'}` : ''
+          ]"
+          :style="[indicatorPointStyle(index)]"
+        ></view>
+      </block>
+      <!-- 序号 -->
+      <block v-if="indicatorType === 'number' && !vertical">
+        <view class="tn-swiper__indicator__number">{{ currentIndex + 1 }}/{{ children.length }}</view>
+      </block>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-custom-swiper',
+    props: {
+      // 当前所在的轮播位置
+      current: {
+        type: Number,
+        default: 0
+      },
+      // 自动切换
+      autoplay: {
+        type: Boolean,
+        default: false
+      },
+      // 自动切换时间间隔
+      interval: {
+        type: Number,
+        default: 5000
+      },
+      // 滑动动画时长
+      duration: {
+        type: Number,
+        default: 500
+      },
+      // 是否采用衔接滑动
+      circular: {
+        type: Boolean,
+        default: false
+      },
+      // 滑动方向为纵向
+      vertical: {
+        type: Boolean,
+        default: false
+      },
+      // 显示指示点
+      indicator: {
+        type: Boolean,
+        default: false
+      },
+      // 指示点类型
+      // rect -> 方形 round -> 圆角方形 dot -> 点 number -> 轮播图下标
+      indicatorType: {
+        type: String,
+        default: 'dot'
+      },
+      // 指示点的位置
+      // topLeft \ topCenter \ topRight \ bottomLeft \ bottomCenter \ bottomRight
+      indicatorPosition: {
+        type: String,
+        default: 'bottomCenter'
+      },
+      // 指示点激活时颜色
+      indicatorActiveColor: {
+        type: String,
+        default: ''
+      },
+      // 指示点未激活时颜色
+      indicatorInactiveColor: {
+        type: String,
+        default: ''
+      },
+      // 前一个轮播的自定义样式
+      prevSwiperStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 当前轮播的自定义样式
+      customSwiperStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 后一个轮播的自定义样式
+      nextSwiperStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      }
+    },
+    computed: {
+      parentData() {
+        return [
+          this.duration,
+          this.currentIndex,
+          this.swiperContainerAnimationFinish,
+          this.circular,
+          this.vertical,
+          this.prevSwiperStyle,
+          this.customSwiperStyle,
+          this.nextSwiperStyle
+        ]
+      },
+      indicatorStyle() {
+        let style = {}
+        if (this.vertical) {
+          if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
+          if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent =  'center'
+          if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent =  'flex-end'
+          if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
+            if (this.vertical) {
+              style.right = '12rpx'
+              style.left = 'auto'
+            } else {
+              style.top = '12rpx'
+              style.bottom = 'auto'
+            }
+          } else {
+            if (this.vertical) {
+              style.right = 'auto'
+              style.left = '12rpx'
+            } else {
+              style.top = 'auto'
+              style.bottom = '12rpx'
+            }
+          }
+        } else {
+          if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
+          if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent =  'center'
+          if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent =  'flex-end'
+          if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
+            style.top = '12rpx'
+            style.bottom = 'auto'
+          } else {
+            style.top = 'auto'
+            style.bottom = '12rpx'
+          }
+        }
+        return style
+      },
+      indicatorPointStyle() {
+        return (index) => {
+          let style = {}
+          if (index === this.currentIndex && this.indicatorActiveColor !== '') {
+            style.backgroundColor = this.indicatorActiveColor
+          } else if (this.indicatorInactiveColor !== '') {
+            style.backgroundColor = this.indicatorInactiveColor
+          }
+          return style
+        }
+      }
+    },
+    watch: {
+      parentData() {
+        if (this.children.length) {
+          this.children.forEach((item) => {
+            // 判断子组件如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+            typeof(item.updateParentData) === 'function' && item.updateParentData()
+          })
+        }
+      },
+      current(nVal, oVal) {
+        if (this.currentIndex === nVal) return
+        this.currentIndex = nVal > this.children.length ? this.children.length - 1 : nVal
+        this.swiperContainerAnimationFinish = false
+        // 设置动画过渡时间
+        this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
+        this.updateSwiperContainerItem(oVal)
+      }
+    },
+    data() {
+      return {
+        // 清除动画定时器
+        clearAnimationTimer: null,
+        // 前后衔接执行定时器
+        convergeTimer: null,
+        // 自动轮播Timer
+        autoPlayTimer: null,
+        // 当前选中的轮播
+        currentIndex: this.current,
+        // swiperContainer样式
+        swiperContainerStyle: {
+          transform: 'translate3d(0px, 0px, 0px)',
+          transitionDuration: '0ms'
+        },
+        // swiperContainer动画
+        containerAnimation: {},
+        // 滑动动画结束标记
+        swiperContainerAnimationFinish: false
+      }
+    },
+    created() {
+      this.children = []
+    },
+    mounted() {
+      this.$nextTick(() => {
+        const index = this.currentIndex > this.children.length ? this.children.length - 1 : this.currentIndex
+        this.updateSwiperContainerStyle(index)
+        this.startAutoPlay()
+      })
+    },
+    methods: {
+      // 更新全部swiperItem的样式
+      updateAllSwiperItemStyle() {
+        this.children.forEach((item, index) => {
+          typeof(item.updateSwiperItemStyle) === 'function' && item.updateSwiperItemStyle(this.children.length)
+        })
+        
+      },
+      // 根据swiperIndex更新swiperItemContainer的样式
+      updateSwiperContainerStyle(index) {
+        if (this.vertical) {
+          this.swiperContainerStyle.transform = `translate3d(0px, ${-index * 100}%, 0px)`
+        } else {
+          this.swiperContainerStyle.transform = `translate3d(${-index * 100}%, 0px, 0px)`
+        }
+      },
+      // 根据传递的值更新swiperItemContainer的位置
+      updateSwiperContainerStyleWithValue(value) {
+        if (this.vertical) {
+          this.swiperContainerStyle.transform = `translate3d(0px, ${(-this.currentIndex * 100) + value * 100}%, 0px)`
+        } else {
+          this.swiperContainerStyle.transform = `translate3d(${(-this.currentIndex * 100) + value * 100}%, 0px, 0px)`
+        }
+      },
+      // 根据传递的方向更新swiperItemContainer的位置
+      updateSwiperContainerStyleWithDirection(direction) {
+        const oldCurrent = this.currentIndex
+        const childrenLength = this.children.length
+        const lastSwiperItemIndex = childrenLength - 1
+        this.swiperContainerAnimationFinish = false
+        
+        
+        // 向后切换一个SwiperItem
+        if (direction === 'reset') {
+          // 设置动画过渡时间
+          this.swiperContainerStyle.transitionDuration = `${this.duration}ms`
+          this.updateSwiperContainerStyle(this.currentIndex)
+          this.clearAnimationTimer = setTimeout(() => {
+            this.clearSwiperContainerAnimation()
+          }, this.duration)
+        } else if (direction === 'reload') {
+          this.clearConvergeSwiperItemTimer()
+          this.clearSwiperContainerAnimation()
+          this.updateSwiperItemStyle(0)
+          this.updateSwiperItemStyle(lastSwiperItemIndex)
+        } else {
+          if (direction === 'left' || direction === 'up') {
+            if (oldCurrent === childrenLength - 1 && !this.circular) {
+              this.clearSwiperContainerAnimation()
+              this.clearConvergeSwiperItemTimer()
+              return
+            } 
+            this.currentIndex = oldCurrent + 1 >= childrenLength ? 0 : oldCurrent + 1
+          } else if (direction === 'right' || direction === 'down') {
+            if (oldCurrent === 0 && !this.circular) {
+              this.clearSwiperContainerAnimation()
+              this.clearConvergeSwiperItemTimer()
+              return
+            } 
+            this.currentIndex = oldCurrent - 1 < 0 ? childrenLength - 1 : oldCurrent - 1
+          }
+          // 设置动画过渡时间
+          this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
+          // this.updateSwiperItemContainerRect(this.currentIndex)
+        }
+        
+        // console.log(direction, oldCurrent, this.currentIndex);
+        this.updateSwiperContainerItem(oldCurrent)
+        
+        // 切换轮播时触发事件
+        this.$emit('change', {
+          current: this.currentIndex
+        })
+      },
+      // 设置自动轮播
+      startAutoPlay() {
+        if (this.autoplay && !this.autoPlayTimer && this.circular) {
+          this.autoPlayTimer = setInterval(() => {
+            this.updateSwiperContainerStyleWithDirection('left')
+          }, this.interval)
+        }
+      },
+      // 停止自动轮播
+      stopAutoPlay() {
+        if (this.autoPlayTimer) {
+          clearInterval(this.autoPlayTimer)
+          this.autoPlayTimer = null
+        }
+      },
+      // 更新swiperContainer和swiperItem相关联信息
+      updateSwiperContainerItem(oldCurrent) {
+        const childrenLength = this.children.length
+        const lastSwiperItemIndex = childrenLength - 1
+        // 判断当前是否为头尾,如果是更新对应的头尾SwiperItem样式
+        // 更新swiperItemContainer的样式
+        if (oldCurrent === 0 && this.currentIndex === lastSwiperItemIndex) {
+          // 先移动到最左边然后再去除动画偏移到正常的位置
+          // this.swiperContainerStyle.transform = `translate3d(100%, 0px, 0px)`
+          this.updateSwiperContainerStyle(-1)
+          this.clearSwiperContainerAnimationTimer()
+          this.clearAnimationTimer = setTimeout(() => {
+            this.convergeSwiperItem()
+          }, this.duration)
+        } else if (oldCurrent === lastSwiperItemIndex && this.currentIndex === 0) {
+          // 先移动到最右边然后再去除动画偏移到正常的位置
+          // this.swiperContainerStyle.transform = `translate3d(${-childrenLength * 100}%, 0px, 0px)`
+          this.updateSwiperContainerStyle(childrenLength)
+          this.clearSwiperContainerAnimationTimer()
+          this.clearAnimationTimer = setTimeout(() => {
+            this.convergeSwiperItem()
+          }, this.duration)
+        } else {
+          this.updateSwiperContainerStyle(this.currentIndex)
+          this.updateSwiperItemStyle(0)
+          this.updateSwiperItemStyle(lastSwiperItemIndex)
+          this.clearAnimationTimer = setTimeout(() => {
+            this.clearSwiperContainerAnimation()
+          }, this.duration)
+        }
+      },
+      // 更新对应swiperItem的信息
+      updateSwiperItemStyle(index) {
+        const childrenLength = this.children.length
+        if (index < 0) index = 0
+        if (index > childrenLength - 1) index = childrenLength - 1
+        
+        typeof(this.children[index].updateSwiperItemStyle) === 'function' && this.children[index].updateSwiperItemStyle(childrenLength, this.currentIndex)
+      },
+      // 更新对应swiperItem的容器信息
+      updateSwiperItemContainerRect(index) {
+        const childrenLength = this.children.length
+        if (index < 0) index = 0
+        if (index > childrenLength - 1) index = childrenLength - 1
+        
+        typeof(this.children[index].getSwiperItemRect) === 'function' && this.children[index].getSwiperItemRect()
+      },
+      // 执行前后衔接
+      convergeSwiperItem() {
+        const lastSwiperItemIndex = this.children.length - 1
+        this.clearSwiperContainerAnimation()
+        this.clearConvergeSwiperItemTimer()
+        this.convergeTimer = setTimeout(() => {
+          this.updateSwiperItemStyle(0)
+          this.updateSwiperItemStyle(lastSwiperItemIndex)
+          this.updateSwiperContainerStyle(this.currentIndex)
+          this.clearConvergeSwiperItemTimer()
+        }, 30)
+      },
+      // 停止/清除切换动画
+      clearSwiperContainerAnimation() {
+        this.swiperContainerStyle.transitionDuration = `0ms`
+        this.swiperContainerAnimationFinish = true
+        this.clearSwiperContainerAnimationTimer()
+      },
+      // 停止/清除执行前后衔接定时器
+      clearConvergeSwiperItemTimer() {
+        if (this.convergeTimer) {
+          clearTimeout(this.convergeTimer)
+          this.convergeTimer = null
+        }
+      },
+      // 停止/清除切换动画定时器
+      clearSwiperContainerAnimationTimer() {
+        if (this.clearAnimationTimer) {
+          clearTimeout(this.clearAnimationTimer)
+          this.clearAnimationTimer = null
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-c-swiper {
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+    height: 100%;
+    
+    .tn-swiper {
+      &__container {
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        top: 0;
+        left: 0;
+        
+        will-change: transform;
+        transition-property: all;
+        transition-timing-function: ease-out;
+      }
+      
+      &__indicator {
+        position: absolute;
+        display: flex;
+        z-index: 1;
+        
+        &--horizontal {
+          padding: 0 24rpx;
+          flex-direction: row;
+          width: 100%;
+        }
+        &--vertical {
+          padding: 24rpx 0;
+          flex-direction: column;
+          height: 100%;
+        }
+        
+        &__rect {
+          background-color: rgba(0, 0, 0, 0.3);
+          transition: all 0.5s;
+          
+          &--horizontal {
+            width: 26rpx;
+            height: 8rpx;
+          }
+          &--vertical {
+            width: 8rpx;
+            height: 26rpx;
+          }
+          
+          &--active {
+            background-color: rgba(255, 255, 255, 0.8);
+          }
+        }
+        
+        &__dot {
+          width: 14rpx;
+          height: 14rpx;
+          border-radius: 20rpx;
+          background-color: rgba(0, 0, 0, 0.3);
+          transition: all 0.5s;
+          
+          &--horizontal {
+            margin: 0 6rpx;
+          }
+          &--vertical {
+            margin: 6rpx 0;
+          }
+          
+          &--active {
+            background-color: rgba(255, 255, 255, 0.8);
+          }
+        }
+        
+        &__round {
+          width: 14rpx;
+          height: 14rpx;
+          border-radius: 20rpx;
+          background-color: rgba(0, 0, 0, 0.3);
+          transition: all 0.5s;
+          
+          &--horizontal {
+            margin: 0 6rpx;
+          }
+          &--vertical {
+            margin: 6rpx 0;
+          }
+          
+          &--active {
+            background-color: rgba(255, 255, 255, 0.8);
+            
+            &--horizontal {
+              width: 34rpx;
+            }
+            &--vertical {
+              height: 34rpx;
+            }
+          }
+        }
+        
+        &__number {
+          padding: 6rpx 16rpx;
+          line-height: 1;
+          background-color: rgba(0, 0, 0, 0.3);
+          color: rgba(255, 255, 255, 0.8);
+          border-radius: 100rpx;
+          font-size: 26rpx;
+        }
+      }
+    }
+  }
+</style>

+ 265 - 0
tuniao-ui/components/tn-drag/index.wxs

@@ -0,0 +1,265 @@
+// 判断是否出界
+var isOutRange = function(x1, y1, x2, y2, x3, y3) {
+  return x1 < 0 || x1 >= y1 || x2 < 0 || x2 >= y2 || x3 < 0 || x3 >= y3
+}
+var edit = false
+
+function bool(str) {
+  return str === 'true' || str === true
+}
+/**
+ * 排序核心
+ * @param {Object} startKey 开始时位置
+ * @param {Object} endKey 结束时位置
+ * @param {Object} instance wxs内的局部变量快照
+ */
+var sortCore = function(startKey, endKey, state) {
+  var basedata = state.basedata
+  var excludeFix = function(sortKey, type) {
+    // fixed 元素位置不会变化, 这里直接用 sortKey 获取,更加便捷
+    if (state.list[sortKey].fixed) {
+      var _sortKey = type ? --sortKey : ++sortKey
+      return excludeFix(sortKey, type)
+    }
+    return sortKey
+  }
+  
+  // 先获取到 endKey 对应的 realKey, 防止下面排序过程中该 realKey 被修改
+  var endRealKey = -1
+  state.list.forEach(function(item) {
+    if (item.sortKey === endKey) endRealKey = item.realKey
+  })
+  
+  return state.list.map(function(item) {
+    if (item.fixed) return item
+    var sortKey = item.sortKey
+    var realKey = item.realKey
+    
+    if (startKey < endKey) {
+      // 正序拖动
+      if (sortKey > startKey && sortKey <= endKey) {
+        --realKey
+        sortKey =  excludeFix(--sortKey, true)
+      } else if (sortKey === startKey) {
+        realKey = endRealKey
+        sortKey = endKey
+      }
+    } else if (startKey > endKey) {
+      // 倒序拖动
+      if (sortKey >= endKey && sortKey < startKey) {
+        ++realKey
+        sortKey = excludeFix(++sortKey, false)
+      } else if (sortKey === startKey) {
+        realKey = endRealKey
+        sortKey = endKey
+      }
+    }
+    
+    if (item.sortKey != sortKey) {
+      item.translateX = (sortKey % basedata.columns) * 100 + '%'
+      item.translateY = Math.floor(sortKey / basedata.columns) * 100 + '%'
+      item.sortKey = sortKey
+      item.realKey = realKey
+    }
+    return item
+  })
+}
+
+var triggerCustomEvent = function(list, type, instance) {
+  if (!instance) return
+  var _list = [],
+    listData = [];
+    
+  list.forEach(function(item) {
+    _list[item.sortKey] = item
+  })
+  _list.forEach(function(item) {
+    listData.push(item.data)
+  })
+  
+  // 编译到小程序 funcName作为参数传递导致事件不执行
+  switch(type) {
+    case 'change':
+      instance.callMethod('change', {data: listData})
+      break
+    case 'sortEnd':
+      instance.callMethod('sortEnd', {data: listData})
+      break
+  }
+}
+
+var listObserver = function(newVal, oldVal, ownerInstance, instance) {
+  var state = ownerInstance.getState()
+  state.itemsInstance = ownerInstance.selectAllComponents('.tn-drag__item')
+  
+  state.list = newVal || []
+  
+  state.list.forEach(function(item, index) {
+    var itemInstance = state.itemsInstance[index]
+    if (item && itemInstance) {
+      itemInstance.setStyle({
+        'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
+      })
+      if (item.fixed) itemInstance.addClass('tn-drag__fixed')
+    }
+  })
+}
+
+var baseDataObserver = function(newVal, oldVal, ownerInstance, instance) {
+  var state = ownerInstance.getState()
+  state.basedata = newVal
+}
+
+var longPress = function(event, ownerInstance) {
+  var instance = event.instance
+  var dataset = instance.getDataset()
+  var state = ownerInstance.getState()
+  
+  edit = bool(dataset.edit)
+  if (!edit) return
+  if (!state.basedata || state.basedata === 'undefined') {
+    state.basedata = JSON.parse(dataset.basedata)
+  }
+  var basedata = state.basedata
+  var touches = event.changedTouches[0]
+  if (!touches) return
+  
+  state.current = +dataset.index
+  
+  // 初始项是固定项则返回
+  var item = state.list[state.current]
+  if (item && item.fixed) return
+  
+  // 如果已经在 drag 中则返回, 防止多指触发 drag 动作, touchstart 事件中有效果
+  if (state.dragging) return
+  
+  ownerInstance.callMethod("drag", {
+    dragging: true
+  })
+  
+  // 计算X, Y轴初始位移,使item中心移动到点击处,单列的时候X轴初始不做位移
+  state.translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
+  state.translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
+  state.touchId = touches.identifier
+  
+  instance.setStyle({
+    'transform': 'translate3d(' + state.translateX + 'px,' + state.translateY +'px, 0)'
+  })
+  state.itemsInstance.forEach(function(item, index) {
+    item.removeClass("tn-drag__transition").removeClass("tn-drag__current")
+    item.addClass(index === state.current ? "tn-drag__current" : "tn-drag__transition")
+  })
+  
+  ownerInstance.callMethod("vibrate")
+  state.dragging = true
+}
+
+var touchStart = function(event, ownerInstance) {
+  var instance = event.instance
+  var dataset = instance.getDataset()
+  edit = bool(dataset.edit)
+}
+
+var touchMove = function(event, ownerInstance) {
+  var instance = event.instance
+  var dataset = instance.getDataset()
+  var state = ownerInstance.getState()
+  var basedata = state.basedata
+  
+  if (!state.dragging || !edit) return
+  var touches = event.changedTouches[0]
+  if (!touches) return
+  
+  // 如果不是同一个触发点则返回
+  if (state.touchId !== touches.identifier) return
+  
+  // 计算X,Y轴位移, 单列时候X轴初始不做位移
+  var translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
+  var translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
+  
+  // 到顶到低自动滑动
+  if (touches.clientY > basedata.windowHeight - basedata.itemHeight - basedata.realBottomSize) {
+    // 当前触摸点pageY + item高度 - (屏幕高度 - 底部固定区域高度)
+    ownerInstance.callMethod('pageScroll', {
+      scrollTop: touches.pageY + basedata.itemHeight - (basedata.windowHeight - basedata.realBottomSize)
+    })
+  } else if (touches.clientY < basedata.itemHeight + basedata.realTopSize) {
+    // 当前触摸点pageY - item高度 - 顶部固定区域高
+    ownerInstance.callMethod('pageScroll', {
+      scrollTop: touches.pageY - basedata.itemHeight - basedata.realTopSize
+    })
+  }
+  
+  // 设置当前激活元素的偏移量
+  instance.setStyle({
+    'transform': 'translate3d('+ translateX + 'px,' + translateY + 'px, 0)'
+  })
+  
+  var startKey = state.list[state.current].sortKey
+  var currentX = Math.round(translateX / basedata.itemWidth)
+  var currentY = Math.round(translateY / basedata.itemHeight)
+  var endKey = currentX + basedata.columns * currentY
+  
+  // 目标项时固定项则返回
+  var item = state.list[endKey]
+  if (item && item.fixed) return
+  
+  // X轴或者Y轴超出范围则返回
+  if (isOutRange(currentX, basedata.columns, currentY, basedata.rows, endKey, state.list.length)) return
+  
+  // 防止拖拽过程中发生乱序问题
+  if (startKey === endKey || startKey === state.preStartKey) return
+  state.preStartKey = startKey
+  
+  var list = sortCore(startKey, endKey, state)
+  state.itemsInstance.forEach(function(itemInstance, index) {
+    var item = list[index]
+    if (index !== state.current) {
+      itemInstance.setStyle({
+        'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
+      })
+    }
+  })
+  
+  // ownerInstance.callMethod('vibrate')
+  ownerInstance.callMethod('listDataChange', {
+    data: list
+  })
+  triggerCustomEvent(list, "change", ownerInstance)
+}
+
+var touchEnd = function(event, ownerInstance) {
+  var instance = event.instance
+  var dataset = instance.getDataset()
+  var state = ownerInstance.getState()
+  var basedata = state.basedata
+  
+  if (!state.dragging || !edit) return
+  triggerCustomEvent(state.list, "sortEnd", ownerInstance)
+  
+  instance.addClass('tn-drag__transition')
+  instance.setStyle({
+    'transform': 'translate3d('+ state.list[state.current].translateX + ',' + state.list[state.current].translateY + ', 0)'
+  })
+  state.itemsInstance.forEach(function(item, index) {
+    item.removeClass('tn-drag__transition')
+  })
+  
+  state.preStartKey = -1
+  state.dragging = false
+  ownerInstance.callMethod('drag', {
+    dragging: false
+  })
+  state.current = -1
+  state.translateX = 0
+  state.translateY = 0
+}
+
+module.exports = {
+	longPress: longPress,
+	touchStart: touchStart,
+	touchMove: touchMove,
+	touchEnd: touchEnd,
+	baseDataObserver: baseDataObserver,
+	listObserver: listObserver
+}

+ 278 - 0
tuniao-ui/components/tn-drag/tn-drag.vue

@@ -0,0 +1,278 @@
+<template>
+  <view
+    class="tn-drag-class tn-drag"
+    :style="{
+      height: wrapHeight + 'rpx'
+    }"
+    :list="listData"
+    :basedata="baseData"
+    :change:list="wxs.listObserver"
+    :change:basedata="wxs.baseDataObserver"
+  >
+    <!-- #ifdef MP-WEIXIN -->
+    <view
+      v-for="(item, index) in listData"
+      :key="item.id"
+      class="tn-drag__item"
+      :style="{
+        width: 100 / columns + '%',
+        height: itemHeight + 'rpx'
+      }"
+      :data-index="index"
+      :data-basedata="baseData"
+      :data-edit="edit"
+      @longpress="wxs.longPress"
+      @touchstart="wxs.touchStart"
+      :catch:touchmove="dragging?wxs.touchMove:''"
+      :catch:touchend="dragging?wxs.touchEnd:''"
+    >
+      <slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
+    </view>
+    <!-- #endif -->
+
+    <!-- #ifndef MP-WEIXIN -->
+    <view
+      v-for="(item, index) in listData"
+      :key="item.id"
+      class="tn-drag__item"
+      :style="{
+        width: 100 / columns + '%',
+        height: itemHeight + 'rpx' 
+      }"
+      @longpress="wxs.longPress"
+      :data-index="index"
+      :data-basedata="baseData"
+      :data-edit="edit"
+      @touchstart="wxs.touchStart"
+      @touchmove="wxs.touchMove"
+      @touchend="wxs.touchEnd"
+    >
+      <slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
+    </view>
+    <!-- #endif -->
+  </view>
+</template>
+<script src="./index.wxs" lang="wxs" module="wxs"></script>
+<script>
+  export default {
+    name: 'tn-drag',
+    props: {
+      // 数据源
+      // 如果属性中包含fixed,则标识当前数据不允许拖动
+      list: {
+        type: Array,
+        default () {
+          return []
+        }
+      },
+      // 是否允许拖动编辑
+      edit: {
+        type: Boolean,
+        default: true
+      },
+      // 列数
+      columns: {
+        type: Number,
+        default: 3
+      },
+      // item元素高度, 单位rpx
+      itemHeight: {
+        type: Number,
+        default: 0
+      },
+      // 当前父元素滚动的高度
+      scrollTop: {
+        type: Number,
+        default: 0
+      }
+    },
+    computed: {
+      wrapHeight() {
+        return this.rows * this.itemHeight
+      }
+    },
+    data() {
+      return {
+        // 未渲染前节点数据
+        baseData: {},
+        // 拖动后的数据
+        dragData: [],
+        // 行数
+        rows: 0,
+        // 渲染数据
+        listData: [],
+        // 标记是否正在拖动
+        dragging: false
+      }
+    },
+    watch: {
+      list(val) {
+        this.listData = []
+        this.$nextTick(() => {
+          this.init()
+        })
+      },
+      columns(val) {
+        this.listData = []
+        this.$nextTick(() => {
+          this.init()
+        })
+      }
+    },
+    mounted() {
+      this.$nextTick(() => {
+        this.init()
+      })
+    },
+    methods: {
+      // 初始化
+      init() {
+        this.dragging = true
+        const initDragItem = item => {
+          const obj = {
+            ...item
+          }
+          const fixed = obj?.fixed || false
+          delete obj["fixed"]
+          return {
+            id: this.unique(),
+            fixed,
+            data: {
+              ...obj
+            }
+          }
+        }
+        
+        let i = 0
+        const listData = (this.list || []).map((item, index) => {
+          let listItem = initDragItem(item)
+          // 真实排序
+          listItem.realKey = i++
+          // 整体排序
+          listItem.sortKey = index
+          listItem.translateX = `${(listItem.sortKey % this.columns) * 100}%`
+          listItem.translateY = `${Math.floor(listItem.sortKey / this.columns) * 100}%`
+          return listItem
+        })
+        this.rows = Math.ceil(listData.length / this.columns)
+        this.listData = listData
+        this.dragData = listData
+        
+        if (listData.length === 0) return
+        // console.log(listData);
+        
+        // 初始化dom元素
+        this.$nextTick(() => {
+          this.initRect()
+        })
+      },
+      // 初始化dom元素
+      initRect() {
+        const {
+          windowWidth,
+          windowHeight
+        } = uni.getSystemInfoSync()
+        
+        let baseData = {}
+        baseData.windowHeight = windowHeight
+        baseData.realTopSize = 0
+        baseData.realBottomSize = 0
+        baseData.columns = this.columns
+        baseData.rows = this.rows
+        
+        const query = uni.createSelectorQuery().in(this)
+        query.select('.tn-drag').boundingClientRect()
+        query.select('.tn-drag__item').boundingClientRect()
+        query.exec(res => {
+          if (!res) {
+            setTimeout(() => {
+              this.initRect()
+            }, 10)
+            return
+          }
+          
+          baseData.itemWidth = res[1].width
+          baseData.itemHeight = res[1].height
+          baseData.left = res[0].left
+          baseData.top = res[0].top + this.scrollTop
+          this.dragging = false
+          this.baseData = baseData
+        })
+        
+      },
+      
+      // 触发震动
+      vibrate() {
+        uni.vibrateShort()
+      },
+      // 滚动到指定的位置
+      pageScroll(e) {
+        uni.pageScrollTo({
+          scrollTop: e.scrollTop,
+          duration: 0
+        })
+      },
+      // 修改拖动状态
+      drag(e) {
+        this.dragging = e.dragging
+      },
+      // 拖拽数据发生改变
+      listDataChange(e) {
+        this.dragData = e.data
+      },
+      // item被点击
+      itemClick(index) {
+        const item = this.dragData[index]
+        this.$emit('click', {
+          key: item.realKey,
+          data: item.data
+        })
+      },
+      
+      // 拖拽结束事件
+      sortEnd(e) {
+        this.$emit('end', {
+          data: e.data
+        })
+      },
+      // 排序发生改变事件
+      change(e) {
+        this.$emit('change', {
+          data: e.data
+        })
+      },
+      
+      // 生成元素唯一id
+      unique(n = 6) {
+        let id = ''
+        for (let i = 0; i < n; i++) id += Math.floor(Math.random() * 10)
+        return 'tn_' + new Date().getTime() + id
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-drag {
+    position: relative;
+    
+    &__item {
+      position: absolute;
+      z-index: 2;
+      top: 0;
+      left: 0;
+    }
+    
+    &__transition {
+      transition: transform 0.25s !important;
+    }
+    
+    &__current {
+      z-index: 10 !important;
+    }
+    
+    &__fixed {
+      z-index: 1 !important;
+    }
+  }
+</style>

+ 190 - 0
tuniao-ui/components/tn-empty/tn-empty.vue

@@ -0,0 +1,190 @@
+<template>
+  <view v-if="show" class="tn-empty-class tn-empty" :style="[emptyStyle]">
+    <view
+      v-if="!isImage"
+      class="tn-empty__icon"
+      :class="[icon ? `tn-icon-${icon}` : `tn-icon-empty-${mode}`]"
+      :style="[iconStyle]"
+    ></view>
+    <image
+      v-else
+      class="tn-empty__image"
+      :style="[imageStyle]"
+      :src="icon"
+      mode="widthFix"
+    ></image>
+    
+    <view
+      class="tn-empty__text"
+      :style="[textStyle]"
+    >{{ text ? text : icons[mode]}}</view>
+    <view v-if="$slots.default || $slots.$default" class="tn-empty__slot">
+      <slot/>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-empty',
+    props: {
+      // 显示空白页
+      show: {
+        type: Boolean,
+        default: true
+      },
+      // 内置icon的名称
+      // 图片路径,建议使用绝对路径
+      icon: {
+        type: String,
+        default: ''
+      },
+      // 预置图标类型
+      mode: {
+        type: String,
+        default: 'data'
+      },
+      // 提示文字
+      text: {
+        type: String,
+        default: ''
+      },
+      // 文字颜色
+      textColor: {
+        type: String,
+        default: ''
+      },
+      // 文字大小,单位rpx
+      textSize: {
+        type: Number,
+        default: 0
+      },
+      // 图标颜色
+      iconColor: {
+        type: String,
+        default: ''
+      },
+      // 图标大小,单位rpx
+      iconSize: {
+        type: Number,
+        default: 0
+      },
+      // 图片宽度(当图标为图片时生效),单位rpx
+      imgWidth: {
+        type: Number,
+        default: 0
+      },
+      // 图片高度(当图标为图片时生效),单位rpx
+      imgHeight: {
+        type: Number,
+        default: 0
+      },
+      // 自定义组件样式
+      customStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      }
+    },
+    computed: {
+      emptyStyle() {
+        let style = {}
+        Object.assign(style, this.customStyle)
+        return style
+      },
+      iconStyle() {
+        let style = {}
+        if (this.iconSize) {
+          style.fontSize = this.iconSize + 'rpx'
+        }
+        if (this.iconColor) {
+          style.color = this.iconColor
+        }
+        return style
+      },
+      imageStyle() {
+        let style = {}
+        if (this.imgWidth) {
+          style.width = this.imgWidth + 'rpx'
+        }
+        if (this.imgHeight) {
+          style.height = this.imgHeight + 'rpx'
+        }
+        return style
+      },
+      textStyle() {
+        let style = {}
+        if (this.textColor) {
+          style.color = this.textColor
+        }
+        if (this.textSize) {
+          style.fontSize = this.textSize + 'rpx'
+        }
+        return style
+      },
+      // 判断传递的icon是否为图片
+      isImage() {
+        return this.icon.indexOf('/') >= 0
+      }
+    },
+    data() {
+      return {
+        icons: {
+          cart: '购物车为空',
+          page: '页面不存在',
+          search: '搜索结果为空',
+          address: '地址为空',
+          network: '网络不通',
+          order: '订单为空',
+          coupon: '优惠券为空',
+          favor: '暂无收藏',
+          permission: '无权限',
+          history: '历史记录为空',
+          message: '暂无消息',
+          list: '列表为空',
+          data: '暂无数据',
+          comment: '暂无评论'
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-empty {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    
+    &__icon {
+      margin-top: 14rpx;
+      color: #AAAAAA;
+      font-size: 90rpx;
+    }
+    
+    &__image {
+      width: 160rpx;
+      height: 160rpx;
+    }
+    
+    &__text {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      margin-top: 20rpx;
+      color: #AAAAAA;
+      font-size: 30rpx;
+    }
+    
+    &__slot {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      margin-top: 20rpx;
+    }
+  }
+</style>

+ 523 - 0
tuniao-ui/components/tn-fab/tn-fab.vue

@@ -0,0 +1,523 @@
+<template>
+  <view class="tn-fab-class tn-fab" @touchmove.stop.prevent>
+    <view
+      class="tn-fab__box"
+      :class="{'tn-fab--right': left === 'auto'}"
+      :style="{
+        left: $t.string.getLengthUnitValue(fabLeft || left),
+        right: $t.string.getLengthUnitValue(fabRight || right),
+        bottom: $t.string.getLengthUnitValue(fabBottom || bottom),
+        zIndex: elZIndex
+      }"
+    >
+      <view
+        v-if="visibleSync"
+        class="tn-fab__btns"
+        :class="[`tn-fab__btns__animation--${animationType}`, 
+          showFab ? `tn-fab__btns--visible--${animationType}` : ''
+        ]"
+      >
+        <view
+          v-for="(item, index) in btnList"
+          :key="index"
+          class="tn-fab__item"
+          :class="[
+            `tn-fab__item__animation--${animationType}`, 
+            {'tn-fab__item--left': right === 'auto'}
+          ]"
+          :style="[fabItemStyle(index)]"
+          @tap.stop="handleClick(index)"
+        >
+          <!-- 带图标或者图片时显示的文字信息 -->
+          <view
+            v-if="animationType !== 'around' && (item.imgUrl || item.icon)"
+            :class="[left === 'auto' ? 'tn-fab__item__text--right' : 'tn-fab__item__text--left']"
+            :style="{
+              color: item.textColor || '#FFF',
+              fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
+            }"
+          >{{ item.text || '' }}</view>
+          
+          <!-- 带图片或者图标时的图片、图标信息 -->
+          <view
+            class="tn-fab__item__btn"
+            :class="[!item.bgColor ? backgroundColorClass : '']"
+            :style="{
+              width: $t.string.getLengthUnitValue(width),
+              height: $t.string.getLengthUnitValue(height),
+              lineHeight: $t.string.getLengthUnitValue(height),
+              backgroundColor: item.bgColor || backgroundColorStyle || '#01BEFF',
+              borderRadius: $t.string.getLengthUnitValue(radius)
+            }"
+          >
+            <!-- 无图片和无图标时只显示文字 -->
+            <view
+              v-if="!item.imgUrl && !item.icon"
+              class="tn-fab__item__btn__title"
+              :style="{
+                color: item.textColor || '#fff',
+                fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
+              }"
+            >{{ item.text || '' }}</view>
+            <!-- 图标 -->
+            <view
+              v-if="item.icon"
+              class="tn-fab__item__btn__icon"
+              :class="[`tn-icon-${item.icon}`]"
+              :style="{
+                color: item.iconColor || '#fff',
+                fontSize: $t.string.getLengthUnitValue(item.iconSize || iconSize || 64)
+              }"
+            ></view>
+            <!-- 图片 -->
+            <image
+              v-else-if="!item.icon && item.imgUrl"
+              class="tn-fab__item__btn__image"
+              :style="{
+                width: $t.string.getLengthUnitValue(item.imgWidth || 64),
+                height: $t.string.getLengthUnitValue(item.imgHeight || 64),
+              }"
+              :src="item.imgUrl"
+            ></image>
+          </view>
+        </view>
+      </view>
+      
+      <view
+        class="tn-fab__item__btn tn-fab__item__btn--fab"
+        :class="[backgroundColorClass, fontColorClass, {'tn-fab__item__btn--active': showFab}]"
+        :style="{
+          width: $t.string.getLengthUnitValue(width),
+          height: $t.string.getLengthUnitValue(height),
+          backgroundColor: backgroundColorStyle || !backgroundColorClass ? '#01BEFF' : '',
+          color: fontColorStyle || '#fff',
+          borderRadius: $t.string.getLengthUnitValue(radius),
+          zIndex: elZIndex - 1
+        }"
+        @tap.stop="fabClick"
+      >
+        <slot>
+          <view class="tn-fab__item__btn__icon" :class="[`tn-icon-${icon}`]" :style="{fontSize: $t.string.getLengthUnitValue(iconSize || 64)}"></view>
+        </slot>
+      </view>
+    </view>
+    <view v-if="visibleSync && showMask" class="tn-fab__mask" :class="{'tn-fab__mask--visible': showFab}" :style="{zIndex: elZIndex - 3}" @tap="clickMask()"></view>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    name: 'tn-fab',
+    mixins: [componentsColorMixin],
+    props: {
+      // 按钮列表
+      // {
+      //   // 背景颜色
+      //   bgColor: '#fff',
+      //   // 图片地址
+      //   imgUrl: '',
+      //   // 图片宽度
+      //   imgWidth: 60,
+      //   // 图片高度
+      //   imgHeight: 60,
+      //   // 图标名称
+      //   icon: '',
+      //   // 图标尺寸
+      //   iconSize: 60,
+      //   // 图标颜色
+      //   iconColor: '#fff',
+      //   // 提示文字
+      //   text: '',
+      //   // 文字大小
+      //   textSize: 30,
+      //   // 字体颜色
+      //   textColor: '#fff'
+      // }
+      btnList: {
+        type: Array,
+        default() {
+          return []
+        }
+      },
+      // 自定义悬浮按钮内容
+      customBtn: {
+        type: Boolean,
+        default: false
+      },
+      // 悬浮按钮的宽度
+      width: {
+        type: [String, Number],
+        default: 88
+      },
+      // 悬浮按钮的高度
+      height: {
+        type: [String, Number],
+        default: 88
+      },
+      // 图标大小
+      iconSize: {
+        type: [String, Number],
+        default: 64
+      },
+      // 图标名称
+      icon: {
+        type: String,
+        default: 'open'
+      },
+      // 按钮圆角
+      radius: {
+        type: [String, Number],
+        default: '50%'
+      },
+      // 按钮距离左边的位置
+      left: {
+        type: [String, Number],
+        default: 'auto'
+      },
+      // 按钮距离右边的位置
+      right: {
+        type: [String, Number],
+        default: 'auto'
+      },
+      // 按钮距离底部的位置
+      bottom: {
+        type: [String, Number],
+        default: 100
+      },
+      // 展示动画类型 up 往上展示 around 环绕
+      animationType: {
+        type: String,
+        default: 'up'
+      },
+      // 当动画为圆环时,每个弹出按钮之间的距离, 单位px
+      aroundBtnDistance: {
+        type: Number,
+        default: 10
+      },
+      zIndex: {
+        type: Number,
+        default: 0
+      },
+      // 显示遮罩
+      showMask: {
+        type: Boolean,
+        default: true
+      },
+      // 点击遮罩是否可以关闭
+      maskCloseable: {
+        type: Boolean,
+        default: true
+      }
+    },
+    data() {
+      return {
+        showFab: false,
+        visibleSync: false,
+        timer: null,
+        fabLeft: 0,
+        fabRight: 0,
+        fabBottom: 0,
+        fabBtnInfo: {
+          width: 0,
+          height: 0,
+          left: 0,
+          right: 0,
+          bottom: 0
+        },
+        systemInfo: {
+          width: 0,
+          height: 0
+        },
+        updateProps: false
+      }
+    },
+    computed: {
+      elZIndex() {
+        return this.zIndex || this.$t.zIndex.fab
+      },
+      propsData() {
+        return [this.width, this.height, this.left, this.right, this.bottom]
+      },
+      fabItemStyle() {
+        return (index) => {
+          let style = {
+            zIndex: this.elZIndex - 2
+          }
+          if (this.animationType === 'up' || !this.showFab) {
+            return style
+          }
+          let base = 1 
+          if (this.left === 'auto') {
+            base = 1
+          } else if (this.right === 'auto') {
+            base = -1
+          }
+          style.transform = `rotate(${base * index * 60}deg) translateX(${(this.aroundBtnDistance + this.fabBtnInfo.width) * (-(base))}px)`
+          return style
+        }
+      }
+    },
+    watch: {
+      propsData() {
+        // 更新按钮信息
+        this.updateProps = true
+      }
+    },
+    mounted() {
+      this.$nextTick(() => {
+        this.getFabBtnRectInfo()
+      })
+    },
+    beforeDestroy() {
+      if (this.timer) {
+        clearTimeout(this.timer)
+      }
+    },
+    methods: {
+      // 按钮点击事件
+      handleClick(index) {
+        this.close()
+        this.$emit('click', {index: index})
+      },
+      // 点击悬浮按钮
+      fabClick() {
+        if (this.showFab) {
+          this.close()
+        } else {
+          // console.log(this.visibleSync);
+          if (this.visibleSync) {
+            this.visibleSync = false
+            return
+          }
+          this.open()
+        }
+      },
+      // 点击遮罩
+      clickMask() {
+        if (!this.showMask || !this.maskCloseable) return
+        this.close()
+      },
+      
+      open() {
+        this.change('visibleSync', 'showFab', true)
+        this.translateFabPosition()
+      },
+      close() {
+        this.change('showFab', 'visibleSync', false)
+        this.fabLeft = 0
+        this.fabRight = 0
+        this.fabBottom = 0
+      },
+      // 关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
+      // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
+      change(param1, param2, status) {
+        this[param1] = status
+        if (status) {
+          // #ifdef H5 || MP
+          this.timer = setTimeout(() => {
+            this[param2] = status
+            this.$emit(status ? 'open' : 'close')
+            clearTimeout(this.timer)
+          }, 10)
+          // #endif
+          // #ifndef H5 || MP
+          this.$nextTick(() => {
+            this[param2] = status
+            this.$emit(status ? 'open' : 'close')
+          })
+          // #endif
+        } else {
+          this.timer = setTimeout(() => {
+            this[param2] = status
+            this.$emit(status ? 'open' : 'close')
+            clearTimeout(this.timer)
+          }, 250)
+        }
+      },
+      
+      /******************** 旋转动画相关函数 ********************/
+      // 获取按钮的信息
+      async getFabBtnRectInfo() {
+        const systemInfo = uni.getSystemInfoSync()
+        const btnRectInfo = await this._tGetRect('.tn-fab__item__btn--fab')
+        if (!btnRectInfo) {
+          setTimeout(() => {
+            this.getFabBtnRectInfo()
+          }, 10)
+          return
+        }
+        console.log(btnRectInfo);
+        this.systemInfo = {
+          width: systemInfo.windowWidth,
+          height: systemInfo.windowHeight
+        }
+        this.fabBtnInfo.width = btnRectInfo.width
+        this.fabBtnInfo.height = btnRectInfo.height
+        this.fabBtnInfo.left = btnRectInfo.left
+        this.fabBtnInfo.right = btnRectInfo.right
+        this.fabBtnInfo.bottom = btnRectInfo.bottom
+      },
+      // 更新悬浮按钮的位置
+      translateFabPosition() {
+        if (this.updateProps) {
+          this.getFabBtnRectInfo()
+          this.updateProps = false
+        }
+        if (this.animationType === 'up') return 
+        // 按钮组的宽度
+        const btnGroupWidth = this.fabBtnInfo.width + this.aroundBtnDistance + 10
+        // 判断当前按钮是在左边还是右边
+        if (this.left === 'auto' && btnGroupWidth > this.systemInfo.width - this.fabBtnInfo.right) {
+          // 距离不够需要移动
+          this.fabRight = btnGroupWidth + 'px'
+        } else if (this.right === 'auto' && btnGroupWidth > this.fabBtnInfo.left) {
+          this.fabLeft = btnGroupWidth + 'px'
+        }
+        
+        if (btnGroupWidth > this.systemInfo.height - this.fabBtnInfo.bottom) {
+          this.fabBottom = btnGroupWidth + 'px'
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-fab {
+    &__box {
+      display: flex;
+      justify-content: center;
+      align-items: flex-start;
+      flex-direction: column;
+      position: fixed;
+      transition: all 0.25s ease-in-out;
+    }
+    
+    &--right {
+      align-items: flex-end;
+    }
+    
+    &__btns {
+      transition: all 0.25s cubic-bezier(0,.13,0,1.43);
+      transform-origin: 80% bottom;
+      
+      &__animation--up {
+        opacity: 0;
+        transform: translateY(100%);
+      }
+      &__animation--around {
+        position: absolute;
+        top: 0;
+        left: 0;
+      }
+      
+      &--visible--up {
+        // visibility: visible;
+        opacity: 1;
+        transform: translateY(0);
+      }
+      &--visible--around {
+        // visibility: visible;
+        // opacity: 1;
+      }
+    }
+    
+    &__item {
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+      padding-bottom: 20rpx;
+      
+      &__animation--around {
+        position: absolute;
+        top: 0;
+        left: 0;
+        transition: transform 0.25s ease-in-out;
+        transform-origin: 50% 50%;
+        padding-bottom: 0 !important;
+      }
+      
+      &--left {
+        flex-flow: row-reverse;
+      }
+      
+      &__text {
+        &--left {
+          padding-left: 14rpx;
+        }
+        &--right {
+          padding-right: 14rpx;
+        }
+      }
+      
+      &__btn {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        box-shadow: 0 0 5rpx 2rpx rgba(0, 0, 0, 0.07);
+        transition: all 0.2s linear;
+        
+        &--active {
+          animation-name: fab-button-animation;
+          animation-duration: 0.2s;
+          animation-timing-function: cubic-bezier(0,.13,0,1.43);
+        }
+        
+        &__title {
+          width: 90%;
+          text-align: center;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+        
+        &__icon {
+          text-align: center;
+          font-size: 64rpx;
+        }
+        
+        &__image {
+          display: block;
+        }
+      }
+    }
+    
+    &__mask {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background-color: $tn-mask-bg-color;
+      transition: all 0.2s ease-in-out;
+      opacity: 0;
+      
+      &--visible {
+        opacity: 1;
+      }
+    }
+  }
+  
+  @keyframes fab-button-animation {
+    0% {
+      transform: scale(0.6);
+    }
+    // 20% {
+    //   transform: scale(1.8);
+    // }
+    // 40% {
+    //   transform: scale(0.4);
+    // }
+    // 50% {
+    //   transform: scale(1.4);
+    // }
+    // 80% {
+    //   transform: scale(0.8);
+    // }
+    100% {
+      transform: scale(1);
+    }
+  }
+</style>

+ 457 - 0
tuniao-ui/components/tn-form-item/tn-form-item.vue

@@ -0,0 +1,457 @@
+<template>
+  <view
+    class="tn-form-item-class tn-form-item"
+    :class="{
+      'tn-border-solid-bottom': elBorderBottom,
+      'tn-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')
+    }"
+  >
+    <view
+      class="tn-form-item__body"
+      :style="{
+        flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
+      }"
+    >
+      <!-- 处理微信小程序中设置属性的问题,不设置值的时候会变成true -->
+      <view
+        class="tn-form-item--left"
+        :style="{
+          width: wLabelWidth,
+          flex: `0 0 ${wLabelWidth}`,
+          marginBottom: elLabelPosition == 'left' ? 0 : '10rpx'
+        }"
+      >
+        <!-- 块对齐 -->
+        <view v-if="required || leftIcon || label" class="tn-form-item--left__content"
+          :style="[leftContentStyle]"
+        >
+          <!-- nvue不支持伪元素before -->
+          <view v-if="leftIcon" class="tn-form-item--left__content__icon">
+            <view :class="[`tn-icon-${leftIcon}`]" :style="leftIconStyle"></view>
+          </view>
+          <!-- <view
+            class="tn-form-item--left__content__label"
+            :style="[elLabelStyle, {
+              'justify-content': elLabelAlign === 'left' ? 'flex-satrt' : elLabelAlign === 'center' ? 'center' : 'flex-end'
+            }]"
+          >
+            {{label}}
+          </view> -->
+          <view
+            class="tn-form-item--left__content__label"
+            :style="[elLabelStyle]"
+          >
+            {{label}}
+          </view>
+          <text v-if="required" class="tn-form-item--left__content--required">*</text>
+        </view>
+      </view>
+      
+      <view class="tn-form-item--right tn-flex">
+        <view class="tn-form-item--right__content">
+          <view class="tn-form-item--right__content__slot">
+            <slot></slot>
+          </view>
+          <view v-if="$slots.right || rightIcon" class="tn-form-item--right__content__icon tn-flex">
+            <view v-if="rightIcon" :class="[`tn-icon-${rightIcon}`]" :style="rightIconStyle"></view>
+            <slot name="right"></slot>
+          </view>
+        </view>
+      </view>
+    </view>
+    
+    <view
+      v-if="validateState === 'error' && showError('message')"
+      class="tn-form-item__message"
+      :style="{
+        paddingLeft: elLabelPosition === 'left' ? elLabelWidth + 'rpx' : '0'
+      }"
+    >
+      {{validateMessage}}
+    </view>
+  </view>
+</template>
+
+<script>
+  import Emitter from '../../libs/utils/emitter.js'
+  import schema from '../../libs/utils/async-validator.js'
+  // 去除警告信息
+  schema.warning = function() {}
+  
+  export default {
+    mixins: [Emitter],
+    name: 'tn-form-item',
+    inject: {
+      tnForm: {
+        default() {
+          return null
+        }
+      }
+    },
+    props: {
+      // label提示语
+      label: {
+        type: String,
+        default: ''
+      },
+      // 绑定的值
+      prop: {
+        type: String,
+        default: ''
+      },
+      // 是否显示表单域的下划线边框
+      borderBottom: {
+        type:Boolean,
+        default: true
+      },
+      // label(标签名称)的位置
+      // left - 左边
+      // top - 上边
+      labelPosition: {
+        type: String,
+        default: ''
+      },
+      // label的宽度
+      labelWidth: {
+        type: Number,
+        default: 0
+      },
+      // label的对齐方式
+      // left - 左对齐
+      // top - 上对齐
+      // right - 右对齐
+      // bottom - 下对齐
+      labelAlign: {
+        type: String,
+        default: ''
+      },
+      // label 的样式
+      labelStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 左侧图标
+      leftIcon: {
+        type: String,
+        default: ''
+      },
+      // 右侧图标
+      rightIcon: {
+        type: String,
+        default: ''
+      },
+      // 左侧图标样式
+      leftIconStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 右侧图标样式
+      rightIconStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 是否显示必填项的*,不做校验用途
+      required: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      // 处理微信小程序label的宽度
+      wLabelWidth() {
+        // 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
+        return this.elLabelPosition === 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.elLabelWidth + 'rpx') : '100%'
+      },
+      // 是否显示错误提示
+      showError() {
+        return type => {
+          if (this.errorType.indexOf('none') >= 0) return false
+          else if (this.errorType.indexOf(type) >= 0) return true
+          else return false
+        }
+      },
+      // label的宽度(默认值为90)
+      elLabelWidth() {
+        return this.labelWidth != 0 ? this.labelWidth : (this.parentData.labelWidth != 0 ? this.parentData.labelWidth : 90)
+      },
+      // label的样式
+      elLabelStyle() {
+        return Object.keys(this.labelStyle).length ? this.labelStyle : (Object.keys(this.parentData.labelStyle).length ? this.parentData.labelStyle : {})
+      },
+      // label显示位置
+      elLabelPosition() {
+        return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition : 'left')
+      },
+      // label对齐方式
+      elLabelAlign() {
+        return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left')
+      },
+      // label下划线
+      elBorderBottom() {
+        return this.borderBottom !== '' ? this.borderBottom : (this.parentData.borderBottom !== '' ? this.parentData.borderBottom : true)
+      },
+      leftContentStyle() {
+        let style = {}
+        if (this.elLabelPosition === 'left') {
+          switch(this.elLabelAlign) {
+            case 'left':
+              style.justifyContent = 'flex-start'
+              break
+            case 'center':
+              style.justifyContent = 'center'
+              break
+            default:
+              style.justifyContent = 'flex-end'
+              break
+          }
+        }
+        
+        return style
+      }
+    },
+    data() {
+      return {
+        // 默认值
+        initialValue: '',
+        // 是否校验成功
+        validateState: '',
+        // 校验失败提示信息
+        validateMessage: '',
+        // 错误的提示方式(参考form组件)
+        errorType: ['message'],
+        // 当前子组件输入的值
+        fieldValue: '',
+        // 父组件的参数
+        // 由于再computed中无法得知this.parent的变化,所以放在data中
+        parentData: {
+          borderBottom: true,
+          labelWidth: 90,
+          labelPosition: 'left',
+          labelAlign: 'left',
+          labelStyle: {},
+        }
+      }
+    },
+    watch: {
+      validateState(val) {
+        this.broadcastInputError()
+      },
+      "tnForm.errorType"(val) {
+        this.errorType = val
+        this.broadcastInputError()
+      }
+    },
+    mounted() {
+      // 组件创建完成后,保存当前实例到form组件中
+      // 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用\
+      this.parent = this.$t.$parent.call(this, 'tn-form')
+      if (this.parent) {
+        // 遍历parentData属性,将parent中同名的属性赋值给parentData
+        Object.keys(this.parentData).map(key => {
+          this.parentData[key] = this.parent[key]
+        })
+        // 如果没有传入prop或者tnForm为空(单独使用form-item组件的时候),就不进行校验
+        if (this.prop) {
+          // 将本实例添加到父组件中
+          this.parent.fields.push(this)
+          this.errorType = this.parent.errorType
+          // 设置初始值
+          this.initialValue = this.fieldValue
+          // 添加表单校验,这里必须要写在$nextTick中,因为tn-form的rules是通过ref手动传入的
+          // 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给tn-form,导致规则为空
+          this.$nextTick(() => {
+            this.setRules()
+          })
+        }
+      }
+    },
+    beforeDestroy() {
+      // 组件销毁前,将实例从tn-form的缓存中移除
+      // 如果当前没有prop的话表示当前不进行删除
+      if (this.parent && this.prop) {
+        this.parent.fields.map((item, index) => {
+          if (item === this) this.parent.fields.splice(index, 1)
+        })
+      }
+    },
+    methods: {
+      // 向input组件发出错误事件
+      broadcastInputError() {
+        this.broadcast('tn-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'))
+      },
+      // 设置校验规则
+      setRules() {
+        let that = this
+        // 从父组件tn-form拿到当前tn-form-item需要验证 的规则
+        // let rules = this.getRules()
+        // if (rules.length) {
+        // 	this.isRequired = rules.some(rule => {
+        // 		// 如果有必填项,就返回,没有的话,就是undefined
+        // 		return rule.required
+        // 	})
+        // }
+        
+        // blur事件
+        this.$on('on-form-blur', that.onFieldBlur)
+        // change事件
+        this.$on('on-form-change', that.onFieldChange)
+      },
+      // 从form的rules属性中取出当前form-item的校验规则
+      getRules() {
+        let rules = this.parent.rules
+        rules = rules ? rules[this.prop] : []
+        
+        // 返回数值形式的值
+        return [].concat(rules || [])
+      },
+      // blur事件时进行表单认证
+      onFieldBlur() {
+        this.validation('blur')
+      },
+      // change事件时进行表单认证
+      onFieldChange() {
+        this.validation('change')
+      },
+      // 过滤出符合要求的rule规则
+      getFilterRule(triggerType = '') {
+        let rules = this.getRules()
+        // 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
+        if (!triggerType) return rules
+        // 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
+        // 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
+        // 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
+        return rules.filter(rule => rule.trigger && rule.trigger.indexOf(triggerType) !== -1)
+      },
+      // 校验数据
+      validation(trigger, callback = ()=>{}) {
+        // 校验之前先获取需要校验的值
+        this.fieldValue = this.parent.model[this.prop]
+        // blur和change是否有当前方式的校验规则
+        let rules = this.getFilterRule(trigger)
+        // 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件tn-form会因为
+        // 对count变量的统计错误而无法进入上一层的回调 
+        if (!rules || rules.length === 0) {
+          return callback('')
+        }
+        // 设置当前为校验中
+        this.validateState = 'validating'
+        // 调用async-validator的方法
+        let validator = new schema({
+          [this.prop]: rules
+        })
+        validator.validate({
+          [this.prop]: this.fieldValue
+        }, {
+          firstFields: true
+        }, (errors, fields) => {
+          // 记录状态和报错信息
+          this.validateState = !errors ? 'success' : 'error'
+          this.validateMessage = errors ? errors[0].message : ''
+          
+          callback(this.validateMessage)
+        })
+      },
+      
+      // 清空当前item信息
+      resetField() {
+        this.parent.model[this.prop] = this.initialValue
+        // 清空错误标记
+        this.validateState = 'success'
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-form-item {
+    display: flex;
+    flex-direction: column;
+    padding: 20rpx 0;
+    font-size: 28rpx;
+    color: $tn-font-color;
+    box-sizing: border-box;
+    line-height: $tn-form-item-height;
+    
+    &__border-bottom--error:after {
+      border-color: $tn-color-red;
+    }
+    
+    &__body {
+      display: flex;
+      flex-direction: row;
+    }
+    
+    &--left {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      
+      &__content {
+        display: flex;
+        flex-direction: row;
+        position: relative;
+        align-items: center;
+        padding-right: 18rpx;
+        flex: 1;
+        
+        &--required {
+          position: relative;
+          right: 0;
+          vertical-align: middle;
+          color: $tn-color-red;
+        }
+        
+        &__icon {
+          color: $tn-font-sub-color;
+          margin-right: 8rpx;
+        }
+        
+        &__label {
+          // display: flex;
+          // flex-direction: row;
+          // align-items: center;
+          // flex: 1;
+        }
+      }
+    }
+    
+    &--right {
+      flex: 1;
+      
+      &__content {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        flex: 1;
+        
+        &__slot {
+          flex: 1;
+          /* #ifndef MP */
+          display: flex;
+          flex-direction: row;
+          align-items: center;
+          /* #endif */
+        }
+        
+        &__icon {
+          margin-left: 10rpx;
+          color: $tn-font-sub-color;
+          font-size: 30rpx;
+        }
+      }
+    }
+    
+    &__message {
+      font-size: 24rpx;
+      line-height: 24rpx;
+      color: $tn-color-red;
+      margin-top: 12rpx;
+    }
+  }
+</style>

+ 139 - 0
tuniao-ui/components/tn-form/tn-form.vue

@@ -0,0 +1,139 @@
+<template>
+  <view class="tn-form-class tn-form">
+    <slot></slot>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-form',
+    props: {
+      // 表单数据对象(需要验证的表单数据)
+      model: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 发生错误时的提示方式
+      // toast - 弹出toast框
+      // message - 提示信息
+      // border - 如果设置了边框,边框会变成红色
+      // border-bottom - 下边框会呈现红色
+      // none - 无提示
+      errorType: {
+        type: Array,
+        default() {
+          return ['message', 'toast']
+        }
+      },
+      // 是否显示表单域的下划线边框
+      borderBottom: {
+        type:Boolean,
+        default: true
+      },
+      // label(标签名称)的位置
+      // left - 左边
+      // top - 上边
+      labelPosition: {
+        type: String,
+        default: 'left'
+      },
+      // label的宽度
+      labelWidth: {
+        type: Number,
+        default: 90
+      },
+      // label的对齐方式
+      // left - 左对齐
+      // center - 居中对齐
+      // right - 右对齐
+      labelAlign: {
+        type: String,
+        default: 'left'
+      },
+      // label 的样式
+      labelStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      }
+    },
+    // 向子孙传递数据
+    provide() {
+      return {
+        tnForm: this
+      }
+    },
+    data() {
+      return {
+        rules: {}
+      }
+    },
+    created() {
+      // 存储当前form下的所有form-item的实例
+      // 不能定义再data中,否则小程序会循环引用而报错
+      this.fields = []
+    },
+    methods: {
+      /**
+       * 设置规则
+       * 
+       * @param {Object} rules
+       */
+      setRules(rules) {
+        this.rules = rules
+      },
+      /**
+       * 清空form-item组件
+       */
+      resetFields() {
+        this.fields.map(field => {
+          field.resetField()
+        })
+      },
+      /**
+       * 校验数据
+       * @param {Object} callback 校验回调方法
+       */
+      validate(callback) {
+        return new Promise(resolve => {
+          // 标记校验是否通过
+          let valid = true
+          // 标记是否检查完毕
+          let count = 0
+          // 存放错误信息
+          let errors = []
+          
+          // 对所有form-item进行校验
+          this.fields.map(field => {
+            // 调用对应form-item实例的validation校验方法
+            field.validation('', error => {
+              // 如果有一个form-item校验不通过,则整个表单校验不通过
+              if (error) {
+                valid = false
+                errors.push(error)
+              }
+              // 当遍历完所有的form-item的校验规则,返回信息
+              if (++count === this.fields.length) {
+                resolve(valid)
+                // 判断是否设置了toast的提示方式,只提示表单域中最前面的一条错误信息
+                if (this.errorType.indexOf('none') === -1 && 
+                    this.errorType.indexOf('toast') >= 0 &&
+                    errors.length > 0) {
+                  this.$t.message.toast(errors[0])
+                }
+                // 调用回调方法
+                if (typeof callback == 'function') callback(valid)
+              }
+            })
+          })
+        })
+      }
+    }
+  }
+</script>
+
+<style>
+</style>

+ 382 - 0
tuniao-ui/components/tn-goods-nav/tn-goods-nav.vue

@@ -0,0 +1,382 @@
+<template>
+  <view
+    class="tn-goods-nav-class tn-goods-nav"
+    :class="[
+      backgroundColorClass,
+      {
+        'tn-goods-nav--fixed': fixed,
+        'tn-safe-area-inset-bottom': safeAreaInsetBottom,
+        'tn-goods-nav--shadow': shadow
+      }
+    ]"
+    :style="[backgroundColorStyle, navStyle]"
+  >
+    <view class="options">
+      <view
+        v-for="(item, index) in optionsData"
+        :key="index"
+        class="options__item"
+        :class="[{'options__item--avatar': item.showAvatar}]"
+        @tap="handleOptionClick(index)"
+      >
+        <block v-if="item.showAvatar">
+          <tn-avatar
+            :src="item.avatar"
+            size="sm"
+            :badge="item.showBadge"
+            :badgeText="item.count"
+            :badgeBgColor="item.countBackgroundColor"
+            :badgeColor="item.countFontColor"
+            :badgeSize="26"
+          ></tn-avatar>
+        </block>
+        <block v-else>
+          <view class="options__item__icon" :class="[`tn-icon-${item.icon}`]" :style="[optionStyle(index, 'icon')]">
+            <tn-badge v-if="item.showBadge" :absolute="true" :backgroundColor="item.countBackgroundColor" :fontColor="item.countFontColor" :fontSize="16" padding="2rpx 5rpx">{{ item.count }}</tn-badge>
+          </view>
+          <view class="options__item__text" :style="[optionStyle(index, 'text')]">{{ item.text }}</view>
+        </block>
+      </view>
+    </view>
+    <view class="buttons">
+      <view
+        v-for="(item, index) in buttonGroupsData"
+        :key="index"
+        class="buttons__item"
+        :class="[buttonClass(index)]"
+        :style="[buttonStyle(index)]"
+        @tap="handleButtonClick(index)"
+      >
+        <view class="buttons__item__text">{{ item.text }}</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-goods-nav',
+    props: {
+      // 选项信息
+      // 建议不超过3个
+      // {
+      //   icon: '', // 图标名称
+      //   text: '', // 显示的文本
+      //   count: '', // 角标的值
+      //   countBackgroundColor: '', // 角标背景颜色
+      //   countFontColor: '', // 角标字体颜色
+      //   iconColor: '', // 图标颜色
+      //   textColor: '', // 文本颜色
+      //   avatar: '', // 显示头像(此时将不显示图标和文本)
+      // }
+      options: {
+        type: Array,
+        default() {
+          return [{
+            icon: 'shop',
+            text: '店铺'
+          },{
+            icon: 'service',
+            text: '客服'
+          },{
+            icon: 'star',
+            text: '收藏'
+          }]
+        }
+      },
+      // 按钮组
+      // 建议不超过2个
+      // {
+      //   text: '', // 显示的文本
+      //   backgroundColor: '', // 按钮背景颜色
+      //   color: '' // 文本颜色
+      // }
+      buttonGroups: {
+        type: Array,
+        default() {
+          return [{
+            text: '加入购物车',
+            backgroundColor: '#FFA726',
+            color: '#FFFFFF'
+          },{
+            text: '结算',
+            backgroundColor: '#FF7043',
+            color: '#FFFFFF'
+          }]
+        }
+      },
+      // 背景颜色
+      backgroundColor: {
+        type: String,
+        default: ''
+      },
+      // 导航的高度,单位rpx
+      height: {
+        type: Number,
+        default: 0
+      },
+      // 显示阴影
+      shadow: {
+        type: Boolean,
+        default: false
+      },
+      // 导航的层级
+      zIndex: {
+        type: Number,
+        default: 0
+      },
+      // 按钮类型
+      // rect -> 方形 paddingRect -> 上下带边距方形 round -> 圆角
+      buttonType: {
+        type: String,
+        default: 'rect'
+      },
+      // 是否固定在底部
+      fixed: {
+        type: Boolean,
+        default: false
+      },
+      // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+      safeAreaInsetBottom: {
+      	type: Boolean,
+      	default: false
+      }
+    },
+    computed: {
+      backgroundColorStyle() {
+        return this.$t.color.getBackgroundColorStyle(this.backgroundColor)
+      },
+      backgroundColorClass() {
+        return this.$t.color.getBackgroundColorInternalClass(this.backgroundColor)
+      },
+      navStyle() {
+        let style = {}
+        if (this.height) {
+          style.height = this.height + 'rpx'
+        }
+        style.zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.goodsNav
+        return style
+      },
+      // 选项style
+      optionStyle() {
+        return (index, type) => {
+          let style = {}
+          const item = this.optionsData[index]
+          if (type === 'icon' && item.iconColor) {
+            style.color = item.iconColor
+          } else if (type === 'text' && item.fontColor) {
+            style.color = item.fontColor
+          }
+          return style
+        }
+      },
+      // 按钮class
+      buttonClass() {
+        return (index) => {
+          let clazz = ''
+          const item = this.buttonGroupsData[index]
+          if (item.backgroundColorClass) {
+            clazz += ` ${item.backgroundColorClass}`
+          }
+          if (item.colorClass) {
+            clazz += ` ${item.colorClass}`
+          }
+          
+          clazz += ` buttons__item--${this.$t.string.humpConvertChar(this.buttonType, '-')}`
+          
+          return clazz
+        }
+      },
+      // 按钮style
+      buttonStyle() {
+        return (index) => {
+          let style = {}
+          const item = this.buttonGroupsData[index]
+          if (item.backgroundColorStyle) {
+            style.backgroundColor = item.backgroundColorStyle
+          }
+          if (item.colorStyle) {
+            style.color = item.colorStyle
+          }
+          return style
+        }
+      }
+    },
+    watch: {
+      options(val) {
+        this.initData()
+      },
+      buttonGroups(val) {
+        this.initData()
+      }
+    },
+    data() {
+      return {
+        // 保存选项数据
+        optionsData: [],
+        // 保存按钮组数据
+        buttonGroupsData: []
+      }
+    },
+    created() {
+      this.initData()
+    },
+    methods: {
+      // 初始化选项和按钮数据
+      initData() {
+        this.handleOptionsData()
+        this.handleButtonGroupsData()
+      },
+      // 选项点击事件
+      handleOptionClick(index) {
+        this.$emit('optionClick', {
+          index: index
+        })
+      },
+      // 按钮点击事件
+      handleButtonClick(index) {
+        this.$emit('buttonClick', {
+          index: index
+        })
+      },
+      // 处理选项组数据
+      handleOptionsData() {
+        this.optionsData = this.options.map((item) => {
+          let option = {...item}
+          option.showAvatar = item.hasOwnProperty('avatar')
+          if (item.hasOwnProperty('count')) {
+            const count = this.$t.number.formatNumberString(item.count, 2)
+            option.showBadge = true
+            option.count = typeof count === 'number' ? String(count) : count
+            option.countBackgroundColor = item.countBackgroundColor ? item.countBackgroundColor : '#01BEFF'
+            option.countFontColor = item.countFontColor ? item.countFontColor : '#FFFFFF'
+          }
+          
+          return option
+        })
+      },
+      // 处理按钮组数据
+      handleButtonGroupsData() {
+        this.buttonGroupsData = this.buttonGroups.map((item) => {
+          let button = {...item}
+          if (item.hasOwnProperty('backgroundColor')) {
+            button.backgroundColorClass = this.$t.color.getBackgroundColorInternalClass(item.backgroundColor)
+            button.backgroundColorStyle = this.$t.color.getBackgroundColorStyle(item.backgroundColor)
+          }
+          if (item.hasOwnProperty('color')) {
+            button.colorClass = this.$t.color.getBackgroundColorInternalClass(item.color)
+            button.colorStyle = this.$t.color.getBackgroundColorStyle(item.color)
+          }
+          return button
+        })
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-goods-nav {
+    background-color: #FFFFFF;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    height: 88rpx;
+    width: 100%;
+    box-sizing: content-box;
+    
+    &--shadow {
+      box-shadow: 0rpx -10rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
+      
+      &::before {
+        content: " ";
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        margin: auto;
+        background-color: transparent;
+        z-index: -1;
+      }
+    }
+    
+    &--fixed {
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      right: 0;
+    }
+    
+    .options {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      height: 100%;
+      color: #AAAAAA;
+      
+      &__item {
+        padding: 0 26rpx;
+        
+        &--avatar {
+          padding: 0rpx 0rpx 0rpx 26rpx !important;
+        }
+        
+        &__icon {
+          position: relative;
+          font-size: 36rpx;
+          margin-bottom: 6rpx;
+        }
+        
+        &__text {
+          font-size: 22rpx;
+        }
+      }
+    }
+    
+    .buttons {
+      flex: 1;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      height: 100%;
+      
+      &__item {
+        flex: 1;
+        padding: 0 10rpx;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        
+        &--rect {
+          height: 100%;
+        }
+        
+        &--padding-rect {
+          height: 80%;
+        }
+        
+        &--round {
+          height: 75%;
+          &:first-child {
+            border-top-left-radius: 100rpx;
+            border-bottom-left-radius: 100rpx;
+          }
+          &:last-child {
+            border-top-right-radius: 100rpx;
+            border-bottom-right-radius: 100rpx;
+          }
+        }
+        
+        &__text {
+          display: inline-block;
+          font-weight: bold;
+          font-size: 30rpx;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          overflow: hidden;
+        }
+      }
+    }
+  }
+</style>

+ 114 - 0
tuniao-ui/components/tn-grid-item/tn-grid-item.vue

@@ -0,0 +1,114 @@
+<template>
+  <view
+    class="tn-grid-item-class tn-grid-item"
+    :class="[
+      backgroundColorClass
+    ]"
+    :hover-class="hoverClass"
+    :hover-stay-time="150"
+    :style="{
+      backgroundColor: backgroundColorStyle,
+      width: gridWidth
+    }"
+    @tap="click"
+  >
+    <view
+      class="tn-grid-item__box"
+    >
+      <slot></slot>
+    </view>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    mixins: [ componentsColorMixin ],
+    name: 'tn-grid-item',
+    props: {
+      // 序号
+      index: {
+        type: [Number, String],
+        default: ''
+      }
+    },
+    data() {
+      return {
+        // 父组件数据
+        parentData: {
+          // 按下去的样式
+          hoverClass: '',
+          col: 3
+        }
+      }
+    },
+    created() {
+      // 父组件实例
+      this.updateParentData()
+      this.parent.children.push(this)
+    },
+    computed: {
+      // 计算每个宫格的宽度
+      gridWidth() {
+        // #ifdef MP-WEIXIN
+        return '100%'
+        // #endif
+        // #ifndef MP-WEIXIN
+        return 100 / Number(this.parentData.col) + '%'
+        // #endif
+      },
+      // 点击效果
+      hoverClass() {
+        return this.parentData.hoverClass !== 'none' 
+                 ? this.parentData.hoverClass + ' tn-grid-item--hover' 
+                 : this.parentData.hoverClass
+      }
+    },
+    methods: {
+      // 获取父组件参数
+      updateParentData() {
+        this.getParentData('tn-grid')
+      },
+      click() {
+        this.$emit('click', this.index)
+        this.parent && this.parent.click(this.index)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-grid-item {
+    box-sizing: border-box;
+    background-color: #FFFFFF;
+    /* #ifndef APP-NVUE */
+    display: flex;
+    flex-direction: row;
+    /* #endif */
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    flex-direction: column;
+    
+    /* #ifdef MP */
+    // float: left;
+    /* #endif */
+    
+    &__box {
+      /* #ifndef APP-NVUE */
+      display: flex;
+      flex-direction: row;
+      /* #endif */
+      align-items: center;
+      justify-content: center;
+      flex-direction: column;
+      flex: 1;
+      width: 100%;
+      height: 100%;
+    }
+    
+    &--hover {
+      background: $tn-space-color !important;
+    }
+  }
+</style>

+ 111 - 0
tuniao-ui/components/tn-grid/tn-grid.vue

@@ -0,0 +1,111 @@
+<template>
+  <view
+    class="tn-grid-class tn-grid"
+    :style="{
+      justifyContent: gridAlignStyle
+    }"
+  >
+    <slot></slot>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-grid',
+    props: {
+      // 分成几列
+      col: {
+        type: [Number, String],
+        default: 3
+      },
+      // 宫格对齐方式 
+      // left 左对齐 center 居中对齐 right 右对齐
+      align: {
+        type: String,
+        default: 'left'
+      },
+      // 点击时的效果,none没有效果
+      hoverClass: {
+        type: String,
+        default: 'tn-hover'
+      }
+    },
+    data() {
+      return {
+        
+      }
+    },
+    watch: {
+      // 当父组件和子组件需要共享参数变化,通知子组件
+      parentData() {
+        if (this.children.length) {
+          this.children.map(child => {
+            // 判断子组件是否有updateParentData方式,有才执行
+            typeof(child.updateParentData) === 'function' && child.updateParentData()
+          })
+        }
+      }
+    },
+    created() {
+      // 如果将children定义在data中,在微信小程序会造成循环引用而报错
+      this.children = []
+    },
+    computed: {
+      // 计算父组件的值是否发生变化
+      parentData() {
+        return [this.hoverClass, this.col, this.border]
+      },
+      // 宫格对齐方式
+      gridAlignStyle() {
+        switch(this.align) {
+          case 'left':
+            return 'flex-start'
+          case 'center':
+            return 'center'
+          case 'right':
+            return 'flex-end'
+          default:
+            return 'flex-start'
+        }
+      }
+    },
+    methods: {
+      click(index) {
+        this.$emit('click', index)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  // 组件中兼容小程序的方式,不过不能使用对齐方式
+  // .tn-grid {
+  //   width: 100%;
+  //   /* #ifdef MP */
+  //   position: relative;
+  //   box-sizing: border-box;
+  //   overflow: hidden;
+  //   /* #endif */
+    
+  //   /* #ifndef MP */
+  //   /* #ifndef APP-NVUE */
+  //   display: flex;
+  //   flex-direction: row;
+  //   /* #endif */
+  //   flex-wrap: wrap;
+  //   align-items: center;
+  //   /* #endif */
+  // }
+  
+  // 在使用组件时兼容小程序
+  .tn-grid {
+    width: 100%;
+    
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    align-items: center;
+  }
+  
+</style>

+ 90 - 0
tuniao-ui/components/tn-index-anchor/tn-index-anchor.vue

@@ -0,0 +1,90 @@
+<template>
+  <!-- 支付宝小程序使用_tGetRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+  <view>
+    <view :id="elId" class="tn-index-anchor__wrap" :style="[wrapperStyle]">
+      <view class="tn-index-anchor" :class="[active ? 'tn-index-anchor--active' : '']" :style="[customAnchorStyle]">
+        <view v-if="useSlot">
+          <slot></slot>
+        </view>
+        <block v-else>
+          <text>{{ index }}</text>
+        </block>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-index-anchor',
+    props: {
+      // 使用自定义内容
+      useSlot: {
+        type: Boolean,
+        default: false
+      },
+      // 索引字符
+      index: {
+        type: String,
+        default: ''
+      },
+      // 自定义样式
+      customStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      }
+    },
+    computed: {
+      customAnchorStyle() {
+        return Object.assign(this.anchorStyle, this.customStyle)
+      }
+    },
+    data() {
+      return {
+        elId: this.$t.uuid(),
+        // 内容的高度
+        height: 0,
+        // 内容的top
+        top: 0,
+        // 是否被激活
+        active: false,
+        // 样式(父组件外部提供)
+        wrapperStyle: {},
+        anchorStyle: {}
+      }
+    },
+    created() {
+      this.parent = false
+    },
+    mounted() {
+      this.parent = this.$t.$parent.call(this, 'tn-index-list')
+      if (this.parent) {
+        this.parent.childrens.push(this)
+        this.parent.updateData()
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-index-anchor {
+    width: 100%;
+    box-sizing: border-box;
+    padding: 8rpx 24rpx;
+    color: $tn-font-color;
+    font-size: 28rpx;
+    font-weight: 500;
+    line-height: 1.2;
+    background-color: rgb(245, 245, 245);
+    
+    &--active {
+      right: 0;
+      left: 0;
+      color: $tn-main-color;
+      background-color: #FFFFFF;
+    }
+  }
+</style>

+ 361 - 0
tuniao-ui/components/tn-index-list/tn-index-list.vue

@@ -0,0 +1,361 @@
+<template>
+  <!-- 支付宝小程序使用_tGetRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+  <view>
+    <view class="tn-index-list-class tn-index-list">
+      <slot></slot>
+      
+      <!-- 侧边栏 -->
+      <view
+        v-if="showSidebar"
+        class="tn-index-list__sidebar"
+        @touchstart.stop.prevent="onTouchMove"
+        @touchmove.stop.prevent="onTouchMove"
+        @touchend.stop.prevent="onTouchStop"
+        @touchcancel.stop.prevent="onTouchStop"
+      >
+        <view
+          v-for="(item, index) in indexList"
+          :key="index"
+          class="tn-index-list__sidebar__item"
+          :style="{
+            zIndex: zIndex + 1,
+            color: activeAnchorIndex === index ? activeColor : ''
+          }"
+        >
+          {{ item }}
+        </view>
+      </view>
+      
+      <!-- 选中弹出框 -->
+      <view
+        v-if="touchMove && indexList[touchMoveIndex]"
+        class="tn-index-list__alert"
+        :style="{
+          zIndex: selectAlertZIndex
+        }"
+      >
+        <text>{{ indexList[touchMoveIndex] }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  // 生成 A-Z的字母列表
+  let indexList = function() {
+    let indexList = []
+    let charCodeOfA = 'A'.charCodeAt(0)
+    for (var i = 0; i < 26; i++) {
+      indexList.push(String.fromCharCode(charCodeOfA + i))
+    }
+    return indexList
+  }
+  
+  export default {
+    name: 'tn-index-list',
+    props: {
+      // 索引列表
+      indexList: {
+        type: Array,
+        default() {
+          return indexList()
+        }
+      },
+      // 是否自动吸顶
+      sticky: {
+        type: Boolean,
+        default: true
+      },
+      // 自动吸顶时距离顶部的距离,单位px
+      stickyTop: {
+        type: Number,
+        default: 0
+      },
+      // 自定义顶栏的高度,单位px
+      customBarHeight: {
+        type: Number,
+        default: 0
+      },
+      // 当前滚动的高度
+      // 由于自定义组件无法获取滚动高度,所以依赖传入
+      scrollTop: {
+        type: Number,
+        default: 0
+      },
+      // 选中索引时的颜色
+      activeColor: {
+        type: String,
+        default: '#01BEFF'
+      },
+      // 吸顶时的z-index
+      zIndex: {
+        type: Number,
+        default: 0
+      }
+    },
+    computed: {
+      // 选中索引列表弹出提示框的z-index
+      selectAlertZIndex() {
+        return this.$t.zIndex.toast
+      },
+      // 吸顶的偏移高度
+      stickyOffsetTop() {
+        // #ifdef H5
+        return this.stickyTop !== '' ? this.stickyTop : 44
+        // #endif
+        // #ifndef H5
+        return this.stickyTop !== '' ? this.stickyTop : 0
+        // #endif
+      }
+    },
+    data() {
+      return {
+        // 当前激活的列表锚点的序号
+        activeAnchorIndex: 0,
+        // 显示侧边索引栏
+        showSidebar: true,
+        // 标记是否开始触摸移动
+        touchMove: false,
+        // 当前触摸移动到对应索引的序号
+        touchMoveIndex: 0,
+        // 滚动到对应锚点的序号
+        scrollToAnchorIndex: 0,
+        // 侧边栏的信息
+        sidebar: {
+          height: 0,
+          top: 0
+        },
+        // 内容区域高度
+        height: 0,
+        // 内容区域top
+        top: 0
+      }
+    },
+    watch: {
+      scrollTop() {
+        this.updateData()
+      }
+    },
+    created() {
+      // 只能在created生命周期定义childrens,如果在data定义,会因为循环引用而报错
+      this.childrens = []
+    },
+    methods: {
+      // 更新数据
+      updateData() {
+        this.timer && clearTimeout(this.timer)
+        this.timer = setTimeout(() => {
+          this.showSidebar = !!this.childrens.length
+          this.getRect().then(() => {
+            this.onScroll()
+          })
+        }, 0)
+      },
+      // 获取对应的信息
+      getRect() {
+        return Promise.all([
+          this.getAnchorRect(),
+          this.getListRect(),
+          this.getSidebarRect()
+        ])
+      },
+      // 获取列表内容子元素信息
+      getAnchorRect() {
+        return Promise.all(this.childrens.map((child, index) => {
+          child._tGetRect('.tn-index-anchor__wrap').then((rect) => {
+            Object.assign(child, {
+              height: rect.height,
+              top: rect.top - this.customBarHeight
+            })
+          })
+        }))
+      },
+      // 获取列表信息
+      getListRect() {
+        return this._tGetRect('.tn-index-list').then(rect => {
+          Object.assign(this, {
+            height: rect.height,
+            top: rect.top + this.scrollTop
+          })
+        })
+      },
+      // 获取侧边滚动栏信息
+      getSidebarRect() {
+        return this._tGetRect('.tn-index-list__sidebar').then(rect => {
+          this.sidebar = {
+            height: rect.height,
+            top: rect.top
+          }
+        })
+      },
+      // 滚动事件
+      onScroll() {
+        const {
+          childrens = []
+        } = this
+        if (!childrens.length) {
+          return
+        }
+        const {
+          sticky,
+          stickyOffsetTop,
+          zIndex,
+          scrollTop,
+          activeColor
+        } = this
+        const active = this.getActiveAnchorIndex()
+        this.activeAnchorIndex = active
+        if (sticky) {
+          let isActiveAnchorSticky = false
+          if (active !== -1) {
+            isActiveAnchorSticky = childrens[active].top <= 0
+          }
+          childrens.forEach((item, index) => {
+            if (index === active) {
+              let wrapperStyle = ''
+              let anchorStyle = {
+                color: `${activeColor}`
+              }
+              if (isActiveAnchorSticky) {
+                wrapperStyle = {
+                  height: `${childrens[index].height}px`
+                }
+                anchorStyle = {
+                  position: 'fixed',
+                  top: `${stickyOffsetTop}px`,
+                  zIndex: `${zIndex ? zIndex : this.$t.zIndex.indexListSticky}`,
+                  color: `${activeColor}`
+                }
+              }
+              item.active = true
+              item.wrapperStyle = wrapperStyle
+              item.anchorStyle = anchorStyle
+            } else if (index === active - 1) {
+              const currentAnchor = childrens[index]
+              const currentOffsetTop = currentAnchor.top
+              const targetOffsetTop = index === childrens.length - 1 ? this.top : childrens[index + 1].top
+              const parentOffsetHeight = targetOffsetTop - currentOffsetTop
+              const translateY = parentOffsetHeight - currentAnchor.height
+              const anchorStyle = {
+                position: 'relative',
+                transform: `translate3d(0, ${translateY}px, 0)`,
+                zIndex: `${zIndex ? zIndex : this.$t.zIndex.indexListSticky}`,
+                color: `${activeColor}`
+              }
+              item.active = false
+              item.anchorStyle = anchorStyle
+            } else {
+              item.active = false
+              item.wrapperStyle = ''
+              item.anchorStyle = ''
+            }
+          })
+        }
+      },
+      // 触摸移动
+      onTouchMove(event) {
+        this.touchMove = true
+        const sidebarLength = this.childrens.length
+        const touch = event.touches[0]
+        const itemHeight = this.sidebar.height / sidebarLength
+        let clientY = touch.clientY
+        let index = Math.floor((clientY - this.sidebar.top) / itemHeight)
+        if (index < 0) {
+          index = 0
+        } else if (index > sidebarLength - 1) {
+          index = sidebarLength - 1
+        }
+        this.touchMoveIndex = index
+        this.scrollToAnchor(index)
+      },
+      // 触摸停止
+      onTouchStop() {
+        this.touchMove = false
+        this.scrollToAnchorIndex = null
+      },
+      // 获取当前的锚点序号
+      getActiveAnchorIndex() {
+        const {
+          childrens,
+          sticky
+        } = this
+        for (let i = this.childrens.length - 1; i >= 0; i--) {
+          const preAnchorHeight = i > 0 ? childrens[i - 1].height : 0
+          const reachTop = sticky ? preAnchorHeight : 0
+          if (reachTop >= childrens[i].top) {
+            return i
+          }
+        }
+        return -1
+      },
+      // 滚动到对应的锚点
+      scrollToAnchor(index) {
+        if (this.scrollToAnchorIndex === index) {
+          return
+        }
+        this.scrollToAnchorIndex = index
+        const anchor = this.childrens.find(item => item.index === this.indexList[index])
+        if (anchor) {
+          const scrollTop = anchor.top + this.scrollTop
+          this.$emit('select', {
+            index: anchor.index,
+            scrollTop: scrollTop
+          })
+          uni.pageScrollTo({
+            duration:0,
+            scrollTop: scrollTop
+          })
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-index-list {
+    position: relative;
+    
+    &__sidebar {
+      display: flex;
+      flex-direction: column;
+      position: fixed;
+      top: 50%;
+      right: 0;
+      text-align: center;
+      transform: translateY(-50%);
+      user-select: none;
+      z-index: 99;
+      
+      &__item {
+        font-weight: 500;
+        padding: 8rpx 18rpx;
+        font-size: 22rpx;
+        line-height: 1;
+      }
+    }
+    
+    &__alert {
+      display: flex;
+      flex-direction: row;
+      position: fixed;
+      width: 120rpx;
+      height: 120rpx;
+      top: 50%;
+      right: 90rpx;
+      align-items: center;
+      justify-content: center;
+      margin-top: -60rpx;
+      border-radius: 24rpx;
+      font-size: 50rpx;
+      color: #FFFFFF;
+      background-color: $tn-font-sub-color;
+      padding: 0;
+      z-index: 9999999;
+      
+      text {
+        line-height: 50rpx;
+      }
+    }
+  }
+</style>

+ 427 - 0
tuniao-ui/components/tn-input/tn-input.vue

@@ -0,0 +1,427 @@
+<template>
+  <view
+    class="tn-input-class tn-input"
+    :class="{
+      'tn-input--border': border,
+      'tn-input--error': validateState
+    }"
+    :style="{
+      padding: `0 ${border ? 20 : 0}rpx`,
+      borderColor: borderColor,
+      textAlign: inputAlign
+    }"
+    @tap.stop="inputClick"
+  >
+    <textarea
+      v-if="type === 'textarea'"
+      class="tn-input__input tn-input__textarea"
+      :style="[inputStyle]"
+      :value="defaultValue"
+      :placeholder="placeholder"
+      :placeholderStyle="placeholderStyle"
+      :disabled="disabled || type === 'select'"
+      :maxlength="maxLength"
+      :fixed="fixed"
+      :focus="focus"
+      :autoHeight="autoHeight"
+      :selectionStart="elSelectionStart"
+      :selectionEnd="elSelectionEnd"
+      :cursorSpacing="cursorSpacing"
+      :showConfirmBar="showConfirmBar"
+      @input="handleInput"
+      @blur="handleBlur"
+      @focus="onFocus"
+      @confirm="onConfirm"
+    />
+    <input
+      v-else
+      class="tn-input__input"
+      :type="type === 'password' ? 'text' : type"
+      :style="[inputStyle]"
+      :value="defaultValue"
+      :password="type === 'password' && !showPassword"
+      :placeholder="placeholder"
+      :placeholderStyle="placeholderStyle"
+      :disabled="disabled || type === 'select'"
+      :maxlength="maxLength"
+      :focus="focus"
+      :confirmType="confirmType"
+      :selectionStart="elSelectionStart"
+      :selectionEnd="elSelectionEnd"
+      :cursorSpacing="cursorSpacing"
+      :showConfirmBar="showConfirmBar"
+      @input="handleInput"
+      @blur="handleBlur"
+      @focus="onFocus"
+      @confirm="onConfirm"
+    />
+    
+    <!-- 右边的icon -->
+    <view class="tn-input__right-icon tn-flex tn-flex-col-center">
+      <!-- 清除按钮 -->
+      <view
+        v-if="clearable && value !== '' && focused"
+        class="tn-input__right-icon__item tn-input__right-icon__clear"
+        @tap="onClear"
+      >
+        <view class="icon tn-icon-close"></view>
+      </view>
+      <view
+        v-else-if="type === 'text' && !focused && showRightIcon && rightIcon !== ''"
+        class="tn-input__right-icon__item tn-input__right-icon__clear"
+      >
+        <view class="icon" :class="[`tn-icon-${rightIcon}`]"></view>
+      </view>
+      <!-- 显示密码按钮 -->
+      <view
+        v-if="passwordIcon && type === 'password'"
+        class="tn-input__right-icon__item tn-input__right-icon__clear"
+        @tap="showPassword = !showPassword"
+      >
+        <view v-if="!showPassword" class="tn-icon-eye-hide"></view>
+        <view v-else class="icon tn-icon-eye"></view>
+      </view>
+      <!-- 可选项箭头 -->
+      <view
+        v-if="type === 'select'"
+        class="tn-input__right-icon__item tn-input__right-icon__select"
+        :class="{
+          'tn-input__right-icon__select--reverse': selectOpen
+        }"
+      >
+        <view class="icon tn-icon-up-triangle"></view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import Emitter from '../../libs/utils/emitter.js'
+  
+  export default {
+    mixins: [Emitter],
+    name: 'tn-input',
+    props: {
+      value: {
+        type: [String, Number],
+        default: ''
+      },
+      // 输入框的类型
+      type: {
+        type: String,
+        default: 'text'
+      },
+      // 输入框文字对齐方式
+      inputAlign: {
+        type: String,
+        default: 'left'
+      },
+      // 文本框为空时显示的信息
+      placeholder: {
+        type: String,
+        default: ''
+      },
+      placeholderStyle: {
+        type: String,
+        default: 'color: #AAAAAA'
+      },
+      // 是否禁用输入框
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      // 可输入文字的最大长度
+      maxLength: {
+        type: Number,
+        default: 255
+      },
+      // 输入框高度
+      height: {
+        type: Number,
+        default: 0
+      },
+      // 根据内容自动调整高度
+      autoHeight: {
+        type: Boolean,
+        default: true
+      },
+      // 键盘右下角显示的文字,仅在text时生效
+      confirmType: {
+        type: String,
+        default: 'done'
+      },
+      // 输入框自定义样式
+      customStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      },
+      // 是否固定输入框
+      fixed: {
+        type: Boolean,
+        default: false
+      },
+      // 是否自动获取焦点
+      focus: {
+        type: Boolean,
+        default: false
+      },
+      // 当type为password时,是否显示右侧密码图标
+      passwordIcon: {
+        type: Boolean,
+        default: true
+      },
+      // 当type为 input或者textarea时是否显示边框
+      border: {
+        type: Boolean,
+        default: false
+      },
+      // 边框的颜色
+      borderColor: {
+        type: String,
+        default: '#dcdfe6'
+      },
+      // 当type为select时,旋转右侧图标,标记当时select是打开还是关闭
+      selectOpen: {
+        type: Boolean,
+        default: false
+      },
+      // 是否可清空
+      clearable: {
+        type: Boolean,
+        default: true
+      },
+      // 光标与键盘的距离
+      cursorSpacing: {
+        type: Number,
+        default: 0
+      },
+      // selectionStart和selectionEnd需要搭配使用,自动聚焦时生效
+      // 光标起始位置
+      selectionStart: {
+        type: Number,
+        default: -1
+      },
+      // 光标结束位置
+      selectionEnd: {
+        type: Number,
+        default: -1
+      },
+      // 自动去除两端空格
+      trim: {
+        type: Boolean,
+        default: true
+      },
+      // 是否显示键盘上方的完成按钮
+      showConfirmBar: {
+        type: Boolean,
+        default: true
+      },
+      // 是否在输入框内最右边显示图标
+      showRightIcon: {
+        type: Boolean,
+        default: false
+      },
+      // 最右边图标的名称
+      rightIcon: {
+        type: String,
+        default: ''
+      }
+    },
+    computed: {
+      // 输入框样式
+      inputStyle() {
+        let style = {}
+        // 如果没有设置高度,根据不同的类型设置一个默认值
+        style.minHeight = this.height ? this.height + 'rpx' : 
+          this.type === 'textarea' ? this.textareaHeight + 'rpx' : this.inputHeight + 'rpx'
+        
+        style = Object.assign(style, this.customStyle)
+        
+        return style
+      },
+      // 光标起始位置
+      elSelectionStart() {
+        return String(this.selectionStart)
+      },
+      // 光标结束位置
+      elSelectionEnd() {
+        return String(this.selectionEnd)
+      }
+    },
+    data() {
+      return {
+        // 默认值
+        defaultValue: this.value,
+        // 输入框高度
+        inputHeight: 70,
+        // textarea的高度
+        textareaHeight: 100,
+        // 标记验证的状态
+        validateState: false,
+        // 标记是否获取到焦点
+        focused: false,
+        // 是否预览密码
+        showPassword: false,
+        // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input事件
+        lastValue: '',
+      }
+    },
+    watch: {
+      value(newVal, oldVal) {
+        this.defaultValue = newVal
+        // 当值发生变化时,并且type为select时,不会触发input事件
+        // 模拟input事件
+        if (newVal !== oldVal && this.type === 'select') {
+          this.handleInput({
+            detail: {
+              value: newVal
+            }
+          })
+        }
+      }
+    },
+    created() {
+      // 监听form-item发出的错误事件,将输入框变成红色
+      this.$on("on-form-item-error", this.onFormItemError)
+    },
+    methods: {
+      /**
+       * input事件
+       */
+      handleInput(event) {
+        let value = event.detail.value
+        // 是否需要去掉空格
+        if (this.trim) value = this.$t.string.trim(value)
+        // 原生事件
+        this.$emit('input', value)
+        // model赋值
+        this.defaultValue = value
+        // 过一个生命周期再发送事件给tn-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
+        // 尚未更新到tn-form-item,导致获取的值为空,从而校验混论
+        // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
+        setTimeout(() => {
+          // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+          // #ifdef MP-TOUTIAO
+          if (this.$t.string.trim(value) === this.lastValue) return
+          this.lastValue = value
+          // #endif
+          
+          // 发送当前的值到form-item进行校验
+          this.dispatch('tn-form-item','on-form-change', value)
+        }, 40)
+      },
+      /**
+       * blur事件
+       */
+      handleBlur(event) {
+        let value = event.detail.value
+        
+        // 由于点击清除图标也会触发blur事件,导致图标消失从而无法点击
+        setTimeout(() => {
+          this.focused = false
+        }, 100)
+        
+        // 原生事件
+        this.$emit('blur', value)
+        // 过一个生命周期再发送事件给tn-form-item,否则this.$emit('blur')更新了父组件的值,但是微信小程序上
+        // 尚未更新到tn-form-item,导致获取的值为空,从而校验混论
+        // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
+        setTimeout(() => {
+          // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+          // #ifdef MP-TOUTIAO
+          if (this.$t.string.trim(value) === this.lastValue) return
+          this.lastValue = value
+          // #endif
+          
+          // 发送当前的值到form-item进行校验
+          this.dispatch('tn-form-item','on-form-blur', value)
+        }, 40)
+      },
+      // 处理校验错误
+      onFormItemError(status) {
+        this.validateState = status
+      },
+      // 聚焦事件
+      onFocus(event) {
+        this.focused = true
+        this.$emit('focus')
+      },
+      // 点击确认按钮事件
+      onConfirm(event) {
+        this.$emit('confirm', event.detail.value)
+      },
+      // 清除事件
+      onClear(event) {
+        this.$emit('input', '')
+      },
+      // 点击事件
+      inputClick() {
+        this.$emit('click')
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-input {
+    display: flex;
+    flex-direction: row;
+    position: relative;
+    flex: 1;
+    
+    &__input {
+      font-size: 28rpx;
+      color: $tn-font-color;
+      flex: 1;
+    }
+    
+    &__textarea {
+      width: auto;
+      font-size: 28rpx;
+      color: $tn-font-color;
+      padding: 10rpx 0;
+      line-height: normal;
+      flex: 1;
+    }
+    
+    &--border {
+      border-radius: 6rpx;
+      border: 2rpx solid $tn-border-solid-color;
+    }
+    
+    &--error {
+      border-color: $tn-color-red !important;
+    }
+    
+    &__right-icon {
+      line-height: 1;
+      .icon {
+        color: $tn-font-sub-color;
+      }
+      
+      &__item {
+        margin-left: 10rpx;
+      }
+      
+      &__clear {
+        .icon {
+          font-size: 32rpx;
+        }
+      }
+      
+      &__select {
+        transition: transform .4s;
+        
+        .icon {
+          font-size: 26rpx;
+        }
+        
+        &--reverse {
+          transform: rotate(-180deg);
+        }
+      }
+    }
+  }
+</style>

+ 220 - 0
tuniao-ui/components/tn-keyboard/tn-keyboard.vue

@@ -0,0 +1,220 @@
+<template>
+  <view v-if="value" class="tn-keyboard-class tn-keyboard">
+    <tn-popup
+      v-model="value"
+      mode="bottom"
+      :popup="false"
+      length="auto"
+      :mask="mask"
+      :maskCloseable="maskCloseable"
+      :safeAreaInsetBottom="safeAreaInsetBottom"
+      :zIndex="elZIndex"
+      @close="popupClose"
+    >
+      <view>
+        <slot></slot>
+      </view>
+      
+      <!-- 提示信息 -->
+      <view v-if="tooltip" class="tn-keyboard__tooltip">
+        <view
+          v-if="cancelBtn"
+          class="tn-keyboard__tooltip__item tn-keyboard__tooltip__cancel"
+          hover-class="tn-keyboard__tooltip__cancel--hover"
+          :hover-stay-time="150"
+          @tap="onCancel"
+        >
+          {{ cancelBtn ? cancelText : ''}}
+        </view>
+        <view v-if="showTips" class="tn-keyboard__tooltip__item tn-keyboard__tooltip__tips">
+          {{ tips ? tips : mode === 'number' ? '数字键盘' : mode === 'card' ? '身份证键盘' : '车牌号码键盘'}}
+        </view>
+        <view
+          v-if="confirmBtn"
+          class="tn-keyboard__tooltip__item tn-keyboard__tooltip__confirm"
+          hover-class="tn-keybord__tooltip__confirm--hover"
+          :hover-stay-time="150"
+          @tap="onConfirm"
+        >
+          {{ confirmBtn ? confirmText : ''}}
+        </view>
+      </view>
+      
+      <!-- 键盘内容 -->
+      <block v-if="mode === 'number' || mode === 'card'">
+        <tn-number-keyboard :mode="mode" :dotEnabled="dotEnabled" :randomEnabled="randomEnabled" @change="change" @backspace="backspaceClick"></tn-number-keyboard>
+      </block>
+      <block v-if="mode === 'car'">
+        <tn-car-keyboard :randomEnabled="randomEnabled" :switchEnMode="switchEnMode" @change="change" @backspace="backspaceClick"></tn-car-keyboard>
+      </block>
+    </tn-popup>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-keyboard',
+    props: {
+      // 控制键盘弹出收回
+      value: {
+        type: Boolean,
+        default: false
+      },
+      // 键盘类型
+      // number - 数字键盘 card - 身份证键盘 car - 车牌号码
+      mode: {
+        type: String,
+        default: 'number'
+      },
+      // 当mode = number时,是否显示'.'符号
+      dotEnabled: {
+        type: Boolean,
+        default: true
+      },
+      // 是否打乱顺序
+      randomEnabled: {
+        type: Boolean,
+        default: false
+      },
+      // 当mode = car,设置键盘中英文状态
+      switchEnMode: {
+        type: Boolean,
+        default: false
+      },
+      // 显示顶部工具条
+      tooltip: {
+        type: Boolean,
+        default: true
+      },
+      // 是否显示提示信息
+      showTips: {
+        type: Boolean,
+        default: true
+      },
+      // 提示文字
+      tips: {
+        type: String,
+        default: ''
+      },
+      // 是否显示取消按钮
+      cancelBtn: {
+        type: Boolean,
+        default: true
+      },
+      // 是否显示确认按钮
+      confirmBtn: {
+        type: Boolean,
+        default: true
+      },
+      // 取消按钮文字
+      cancelText: {
+        type: String,
+        default: '取消'
+      },
+      // 确认按钮文字
+      confirmText: {
+        type: String,
+        default: '确认'
+      },
+      // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+      safeAreaInsetBottom: {
+      	type: Boolean,
+      	default: false
+      },
+      // 是否可以通过点击遮罩进行关闭
+      maskCloseable: {
+      	type: Boolean,
+      	default: true
+      },
+      // 是否显示遮罩
+      mask: {
+        type: Boolean,
+        default: true
+      },
+      // z-index
+      zIndex: {
+        type: Number,
+        default: 0
+      }
+    },
+    computed: {
+      elZIndex() {
+        return this.zIndex ? this.zIndex : this.$t.zIndex.popup
+      }
+    },
+    data() {
+      return {
+        
+      }
+    },
+    methods: {
+      change(e) {
+        this.$emit('change', e)
+      },
+      // 关闭键盘
+      popupClose() {
+        // 修改value的值
+        this.$emit('input', false)
+      },
+      // 输入完成
+      onConfirm() {
+        this.popupClose()
+        this.$emit('confirm')
+      },
+      // 输入取消
+      onCancel() {
+        this.popupClose()
+        this.$emit('cancel')
+      },
+      // 点击退格
+      backspaceClick() {
+        this.$emit('backspace')
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-keyboard {
+    position: relative;
+    
+    &__tooltip {
+      display: flex;
+      flex-direction: row;
+      justify-content: space-between;
+      
+      &__item {
+        color: $tn-font-color;
+        flex: 0 0 33.3333333333%;
+        text-align: center;
+        font-size: 28rpx;
+        padding: 20rpx 10rpx;
+      }
+      
+      &__cancel {
+        text-align: left;
+        flex-grow: 1;
+        flex-wrap: 0;
+        padding-left: 40rpx;
+        color: $tn-content-color;
+        
+        &--hover {
+          color: $tn-font-color;
+        }
+      }
+      
+      &__confirm {
+        text-align: right;
+        flex-grow: 1;
+        flex-wrap: 0;
+        padding-right: 40rpx;
+        color: $tn-main-color;
+        
+        &--hover {
+          color: $tn-color-blue;
+        }
+      }
+    }
+  }
+</style>

+ 225 - 0
tuniao-ui/components/tn-landscape/tn-landscape.vue

@@ -0,0 +1,225 @@
+<template>
+  <view class="tn-landscape-class tn-landscape">
+    <view v-if="showValue" class="tn-landscape__container" :style="[containerStyle]">
+      <slot></slot>
+      <view
+        v-if="closeBtn"
+        class="tn-landscape__icon tn-icon-close-fill"
+        :class="[{
+          'tn-landscape__icon--left-top': closePosition === 'leftTop',
+          'tn-landscape__icon--right-top': closePosition === 'rightTop',
+          'tn-landscape__icon--bottom': closePosition === 'bottom'
+        }]"
+        :style="[closeBtnStyle]"
+        @tap="close"
+      ></view>
+    </view>
+    <view
+      v-if="mask"
+      class="tn-landscape__mask"
+      :class="[{'tn-landscape__mask--show': showValue}]"
+      :style="[maskStyle]"
+      @tap="closeMask"
+    ></view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-landscape',
+    props: {
+      // 显示
+      show: {
+        type: Boolean,
+        default: false
+      },
+      // 显示关闭图标
+      closeBtn: {
+        type: Boolean,
+        default: true
+      },
+      // 关闭图标颜色
+      closeColor: {
+        type: String,
+        default: ''
+      },
+      // 关闭图标大小,单位rpx
+      closeSize: {
+        type: Number,
+        default: 0
+      },
+      // 关闭图标位置
+      // leftTop -> 左上角 rightTop -> 右上角 bottom -> 底部
+      closePosition: {
+        type: String,
+        default: 'rightTop'
+      },
+      // 关闭图标top值,单位rpx
+      // 当关闭图标在leftTop或者rightTop时生效
+      closeTop: {
+        type: Number,
+        default: 0
+      },
+      // 关闭图标right值,单位rpx
+      // 当关闭图标在RightTop时生效
+      closeRight: {
+        type: Number,
+        default: 0
+      },
+      // 关闭图标bottom值,单位rpx
+      // 当关闭图标在bottom时生效
+      closeBottom: {
+        type: Number,
+        default: 0
+      },
+      // 关闭图标left值,单位rpx
+      // 当关闭图标在leftTop时生效
+      closeLeft: {
+        type: Number,
+        default: 0
+      },
+      // 显示遮罩
+      mask: {
+        type: Boolean,
+        default: true
+      },
+      // 点击遮罩可以关闭
+      maskCloseable: {
+        type: Boolean,
+        default: true
+      },
+      // zIndex
+      zIndex: {
+        type: Number,
+        default: 0
+      }
+    },
+    computed: {
+      containerStyle() {
+        let style = {}
+        style.zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.landsacpe
+        return style
+      },
+      closeBtnStyle() {
+        let style = {}
+        if (this.closePosition === 'leftTop') {
+          if (this.closeTop) {
+            style.top = this.closeTop + 'rpx'
+          }
+          if (this.closeLeft) {
+            style.left = this.closeLeft + 'rpx'
+          }
+        } else if (this.closePosition === 'rightTop') {
+          if (this.closeTop) {
+            style.top = this.closeTop + 'rpx'
+          }
+          if (this.closeRight) {
+            style.right = this.closeRight + 'rpx'
+          }
+        } else if (this.closePosition === 'bottom') {
+          if (this.closeBottom) {
+            style.bottom = this.closeBottom + 'rpx'
+          }
+        }
+        if (this.closeSize) {
+          style.fontSize = this.closeSize + 'rpx'
+        }
+        if (this.closeColor) {
+          style.color = this.closeColor
+        }
+        return style
+      },
+      maskStyle() {
+        let style = {}
+        style.zIndex = this.zIndex ? this.zIndex - 1 : this.$t.zIndex.landsacpe - 1
+        return style
+      }
+    },
+    watch: {
+      show: {
+        handler(val) {
+          this.showValue = val
+        },
+        immediate: true
+      }
+    },
+    data() {
+      return {
+        showValue: false
+      }
+    },
+    methods: {
+      // 关闭压屏窗
+      close() {
+        this.showValue = false
+        this.$emit('close')
+      },
+      // 点击遮罩关闭
+      closeMask() {
+        if (!this.maskCloseable) return
+        this.close()
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-landscape {
+    width: 100%;
+    overflow: hidden;
+    
+    &__container {
+      max-width: 100%;
+      position: fixed;
+      display: inline-flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+    }
+    
+    &__icon {
+      position: absolute;
+      text-align: center;
+      font-size: 50rpx;
+      color: #FFFFFF;
+      
+      &--left-top {
+        top: -40rpx;
+        left: 20rpx;
+      }
+      
+      &--right-top {
+        top: -40rpx;
+        right: 40rpx;
+      }
+      
+      &--bottom {
+        left: 50%;
+        bottom: -40rpx;
+        transform: translateX(-50%);
+      }
+    }
+    
+    &__mask {
+      position: fixed;
+      width: 100%;
+      height: 100%;
+      background-color: $tn-mask-bg-color;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      opacity: 0;
+      transform: scale3d(1, 1, 0);
+      transition: all 0.25s ease-in;
+      
+      &--show {
+        opacity: 1 !important;
+        transform: scale3d(1, 1, 1);
+      }
+    }
+  }
+</style>

ファイルの差分が大きいため隠しています
+ 254 - 0
tuniao-ui/components/tn-lazy-load/tn-lazy-load.vue


+ 143 - 0
tuniao-ui/components/tn-line-progress/tn-line-progress.vue

@@ -0,0 +1,143 @@
+<template>
+  <view
+    class="tn-line-progress-class tn-line-progress"
+    :style="[progressStyle]"
+  >
+    <view
+      class="tn-line-progress--active"
+      :class="[
+        $t.color.getBackgroundColorInternalClass(activeColor),
+        striped ? stripedAnimation ? 'tn-line-progress__striped tn-line-progress__striped--active' : 'tn-line-progress__striped' : '',
+      ]"
+      :style="[progressActiveStyle]"
+    >
+      <slot v-if="$slots.default || $slots.$default"></slot>
+      <block v-else-if="showPercent">{{ percent + '%' }}</block>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: 'tn-line-progress',
+    props: {
+      // 进度(百分比)
+      percent: {
+        type: Number,
+        default: 0,
+        validator: val => {
+          return val >= 0 && val <= 100
+        }
+      },
+      // 高度
+      height: {
+        type: Number,
+        default: 0
+      },
+      // 是否显示为圆角
+      round: {
+        type: Boolean,
+        default: true
+      },
+      // 是否显示条纹
+      striped: {
+        type: Boolean,
+        default: false
+      },
+      // 条纹是否运动
+      stripedAnimation: {
+        type: Boolean,
+        default: true
+      },
+      // 激活部分颜色
+      activeColor: {
+        type: String,
+        default: ''
+      },
+      // 非激活部分颜色
+      inactiveColor: {
+        type: String,
+        default: ''
+      },
+      // 是否显示进度条内部百分比值
+      showPercent: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      progressStyle() {
+        let style = {}
+        style.borderRadius = this.round ? '100rpx' : 0
+        if (this.height) {
+          style.height = this.$t.string.getLengthUnitValue(this.height)
+        }
+        if (this.inactiveColor) {
+          style.backgroundColor = this.inactiveColor
+        }
+        return style
+      },
+      progressActiveStyle() {
+        let style = {}
+        style.width = this.percent + '%'
+        if (this.$t.color.getBackgroundColorStyle(this.activeColor)) {
+          style.backgroundColor = this.$t.color.getBackgroundColorStyle(this.activeColor)
+        }
+        return style
+      }
+    },
+    data() {
+      return {
+        
+      }
+    },
+    
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-line-progress {
+    /* #ifndef APP-NVUE */
+    display: inline-flex;
+    /* #endif */
+    align-items: center;
+    width: 100%;
+    height: 28rpx;
+    overflow: hidden;
+    border-radius: 100rpx;
+    background-color: $tn-progress-bg-color;
+    
+    &--active {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-items: flex-end;
+      justify-content: space-around;
+      width: 0;
+      height: 100%;
+      font-size: 20rpx;
+      color: #FFFFFF;
+      background-color: $tn-main-color;
+      transition: all 0.3s ease;
+    }
+    
+    &__striped {
+      background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+      background-size: 80rpx 80rpx;
+      
+      &--active {
+        animation: progress-striped 2s linear infinite;
+      }
+    }
+  }
+  
+  @keyframes progress-striped {
+    0% {
+      background-position: 0 0;
+    }
+    100% {
+      background-position: 80rpx 0;
+    }
+  }
+</style>

+ 209 - 0
tuniao-ui/components/tn-list-cell/tn-list-cell.vue

@@ -0,0 +1,209 @@
+<template>
+  <view
+    class="tn-list-cell-class tn-list-cell"
+    :class="[
+      backgroundColorClass,
+      fontColorClass,
+      cellClass
+    ]"
+    :hover-class="hover ? 'tn-hover' : ''"
+    :hover-stay-time="150"
+    :style="[cellStyle]"
+    @tap="handleClick"
+  >
+    <slot></slot>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    mixins: [ componentsColorMixin ],
+    name: 'tn-list-cell',
+    props: {
+      // 列表序号
+      index: {
+        type: [Number, String],
+        default: '0'
+      },
+      // 内边距
+      padding: {
+        type: String,
+        default: ''
+      },
+      // 是否有箭头
+      arrow: {
+        type: Boolean,
+        default: false
+      },
+      //箭头是否有偏移距离
+      arrowRight: {
+      	type: Boolean,
+      	default: true
+      },
+      // 是否有点击效果
+      hover: {
+        type: Boolean,
+        default: false
+      },
+      // 隐藏线条
+      unlined: {
+        type: Boolean,
+        default: false
+      },
+      //线条是否有左偏移距离
+      lineLeft: {
+      	type: Boolean,
+      	default: true
+      },
+      //线条是否有右偏移距离
+      lineRight: {
+      	type: Boolean,
+      	default: true
+      },
+      //是否加圆角
+      radius: {
+      	type: Boolean,
+      	default: false
+      }
+    },
+    computed: {
+      cellClass() {
+        let clazz = ''
+        
+        if (this.arrow) {
+          clazz += ' tn-list-cell--arrow'
+          if (!this.arrowRight) {
+            clazz += ' tn-list-cell--arrow--none-right'
+          }
+        }
+        
+        if (this.unlined) {
+          clazz += ' tn-list-cell--unlined'
+        } else {
+          if (this.lineLeft) {
+            clazz += ' tn-list-cell--line-left'
+          }
+          if (this.lineRight) {
+            clazz += ' tn-list-cell--line-right'
+          }
+        }
+        
+        if (this.radius) {
+          clazz += ' tn-list-cell--radius'
+        }
+        
+        return clazz
+      },
+      cellStyle() {
+        let style = {}
+        
+        if (this.backgroundColorStyle) {
+          style.backgroundColor = this.backgroundColorStyle
+        }
+        
+        if (this.fontColorStyle) {
+          style.color = this.fontColorStyle
+        }
+        
+        if (this.fontSize) {
+          style.fontSize = this.fontSize + this.fontUnit
+        }
+        
+        if (this.padding) {
+          style.padding = this.padding
+        }
+        
+        return style
+      },
+      
+    },
+    data() {
+      return {
+        
+      }
+    },
+    methods: {
+      // 处理点击事件
+      handleClick() {
+        this.$emit("click", {
+          index: Number(this.index)
+        })
+        this.$emit("tap", {
+          index: Number(this.index)
+        })
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-list-cell {
+    position: relative;
+    width: 100%;
+    box-sizing: border-box;
+    background-color: #FFFFFF;
+    color: $tn-font-color;
+    font-size: 28rpx;
+    padding: 26rpx 30rpx;
+    
+    &--radius {
+      border-radius: 12rpx;
+      overflow: hidden;
+    }
+    
+    &--arrow {
+      &::before {
+        content: " ";
+        position: absolute;
+        top: 50%;
+        right: 30rpx;
+        width: 20rpx;
+        height: 20rpx;
+        margin-top: -12rpx;
+        border-width: 4rpx 4rpx 0 0;
+        border-color: $tn-font-holder-color;
+        border-style: solid;
+        transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
+      }
+      
+      &--none-right {
+        &::before {
+          right: 0 !important;
+        }
+      }
+    }
+    
+    &::after {
+      content: " ";
+      position: absolute;
+      bottom: 0;
+      right: 0;
+      left: 0;
+      pointer-events: none;
+      border-bottom: 1px solid $tn-border-solid-color;
+      transform: scaleY(0.5) translateZ(0);
+      transform-origin: 0 100%;
+    }
+    
+    &--line-left {
+      &::after {
+        left: 30rpx !important;
+      }
+    }
+    
+    &--line-right {
+      &::after {
+        right: 30rpx !important;
+      }
+    }
+    
+    &--unlined {
+      &::after {
+        border-bottom: 0 !important;
+      }
+    }
+  }
+  
+</style>

+ 184 - 0
tuniao-ui/components/tn-list-view/tn-list-view.vue

@@ -0,0 +1,184 @@
+<template>
+  <view
+    class="tn-list-view-class tn-list-view"
+    :class="[
+      backgroundColorClass,
+      viewClass
+    ]"
+    :style="[viewStyle]"
+  >
+    <view
+      v-if="showTitle"
+      class="tn-list-view__title"
+      :class="[
+        fontColorClass
+      ]"
+      :style="[titleStyle]"
+      @tap="handleClickTitle"
+    >{{ title }}</view>
+    
+    <view
+      v-else
+      :class="[{'tn-list-view__title--card': card}]"
+      @tap="handleClickTitle"
+    >
+      <slot name="title"></slot>
+    </view>
+    
+    <view
+      class="tn-list-view__content tn-border-solid-top tn-border-solid-bottom"
+      :class="[contentClass]"
+    >
+      <slot></slot>
+    </view>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    mixins: [ componentsColorMixin ],
+    name: 'tn-list-view',
+    props: {
+      // 标题
+      title: {
+        type: String,
+        default: ''
+      },
+      // 去掉边框 上边框 top, 下边框 bottom, 所有边框 all
+      unlined: {
+        type: String,
+        default: 'all'
+      },
+      // 上外边距
+      marginTop: {
+        type: String,
+        default: ''
+      },
+      // 内容是否显示为卡片模式
+      card: {
+        type: Boolean,
+        default: false
+      },
+      // 是否自定义标题
+      customTitle: {
+        type: Boolean,
+        default: false
+      }
+    },
+    computed: {
+      showTitle() {
+        return !this.customTitle && this.title
+      },
+      viewClass() {
+        let clazz = ''
+        
+        if (this.card) {
+          clazz += ' tn-list-view--card'
+        }
+        
+        return clazz
+      },
+      viewStyle() {
+        let style = {}
+        
+        if (this.backgroundColorStyle) {
+          style.backgroundColor = this.backgroundColorStyle
+        }
+        
+        if (this.marginTop) {
+          style.marginTop = this.marginTop
+        }
+        
+        return style
+      },
+      titleStyle() {
+        let style = {}
+        
+        if (this.fontColorStyle) {
+          style.color = this.fontColorStyle
+        }
+        if (this.fontSize) {
+          style.fontSize = this.fontSize + this.fontUnit
+        }
+        
+        return style
+      },
+      contentClass() {
+        let clazz = ''
+        
+        if (this.card) {
+          clazz += ' tn-list-view__content--card'
+        }
+        
+        switch(this.unlined) {
+          case 'top':
+            clazz += ' tn-none-border-top'
+            break
+          case 'bottom':
+            clazz += ' tn-none-border-bottom'
+            break
+          case 'all':
+            clazz += ' tn-none-border'
+            break
+        }
+        
+        return clazz
+      }
+    },
+    data () {
+      return {
+        kindShowFlag: this.showKind
+      }
+    },
+    methods: {
+      // 处理标题点击事件
+      handleClickTitle() {
+        if (!this.kindList) return
+        this.kindShowFlag = !this.kindShowFlag
+        this.$emit("clickTitle", {})
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+  .tn-list-view {
+    background-color: #FFFFFF;
+    
+    &__title {
+      width: 100%;
+      padding: 30rpx;
+      font-size: 30rpx;
+      line-height: 30rpx;
+      box-sizing: border-box;
+      
+      &--card {
+        // margin: 0rpx 30rpx;
+      }
+    }
+    
+    &__content {
+      width: 100%;
+      position: relative;
+      border-radius: 0;
+      
+      &--card {
+        // width: auto;
+        // overflow: hidden;
+        // margin-right: 30rpx;
+        // margin-left: 30rpx;
+        // border-radius: 20rpx
+      }
+    }
+    
+    &--card {
+      // padding-bottom: 30rpx;
+      border-radius: 20rpx;
+      overflow: hidden;
+    }
+    
+  }
+  
+</style>

+ 188 - 0
tuniao-ui/components/tn-load-more/tn-load-more.vue

@@ -0,0 +1,188 @@
+<template>
+  <view class="tn-load-more-class tn-load-more">
+    <view
+      class="tn-load-more__wrap"
+      :class="[backgroundColorClass]"
+      :style="[loadStyle]"
+    >
+      <view class="tn-load-more__line"></view>
+      <view
+        class="tn-load-more__content"
+        :class="[{'tn-load-more__content--more': (status === 'loadmore' || status === 'nomore')}]"
+      >
+        <view class="tn-load-more__loading">
+          <tn-loading
+            class="tn-load-more__loading__icon"
+            :mode="loadingIconType"
+            :show="status === 'loading' && loadingIcon"
+            :color="loadingIconColor"
+          ></tn-loading>
+        </view>
+        <view
+          class="tn-load-more__text"
+          :class="[fontColorClass, {'tn-load-more__text--dot': (status === 'nomore' && dot)}]"
+          :style="[loadTextStyle]"
+        >{{ showText }}</view>
+      </view>
+      <view class="tn-load-more__line"></view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import componentsColorMixin from '../../libs/mixin/components_color.js'
+  export default {
+    name: 'tn-load-more',
+    mixins: [componentsColorMixin],
+    props: {
+      // 加载状态
+      // loadmore -> 加载更多
+      // loading -> 加载中
+      // nomore -> 没有更多
+      status: {
+        type: String,
+        default: 'loadmore'
+      },
+      // 显示加载图标
+      loadingIcon: {
+        type: Boolean,
+        default: true
+      },
+      // 加载图标样式,参考tn-loading组件的加载类型
+      loadingIconType: {
+        type: String,
+        default: 'circle'
+      },
+      // 在圆圈加载状态下,圆圈的颜色
+      loadingIconColor: {
+        type: String,
+        default: ''
+      },
+      // 显示的文字
+      loadText: {
+        type: Object,
+        default() {
+          return {
+            loadmore: '加载更多',
+            loading: '正在加载...',
+            nomore: '没有更多了'
+          }
+        }
+      },
+      // 是否显示粗点,在nomore状态下生效
+      dot: {
+        type: Boolean,
+        default: false
+      },
+      // 自定义组件样式
+      customStyle: {
+        type: Object,
+        default() {
+          return {}
+        }
+      }
+    },
+    computed: {
+      loadStyle() {
+        let style = {}
+        if (this.backgroundColorStyle) {
+          style.backgroundColor = this.backgroundColorStyle
+        }
+        // 合并用户自定义样式
+        if (Object.keys(this.customStyle).length > 0) {
+          Object.assign(style, this.customStyle)
+        }
+        return style
+      },
+      loadTextStyle() {
+        let style = {}
+        if (this.fontColorStyle) {
+          style.color = this.fontColorStyle
+        }
+        if (this.fontSizeStyle) {
+          style.fontSize = this.fontSizeStyle
+          style.lineHeight = this.$t.string.getLengthUnitValue(this.fontSize + 2, this.fontUnit)
+        }
+        return style
+      },
+      // 显示的提示文字
+      showText() {
+        let text = ''
+        if (this.status === 'loadmore') text = this.loadText.loadmore || '加载更多'
+        else if (this.status === 'loading') text = this.loadText.loading || '正在加载...'
+        else if (this.status === 'nomore' && this.dot) text = this.dotText
+        else text = this.loadText.nomore || '没有更多了'
+        
+        return text
+      }
+    },
+    data() {
+      return {
+        // 粗点
+        dotText: '●'
+      }
+    },
+    methods: {
+      // 处理加载更多事件
+      loadMore() {
+        // 只有在 loadmore 状态下点击才会发送点击事件,内容不满一屏时无法触发底部上拉事件,所以需要点击来触发
+        if (this.status === 'loadmore') this.$emit('loadmore')
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .tn-load-more {
+    
+    &__wrap {
+      background-color: transparent;
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+      align-items: center;
+      color: $tn-content-color;
+    }
+    
+    &__line {
+      vertical-align: middle;
+      border: 1px solid $tn-content-color;
+      width: 50rpx;
+      transform: scaleY(0.5);
+    }
+    
+    &__content {
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+      align-items: center;
+      padding: 0 12rpx;
+      
+      &--more {
+        position: relative;
+      }
+    }
+    
+    &__loading {
+      margin-right: 8rpx;
+      
+      &__icon {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        align-items: center;
+      }
+    }
+    
+    &__text {
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      line-height: 30rpx;
+      
+      &--dot {
+        font-size: 28rpx;
+      }
+    }
+  }
+</style>

ファイルの差分が大きいため隠しています
+ 114 - 0
tuniao-ui/components/tn-loading/tn-loading.vue


+ 0 - 0
tuniao-ui/components/tn-modal/tn-modal.vue


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません