瀏覽代碼

农商云小程序消息模块

潘超林 10 月之前
當前提交
dfd0269a28
共有 100 個文件被更改,包括 48005 次插入0 次删除
  1. 5 0
      .gitignore
  2. 23 0
      .hbuilderx/launch.json
  3. 105 0
      App.vue
  4. 62 0
      README.md
  5. 38 0
      androidPrivacy.json
  6. 34 0
      api/home/index.js
  7. 6 0
      api/index.js
  8. 142 0
      api/message/index.js
  9. 136 0
      api/request.js
  10. 36 0
      app.config.js
  11. 229 0
      components/classification.vue
  12. 305 0
      components/gjs-selectCity.vue
  13. 43 0
      index.html
  14. 15 0
      locales/en/index.ts
  15. 41 0
      locales/en/login.json
  16. 15 0
      locales/index.ts
  17. 15 0
      locales/zh_cn/index.ts
  18. 41 0
      locales/zh_cn/login.json
  19. 93 0
      loginChat.ts
  20. 43 0
      main.js
  21. 175 0
      manifest.json
  22. 36748 0
      package-lock.json
  23. 56 0
      package.json
  24. 149 0
      pages.json
  25. 171 0
      pages/group/add.vue
  26. 125 0
      pages/group/black-list.vue
  27. 84 0
      pages/group/chat.vue
  28. 287 0
      pages/group/data.vue
  29. 509 0
      pages/group/index.vue
  30. 0 0
      pages/group/person.vue
  31. 0 0
      pages/group/scan.vue
  32. 217 0
      pages/message/interactive.vue
  33. 202 0
      pages/message/serviceNotice.vue
  34. 140 0
      pages/views/login.vue
  35. 432 0
      pages/views/profile.vue
  36. 4 0
      shims-vue.d.ts
  37. 19 0
      static/background.svg
  38. 二進制
      static/im-app.png
  39. 二進制
      static/img/hmd.png
  40. 二進制
      static/img/lxr.png
  41. 二進制
      static/img/ql.png
  42. 二進制
      static/login-bg.png
  43. 9 0
      static/logo-back.svg
  44. 二進制
      static/logo.png
  45. 二進制
      static/message-selected.png
  46. 二進制
      static/message.png
  47. 二進制
      static/profile-selected.png
  48. 二進制
      static/profile.png
  49. 二進制
      static/relation-selected.png
  50. 二進制
      static/relation.png
  51. 27 0
      static/selected.svg
  52. 1 0
      timpush-configs.json
  53. 77 0
      uni.scss
  54. 109 0
      uni_modules/uview-ui/components/u-number-box/props.js
  55. 416 0
      uni_modules/uview-ui/components/u-number-box/u-number-box.vue
  56. 19 0
      uni_modules/uview-ui/components/u-number-keyboard/props.js
  57. 196 0
      uni_modules/uview-ui/components/u-number-keyboard/u-number-keyboard.vue
  58. 24 0
      uni_modules/uview-ui/components/u-overlay/props.js
  59. 68 0
      uni_modules/uview-ui/components/u-overlay/u-overlay.vue
  60. 499 0
      uni_modules/uview-ui/components/u-parse/node/node.vue
  61. 1075 0
      uni_modules/uview-ui/components/u-parse/parser.js
  62. 45 0
      uni_modules/uview-ui/components/u-parse/props.js
  63. 366 0
      uni_modules/uview-ui/components/u-parse/u-parse.vue
  64. 5 0
      uni_modules/uview-ui/components/u-picker-column/props.js
  65. 27 0
      uni_modules/uview-ui/components/u-picker-column/u-picker-column.vue
  66. 79 0
      uni_modules/uview-ui/components/u-picker/props.js
  67. 286 0
      uni_modules/uview-ui/components/u-picker/u-picker.vue
  68. 79 0
      uni_modules/uview-ui/components/u-popup/props.js
  69. 304 0
      uni_modules/uview-ui/components/u-popup/u-popup.vue
  70. 85 0
      uni_modules/uview-ui/components/u-radio-group/props.js
  71. 108 0
      uni_modules/uview-ui/components/u-radio-group/u-radio-group.vue
  72. 64 0
      uni_modules/uview-ui/components/u-radio/props.js
  73. 339 0
      uni_modules/uview-ui/components/u-radio/u-radio.vue
  74. 69 0
      uni_modules/uview-ui/components/u-rate/props.js
  75. 306 0
      uni_modules/uview-ui/components/u-rate/u-rate.vue
  76. 61 0
      uni_modules/uview-ui/components/u-read-more/props.js
  77. 157 0
      uni_modules/uview-ui/components/u-read-more/u-read-more.vue
  78. 39 0
      uni_modules/uview-ui/components/u-row-notice/props.js
  79. 330 0
      uni_modules/uview-ui/components/u-row-notice/u-row-notice.vue
  80. 19 0
      uni_modules/uview-ui/components/u-row/props.js
  81. 93 0
      uni_modules/uview-ui/components/u-row/u-row.vue
  82. 5 0
      uni_modules/uview-ui/components/u-safe-bottom/props.js
  83. 56 0
      uni_modules/uview-ui/components/u-safe-bottom/u-safe-bottom.vue
  84. 28 0
      uni_modules/uview-ui/components/u-scroll-list/nvue.js
  85. 0 0
      uni_modules/uview-ui/components/u-scroll-list/other.js
  86. 34 0
      uni_modules/uview-ui/components/u-scroll-list/props.js
  87. 50 0
      uni_modules/uview-ui/components/u-scroll-list/scrollWxs.wxs
  88. 224 0
      uni_modules/uview-ui/components/u-scroll-list/u-scroll-list.vue
  89. 118 0
      uni_modules/uview-ui/components/u-search/props.js
  90. 303 0
      uni_modules/uview-ui/components/u-search/u-search.vue
  91. 59 0
      uni_modules/uview-ui/components/u-skeleton/props.js
  92. 244 0
      uni_modules/uview-ui/components/u-skeleton/u-skeleton.vue
  93. 113 0
      uni_modules/uview-ui/components/u-slider/mpother.js
  94. 42 0
      uni_modules/uview-ui/components/u-slider/mpwxs.js
  95. 121 0
      uni_modules/uview-ui/components/u-slider/mpwxs.wxs
  96. 180 0
      uni_modules/uview-ui/components/u-slider/nvue - 副本.js
  97. 193 0
      uni_modules/uview-ui/components/u-slider/nvue.js
  98. 54 0
      uni_modules/uview-ui/components/u-slider/props.js
  99. 55 0
      uni_modules/uview-ui/components/u-slider/u-slider.vue
  100. 0 0
      uni_modules/uview-ui/components/u-status-bar/props.js

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+# 忽略以下文件和目录
+node_modules/
+node_modules
+unpackage/
+unpackage

+ 23 - 0
.hbuilderx/launch.json

@@ -0,0 +1,23 @@
+{
+    // 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" : [
+        {
+            "app-plus" : {
+                "launchtype" : "local"
+            },
+            "default" : {
+                "launchtype" : "local"
+            },
+            "mp-weixin" : {
+                "launchtype" : "local"
+            },
+            "type" : "uniCloud"
+        },
+        {
+            "playground" : "custom",
+            "type" : "uni-app:app-ios"
+        }
+    ]
+}

+ 105 - 0
App.vue

@@ -0,0 +1,105 @@
+<script lang="ts">
+import { TUIChatKit } from "./TUIKit";
+import { TUITranslateService } from "@tencentcloud/chat-uikit-engine";
+// #ifndef MP-WEIXIN
+import { locales } from "./locales";
+// #endif
+import TIMPushConfigs from "./timpush-configs.json";
+// #ifdef APP-PLUS
+// register TencentCloud-TIMPush
+import { IEnterChatConfig, loginFromStorage, openChat } from "./loginChat";
+import TUIChatEngine from "@tencentcloud/chat-uikit-engine";
+import { getNotificationAuth } from "./utils/getNotificationAuth";
+const TIMPush: any = uni.requireNativePlugin("TencentCloud-TIMPush");
+console.warn(
+  `TencentCloud-TIMPush: uni.requireNativePlugin ${TIMPush ? "success" : "fail"}`
+);
+uni.$TIMPush = TIMPush;
+uni.$TIMPushConfigs = TIMPushConfigs;
+const enterChatConfig: IEnterChatConfig = {
+  isLoginChat: false,
+  conversationID: "",
+};
+// register TencentCloud-TUICallKit
+const TUICallKit: any = uni.requireNativePlugin("TencentCloud-TUICallKit");
+console.warn(
+  `TencentCloud-TUICallKit: uni.requireNativePlugin ${TUICallKit ? "success" : "fail"}`
+);
+uni.$TUICallKit = TUICallKit;
+// #endif
+
+// #ifdef APP-ANDROID
+const notificationChannelInfo = {
+  notificationChannelList: [
+    {
+      channelID: "tuikit", // 控制台配置 oppo 的 channelID
+      channelName: "tuikit", // 自定义名称
+      channelDesc: "自定义铃音", // 自定义描述
+      channelSound: "private_ring", // 自定义铃音的名称且不需要后缀名
+    },
+  ],
+};
+uni.$TIMPush.createNotificationChannels(notificationChannelInfo);
+// #endif
+
+// #ifndef MP-WEIXIN
+TUITranslateService.provideLanguages(locales);
+TUITranslateService.useI18n();
+// #endif
+
+TUIChatKit.init();
+const SDKAppID = 1600005199; // Your SDKAppID
+const secretKey = "82f8b45664eb3252cf02f939aaac43cd81ba25b7c7f94f45be4f05e173e1ab2d"; // Your secretKey
+
+uni.$chat_SDKAppID = SDKAppID;
+uni.$chat_secretKey = secretKey;
+
+export default {
+  onLaunch: function () {
+    // #ifdef APP-PLUS
+    // 在 App.vue, 生命钩子 onLaunch 中监听
+    if (typeof uni.$TIMPush === "undefined") {
+      console.warn(
+        "如果您使用推送功能,需集成 TIMPush 插件,使用真机运行并且自定义基座调试,请参考官网文档:https://cloud.tencent.com/document/product/269/103522"
+      );
+    } else {
+      getNotificationAuth();
+      uni.$on("uikitLogin", () => {
+        enterChatConfig.isLoginChat = true;
+        openChat(enterChatConfig);
+      });
+      uni.$TIMPush.setOfflinePushListener((data) => {
+        const { notification = "" } = data?.data || {};
+        if (!notification) {
+          return;
+        }
+        const { entity } = JSON.parse(notification);
+        const type =
+          entity.chatType === 1
+            ? TUIChatEngine.TYPES.CONV_C2C
+            : TUIChatEngine.TYPES.CONV_GROUP;
+        enterChatConfig.conversationID = `${type}${entity.sender}`;
+        if (enterChatConfig.isLoginChat && entity.sender) {
+          openChat(enterChatConfig);
+        }
+      });
+      loginFromStorage();
+    }
+    // #endif
+  },
+
+  onShow: function () {},
+  onHide: function () {},
+};
+</script>
+<style>
+/* 每个页面公共css */
+uni-page-body,
+html,
+body,
+page {
+  width: 100% !important;
+  height: 100% !important;
+  overflow: hidden;
+}
+</style>

+ 62 - 0
README.md

@@ -0,0 +1,62 @@
+## 关于 chat-uikit-uniapp
+
+chat-uikit-uniapp (vue2 / vue3)是基于腾讯云 Chat SDK 的一款 uniapp UI 组件库,它提供了一些通用的 UI 组件,包含会话、聊天、群组、关系链等功能。基于这些精心设计的 UI 组件,您可以快速构建优雅的、可靠的、可扩展的 Chat 应用。
+
+> [!IMPORTANT]
+> 为尊重表情设计版权,IM Demo/TUIKit 工程中不包含大表情元素切图,正式上线商用前请您替换为自己设计或拥有版权的其他表情包。默认的小黄脸表情包版权归腾讯云所有,可有偿授权使用,如您希望获得授权可 提交工单 联系我们。
+>
+> 提交工单链接:https://console.cloud.tencent.com/workorder/category?level1_id=29&level2_id=40&source=14&data_title=%E5%8D%B3%E6%97%B6%E9%80%9A%E4%BF%A1%20IM&step=1
+
+chat-uikit-uniapp 界面效果如下图所示:
+![](https://qcloudimg.tencent-cloud.cn/raw/2f16b1be0591a325250f9066af898036.png)
+
+## chat-uikit-uniapp 支持平台
+
+- Android
+- iOS
+- 微信小程序
+- H5
+
+## 快速跑通DEMO
+
+### 第一步:下载 chat-uikit-uniapp 项目并安装依赖
+
+下载源码到你当前的工作空间 `${workspace}` 并将项目命名为 `chat-uikit-uniapp`。
+
+切换路径到 `${workspace}/chat-uikit-uniapp/sample` 中,并下载项目依赖。
+
+```bash
+git clone https://github.com/TencentCloud/chat-uikit-uniapp.git && cd chat-uikit-uniapp/sample && npm i
+```
+
+### 第二步:通过 HBuilderX 打开项目
+
+通过 [HBuilderX](https://dcloud.io/hbuilderx.html) 打开项目中的 sample 文件夹 `${workspace}/chat-uikit-uniapp/sample`。
+
+### 第三步:获取 SDKAppID 与 secretKey
+
+设置 App.vue 文件示例代码中的相关参数 SDKAppID 与 secretKey 。
+
+SDKAppID 和 secretKey 等信息,可通过 [即时通信 IM 控制台](https://console.cloud.tencent.com/im) 获取,单击目标应用卡片,进入应用的基础配置页面。例如:  
+![image](https://user-images.githubusercontent.com/57951148/192587785-6577cc5e-acf9-423c-86d0-52c67234ab1f.png)
+
+将获得的SDKAppID和secretKey,赋值给 `${workspace}/chat-uikit-uniapp/sample/App.vue` 文件中第12行和第13行的 `SDKAppID` 和 `secretKey` 参数。
+
+```js
+const SDKAppID = 0; // Your SDKAppID
+const secretKey = "xxx"; //Your secretKey
+```
+
+### 第四步:使用 HBuilderX 运行 sample
+
+打开 HBuilderX 工具栏 -> 运行 -> 运行到小程序模拟器 -> 微信开发者工具 - [sample]。
+
+### 第五步:使用微信开发者工具打开小程序(可选)
+
+在第四步中,HBuilderX 编译之后可能出现无法自动拉起[微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)的情况,您可以手动使用微信开发者工具打开小程序。
+
+使用微信开发者工具打开项目 `${workspace}/chat-uikit-uniapp/sample/unpackage/dist/dev/mp-weixin` 后,手动编译小程序。
+
+## 相关链接
+
+[即时通信 IM 快速入门 chat-uikit-uniapp(uniapp vue2/vue3)](https://cloud.tencent.com/document/product/269/64506)

File diff suppressed because it is too large
+ 38 - 0
androidPrivacy.json


+ 34 - 0
api/home/index.js

@@ -0,0 +1,34 @@
+import Request from '../request';
+
+let request = Request.http;
+
+export default {
+	/**
+  * 获取所有群组
+  * @param {*} param 
+  * @returns 
+  */
+	getClassificationList(param) {
+		return request({
+			url: '/system/app/v1/classification/getClassificationList',
+			method: 'post',
+			data: param,
+			header: {
+				'Content-Type': 'application/json'
+			}
+		})
+	},
+	/**
+	 * 获取城市列表
+	 * @param {number} spuId     商品id
+	 */
+	getCity(param) {
+		return request({
+			url: '/system/app/v1/city',
+			method: 'get',
+			data: param,
+
+		})
+	},
+}
+

+ 6 - 0
api/index.js

@@ -0,0 +1,6 @@
+import home from './home/index.js';
+import message from './message/index.js'
+export default {
+    ...home,
+    ...message
+}

+ 142 - 0
api/message/index.js

@@ -0,0 +1,142 @@
+import Request from '../request';
+
+let request = Request.http;
+
+
+
+export default {
+    /**
+  * 获取所有群组
+  * @param {*} param 
+  * @returns 
+  */
+    getAllGroups(param) {
+        return request({
+            url: '/im/app/v1/im/getAllGroups',
+            method: 'post',
+            data: param,
+        })
+    },
+    /**
+     * 入群下单
+     * @param {*} param 
+     * @returns 
+     */
+    addImGroupOrder(param) {
+        return request({
+            url: '/order/app/v1/imorder/addImGroupOrder',
+            method: 'post',
+            data: param,
+            header: {
+                'Content-Type': 'application/json'
+            }
+        })
+    },
+    /**
+     * 根据群组groupId查询群组信息--是否已加入该群组
+     * @param {*} param 
+     * @returns 
+     */
+    getGroupByGroupId(param) {
+        return request({
+            url: '/im/app/v1/im/getResourceAggregationGroupByGroupId',
+            method: 'get',
+            data: param,
+        })
+    },
+    /**
+ * 加入指定的群组
+ * @param {*} param 
+ * @returns 
+ */
+    addRagUser(param) {
+        return request({
+            url: '/im/app/v1/im/addRagUser',
+            method: 'post',
+            data: param,
+            // header: {
+            //     'Content-type': "application/x-www-form-urlencoded",
+            // }
+        })
+    },
+
+    /**
+     * 根据手机号获取用户
+     * @param {*} param 
+     * @returns 
+     */
+    getUserInfoByPhone(param) {
+        return request({
+            url: '/user/app/v1/user/getUserInfoByPhone/' + param,
+            method: 'post',
+        })
+    },
+
+    /**
+     * 获取用户信息
+     * @param {*} param 
+     * @returns 
+     */
+    getUserInfo(param) {
+        return request({
+            url: '/user/app/v1/user/getUserInfo/' + param,
+            method: 'post',
+        })
+    },
+
+    /**
+       * 获取店铺信息
+       * @param {*} param 
+       * @returns 
+       */
+    getByOwnerId(param) {
+        return request({
+            url: '/user/app/v1/store/getByOwnerId/' + param,
+            method: 'post',
+        })
+    },
+
+    /**
+     * 订单详情
+     * @param {*} param 
+     * @returns 
+     */
+    orderDetail(param) {
+        return request({
+            url: '/order/app/v1/seller/order/detail-v2/' + param,
+            method: 'get',
+        })
+    },
+
+    /**
+  * 通知列表
+  * @param {*} param 
+  * @returns 
+  */
+    noticeList(param) {
+        return request({
+            url: '/user/app/v1/notice/list',
+            method: 'get',
+            data: param
+        })
+    },
+
+
+    nuRead(param) {
+        return request({
+            url: '/user/app/v1/notice/nuRead/' + param,
+            method: 'get',
+        })
+    },
+
+
+    readAll(param) {
+        return request({
+            url: '/user/app/v1/notice/readAll',
+            method: 'put',
+            data: param
+        })
+    },
+}
+
+

+ 136 - 0
api/request.js

@@ -0,0 +1,136 @@
+import {
+	requestConfig
+} from '@/app.config.js'
+let isRefreshing = true // 是否正在刷新的标记
+let subscribers = []; // 缓存
+
+function onAccessTokenFetched() {
+	// 更新个人中心的数据
+	uni.$emit("userinfoRefresh", true)
+	subscribers.forEach((callback) => {
+		callback()
+	})
+	subscribers = [];
+}
+
+function addSubscriber(callback) {
+	subscribers.push(callback)
+}
+
+function checkStatus(param) {
+	// 刷新token的函数,这需要添加一个开关,防止重复请求
+	if (isRefreshing) {
+		referToken()
+	}
+	isRefreshing = false;
+	// 将token刷新成功后的回调请求缓存
+	const retryOriginalRequest = new Promise((resolve) => {
+		addSubscriber(() => {
+			resolve(Request.prototype.http(param))
+		})
+	});
+	return retryOriginalRequest;
+}
+
+// 无感刷新
+function referToken() {
+	uni.removeStorageSync('token');
+
+	api.login({
+		grant_type: 'refresh',
+		refresh_token: uni.getStorageSync('refresh_token'),
+		client_type: 0,
+	}).then(res => {
+		if (res.code === 200) {
+			uni.removeStorageSync('refresh_token');
+
+			uni.setStorageSync("token", 'Bearer ' + res.data
+				.access_token);
+			uni.setStorageSync("refresh_token", res.data
+				.refresh_token);
+			//执行缓存中的请求
+			onAccessTokenFetched()
+			//延迟几秒再将刷新token的开关放开,不然偶尔还是会重复提交刷新token的请求
+			setTimeout(() => {
+				isRefreshing = true;
+			}, 3000)
+
+		}
+	})
+}
+
+class Request {
+	http(param) {
+		let url = param.url,
+			header = param.header,
+			method = param.method,
+			data = param.data,
+			dataType = 'json',
+			responseType = param.responseType || 'text'
+		if (header) {
+			header = param.header
+		} else {
+			header = {
+				'Content-type': "application/x-www-form-urlencoded",
+			};
+		}
+		const token = uni.getStorageSync('token');
+		header['token'] = header['token'] ? header['token'] : token ? token : '';
+		// 返回promise
+		return new Promise((resolve, reject) => {
+			let baseUrl
+			if (process.env.NODE_ENV === 'production') {
+				baseUrl = requestConfig.prodBaseUrl
+			} else {
+				baseUrl = requestConfig.test ? requestConfig.testBaseUrl : requestConfig.devBaseUrl
+			}
+			// 请求
+			uni.request({
+				url: baseUrl + url,
+				data: data,
+				method: method,
+				dataType: dataType,
+				responseType: responseType,
+				header: header,
+				success: (res) => {
+					if (res.statusCode && res.statusCode != 200) {
+						uni.$u.toast("api错误" + res.data.error)
+						return;
+					}
+					if (res.data.code !== 200) {
+						// code判断:200成功,不等于200错误
+						if (res.data.code == 501) {
+							if (!uni.getStorageSync('refresh_token')) {
+								webUni.webViewuni.reLaunch({
+									url: "/pages/index/login/login",
+								});
+							}
+							// 登录信息失效,静默登录
+							checkStatus(param).then(res => {
+								resolve(res)
+							})
+
+						} else {
+							uni.$u.toast(res.data.msg)
+						}
+					}
+					// 将结果抛出
+					resolve(res.data)
+				},
+				//请求失败
+				fail: (e) => {
+					uni.$u.toast('网络异常, 请检查网络连接')
+					resolve(e.data);
+				},
+				//请求完成
+				complete() {
+					resolve();
+					return;
+				},
+
+			})
+		})
+
+	}
+}
+export default new Request()

+ 36 - 0
app.config.js

@@ -0,0 +1,36 @@
+/**
+ * 网络请求相关的全局配置
+ */
+export const requestConfig = {
+	/**
+	 * 是否测试环境,该配置会覆盖开发环境的变量
+	 * 只有发布测试包的时候才修改为 true
+	 */
+	test: false,
+	/**
+	 * 开发环境请求的根路径
+	 */
+	devBaseUrl: 'http://192.168.0.16:9001/api',
+	// devBaseUrl: 'https://admin-test.sxdirectpurchase.com/api/',
+	/**
+	 * 测试环境请求的根路径
+	 */
+	// testBaseUrl: 'http://192.168.0.22:9001/api/',//中亿云
+	testBaseUrl: 'http://169.254.18.122:9001/api',//WIFI6
+
+	/**
+	 * 生产环境请求的根路径
+	 */
+	prodBaseUrl: 'https://applet.sxdirectpurchase.com/api/',
+	// applet.sxdirectpurchase.com
+	staticDir: 'https://bucket.sxdirectpurchase.com/wx',
+	/**
+	 * 请求参数相关默认配置
+	 */
+	params: {
+		/**
+		 * 企业 id
+		 */
+		companyId: 1,
+	}
+}

+ 229 - 0
components/classification.vue

@@ -0,0 +1,229 @@
+<template>
+  <!-- 分类 -->
+  <view>
+    <view class="flex" style="height: 600rpx">
+      <scroll-view scroll-y="true" class="box1" style="height: 600rpx">
+        <view
+          class="itemClass text-overflow"
+          v-for="(item, index) in provinceList"
+          :key="item.id"
+          :class="{ selectedProvice: provinceIndex == index }"
+          :style="{ color: provinceIndex == index ? '#00D36D' : '' }"
+          @click="getClassificationList2(item.id, index)"
+        >
+          {{ item.className }}
+        </view>
+      </scroll-view>
+      <scroll-view scroll-y="true" class="box2" style="height: 600rpx">
+        <view
+          class="itemClass text-overflow"
+          v-for="(item, index) in cityList"
+          :key="item.id"
+          :style="{ color: cityIndex == index ? '#00D36D' : '' }"
+          @click="getClassificationList3(item.id, index)"
+        >
+          {{ item.className }}
+        </view>
+      </scroll-view>
+      <scroll-view
+        scroll-y="true"
+        class="box3"
+        style="height: 600rpx"
+        :class="{ bl: countyList.length > 0 }"
+      >
+        <view
+          class="itemClass text-overflow"
+          v-for="(item, index) in countyList"
+          :key="item.id"
+          :style="{ color: countyIndex == index ? '#00D36D' : '' }"
+          @click="selectCounty({ className: item.className, id: item.id }, index)"
+        >
+          {{ item.className }}
+        </view>
+      </scroll-view>
+    </view>
+    <view class="class-btn d-flex-center" v-if="showBottomConfim">
+      <view class="d-flex-center" @click="reset">重置</view>
+      <view class="d-flex-center" @click="submitForm">确定</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getClassificationList, getClassCateSelf } from "@/api/home/index.js";
+export default {
+  props: {
+    showBottomConfim: {
+      type: Boolean,
+      default: false,
+    },
+    isPublish: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      provinceIndex: null,
+      cityIndex: 0,
+      countyIndex: 0,
+      provinceList: [],
+      cityList: [],
+      countyList: [],
+      className: "分类",
+      id: "",
+      selectCode: [], //三级id
+    };
+  },
+  mounted() {
+    this.getClassificationList1();
+  },
+  methods: {
+    // 获取一级分类
+    getClassificationList1() {
+      let param = {
+        className: "",
+        parentId: "",
+      };
+      if (this.isPublish) {
+        this.api.getClassCateSelf().then((res) => {
+          if (res.code == 200) {
+            this.provinceList = res.data;
+          }
+        });
+      } else {
+        this.api.getClassificationList(param).then((res) => {
+          if (res.code == 200) {
+            this.provinceList = res.data;
+          }
+        });
+      }
+    },
+    // 获取二级分类
+    getClassificationList2(id, index) {
+      this.provinceIndex = index;
+      this.cityIndex = 0;
+      this.countyIndex = 0;
+      let param = {
+        className: "",
+        parentId: id,
+      };
+      this.selectCode[0] = id;
+      this.api.getClassificationList(param).then((res) => {
+        if (res.code == 200) {
+          this.cityList = res.data;
+          this.countyList = [];
+        }
+      });
+    },
+    // 获取三级分类
+    getClassificationList3(id, index) {
+      this.cityIndex = index;
+      this.countyIndex = 0;
+      let param = {
+        className: "",
+        parentId: id,
+      };
+      this.selectCode[1] = id;
+      this.api.getClassificationList(param).then((res) => {
+        if (res.code == 200) {
+          this.countyList = res.data;
+        }
+      });
+    },
+    selectCounty(item, index) {
+      this.countyIndex = index;
+      this.className = item.className;
+      this.id = item.id;
+      this.selectCode[2] = item.id;
+      this.$emit("select", {
+        className: item.className,
+        id: item.id,
+        selectCode: this.selectCode,
+      });
+    },
+    reset() {
+      this.countyIndex = 0;
+      this.$emit("search", {
+        className: "分类",
+        id: "",
+      });
+    },
+    submitForm() {
+      this.$emit("search", {
+        className: this.className,
+        id: this.id,
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.class-btn {
+  height: 120rpx;
+  background: #ffffff;
+  font-size: 32rpx;
+  color: white;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-around;
+  & > view:nth-of-type(1) {
+    width: 334rpx;
+    height: 90rpx;
+    background: #e6faf1;
+    border-radius: 44rpx;
+    border: 2rpx solid #00d36d;
+    color: #00d36d;
+  }
+
+  & > view:nth-of-type(2) {
+    width: 334rpx;
+    height: 90rpx;
+    color: #fff;
+    background: linear-gradient(275deg, #01cf6c 0%, #07e278 100%);
+    border-radius: 44rpx;
+  }
+}
+
+.flex {
+  display: flex;
+}
+
+.box1 {
+  width: 220rpx;
+  background-color: #f5f7f7;
+}
+
+.box2 {
+  width: 220rpx;
+}
+
+.bl {
+  border-left: 1rpx solid #eeeeee;
+}
+
+.box3 {
+  width: calc(100% - 440rpx);
+}
+
+.itemClass {
+  height: 90rpx;
+  line-height: 90rpx;
+  padding-left: 20rpx;
+  padding-right: 15rpx;
+  font-size: 28rpx;
+  color: #333333;
+}
+
+.selectedProvice {
+  background-color: #ffffff;
+}
+
+/deep/::-webkit-scrollbar {
+  display: none;
+  width: 0;
+  height: 0;
+}
+</style>

+ 305 - 0
components/gjs-selectCity.vue

@@ -0,0 +1,305 @@
+<template>
+  <view class="">
+    <view class="flex">
+      <scroll-view scroll-y="true" class="box1" :style="{ height: scrollHeight + 'rpx' }">
+        <view
+          class="itemClass text-overflow"
+          v-for="(item, index) in provinceList"
+          :key="item.cityId"
+          :class="{ selectedProvice: provinceIndex == index }"
+          :style="{ color: provinceIndex == index ? selectedColor : '' }"
+          @click="getClassificationList2(item.cityId, index)"
+        >
+          {{ item.name }}
+        </view>
+      </scroll-view>
+      <scroll-view scroll-y="true" class="box2" :style="{ height: scrollHeight + 'rpx' }">
+        <view
+          class="itemClass text-overflow"
+          v-for="(item, index) in cityList"
+          :key="item.cityId"
+          :style="{ color: cityIndex == index ? selectedColor : '' }"
+          @click="getClassificationList3(item.cityId, index)"
+        >
+          {{ item.name }}
+        </view>
+      </scroll-view>
+      <scroll-view
+        scroll-y="true"
+        class="box3"
+        v-if="selectType == 'country' && !multiple"
+        :style="{ height: scrollHeight + 'rpx' }"
+        :class="{ bl: countyList.length > 0 }"
+      >
+        <view
+          class="itemClass text-overflow"
+          v-for="(item, index) in countyList"
+          :key="item.cityId"
+          :style="{ color: countyIndex == index ? selectedColor : '' }"
+          @click="selectCounty({ name: item.name, cityId: item.cityId }, index)"
+        >
+          {{ item.name }}
+        </view>
+      </scroll-view>
+      <scroll-view
+        scroll-y="true"
+        class="box3"
+        v-else
+        :style="{ height: scrollHeight + 'rpx' }"
+        :class="{ bl: countyList.length > 0 }"
+      >
+        <!-- <view
+					class="itemClass text-overflow"
+					v-for="(item, index) in countyList"
+					:key="item.cityId"
+					:style="{ color: countyIndex == index ? selectedColor : '' }"
+					@click="selectCounty({ name: item.name, cityId: item.cityId }, index)"
+				>
+					{{ item.name }}
+				</view> -->
+        <u-checkbox-group
+          v-model="selectValue"
+          iconPlacement="right"
+          placement="column"
+          @change="checkboxChange"
+        >
+          <view class="check-box-item" v-for="(item, index) in countyList" :key="index">
+            <u-checkbox
+              activeColor="#00d36d"
+              labelSize="28rpx"
+              labelColor="#333333"
+              shape="circle"
+              :label="item.name"
+              :name="item.cityId"
+            ></u-checkbox>
+          </view>
+        </u-checkbox-group>
+      </scroll-view>
+    </view>
+    <view class="pop-bottom" v-if="multiple">
+      <!-- <u-button shape="circle" text="取消" :customStyle="{ width: '300rpx' }"></u-button> -->
+      <u-button
+        color="linear-gradient( 275deg, #01CF6C 0%, #07E278 100%);"
+        shape="circle"
+        text="确定"
+        @click="submit"
+      ></u-button>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: "gjs-selectCity",
+  props: {
+    selectType: {
+      type: String,
+      default: "country", //province选择省份city选择城市country选择区县
+      validator: function (value) {
+        // 这个数组包含所有允许的值
+        const validValues = ["province", "city", "country"];
+        // 如果value存在于validValues数组中,则返回true
+        return validValues.indexOf(value) !== -1;
+      },
+    },
+    scrollHeight: {
+      type: Number,
+      default: 572,
+    },
+    selectedColor: {
+      type: String,
+      default: "#00D36D",
+    },
+    multiple: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      provinceIndex: null,
+      cityIndex: 0,
+      countyIndex: 0,
+
+      provinceList: [],
+      cityList: [],
+      countyList: [],
+      selectCode: [],
+      provenceName: "",
+      cityName: "",
+      // 新增多选
+      selectValue: [],
+    };
+  },
+  mounted() {
+    this.getClassificationList1();
+  },
+  methods: {
+    // 获取一级
+    getClassificationList1() {
+      let param = {
+        levelType: "",
+        parentCityId: "",
+      };
+      this.api.getCity(param).then((res) => {
+        if (res.code == 200) {
+          this.provinceList = res.data;
+        }
+      });
+    },
+    // 获取二级
+    getClassificationList2(id, index) {
+      this.provinceIndex = index;
+      this.cityIndex = 0;
+      this.countyIndex = 0;
+      // 选择省份后  清除区信息
+      this.countyList = [];
+      let param = {
+        levelType: 2,
+        parentCityId: id,
+      };
+      this.selectCode[0] = id;
+      this.provenceName = this.provinceList[index].name;
+      if (this.selectType == "province") {
+        this.$emit("select", {
+          name: this.provinceList[index].name,
+          cityId: id,
+        });
+        return;
+      }
+      this.api.getCity(param).then((res) => {
+        if (res.code == 200) {
+          this.cityList = res.data;
+        }
+      });
+    },
+    // 获取三级
+    getClassificationList3(id, index) {
+      // 选择城市 情况 多选
+      this.selectValue = [];
+      this.cityIndex = index;
+      this.countyIndex = 0;
+      let param = {
+        levelType: 3,
+        parentCityId: id,
+      };
+      this.selectCode[1] = id;
+      this.cityName = this.cityList[index].name;
+      if (this.selectType == "city") {
+        this.$emit("select", {
+          name: this.cityList[index].name,
+          cityId: id,
+          selectCode: this.selectCode,
+          wholeName: this.provenceName + this.cityName,
+        });
+        return;
+      }
+      this.api.getCity(param).then((res) => {
+        if (res.code == 200) {
+          this.countyList = res.data;
+        }
+      });
+    },
+    selectCounty(item, index) {
+      this.countyIndex = index;
+      this.$emit("select", {
+        name: item.name,
+        cityId: item.cityId,
+      });
+    },
+    // 新增多选提交按钮
+    submit() {
+      // console.log(this.selectValue);
+      let _arr = this.findObjectsByIds(this.countyList, this.selectValue);
+      this.$emit("select", _arr);
+      // console.log(_arr)
+    },
+    // 多选事件
+    checkboxChange(e) {
+      // console.log(e)
+    },
+    // 筛选事件
+    findObjectsByIds(objArray, numArray) {
+      return numArray
+        .map((id) => {
+          const obj = objArray.filter((item) => item.cityId === id);
+          return obj.length > 0 ? obj[0] : null;
+        })
+        .filter((item) => item !== null);
+    },
+  },
+};
+</script>
+
+<style>
+.flex {
+  display: flex;
+}
+
+.box1 {
+  /* width: 220rpx; */
+  display: flex;
+  flex: 1;
+  background-color: #f5f7f7;
+}
+
+.box2 {
+  display: flex;
+  flex: 1;
+  /* width: 220rpx; */
+}
+
+.bl {
+  /* border-left: 1rpx solid #eeeeee; */
+}
+
+.box3 {
+  display: flex;
+  flex: 1;
+  /* width: calc(100% - 440rpx); */
+}
+
+.itemClass {
+  height: 90rpx;
+  line-height: 90rpx;
+  padding-left: 20rpx;
+  padding-right: 15rpx;
+  font-size: 28rpx;
+  color: #333333;
+}
+.text-overflow {
+  display: -webkit-box; /* 使用-webkit-box布局模型 */
+  -webkit-box-orient: vertical; /* 指定盒子内容的排列方向为垂直 */
+  -webkit-line-clamp: 1; /* 控制显示的行数,这里设置为1 */
+  overflow: hidden; /* 当内容超出容器时隐藏多余的内容 */
+  text-overflow: ellipsis; /* 使用省略号显示多余的文本内容 */
+}
+.selectedProvice {
+  background-color: #ffffff;
+}
+
+/deep/::-webkit-scrollbar {
+  display: none;
+  width: 0;
+  height: 0;
+}
+.pop-bottom {
+  margin: 24rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-around;
+}
+.check-box-item {
+  box-sizing: border-box;
+  /* padding: 10rpx 15rpx; */
+  height: 90rpx;
+  line-height: 90rpx;
+  padding-left: 20rpx;
+  padding-right: 36rpx;
+  /* display: flex;
+	flex-direction:row;
+	align-items: center;
+	justify-content: space-between; */
+}
+</style>

+ 43 - 0
index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+  <meta charset="utf-8">
+
+  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+  <meta http-equiv="Pragma" content="no-cache" />
+  <meta http-equiv="Expires" content="0" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport"
+    content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
+  <title>
+  </title>
+  <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>
+  <script type="text/javascript"
+    src="https://qianzhiy-applet.oss-rg-china-mainland.aliyuncs.com/h5/js/jweixin-1.6.0.js"></script>
+
+  <script type="text/javascript"
+    src="https://qianzhiy-applet.oss-rg-china-mainland.aliyuncs.com/h5/js/uni.webview.js"></script>
+  <!-- <script type="text/javascript" src="<%= BASE_URL %>utils/uni.webview.1.5.w5.js"></script> -->
+  <link rel="stylesheet" href="<%= BASE_URL %>static/index.css" />
+  <!-- <script>
+        window.jWeixin = window.wx;
+        delete window.wx;  
+      </script> -->
+
+</head>
+
+<body>
+  <noscript>
+    <strong>Please enable JavaScript to continue.</strong>
+  </noscript>
+  <div id="app"></div>
+  <script type="module" src="/main.js"></script>
+</body>
+<script>
+</script>
+
+</html>

+ 15 - 0
locales/en/index.ts

@@ -0,0 +1,15 @@
+import Login from './login.json';
+
+const messages = {
+  en: {
+    当前语言: 'English',
+    即时通信: 'Chat',
+    即时通信IM: 'Chat',
+    社交娱乐: 'Entertaining & Socializing',
+    腾讯云: 'Tencent Cloud',
+    使用指引: 'User Guide',
+    Login,
+  }
+}
+
+export default messages

+ 41 - 0
locales/en/login.json

@@ -0,0 +1,41 @@
+{
+  "登录": "log in",
+  "登录当前账号": "Log in with current account",
+  "切换其他账号": "Switch accounts",
+  "我已阅读并同意": "I have read and agree to",
+  "和": "and",
+  "隐私条例": "Privacy Policy",
+  "用户协议": "User Agreement",
+  "免费体验": "Free trial",
+  "体验更多Demo": "More demos",
+  "访问官网": "Official website",
+  "超10亿用户的信赖": "1+ billion users trust Tencent Cloud Instant Messenger",
+  "一分钟": "1 Minute",
+  "改2行代码,1分钟跑通demo": "Quick demo run in 1 minute with changing only 2 lines of code required",
+  "每月服务客户数超过10000家": "Over 10,000 customers per month",
+  "消息收发成功率": "Success rate of message sending & receiving and service reliability higher than 99.99%",
+  "10亿": "1 billion",
+  "每月活跃用户数超过10亿": "Over 1 billion active users per month",
+  "请输入手机号": "Enter a mobile number",
+  "请输入userID": "Please enter your userID",
+  "请输入验证码": "Enter the verification code",
+  "请输入正确的手机号": "Enter a valid mobile number",
+  "请先勾选用户协议": "Agree to the User Agreement first",
+  "获取验证码": "Send verification code",
+  "登录失败": "Login failed",
+  "登录过期": "Login expired",
+  "操作失败": "failed",
+  "验证码发送成功": "Verification code sent successfully",
+  "发送验证码失败": "Failed to send the verification code",
+  "Android": "Android",
+  "iOS": "iOS",
+  "小程序": "WeChat Mini Program",
+  "Windows": "Windows",
+  "Mac OS": "Mac OS",
+  "扫描二维码下载": "Applet scanning QR code download",
+  "微信扫码进入": " WeChat scanning QR code to enter",
+  "点击直接下载": "Click to download directly",
+  "更多客户端体验": "More client experiences",
+  "微信扫一扫,免费体验腾讯云 IM 小程序": "Scan the QR code on WeChat to experience the Tencent Cloud Chat mini program for free",
+  "或者截图至相册,切换微信扫一扫识别体验": "Or take a screenshot to the photo album to switch the wechat scanning recognition experience"
+}

+ 15 - 0
locales/index.ts

@@ -0,0 +1,15 @@
+import en from './en'
+import zhcn from './zh_cn'
+import TUILocales from '../TUIKit/locales/index'
+
+const demoLocales = {
+  ...en,
+  ...zhcn
+}
+
+const locales = {
+  en: { ...demoLocales.en, ...TUILocales.en },
+  zh_cn: { ...demoLocales.zh_cn, ...TUILocales.zh_cn }
+}
+
+export { locales, demoLocales }

+ 15 - 0
locales/zh_cn/index.ts

@@ -0,0 +1,15 @@
+import Login from './login.json';
+
+const messages = {
+  zh_cn: {
+    当前语言: '简体中文',
+    即时通信: '即时通信',
+    即时通信IM: '即时通信IM',
+    社交娱乐: '社交娱乐',
+    腾讯云: '腾讯云',
+    使用指引: '使用指引',
+    Login,
+  },
+};
+
+export default messages;

+ 41 - 0
locales/zh_cn/login.json

@@ -0,0 +1,41 @@
+{
+  "登录": "登录",
+  "登录当前账号": "登录当前账号",
+  "切换其他账号": "切换其他账号",
+  "我已阅读并同意": "我已阅读并同意",
+  "和": "和",
+  "隐私条例": "隐私条例",
+  "用户协议": "用户协议",
+  "免费体验": "免费体验",
+  "体验更多Demo": "体验更多Demo",
+  "访问官网": "访问官网",
+  "超10亿用户的信赖": "超10亿用户的信赖",
+  "一分钟": "一分钟",
+  "改2行代码,1分钟跑通demo": "改2行代码,1分钟跑通demo",
+  "每月服务客户数超过10000家": "每月服务客户数超过10000家",
+  "消息收发成功率": "消息收发成功率,服务可靠性高于99.99%",
+  "10亿": "10亿",
+  "每月活跃用户数超过10亿": "每月活跃用户数超过10亿",
+  "请输入手机号": "请输入手机号",
+  "请输入userID": "请输入userID",
+  "请输入正确的手机号": "请输入正确的手机号",
+  "请先勾选用户协议": "请先勾选用户协议",
+  "请输入验证码": "请输入验证码",
+  "获取验证码": "获取验证码",
+  "登录失败": "登录失败",
+  "登录过期": "登录过期",
+  "操作失败": "操作失败",
+  "验证码发送成功": "验证码发送成功",
+  "发送验证码失败": "验证码发送失败",
+  "Android": "Android",
+  "iOS": "iOS",
+  "小程序": "小程序",
+  "Windows": "Windows",
+  "Mac OS": "Mac OS",
+  "扫描二维码下载": "扫描二维码下载",
+  "微信扫码进入": "微信扫码进入",
+  "点击直接下载": "点击直接下载",
+  "更多客户端体验": "更多客户端体验",
+  "微信扫一扫,免费体验腾讯云 IM 小程序": "微信扫一扫,免费体验腾讯云 IM 小程序",
+  "或者截图至相册,切换微信扫一扫识别体验": "或者截图至相册,切换微信扫一扫识别体验"
+}

+ 93 - 0
loginChat.ts

@@ -0,0 +1,93 @@
+import { TUILogin } from '@tencentcloud/tui-core';
+import { TUIUserService, TUIConversationService, TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
+import { TUIGlobal } from "@tencentcloud/universal-api";
+
+export const loginChat = (loginInfo) => {
+  return TUILogin.login(loginInfo)
+    .then((res: any) => {
+      let personId = uni.getStorageSync('personId');
+      let type = uni.getStorageSync('type');
+      if (type == 'link') {
+        TUIGlobal?.reLaunch({
+          url: "/TUIKit/components/TUIChat/indexlink",
+        });
+        TUIConversationService.switchConversation(`C2C${personId}`);
+
+      } else {
+        uni?.reLaunch({
+          url: '/TUIKit/components/TUIConversation/index',
+          success: () => {
+            uni?.$emit('uikitLogin', res);
+          },
+        });
+        TUIUserService.switchUserStatus({ displayOnlineStatus: true });
+        uni?.setStorage({
+          key: 'userInfo',
+          data: JSON.stringify({
+            ...loginInfo,
+            TIMPush: undefined,
+            pushConfig: {},
+          }),
+        });
+      }
+
+      return res;
+    })
+};
+
+export const loginFromStorage = () => {
+  uni?.getStorage({
+    key: 'userInfo',
+    success: function (res) {
+      if (res.data) {
+        const loginInfo = {
+          ...JSON.parse(res.data)
+        }
+        if (uni?.$TIMPush) {
+          loginInfo.TIMPush = uni?.$TIMPush;
+          loginInfo.pushConfig = {
+            androidConfig: uni?.$TIMPushConfigs, // Android 推送配置,如不需要可传空。
+            iOSConfig: {
+              iOSBusinessID: '29064', // iOS 推送配置,如不需要可传空。
+            },
+          }
+        }
+        loginChat(loginInfo).catch(() => {
+          uni?.removeStorage({
+            key: 'userInfo',
+          });
+        })
+      }
+    },
+  });
+};
+
+export declare interface IEnterChatConfig {
+  isLoginChat: boolean;
+  conversationID: string;
+}
+
+export const openChat = (enterChatConfig: IEnterChatConfig) => {
+  const { isLoginChat = false, conversationID = '' } = enterChatConfig || {};
+  const chatPath = '/TUIKit/components/TUIChat/index';
+  const currentConversationID = TUIStore.getData(StoreName.CONV, 'currentConversationID');
+  if (!isLoginChat || !conversationID) {
+    return;
+  }
+  if (!currentConversationID) {
+    TUIConversationService.switchConversation(conversationID);
+    uni?.navigateTo({
+      url: chatPath,
+    });
+  } else if (currentConversationID !== conversationID) {
+    uni.navigateBack({
+      delta: 1,
+      success: () => {
+        TUIConversationService.switchConversation(conversationID);
+        uni?.navigateTo({
+          url: chatPath,
+        });
+      },
+    });
+  }
+};

+ 43 - 0
main.js

@@ -0,0 +1,43 @@
+/* eslint-disable no-empty */
+/* eslint-disable no-implicit-coercion */
+import App from "./App";
+import uView from 'uview-ui'
+Vue.use(uView)
+// 如此配置即可
+uni.$u.config.unit = 'rpx'
+
+import moment from 'moment'
+Vue.prototype.$moment = moment;
+
+Vue.prototype.getStaticFilePath = function (url) {
+  return Vue.prototype.staticDir + url;
+};
+
+
+import api from '@/api/index.js'
+Vue.prototype.api = api;
+
+
+// #ifndef VUE3
+import Vue from "vue";
+import VueCompositionAPI from "@vue/composition-api";
+Vue.use(VueCompositionAPI);
+import unifyPromiseVue2 from "./TUIKit/utils/unifyPromiseVue2";
+Vue.config.productionTip = false;
+App.mpType = "app";
+unifyPromiseVue2();
+const app = new Vue({
+  ...App,
+});
+app.$mount();
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from "vue";
+export function createApp() {
+  const app = createSSRApp(App);
+  return {
+    app,
+  };
+}
+// #endif

+ 175 - 0
manifest.json

@@ -0,0 +1,175 @@
+{
+    "name" : "消息",
+    "appid" : "__UNI__EC201C9",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {
+            "Push" : {},
+            "Record" : {},
+            "VideoPlayer" : {},
+            "Camera" : {}
+        },
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {
+                "capabilities" : {
+                    "entitlements" : {
+                        "com.apple.security.application-groups" : [ "" ]
+                    }
+                },
+                "dSYMs" : false
+            },
+            /* SDK配置 */
+            "sdkConfigs" : {
+                "push" : {},
+                "ad" : {}
+            }
+        },
+        "nativePlugins" : {
+            "TencentCloud-TIMPush" : {
+                "TIMPushAppGroupID" : "0",
+                "com.hihonor.push.app_id" : "0",
+                "com.vivo.push.api_key" : "0",
+                "com.vivo.push.app_id" : "0",
+                "__plugin_info__" : {
+                    "name" : "【官方】uni-app 推送插件 TencentCloud-TIMPush",
+                    "description" : "腾讯云即时通信 IM Push 插件,目前推送支持小米、华为、荣耀、OPPO、vivo、魅族、APNs、一加、realme、iQOO 和 苹果等厂商通道。",
+                    "platforms" : "Android,iOS",
+                    "url" : "https://ext.dcloud.net.cn/plugin?id=16428",
+                    "android_package_name" : "",
+                    "ios_bundle_id" : "",
+                    "isCloud" : true,
+                    "bought" : 1,
+                    "pid" : "16428",
+                    "parameters" : {
+                        "TIMPushAppGroupID" : {
+                            "des" : "iOS平台的AppGroupID",
+                            "key" : "TIMPushAppGroupID",
+                            "value" : ""
+                        },
+                        "com.hihonor.push.app_id" : {
+                            "des" : "Honor推送 app_id",
+                            "key" : "com.hihonor.push.app_id",
+                            "value" : ""
+                        },
+                        "com.vivo.push.api_key" : {
+                            "des" : "VIVO推送 api_key",
+                            "key" : "",
+                            "value" : ""
+                        },
+                        "com.vivo.push.app_id" : {
+                            "des" : "VIVO推送 app_id",
+                            "key" : "",
+                            "value" : ""
+                        }
+                    }
+                }
+            },
+            "TencentCloud-TUICallKit" : {
+                "__plugin_info__" : {
+                    "name" : "【官方】腾讯云音视频通话插件TencentCloud-TUICallKit",
+                    "description" : "TUICallKit 是腾讯云官方推出的音视频通话插件,支持 1v1 通话和群组通话,并提供“类微信\"的 UI 交互,开发者仅需三个 API 就可实现。",
+                    "platforms" : "Android,iOS",
+                    "url" : "https://ext.dcloud.net.cn/plugin?id=9035",
+                    "android_package_name" : "",
+                    "ios_bundle_id" : "",
+                    "isCloud" : true,
+                    "bought" : 1,
+                    "pid" : "9035",
+                    "parameters" : {}
+                }
+            }
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* H5 特有相关 :关闭 treeshaking 是为了规避 uni[methond]() is not a function 的问题 */
+    "h5" : {
+        "optimization" : {
+            "treeShaking" : {
+                "enable" : false
+            }
+        },
+        "devServer" : {
+            "port" : 8080,
+            "disableHostCheck" : true,
+            "proxy" : {
+                "/api" : {
+                    "target" : "http://192.168.18.122:9001/",
+                    // "target": "https://admin-test.sxdirectpurchase.com",
+                    "changeOrigin" : true,
+                    "secure" : false,
+                    "pathRewrite" : {
+                        "^/api" : "/api"
+                    }
+                }
+            },
+            "https" : false
+        },
+        "template" : "index.html",
+        "router" : {
+            "base" : "/message"
+        }
+    },
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "libVersion" : "latest",
+        "appid" : "",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : true,
+            "minified" : true,
+            "ignoreDevUnusedFiles" : false,
+            "ignoreUploadUnusedFiles" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "2"
+}

File diff suppressed because it is too large
+ 36748 - 0
package-lock.json


+ 56 - 0
package.json

@@ -0,0 +1,56 @@
+{
+  "name": "sample-uniapp",
+  "version": "2.2.4",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "@tencentcloud/call-uikit-vue": "latest",
+    "@tencentcloud/call-uikit-vue2.6": "latest",
+    "@tencentcloud/call-uikit-wechat": "latest",
+    "@tencentcloud/chat": "latest",
+    "@tencentcloud/chat-uikit-engine": "latest",
+    "@tencentcloud/tui-core": "latest",
+    "@tencentcloud/tui-customer-service-plugin": "^2.2.3",
+    "@tencentcloud/universal-api": "latest",
+    "@vue/composition-api": "^1.7.1",
+    "dayjs": "^1.11.10",
+    "lodash": "^4.17.21",
+    "moment": "^2.30.1",
+    "tim-profanity-filter-plugin": "latest",
+    "tim-upload-plugin": "latest",
+    "unplugin-vue2-script-setup": "^0.11.3",
+    "uview-ui": "^2.0.36"
+  },
+  "devDependencies": {
+    "@babel/plugin-transform-runtime": "^7.17.0",
+    "@babel/runtime-corejs3": "^7.17.2",
+    "@stylistic/eslint-plugin": "^1.6.2",
+    "@types/lodash": "^4.14.202",
+    "@types/uni-app": "^1.4.4",
+    "@typescript-eslint/eslint-plugin": "^7.0.1",
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-typescript": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/composition-api": "^1.7.1",
+    "@vue/eslint-config-typescript": "^12.0.0",
+    "eslint": "^8.56.0",
+    "eslint-plugin-vue": "^9.21.1",
+    "postcss-html": "^1.6.0",
+    "sass": "^1.26.5",
+    "sass-loader": "^8.0.2",
+    "stylelint": "^16.2.1",
+    "stylelint-config-standard": "^36.0.0",
+    "stylelint-config-standard-scss": "^13.0.0",
+    "stylelint-config-standard-vue": "^1.0.0",
+    "typescript": "^4.9.5",
+    "unplugin-vue2-script-setup": "^0.11.3",
+    "vue-tsc": "^1.2.0"
+  }
+}

+ 149 - 0
pages.json

@@ -0,0 +1,149 @@
+{
+  "pages": [
+    {
+      "path": "pages/views/login",
+      "style": {}
+    },
+    {
+      "path": "TUIKit/components/TUIConversation/index",
+      "style": {
+        "navigationBarTitleText": "消息",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "TUIKit/components/TUIChat/index",
+      "style": {
+        "navigationBarTitleText": "",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "TUIKit/components/TUIChat/indexlink",
+      "style": {
+        "navigationBarTitleText": "",
+        "autoBackButton": true,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "TUIKit/components/TUIContact/index",
+      "style": {
+        "navigationBarTitleText": "消息",
+        "navigationStyle": "custom",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/views/profile",
+      "style": {
+        "navigationBarTitleText": "消息",
+        "navigationStyle": "custom",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "TUIKit/components/TUIChat/video-play",
+      "style": {
+        "navigationBarTitleText": "消息",
+        "navigationStyle": "custom",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "TUIKit/components/TUIChat/web-view",
+      "style": {
+        "navigationBarTitleText": "消息",
+        "navigationStyle": "custom",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "TUIKit/components/TUIGroup/index",
+      "style": {
+        "navigationBarTitleText": "消息",
+        "navigationStyle": "custom",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "TUIKit/components/TUISearch/index",
+      "style": {
+        "navigationBarTitleText": "聊天记录",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/group/index",
+      "style": {
+        "navigationBarTitleText": "资源聚合群",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/group/data",
+      "style": {
+        "navigationBarTitleText": "群资料",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/group/scan",
+      "style": {
+        "navigationBarTitleText": "扫码进群",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/group/person",
+      "style": {
+        "navigationBarTitleText": "新的联系人",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/group/chat",
+      "style": {
+        "navigationBarTitleText": "我的群聊",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/group/black-list",
+      "style": {
+        "navigationBarTitleText": "黑名单",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/group/add",
+      "style": {
+        "navigationBarTitleText": "添加好友",
+        "autoBackButton": true
+      }
+    },
+    {
+      "path": "pages/message/serviceNotice",
+      "style": {
+        "navigationBarTitleText": "服务通知",
+        "autoBackButton": true,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/message/interactive",
+      "style": {
+        "navigationBarTitleText": "互动消息",
+        "autoBackButton": true,
+        "navigationStyle": "custom"
+      }
+    }
+  ],
+  "globalStyle": {
+    "navigationBarTextStyle": "black",
+    "navigationBarTitleText": "",
+    "navigationBarBackgroundColor": "#F8F8F8",
+    "backgroundColor": "#F8F8F8"
+  },
+  "uniIdRouter": {}
+}

+ 171 - 0
pages/group/add.vue

@@ -0,0 +1,171 @@
+<template>
+  <div style="padding-top: 20px">
+    <u--input
+      placeholder="请输入"
+      prefixIcon="search"
+      prefixIconStyle="font-size: 22px;color: #909399"
+      border="surround"
+      v-model="phone"
+    >
+      <template slot="suffix">
+        <u-button
+          @click="query"
+          text="搜索"
+          style="
+            height: 30px;
+            border-radius: 20px;
+            background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%);
+            color: white;
+          "
+        ></u-button>
+      </template>
+    </u--input>
+    <div v-if="userInfo">
+      <view class="main">
+        <view class="item">
+          <view class="left">
+            <img :src="userInfo.headUrl" alt="" class="img" />
+          </view>
+          <view class="right">
+            <view class="right_contont">
+              <view style="width: 80%">
+                <view class="title">{{ userInfo.nick }}</view>
+              </view>
+              <view @click="addUser(userInfo)">
+                <u-button
+                  type="primary"
+                  text="添加"
+                  color="linear-gradient( 275deg, #01CF6C 0%, #07E278 100%)"
+                  shape="circle"
+                  style="height: 30px"
+                ></u-button>
+              </view>
+            </view>
+            <view class="remake"> {{ userInfo.introduction }} </view>
+          </view>
+        </view>
+      </view>
+    </div>
+  </div>
+</template>
+
+<script>
+import TUIChatEngine, {
+  TUIFriendService,
+  TUIConversationService,
+  TUIGroupService,
+  TUIUserService,
+  TUITranslateService,
+  AddFriendParams,
+  JoinGroupParams,
+} from "@tencentcloud/chat-uikit-engine";
+import { Toast, TOAST_TYPE } from "../../TUIKit/components/common/Toast/index";
+export default {
+  data() {
+    return {
+      phone: "",
+      userInfo: "",
+    };
+  },
+  methods: {
+    query() {
+      this.api.getUserInfoByPhone(this.phone).then((res) => {
+        if (res.code === 200) {
+          console.log(res);
+          this.userInfo = res.data;
+        }
+      });
+    },
+    addUser(record) {
+      let params = {
+        remark: "",
+        source: "AddSource_Type_Web",
+        to: JSON.stringify(record.id),
+        wording: "",
+      };
+      TUIFriendService.addFriend(params)
+        .then(() => {
+          Toast({
+            message: TUITranslateService.t("TUIContact.申请已发送"),
+            type: TOAST_TYPE.SUCCESS,
+          });
+        })
+        .catch((error) => {
+          console.log(error);
+          Toast({
+            message: TUITranslateService.t("TUIContact.申请发送失败"),
+            type: TOAST_TYPE.ERROR,
+          });
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scope>
+::v-deep .u-input {
+  border: 1px solid #00d36e;
+  border-radius: 20px;
+}
+.main {
+  .item {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    padding: 10px 20px;
+    background: white;
+    .left {
+      width: 20%;
+      margin-right: 10px;
+      .img {
+        width: 110rpx;
+        height: 110rpx;
+        border-radius: 50%;
+      }
+    }
+    .right {
+      width: 80%;
+      line-height: 25px;
+      .remake {
+        font-weight: 400;
+        font-size: 28rpx;
+        color: #999999;
+      }
+      .right_contont {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        .gys {
+          width: 168rpx;
+          height: 28rpx;
+          font-weight: 500;
+          font-size: 24rpx;
+          color: #ff8b2f;
+          line-height: 28rpx;
+          font-style: normal;
+        }
+        .cgs {
+          width: 168rpx;
+          height: 28rpx;
+          font-weight: 500;
+          font-size: 24rpx;
+          color: #00d36d;
+          line-height: 28rpx;
+          font-style: normal;
+        }
+      }
+      .value {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        height: 60rpx;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 32rpx;
+        color: #333333;
+      }
+    }
+  }
+}
+</style>

+ 125 - 0
pages/group/black-list.vue

@@ -0,0 +1,125 @@
+<template>
+  <view>
+    <template v-if="!isShowContactInfo">
+      <view
+        class="item"
+        v-for="(item, index) in contactListMap.blackList.list"
+        :key="index"
+        @click="selectItem(item)"
+      >
+        <view>
+          <image :src="item.avatar" class="img" />
+        </view>
+        <view class="right">
+          <text class="name">{{ item.nick }}</text>
+        </view>
+      </view>
+    </template>
+    <view v-if="isShowContactInfo">
+      <view class="item">
+        <view>
+          <image :src="detailObj.avatar" class="img" />
+        </view>
+        <view class="right">
+          <view class="name">{{ detailObj.nick }}</view>
+          <view class="id">ID:{{ detailObj.userID }}</view>
+        </view>
+      </view>
+      <view
+        style="
+          margin-top: 20px;
+          display: flex;
+          flex-direction: row;
+          align-items: center;
+          justify-content: space-between;
+          padding: 0px 20px;
+        "
+      >
+        加入黑名单<u-switch v-model="switchValue" @change="change" size="60"></u-switch>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {
+  TUITranslateService,
+  TUIStore,
+  StoreName,
+  IGroupModel,
+  TUIFriendService,
+  Friend,
+  FriendApplication,
+  TUIUserService,
+  TUIConversationService,
+} from "@tencentcloud/chat-uikit-engine";
+import ContactInfo from "../../TUIKit/components/TUIContact/contact-info/index.vue";
+import { isPC, isUniFrameWork } from "../../TUIKit/utils/env";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import {
+  IContactList,
+  IContactSearchResult,
+  IBlackListUserItem,
+  IUserStatus,
+  IUserStatusMap,
+  IContactInfoType,
+} from "../../TUIKit/interface";
+import { TUIGlobal } from "@tencentcloud/universal-api";
+import { Toast, TOAST_TYPE } from "../../TUIKit/components/common/Toast/index";
+import { ref, computed, onMounted, onUnmounted, provide } from "../../TUIKit/adapter-vue";
+const currentContactInfo = ref();
+const contactListMap = ref();
+const isShowContactInfo = ref(false);
+const switchValue = ref(true);
+onLoad((options) => {
+  contactListMap.value = JSON.parse(options.list);
+  console.log(contactListMap.value);
+});
+
+const change = () => {
+  TUIUserService.removeFromBlacklist({
+    userIDList: [detailObj.value.userID],
+  })
+    .then(() => {
+      TUIGlobal?.navigateTo({
+        url: "/TUIKit/components/TUIContact/index",
+      });
+      isShowContactInfo.value = !isShowContactInfo.value;
+    })
+    .catch((error: any) => {
+      Toast({
+        message: TUITranslateService.t("TUIContact.移除黑名单失败"),
+        type: TOAST_TYPE.ERROR,
+      });
+    });
+};
+const detailObj = ref();
+const selectItem = (record) => {
+  isShowContactInfo.value = !isShowContactInfo.value;
+  detailObj.value = record;
+};
+</script>
+
+<style lang="scss" scope>
+.item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 20rpx;
+  border-bottom: 1px solid #f1f1f1;
+  .right {
+    margin-left: 10px;
+    .name {
+      font-weight: 600;
+      font-size: 32rpx;
+      color: #333333;
+    }
+  }
+
+  .img {
+    width: 60px;
+    height: 60px;
+    border-radius: 50%;
+  }
+}
+</style>

+ 84 - 0
pages/group/chat.vue

@@ -0,0 +1,84 @@
+<template>
+  <view>
+    <template>
+      <view
+        class="item"
+        v-for="(item, index) in contactListMap.groupList.list"
+        :key="index"
+        @click="selectItem(item)"
+      >
+        <view>
+          <image :src="item.avatar" class="img" />
+        </view>
+        <view class="right">
+          <text class="name">{{ item.name }}</text>
+        </view>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script setup lang="ts">
+import {
+  TUITranslateService,
+  TUIStore,
+  StoreName,
+  IGroupModel,
+  TUIFriendService,
+  Friend,
+  FriendApplication,
+  TUIUserService,
+  TUIConversationService,
+} from "@tencentcloud/chat-uikit-engine";
+import ContactInfo from "../../TUIKit/components/TUIContact/contact-info/index.vue";
+import { isPC, isUniFrameWork } from "../../TUIKit/utils/env";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import {
+  IContactList,
+  IContactSearchResult,
+  IBlackListUserItem,
+  IUserStatus,
+  IUserStatusMap,
+  IContactInfoType,
+} from "../../TUIKit/interface";
+import { TUIGlobal } from "@tencentcloud/universal-api";
+import { ref, computed, onMounted, onUnmounted, provide } from "../../TUIKit/adapter-vue";
+const currentContactInfo = ref();
+const contactListMap = ref();
+const isShowContactInfo = ref(false);
+onLoad((options) => {
+  contactListMap.value = JSON.parse(options.list);
+});
+const emits = defineEmits(["switchConversation"]);
+function selectItem(item: any) {
+  isUniFrameWork &&
+    TUIGlobal?.navigateTo({
+      url: "/TUIKit/components/TUIChat/index",
+    });
+  TUIConversationService.switchConversation(`GROUP${item.groupID}`);
+}
+</script>
+
+<style lang="scss" scope>
+.item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 20rpx;
+  border-bottom: 1px solid #f1f1f1;
+  .right {
+    margin-left: 10px;
+    .name {
+      font-weight: 600;
+      font-size: 32rpx;
+      color: #333333;
+    }
+  }
+
+  .img {
+    width: 60px;
+    height: 60px;
+    border-radius: 50%;
+  }
+}
+</style>

+ 287 - 0
pages/group/data.vue

@@ -0,0 +1,287 @@
+<template>
+  <view class="supply-hall">
+    <u-navbar :autoBack="true" bgColor="transparent"> 群资料 </u-navbar>
+    <view class="supply-hall-header">
+      <view :style="{ height: statusBarHeight + 'px' }"></view>
+    </view>
+
+    <view>
+      <view class="main">
+        <view class="item">
+          <view class="left">
+            <img :src="info.faceUrl" alt="" class="img" />
+          </view>
+          <view class="right">
+            <view class="right_contont">
+              <view style="width: 80%">
+                <view class="title">{{ info.name ? info.name : "" }}</view>
+                <view class="value">群号: {{ info.groupId ? info.groupId : "" }}</view>
+              </view>
+              <view class="right_button">
+                <image
+                  :src="getStaticFilePath('/static/imImges/ewm.png')"
+                  style="width: 40rpx; height: 40rpx"
+                />
+              </view>
+            </view>
+            <view class="remake"> {{ info.introduction ? info.introduction : "" }} </view>
+          </view>
+        </view>
+      </view>
+      <view class="contont">
+        <view class="header">
+          <text>成员概况</text>
+          <text>共 {{ info.dealerCount + info.nowSupplierCounts }}人</text>
+        </view>
+        <view class="schedule">
+          <view>
+            <view class="header title">
+              <text> 供应商人数</text>
+              <text>剩{{ info.supplierCount - info.nowSupplierCounts }}人满员</text>
+            </view>
+            <view>
+              <u-line-progress
+                :percentage="info.nowSupplierCounts"
+                activeColor="#00C868"
+              ></u-line-progress>
+            </view>
+          </view>
+          <view>
+            <view class="header title">
+              <text>司机人数</text>
+              <text>剩10人满员</text>
+            </view>
+            <view>
+              <u-line-progress :percentage="30" activeColor="#FFE000"></u-line-progress>
+            </view>
+          </view>
+          <view>
+            <view class="header title">
+              <text> 采购商人数</text>
+              <text>剩{{ info.purchaserCount - info.nowPurchaserCounts }}人满员</text>
+            </view>
+            <view>
+              <u-line-progress
+                :percentage="info.nowPurchaserCounts"
+                activeColor="#00EBFF"
+              ></u-line-progress>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="xian"> </view>
+      <view class="announcement">
+        <view class="header">
+          <text>群公告</text>
+        </view>
+        <view style="padding: 20px">
+          {{ info.introduction ? info.introduction : "" }}
+        </view>
+      </view>
+    </view>
+    <view class="popup">
+      <u-button
+        type="primary"
+        :plain="true"
+        :text="`${info.price}元加入群聊`"
+        @click="addGroup"
+      ></u-button>
+    </view>
+  </view>
+</template>
+
+<script>
+import gjsSelectCity from "@/components/gjs-selectCity.vue";
+import classification from "@/components/classification.vue";
+
+import * as MsgApi from "@/api/message/index.js";
+export default {
+  components: {
+    gjsSelectCity,
+    classification,
+  },
+  data() {
+    return {
+      show: false,
+      popupShow: false,
+      statusBarHeight: 0,
+      bottomStatusHeight: 0,
+      groupInfo: {},
+      info: {},
+    };
+  },
+  mounted() {
+    // #ifndef H5 || APP-PLUS || MP-ALIPAY
+    const { windowHeight, screenHeight, safeArea, statusBarHeight } = uni.$u.sys(); // 获取页面高度
+    let menuButtonObject = uni.getMenuButtonBoundingClientRect();
+    let navHeight =
+      menuButtonObject.height + (menuButtonObject.top - statusBarHeight) * 2;
+    this.statusBarHeight = navHeight + statusBarHeight + 4;
+    let tabBarHeight = windowHeight - safeArea.bottom;
+    this.bottomStatusHeight = screenHeight - this.statusBarHeight - tabBarHeight;
+    // #endif
+  },
+  onLoad(option) {
+    console.log(option);
+    this.groupInfo = JSON.parse(option.info);
+    this.getGroupInfo();
+  },
+  computed: {},
+  onReady() {},
+  methods: {
+    async getGroupInfo() {
+      let params = {
+        groupId: this.groupInfo.groupId,
+        userId: uni.getStorageSync("userid"),
+      };
+
+      const res = await MsgApi.default.getGroupByGroupId(params);
+      console.log("1231231", res);
+      if (res.code == 200) {
+        this.info = res.data;
+      }
+    },
+    addGroup() {
+      let userid = uni.getStorageSync("userId");
+      let openid = uni.getStorageSync("openid");
+      if (this.groupInfo.price == 0) {
+        let params = {
+          userId: userid,
+          ragId: this.groupInfo.id,
+          ragStatus: 0,
+          userType: 1,
+        };
+        this.api.addRagUser(params).then((res) => {
+          if (res.code === 200) {
+            if (res.msg == "该用户已加入过该群组") {
+            }
+          }
+        });
+      } else {
+        let params = {
+          body: this.groupInfo.introduction,
+          attach: 1,
+          oriMon: this.groupInfo.price,
+          payKinds: 0,
+          payMeth: 2,
+          ragId: this.groupInfo.id,
+          openId: openid,
+        };
+        this.api.addImGroupOrder(params).then((res) => {
+          if (res.code === 200) {
+            this.show = false;
+            webUni.webView.navigateTo({
+              url:
+                `/subpages/message/pay/index?info=` + encodeURIComponent(res.data.result),
+            });
+          }
+        });
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.supply-hall {
+  background: #f4f4f4;
+  height: 100%;
+}
+.xian {
+  background: #f4f4f4;
+  height: 10px;
+  width: 100%;
+}
+.main {
+  border-bottom: 1px solid #f4f4f4;
+  background: white;
+  .item {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    padding: 10px 20px;
+    background: white;
+    .left {
+      width: 20%;
+      margin-right: 10px;
+      .img {
+        width: 110rpx;
+        height: 110rpx;
+        border-radius: 50%;
+      }
+    }
+    .right {
+      width: 80%;
+      line-height: 25px;
+      .remake {
+        font-weight: 400;
+        font-size: 28rpx;
+        color: #999999;
+      }
+      .right_contont {
+        display: flex;
+        flex-direction: row;
+        align-items: flex-start;
+        justify-content: space-between;
+      }
+      .value {
+        font-weight: 500;
+        font-size: 28rpx;
+        color: #999999;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 32rpx;
+        color: #333333;
+      }
+    }
+  }
+}
+.announcement {
+  padding: 10px;
+  height: 350rpx;
+  background: white;
+  .header {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+  }
+}
+.contont {
+  padding: 10px;
+  height: 350rpx;
+  background: white;
+  .header {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+  }
+  .schedule {
+    height: 100px;
+    width: 100%;
+    line-height: 40px;
+    .title {
+      font-weight: 500;
+      font-size: 24rpx;
+      color: #999999;
+    }
+  }
+}
+.popup {
+  position: fixed;
+  width: 96%;
+  bottom: 20px;
+  left: 2%;
+}
+.popup ::v-deep .u-button--primary.data-v-2bf0e569 {
+  width: 100% !important;
+  height: 38px !important;
+  background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%) !important;
+  border: none !important;
+  border-radius: 29rpx !important;
+  font-size: 28rpx !important;
+  font-weight: bold !important;
+  margin-top: 30px;
+  color: white;
+}
+</style>

+ 509 - 0
pages/group/index.vue

@@ -0,0 +1,509 @@
+<template>
+  <view class="supply-hall">
+    <z-paging ref="paging" v-model="dataList" @query="queryList">
+      <template slot="top">
+        <u-navbar :autoBack="true" bgColor="transparent">
+          <view
+            slot="center"
+            class="supply-hall-search"
+            style="width: 530rpx; display: flex; justify-content: flex-start"
+          >
+            <view class="" style="width: 400rpx">
+              <u-input
+                class="header-search"
+                placeholder="分类,地区,群名称"
+                prefixIcon="search"
+                prefixIconStyle="font-size: 22px;color: #909399"
+              >
+                <template slot="suffix">
+                  <u-button text="搜索" type="primary"></u-button>
+                </template>
+              </u-input>
+            </view>
+          </view>
+        </u-navbar>
+        <view class="supply-hall-header">
+          <view :style="{ height: statusBarHeight + 'px' }"></view>
+        </view>
+        <view class="supply-hall-screen">
+          <view class="supply-hall-screen-item" @click="screenClick(1)">
+            <view class="">
+              <u--text
+                :text="screenData.className"
+                size="28rpx"
+                :color="screenIndex == 1 ? '#00D36D' : '#666666'"
+              ></u--text>
+            </view>
+            <view class="ml6">
+              <u-icon
+                :name="screenIndex == 1 ? 'arrow-up-fill' : 'arrow-down-fill'"
+                :color="screenIndex == 1 ? '#00D36D' : '#b7b7b7'"
+                size="20rpx"
+              ></u-icon>
+            </view>
+          </view>
+          <view class="supply-hall-screen-item" @click="screenClick(2)">
+            <view class="">
+              <u--text
+                :text="screenData.addressName"
+                size="28rpx"
+                :color="screenIndex == 2 ? '#00D36D' : '#666666'"
+              ></u--text>
+            </view>
+            <view class="ml6">
+              <u-icon
+                :name="screenIndex == 2 ? 'arrow-up-fill' : 'arrow-down-fill'"
+                :color="screenIndex == 2 ? '#00D36D' : '#b7b7b7'"
+                size="20rpx"
+              ></u-icon>
+            </view>
+          </view>
+
+          <view
+            class=""
+            v-show="screenStatus"
+            style="
+              background-color: #ffffff;
+              position: absolute;
+              top: 70rpx;
+              z-index: 997;
+              width: 100%;
+            "
+          >
+            <view class="" v-show="screenIndex == 1">
+              <classification @select="selectClass"></classification>
+            </view>
+            <view class="" v-show="screenIndex == 2">
+              <gjsSelectCity @select="selectAddress"></gjsSelectCity>
+            </view>
+          </view>
+        </view>
+      </template>
+      <view class="main">
+        <view
+          class="item"
+          v-for="(item, index) in dataList"
+          :key="index"
+          @click="grouoDetail(item)"
+        >
+          <view class="left">
+            <img :src="item.faceUrl" alt="" class="img" />
+          </view>
+          <view class="right">
+            <view class="right_contont">
+              <view style="width: 80%">
+                <view class="title">{{ item.name }}</view>
+                <view class="value">
+                  <view class="gys"> 供应商:{{ item.supplierCount }}人 </view>
+                  <view class="cgs"> 采购商:{{ item.purchaserCount }} 人 </view>
+                </view>
+              </view>
+              <view @click.stop="join(item)">
+                <u-button
+                  type="primary"
+                  text="加入"
+                  color="linear-gradient( 275deg, #01CF6C 0%, #07E278 100%)"
+                  shape="circle"
+                ></u-button>
+              </view>
+            </view>
+            <view class="remake"> {{ item.introduction }} </view>
+          </view>
+        </view>
+      </view>
+    </z-paging>
+
+    <u-popup :show="show" @close="show = false" @open="open">
+      <view style="padding: 15px">
+        <view style="text-align: right; position: absolute; right: 20rpx">
+          <image
+            src="https://directpurchase-oss-dev.oss-cn-chengdu.aliyuncs.com/wx/static/imImges/close.png"
+            style="width: 40rpx; height: 40rpx"
+            @click="show = false"
+          />
+        </view>
+        <view class="main">
+          <view class="item">
+            <view class="left">
+              <img :src="itemData.faceUrl" alt="" class="img" />
+            </view>
+            <view class="right">
+              <view class="right_contont">
+                <view style="width: 80%">
+                  <view class="title">{{ itemData.name }}</view>
+                </view>
+              </view>
+
+              <view class="remake"> 群ID:{{ itemData.id }} </view>
+              <view class="remake"> {{ itemData.introduction }} </view>
+            </view>
+          </view>
+          <view class="imgs">
+            <view class="item_img">
+              <image
+                src="https://directpurchase-oss-dev.oss-cn-chengdu.aliyuncs.com/wx/static/imImges/yzkh.png"
+                class="icon"
+              />
+              <view> 优质客户 </view>
+            </view>
+            <view class="item_img">
+              <image
+                src="https://directpurchase-oss-dev.oss-cn-chengdu.aliyuncs.com/wx/static/imImges/xxgx.png"
+                class="icon"
+              />
+              <view> 信息共享 </view>
+            </view>
+            <view class="item_img">
+              <image
+                src="https://directpurchase-oss-dev.oss-cn-chengdu.aliyuncs.com/wx/static/imImges/qsmh.png"
+                class="icon"
+              />
+              <view> 轻松卖货 </view>
+            </view>
+          </view>
+        </view>
+        <view class="popup" style="margin-top: 40rpx">
+          <u-button
+            type="primary"
+            :plain="true"
+            :text="`${itemData.price}元加入群聊`"
+            :customStyle="customStyle"
+            @click="addGroup"
+          ></u-button>
+        </view>
+      </view>
+    </u-popup>
+  </view>
+</template>
+
+<script>
+import gjsSelectCity from "@/components/gjs-selectCity.vue";
+import classification from "@/components/classification.vue";
+export default {
+  components: {
+    gjsSelectCity,
+    classification,
+  },
+  data() {
+    return {
+      show: false,
+      popupShow: false,
+      statusBarHeight: 0,
+      bottomStatusHeight: 0,
+      pullDownStatus: "loading",
+      screenData: {
+        className: "分类",
+        classId: "",
+        addressName: "发货地",
+        addressId: "",
+      },
+
+      queryParams: {
+        pageNumber: 1,
+        pageSize: 5,
+        area: "",
+        category: "",
+      },
+      screenIndex: 0,
+      screenStatus: false,
+      classList: [],
+      dataList: [],
+      itemData: {},
+    };
+  },
+  mounted() {
+    // #ifndef H5 || APP-PLUS || MP-ALIPAY
+    const { windowHeight, screenHeight, safeArea, statusBarHeight } = uni.$u.sys(); // 获取页面高度
+    let menuButtonObject = uni.getMenuButtonBoundingClientRect();
+    let navHeight =
+      menuButtonObject.height + (menuButtonObject.top - statusBarHeight) * 2;
+    this.statusBarHeight = navHeight + statusBarHeight + 4;
+    let tabBarHeight = windowHeight - safeArea.bottom;
+    this.bottomStatusHeight = screenHeight - this.statusBarHeight - tabBarHeight;
+    // #endif
+  },
+  onLoad(option) {
+    console.log("option", option);
+  },
+
+  onReady() {},
+  methods: {
+    setData(aaa) {
+      console.log("12312321", aaa);
+    },
+    addGroup() {
+      let userid = uni.getStorageSync("userId");
+      let openid = uni.getStorageSync("openid");
+      if (this.itemData.price == 0) {
+        let params = {
+          userId: userid,
+          ragId: this.itemData.id,
+          ragStatus: 0,
+          userType: 1,
+        };
+        this.api.addRagUser(params).then((res) => {
+          if (res.code === 200) {
+            if (res.msg == "该用户已加入过该群组") {
+            }
+          }
+        });
+      } else {
+        let params = {
+          body: this.itemData.introduction,
+          attach: 1,
+          oriMon: this.itemData.price,
+          payKinds: 0,
+          payMeth: 2,
+          ragId: this.itemData.id,
+          openId: openid,
+        };
+        this.api.addImGroupOrder(params).then((res) => {
+          if (res.code === 200) {
+            this.show = false;
+            webUni.webView.navigateTo({
+              url:
+                `/subpages/message/pay/index?info=` + encodeURIComponent(res.data.result),
+            });
+          }
+        });
+      }
+    },
+
+    join(record) {
+      this.itemData = record;
+      if (record.isAddGroupStatus == 1) {
+        //已加群
+      } else {
+        this.show = true;
+      }
+    },
+    queryList(pageNumber, pageSize) {
+      this.queryParams.pageNumber = pageNumber;
+      this.queryParams.pageSize = pageSize;
+      let dataList = [];
+      this.$refs.paging.complete(dataList);
+      this.api
+        .getAllGroups(this.queryParams)
+        .then((res) => {
+          if (res.code === 200) {
+            console.log("123213", res);
+
+            this.$refs.paging.complete(res.data.content);
+          }
+        })
+        .catch((res) => {
+          this.$refs.paging.complete(false);
+        });
+    },
+    grouoDetail(record) {
+      uni.navigateTo({
+        url: "/pages/group/data?info=" + JSON.stringify(record),
+      });
+    },
+    // 筛选点击事件
+    screenClick(i) {
+      this.screenIndex = i;
+      if (this.screenStatus) {
+        this.screenIndex = "";
+        this.screenStatus = false;
+      } else {
+        this.screenStatus = true;
+      }
+    },
+    // 发货地返回值
+    selectAddress(i) {
+      this.screenData.addressName = i.name;
+      this.screenData.addressId = i.cityId;
+      this.screenStatus = false;
+      this.ajax.page = 1;
+      this.ajax.load = true;
+      this.getList();
+    },
+    // 分类返回值
+    selectClass(i) {
+      this.screenData.className = i.className;
+      this.screenData.classId = i.id;
+      this.screenStatus = false;
+      this.ajax.page = 1;
+      this.ajax.load = true;
+      this.getList();
+    },
+
+    // 筛选清空事件
+    screenReset() {
+      this.screenData.lowestPrice = "";
+      this.screenData.maxPrice = "";
+      this.screenData.minPurchase = "";
+      this.screenData.maxPurchase = "";
+    },
+    // 筛选确定
+    screenOk() {
+      this.ajax.page = 1;
+      this.ajax.load = true;
+      this.getList();
+      this.screenStatus = false;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.imgs {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+  align-items: center;
+  margin-top: 10px;
+  .item_img {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 30%;
+    .icon {
+      width: 120rpx;
+      height: 120rpx;
+    }
+  }
+}
+
+.popup ::v-deep .u-button {
+  width: 100% !important;
+  height: 38px !important;
+  background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%) !important;
+  border: none !important;
+  border-radius: 29rpx !important;
+  font-size: 28rpx !important;
+  font-weight: bold !important;
+  margin-top: 30px;
+  color: white;
+}
+.main {
+  .item {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    padding: 10px 20px;
+    background: white;
+    .left {
+      width: 20%;
+      margin-right: 10px;
+      .img {
+        width: 110rpx;
+        height: 110rpx;
+        border-radius: 50%;
+      }
+    }
+    .right {
+      width: 80%;
+      line-height: 25px;
+      .remake {
+        font-weight: 400;
+        font-size: 28rpx;
+        color: #999999;
+      }
+      .right_contont {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        .gys {
+          width: 168rpx;
+          height: 28rpx;
+          font-weight: 500;
+          font-size: 24rpx;
+          color: #ff8b2f;
+          line-height: 28rpx;
+          font-style: normal;
+        }
+        .cgs {
+          width: 168rpx;
+          height: 28rpx;
+          font-weight: 500;
+          font-size: 24rpx;
+          color: #00d36d;
+          line-height: 28rpx;
+          font-style: normal;
+        }
+      }
+      .value {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        height: 60rpx;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 32rpx;
+        color: #333333;
+      }
+    }
+  }
+}
+.supply-hall {
+  width: 750rpx;
+  height: 100vh;
+  background-color: white;
+  overflow: hidden;
+
+  &-tabs {
+    height: 70rpx;
+    background-color: #ffffff;
+    border-radius: 0rpx 0rpx 28rpx 28rpx;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0rpx 24rpx;
+
+    &-item {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  &-screen {
+    height: 70rpx;
+    background-color: #ffffff;
+    // border-radius: 0rpx 0rpx 28rpx 28rpx;
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    position: relative;
+
+    &-item {
+      display: flex;
+      align-items: center;
+    }
+  }
+}
+
+.ml6 {
+  margin-left: 6rpx;
+}
+
+::v-deep .supply-hall-search .u-input.data-v-113bc24f {
+  background-color: #fff !important;
+  box-sizing: border-box !important;
+  border-radius: 33rpx !important;
+  height: 64rpx !important;
+  padding-right: 2rpx !important;
+}
+
+::v-deep .popup-item .u-input.data-v-113bc24f {
+  background-color: #fff !important;
+  box-sizing: border-box !important;
+  border-radius: 33rpx !important;
+  height: 64rpx !important;
+  padding-right: 2rpx !important;
+  width: 200rpx !important;
+}
+
+::v-deep .u-button--primary.data-v-2bf0e569 {
+  width: 110rpx !important;
+  height: 56rpx !important;
+  background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%) !important;
+  border: none !important;
+  border-radius: 29rpx !important;
+  font-size: 28rpx !important;
+  font-weight: bold !important;
+}
+</style>

+ 0 - 0
pages/group/person.vue


+ 0 - 0
pages/group/scan.vue


+ 217 - 0
pages/message/interactive.vue

@@ -0,0 +1,217 @@
+<template>
+  <view class="supply-hall">
+    <z-paging ref="paging" v-model="dataList" @query="queryList">
+      <template slot="top">
+        <view style="width: 100%; height: 50px; border-bottom: 1px solid #f1f1f1">
+          <u-navbar
+            @leftClick="leftClick"
+            title="服务通知"
+            bgColor="transparent"
+            :autoBack="true"
+            leftIconSize="20px"
+          ></u-navbar>
+        </view>
+      </template>
+      <view class="main" v-for="(item, index) in dataList" :key="index">
+        <view class="item">
+          <view class="left">
+            <image :src="item.fromUserAvatar" style="width: 70px; height: 70px" />
+          </view>
+          <view class="right">
+            <view>
+              <view class="title">
+                {{ item.refTitle }}
+              </view>
+              <view class="contont">
+                {{ item.content }}
+              </view>
+              <view>
+                {{ item.createTime ? $moment(item.createTime).format("YYYY-MM-DD") : "" }}
+              </view>
+            </view>
+            <view>
+              <image
+                :src="item.extendInfo ? JSON.parse(item.extendInfo).refImg : ''"
+                style="width: 70px; height: 70px"
+                mode=""
+              />
+            </view>
+          </view>
+        </view>
+      </view>
+    </z-paging>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      show: false,
+      dataList: [],
+      queryParams: {
+        type: "1",
+        toUserId: "",
+        page: "1",
+        size: "10",
+      },
+    };
+  },
+  mounted() {
+    // #ifndef H5 || APP-PLUS || MP-ALIPAY
+    const { windowHeight, screenHeight, safeArea, statusBarHeight } = uni.$u.sys(); // 获取页面高度
+    let menuButtonObject = uni.getMenuButtonBoundingClientRect();
+    let navHeight =
+      menuButtonObject.height + (menuButtonObject.top - statusBarHeight) * 2;
+    this.statusBarHeight = navHeight + statusBarHeight + 4;
+    let tabBarHeight = windowHeight - safeArea.bottom;
+    this.bottomStatusHeight = screenHeight - this.statusBarHeight - tabBarHeight;
+    // #endif
+  },
+  onLoad(option) {
+    console.log("option", option);
+  },
+
+  onReady() {},
+  methods: {
+    leftClick() {
+      uni.reLaunch({
+        url: "/TUIKit/components/TUIConversation/index",
+      });
+    },
+    queryList(pageNumber, pageSize) {
+      this.queryParams.page = pageNumber;
+      this.queryParams.size = pageSize;
+      let dataList = [];
+      this.$refs.paging.complete(dataList);
+      this.api
+        .noticeList(this.queryParams)
+        .then((res) => {
+          if (res.code === 200) {
+            this.$refs.paging.complete(res.data.content);
+            this.api.readAll();
+          }
+        })
+        .catch((res) => {
+          this.$refs.paging.complete(false);
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.popup ::v-deep .u-button {
+  width: 100% !important;
+  height: 38px !important;
+  background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%) !important;
+  border: none !important;
+  border-radius: 29rpx !important;
+  font-size: 28rpx !important;
+  font-weight: bold !important;
+  margin-top: 30px;
+  color: white;
+}
+.main {
+  .item {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    padding: 10px 20px;
+    background: white;
+    .left {
+      width: 20%;
+      margin-right: 10px;
+      .img {
+        width: 110rpx;
+        height: 110rpx;
+        border-radius: 50%;
+      }
+    }
+    .right {
+      width: 80%;
+      line-height: 25px;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: space-around;
+      .contont {
+        font-size: 26rpx;
+        color: #333333;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 36rpx;
+        color: #333333;
+      }
+    }
+  }
+}
+.supply-hall {
+  width: 750rpx;
+  height: 100vh;
+  background-color: white;
+  overflow: hidden;
+
+  &-tabs {
+    height: 70rpx;
+    background-color: #ffffff;
+    border-radius: 0rpx 0rpx 28rpx 28rpx;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0rpx 24rpx;
+
+    &-item {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  &-screen {
+    height: 70rpx;
+    background-color: #ffffff;
+    // border-radius: 0rpx 0rpx 28rpx 28rpx;
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    position: relative;
+
+    &-item {
+      display: flex;
+      align-items: center;
+    }
+  }
+}
+
+.ml6 {
+  margin-left: 6rpx;
+}
+
+::v-deep .supply-hall-search .u-input.data-v-113bc24f {
+  background-color: #fff !important;
+  box-sizing: border-box !important;
+  border-radius: 33rpx !important;
+  height: 64rpx !important;
+  padding-right: 2rpx !important;
+}
+
+::v-deep .popup-item .u-input.data-v-113bc24f {
+  background-color: #fff !important;
+  box-sizing: border-box !important;
+  border-radius: 33rpx !important;
+  height: 64rpx !important;
+  padding-right: 2rpx !important;
+  width: 200rpx !important;
+}
+
+::v-deep .u-button--primary.data-v-2bf0e569 {
+  width: 110rpx !important;
+  height: 56rpx !important;
+  background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%) !important;
+  border: none !important;
+  border-radius: 29rpx !important;
+  font-size: 28rpx !important;
+  font-weight: bold !important;
+}
+</style>

+ 202 - 0
pages/message/serviceNotice.vue

@@ -0,0 +1,202 @@
+<template>
+  <view class="supply-hall">
+    <z-paging ref="paging" v-model="dataList" @query="queryList">
+      <template slot="top">
+        <view style="width: 100%; height: 50px; border-bottom: 1px solid #f1f1f1">
+          <u-navbar
+            @leftClick="leftClick"
+            title="服务通知"
+            bgColor="transparent"
+            :autoBack="true"
+            leftIconSize="20px"
+          ></u-navbar>
+        </view>
+      </template>
+      <view class="main" v-for="(item, index) in dataList" :key="index">
+        <view class="item">
+          <view class="right">
+            <view>
+              <view
+                style="display: flex; flex-direction: row; justify-content: space-between"
+              >
+                <text class="title">
+                  {{ item.refTitle }}
+                </text>
+                <text>
+                  {{
+                    item.createTime ? $moment(item.createTime).format("YYYY-MM-DD") : ""
+                  }}
+                </text>
+              </view>
+              <view class="contont">
+                {{ item.content }}
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+    </z-paging>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      show: false,
+      dataList: [],
+      queryParams: {
+        type: "3",
+        toUserId: "",
+        page: "1",
+        size: "10",
+      },
+    };
+  },
+  mounted() {
+    // #ifndef H5 || APP-PLUS || MP-ALIPAY
+    const { windowHeight, screenHeight, safeArea, statusBarHeight } = uni.$u.sys(); // 获取页面高度
+    let menuButtonObject = uni.getMenuButtonBoundingClientRect();
+    let navHeight =
+      menuButtonObject.height + (menuButtonObject.top - statusBarHeight) * 2;
+    this.statusBarHeight = navHeight + statusBarHeight + 4;
+    let tabBarHeight = windowHeight - safeArea.bottom;
+    this.bottomStatusHeight = screenHeight - this.statusBarHeight - tabBarHeight;
+    // #endif
+  },
+  onLoad(option) {
+    console.log("option", option);
+  },
+
+  onReady() {},
+  methods: {
+    leftClick() {
+      uni.reLaunch({
+        url: "/TUIKit/components/TUIConversation/index",
+      });
+    },
+    queryList(pageNumber, pageSize) {
+      this.queryParams.page = pageNumber;
+      this.queryParams.size = pageSize;
+      let dataList = [];
+      this.$refs.paging.complete(dataList);
+      this.api
+        .noticeList(this.queryParams)
+        .then((res) => {
+          if (res.code === 200) {
+            console.log("123213", res);
+            this.$refs.paging.complete(res.data.content);
+            this.api.readAll();
+          }
+        })
+        .catch((res) => {
+          this.$refs.paging.complete(false);
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.popup ::v-deep .u-button {
+  width: 100% !important;
+  height: 38px !important;
+  background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%) !important;
+  border: none !important;
+  border-radius: 29rpx !important;
+  font-size: 28rpx !important;
+  font-weight: bold !important;
+  margin-top: 30px;
+  color: white;
+}
+.main {
+  .item {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    padding: 10px 20px;
+    background: white;
+    border-bottom: 1px solid #f1f1f1;
+    .right {
+      width: 100%;
+      line-height: 25px;
+      .contont {
+        font-size: 26rpx;
+        color: #333333;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 36rpx;
+        color: #333333;
+      }
+    }
+  }
+}
+.supply-hall {
+  width: 750rpx;
+  height: 100vh;
+  background-color: white;
+  overflow: hidden;
+
+  &-tabs {
+    height: 70rpx;
+    background-color: #ffffff;
+    border-radius: 0rpx 0rpx 28rpx 28rpx;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0rpx 24rpx;
+
+    &-item {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  &-screen {
+    height: 70rpx;
+    background-color: #ffffff;
+    // border-radius: 0rpx 0rpx 28rpx 28rpx;
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    position: relative;
+
+    &-item {
+      display: flex;
+      align-items: center;
+    }
+  }
+}
+
+.ml6 {
+  margin-left: 6rpx;
+}
+
+::v-deep .supply-hall-search .u-input.data-v-113bc24f {
+  background-color: #fff !important;
+  box-sizing: border-box !important;
+  border-radius: 33rpx !important;
+  height: 64rpx !important;
+  padding-right: 2rpx !important;
+}
+
+::v-deep .popup-item .u-input.data-v-113bc24f {
+  background-color: #fff !important;
+  box-sizing: border-box !important;
+  border-radius: 33rpx !important;
+  height: 64rpx !important;
+  padding-right: 2rpx !important;
+  width: 200rpx !important;
+}
+
+::v-deep .u-button--primary.data-v-2bf0e569 {
+  width: 110rpx !important;
+  height: 56rpx !important;
+  background: linear-gradient(87deg, #28d141 0%, #28ef8c 100%) !important;
+  border: none !important;
+  border-radius: 29rpx !important;
+  font-size: 28rpx !important;
+  font-weight: bold !important;
+}
+</style>

+ 140 - 0
pages/views/login.vue

@@ -0,0 +1,140 @@
+<template></template>
+
+<script lang="ts" setup>
+import { ref, vueVersion } from "../../TUIKit/adapter-vue";
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { TUITranslateService } from "@tencentcloud/chat-uikit-engine";
+import Link from "../../utils/link";
+import { genTestUserSig } from "../../TUIKit/debug";
+import { isPC, isH5, isApp } from "../../TUIKit/utils/env";
+import Icon from "../../TUIKit/components/common/Icon.vue";
+import logo from "../../static/logo-back.svg";
+import { loginChat } from "../../loginChat";
+const privateAgree = ref(false);
+const inputValue = ref("");
+onLoad((options) => {
+  let personId = getQueryString("personId");
+  let type = getQueryString("type");
+  let openid = getQueryString("openid");
+  // 获取跳转的token和userId
+
+  let token = getQueryString("token");
+  let id = getQueryString("id");
+
+  uni.setStorageSync("personId", personId);
+  uni.setStorageSync("type", type);
+  uni.setStorageSync("token", token);
+  uni.setStorageSync("userId", id);
+  uni.setStorageSync("openid", openid);
+  let params = {
+    token: token,
+    id: id,
+  };
+  init(params);
+});
+
+function getQueryString(name) {
+  var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
+  var r = window.location.search.substr(1).match(reg);
+  if (r != null) {
+    return unescape(r[2]);
+  }
+  return null;
+}
+
+function init(params) {
+  inputValue.value = params.id;
+  handleLoginInfo();
+}
+const onAgreePrivateProtocol = () => {
+  privateAgree.value = !privateAgree.value;
+};
+
+const handleLoginInfo = () => {
+  const options = genTestUserSig({
+    SDKAppID: uni.$chat_SDKAppID,
+    secretKey: uni.$chat_secretKey,
+    userID: inputValue.value,
+  });
+  const loginInfo = {
+    SDKAppID: uni.$chat_SDKAppID,
+    userID: inputValue.value,
+    userSig: options.userSig,
+    useUploadPlugin: true,
+    framework: `vue${vueVersion}`,
+    TIMPush: uni.$TIMPush, // register TencentCloud-TIMPush
+    pushConfig: {
+      androidConfig: uni.$TIMPushConfigs, // Android timpush-configs.json
+      iOSConfig: {
+        iOSBusinessID: "", // iOS Certificate ID
+      },
+    },
+  };
+  login(loginInfo);
+};
+
+const login = (loginInfo: any) => {
+  loginChat(loginInfo).catch(() => {
+    uni.showToast({
+      title: TUITranslateService.t("Login.登录失败"),
+      icon: "none",
+    });
+  });
+};
+
+const openFullPlatformLink = (link: string) => {
+  if (isPC || isH5) {
+    window.open(link);
+  } else if (isApp) {
+    plus?.runtime?.openURL(link);
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../styles/login";
+
+.icon {
+  display: inline;
+}
+
+.btn {
+  background: none;
+  border: none;
+}
+
+.icon-unselected {
+  display: inline-block;
+  width: 12px;
+  height: 12px;
+  background: #fff;
+  border: 1px solid #ddd;
+  border-radius: 8px;
+}
+
+.selected-icon {
+  width: 14px;
+  height: 14px;
+}
+
+.icon-default {
+  margin: 7px 6px 0 0;
+}
+
+.login-input-uniapp {
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  outline: none;
+  height: 40px;
+  padding: 0 0 0 14px;
+}
+
+.logo-back-png {
+  width: 4.61rem;
+  height: 3.23rem;
+}
+
+.private-content-link {
+  display: inline-block;
+}
+</style>

+ 432 - 0
pages/views/profile.vue

@@ -0,0 +1,432 @@
+<template>
+  <div :class="['TUI-profile', !isPC && 'TUI-profile-h5']">
+    <div
+      v-if="displayType !== 'setting'"
+      :class="['TUI-profile-basic', !isPC && 'TUI-profile-h5-basic']"
+    >
+      <img
+        :class="[
+          'TUI-profile-basic-avatar',
+          !isPC && 'TUI-profile-h5-basic-avatar',
+        ]"
+        :src="
+          userProfile.avatar ||
+            'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
+        "
+      >
+      <div
+        :class="[
+          'TUI-profile-basic-info',
+          !isPC && 'TUI-profile-h5-basic-info',
+        ]"
+      >
+        <div
+          :class="[
+            'TUI-profile-basic-info-nick',
+            !isPC && 'TUI-profile-h5-basic-info-nick',
+          ]"
+        >
+          {{ userProfile.nick || "-" }}
+        </div>
+        <div
+          :class="[
+            'TUI-profile-basic-info-id',
+            !isPC && 'TUI-profile-h5-basic-info-id',
+          ]"
+        >
+          <label
+            :class="[
+              'TUI-profile-basic-info-id-label',
+              !isPC && 'TUI-profile-h5-basic-info-id-label',
+            ]"
+          >{{ "用户ID" }}:</label>
+          <div
+            :class="[
+              'TUI-profile-basic-info-id-value',
+              !isPC && 'TUI-profile-h5-basic-info-id-value',
+            ]"
+          >
+            {{ userProfile.userID }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      v-if="displayType !== 'profile' && (!isPC || showSetting)"
+      ref="settingDomRef"
+      :class="['TUI-profile-setting', !isPC && 'TUI-profile-h5-setting']"
+    >
+      <div
+        v-for="(item, key) in settingList"
+        :key="key"
+        :class="[
+          'TUI-profile-setting-item',
+          !isPC && 'TUI-profile-h5-setting-item',
+          item.value === 'exit' && 'TUI-profile-h5-setting-item-exit',
+        ]"
+      >
+        <div
+          :class="[
+            'TUI-profile-setting-item-label',
+            !isPC && 'TUI-profile-h5-setting-item-label',
+          ]"
+          @click="handleSettingListItemOnClick(item)"
+        >
+          <div :class="['label-left']">
+            <div :class="['label-title']">
+              {{ item.label }}
+            </div>
+            <div
+              v-if="
+                item.children && !isPC && item.childrenShowType === 'switch'
+              "
+              :class="['label-desc']"
+            >
+              {{ item.value }}
+            </div>
+          </div>
+          <div :class="['label-right']">
+            <div
+              v-if="
+                !isPC &&
+                  item.children &&
+                  item.selectedChild &&
+                  item.childrenShowType === 'bottomPopup'
+              "
+              :class="[
+                'TUI-profile-setting-item-label-value',
+                !isPC && 'TUI-profile-h5-setting-item-label-value',
+              ]"
+            >
+              {{ generateLabel(item) }}
+            </div>
+            <Icon
+              v-if="item.children"
+              :file="rightArrowIcon"
+              width="14px"
+              height="14px"
+              style="width: 14px; height: 14px; display: flex"
+            />
+          </div>
+        </div>
+        <!-- 移动端 children显示,分多个类型 -->
+        <BottomPopup
+          v-if="
+            item.children && !isPC && item.childrenShowType === 'bottomPopup'
+          "
+          :show="item.showChildren"
+          @onClose="bottomPopupOnClose(item)"
+        >
+          <div
+            v-for="child in item.children"
+            :class="[
+              'TUI-profile-setting-item-bottom-popup',
+              !isPC && 'TUI-profile-h5-setting-item-bottom-popup',
+            ]"
+            @click="handleSettingListItemOnClick(child)"
+          >
+            {{ child.label }}
+          </div>
+        </BottomPopup>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import TUIChatEngine, {
+  TUIUserService,
+  TUIStore,
+  StoreName,
+  TUIChatService,
+} from '@tencentcloud/chat-uikit-engine';
+import { TUILogin } from '@tencentcloud/tui-core';
+import { TUIGlobal } from '@tencentcloud/universal-api';
+import { ref, defineProps, onMounted } from '../../TUIKit/adapter-vue';
+import { isPC } from '../../TUIKit/utils/env';
+import { Toast, TOAST_TYPE } from '../../TUIKit/components/common/Toast/index';
+import BottomPopup from '../../TUIKit/components/common/BottomPopup/index.vue';
+import Icon from '../../TUIKit/components/common/Icon.vue';
+import rightArrowIcon from '../../TUIKit/assets/icon/right-icon.svg';
+import { IUserProfile } from '../../TUIKit/interface';
+import { onHide } from '@dcloudio/uni-app';
+import { translator } from '../../TUIKit/components/TUIChat/utils/translation';
+import { removeTokenStorage } from '../../utils/token';
+
+const props = defineProps({
+  displayType: {
+    type: String,
+    default: 'all', // "profile"/"setting"/"all"
+  },
+  showSetting: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+const settingDomRef = ref();
+const userProfile = ref<IUserProfile>({});
+const settingList = ref<{
+  [propsName: string]: {
+    value: string;
+    label: string;
+    onClick?: any;
+    // children相关
+    selectedChild?: string;
+    childrenShowType?: string; // "bottomPopup"/"switch"
+    showChildren?: boolean;
+    children?: {
+      [propsName: string]: {
+        value: string;
+        label: string;
+        onClick?: any;
+      };
+    };
+  };
+}>({
+  editProfile: {
+    value: 'editProfile',
+    label: '编辑资料(暂未开放)',
+    onClick: (item: any) => {
+      console.warn('编辑资料功能努力开发中,敬请期待');
+    },
+  },
+  allowType: {
+    value: 'allowType',
+    label: '加我为好友时',
+    selectedChild: '',
+    childrenShowType: 'bottomPopup',
+    showChildren: false,
+    onClick: (item: any) => {
+      if (!isPC) {
+        item.showChildren = true;
+      }
+    },
+    children: {
+      [TUIChatEngine.TYPES.ALLOW_TYPE_ALLOW_ANY]: {
+        value: TUIChatEngine.TYPES.ALLOW_TYPE_ALLOW_ANY,
+        label: '同意任何用户加好友',
+        onClick: (item: any) => {
+          updateMyProfile({ allowType: item.value });
+        },
+      },
+      [TUIChatEngine.TYPES.ALLOW_TYPE_NEED_CONFIRM]: {
+        value: TUIChatEngine.TYPES.ALLOW_TYPE_NEED_CONFIRM,
+        label: '需要验证',
+        onClick: (item: any) => {
+          updateMyProfile({ allowType: item.value });
+        },
+      },
+      [TUIChatEngine.TYPES.ALLOW_TYPE_DENY_ANY]: {
+        value: TUIChatEngine.TYPES.ALLOW_TYPE_DENY_ANY,
+        label: '拒绝任何人加好友',
+        onClick: (item: any) => {
+          updateMyProfile({ allowType: item.value });
+        },
+      },
+    },
+  },
+  displayMessageReadReceipt: {
+    value: 'displayMessageReadReceipt',
+    label: '消息阅读状态',
+    selectedChild: 'userLevelReadReceiptOpen',
+    childrenShowType: 'bottomPopup',
+    showChildren: false,
+    onClick(item: any) {
+      if (!isPC) {
+        item.showChildren = true;
+      }
+    },
+    children: {
+      userLevelReadReceiptOpen: {
+        value: 'userLevelReadReceiptOpen',
+        label: '开启',
+        onClick() {
+          switchEnabelUserLevelReadRecript(true);
+        },
+      },
+      userLevelReadReceiptClose: {
+        value: 'userLevelReadReceiptClose',
+        label: '关闭',
+        onClick() {
+          switchEnabelUserLevelReadRecript(false);
+        },
+      },
+    },
+  },
+  displayOnlineStatus: {
+    value: 'displayOnlineStatus',
+    label: '显示在线状态',
+    selectedChild: 'userLevelOnlineStatusOpen',
+    childrenShowType: 'bottomPopup',
+    showChildren: false,
+    onClick(item: any) {
+      if (!isPC) {
+        item.showChildren = true;
+      }
+    },
+    children: {
+      userLevelOnlineStatusOpen: {
+        value: 'userLevelOnlineStatusOpen',
+        label: '开启',
+        onClick() {
+          switchUserLevelOnlineStatus(true);
+        },
+      },
+      userLevelOnlineStatusClose: {
+        value: 'userLevelOnlineStatusClose',
+        label: '关闭',
+        onClick() {
+          switchUserLevelOnlineStatus(false);
+        },
+      },
+    },
+  },
+  translateLanguage: {
+    value: 'translateLanguage',
+    label: '翻译语言',
+    selectedChild: 'zh',
+    childrenShowType: 'bottomPopup',
+    showChildren: false,
+    onClick(item: any) {
+      if (!isPC) {
+        item.showChildren = true;
+      }
+    },
+    children: {
+      zh: {
+        value: 'zh',
+        label: '中文',
+        onClick() {
+          switchTranslationTargetLanguage('zh');
+        },
+      },
+      en: {
+        value: 'en',
+        label: 'English',
+        onClick() {
+          switchTranslationTargetLanguage('en');
+        },
+      },
+      jp: {
+        value: 'jp',
+        label: '日本語',
+        onClick() {
+          switchTranslationTargetLanguage('jp');
+        },
+      },
+      kr: {
+        value: 'kr',
+        label: '한국인',
+        onClick() {
+          switchTranslationTargetLanguage('kr');
+        },
+      },
+    },
+  },
+  exit: {
+    value: 'exit',
+    label: '退出登录',
+    onClick: (item: any) => {
+      TUILogin.logout().then(() => {
+        uni.removeStorage({
+          key: 'userInfo',
+        });
+        removeTokenStorage();
+        TUIGlobal?.reLaunch({
+          url: '/pages/views/login',
+        });
+      });
+    },
+  },
+});
+
+const handleSettingListItemOnClick = (item: any) => {
+  if (item?.onClick && typeof item?.onClick === 'function') {
+    item.onClick(item);
+  }
+};
+
+const bottomPopupOnClose = (item: any) => {
+  item.showChildren = false;
+};
+
+const generateLabel = (item: any) => {
+  return item?.children[item?.selectedChild]?.label;
+};
+
+const updateMyProfile = (props: object) => {
+  TUIUserService.updateMyProfile(props)
+    .then((res: any) => {
+      Toast({
+        message: '更新用户资料成功',
+        type: TOAST_TYPE.SUCCESS,
+        duration: 0,
+      });
+      if ('allowType' in props) {
+        settingList.value['allowType'].showChildren = false;
+      }
+    })
+    .catch((err: any) => {
+      console.warn('更新用户资料失败', err);
+      Toast({
+        message: '更新用户资料失败',
+        type: TOAST_TYPE.ERROR,
+        duration: 0,
+      });
+    });
+};
+
+TUIStore.watch(StoreName.USER, {
+  userProfile: (userProfileData: IUserProfile) => {
+    userProfile.value = userProfileData;
+    if (userProfile?.value?.allowType) {
+      settingList.value.allowType.selectedChild = userProfile?.value?.allowType;
+    }
+  },
+  displayMessageReadReceipt(isDisplay: boolean) {
+    settingList.value.displayMessageReadReceipt.selectedChild
+      = isDisplay ? 'userLevelReadReceiptOpen' : 'userLevelReadReceiptClose';
+  },
+  displayOnlineStatus(isOnlineStatusDisplay: boolean) {
+    settingList.value.displayOnlineStatus.selectedChild = isOnlineStatusDisplay
+      ? 'userLevelOnlineStatusOpen'
+      : 'userLevelOnlineStatusClose';
+  },
+});
+
+// 规避TUIStore.watch userProfile 登录后暂时不能及时触发更新
+onMounted(() => {
+  // 查询自己的资料
+  TUIUserService.getUserProfile().then((res: any) => {
+    userProfile.value = res.data;
+  });
+});
+
+// tabbar 切换其他tab,关闭profile已经打开的设置弹窗
+onHide(() => {
+  for (const settingItemKey in settingList.value) {
+    if (settingList?.value[settingItemKey]?.hasOwnProperty('showChildren')) {
+      settingList.value[settingItemKey].showChildren = false;
+    }
+  }
+});
+
+function switchEnabelUserLevelReadRecript(status: boolean) {
+  TUIStore.update(StoreName.USER, 'displayMessageReadReceipt', status);
+  settingList.value['displayMessageReadReceipt'].showChildren = false;
+}
+
+function switchUserLevelOnlineStatus(status: boolean) {
+  TUIUserService.switchUserStatus({ displayOnlineStatus: status });
+  settingList.value['displayOnlineStatus'].showChildren = false;
+}
+
+function switchTranslationTargetLanguage(lang: string) {
+  translator.clear();
+  TUIChatService.setTranslationLanguage(lang);
+  settingList.value.translateLanguage.selectedChild = lang;
+  settingList.value.translateLanguage.showChildren = false;
+}
+</script>
+
+<style lang="scss" scoped src="../../styles/profile/index.scss"></style>

+ 4 - 0
shims-vue.d.ts

@@ -0,0 +1,4 @@
+declare module "*.vue" {
+  import Vue from 'vue'
+  export default Vue
+}

+ 19 - 0
static/background.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="375px" height="268px" viewBox="0 0 375 268" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
+    <title>背景</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <linearGradient x1="50%" y1="81.9277858%" x2="50%" y2="100%" id="linearGradient-1">
+            <stop stop-color="#006EFF" offset="0%"></stop>
+            <stop stop-color="#00C8DC" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="浅色版本" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="主页" fill="url(#linearGradient-1)">
+            <g id="编组-4" transform="translate(-315.000000, -732.000000)">
+                <circle id="蒙版" cx="500" cy="500" r="500"></circle>
+            </g>
+        </g>
+    </g>
+</svg>

二進制
static/im-app.png


二進制
static/img/hmd.png


二進制
static/img/lxr.png


二進制
static/img/ql.png


二進制
static/login-bg.png


File diff suppressed because it is too large
+ 9 - 0
static/logo-back.svg


二進制
static/logo.png


二進制
static/message-selected.png


二進制
static/message.png


二進制
static/profile-selected.png


二進制
static/profile.png


二進制
static/relation-selected.png


二進制
static/relation.png


+ 27 - 0
static/selected.svg

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
+  <title>编组 14</title>
+  <defs>
+    <filter x="-10.6%" y="-5.4%" width="121.2%" height="110.9%" filterUnits="objectBoundingBox" id="filter-1">
+      <feOffset dx="0" dy="7" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+        <feMergeNode in="SourceGraphic"></feMergeNode>
+      </feMerge>
+    </filter>
+  </defs>
+  <g id="new" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="自定义消息" transform="translate(-458.000000, -318.000000)">
+      <g id="编组-32" filter="url(#filter-1)" transform="translate(44.000000, 60.000000)">
+        <g id="编组-24" transform="translate(30.000000, 250.000000)">
+          <g id="编组-14" transform="translate(384.000000, 8.000000)">
+            <circle id="椭圆形" fill="#006EFF" fill-rule="nonzero" cx="8" cy="8" r="8"></circle>
+            <polyline id="路径-4" stroke="#FFFFFF" stroke-width="2" transform="translate(8.042641, 6.242641) rotate(-315.000000) translate(-8.042641, -6.242641) " points="6.04264069 10.2426407 10.0426407 10.2426407 10.0426407 2.24264069"></polyline>
+          </g>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 1 - 0
timpush-configs.json

@@ -0,0 +1 @@
+{}

+ 77 - 0
uni.scss

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

+ 109 - 0
uni_modules/uview-ui/components/u-number-box/props.js

@@ -0,0 +1,109 @@
+export default {
+    props: {
+        // 步进器标识符,在change回调返回
+        name: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.name
+        },
+        // 用于双向绑定的值,初始化时设置设为默认min值(最小值)
+        value: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.value
+        },
+        // 最小值
+        min: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.min
+        },
+        // 最大值
+        max: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.max
+        },
+        // 加减的步长,可为小数
+        step: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.step
+        },
+        // 是否只允许输入整数
+        integer: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.integer
+        },
+        // 是否禁用,包括输入框,加减按钮
+        disabled: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.disabled
+        },
+        // 是否禁用输入框
+        disabledInput: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.disabledInput
+        },
+        // 是否开启异步变更,开启后需要手动控制输入值
+        asyncChange: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.asyncChange
+        },
+        // 输入框宽度,单位为px
+        inputWidth: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.inputWidth
+        },
+        // 是否显示减少按钮
+        showMinus: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.showMinus
+        },
+        // 是否显示增加按钮
+        showPlus: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.showPlus
+        },
+        // 显示的小数位数
+        decimalLength: {
+            type: [String, Number, null],
+            default: uni.$u.props.numberBox.decimalLength
+        },
+        // 是否开启长按加减手势
+        longPress: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.longPress
+        },
+        // 输入框文字和加减按钮图标的颜色
+        color: {
+            type: String,
+            default: uni.$u.props.numberBox.color
+        },
+        // 按钮大小,宽高等于此值,单位px,输入框高度和此值保持一致
+        buttonSize: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.buttonSize
+        },
+        // 输入框和按钮的背景颜色
+        bgColor: {
+            type: String,
+            default: uni.$u.props.numberBox.bgColor
+        },
+        // 指定光标于键盘的距离,避免键盘遮挡输入框,单位px
+        cursorSpacing: {
+            type: [String, Number],
+            default: uni.$u.props.numberBox.cursorSpacing
+        },
+        // 是否禁用增加按钮
+        disablePlus: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.disablePlus
+        },
+        // 是否禁用减少按钮
+        disableMinus: {
+            type: Boolean,
+            default: uni.$u.props.numberBox.disableMinus
+        },
+        // 加减按钮图标的样式
+        iconStyle: {
+            type: [Object, String],
+            default: uni.$u.props.numberBox.iconStyle
+        }
+    }
+}

+ 416 - 0
uni_modules/uview-ui/components/u-number-box/u-number-box.vue

@@ -0,0 +1,416 @@
+<template>
+	<view class="u-number-box">
+		<view
+		    class="u-number-box__slot"
+		    @tap.stop="clickHandler('minus')"
+		    @touchstart="onTouchStart('minus')"
+		    @touchend.stop="clearTimeout"
+		    v-if="showMinus && $slots.minus"
+		>
+			<slot name="minus" />
+		</view>
+		<view
+		    v-else-if="showMinus"
+		    class="u-number-box__minus"
+		    @tap.stop="clickHandler('minus')"
+		    @touchstart="onTouchStart('minus')"
+		    @touchend.stop="clearTimeout"
+		    hover-class="u-number-box__minus--hover"
+		    hover-stay-time="150"
+		    :class="{ 'u-number-box__minus--disabled': isDisabled('minus') }"
+		    :style="[buttonStyle('minus')]"
+		>
+			<u-icon
+			    name="minus"
+			    :color="isDisabled('minus') ? '#c8c9cc' : '#323233'"
+			    size="15"
+			    bold
+				:customStyle="iconStyle"
+			></u-icon>
+		</view>
+
+		<slot name="input">
+			<input
+			    :disabled="disabledInput || disabled"
+			    :cursor-spacing="getCursorSpacing"
+			    :class="{ 'u-number-box__input--disabled': disabled || disabledInput }"
+			    v-model="currentValue"
+			    class="u-number-box__input"
+			    @blur="onBlur"
+			    @focus="onFocus"
+			    @input="onInput"
+			    type="number"
+			    :style="[inputStyle]"
+			/>
+		</slot>
+		<view
+		    class="u-number-box__slot"
+		    @tap.stop="clickHandler('plus')"
+		    @touchstart="onTouchStart('plus')"
+		    @touchend.stop="clearTimeout"
+		    v-if="showPlus && $slots.plus"
+		>
+			<slot name="plus" />
+		</view>
+		<view
+		    v-else-if="showPlus"
+		    class="u-number-box__plus"
+		    @tap.stop="clickHandler('plus')"
+		    @touchstart="onTouchStart('plus')"
+		    @touchend.stop="clearTimeout"
+		    hover-class="u-number-box__plus--hover"
+		    hover-stay-time="150"
+		    :class="{ 'u-number-box__minus--disabled': isDisabled('plus') }"
+		    :style="[buttonStyle('plus')]"
+		>
+			<u-icon
+			    name="plus"
+			    :color="isDisabled('plus') ? '#c8c9cc' : '#323233'"
+			    size="15"
+			    bold
+				:customStyle="iconStyle"
+			></u-icon>
+		</view>
+	</view>
+</template>
+
+<script>
+	import props from './props.js';
+	/**
+	 * numberBox 步进器
+	 * @description 该组件一般用于商城购物选择物品数量的场景。
+	 * @tutorial https://uviewui.com/components/numberBox.html
+	 * @property {String | Number}	name			步进器标识符,在change回调返回
+	 * @property {String | Number}	value			用于双向绑定的值,初始化时设置设为默认min值(最小值)  (默认 0 )
+	 * @property {String | Number}	min				最小值 (默认 1 )
+	 * @property {String | Number}	max				最大值 (默认 Number.MAX_SAFE_INTEGER )
+	 * @property {String | Number}	step			加减的步长,可为小数 (默认 1 )
+	 * @property {Boolean}			integer			是否只允许输入整数 (默认 false )
+	 * @property {Boolean}			disabled		是否禁用,包括输入框,加减按钮 (默认 false )
+	 * @property {Boolean}			disabledInput	是否禁用输入框 (默认 false )
+	 * @property {Boolean}			asyncChange		是否开启异步变更,开启后需要手动控制输入值 (默认 false )
+	 * @property {String | Number}	inputWidth		输入框宽度,单位为px (默认 35 )
+	 * @property {Boolean}			showMinus		是否显示减少按钮 (默认 true )
+	 * @property {Boolean}			showPlus		是否显示增加按钮 (默认 true )
+	 * @property {String | Number}	decimalLength	显示的小数位数
+	 * @property {Boolean}			longPress		是否开启长按加减手势 (默认 true )
+	 * @property {String}			color			输入框文字和加减按钮图标的颜色 (默认 '#323233' )
+	 * @property {String | Number}	buttonSize		按钮大小,宽高等于此值,单位px,输入框高度和此值保持一致 (默认 30 )
+	 * @property {String}			bgColor			输入框和按钮的背景颜色 (默认 '#EBECEE' )
+	 * @property {String | Number}	cursorSpacing	指定光标于键盘的距离,避免键盘遮挡输入框,单位px (默认 100 )
+	 * @property {Boolean}			disablePlus		是否禁用增加按钮 (默认 false )
+	 * @property {Boolean}			disableMinus	是否禁用减少按钮 (默认 false )
+	 * @property {Object | String}	iconStyle		加减按钮图标的样式
+	 *
+	 * @event {Function}	onFocus	输入框活动焦点
+	 * @event {Function}	onBlur	输入框失去焦点
+	 * @event {Function}	onInput	输入框值发生变化
+	 * @event {Function}	onChange
+	 * @example <u-number-box v-model="value" @change="valChange"></u-number-box>
+	 */
+	export default {
+		name: 'u-number-box',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		data() {
+			return {
+				// 输入框实际操作的值
+				currentValue: '',
+				// 定时器
+				longPressTimer: null
+			}
+		},
+		watch: {
+			// 多个值之间,只要一个值发生变化,都要重新检查check()函数
+			watchChange(n) {
+				this.check()
+			},
+			// 监听v-mode的变化,重新初始化内部的值
+			value(n) {
+				if (n !== this.currentValue) {
+					this.currentValue = this.format(this.value)
+				}
+			}
+		},
+		computed: {
+			getCursorSpacing() {
+				// 判断传入的单位,如果为px单位,需要转成px
+				return uni.$u.getPx(this.cursorSpacing)
+			},
+			// 按钮的样式
+			buttonStyle() {
+				return (type) => {
+					const style = {
+						backgroundColor: this.bgColor,
+						height: uni.$u.addUnit(this.buttonSize),
+						color: this.color
+					}
+					if (this.isDisabled(type)) {
+						style.backgroundColor = '#f7f8fa'
+					}
+					return style
+				}
+			},
+			// 输入框的样式
+			inputStyle() {
+				const disabled = this.disabled || this.disabledInput
+				const style = {
+					color: this.color,
+					backgroundColor: this.bgColor,
+					height: uni.$u.addUnit(this.buttonSize),
+					width: uni.$u.addUnit(this.inputWidth)
+				}
+				return style
+			},
+			// 用于监听多个值发生变化
+			watchChange() {
+				return [this.integer, this.decimalLength, this.min, this.max]
+			},
+			isDisabled() {
+				return (type) => {
+					if (type === 'plus') {
+						// 在点击增加按钮情况下,判断整体的disabled,是否单独禁用增加按钮,以及当前值是否大于最大的允许值
+						return (
+							this.disabled ||
+							this.disablePlus ||
+							this.currentValue >= this.max
+						)
+					}
+					// 点击减少按钮同理
+					return (
+						this.disabled ||
+						this.disableMinus ||
+						this.currentValue <= this.min
+					)
+				}
+			},
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			init() {
+				this.currentValue = this.format(this.value)
+			},
+			// 格式化整理数据,限制范围
+			format(value) {
+				value = this.filter(value)
+				// 如果为空字符串,那么设置为0,同时将值转为Number类型
+				value = value === '' ? 0 : +value
+				// 对比最大最小值,取在min和max之间的值
+				value = Math.max(Math.min(this.max, value), this.min)
+				// 如果设定了最大的小数位数,使用toFixed去进行格式化
+				if (this.decimalLength !== null) {
+					value = value.toFixed(this.decimalLength)
+				}
+				return value
+			},
+			// 过滤非法的字符
+			filter(value) {
+				// 只允许0-9之间的数字,"."为小数点,"-"为负数时候使用
+				value = String(value).replace(/[^0-9.-]/g, '')
+				// 如果只允许输入整数,则过滤掉小数点后的部分
+				if (this.integer && value.indexOf('.') !== -1) {
+					value = value.split('.')[0]
+				}
+				return value;
+			},
+			check() {
+				// 格式化了之后,如果前后的值不相等,那么设置为格式化后的值
+				const val = this.format(this.currentValue);
+				if (val !== this.currentValue) {
+					this.currentValue = val
+				}
+			},
+			// 判断是否出于禁止操作状态
+			// isDisabled(type) {
+			// 	if (type === 'plus') {
+			// 		// 在点击增加按钮情况下,判断整体的disabled,是否单独禁用增加按钮,以及当前值是否大于最大的允许值
+			// 		return (
+			// 			this.disabled ||
+			// 			this.disablePlus ||
+			// 			this.currentValue >= this.max
+			// 		)
+			// 	}
+			// 	// 点击减少按钮同理
+			// 	return (
+			// 		this.disabled ||
+			// 		this.disableMinus ||
+			// 		this.currentValue <= this.min
+			// 	)
+			// },
+			// 输入框活动焦点
+			onFocus(event) {
+				this.$emit('focus', {
+					...event.detail,
+					name: this.name,
+				})
+			},
+			// 输入框失去焦点
+			onBlur(event) {
+				// 对输入值进行格式化
+				const value = this.format(event.detail.value)
+				// 发出blur事件
+				this.$emit(
+					'blur',{
+						...event.detail,
+						name: this.name,
+					}
+				)
+			},
+			// 输入框值发生变化
+			onInput(e) {
+				const {
+					value = ''
+				} = e.detail || {}
+				// 为空返回
+				if (value === '') return
+				let formatted = this.filter(value)
+				// 最大允许的小数长度
+				if (this.decimalLength !== null && formatted.indexOf('.') !== -1) {
+					const pair = formatted.split('.');
+					formatted = `${pair[0]}.${pair[1].slice(0, this.decimalLength)}`
+				}
+				formatted = this.format(formatted)
+				this.emitChange(formatted);
+			},
+			// 发出change事件
+			emitChange(value) {
+				// 如果开启了异步变更值,则不修改内部的值,需要用户手动在外部通过v-model变更
+				if (!this.asyncChange) {
+					this.$nextTick(() => {
+						this.$emit('input', value)
+						this.currentValue = value
+						this.$forceUpdate()
+					})
+				}
+				this.$emit('change', {
+					value,
+					name: this.name,
+				});
+			},
+			onChange() {
+				const {
+					type
+				} = this
+				if (this.isDisabled(type)) {
+					return this.$emit('overlimit', type)
+				}
+				const diff = type === 'minus' ? -this.step : +this.step
+				const value = this.format(this.add(+this.currentValue, diff))
+				this.emitChange(value)
+				this.$emit(type)
+			},
+			// 对值扩大后进行四舍五入,再除以扩大因子,避免出现浮点数操作的精度问题
+			add(num1, num2) {
+				const cardinal = Math.pow(10, 10);
+				return Math.round((num1 + num2) * cardinal) / cardinal
+			},
+			// 点击加减按钮
+			clickHandler(type) {
+				this.type = type
+				this.onChange()
+			},
+			longPressStep() {
+				// 每隔一段时间,重新调用longPressStep方法,实现长按加减
+				this.clearTimeout()
+				this.longPressTimer = setTimeout(() => {
+					this.onChange()
+					this.longPressStep()
+				}, 250);
+			},
+			onTouchStart(type) {
+				if (!this.longPress) return
+				this.clearTimeout()
+				this.type = type
+				// 一定时间后,默认达到长按状态
+				this.longPressTimer = setTimeout(() => {
+					this.onChange()
+					this.longPressStep()
+				}, 600)
+			},
+			// 触摸结束,清除定时器,停止长按加减
+			onTouchEnd() {
+				if (!this.longPress) return
+				this.clearTimeout()
+			},
+			// 清除定时器
+			clearTimeout() {
+				clearTimeout(this.longPressTimer)
+				this.longPressTimer = null
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import '../../libs/css/components.scss';
+
+	$u-numberBox-hover-bgColor: #E6E6E6 !default;
+	$u-numberBox-disabled-color: #c8c9cc !default;
+	$u-numberBox-disabled-bgColor: #f7f8fa !default;
+	$u-numberBox-plus-radius: 4px !default;
+	$u-numberBox-minus-radius: 4px !default;
+	$u-numberBox-input-text-align: center !default;
+	$u-numberBox-input-font-size: 15px !default;
+	$u-numberBox-input-padding: 0 !default;
+	$u-numberBox-input-margin: 0 2px !default;
+	$u-numberBox-input-disabled-color: #c8c9cc !default;
+	$u-numberBox-input-disabled-bgColor: #f2f3f5 !default;
+
+	.u-number-box {
+		@include flex(row);
+		align-items: center;
+
+		&__slot {
+			/* #ifndef APP-NVUE */
+			touch-action: none;
+			/* #endif */
+		}
+
+		&__plus,
+		&__minus {
+			width: 35px;
+			@include flex;
+			justify-content: center;
+			align-items: center;
+			/* #ifndef APP-NVUE */
+			touch-action: none;
+			/* #endif */
+
+			&--hover {
+				background-color: $u-numberBox-hover-bgColor !important;
+			}
+
+			&--disabled {
+				color: $u-numberBox-disabled-color;
+				background-color: $u-numberBox-disabled-bgColor;
+			}
+		}
+
+		&__plus {
+			border-top-right-radius: $u-numberBox-plus-radius;
+			border-bottom-right-radius: $u-numberBox-plus-radius;
+		}
+
+		&__minus {
+			border-top-left-radius: $u-numberBox-minus-radius;
+			border-bottom-left-radius: $u-numberBox-minus-radius;
+		}
+
+		&__input {
+			position: relative;
+			text-align: $u-numberBox-input-text-align;
+			font-size: $u-numberBox-input-font-size;
+			padding: $u-numberBox-input-padding;
+			margin: $u-numberBox-input-margin;
+			@include flex;
+			align-items: center;
+			justify-content: center;
+
+			&--disabled {
+				color: $u-numberBox-input-disabled-color;
+				background-color: $u-numberBox-input-disabled-bgColor;
+			}
+		}
+	}
+</style>

+ 19 - 0
uni_modules/uview-ui/components/u-number-keyboard/props.js

@@ -0,0 +1,19 @@
+export default {
+    props: {
+        // 键盘的类型,number-数字键盘,card-身份证键盘
+        mode: {
+            type: String,
+            default: uni.$u.props.numberKeyboard.value
+        },
+        // 是否显示键盘的"."符号
+        dotDisabled: {
+            type: Boolean,
+            default: uni.$u.props.numberKeyboard.dotDisabled
+        },
+        // 是否打乱键盘按键的顺序
+        random: {
+            type: Boolean,
+            default: uni.$u.props.numberKeyboard.random
+        }
+    }
+}

+ 196 - 0
uni_modules/uview-ui/components/u-number-keyboard/u-number-keyboard.vue

@@ -0,0 +1,196 @@
+<template>
+	<view
+		class="u-keyboard"
+		@touchmove.stop.prevent="noop"
+	>
+		<view
+			class="u-keyboard__button-wrapper"
+			v-for="(item, index) in numList"
+			:key="index"
+		>
+			<view
+				class="u-keyboard__button-wrapper__button"
+				:style="[itemStyle(index)]"
+				@tap="keyboardClick(item)"
+				hover-class="u-hover-class"
+				:hover-stay-time="200"
+			>
+				<text class="u-keyboard__button-wrapper__button__text">{{ item }}</text>
+			</view>
+		</view>
+		<view
+			class="u-keyboard__button-wrapper"
+		>
+			<view
+				class="u-keyboard__button-wrapper__button u-keyboard__button-wrapper__button--gray"
+				hover-class="u-hover-class"
+				:hover-stay-time="200"
+				@touchstart.stop="backspaceClick"
+				@touchend="clearTimer"
+			>
+				<u-icon
+					name="backspace"
+					color="#303133"
+					size="28"
+				></u-icon>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import props from './props.js';
+
+	/**
+	 * keyboard 键盘组件
+	 * @description
+	 * @tutorial
+	 * @property {String}	mode		键盘的类型,number-数字键盘,card-身份证键盘
+	 * @property {Boolean}	dotDisabled	是否显示键盘的"."符号
+	 * @property {Boolean}	random		是否打乱键盘按键的顺序
+	 * @event {Function} change		点击键盘触发
+	 * @event {Function} backspace	点击退格键触发
+	 * @example
+	 */
+	export default {
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		data() {
+			return {
+				backspace: 'backspace', // 退格键内容
+				dot: '.', // 点
+				timer: null, // 长按多次删除的事件监听
+				cardX: 'X' // 身份证的X符号
+			};
+		},
+		computed: {
+			// 键盘需要显示的内容
+			numList() {
+				let tmp = [];
+				if (this.dotDisabled && this.mode == 'number') {
+					if (!this.random) {
+						return [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
+					} else {
+						return uni.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
+					}
+				} else if (!this.dotDisabled && this.mode == 'number') {
+					if (!this.random) {
+						return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0];
+					} else {
+						return uni.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0]);
+					}
+				} else if (this.mode == 'card') {
+					if (!this.random) {
+						return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0];
+					} else {
+						return uni.$u.randomArray([1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0]);
+					}
+				}
+			},
+			// 按键的样式,在非乱序&&数字键盘&&不显示点按钮时,index为9时,按键占位两个空间
+			itemStyle() {
+				return index => {
+					let style = {};
+					if (this.mode == 'number' && this.dotDisabled && index == 9) style.width = '464rpx';
+					return style;
+				};
+			},
+			// 是否让按键显示灰色,只在非乱序&&数字键盘&&且允许点按键的时候
+			btnBgGray() {
+				return index => {
+					if (!this.random && index == 9 && (this.mode != 'number' || (this.mode == 'number' && !this
+							.dotDisabled))) return true;
+					else return false;
+				};
+			},
+		},
+		created() {
+
+		},
+		methods: {
+			// 点击退格键
+			backspaceClick() {
+				this.$emit('backspace');
+				clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+				this.timer = null;
+				this.timer = setInterval(() => {
+					this.$emit('backspace');
+				}, 250);
+			},
+			clearTimer() {
+				clearInterval(this.timer);
+				this.timer = null;
+			},
+			// 获取键盘显示的内容
+			keyboardClick(val) {
+				// 允许键盘显示点模式和触发非点按键时,将内容转为数字类型
+				if (!this.dotDisabled && val != this.dot && val != this.cardX) val = Number(val);
+				this.$emit('change', val);
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+	$u-number-keyboard-background-color:rgb(224, 228, 230) !default;
+	$u-number-keyboard-padding:8px 10rpx 8px 10rpx !default;
+	$u-number-keyboard-button-width:222rpx !default;
+	$u-number-keyboard-button-margin:4px 6rpx !default;
+	$u-number-keyboard-button-border-top-left-radius:4px !default;
+	$u-number-keyboard-button-border-top-right-radius:4px !default;
+	$u-number-keyboard-button-border-bottom-left-radius:4px !default;
+	$u-number-keyboard-button-border-bottom-right-radius:4px !default;
+	$u-number-keyboard-button-height: 90rpx!default;
+	$u-number-keyboard-button-background-color:#FFFFFF !default;
+	$u-number-keyboard-button-box-shadow:0 2px 0px #BBBCBE !default;
+	$u-number-keyboard-text-font-size:20px !default;
+	$u-number-keyboard-text-font-weight:500 !default;
+	$u-number-keyboard-text-color:$u-main-color !default;
+	$u-number-keyboard-gray-background-color:rgb(200, 202, 210) !default;
+	$u-number-keyboard-u-hover-class-background-color: #BBBCC6 !default;
+
+	.u-keyboard {
+		@include flex;
+		flex-direction: row;
+		justify-content: space-around;
+		background-color: $u-number-keyboard-background-color;
+		flex-wrap: wrap;
+		padding: $u-number-keyboard-padding;
+
+		&__button-wrapper {
+			box-shadow: $u-number-keyboard-button-box-shadow;
+			margin: $u-number-keyboard-button-margin;
+			border-top-left-radius: $u-number-keyboard-button-border-top-left-radius;
+			border-top-right-radius: $u-number-keyboard-button-border-top-right-radius;
+			border-bottom-left-radius: $u-number-keyboard-button-border-bottom-left-radius;
+			border-bottom-right-radius: $u-number-keyboard-button-border-bottom-right-radius;
+
+			&__button {
+				width: $u-number-keyboard-button-width;
+				height: $u-number-keyboard-button-height;
+				background-color: $u-number-keyboard-button-background-color;
+				@include flex;
+				justify-content: center;
+				align-items: center;
+				border-top-left-radius: $u-number-keyboard-button-border-top-left-radius;
+				border-top-right-radius: $u-number-keyboard-button-border-top-right-radius;
+				border-bottom-left-radius: $u-number-keyboard-button-border-bottom-left-radius;
+				border-bottom-right-radius: $u-number-keyboard-button-border-bottom-right-radius;
+
+				&__text {
+					font-size: $u-number-keyboard-text-font-size;
+					font-weight: $u-number-keyboard-text-font-weight;
+					color: $u-number-keyboard-text-color;
+				}
+
+				&--gray {
+					background-color: $u-number-keyboard-gray-background-color;
+				}
+			}
+		}
+	}
+
+	.u-hover-class {
+		background-color: $u-number-keyboard-u-hover-class-background-color;
+	}
+</style>

+ 24 - 0
uni_modules/uview-ui/components/u-overlay/props.js

@@ -0,0 +1,24 @@
+export default {
+    props: {
+        // 是否显示遮罩
+        show: {
+            type: Boolean,
+            default: uni.$u.props.overlay.show
+        },
+        // 层级z-index
+        zIndex: {
+            type: [String, Number],
+            default: uni.$u.props.overlay.zIndex
+        },
+        // 遮罩的过渡时间,单位为ms
+        duration: {
+            type: [String, Number],
+            default: uni.$u.props.overlay.duration
+        },
+        // 不透明度值,当做rgba的第四个参数
+        opacity: {
+            type: [String, Number],
+            default: uni.$u.props.overlay.opacity
+        }
+    }
+}

+ 68 - 0
uni_modules/uview-ui/components/u-overlay/u-overlay.vue

@@ -0,0 +1,68 @@
+<template>
+	<u-transition
+	    :show="show"
+	    custom-class="u-overlay"
+	    :duration="duration"
+	    :custom-style="overlayStyle"
+	    @click="clickHandler"
+	>
+		<slot />
+	</u-transition>
+</template>
+
+<script>
+	import props from './props.js';
+
+	/**
+	 * overlay 遮罩
+	 * @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景
+	 * @tutorial https://www.uviewui.com/components/overlay.html
+	 * @property {Boolean}			show		是否显示遮罩(默认 false )
+	 * @property {String | Number}	zIndex		zIndex 层级(默认 10070 )
+	 * @property {String | Number}	duration	动画时长,单位毫秒(默认 300 )
+	 * @property {String | Number}	opacity		不透明度值,当做rgba的第四个参数 (默认 0.5 )
+	 * @property {Object}			customStyle	定义需要用到的外部样式
+	 * @event {Function} click 点击遮罩发送事件
+	 * @example <u-overlay :show="show" @click="show = false"></u-overlay>
+	 */
+	export default {
+		name: "u-overlay",
+		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
+		computed: {
+			overlayStyle() {
+				const style = {
+					position: 'fixed',
+					top: 0,
+					left: 0,
+					right: 0,
+					zIndex: this.zIndex,
+					bottom: 0,
+					'background-color': `rgba(0, 0, 0, ${this.opacity})`
+				}
+				return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
+			}
+		},
+		methods: {
+			clickHandler() {
+				this.$emit('click')
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+     $u-overlay-top:0 !default;
+     $u-overlay-left:0 !default;
+     $u-overlay-width:100% !default;
+     $u-overlay-height:100% !default;
+     $u-overlay-background-color:rgba(0, 0, 0, .7) !default;
+	.u-overlay {
+		position: fixed;
+		top:$u-overlay-top;
+		left:$u-overlay-left;
+		width: $u-overlay-width;
+		height:$u-overlay-height;
+		background-color:$u-overlay-background-color;
+	}
+</style>

+ 499 - 0
uni_modules/uview-ui/components/u-parse/node/node.vue

@@ -0,0 +1,499 @@
+<template>
+  <view :id="attrs.id" :class="'_'+name+' '+attrs.class" :style="attrs.style">
+    <block v-for="(n, i) in childs" v-bind:key="i">
+      <!-- 图片 -->
+      <!-- 占位图 -->
+      <image v-if="n.name=='img'&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
+      <!-- 显示图片 -->
+      <!-- #ifdef H5 || APP-PLUS -->
+      <img v-if="n.name=='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]==-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap"/>
+      <!-- #endif -->
+      <!-- #ifndef H5 || APP-PLUS -->
+      <image v-if="n.name=='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]==-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="n.h?'':'widthFix'" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- 文本 -->
+      <!-- #ifndef MP-BAIDU -->
+      <text v-else-if="n.type=='text'" decode>{{n.text}}</text>
+      <!-- #endif -->
+      <text v-else-if="n.name=='br'">\n</text>
+      <!-- 链接 -->
+      <view v-else-if="n.name=='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
+        <node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
+      </view>
+      <!-- 视频 -->
+      <!-- #ifdef APP-PLUS -->
+      <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" />
+      <!-- #endif -->
+      <!-- #ifndef APP-PLUS -->
+      <video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <!-- #endif -->
+      <!-- #ifdef H5 || APP-PLUS -->
+      <iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
+      <embed v-else-if="n.name=='embed'" :style="n.attrs.style" :src="n.attrs.src" />
+      <!-- #endif -->
+      <!-- #ifndef MP-TOUTIAO -->
+      <!-- 音频 -->
+      <audio v-else-if="n.name=='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <!-- #endif -->
+      <view v-else-if="(n.name=='table'&&n.c)||n.name=='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
+        <node v-if="n.name=='li'" :childs="n.children" :opts="opts" />
+        <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
+          <node v-if="tbody.name=='td'||tbody.name=='th'" :childs="tbody.children" :opts="opts" />
+          <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
+            <view v-if="tr.name=='td'||tr.name=='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <node :childs="tr.children" :opts="opts" />
+            </view>
+            <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
+                <node :childs="td.children" :opts="opts" />
+              </view>
+            </view>
+          </block>
+        </view>
+      </view>
+      
+      <!-- 富文本 -->
+      <!-- #ifdef H5 || MP-WEIXIN || MP-QQ || APP-PLUS || MP-360 -->
+      <rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :style="n.f" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || MP-WEIXIN || MP-QQ || APP-PLUS || MP-360 -->
+      <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="n.f+';display:inline'" :preview="false" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- 继续递归 -->
+      <view v-else-if="n.c==2" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
+        <node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
+      </view>
+      <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
+    </block>
+  </view>
+</template>
+<script module="handler" lang="wxs">
+// 行内标签列表
+var inlineTags = {
+  abbr: true,
+  b: true,
+  big: true,
+  code: true,
+  del: true,
+  em: true,
+  i: true,
+  ins: true,
+  label: true,
+  q: true,
+  small: true,
+  span: true,
+  strong: true,
+  sub: true,
+  sup: true
+}
+/**
+ * @description 是否使用 rich-text 显示剩余内容
+ */
+module.exports = {
+  use: function (item) {
+  // 微信和 QQ 的 rich-text inline 布局无效
+  if (inlineTags[item.name] || (item.attrs.style || '').indexOf('display:inline') != -1)
+    return false
+  return !item.c
+  }
+}
+</script>
+<script>
+
+import node from './node'
+export default {
+  name: 'node',
+  // #ifdef MP-WEIXIN
+  options: {
+    virtualHost: true
+  },
+  // #endif
+  data() {
+    return {
+      ctrl: {}
+    }
+  },
+  props: {
+    name: String,
+    attrs: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    childs: Array,
+    opts: Array
+  },
+  components: {
+
+    node
+  },
+  mounted() {
+    for (this.root = this.$parent; this.root.$options.name != 'mp-html'; this.root = this.root.$parent);
+    // #ifdef H5 || APP-PLUS
+    if (this.opts[0]) {
+      for (var i = this.childs.length; i--;)
+        if (this.childs[i].name == 'img')
+          break
+      if (i != -1) {
+        this.observer = uni.createIntersectionObserver(this).relativeToViewport({
+          top: 500,
+          bottom: 500
+        })
+        this.observer.observe('._img', res => {
+          if (res.intersectionRatio) {
+            this.$set(this.ctrl, 'load', 1)
+            this.observer.disconnect()
+          }
+        })
+      }
+    }
+    // #endif
+  },
+  beforeDestroy() {
+    // #ifdef H5 || APP-PLUS
+    if (this.observer)
+      this.observer.disconnect()
+    // #endif
+  },
+  methods:{
+    // #ifdef MP-WEIXIN
+    toJSON() { },
+    // #endif
+    /**
+     * @description 播放视频事件
+     * @param {Event} e 
+     */
+    play(e) {
+      // #ifndef APP-PLUS
+      if (this.root.pauseVideo) {
+        var flag = false, id = e.target.id
+        for (var i = this.root._videos.length; i--;) {
+          if (this.root._videos[i].id == id)
+            flag = true
+          else
+            this.root._videos[i].pause() // 自动暂停其他视频
+        }
+        // 将自己加入列表
+        if (!flag) {
+          var ctx = uni.createVideoContext(id
+            // #ifndef MP-BAIDU
+            , this
+            // #endif
+          )
+          ctx.id = id
+          this.root._videos.push(ctx)
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 图片点击事件
+     * @param {Event} e 
+     */
+    imgTap(e) {
+      var node = this.childs[e.currentTarget.dataset.i]
+      if (node.a)
+        return this.linkTap(node.a)
+      if (node.attrs.ignore)
+        return
+      // #ifdef H5 || APP-PLUS
+      node.attrs.src = node.attrs.src || node.attrs['data-src']
+      // #endif
+      this.root.$emit('imgTap', node.attrs)
+      // 自动预览图片
+      if (this.root.previewImg)
+        uni.previewImage({
+          current: parseInt(node.attrs.i),
+          urls: this.root.imgList
+        })
+    },
+
+    /**
+     * @description 图片长按
+     */
+    imgLongTap(e) {
+      // #ifdef APP-PLUS
+      var attrs = this.childs[e.currentTarget.dataset.i].attrs
+      if (!attrs.ignore)
+        uni.showActionSheet({
+          itemList: ['保存图片'],
+          success: () => {
+            uni.downloadFile({
+              url: this.root.imgList[attrs.i],
+              success: res => {
+                uni.saveImageToPhotosAlbum({
+                  filePath: res.tempFilePath,
+                  success() {
+                    uni.showToast({
+                      title: '保存成功'
+                    })
+                  }
+                })
+              }
+            })
+          }
+        })
+      // #endif
+    },
+
+    /**
+     * @description 图片加载完成事件
+     * @param {Event} e 
+     */
+    imgLoad(e) {
+      var i = e.currentTarget.dataset.i
+      // #ifndef H5 || APP-PLUS
+      // 设置原宽度
+      if (!this.childs[i].w)
+        this.$set(this.ctrl, i, e.detail.width)
+      else
+        // #endif
+        // 加载完毕,取消加载中占位图
+        if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] == -1)
+          this.$set(this.ctrl, i, 1)
+    },
+
+    /**
+     * @description 链接点击事件
+     * @param {Event} e 
+     */
+    linkTap(e) {
+      var attrs = e.currentTarget ? this.childs[e.currentTarget.dataset.i].attrs : e,
+        href = attrs.href
+      this.root.$emit('linkTap', attrs)
+      if (href) {
+        // 跳转锚点
+        if (href[0] == '#')
+          this.root.navigateTo(href.substring(1)).catch(() => { })
+        // 复制外部链接
+        else if (href.includes('://')) {
+          if (this.root.copyLink) {
+            // #ifdef H5
+            window.open(href)
+            // #endif
+            // #ifdef MP
+            uni.setClipboardData({
+              data: href,
+              success: () =>
+                uni.showToast({
+                  title: '链接已复制'
+                })
+            })
+            // #endif
+            // #ifdef APP-PLUS
+            plus.runtime.openWeb(href)
+            // #endif
+          }
+        }
+        // 跳转页面
+        else
+          uni.navigateTo({
+            url: href,
+            fail() {
+              uni.switchTab({
+                url: href,
+                fail() { }
+              })
+            }
+          })
+      }
+    },
+
+    /**
+     * @description 错误事件
+     * @param {Event} e 
+     */
+    mediaError(e) {
+      var i = e.currentTarget.dataset.i,
+        node = this.childs[i]
+      // 加载其他源
+      if (node.name == 'video' || node.name == 'audio') {
+        var index = (this.ctrl[i] || 0) + 1
+        if (index > node.src.length)
+          index = 0
+        if (index < node.src.length)
+          return this.$set(this.ctrl, i, index)
+      }
+      // 显示错误占位图
+      else if (node.name == 'img' && this.opts[2])
+        this.$set(this.ctrl, i, -1)
+      if (this.root)
+        this.root.$emit('error', {
+          source: node.name,
+          attrs: node.attrs,
+          errMsg: e.detail.errMsg
+        })
+    }
+  }
+}
+</script>
+<style>
+/* a 标签默认效果 */
+._a {
+  padding: 1.5px 0 1.5px 0;
+  color: #366092;
+  word-break: break-all;
+}
+
+/* a 标签点击态效果 */
+._hover {
+  text-decoration: underline;
+  opacity: 0.7;
+}
+
+/* 图片默认效果 */
+._img {
+  max-width: 100%;
+  -webkit-touch-callout: none;
+}
+
+/* 内部样式 */
+
+._b,
+._strong {
+  font-weight: bold;
+}
+
+._code {
+  font-family: monospace;
+}
+
+._del {
+  text-decoration: line-through;
+}
+
+._em,
+._i {
+  font-style: italic;
+}
+
+._h1 {
+  font-size: 2em;
+}
+
+._h2 {
+  font-size: 1.5em;
+}
+
+._h3 {
+  font-size: 1.17em;
+}
+
+._h5 {
+  font-size: 0.83em;
+}
+
+._h6 {
+  font-size: 0.67em;
+}
+
+._h1,
+._h2,
+._h3,
+._h4,
+._h5,
+._h6 {
+  display: block;
+  font-weight: bold;
+}
+
+._image {
+  height: 1px;
+}
+
+._ins {
+  text-decoration: underline;
+}
+
+._li {
+  display: list-item;
+}
+
+._ol {
+  list-style-type: decimal;
+}
+
+._ol,
+._ul {
+  display: block;
+  padding-left: 40px;
+  margin: 1em 0;
+}
+
+._q::before {
+  content: '"';
+}
+
+._q::after {
+  content: '"';
+}
+
+._sub {
+  font-size: smaller;
+  vertical-align: sub;
+}
+
+._sup {
+  font-size: smaller;
+  vertical-align: super;
+}
+
+._thead,
+._tbody,
+._tfoot {
+  display: table-row-group;
+}
+
+._tr {
+  display: table-row;
+}
+
+._td,
+._th {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+._th {
+  font-weight: bold;
+  text-align: center;
+}
+
+._ul {
+  list-style-type: disc;
+}
+
+._ul ._ul {
+  margin: 0;
+  list-style-type: circle;
+}
+
+._ul ._ul ._ul {
+  list-style-type: square;
+}
+
+._abbr,
+._b,
+._code,
+._del,
+._em,
+._i,
+._ins,
+._label,
+._q,
+._span,
+._strong,
+._sub,
+._sup {
+  display: inline;
+}
+
+/* #ifdef APP-PLUS */
+._video {
+  width: 300px;
+  height: 225px;
+}
+/* #endif */
+</style>

File diff suppressed because it is too large
+ 1075 - 0
uni_modules/uview-ui/components/u-parse/parser.js


+ 45 - 0
uni_modules/uview-ui/components/u-parse/props.js

@@ -0,0 +1,45 @@
+export default {
+    props: {
+        // #ifdef APP-PLUS-NVUE
+        bgColor: String,
+        // #endif
+        content: String,
+        copyLink: {
+		  type: Boolean,
+		  default: uni.$u.props.parse.copyLink
+        },
+        domain: String,
+        errorImg: {
+		  type: String,
+		  default: uni.$u.props.parse.errorImg
+        },
+        lazyLoad: {
+		  type: Boolean,
+		  default: uni.$u.props.parse.lazyLoad
+        },
+        loadingImg: {
+		  type: String,
+		  default: uni.$u.props.parse.loadingImg
+        },
+        pauseVideo: {
+		  type: Boolean,
+		  default: uni.$u.props.parse.pauseVideo
+        },
+        previewImg: {
+		  type: Boolean,
+		  default: uni.$u.props.parse.previewImg
+        },
+        scrollTable: Boolean,
+        selectable: Boolean,
+        setTitle: {
+		  type: Boolean,
+		  default: uni.$u.props.parse.setTitle
+        },
+        showImgMenu: {
+		  type: Boolean,
+		  default: uni.$u.props.parse.showImgMenu
+        },
+        tagStyle: Object,
+        useAnchor: null
+	  }
+}

+ 366 - 0
uni_modules/uview-ui/components/u-parse/u-parse.vue

@@ -0,0 +1,366 @@
+<template>
+  <view id="_root" :class="(selectable?'_select ':'')+'_root'">
+    <slot v-if="!nodes[0]" />
+    <!-- #ifndef APP-PLUS-NVUE -->
+    <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu]" />
+    <!-- #endif -->
+    <!-- #ifdef APP-PLUS-NVUE -->
+    <web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+	import props from './props.js';
+/**
+ * mp-html v2.0.4
+ * @description 富文本组件
+ * @tutorial https://github.com/jin-yufeng/mp-html
+ * @property {String}			bgColor		背景颜色,只适用与APP-PLUS-NVUE
+ * @property {String}			content		用于渲染的富文本字符串(默认 true )
+ * @property {Boolean}			copyLink	是否允许外部链接被点击时自动复制
+ * @property {String}			domain		主域名,用于拼接链接
+ * @property {String}			errorImg	图片出错时的占位图链接
+ * @property {Boolean}			lazyLoad	是否开启图片懒加载(默认 true )
+ * @property {string}			loadingImg	图片加载过程中的占位图链接
+ * @property {Boolean}			pauseVideo	是否在播放一个视频时自动暂停其它视频(默认 true )
+ * @property {Boolean}			previewImg	是否允许图片被点击时自动预览(默认 true )
+ * @property {Boolean}			scrollTable	是否给每个表格添加一个滚动层使其能单独横向滚动
+ * @property {Boolean}			selectable	是否开启长按复制
+ * @property {Boolean}			setTitle	是否将 title 标签的内容设置到页面标题(默认 true )
+ * @property {Boolean}			showImgMenu	是否允许图片被长按时显示菜单(默认 true )
+ * @property {Object}			tagStyle	标签的默认样式
+ * @property {Boolean | Number}	useAnchor	是否使用锚点链接
+ * 
+ * @event {Function}	load	dom 结构加载完毕时触发
+ * @event {Function}	ready	所有图片加载完毕时触发
+ * @event {Function}	imgTap	图片被点击时触发
+ * @event {Function}	linkTap	链接被点击时触发
+ * @event {Function}	error	媒体加载出错时触发
+ */
+const plugins=[]
+const parser = require('./parser')
+// #ifndef APP-PLUS-NVUE
+import node from './node/node'
+// #endif
+// #ifdef APP-PLUS-NVUE
+const dom = weex.requireModule('dom')
+// #endif
+export default {
+  name: 'mp-html',
+  data() {
+    return {
+      nodes: [],
+      // #ifdef APP-PLUS-NVUE
+      height: 0
+      // #endif
+    }
+  },
+  mixins:[props],
+  // #ifndef APP-PLUS-NVUE
+  components: {
+    node
+  },
+  // #endif
+  watch: {
+    content(content) {
+      this.setContent(content)
+    }
+  },
+  created() {
+    this.plugins = []
+    for (let i = plugins.length; i--;)
+      this.plugins.push(new plugins[i](this))
+  },
+  mounted() {
+    if (this.content && !this.nodes.length)
+      this.setContent(this.content)
+  },
+  beforeDestroy() {
+    this._hook('onDetached')
+    clearInterval(this._timer)
+  },
+  methods: {
+    /**
+     * @description 将锚点跳转的范围限定在一个 scroll-view 内
+     * @param {Object} page scroll-view 所在页面的示例
+     * @param {String} selector scroll-view 的选择器
+     * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
+     */
+    in(page, selector, scrollTop) {
+      // #ifndef APP-PLUS-NVUE
+      if (page && selector && scrollTop)
+        this._in = {
+          page,
+          selector,
+          scrollTop
+        }
+      // #endif
+    },
+
+    /**
+     * @description 锚点跳转
+     * @param {String} id 要跳转的锚点 id
+     * @param {Number} offset 跳转位置的偏移量
+     * @returns {Promise}
+     */
+    navigateTo(id, offset) {
+      return new Promise((resolve, reject) => {
+        if (!this.useAnchor)
+          return reject('Anchor is disabled')
+        offset = offset || parseInt(this.useAnchor) || 0
+        // #ifdef APP-PLUS-NVUE
+        if (!id) {
+          dom.scrollToElement(this.$refs.web, {
+            offset
+          })
+          resolve()
+        } else {
+          this._navigateTo = {
+            resolve,
+            reject,
+            offset
+          }
+          this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
+        }
+        // #endif
+        // #ifndef APP-PLUS-NVUE
+        let deep = ' '
+        // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
+        deep = '>>>'
+        // #endif
+        const selector = uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this._in ? this._in.page : this)
+          // #endif
+          .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
+        if (this._in)
+          selector.select(this._in.selector).scrollOffset()
+            .select(this._in.selector).boundingClientRect() // 获取 scroll-view 的位置和滚动距离
+        else
+          selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
+        selector.exec(res => {
+          if (!res[0])
+            return reject('Label not found')
+          const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
+          if (this._in)
+            // scroll-view 跳转
+            this._in.page[this._in.scrollTop] = scrollTop
+          else
+            // 页面跳转
+            uni.pageScrollTo({
+              scrollTop,
+              duration: 300
+            })
+          resolve()
+        })
+        // #endif
+      })
+    },
+
+    /**
+     * @description 获取文本内容
+     * @return {String}
+     */
+    getText() {
+      let text = '';
+      (function traversal(nodes) {
+        for (let i = 0; i < nodes.length; i++) {
+          const node = nodes[i]
+          if (node.type == 'text')
+            text += node.text.replace(/&amp;/g, '&')
+          else if (node.name == 'br')
+            text += '\n'
+          else {
+            // 块级标签前后加换行
+            const isBlock = node.name == 'p' || node.name == 'div' || node.name == 'tr' || node.name == 'li' || (node.name[0] == 'h' && node.name[1] > '0' && node.name[1] < '7')
+            if (isBlock && text && text[text.length - 1] != '\n')
+              text += '\n'
+            // 递归获取子节点的文本
+            if (node.children)
+              traversal(node.children)
+            if (isBlock && text[text.length - 1] != '\n')
+              text += '\n'
+            else if (node.name == 'td' || node.name == 'th')
+              text += '\t'
+          }
+        }
+      })(this.nodes)
+      return text
+    },
+
+    /**
+     * @description 获取内容大小和位置
+     * @return {Promise}
+     */
+    getRect() {
+      return new Promise((resolve, reject) => {
+        uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this)
+          // #endif
+          .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject('Root label not found'))
+      })
+    },
+
+    /**
+     * @description 设置内容
+     * @param {String} content html 内容
+     * @param {Boolean} append 是否在尾部追加
+     */
+    setContent(content, append) {
+      if (!append || !this.imgList)
+        this.imgList = []
+      const nodes = new parser(this).parse(content)
+      // #ifdef APP-PLUS-NVUE
+      if (this._ready)
+        this._set(nodes, append)
+      // #endif
+      this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
+
+      // #ifndef APP-PLUS-NVUE
+      this._videos = []
+      this.$nextTick(() => {
+        this._hook('onLoad')
+        this.$emit('load')
+      })
+
+      // 等待图片加载完毕
+      let height
+      clearInterval(this._timer)
+      this._timer = setInterval(() => {
+        this.getRect().then(rect => {
+          // 350ms 总高度无变化就触发 ready 事件
+          if (rect.height == height) {
+            this.$emit('ready', rect)
+            clearInterval(this._timer)
+          }
+          height = rect.height
+        }).catch(() => { })
+      }, 350)
+      // #endif
+    },
+
+    /**
+     * @description 调用插件钩子函数
+     */
+    _hook(name) {
+      for (let i = plugins.length; i--;)
+        if (this.plugins[i][name])
+          this.plugins[i][name]()
+    },
+
+    // #ifdef APP-PLUS-NVUE
+    /**
+     * @description 设置内容
+     */
+    _set(nodes, append) {
+      this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes) + ',' + JSON.stringify([this.bgColor, this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
+    },
+
+    /**
+     * @description 接收到 web-view 消息
+     */
+    _onMessage(e) {
+      const message = e.detail.data[0]
+      switch (message.action) {
+        // web-view 初始化完毕
+        case 'onJSBridgeReady':
+          this._ready = true
+          if (this.nodes)
+            this._set(this.nodes)
+          break
+        // 内容 dom 加载完毕
+        case 'onLoad':
+          this.height = message.height
+          this._hook('onLoad')
+          this.$emit('load')
+          break
+        // 所有图片加载完毕
+        case 'onReady':
+          this.getRect().then(res => {
+            this.$emit('ready', res)
+          }).catch(() => { })
+          break
+        // 总高度发生变化
+        case 'onHeightChange':
+          this.height = message.height
+          break
+        // 图片点击
+        case 'onImgTap':
+          this.$emit('imgTap', message.attrs)
+          if (this.previewImg)
+            uni.previewImage({
+              current: parseInt(message.attrs.i),
+              urls: this.imgList
+            })
+          break
+        // 链接点击
+        case 'onLinkTap':
+          const href = message.attrs.href
+          this.$emit('linkTap', message.attrs)
+          if (href) {
+            // 锚点跳转
+            if (href[0] == '#') {
+              if (this.useAnchor)
+                dom.scrollToElement(this.$refs.web, {
+                  offset: message.offset
+                })
+            }
+            // 打开外链
+            else if (href.includes('://')) {
+              if (this.copyLink)
+                plus.runtime.openWeb(href)
+            }
+            else
+              uni.navigateTo({
+                url: href,
+                fail() {
+                  wx.switchTab({
+                    url: href
+                  })
+                }
+              })
+          }
+          break
+        // 获取到锚点的偏移量
+        case 'getOffset':
+          if (typeof message.offset == 'number') {
+            dom.scrollToElement(this.$refs.web, {
+              offset: message.offset + this._navigateTo.offset
+            })
+            this._navigateTo.resolve()
+          } else
+            this._navigateTo.reject('Label not found')
+          break
+        // 点击
+        case 'onClick':
+          this.$emit('tap')
+          break
+        // 出错
+        case 'onError':
+          this.$emit('error', {
+            source: message.source,
+            attrs: message.attrs
+          })
+      }
+    }
+    // #endif
+  }
+}
+</script>
+
+<style>
+/* #ifndef APP-PLUS-NVUE */
+/* 根节点样式 */
+._root {
+  overflow: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+/* 长按复制 */
+._select {
+  user-select: text;
+}
+/* #endif */
+</style>

+ 5 - 0
uni_modules/uview-ui/components/u-picker-column/props.js

@@ -0,0 +1,5 @@
+export default {
+    props: {
+
+    }
+}

+ 27 - 0
uni_modules/uview-ui/components/u-picker-column/u-picker-column.vue

@@ -0,0 +1,27 @@
+<template>
+	<picker-view-column>
+		<view class="u-picker-column">
+
+		</view>
+	</picker-view-column>
+</template>
+
+<script>
+	import props from './props.js';
+	/**
+	 * PickerColumn 
+	 * @description 
+	 * @tutorial url
+	 * @property {String}
+	 * @event {Function}
+	 * @example
+	 */
+	export default {
+		name: 'u-picker-column',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+</style>

+ 79 - 0
uni_modules/uview-ui/components/u-picker/props.js

@@ -0,0 +1,79 @@
+export default {
+    props: {
+        // 是否展示picker弹窗
+        show: {
+            type: Boolean,
+            default: uni.$u.props.picker.show
+        },
+        // 是否展示顶部的操作栏
+        showToolbar: {
+            type: Boolean,
+            default: uni.$u.props.picker.showToolbar
+        },
+        // 顶部标题
+        title: {
+            type: String,
+            default: uni.$u.props.picker.title
+        },
+        // 对象数组,设置每一列的数据
+        columns: {
+            type: Array,
+            default: uni.$u.props.picker.columns
+        },
+        // 是否显示加载中状态
+        loading: {
+            type: Boolean,
+            default: uni.$u.props.picker.loading
+        },
+        // 各列中,单个选项的高度
+        itemHeight: {
+            type: [String, Number],
+            default: uni.$u.props.picker.itemHeight
+        },
+        // 取消按钮的文字
+        cancelText: {
+            type: String,
+            default: uni.$u.props.picker.cancelText
+        },
+        // 确认按钮的文字
+        confirmText: {
+            type: String,
+            default: uni.$u.props.picker.confirmText
+        },
+        // 取消按钮的颜色
+        cancelColor: {
+            type: String,
+            default: uni.$u.props.picker.cancelColor
+        },
+        // 确认按钮的颜色
+        confirmColor: {
+            type: String,
+            default: uni.$u.props.picker.confirmColor
+        },
+        // 每列中可见选项的数量
+        visibleItemCount: {
+            type: [String, Number],
+            default: uni.$u.props.picker.visibleItemCount
+        },
+        // 选项对象中,需要展示的属性键名
+        keyName: {
+            type: String,
+            default: uni.$u.props.picker.keyName
+        },
+        // 是否允许点击遮罩关闭选择器
+        closeOnClickOverlay: {
+            type: Boolean,
+            default: uni.$u.props.picker.closeOnClickOverlay
+        },
+        // 各列的默认索引
+        defaultIndex: {
+            type: Array,
+            default: uni.$u.props.picker.defaultIndex
+        },
+		// 是否在手指松开时立即触发 change 事件。若不开启则会在滚动动画结束后触发 change 事件,只在微信2.21.1及以上有效
+		immediateChange: {
+			type: Boolean,
+			default: uni.$u.props.picker.immediateChange
+		}
+    }
+}

+ 286 - 0
uni_modules/uview-ui/components/u-picker/u-picker.vue

@@ -0,0 +1,286 @@
+<template>
+	<u-popup
+		:show="show"
+		@close="closeHandler"
+	>
+		<view class="u-picker">
+			<u-toolbar
+				v-if="showToolbar"
+				:cancelColor="cancelColor"
+				:confirmColor="confirmColor"
+				:cancelText="cancelText"
+				:confirmText="confirmText"
+				:title="title"
+				@cancel="cancel"
+				@confirm="confirm"
+			></u-toolbar>
+			<picker-view
+				class="u-picker__view"
+				:indicatorStyle="`height: ${$u.addUnit(itemHeight)}`"
+				:value="innerIndex"
+				:immediateChange="immediateChange"
+				:style="{
+					height: `${$u.addUnit(visibleItemCount * itemHeight)}`
+				}"
+				@change="changeHandler"
+			>
+				<picker-view-column
+					v-for="(item, index) in innerColumns"
+					:key="index"
+					class="u-picker__view__column"
+				>
+					<text
+						v-if="$u.test.array(item)"
+						class="u-picker__view__column__item u-line-1"
+						v-for="(item1, index1) in item"
+						:key="index1"
+						:style="{
+							height: $u.addUnit(itemHeight),
+							lineHeight: $u.addUnit(itemHeight),
+							fontWeight: index1 === innerIndex[index] ? 'bold' : 'normal',
+							display: 'block'
+						}"
+					>{{ getItemText(item1) }}</text>
+				</picker-view-column>
+			</picker-view>
+			<view
+				v-if="loading"
+				class="u-picker--loading"
+			>
+				<u-loading-icon mode="circle"></u-loading-icon>
+			</view>
+		</view>
+	</u-popup>
+</template>
+
+<script>
+/**
+ * u-picker
+ * @description 选择器
+ * @property {Boolean}			show				是否显示picker弹窗(默认 false )
+ * @property {Boolean}			showToolbar			是否显示顶部的操作栏(默认 true )
+ * @property {String}			title				顶部标题
+ * @property {Array}			columns				对象数组,设置每一列的数据
+ * @property {Boolean}			loading				是否显示加载中状态(默认 false )
+ * @property {String | Number}	itemHeight			各列中,单个选项的高度(默认 44 )
+ * @property {String}			cancelText			取消按钮的文字(默认 '取消' )
+ * @property {String}			confirmText			确认按钮的文字(默认 '确定' )
+ * @property {String}			cancelColor			取消按钮的颜色(默认 '#909193' )
+ * @property {String}			confirmColor		确认按钮的颜色(默认 '#3c9cff' )
+ * @property {String | Number}	visibleItemCount	每列中可见选项的数量(默认 5 )
+ * @property {String}			keyName				选项对象中,需要展示的属性键名(默认 'text' )
+ * @property {Boolean}			closeOnClickOverlay	是否允许点击遮罩关闭选择器(默认 false )
+ * @property {Array}			defaultIndex		各列的默认索引
+ * @property {Boolean}			immediateChange		是否在手指松开时立即触发change事件(默认 false )
+ * @event {Function} close		关闭选择器时触发
+ * @event {Function} cancel		点击取消按钮触发
+ * @event {Function} change		当选择值变化时触发
+ * @event {Function} confirm	点击确定按钮,返回当前选择的值
+ */
+import props from './props.js';
+export default {
+	name: 'u-picker',
+	mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+	data() {
+		return {
+			// 上一次选择的列索引
+			lastIndex: [],
+			// 索引值 ,对应picker-view的value
+			innerIndex: [],
+			// 各列的值
+			innerColumns: [],
+			// 上一次的变化列索引
+			columnIndex: 0,
+		}
+	},
+	watch: {
+		// 监听默认索引的变化,重新设置对应的值
+		defaultIndex: {
+			immediate: true,
+			handler(n) {
+				this.setIndexs(n, true)
+			}
+		},
+		// 监听columns参数的变化
+		columns: {
+			immediate: true,
+			handler(n) {
+				this.setColumns(n)
+			}
+		},
+	},
+	methods: {
+		// 获取item需要显示的文字,判别为对象还是文本
+		getItemText(item) {
+			if (uni.$u.test.object(item)) {
+				return item[this.keyName]
+			} else {
+				return item
+			}
+		},
+		// 关闭选择器
+		closeHandler() {
+			if (this.closeOnClickOverlay) {
+				this.$emit('close')
+			}
+		},
+		// 点击工具栏的取消按钮
+		cancel() {
+			this.$emit('cancel')
+		},
+		// 点击工具栏的确定按钮
+		confirm() {
+			this.$emit('confirm', {
+				indexs: this.innerIndex,
+				value: this.innerColumns.map((item, index) => item[this.innerIndex[index]]),
+				values: this.innerColumns
+			})
+		},
+		// 选择器某一列的数据发生变化时触发
+		changeHandler(e) {
+			const {
+				value
+			} = e.detail
+			let index = 0,
+				columnIndex = 0
+			// 通过对比前后两次的列索引,得出当前变化的是哪一列
+			for (let i = 0; i < value.length; i++) {
+				let item = value[i]
+				if (item !== (this.lastIndex[i] || 0)) { // 把undefined转为合法假值0
+					// 设置columnIndex为当前变化列的索引
+					columnIndex = i
+					// index则为变化列中的变化项的索引
+					index = item
+					break // 终止循环,即使少一次循环,也是性能的提升
+				}
+			}
+			this.columnIndex = columnIndex
+			const values = this.innerColumns
+			// 将当前的各项变化索引,设置为"上一次"的索引变化值
+			this.setLastIndex(value)
+			this.setIndexs(value)
+
+			this.$emit('change', {
+				// #ifndef MP-WEIXIN || MP-LARK || MP-TOUTIAO
+				// 微信小程序不能传递this,会因为循环引用而报错
+				picker: this,
+				// #endif
+				value: this.innerColumns.map((item, index) => item[value[index]]),
+				index,
+				indexs: value,
+				// values为当前变化列的数组内容
+				values,
+				columnIndex
+			})
+		},
+		// 设置index索引,此方法可被外部调用设置
+		setIndexs(index, setLastIndex) {
+			this.innerIndex = uni.$u.deepClone(index)
+			if (setLastIndex) {
+				this.setLastIndex(index)
+			}
+		},
+		// 记录上一次的各列索引位置
+		setLastIndex(index) {
+			// 当能进入此方法,意味着当前设置的各列默认索引,即为“上一次”的选中值,需要记录,是因为changeHandler中
+			// 需要拿前后的变化值进行对比,得出当前发生改变的是哪一列
+			this.lastIndex = uni.$u.deepClone(index)
+		},
+		// 设置对应列选项的所有值
+		setColumnValues(columnIndex, values) {
+			// 替换innerColumns数组中columnIndex索引的值为values,使用的是数组的splice方法
+			this.innerColumns.splice(columnIndex, 1, values)
+			// 替换完成之后将修改列之后的已选值置空
+			this.setLastIndex(this.innerIndex.slice(0,columnIndex))
+			// 拷贝一份原有的innerIndex做临时变量,将大于当前变化列的所有的列的默认索引设置为0
+			let tmpIndex = uni.$u.deepClone(this.innerIndex)
+			for (let i = 0; i < this.innerColumns.length; i++) {
+				if (i > this.columnIndex) {
+					tmpIndex[i] = 0
+				}
+			}
+			// 一次性赋值,不能单个修改,否则无效
+			this.setIndexs(tmpIndex)
+		},
+		// 获取对应列的所有选项
+		getColumnValues(columnIndex) {
+			// 进行同步阻塞,因为外部得到change事件之后,可能需要执行setColumnValues更新列的值
+			// 索引如果在外部change的回调中调用getColumnValues的话,可能无法得到变更后的列值,这里进行一定延时,保证值的准确性
+			(async () => {
+				await uni.$u.sleep()
+			})()
+			return this.innerColumns[columnIndex]
+		},
+		// 设置整体各列的columns的值
+		setColumns(columns) {
+			this.innerColumns = uni.$u.deepClone(columns)
+			// 如果在设置各列数据时,没有被设置默认的各列索引defaultIndex,那么用0去填充它,数组长度为列的数量
+			if (this.innerIndex.length === 0) {
+				this.innerIndex = new Array(columns.length).fill(0)
+			}
+		},
+		// 获取各列选中值对应的索引
+		getIndexs() {
+			return this.innerIndex
+		},
+		// 获取各列选中的值
+		getValues() {
+			// 进行同步阻塞,因为外部得到change事件之后,可能需要执行setColumnValues更新列的值
+			// 索引如果在外部change的回调中调用getValues的话,可能无法得到变更后的列值,这里进行一定延时,保证值的准确性
+			(async () => {
+				await uni.$u.sleep()
+			})()
+			return this.innerColumns.map((item, index) => item[this.innerIndex[index]])
+		}
+	},
+}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+
+	.u-picker {
+		position: relative;
+
+		&__view {
+
+			&__column {
+				@include flex;
+				flex: 1;
+				justify-content: center;
+
+				&__item {
+					@include flex;
+					justify-content: center;
+					align-items: center;
+					font-size: 16px;
+					text-align: center;
+					/* #ifndef APP-NVUE */
+					display: block;
+					/* #endif */
+					color: $u-main-color;
+
+					&--disabled {
+						/* #ifndef APP-NVUE */
+						cursor: not-allowed;
+						/* #endif */
+						opacity: 0.35;
+					}
+				}
+			}
+		}
+
+		&--loading {
+			position: absolute;
+			top: 0;
+			right: 0;
+			left: 0;
+			bottom: 0;
+			@include flex;
+			justify-content: center;
+			align-items: center;
+			background-color: rgba(255, 255, 255, 0.87);
+			z-index: 1000;
+		}
+	}
+</style>

+ 79 - 0
uni_modules/uview-ui/components/u-popup/props.js

@@ -0,0 +1,79 @@
+export default {
+    props: {
+        // 是否展示弹窗
+        show: {
+            type: Boolean,
+            default: uni.$u.props.popup.show
+        },
+        // 是否显示遮罩
+        overlay: {
+            type: Boolean,
+            default: uni.$u.props.popup.overlay
+        },
+        // 弹出的方向,可选值为 top bottom right left center
+        mode: {
+            type: String,
+            default: uni.$u.props.popup.mode
+        },
+        // 动画时长,单位ms
+        duration: {
+            type: [String, Number],
+            default: uni.$u.props.popup.duration
+        },
+        // 是否显示关闭图标
+        closeable: {
+            type: Boolean,
+            default: uni.$u.props.popup.closeable
+        },
+        // 自定义遮罩的样式
+        overlayStyle: {
+            type: [Object, String],
+            default: uni.$u.props.popup.overlayStyle
+        },
+        // 点击遮罩是否关闭弹窗
+        closeOnClickOverlay: {
+            type: Boolean,
+            default: uni.$u.props.popup.closeOnClickOverlay
+        },
+        // 层级
+        zIndex: {
+            type: [String, Number],
+            default: uni.$u.props.popup.zIndex
+        },
+        // 是否为iPhoneX留出底部安全距离
+        safeAreaInsetBottom: {
+            type: Boolean,
+            default: uni.$u.props.popup.safeAreaInsetBottom
+        },
+        // 是否留出顶部安全距离(状态栏高度)
+        safeAreaInsetTop: {
+            type: Boolean,
+            default: uni.$u.props.popup.safeAreaInsetTop
+        },
+        // 自定义关闭图标位置,top-left为左上角,top-right为右上角,bottom-left为左下角,bottom-right为右下角
+        closeIconPos: {
+            type: String,
+            default: uni.$u.props.popup.closeIconPos
+        },
+        // 是否显示圆角
+        round: {
+            type: [Boolean, String, Number],
+            default: uni.$u.props.popup.round
+        },
+        // mode=center,也即中部弹出时,是否使用缩放模式
+        zoom: {
+            type: Boolean,
+            default: uni.$u.props.popup.zoom
+        },
+        // 弹窗背景色,设置为transparent可去除白色背景
+        bgColor: {
+            type: String,
+            default: uni.$u.props.popup.bgColor
+        },
+        // 遮罩的透明度,0-1之间
+        overlayOpacity: {
+            type: [Number, String],
+            default: uni.$u.props.popup.overlayOpacity
+        }
+    }
+}

+ 304 - 0
uni_modules/uview-ui/components/u-popup/u-popup.vue

@@ -0,0 +1,304 @@
+<template>
+	<view class="u-popup">
+		<u-overlay
+			:show="show"
+			@click="overlayClick"
+			v-if="overlay"
+			:duration="overlayDuration"
+			:customStyle="overlayStyle"
+			:opacity="overlayOpacity"
+		></u-overlay>
+		<u-transition
+			:show="show"
+			:customStyle="transitionStyle"
+			:mode="position"
+			:duration="duration"
+			@afterEnter="afterEnter"
+			@click="clickHandler"
+		>
+			<view
+				class="u-popup__content"
+				:style="[contentStyle]"
+				@tap.stop="noop"
+			>
+				<u-status-bar v-if="safeAreaInsetTop"></u-status-bar>
+				<slot></slot>
+				<view
+					v-if="closeable"
+					@tap.stop="close"
+					class="u-popup__content__close"
+					:class="['u-popup__content__close--' + closeIconPos]"
+					hover-class="u-popup__content__close--hover"
+					hover-stay-time="150"
+				>
+					<u-icon
+						name="close"
+						color="#909399"
+						size="18"
+						bold
+					></u-icon>
+				</view>
+				<u-safe-bottom v-if="safeAreaInsetBottom"></u-safe-bottom>
+			</view>
+		</u-transition>
+	</view>
+</template>
+
+<script>
+	import props from './props.js';
+
+	/**
+	 * popup 弹窗
+	 * @description 弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义
+	 * @tutorial https://www.uviewui.com/components/popup.html
+	 * @property {Boolean}			show				是否展示弹窗 (默认 false )
+	 * @property {Boolean}			overlay				是否显示遮罩 (默认 true )
+	 * @property {String}			mode				弹出方向(默认 'bottom' )
+	 * @property {String | Number}	duration			动画时长,单位ms (默认 300 )
+	 * @property {String | Number}	overlayDuration			遮罩层动画时长,单位ms (默认 350 )
+	 * @property {Boolean}			closeable			是否显示关闭图标(默认 false )
+	 * @property {Object | String}	overlayStyle		自定义遮罩的样式
+	 * @property {String | Number}	overlayOpacity		遮罩透明度,0-1之间(默认 0.5)
+	 * @property {Boolean}			closeOnClickOverlay	点击遮罩是否关闭弹窗 (默认  true )
+	 * @property {String | Number}	zIndex				层级 (默认 10075 )
+	 * @property {Boolean}			safeAreaInsetBottom	是否为iPhoneX留出底部安全距离 (默认 true )
+	 * @property {Boolean}			safeAreaInsetTop	是否留出顶部安全距离(状态栏高度) (默认 false )
+	 * @property {String}			closeIconPos		自定义关闭图标位置(默认 'top-right' )
+	 * @property {String | Number}	round				圆角值(默认 0)
+	 * @property {Boolean}			zoom				当mode=center时 是否开启缩放(默认 true )
+	 * @property {Object}			customStyle			组件的样式,对象形式
+	 * @event {Function} open 弹出层打开
+	 * @event {Function} close 弹出层收起
+	 * @example <u-popup v-model="show"><text>出淤泥而不染,濯清涟而不妖</text></u-popup>
+	 */
+	export default {
+		name: 'u-popup',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		data() {
+			return {
+				overlayDuration: this.duration + 50
+			}
+		},
+		watch: {
+			show(newValue, oldValue) {
+				if (newValue === true) {
+					// #ifdef MP-WEIXIN
+					const children = this.$children
+					this.retryComputedComponentRect(children)
+					// #endif
+				}
+			}
+		},
+		computed: {
+			transitionStyle() {
+				const style = {
+					zIndex: this.zIndex,
+					position: 'fixed',
+					display: 'flex',
+				}
+				style[this.mode] = 0
+				if (this.mode === 'left') {
+					return uni.$u.deepMerge(style, {
+						bottom: 0,
+						top: 0,
+					})
+				} else if (this.mode === 'right') {
+					return uni.$u.deepMerge(style, {
+						bottom: 0,
+						top: 0,
+					})
+				} else if (this.mode === 'top') {
+					return uni.$u.deepMerge(style, {
+						left: 0,
+						right: 0
+					})
+				} else if (this.mode === 'bottom') {
+					return uni.$u.deepMerge(style, {
+						left: 0,
+						right: 0,
+					})
+				} else if (this.mode === 'center') {
+					return uni.$u.deepMerge(style, {
+						alignItems: 'center',
+						'justify-content': 'center',
+						top: 0,
+						left: 0,
+						right: 0,
+						bottom: 0
+					})
+				}
+			},
+			contentStyle() {
+				const style = {}
+				// 通过设备信息的safeAreaInsets值来判断是否需要预留顶部状态栏和底部安全局的位置
+				// 不使用css方案,是因为nvue不支持css的iPhoneX安全区查询属性
+				const {
+					safeAreaInsets
+				} = uni.$u.sys()
+				if (this.mode !== 'center') {
+					style.flex = 1
+				}
+				// 背景色,一般用于设置为transparent,去除默认的白色背景
+				if (this.bgColor) {
+					style.backgroundColor = this.bgColor
+				}
+				if(this.round) {
+					const value = uni.$u.addUnit(this.round)
+					if(this.mode === 'top') {
+						style.borderBottomLeftRadius = value
+						style.borderBottomRightRadius = value
+					} else if(this.mode === 'bottom') {
+						style.borderTopLeftRadius = value
+						style.borderTopRightRadius = value
+					} else if(this.mode === 'center') {
+						style.borderRadius = value
+					} 
+				}
+				return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
+			},
+			position() {
+				if (this.mode === 'center') {
+					return this.zoom ? 'fade-zoom' : 'fade'
+				}
+				if (this.mode === 'left') {
+					return 'slide-left'
+				}
+				if (this.mode === 'right') {
+					return 'slide-right'
+				}
+				if (this.mode === 'bottom') {
+					return 'slide-up'
+				}
+				if (this.mode === 'top') {
+					return 'slide-down'
+				}
+			},
+		},
+		methods: {
+			// 点击遮罩
+			overlayClick() {
+				if (this.closeOnClickOverlay) {
+					this.$emit('close')
+				}
+			},
+			close(e) {
+				this.$emit('close')
+			},
+			afterEnter() {
+				this.$emit('open')
+			},
+			clickHandler() {
+				// 由于中部弹出时,其u-transition占据了整个页面相当于遮罩,此时需要发出遮罩点击事件,是否无法通过点击遮罩关闭弹窗
+				if(this.mode === 'center') {
+					this.overlayClick()
+				}
+				this.$emit('click')
+			},
+			// #ifdef MP-WEIXIN
+			retryComputedComponentRect(children) {
+				// 组件内部需要计算节点的组件
+				const names = ['u-calendar-month', 'u-album', 'u-collapse-item', 'u-dropdown', 'u-index-item', 'u-index-list',
+					'u-line-progress', 'u-list-item', 'u-rate', 'u-read-more', 'u-row', 'u-row-notice', 'u-scroll-list',
+					'u-skeleton', 'u-slider', 'u-steps-item', 'u-sticky', 'u-subsection', 'u-swipe-action-item', 'u-tabbar',
+					'u-tabs', 'u-tooltip'
+				]
+				// 历遍所有的子组件节点
+				for (let i = 0; i < children.length; i++) {
+					const child = children[i]
+					// 拿到子组件的子组件
+					const grandChild = child.$children
+					// 判断如果在需要重新初始化的组件数组中名中,并且存在init方法的话,则执行
+					if (names.includes(child.$options.name) && typeof child?.init === 'function') {
+						// 需要进行一定的延时,因为初始化页面需要时间
+						uni.$u.sleep(50).then(() => {
+							child.init()
+						})
+					}
+					// 如果子组件还有孙组件,进行递归历遍
+					if (grandChild.length) {
+						this.retryComputedComponentRect(grandChild)
+					}
+				}
+			}
+			// #endif
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+	$u-popup-flex:1 !default;
+	$u-popup-content-background-color: #fff !default;
+
+	.u-popup {
+		flex: $u-popup-flex;
+
+		&__content {
+			background-color: $u-popup-content-background-color;
+			position: relative;
+
+			&--round-top {
+				border-top-left-radius: 0;
+				border-top-right-radius: 0;
+				border-bottom-left-radius: 10px;
+				border-bottom-right-radius: 10px;
+			}
+
+			&--round-left {
+				border-top-left-radius: 0;
+				border-top-right-radius: 10px;
+				border-bottom-left-radius: 0;
+				border-bottom-right-radius: 10px;
+			}
+
+			&--round-right {
+				border-top-left-radius: 10px;
+				border-top-right-radius: 0;
+				border-bottom-left-radius: 10px;
+				border-bottom-right-radius: 0;
+			}
+
+			&--round-bottom {
+				border-top-left-radius: 10px;
+				border-top-right-radius: 10px;
+				border-bottom-left-radius: 0;
+				border-bottom-right-radius: 0;
+			}
+
+			&--round-center {
+				border-top-left-radius: 10px;
+				border-top-right-radius: 10px;
+				border-bottom-left-radius: 10px;
+				border-bottom-right-radius: 10px;
+			}
+
+			&__close {
+				position: absolute;
+
+				&--hover {
+					opacity: 0.4;
+				}
+			}
+
+			&__close--top-left {
+				top: 15px;
+				left: 15px;
+			}
+
+			&__close--top-right {
+				top: 15px;
+				right: 15px;
+			}
+
+			&__close--bottom-left {
+				bottom: 15px;
+				left: 15px;
+			}
+
+			&__close--bottom-right {
+				right: 15px;
+				bottom: 15px;
+			}
+		}
+	}
+</style>

+ 85 - 0
uni_modules/uview-ui/components/u-radio-group/props.js

@@ -0,0 +1,85 @@
+export default {
+    props: {
+        // 绑定的值
+        value: {
+            type: [String, Number, Boolean],
+            default: uni.$u.props.radioGroup.value
+        },
+
+        // 是否禁用全部radio
+        disabled: {
+            type: Boolean,
+            default: uni.$u.props.radioGroup.disabled
+        },
+        // 形状,circle-圆形,square-方形
+        shape: {
+            type: String,
+            default: uni.$u.props.radioGroup.shape
+        },
+        // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值
+        activeColor: {
+            type: String,
+            default: uni.$u.props.radioGroup.activeColor
+        },
+        // 未选中的颜色
+        inactiveColor: {
+            type: String,
+            default: uni.$u.props.radioGroup.inactiveColor
+        },
+        // 标识符
+        name: {
+            type: String,
+            default: uni.$u.props.radioGroup.name
+        },
+        // 整个组件的尺寸,默认px
+        size: {
+            type: [String, Number],
+            default: uni.$u.props.radioGroup.size
+        },
+        // 布局方式,row-横向,column-纵向
+        placement: {
+            type: String,
+            default: uni.$u.props.radioGroup.placement
+        },
+        // label的文本
+        label: {
+            type: [String],
+            default: uni.$u.props.radioGroup.label
+        },
+        // label的颜色 (默认 '#303133' )
+        labelColor: {
+            type: [String],
+            default: uni.$u.props.radioGroup.labelColor
+        },
+        // label的字体大小,px单位
+        labelSize: {
+            type: [String, Number],
+            default: uni.$u.props.radioGroup.labelSize
+        },
+        // 是否禁止点击文本操作checkbox(默认 false )
+        labelDisabled: {
+            type: Boolean,
+            default: uni.$u.props.radioGroup.labelDisabled
+        },
+        // 图标颜色
+        iconColor: {
+            type: String,
+            default: uni.$u.props.radioGroup.iconColor
+        },
+        // 图标的大小,单位px
+        iconSize: {
+            type: [String, Number],
+            default: uni.$u.props.radioGroup.iconSize
+        },
+        // 竖向配列时,是否显示下划线
+        borderBottom: {
+            type: Boolean,
+            default: uni.$u.props.radioGroup.borderBottom
+        },
+        // 图标与文字的对齐方式
+        iconPlacement: {
+            type: String,
+            default: uni.$u.props.radio.iconPlacement
+        }
+    }
+}

+ 108 - 0
uni_modules/uview-ui/components/u-radio-group/u-radio-group.vue

@@ -0,0 +1,108 @@
+<template>
+	<view
+	    class="u-radio-group"
+	    :class="bemClass"
+	>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	import props from './props.js';
+
+	/**
+	 * radioRroup 单选框父组件
+	 * @description 单选框用于有一个选择,用户只能选择其中一个的场景。搭配u-radio使用
+	 * @tutorial https://www.uviewui.com/components/radio.html
+	 * @property {String | Number | Boolean}	value 			绑定的值
+	 * @property {Boolean}						disabled		是否禁用所有radio(默认 false )
+	 * @property {String}						shape			外观形状,shape-方形,circle-圆形(默认 circle )
+	 * @property {String}						activeColor		选中时的颜色,应用到所有子Radio组件(默认 '#2979ff' )
+	 * @property {String}						inactiveColor	未选中的颜色 (默认 '#c8c9cc' )
+	 * @property {String}						name			标识符
+	 * @property {String | Number}				size			组件整体的大小,单位px(默认 18 )
+	 * @property {String}						placement		布局方式,row-横向,column-纵向 (默认 'row' )
+	 * @property {String}						label			文本
+	 * @property {String}						labelColor		label的颜色 (默认 '#303133' )
+	 * @property {String | Number}				labelSize		label的字体大小,px单位 (默认 14 )
+	 * @property {Boolean}						labelDisabled	是否禁止点击文本操作checkbox(默认 false )
+	 * @property {String}						iconColor		图标颜色 (默认 '#ffffff' )
+	 * @property {String | Number}				iconSize		图标的大小,单位px (默认 12 )
+	 * @property {Boolean}						borderBottom	placement为row时,是否显示下边框 (默认 false )
+	 * @property {String}						iconPlacement	图标与文字的对齐方式 (默认 'left' )
+     * @property {Object}						customStyle		组件的样式,对象形式
+	 * @event {Function} change 任一个radio状态发生变化时触发
+	 * @example <u-radio-group v-model="value"></u-radio-group>
+	 */
+	export default {
+		name: 'u-radio-group',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
+		computed: {
+			// 这里computed的变量,都是子组件u-radio需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
+			// 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(u-radio-group)
+			// 拉取父组件新的变化后的参数
+			parentData() {
+				return [this.value, this.disabled, this.inactiveColor, this.activeColor, this.size, this.labelDisabled, this.shape,
+					this.iconSize, this.borderBottom, this.placement
+				]
+			},
+			bemClass() {
+				// this.bem为一个computed变量,在mixin中
+				return this.bem('radio-group', ['placement'])
+			},
+		},
+		watch: {
+			// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
+			parentData() {
+				if (this.children.length) {
+					this.children.map(child => {
+						// 判断子组件(u-radio)如果有init方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+						typeof(child.init) === 'function' && child.init()
+					})
+				}
+			},
+		},
+		data() {
+			return {
+
+			}
+		},
+		created() {
+			this.children = []
+		},
+		methods: {
+			// 将其他的radio设置为未选中的状态
+			unCheckedOther(childInstance) {
+				this.children.map(child => {
+					// 所有子radio中,被操作组件实例的checked的值无需修改
+					if (childInstance !== child) {
+						child.checked = false
+					}
+				})
+				const {
+					name
+				} = childInstance
+				// 通过emit事件,设置父组件通过v-model双向绑定的值
+				this.$emit('input', name)
+				// 发出事件
+				this.$emit('change', name)
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+
+	.u-radio-group {
+		flex: 1;
+
+		&--row {
+			@include flex;
+		}
+
+		&--column {
+			@include flex(column);
+		}
+	}
+</style>

+ 64 - 0
uni_modules/uview-ui/components/u-radio/props.js

@@ -0,0 +1,64 @@
+export default {
+    props: {
+        // radio的名称
+        name: {
+            type: [String, Number, Boolean],
+            default: uni.$u.props.radio.name
+        },
+        // 形状,square为方形,circle为圆型
+        shape: {
+            type: String,
+            default: uni.$u.props.radio.shape
+        },
+        // 是否禁用
+        disabled: {
+            type: [String, Boolean],
+            default: uni.$u.props.radio.disabled
+        },
+        // 是否禁止点击提示语选中单选框
+        labelDisabled: {
+            type: [String, Boolean],
+            default: uni.$u.props.radio.labelDisabled
+        },
+        // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值
+        activeColor: {
+            type: String,
+            default: uni.$u.props.radio.activeColor
+        },
+        // 未选中的颜色
+        inactiveColor: {
+            type: String,
+            default: uni.$u.props.radio.inactiveColor
+        },
+        // 图标的大小,单位px
+        iconSize: {
+            type: [String, Number],
+            default: uni.$u.props.radio.iconSize
+        },
+        // label的字体大小,px单位
+        labelSize: {
+            type: [String, Number],
+            default: uni.$u.props.radio.labelSize
+        },
+        // label提示文字,因为nvue下,直接slot进来的文字,由于特殊的结构,无法修改样式
+        label: {
+            type: [String, Number],
+            default: uni.$u.props.radio.label
+        },
+        // 整体的大小
+        size: {
+            type: [String, Number],
+            default: uni.$u.props.radio.size
+        },
+        // 图标颜色
+        color: {
+            type: String,
+            default: uni.$u.props.radio.color
+        },
+        // label的颜色
+        labelColor: {
+            type: String,
+            default: uni.$u.props.radio.labelColor
+        }
+    }
+}

+ 339 - 0
uni_modules/uview-ui/components/u-radio/u-radio.vue

@@ -0,0 +1,339 @@
+<template>
+	<view
+	    class="u-radio"
+		@tap.stop="wrapperClickHandler"
+	    :style="[radioStyle]"
+	    :class="[`u-radio-label--${parentData.iconPlacement}`, parentData.borderBottom && parentData.placement === 'column' && 'u-border-bottom']"
+	>
+		<view
+		    class="u-radio__icon-wrap"
+		    @tap.stop="iconClickHandler"
+		    :class="iconClasses"
+		    :style="[iconWrapStyle]"
+		>
+			<slot name="icon">
+				<u-icon
+				    class="u-radio__icon-wrap__icon"
+				    name="checkbox-mark"
+				    :size="elIconSize"
+				    :color="elIconColor"
+				/>
+			</slot>
+		</view>
+		<slot>
+			<text
+				class="u-radio__text"
+				@tap.stop="labelClickHandler"
+				:style="{
+					color: elDisabled ? elInactiveColor : elLabelColor,
+					fontSize: elLabelSize,
+					lineHeight: elLabelSize
+				}"
+			>{{label}}</text>
+		</slot>
+	</view>
+</template>
+
+<script>
+	import props from './props.js';
+	/**
+	 * radio 单选框
+	 * @description 单选框用于有一个选择,用户只能选择其中一个的场景。搭配u-radio-group使用
+	 * @tutorial https://www.uviewui.com/components/radio.html
+	 * @property {String | Number}	name			radio的名称
+	 * @property {String}			shape			形状,square为方形,circle为圆型
+	 * @property {Boolean}			disabled		是否禁用
+	 * @property {String | Boolean}	labelDisabled	是否禁止点击提示语选中单选框
+	 * @property {String}			activeColor		选中时的颜色,如设置parent的active-color将失效
+	 * @property {String}			inactiveColor	未选中的颜色
+	 * @property {String | Number}	iconSize		图标大小,单位px
+	 * @property {String | Number}	labelSize		label字体大小,单位px
+	 * @property {String | Number}	label			label提示文字,因为nvue下,直接slot进来的文字,由于特殊的结构,无法修改样式
+	 * @property {String | Number}	size			整体的大小
+	 * @property {String}			iconColor		图标颜色
+	 * @property {String}			labelColor		label的颜色
+	 * @property {Object}			customStyle		组件的样式,对象形式
+	 * 
+	 * @event {Function} change 某个radio状态发生变化时触发(选中状态)
+	 * @example <u-radio :labelDisabled="false">门掩黄昏,无计留春住</u-radio>
+	 */
+	export default {
+		name: "u-radio",
+		
+		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
+		data() {
+			return {
+				checked: false,
+				// 当你看到这段代码的时候,
+				// 父组件的默认值,因为头条小程序不支持在computed中使用this.parent.shape的形式
+				// 故只能使用如此方法
+				parentData: {
+					iconSize: 12,
+					labelDisabled: null,
+					disabled: null,
+					shape: null,
+					activeColor: null,
+					inactiveColor: null,
+					size: 18,
+					value: null,
+					iconColor: null,
+					placement: 'row',
+					borderBottom: false,
+					iconPlacement: 'left'
+				}
+			}
+		},
+		computed: {
+			// 是否禁用,如果父组件u-raios-group禁用的话,将会忽略子组件的配置
+			elDisabled() {
+				return this.disabled !== '' ? this.disabled : this.parentData.disabled !== null ? this.parentData.disabled : false;
+			},
+			// 是否禁用label点击
+			elLabelDisabled() {
+				return this.labelDisabled !== '' ? this.labelDisabled : this.parentData.labelDisabled !== null ? this.parentData.labelDisabled :
+					false;
+			},
+			// 组件尺寸,对应size的值,默认值为21px
+			elSize() {
+				return this.size ? this.size : (this.parentData.size ? this.parentData.size : 21);
+			},
+			// 组件的勾选图标的尺寸,默认12px
+			elIconSize() {
+				return this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 12);
+			},
+			// 组件选中激活时的颜色
+			elActiveColor() {
+				return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : '#2979ff');
+			},
+			// 组件选未中激活时的颜色
+			elInactiveColor() {
+				return this.inactiveColor ? this.inactiveColor : (this.parentData.inactiveColor ? this.parentData.inactiveColor :
+					'#c8c9cc');
+			},
+			// label的颜色
+			elLabelColor() {
+				return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : '#606266')
+			},
+			// 组件的形状
+			elShape() {
+				return this.shape ? this.shape : (this.parentData.shape ? this.parentData.shape : 'circle');
+			},
+			// label大小
+			elLabelSize() {
+				return uni.$u.addUnit(this.labelSize ? this.labelSize : (this.parentData.labelSize ? this.parentData.labelSize :
+					'15'))
+			},
+			elIconColor() {
+				const iconColor = this.iconColor ? this.iconColor : (this.parentData.iconColor ? this.parentData.iconColor :
+					'#ffffff');
+				// 图标的颜色
+				if (this.elDisabled) {
+					// disabled状态下,已勾选的radio图标改为elInactiveColor
+					return this.checked ? this.elInactiveColor : 'transparent'
+				} else {
+					return this.checked ? iconColor : 'transparent'
+				}
+			},
+			iconClasses() {
+				let classes = []
+				// 组件的形状
+				classes.push('u-radio__icon-wrap--' + this.elShape)
+				if (this.elDisabled) {
+					classes.push('u-radio__icon-wrap--disabled')
+				}
+				if (this.checked && this.elDisabled) {
+					classes.push('u-radio__icon-wrap--disabled--checked')
+				}
+				// 支付宝,头条小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
+				// #ifdef MP-ALIPAY || MP-TOUTIAO
+				classes = classes.join(' ')
+				// #endif
+				return classes
+			},
+			iconWrapStyle() {
+				// radio的整体样式
+				const style = {}
+				style.backgroundColor = this.checked && !this.elDisabled ? this.elActiveColor : '#ffffff'
+				style.borderColor = this.checked && !this.elDisabled ? this.elActiveColor : this.elInactiveColor
+				style.width = uni.$u.addUnit(this.elSize)
+				style.height = uni.$u.addUnit(this.elSize)
+				// 如果是图标在右边的话,移除它的右边距
+				if (this.parentData.iconPlacement === 'right') {
+					style.marginRight = 0
+				}
+				return style
+			},
+			radioStyle() {
+				const style = {}
+				if(this.parentData.borderBottom && this.parentData.placement === 'row') {
+					uni.$u.error('检测到您将borderBottom设置为true,需要同时将u-radio-group的placement设置为column才有效')
+				}
+				// 当父组件设置了显示下边框并且排列形式为纵向时,给内容和边框之间加上一定间隔
+				if(this.parentData.borderBottom && this.parentData.placement === 'column') {
+					// ios像素密度高,需要多一点的距离
+					style.paddingBottom = uni.$u.os() === 'ios' ? '12px' : '8px'
+				}
+				return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
+			}
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			init() {
+				// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
+				this.updateParentData()
+				if (!this.parent) {
+					uni.$u.error('u-radio必须搭配u-radio-group组件使用')
+				}
+				// 设置初始化时,是否默认选中的状态
+				this.checked = this.name === this.parentData.value
+			},
+			updateParentData() {
+				this.getParentData('u-radio-group')
+			},
+			// 点击图标
+			iconClickHandler(e) {
+				this.preventEvent(e)
+				// 如果整体被禁用,不允许被点击
+				if (!this.elDisabled) {
+					this.setRadioCheckedStatus()
+				}
+			},
+			// 横向两端排列时,点击组件即可触发选中事件
+			wrapperClickHandler(e) {
+				this.parentData.iconPlacement === 'right' && this.iconClickHandler(e)
+			},
+			// 点击label
+			labelClickHandler(e) {
+				this.preventEvent(e)
+				// 如果按钮整体被禁用或者label被禁用,则不允许点击文字修改状态
+				if (!this.elLabelDisabled && !this.elDisabled) {
+					this.setRadioCheckedStatus()
+				}
+			},
+			emitEvent() {
+				// u-radio的checked不为true时(意味着未选中),才发出事件,避免多次点击触发事件
+				if (!this.checked) {
+					this.$emit('change', this.name)
+					// 尝试调用u-form的验证方法,进行一定延迟,否则微信小程序更新可能会不及时
+					this.$nextTick(() => {
+						uni.$u.formValidate(this, 'change')
+					})
+				}
+			},
+			// 改变组件选中状态
+			// 这里的改变的依据是,更改本组件的checked值为true,同时通过父组件遍历所有u-radio实例
+			// 将本组件外的其他u-radio的checked都设置为false(都被取消选中状态),因而只剩下一个为选中状态
+			setRadioCheckedStatus() {
+				this.emitEvent()
+				// 将本组件标记为选中状态
+				this.checked = true
+				typeof this.parent.unCheckedOther === 'function' && this.parent.unCheckedOther(this)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+	$u-radio-wrap-margin-right:6px !default;
+	$u-radio-wrap-font-size:20px !default;
+	$u-radio-wrap-border-width:1px !default;
+	$u-radio-wrap-border-color: #c8c9cc !default;
+	$u-radio-line-height:0 !default;
+	$u-radio-circle-border-radius:100% !default;
+	$u-radio-square-border-radius:3px !default;
+	$u-radio-checked-color:#fff !default;
+	$u-radio-checked-background-color:red !default;
+	$u-radio-checked-border-color: #2979ff !default;
+	$u-radio-disabled-background-color:#ebedf0 !default;
+	$u-radio-disabled--checked-color:#c8c9cc !default;
+	$u-radio-label-margin-left: 5px !default;
+	$u-radio-label-margin-right:12px !default;
+	$u-radio-label-color:$u-content-color !default;
+	$u-radio-label-font-size:15px !default;
+	$u-radio-label-disabled-color:#c8c9cc !default;
+	
+	.u-radio {
+		/* #ifndef APP-NVUE */
+		@include flex(row);
+		/* #endif */
+		overflow: hidden;
+		flex-direction: row;
+		align-items: center;
+
+		&-label--left {
+			flex-direction: row
+		}
+
+		&-label--right {
+			flex-direction: row-reverse;
+			justify-content: space-between
+		}
+
+		&__icon-wrap {
+			/* #ifndef APP-NVUE */
+			box-sizing: border-box;
+			// nvue下,border-color过渡有问题
+			transition-property: border-color, background-color, color;
+			transition-duration: 0.2s;
+			/* #endif */
+			color: $u-content-color;
+			@include flex;
+			align-items: center;
+			justify-content: center;
+			color: transparent;
+			text-align: center;
+			margin-right: $u-radio-wrap-margin-right;
+			font-size: $u-radio-wrap-font-size;
+			border-width: $u-radio-wrap-border-width;
+			border-color: $u-radio-wrap-border-color;
+			border-style: solid;
+
+			/* #ifdef MP-TOUTIAO */
+			// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
+			&__icon {
+				line-height: $u-radio-line-height;
+			}
+
+			/* #endif */
+
+			&--circle {
+				border-radius: $u-radio-circle-border-radius;
+			}
+
+			&--square {
+				border-radius: $u-radio-square-border-radius;
+			}
+
+			&--checked {
+				color: $u-radio-checked-color;
+				background-color: $u-radio-checked-background-color;
+				border-color: $u-radio-checked-border-color;
+			}
+
+			&--disabled {
+				background-color: $u-radio-disabled-background-color !important;
+			}
+
+			&--disabled--checked {
+				color: $u-radio-disabled--checked-color !important;
+			}
+		}
+
+		&__label {
+			/* #ifndef APP-NVUE */
+			word-wrap: break-word;
+			/* #endif */
+			margin-left: $u-radio-label-margin-left;
+			margin-right: $u-radio-label-margin-right;
+			color: $u-radio-label-color;
+			font-size: $u-radio-label-font-size;
+
+			&--disabled {
+				color: $u-radio-label-disabled-color;
+			}
+		}
+	}
+</style>

+ 69 - 0
uni_modules/uview-ui/components/u-rate/props.js

@@ -0,0 +1,69 @@
+export default {
+    props: {
+        // 用于v-model双向绑定选中的星星数量
+        value: {
+            type: [String, Number],
+            default: uni.$u.props.rate.value
+        },
+        // 要显示的星星数量
+        count: {
+            type: [String, Number],
+            default: uni.$u.props.rate.count
+        },
+        // 是否不可选中
+        disabled: {
+            type: Boolean,
+            default: uni.$u.props.rate.disabled
+        },
+        // 是否只读
+        readonly: {
+            type: Boolean,
+            default: uni.$u.props.rate.readonly
+        },
+        // 星星的大小,单位px
+        size: {
+            type: [String, Number],
+            default: uni.$u.props.rate.size
+        },
+        // 未选中时的颜色
+        inactiveColor: {
+            type: String,
+            default: uni.$u.props.rate.inactiveColor
+        },
+        // 选中的颜色
+        activeColor: {
+            type: String,
+            default: uni.$u.props.rate.activeColor
+        },
+        // 星星之间的间距,单位px
+        gutter: {
+            type: [String, Number],
+            default: uni.$u.props.rate.gutter
+        },
+        // 最少能选择的星星个数
+        minCount: {
+            type: [String, Number],
+            default: uni.$u.props.rate.minCount
+        },
+        // 是否允许半星
+        allowHalf: {
+            type: Boolean,
+            default: uni.$u.props.rate.allowHalf
+        },
+        // 选中时的图标(星星)
+        activeIcon: {
+            type: String,
+            default: uni.$u.props.rate.activeIcon
+        },
+        // 未选中时的图标(星星)
+        inactiveIcon: {
+            type: String,
+            default: uni.$u.props.rate.inactiveIcon
+        },
+        // 是否可以通过滑动手势选择评分
+        touchable: {
+            type: Boolean,
+            default: uni.$u.props.rate.touchable
+        }
+    }
+}

+ 306 - 0
uni_modules/uview-ui/components/u-rate/u-rate.vue

@@ -0,0 +1,306 @@
+<template>
+    <view
+        class="u-rate"
+        :id="elId"
+        ref="u-rate"
+        :style="[$u.addStyle(customStyle)]"
+    >
+        <view
+            class="u-rate__content"
+            @touchmove.stop="touchMove"
+            @touchend.stop="touchEnd"
+        >
+            <view
+                class="u-rate__content__item"
+                v-for="(item, index) in Number(count)"
+                :key="index"
+                :class="[elClass]"
+            >
+                <view
+                    class="u-rate__content__item__icon-wrap"
+                    ref="u-rate__content__item__icon-wrap"
+                    @tap.stop="clickHandler($event, index + 1)"
+                >
+                    <u-icon
+                        :name="
+                            Math.floor(activeIndex) > index
+                                ? activeIcon
+                                : inactiveIcon
+                        "
+                        :color="
+                            disabled
+                                ? '#c8c9cc'
+                                : Math.floor(activeIndex) > index
+                                ? activeColor
+                                : inactiveColor
+                        "
+                        :custom-style="{
+                            'padding-left': $u.addUnit(gutter / 2),
+							'padding-right': $u.addUnit(gutter / 2)
+                        }"
+                        :size="size"
+                    ></u-icon>
+                </view>
+                <view
+                    v-if="allowHalf"
+                    @tap.stop="clickHandler($event, index + 1)"
+                    class="u-rate__content__item__icon-wrap u-rate__content__item__icon-wrap--half"
+                    :style="[{
+                        width: $u.addUnit(rateWidth / 2),
+                    }]"
+                    ref="u-rate__content__item__icon-wrap"
+                >
+                    <u-icon
+                        :name="
+                            Math.ceil(activeIndex) > index
+                                ? activeIcon
+                                : inactiveIcon
+                        "
+                        :color="
+                            disabled
+                                ? '#c8c9cc'
+                                : Math.ceil(activeIndex) > index
+                                ? activeColor
+                                : inactiveColor
+                        "
+                        :custom-style="{
+							'padding-left': $u.addUnit(gutter / 2),
+							'padding-right': $u.addUnit(gutter / 2)
+                        }"
+                        :size="size"
+                    ></u-icon>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script>
+	import props from './props.js';
+
+	// #ifdef APP-NVUE
+	const dom = weex.requireModule("dom");
+	// #endif
+	/**
+	 * rate 评分
+	 * @description 该组件一般用于满意度调查,星型评分的场景
+	 * @tutorial https://www.uviewui.com/components/rate.html
+	 * @property {String | Number}	value			用于v-model双向绑定选中的星星数量 (默认 1 )
+	 * @property {String | Number}	count			最多可选的星星数量 (默认 5 )
+	 * @property {Boolean}			disabled		是否禁止用户操作 (默认 false )
+	 * @property {Boolean}			readonly		是否只读 (默认 false )
+	 * @property {String | Number}	size			星星的大小,单位px (默认 18 )
+	 * @property {String}			inactiveColor	未选中星星的颜色 (默认 '#b2b2b2' )
+	 * @property {String}			activeColor		选中的星星颜色 (默认 '#FA3534' )
+	 * @property {String | Number}	gutter			星星之间的距离 (默认 4 )
+	 * @property {String | Number}	minCount		最少选中星星的个数 (默认 1 )
+	 * @property {Boolean}			allowHalf		是否允许半星选择 (默认 false )
+	 * @property {String}			activeIcon		选中时的图标名,只能为uView的内置图标 (默认 'star-fill' )
+	 * @property {String}			inactiveIcon	未选中时的图标名,只能为uView的内置图标 (默认 'star' )
+	 * @property {Boolean}			touchable		是否可以通过滑动手势选择评分 (默认 'true' )
+	 * @property {Object}			customStyle		组件的样式,对象形式
+	 * @event {Function} change 选中的星星发生变化时触发
+	 * @example <u-rate :count="count" :value="2"></u-rate>
+	 */
+	export default {
+		name: "u-rate",
+		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
+		data() {
+			return {
+				// 生成一个唯一id,否则一个页面多个评分组件,会造成冲突
+				elId: uni.$u.guid(),
+				elClass: uni.$u.guid(),
+				rateBoxLeft: 0, // 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
+				activeIndex: this.value,
+				rateWidth: 0, // 每个星星的宽度
+				// 标识是否正在滑动,由于iOS事件上touch比click先触发,导致快速滑动结束后,接着触发click,导致事件混乱而出错
+				moving: false,
+			};
+		},
+		watch: {
+			value(val) {
+				this.activeIndex = val;
+			},
+			activeIndex: 'emitEvent'
+		},
+		methods: {
+			init() {
+				uni.$u.sleep().then(() => {
+					this.getRateItemRect();
+					this.getRateIconWrapRect();
+				})
+			},
+			// 获取评分组件盒子的布局信息
+			async getRateItemRect() {
+				await uni.$u.sleep();
+				// uView封装的获取节点的方法,详见文档
+				// #ifndef APP-NVUE
+				this.$uGetRect("#" + this.elId).then((res) => {
+					this.rateBoxLeft = res.left;
+				});
+				// #endif
+				// #ifdef APP-NVUE
+				dom.getComponentRect(this.$refs["u-rate"], (res) => {
+					this.rateBoxLeft = res.size.left;
+				});
+				// #endif
+			},
+			// 获取单个星星的尺寸
+			getRateIconWrapRect() {
+				// uView封装的获取节点的方法,详见文档
+				// #ifndef APP-NVUE
+				this.$uGetRect("." + this.elClass).then((res) => {
+					this.rateWidth = res.width;
+				});
+				// #endif
+				// #ifdef APP-NVUE
+				dom.getComponentRect(
+					this.$refs["u-rate__content__item__icon-wrap"][0],
+					(res) => {
+						this.rateWidth = res.size.width;
+					}
+				);
+				// #endif
+			},
+			// 手指滑动
+			touchMove(e) {
+				// 如果禁止通过手动滑动选择,返回
+				if (!this.touchable) {
+					return;
+				}
+				this.preventEvent(e);
+				const x = e.changedTouches[0].pageX;
+				this.getActiveIndex(x);
+			},
+			// 停止滑动
+			touchEnd(e) {
+				// 如果禁止通过手动滑动选择,返回
+				if (!this.touchable) {
+					return;
+				}
+				this.preventEvent(e);
+				const x = e.changedTouches[0].pageX;
+				this.getActiveIndex(x);
+			},
+			// 通过点击,直接选中
+			clickHandler(e, index) {
+				// ios上,moving状态取消事件触发
+				if (uni.$u.os() === "ios" && this.moving) {
+					return;
+				}
+				this.preventEvent(e);
+				let x = 0;
+				// 点击时,在nvue上,无法获得点击的坐标,所以无法实现点击半星选择
+				// #ifndef APP-NVUE
+				x = e.changedTouches[0].pageX;
+				// #endif
+				// #ifdef APP-NVUE
+				// nvue下,无法通过点击获得坐标信息,这里通过元素的位置尺寸值模拟坐标
+				x = index * this.rateWidth + this.rateBoxLeft;
+				// #endif
+				this.getActiveIndex(x,true);
+			},
+			// 发出事件
+			emitEvent() {
+				// 发出change事件
+				this.$emit("change", this.activeIndex);
+				// 同时修改双向绑定的value的值
+				this.$emit("input", this.activeIndex);
+			},
+			// 获取当前激活的评分图标
+			getActiveIndex(x,isClick = false) {
+				if (this.disabled || this.readonly) {
+					return;
+				}
+				// 判断当前操作的点的x坐标值,是否在允许的边界范围内
+				const allRateWidth = this.rateWidth * this.count + this.rateBoxLeft;
+				// 如果小于第一个图标的左边界,设置为最小值,如果大于所有图标的宽度,则设置为最大值
+				x = uni.$u.range(this.rateBoxLeft, allRateWidth, x) - this.rateBoxLeft
+				// 滑动点相对于评分盒子左边的距离
+				const distance = x;
+				// 滑动的距离,相当于多少颗星星
+				let index;
+				// 判断是否允许半星
+				if (this.allowHalf) {
+					index = Math.floor(distance / this.rateWidth);
+					// 取余,判断小数的区间范围
+					const decimal = distance % this.rateWidth;
+					if (decimal <= this.rateWidth / 2 && decimal > 0) {
+						index += 0.5;
+					} else if (decimal > this.rateWidth / 2) {
+						index++;
+					}
+				} else {
+					index = Math.floor(distance / this.rateWidth);
+					// 取余,判断小数的区间范围
+					const decimal = distance % this.rateWidth;
+					// 非半星时,只有超过了图标的一半距离,才认为是选择了这颗星
+					if (isClick){
+						if (decimal > 0) index++;
+					} else {
+						if (decimal > this.rateWidth / 2) index++;
+					}
+
+				}
+				this.activeIndex = Math.min(index, this.count);
+				// 对最少颗星星的限制
+				if (this.activeIndex < this.minCount) {
+					this.activeIndex = this.minCount;
+				}
+
+				// 设置延时为了让click事件在touchmove之前触发
+				setTimeout(() => {
+					this.moving = true;
+				}, 10);
+				// 一定时间后,取消标识为移动中状态,是为了让click事件无效
+				setTimeout(() => {
+					this.moving = false;
+				}, 10);
+			},
+		},
+		mounted() {
+			this.init();
+		},
+	};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/components.scss";
+$u-rate-margin: 0 !default;
+$u-rate-padding: 0 !default;
+$u-rate-item-icon-wrap-half-top: 0 !default;
+$u-rate-item-icon-wrap-half-left: 0 !default;
+
+.u-rate {
+    @include flex;
+    align-items: center;
+    margin: $u-rate-margin;
+    padding: $u-rate-padding;
+    /* #ifndef APP-NVUE */
+    touch-action: none;
+    /* #endif */
+
+    &__content {
+        @include flex;
+
+		&__item {
+		    position: relative;
+
+		    &__icon-wrap {
+		        &--half {
+		            position: absolute;
+		            overflow: hidden;
+		            top: $u-rate-item-icon-wrap-half-top;
+		            left: $u-rate-item-icon-wrap-half-left;
+		        }
+		    }
+		}
+    }
+}
+
+.u-icon {
+    /* #ifndef APP-NVUE */
+    box-sizing: border-box;
+    /* #endif */
+}
+</style>

+ 61 - 0
uni_modules/uview-ui/components/u-read-more/props.js

@@ -0,0 +1,61 @@
+export default {
+    props: {
+        // 默认的显示占位高度
+        showHeight: {
+            type: [String, Number],
+            default: uni.$u.props.readMore.showHeight
+        },
+        // 展开后是否显示"收起"按钮
+        toggle: {
+            type: Boolean,
+            default: uni.$u.props.readMore.toggle
+        },
+        // 关闭时的提示文字
+        closeText: {
+            type: String,
+            default: uni.$u.props.readMore.closeText
+        },
+        // 展开时的提示文字
+        openText: {
+            type: String,
+            default: uni.$u.props.readMore.openText
+        },
+        // 提示的文字颜色
+        color: {
+            type: String,
+            default: uni.$u.props.readMore.color
+        },
+        // 提示文字的大小
+        fontSize: {
+            type: [String, Number],
+            default: uni.$u.props.readMore.fontSize
+        },
+        // 是否显示阴影
+        // 此参数不能写在props/readMore.js中进行默认配置,因为使用了条件编译,在外部js中
+        // uni无法准确识别当前是否处于nvue还是非nvue下
+        shadowStyle: {
+            type: Object,
+            default: () => ({
+                // #ifndef APP-NVUE
+                backgroundImage: 'linear-gradient(-180deg, rgba(255, 255, 255, 0) 0%, #fff 80%)',
+                // #endif
+                // #ifdef APP-NVUE
+                // nvue上不支持设置复杂的backgroundImage属性
+                backgroundImage: 'linear-gradient(to top, #fff, rgba(255, 255, 255, 0.5))',
+                // #endif
+                paddingTop: '100px',
+                marginTop: '-100px'
+            })
+        },
+        // 段落首行缩进的字符个数
+        textIndent: {
+            type: String,
+            default: uni.$u.props.readMore.textIndent
+        },
+        // open和close事件时,将此参数返回在回调参数中
+        name: {
+            type: [String, Number],
+            default: uni.$u.props.readMore.name
+        }
+    }
+}

+ 157 - 0
uni_modules/uview-ui/components/u-read-more/u-read-more.vue

@@ -0,0 +1,157 @@
+<template>
+	<view class="u-read-more">
+		<view
+		    class="u-read-more__content"
+		    :style="{
+				height: isLongContent && status === 'close' ? $u.addUnit(showHeight) : $u.addUnit(contentHeight),
+				textIndent: textIndent
+			}"
+		>
+			<view
+			    class="u-read-more__content__inner"
+			    ref="u-read-more__content__inner"
+			    :class="[elId]"
+			>
+				<slot></slot>
+			</view>
+		</view>
+		<view
+		    class="u-read-more__toggle"
+		    :style="[innerShadowStyle]"
+		    v-if="isLongContent"
+		>
+			<slot name="toggle">
+				<view
+				    class="u-read-more__toggle__text"
+				    @tap="toggleReadMore"
+				>
+					<u--text
+					    :text="status === 'close' ? closeText : openText"
+					    :color="color"
+					    :size="fontSize"
+					    :lineHeight="fontSize"
+					    margin="0 5px 0 0"
+					></u--text>
+					<view class="u-read-more__toggle__icon">
+						<u-icon
+						    :color="color"
+						    :size="fontSize + 2"
+						    :name="status === 'close' ? 'arrow-down' : 'arrow-up'"
+						></u-icon>
+					</view>
+				</view>
+			</slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	// #ifdef APP-NVUE
+	const dom = uni.requireNativePlugin('dom')
+	// #endif
+	import props from './props.js';
+	/**
+	 * readMore 阅读更多
+	 * @description 该组件一般用于内容较长,预先收起一部分,点击展开全部内容的场景。
+	 * @tutorial https://www.uviewui.com/components/readMore.html
+	 * @property {String | Number}	showHeight	内容超出此高度才会显示展开全文按钮,单位px(默认 400 )
+	 * @property {Boolean}			toggle		展开后是否显示收起按钮(默认 false )
+	 * @property {String}			closeText	关闭时的提示文字(默认 '展开阅读全文' )
+	 * @property {String}			openText	展开时的提示文字(默认 '收起' )
+	 * @property {String}			color		提示文字的颜色(默认 '#2979ff' )
+	 * @property {String | Number}	fontSize	提示文字的大小,单位px (默认 14 )
+	 * @property {Object}			shadowStyle	显示阴影的样式
+	 * @property {String}			textIndent	段落首行缩进的字符个数 (默认 '2em' )
+	 * @property {String | Number}	name		用于在 open 和 close 事件中当作回调参数返回
+	 * @event {Function} open 内容被展开时触发
+	 * @event {Function} close 内容被收起时触发
+	 * @example <u-read-more><rich-text :nodes="content"></rich-text></u-read-more>
+	 */
+	export default {
+		name: 'u-read-more',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		data() {
+			return {
+				isLongContent: false, // 是否需要隐藏一部分内容
+				status: 'close', // 当前隐藏与显示的状态,close-收起状态,open-展开状态
+				elId: uni.$u.guid(), // 生成唯一class
+				contentHeight: 100, // 内容高度
+			}
+		},
+		computed: {
+			// 展开后无需阴影,收起时才需要阴影样式
+			innerShadowStyle() {
+				if (this.status === 'open') return {}
+				else return this.shadowStyle
+			}
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			async init() {
+				this.getContentHeight().then(height => {
+					this.contentHeight = height
+					// 判断高度,如果真实内容高度大于占位高度,则显示收起与展开的控制按钮
+					if (height > uni.$u.getPx(this.showHeight)) {
+						this.isLongContent = true
+						this.status = 'close'
+					}
+				})
+			},
+			// 获取内容的高度
+			async getContentHeight() {
+				// 延时一定时间再获取节点
+				await uni.$u.sleep(30)
+				return new Promise(resolve => {
+					// #ifndef APP-NVUE
+					this.$uGetRect('.' + this.elId).then(res => {
+						resolve(res.height)
+					})
+					// #endif
+
+					// #ifdef APP-NVUE
+					const ref = this.$refs['u-read-more__content__inner']
+					dom.getComponentRect(ref, (res) => {
+						resolve(res.size.height)
+					})
+					// #endif
+				})
+			},
+			// 展开或者收起
+			toggleReadMore() {
+				this.status = this.status === 'close' ? 'open' : 'close'
+				// 如果toggle为false,隐藏"收起"部分的内容
+				if (this.toggle == false) this.isLongContent = false
+				// 发出打开或者收齐的事件
+				this.$emit(this.status, this.name)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/components.scss";
+
+.u-read-more {
+
+	&__content {
+		overflow: hidden;
+		color: $u-content-color;
+		font-size: 15px;
+		text-align: left;
+	}
+
+	&__toggle {
+		@include flex;
+		justify-content: center;
+
+		&__text {
+			@include flex;
+			align-items: center;
+			justify-content: center;
+			margin-top: 5px;
+		}
+	}
+}
+</style>

+ 39 - 0
uni_modules/uview-ui/components/u-row-notice/props.js

@@ -0,0 +1,39 @@
+export default {
+    props: {
+        // 显示的内容,字符串
+        text: {
+            type: String,
+            default: uni.$u.props.rowNotice.text
+        },
+        // 是否显示左侧的音量图标
+        icon: {
+            type: String,
+            default: uni.$u.props.rowNotice.icon
+        },
+        // 通告模式,link-显示右箭头,closable-显示右侧关闭图标
+        mode: {
+            type: String,
+            default: uni.$u.props.rowNotice.mode
+        },
+        // 文字颜色,各图标也会使用文字颜色
+        color: {
+            type: String,
+            default: uni.$u.props.rowNotice.color
+        },
+        // 背景颜色
+        bgColor: {
+            type: String,
+            default: uni.$u.props.rowNotice.bgColor
+        },
+        // 字体大小,单位px
+        fontSize: {
+            type: [String, Number],
+            default: uni.$u.props.rowNotice.fontSize
+        },
+        // 水平滚动时的滚动速度,即每秒滚动多少px(rpx),这有利于控制文字无论多少时,都能有一个恒定的速度
+        speed: {
+            type: [String, Number],
+            default: uni.$u.props.rowNotice.speed
+        }
+    }
+}

+ 330 - 0
uni_modules/uview-ui/components/u-row-notice/u-row-notice.vue

@@ -0,0 +1,330 @@
+<template>
+	<view
+		class="u-notice"
+		@tap="clickHandler"
+	>
+		<slot name="icon">
+			<view
+				class="u-notice__left-icon"
+				v-if="icon"
+			>
+				<u-icon
+					:name="icon"
+					:color="color"
+					size="19"
+				></u-icon>
+			</view>
+		</slot>
+		<view
+			class="u-notice__content"
+			ref="u-notice__content"
+		>
+			<view
+				ref="u-notice__content__text"
+				class="u-notice__content__text"
+				:style="[animationStyle]"
+			>
+				<text
+					v-for="(item, index) in innerText"
+					:key="index"
+					:style="[textStyle]"
+				>{{item}}</text>
+			</view>
+		</view>
+		<view
+			class="u-notice__right-icon"
+			v-if="['link', 'closable'].includes(mode)"
+		>
+			<u-icon
+				v-if="mode === 'link'"
+				name="arrow-right"
+				:size="17"
+				:color="color"
+			></u-icon>
+			<u-icon
+				v-if="mode === 'closable'"
+				@click="close"
+				name="close"
+				:size="16"
+				:color="color"
+			></u-icon>
+		</view>
+	</view>
+</template>
+<script>
+	import props from './props.js';
+	// #ifdef APP-NVUE
+	const animation = uni.requireNativePlugin('animation')
+	const dom = uni.requireNativePlugin('dom')
+	// #endif
+	/**
+	 * RowNotice 滚动通知中的水平滚动模式
+	 * @description 水平滚动
+	 * @tutorial https://www.uviewui.com/components/noticeBar.html
+	 * @property {String | Number}	text			显示的内容,字符串
+	 * @property {String}			icon			是否显示左侧的音量图标 (默认 'volume' )
+	 * @property {String}			mode			通告模式,link-显示右箭头,closable-显示右侧关闭图标
+	 * @property {String}			color			文字颜色,各图标也会使用文字颜色 (默认 '#f9ae3d' )
+	 * @property {String}			bgColor			背景颜色 (默认 ''#fdf6ec' )
+	 * @property {String | Number}	fontSize		字体大小,单位px (默认 14 )
+	 * @property {String | Number}	speed			水平滚动时的滚动速度,即每秒滚动多少px(rpx),这有利于控制文字无论多少时,都能有一个恒定的速度  (默认 80 )
+	 * 
+	 * @event {Function} click 点击通告文字触发
+	 * @event {Function} close 点击右侧关闭图标触发
+	 * @example 
+	 */
+	export default {
+		name: 'u-row-notice',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
+		data() {
+			return {
+				animationDuration: '0', // 动画执行时间
+				animationPlayState: 'paused', // 动画的开始和结束执行
+				// nvue下,内容发生变化,导致滚动宽度也变化,需要标志为是否需要重新计算宽度
+				// 不能在内容变化时直接重新计算,因为nvue的animation模块上一次的滚动不是刚好结束,会有影响
+				nvueInit: true,
+				show: true
+			};
+		},
+		watch: {
+			text: {
+				immediate: true,
+				handler(newValue, oldValue) {
+					// #ifdef APP-NVUE
+					this.nvueInit = true
+					// #endif
+					// #ifndef APP-NVUE
+					this.vue()
+					// #endif
+					
+					if(!uni.$u.test.string(newValue)) {
+						uni.$u.error('noticebar组件direction为row时,要求text参数为字符串形式')
+					}
+				}
+			},
+			fontSize() {
+				// #ifdef APP-NVUE
+				this.nvueInit = true
+				// #endif
+				// #ifndef APP-NVUE
+				this.vue()
+				// #endif
+			},
+			speed() {
+				// #ifdef APP-NVUE
+				this.nvueInit = true
+				// #endif
+				// #ifndef APP-NVUE
+				this.vue()
+				// #endif
+			}
+		},
+		computed: {
+			// 文字内容的样式
+			textStyle() {
+				let style = {}
+				style.color = this.color
+				style.fontSize = uni.$u.addUnit(this.fontSize)
+				return style
+			},
+			animationStyle() {
+				let style = {}
+				style.animationDuration = this.animationDuration
+				style.animationPlayState = this.animationPlayState
+				return style
+			},
+			// 内部对用户传入的数据进一步分割,放到多个text标签循环,否则如果用户传入的字符串很长(100个字符以上)
+			// 放在一个text标签中进行滚动,在低端安卓机上,动画可能会出现抖动现象,需要分割到多个text中可解决此问题
+			innerText() {
+				let result = [],
+					// 每组text标签的字符长度
+					len = 20
+				const textArr = this.text.split('')
+				for (let i = 0; i < textArr.length; i += len) {
+					// 对拆分的后的text进行slice分割,得到的为数组再进行join拼接为字符串
+					result.push(textArr.slice(i, i + len).join(''))
+				}
+				return result
+			}
+		},
+		mounted() {
+			// #ifdef APP-PLUS
+			// 在APP上(含nvue),监听当前webview是否处于隐藏状态(进入下一页时即为hide状态)
+			// 如果webivew隐藏了,为了节省性能的损耗,应停止动画的执行,同时也是为了保持进入下一页返回后,滚动位置保持不变
+			var pages = getCurrentPages()
+			var page = pages[pages.length - 1]
+			var currentWebview = page.$getAppWebview()
+			currentWebview.addEventListener('hide', () => {
+				this.webviewHide = true
+			})
+			currentWebview.addEventListener('show', () => {
+				this.webviewHide = false
+			})
+			// #endif
+
+			this.init()
+		},
+		methods: {
+			init() {
+				// #ifdef APP-NVUE
+				this.nvue()
+				// #endif
+
+				// #ifndef APP-NVUE
+				this.vue()
+				// #endif
+				
+				if(!uni.$u.test.string(this.text)) {
+					uni.$u.error('noticebar组件direction为row时,要求text参数为字符串形式')
+				}
+			},
+			// vue版处理
+			async vue() {
+				// #ifndef APP-NVUE
+				let boxWidth = 0,
+					textWidth = 0
+				// 进行一定的延时
+				await uni.$u.sleep()
+				// 查询盒子和文字的宽度
+				textWidth = (await this.$uGetRect('.u-notice__content__text')).width
+				boxWidth = (await this.$uGetRect('.u-notice__content')).width
+				// 根据t=s/v(时间=路程/速度),这里为何不需要加上#u-notice-box的宽度,因为中设置了.u-notice-content样式中设置了padding-left: 100%
+				// 恰巧计算出来的结果中已经包含了#u-notice-box的宽度
+				this.animationDuration = `${textWidth / uni.$u.getPx(this.speed)}s`
+				// 这里必须这样开始动画,否则在APP上动画速度不会改变
+				this.animationPlayState = 'paused'
+				setTimeout(() => {
+					this.animationPlayState = 'running'
+				}, 10)
+				// #endif
+			},
+			// nvue版处理
+			async nvue() {
+				// #ifdef APP-NVUE
+				this.nvueInit = false
+				let boxWidth = 0,
+					textWidth = 0
+				// 进行一定的延时
+				await uni.$u.sleep()
+				// 查询盒子和文字的宽度
+				textWidth = (await this.getNvueRect('u-notice__content__text')).width
+				boxWidth = (await this.getNvueRect('u-notice__content')).width
+				// 将文字移动到盒子的右边沿,之所以需要这么做,是因为nvue不支持100%单位,否则可以通过css设置
+				animation.transition(this.$refs['u-notice__content__text'], {
+					styles: {
+						transform: `translateX(${boxWidth}px)`
+					},
+				}, () => {
+					// 如果非禁止动画,则开始滚动
+					!this.stopAnimation && this.loopAnimation(textWidth, boxWidth)
+				});
+				// #endif
+			},
+			loopAnimation(textWidth, boxWidth) {
+				// #ifdef APP-NVUE
+				animation.transition(this.$refs['u-notice__content__text'], {
+					styles: {
+						// 目标移动终点为-textWidth,也即当文字的最右边贴到盒子的左边框的位置
+						transform: `translateX(-${textWidth}px)`
+					},
+					// 滚动时间的计算为,时间 = 路程(boxWidth + textWidth) / 速度,最后转为毫秒
+					duration: (boxWidth + textWidth) / uni.$u.getPx(this.speed) * 1000,
+					delay: 10
+				}, () => {
+					animation.transition(this.$refs['u-notice__content__text'], {
+						styles: {
+							// 重新将文字移动到盒子的右边沿
+							transform: `translateX(${this.stopAnimation ? 0 : boxWidth}px)`
+						},
+					}, () => {
+						// 如果非禁止动画,则继续下一轮滚动
+						if (!this.stopAnimation) {
+							// 判断是否需要初始化计算尺寸
+							if (this.nvueInit) {
+								this.nvue()
+							} else {
+								this.loopAnimation(textWidth, boxWidth)
+							}
+						}
+					});
+				})
+				// #endif
+			},
+			getNvueRect(el) {
+				// #ifdef APP-NVUE
+				// 返回一个promise
+				return new Promise(resolve => {
+					dom.getComponentRect(this.$refs[el], (res) => {
+						resolve(res.size)
+					})
+				})
+				// #endif
+			},
+			// 点击通告栏
+			clickHandler(index) {
+				this.$emit('click')
+			},
+			// 点击右侧按钮,需要判断点击的是关闭图标还是箭头图标
+			close() {
+				this.$emit('close')
+			}
+		},
+		// #ifdef APP-NVUE
+		beforeDestroy() {
+			this.stopAnimation = true
+		},
+		// #endif
+	};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+
+	.u-notice {
+		@include flex;
+		align-items: center;
+		justify-content: space-between;
+
+		&__left-icon {
+			align-items: center;
+			margin-right: 5px;
+		}
+
+		&__right-icon {
+			margin-left: 5px;
+			align-items: center;
+		}
+
+		&__content {
+			text-align: right;
+			flex: 1;
+			@include flex;
+			flex-wrap: nowrap;
+			overflow: hidden;
+
+			&__text {
+				font-size: 14px;
+				color: $u-warning;
+				/* #ifndef APP-NVUE */
+				// 这一句很重要,为了能让滚动左右连接起来
+				padding-left: 100%;
+				word-break: keep-all;
+				white-space: nowrap;
+				animation: u-loop-animation 10s linear infinite both;
+				/* #endif */
+				@include flex(row);
+			}
+		}
+
+	}
+
+	@keyframes u-loop-animation {
+		0% {
+			transform: translate3d(0, 0, 0);
+		}
+
+		100% {
+			transform: translate3d(-100%, 0, 0);
+		}
+	}
+</style>

+ 19 - 0
uni_modules/uview-ui/components/u-row/props.js

@@ -0,0 +1,19 @@
+export default {
+    props: {
+        // 给col添加间距,左右边距各占一半
+        gutter: {
+            type: [String, Number],
+            default: uni.$u.props.row.gutter
+        },
+        // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
+        justify: {
+            type: String,
+            default: uni.$u.props.row.justify
+        },
+        // 垂直对齐方式,可选值为top、center、bottom
+        align: {
+            type: String,
+            default: uni.$u.props.row.align
+        }
+    }
+}

+ 93 - 0
uni_modules/uview-ui/components/u-row/u-row.vue

@@ -0,0 +1,93 @@
+<template>
+	<view
+	    class="u-row"
+		ref="u-row"
+	    :style="[rowStyle]"
+	    @tap="clickHandler"
+	>
+		<slot />
+	</view>
+</template>
+
+<script>
+	// #ifdef APP-NVUE
+	const dom = uni.requireNativePlugin('dom')
+	// #endif
+	import props from './props.js';
+	/**
+	 * Row 栅格系统中的行
+	 * @description 通过基础的 12 分栏,迅速简便地创建布局 
+	 * @tutorial https://www.uviewui.com/components/layout.html
+	 * @property {String | Number}	gutter		栅格间隔,左右各为此值的一半,单位px  (默认 0 )
+	 * @property {String}			justify		水平排列方式(微信小程序暂不支持) 可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)  (默认 'start' )
+	 * @property {String}			align		垂直排列方式 (默认 'center' )
+	 * @property {Object}			customStyle	定义需要用到的外部样式
+	 * 
+	 * @event {Function} click row被点击
+	 * @example <u-row justify="space-between" customStyle="margin-bottom: 10px"></u-row>
+	 */
+	export default {
+		name: "u-row",
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		data() {
+			return {
+				
+			}
+		},
+		computed: {
+			uJustify() {
+				if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify
+				else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify
+				else return this.justify
+			},
+			uAlignItem() {
+				if (this.align == 'top') return 'flex-start'
+				if (this.align == 'bottom') return 'flex-end'
+				else return this.align
+			},
+			rowStyle() {
+				const style = {
+					alignItems: this.uAlignItem,
+					justifyContent: this.uJustify
+				}
+				// 通过给u-row左右两边的负外边距,消除u-col在有gutter时,第一个和最后一个元素的左内边距和右内边距造成的影响
+				if(this.gutter) {
+					style.marginLeft = uni.$u.addUnit(-Number(this.gutter)/2)
+					style.marginRight = uni.$u.addUnit(-Number(this.gutter)/2)
+				}
+				return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
+			}
+		},
+		methods: {
+			clickHandler(e) {
+				this.$emit('click')
+			},
+			async getComponentWidth() {
+				// 延时一定时间,以确保节点渲染完成
+				await uni.$u.sleep()
+				return new Promise(resolve => {
+					// uView封装的获取节点的方法,详见文档
+					// #ifndef APP-NVUE
+					this.$uGetRect('.u-row').then(res => {
+						resolve(res.width)
+					})
+					// #endif
+					// #ifdef APP-NVUE
+					// nvue的dom模块用于获取节点
+					dom.getComponentRect(this.$refs['u-row'], (res) => {
+						resolve(res.size.width)
+					})
+					// #endif
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+	
+	.u-row {
+		@include flex;
+	}
+</style>

+ 5 - 0
uni_modules/uview-ui/components/u-safe-bottom/props.js

@@ -0,0 +1,5 @@
+export default {
+    props: {
+
+    }
+}

+ 56 - 0
uni_modules/uview-ui/components/u-safe-bottom/u-safe-bottom.vue

@@ -0,0 +1,56 @@
+<template>
+	<view
+		class="u-safe-bottom"
+		:style="[style]"
+		:class="[!isNvue && 'u-safe-area-inset-bottom']"
+	>
+	</view>
+</template>
+
+<script>
+	import props from "./props.js";
+	/**
+	 * SafeBottom 底部安全区
+	 * @description 这个适配,主要是针对IPhone X等一些底部带指示条的机型,指示条的操作区域与页面底部存在重合,容易导致用户误操作,因此我们需要针对这些机型进行底部安全区适配。
+	 * @tutorial https://www.uviewui.com/components/safeAreaInset.html
+	 * @property {type}		prop_name
+	 * @property {Object}	customStyle	定义需要用到的外部样式
+	 *
+	 * @event {Function()}
+	 * @example <u-status-bar></u-status-bar>
+	 */
+	export default {
+		name: "u-safe-bottom",
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		data() {
+			return {
+				safeAreaBottomHeight: 0,
+				isNvue: false,
+			};
+		},
+		computed: {
+			style() {
+				const style = {};
+				// #ifdef APP-NVUE
+				// nvue下,高度使用js计算填充
+				style.height = uni.$u.addUnit(uni.$u.sys().safeAreaInsets.bottom, 'px');
+				// #endif
+				return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle));
+			},
+		},
+		mounted() {
+			// #ifdef APP-NVUE
+			// 标识为是否nvue
+			this.isNvue = true;
+			// #endif
+		},
+	};
+</script>
+
+<style lang="scss" scoped>
+	.u-safe-bottom {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		/* #endif */
+	}
+</style>

+ 28 - 0
uni_modules/uview-ui/components/u-scroll-list/nvue.js

@@ -0,0 +1,28 @@
+// 引入bindingx,此库类似于微信小程序wxs,目的是让js运行在视图层,减少视图层和逻辑层的通信折损
+const BindingX = uni.requireNativePlugin('bindingx')
+
+export default {
+    methods: {
+        // 此处不写注释,请自行体会
+        nvueScrollHandler(e) {
+            const anchor = this.$refs['u-scroll-list__scroll-view'].ref
+            const element = this.$refs['u-scroll-list__indicator__line__bar'].ref
+            const scrollLeft = e.contentOffset.x
+            const contentSize = e.contentSize.width
+            const { scrollWidth } = this
+            const barAllMoveWidth = this.indicatorWidth - this.indicatorBarWidth
+            // 在安卓和iOS上,需要除的倍数不一样,iOS需要除以2
+            const actionNum = uni.$u.os() === 'ios' ? 2 : 1
+            const expression = `(x / ${actionNum}) / ${contentSize - scrollWidth} * ${barAllMoveWidth}`
+            BindingX.bind({
+                anchor,
+                eventType: 'scroll',
+                props: [{
+                    element,
+                    property: 'transform.translateX',
+                    expression
+                }]
+            })
+        }
+    }
+}

+ 0 - 0
uni_modules/uview-ui/components/u-scroll-list/other.js


+ 34 - 0
uni_modules/uview-ui/components/u-scroll-list/props.js

@@ -0,0 +1,34 @@
+export default {
+    props: {
+        // 指示器的整体宽度
+        indicatorWidth: {
+            type: [String, Number],
+            default: uni.$u.props.scrollList.indicatorWidth
+        },
+        // 滑块的宽度
+        indicatorBarWidth: {
+            type: [String, Number],
+            default: uni.$u.props.scrollList.indicatorBarWidth
+        },
+        // 是否显示面板指示器
+        indicator: {
+            type: Boolean,
+            default: uni.$u.props.scrollList.indicator
+        },
+        // 指示器非激活颜色
+        indicatorColor: {
+            type: String,
+            default: uni.$u.props.scrollList.indicatorColor
+        },
+        // 指示器的激活颜色
+        indicatorActiveColor: {
+            type: String,
+            default: uni.$u.props.scrollList.indicatorActiveColor
+        },
+        // 指示器样式,可通过bottom,left,right进行定位
+        indicatorStyle: {
+            type: [String, Object],
+            default: uni.$u.props.scrollList.indicatorStyle
+        }
+    }
+}

+ 50 - 0
uni_modules/uview-ui/components/u-scroll-list/scrollWxs.wxs

@@ -0,0 +1,50 @@
+function scroll(event, ownerInstance) {
+	// detail中含有scroll-view的信息,比如scroll-view的实际宽度,当前时间点scroll-view的移动距离等
+	var detail = event.detail
+	var scrollWidth = detail.scrollWidth
+	var scrollLeft = detail.scrollLeft
+	// 获取当前组件的dataset,说白了就是祸国殃民的腾xun搞出来的垃ji
+	var dataset = event.currentTarget.dataset
+	// 此为scroll-view外部包裹元素的宽度
+	// 某些HX版本(3.1.18),发现view元素中大写的data-scrollWidth,在wxs中,变成了全部小写,所以这里需要特别处理
+	var scrollComponentWidth = dataset.scrollWidth || dataset.scrollwidth || 0
+	// 指示器和滑块的宽度
+	var indicatorWidth = dataset.indicatorWidth || dataset.indicatorwidth || 0
+	var barWidth = dataset.barWidth || dataset.barwidth || 0
+	// 此处的计算理由为:scroll-view的滚动距离与目标滚动距离(scroll-view的实际宽度减去包裹元素的宽度)之比,等于滑块当前移动距离与总需
+	// 滑动距离(指示器的总宽度减去滑块宽度)的比值
+	var x = scrollLeft / (scrollWidth - scrollComponentWidth) * (indicatorWidth - barWidth)
+	setBarStyle(ownerInstance, x)
+}
+
+// 由于webview的无能,无法保证scroll-view在滑动过程中,一直触发scroll事件,会导致
+// 无法监听到某些滚动值,当在首尾临界值无法监听到时,这是致命的,因为错失这些值会导致滑块无法回到起点和终点
+// 所以这里需要对临界值做监听并处理
+function scrolltolower(event, ownerInstance) {
+	ownerInstance.callMethod('scrollEvent', 'right')
+	// 获取当前组件的dataset
+	var dataset = event.currentTarget.dataset
+	// 指示器和滑块的宽度
+	var indicatorWidth = dataset.indicatorWidth || dataset.indicatorwidth || 0
+	var barWidth = dataset.barWidth || dataset.barwidth || 0
+	// scroll-view滚动到右边终点时,将滑块也设置为到右边的终点,它所需移动的距离为:指示器宽度 - 滑块宽度
+	setBarStyle(ownerInstance, indicatorWidth - barWidth)
+}
+
+function scrolltoupper(event, ownerInstance) {
+	ownerInstance.callMethod('scrollEvent', 'left')
+	// 滚动到左边时,将滑块设置为0的偏移距离,回到起点
+	setBarStyle(ownerInstance, 0)
+}
+
+function setBarStyle(ownerInstance, x) {
+	ownerInstance.selectComponent('.u-scroll-list__indicator__line__bar') && ownerInstance.selectComponent('.u-scroll-list__indicator__line__bar').setStyle({
+		transform: 'translateX(' + x + 'px)'
+	})
+}
+
+module.exports = {
+	scroll: scroll,
+	scrolltolower: scrolltolower,
+	scrolltoupper: scrolltoupper
+}

+ 224 - 0
uni_modules/uview-ui/components/u-scroll-list/u-scroll-list.vue

@@ -0,0 +1,224 @@
+<template>
+	<view
+		class="u-scroll-list"
+		ref="u-scroll-list"
+	>
+		<!-- #ifdef APP-NVUE -->
+		<!-- nvue使用bindingX实现,以得到更好的性能 -->
+		<scroller
+			class="u-scroll-list__scroll-view"
+			ref="u-scroll-list__scroll-view"
+			scroll-direction="horizontal"
+			:show-scrollbar="false"
+			:offset-accuracy="1"
+			@scroll="nvueScrollHandler"
+		>
+			<view class="u-scroll-list__scroll-view__content">
+				<slot />
+			</view>
+		</scroller>
+		<!-- #endif -->
+		<!-- #ifndef APP-NVUE -->
+		<!-- #ifdef MP-WEIXIN || APP-VUE || H5 || MP-QQ -->
+		<!-- 以上平台,支持wxs -->
+		<scroll-view
+			class="u-scroll-list__scroll-view"
+			scroll-x
+			@scroll="wxs.scroll"
+			@scrolltoupper="wxs.scrolltoupper"
+			@scrolltolower="wxs.scrolltolower"
+			:data-scrollWidth="scrollWidth"
+			:data-barWidth="$u.getPx(indicatorBarWidth)"
+			:data-indicatorWidth="$u.getPx(indicatorWidth)"
+			:show-scrollbar="false"
+			:upper-threshold="0"
+			:lower-threshold="0"
+		>
+			<!-- #endif -->
+			<!-- #ifndef APP-NVUE || MP-WEIXIN || H5 || APP-VUE || MP-QQ -->
+			<!-- 非以上平台,只能使用普通js实现 -->
+			<scroll-view
+				class="u-scroll-list__scroll-view"
+				scroll-x
+				@scroll="scrollHandler"
+				@scrolltoupper="scrolltoupperHandler"
+				@scrolltolower="scrolltolowerHandler"
+				:show-scrollbar="false"
+				:upper-threshold="0"
+				:lower-threshold="0"
+			>
+				<!-- #endif -->
+				<view class="u-scroll-list__scroll-view__content">
+					<slot />
+				</view>
+			</scroll-view>
+			<!-- #endif -->
+			<view
+				class="u-scroll-list__indicator"
+				v-if="indicator"
+				:style="[$u.addStyle(indicatorStyle)]"
+			>
+				<view
+					class="u-scroll-list__indicator__line"
+					:style="[lineStyle]"
+				>
+					<view
+						class="u-scroll-list__indicator__line__bar"
+						:style="[barStyle]"
+						ref="u-scroll-list__indicator__line__bar"
+					></view>
+				</view>
+			</view>
+	</view>
+</template>
+
+<script
+	src="./scrollWxs.wxs"
+	module="wxs"
+	lang="wxs"
+></script>
+
+<script>
+/**
+ * scrollList 横向滚动列表
+ * @description 该组件一般用于同时展示多个商品、分类的场景,也可以完成左右滑动的列表。
+ * @tutorial https://www.uviewui.com/components/scrollList.html
+ * @property {String | Number}	indicatorWidth			指示器的整体宽度 (默认 50 )
+ * @property {String | Number}	indicatorBarWidth		滑块的宽度 (默认 20 )
+ * @property {Boolean}			indicator				是否显示面板指示器 (默认 true )
+ * @property {String}			indicatorColor			指示器非激活颜色 (默认 '#f2f2f2' )
+ * @property {String}			indicatorActiveColor	指示器的激活颜色 (默认 '#3c9cff' )
+ * @property {String | Object}	indicatorStyle			指示器样式,可通过bottom,left,right进行定位
+ * @event {Function} left	滑动到左边时触发
+ * @event {Function} right	滑动到右边时触发
+ * @example
+ */
+// #ifdef APP-NVUE
+const dom = uni.requireNativePlugin('dom')
+import nvueMixin from "./nvue.js"
+// #endif
+import props from './props.js';
+export default {
+	name: 'u-scroll-list',
+	mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+	// #ifdef APP-NVUE
+	mixins: [uni.$u.mpMixin, uni.$u.mixin, nvueMixin, props],
+	// #endif
+	data() {
+		return {
+			scrollInfo: {
+				scrollLeft: 0,
+				scrollWidth: 0
+			},
+			scrollWidth: 0
+		}
+	},
+	computed: {
+		// 指示器为线型的样式
+		barStyle() {
+			const style = {}
+			// #ifndef APP-NVUE || MP-WEIXIN || H5 || APP-VUE || MP-QQ
+			// 此为普通js方案,只有在非nvue和不支持wxs方案的端才使用、
+			// 此处的计算理由为:scroll-view的滚动距离与目标滚动距离(scroll-view的实际宽度减去包裹元素的宽度)之比,等于滑块当前移动距离与总需
+			// 滑动距离(指示器的总宽度减去滑块宽度)的比值
+			const scrollLeft = this.scrollInfo.scrollLeft,
+				scrollWidth = this.scrollInfo.scrollWidth,
+				barAllMoveWidth = this.indicatorWidth - this.indicatorBarWidth
+			const x = scrollLeft / (scrollWidth - this.scrollWidth) * barAllMoveWidth
+			style.transform = `translateX(${ x }px)`
+			// #endif
+			// 设置滑块的宽度和背景色,是每个平台都需要的
+			style.width = uni.$u.addUnit(this.indicatorBarWidth)
+			style.backgroundColor = this.indicatorActiveColor
+			return style
+		},
+		lineStyle() {
+			const style = {}
+			// 指示器整体的样式,需要设置其宽度和背景色
+			style.width = uni.$u.addUnit(this.indicatorWidth)
+			style.backgroundColor = this.indicatorColor
+			return style
+		}
+	},
+	mounted() {
+		this.init()
+	},
+	methods: {
+		init() {
+			this.getComponentWidth()
+		},
+		// #ifndef APP-NVUE || MP-WEIXIN || H5 || APP-VUE || MP-QQ
+		// scroll-view触发滚动事件
+		scrollHandler(e) {
+			this.scrollInfo = e.detail
+		},
+		scrolltoupperHandler() {
+			this.scrollEvent('left')
+			this.scrollInfo.scrollLeft = 0
+		},
+		scrolltolowerHandler() {
+			this.scrollEvent('right')
+			// 在普通js方案中,滚动到右边时,通过设置this.scrollInfo,模拟出滚动到右边的情况
+			// 因为上方是用过computed计算的,设置后,会自动调整滑块的位置
+			this.scrollInfo.scrollLeft = uni.$u.getPx(this.indicatorWidth) - uni.$u.getPx(this.indicatorBarWidth)
+		},
+		// #endif
+		//
+		scrollEvent(status) {
+			this.$emit(status)
+		},
+		// 获取组件的宽度
+		async getComponentWidth() {
+			// 延时一定时间,以获取dom尺寸
+			await uni.$u.sleep(30)
+			// #ifndef APP-NVUE
+			this.$uGetRect('.u-scroll-list').then(size => {
+				this.scrollWidth = size.width
+			})
+			// #endif
+
+			// #ifdef APP-NVUE
+			const ref = this.$refs['u-scroll-list']
+			ref && dom.getComponentRect(ref, (res) => {
+				this.scrollWidth = res.size.width
+			})
+			// #endif
+		},
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/components.scss";
+
+.u-scroll-list {
+	padding-bottom: 10px;
+
+	&__scroll-view {
+		@include flex;
+
+		&__content {
+			@include flex;
+		}
+	}
+
+	&__indicator {
+		@include flex;
+		justify-content: center;
+		margin-top: 15px;
+
+		&__line {
+			width: 60px;
+			height: 4px;
+			border-radius: 100px;
+			overflow: hidden;
+
+			&__bar {
+				width: 20px;
+				height: 4px;
+				border-radius: 100px;
+			}
+		}
+	}
+}
+</style>

+ 118 - 0
uni_modules/uview-ui/components/u-search/props.js

@@ -0,0 +1,118 @@
+export default {
+    props: {
+        // 搜索框形状,round-圆形,square-方形
+        shape: {
+            type: String,
+            default: uni.$u.props.search.shape
+        },
+        // 搜索框背景色,默认值#f2f2f2
+        bgColor: {
+            type: String,
+            default: uni.$u.props.search.bgColor
+        },
+        // 占位提示文字
+        placeholder: {
+            type: String,
+            default: uni.$u.props.search.placeholder
+        },
+        // 是否启用清除控件
+        clearabled: {
+            type: Boolean,
+            default: uni.$u.props.search.clearabled
+        },
+        // 是否自动聚焦
+        focus: {
+            type: Boolean,
+            default: uni.$u.props.search.focus
+        },
+        // 是否在搜索框右侧显示取消按钮
+        showAction: {
+            type: Boolean,
+            default: uni.$u.props.search.showAction
+        },
+        // 右边控件的样式
+        actionStyle: {
+            type: Object,
+            default: uni.$u.props.search.actionStyle
+        },
+        // 取消按钮文字
+        actionText: {
+            type: String,
+            default: uni.$u.props.search.actionText
+        },
+        // 输入框内容对齐方式,可选值为 left|center|right
+        inputAlign: {
+            type: String,
+            default: uni.$u.props.search.inputAlign
+        },
+        // input输入框的样式,可以定义文字颜色,大小等,对象形式
+        inputStyle: {
+            type: Object,
+            default: uni.$u.props.search.inputStyle
+        },
+        // 是否启用输入框
+        disabled: {
+            type: Boolean,
+            default: uni.$u.props.search.disabled
+        },
+        // 边框颜色
+        borderColor: {
+            type: String,
+            default: uni.$u.props.search.borderColor
+        },
+        // 搜索图标的颜色,默认同输入框字体颜色
+        searchIconColor: {
+            type: String,
+            default: uni.$u.props.search.searchIconColor
+        },
+        // 输入框字体颜色
+        color: {
+            type: String,
+            default: uni.$u.props.search.color
+        },
+        // placeholder的颜色
+        placeholderColor: {
+            type: String,
+            default: uni.$u.props.search.placeholderColor
+        },
+        // 左边输入框的图标,可以为uView图标名称或图片路径
+        searchIcon: {
+            type: String,
+            default: uni.$u.props.search.searchIcon
+        },
+        searchIconSize: {
+            type: [Number, String],
+            default: uni.$u.props.search.searchIconSize
+        },
+        // 组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30px"、"30px 20px"等写法
+        margin: {
+            type: String,
+            default: uni.$u.props.search.margin
+        },
+        // 开启showAction时,是否在input获取焦点时才显示
+        animation: {
+            type: Boolean,
+            default: uni.$u.props.search.animation
+        },
+        // 输入框的初始化内容
+        value: {
+            type: String,
+            default: uni.$u.props.search.value
+        },
+        // 输入框最大能输入的长度,-1为不限制长度(来自uniapp文档)
+        maxlength: {
+            type: [String, Number],
+            default: uni.$u.props.search.maxlength
+        },
+        // 搜索框高度,单位px
+        height: {
+            type: [String, Number],
+            default: uni.$u.props.search.height
+        },
+        // 搜索框左侧文本
+        label: {
+            type: [String, Number, null],
+            default: uni.$u.props.search.label
+        }
+    }
+}

+ 303 - 0
uni_modules/uview-ui/components/u-search/u-search.vue

@@ -0,0 +1,303 @@
+<template>
+	<view
+	    class="u-search"
+	    @tap="clickHandler"
+	    :style="[{
+			margin: margin,
+		}, $u.addStyle(customStyle)]"
+	>
+		<view
+		    class="u-search__content"
+		    :style="{
+				backgroundColor: bgColor,
+				borderRadius: shape == 'round' ? '100px' : '4px',
+				borderColor: borderColor,
+			}"
+		>
+			<template v-if="$slots.label || label !== null">
+				<slot name="label">
+					<text class="u-search__content__label">{{ label }}</text>
+				</slot>
+			</template>
+			<view class="u-search__content__icon">
+				<u-icon
+					@tap="clickIcon"
+				    :size="searchIconSize"
+				    :name="searchIcon"
+				    :color="searchIconColor ? searchIconColor : color"
+				></u-icon>
+			</view>
+			<input
+			    confirm-type="search"
+			    @blur="blur"
+			    :value="value"
+			    @confirm="search"
+			    @input="inputChange"
+			    :disabled="disabled"
+			    @focus="getFocus"
+			    :focus="focus"
+			    :maxlength="maxlength"
+			    placeholder-class="u-search__content__input--placeholder"
+			    :placeholder="placeholder"
+			    :placeholder-style="`color: ${placeholderColor}`"
+			    class="u-search__content__input"
+			    type="text"
+			    :style="[{
+					textAlign: inputAlign,
+					color: color,
+					backgroundColor: bgColor,
+					height: $u.addUnit(height)
+				}, inputStyle]"
+			/>
+			<view
+			    class="u-search__content__icon u-search__content__close"
+			    v-if="keyword && clearabled && focused"
+			    @tap="clear"
+			>
+				<u-icon
+				    name="close"
+				    size="11"
+				    color="#ffffff"
+					customStyle="line-height: 12px"
+				></u-icon>
+			</view>
+		</view>
+		<text
+		    :style="[actionStyle]"
+		    class="u-search__action"
+		    :class="[(showActionBtn || show) && 'u-search__action--active']"
+		    @tap.stop.prevent="custom"
+		>{{ actionText }}</text>
+	</view>
+</template>
+
+<script>
+	import props from './props.js';
+
+	/**
+	 * search 搜索框
+	 * @description 搜索组件,集成了常见搜索框所需功能,用户可以一键引入,开箱即用。
+	 * @tutorial https://www.uviewui.com/components/search.html
+	 * @property {String}			shape				搜索框形状,round-圆形,square-方形(默认 'round' )
+	 * @property {String}			bgColor				搜索框背景颜色(默认 '#f2f2f2' )
+	 * @property {String}			placeholder			占位文字内容(默认 '请输入关键字' )
+	 * @property {Boolean}			clearabled			是否启用清除控件(默认 true )
+	 * @property {Boolean}			focus				是否自动获得焦点(默认 false )
+	 * @property {Boolean}			showAction			是否显示右侧控件(默认 true )
+	 * @property {Object}			actionStyle			右侧控件的样式,对象形式
+	 * @property {String}			actionText			右侧控件文字(默认 '搜索' )
+	 * @property {String}			inputAlign			输入框内容水平对齐方式 (默认 'left' )
+	 * @property {Object}			inputStyle			自定义输入框样式,对象形式
+	 * @property {Boolean}			disabled			是否启用输入框(默认 false )
+	 * @property {String}			borderColor			边框颜色,配置了颜色,才会有边框 (默认 'transparent' )
+	 * @property {String}			searchIconColor		搜索图标的颜色,默认同输入框字体颜色 (默认 '#909399' )
+	 * @property {Number | String}	searchIconSize 搜索图标的字体,默认22
+	 * @property {String}			color				输入框字体颜色(默认 '#606266' )
+	 * @property {String}			placeholderColor	placeholder的颜色(默认 '#909399' )
+	 * @property {String}			searchIcon			输入框左边的图标,可以为uView图标名称或图片路径  (默认 'search' )
+	 * @property {String}			margin				组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30px"   (默认 '0' )
+	 * @property {Boolean} 			animation			是否开启动画,见上方说明(默认 false )
+	 * @property {String}			value				输入框初始值
+	 * @property {String | Number}	maxlength			输入框最大能输入的长度,-1为不限制长度  (默认 '-1' )
+	 * @property {String | Number}	height				输入框高度,单位px(默认 64 )
+	 * @property {String | Number}	label				搜索框左边显示内容
+	 * @property {Object}			customStyle			定义需要用到的外部样式
+	 *
+	 * @event {Function} change 输入框内容发生变化时触发
+	 * @event {Function} search 用户确定搜索时触发,用户按回车键,或者手机键盘右下角的"搜索"键时触发
+	 * @event {Function} custom 用户点击右侧控件时触发
+	 * @event {Function} clear 用户点击清除按钮时触发
+	 * @example <u-search placeholder="日照香炉生紫烟" v-model="keyword"></u-search>
+	 */
+	export default {
+		name: "u-search",
+		mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
+		data() {
+			return {
+				keyword: '',
+				showClear: false, // 是否显示右边的清除图标
+				show: false,
+				// 标记input当前状态是否处于聚焦中,如果是,才会显示右侧的清除控件
+				focused: this.focus
+				// 绑定输入框的值
+				// inputValue: this.value
+			};
+		},
+		watch: {
+			keyword(nVal) {
+				// 双向绑定值,让v-model绑定的值双向变化
+				this.$emit('input', nVal);
+				// 触发change事件,事件效果和v-model双向绑定的效果一样,让用户多一个选择
+				this.$emit('change', nVal);
+			},
+			value: {
+				immediate: true,
+				handler(nVal) {
+					this.keyword = nVal;
+				}
+			}
+		},
+		computed: {
+			showActionBtn() {
+				return !this.animation && this.showAction
+			}
+		},
+		methods: {
+			// 目前HX2.6.9 v-model双向绑定无效,故监听input事件获取输入框内容的变化
+			inputChange(e) {
+				this.keyword = e.detail.value;
+			},
+			// 清空输入
+			// 也可以作为用户通过this.$refs形式调用清空输入框内容
+			clear() {
+				this.keyword = '';
+				// 延后发出事件,避免在父组件监听clear事件时,value为更新前的值(不为空)
+				this.$nextTick(() => {
+					this.$emit('clear');
+				})
+			},
+			// 确定搜索
+			search(e) {
+				this.$emit('search', e.detail.value);
+				try {
+					// 收起键盘
+					uni.hideKeyboard();
+				} catch (e) {}
+			},
+			// 点击右边自定义按钮的事件
+			custom() {
+				this.$emit('custom', this.keyword);
+				try {
+					// 收起键盘
+					uni.hideKeyboard();
+				} catch (e) {}
+			},
+			// 获取焦点
+			getFocus() {
+				this.focused = true;
+				// 开启右侧搜索按钮展开的动画效果
+				if (this.animation && this.showAction) this.show = true;
+				this.$emit('focus', this.keyword);
+			},
+			// 失去焦点
+			blur() {
+				// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+				// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+				setTimeout(() => {
+					this.focused = false;
+				}, 100)
+				this.show = false;
+				this.$emit('blur', this.keyword);
+			},
+			// 点击搜索框,只有disabled=true时才发出事件,因为禁止了输入,意味着是想跳转真正的搜索页
+			clickHandler() {
+				if (this.disabled) this.$emit('click');
+			},
+			// 点击左边图标
+			clickIcon() {
+				this.$emit('clickIcon');
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/components.scss";
+$u-search-content-padding: 0 10px !default;
+$u-search-label-color: $u-main-color !default;
+$u-search-label-font-size: 14px !default;
+$u-search-label-margin: 0 4px !default;
+$u-search-close-size: 20px !default;
+$u-search-close-radius: 100px !default;
+$u-search-close-bgColor: #C6C7CB !default;
+$u-search-close-transform: scale(0.82) !default;
+$u-search-input-font-size: 14px !default;
+$u-search-input-margin: 0 5px !default;
+$u-search-input-color: $u-main-color !default;
+$u-search-input-placeholder-color: $u-tips-color !default;
+$u-search-action-font-size: 14px !default;
+$u-search-action-color: $u-main-color !default;
+$u-search-action-width: 0 !default;
+$u-search-action-active-width: 40px !default;
+$u-search-action-margin-left: 5px !default;
+
+/* #ifdef H5 */
+// iOS15在H5下,hx的某些版本,input type=search时,会多了一个搜索图标,进行移除
+[type="search"]::-webkit-search-decoration {
+    display: none;
+}
+/* #endif */
+
+.u-search {
+	@include flex(row);
+	align-items: center;
+	flex: 1;
+
+	&__content {
+		@include flex;
+		align-items: center;
+		padding: $u-search-content-padding;
+		flex: 1;
+		justify-content: space-between;
+		border-width: 1px;
+		border-color: transparent;
+		border-style: solid;
+		overflow: hidden;
+
+		&__icon {
+			@include flex;
+			align-items: center;
+		}
+
+		&__label {
+			color: $u-search-label-color;
+			font-size: $u-search-label-font-size;
+			margin: $u-search-label-margin;
+		}
+
+		&__close {
+			width: $u-search-close-size;
+			height: $u-search-close-size;
+			border-top-left-radius: $u-search-close-radius;
+			border-top-right-radius: $u-search-close-radius;
+			border-bottom-left-radius: $u-search-close-radius;
+			border-bottom-right-radius: $u-search-close-radius;
+			background-color: $u-search-close-bgColor;
+			@include flex(row);
+			align-items: center;
+			justify-content: center;
+			transform: $u-search-close-transform;
+		}
+
+		&__input {
+			flex: 1;
+			font-size: $u-search-input-font-size;
+			line-height: 1;
+			margin: $u-search-input-margin;
+			color: $u-search-input-color;
+
+			&--placeholder {
+				color: $u-search-input-placeholder-color;
+			}
+		}
+	}
+
+	&__action {
+		font-size: $u-search-action-font-size;
+		color: $u-search-action-color;
+		width: $u-search-action-width;
+		overflow: hidden;
+		transition-property: width;
+		transition-duration: 0.3s;
+		/* #ifndef APP-NVUE */
+		white-space: nowrap;
+		/* #endif */
+		text-align: center;
+
+		&--active {
+			width: $u-search-action-active-width;
+			margin-left: $u-search-action-margin-left;
+		}
+	}
+}
+</style>

+ 59 - 0
uni_modules/uview-ui/components/u-skeleton/props.js

@@ -0,0 +1,59 @@
+export default {
+    props: {
+        // 是否展示骨架组件
+        loading: {
+            type: Boolean,
+            default: uni.$u.props.skeleton.loading
+        },
+        // 是否开启动画效果
+        animate: {
+            type: Boolean,
+            default: uni.$u.props.skeleton.animate
+        },
+        // 段落占位图行数
+        rows: {
+            type: [String, Number],
+            default: uni.$u.props.skeleton.rows
+        },
+        // 段落占位图的宽度
+        rowsWidth: {
+            type: [String, Number, Array],
+            default: uni.$u.props.skeleton.rowsWidth
+        },
+        // 段落占位图的高度
+        rowsHeight: {
+            type: [String, Number, Array],
+            default: uni.$u.props.skeleton.rowsHeight
+        },
+        // 是否展示标题占位图
+        title: {
+            type: Boolean,
+            default: uni.$u.props.skeleton.title
+        },
+        // 段落标题的宽度
+        titleWidth: {
+            type: [String, Number],
+            default: uni.$u.props.skeleton.titleWidth
+        },
+        // 段落标题的高度
+        titleHeight: {
+            type: [String, Number],
+            default: uni.$u.props.skeleton.titleHeight
+        },
+        // 是否展示头像占位图
+        avatar: {
+            type: Boolean,
+            default: uni.$u.props.skeleton.avatar
+        },
+        // 头像占位图大小
+        avatarSize: {
+            type: [String, Number],
+            default: uni.$u.props.skeleton.avatarSize
+        },
+        // 头像占位图的形状,circle-圆形,square-方形
+        avatarShape: {
+            type: String,
+            default: uni.$u.props.skeleton.avatarShape
+        }
+    }
+}

+ 244 - 0
uni_modules/uview-ui/components/u-skeleton/u-skeleton.vue

@@ -0,0 +1,244 @@
+<template>
+	<view class="u-skeleton">
+		<view
+		    class="u-skeleton__wrapper"
+		    ref="u-skeleton__wrapper"
+		    v-if="loading"
+			style="display: flex; flex-direction: row;"
+		>
+			<view
+			    class="u-skeleton__wrapper__avatar"
+			    v-if="avatar"
+			    :class="[`u-skeleton__wrapper__avatar--${avatarShape}`, animate && 'animate']"
+			    :style="{
+						height: $u.addUnit(avatarSize),
+						width: $u.addUnit(avatarSize)
+					}"
+			></view>
+			<view
+			    class="u-skeleton__wrapper__content"
+			    ref="u-skeleton__wrapper__content"
+				style="flex: 1;"
+			>
+				<view
+				    class="u-skeleton__wrapper__content__title"
+				    v-if="title"
+				    :style="{
+							width: uTitleWidth,
+							height: $u.addUnit(titleHeight),
+						}"
+				    :class="[animate && 'animate']"
+				></view>
+				<view
+				    class="u-skeleton__wrapper__content__rows"
+				    :class="[animate && 'animate']"
+				    v-for="(item, index) in rowsArray"
+				    :key="index"
+				    :style="{
+							 width: item.width,
+							 height: item.height,
+							 marginTop: item.marginTop
+						}"
+				>
+		
+				</view>
+			</view>
+		</view>
+		<slot v-else />
+	</view>
+</template>
+
+<script>
+	import props from './props.js';
+	// #ifdef APP-NVUE
+	// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
+	const dom = uni.requireNativePlugin('dom')
+	const animation = uni.requireNativePlugin('animation')
+	// #endif
+	/**
+	 * Skeleton 骨架屏
+	 * @description 骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。
+	 * @tutorial https://www.uviewui.com/components/skeleton.html
+	 * @property {Boolean}					loading		是否显示骨架占位图,设置为false将会展示子组件内容 (默认 true )
+	 * @property {Boolean}					animate		是否开启动画效果 (默认 true )
+	 * @property {String | Number}			rows		段落占位图行数 (默认 0 )
+	 * @property {String | Number | Array}	rowsWidth	段落占位图的宽度,可以为百分比,数值,带单位字符串等,可通过数组传入指定每个段落行的宽度 (默认 '100%' )
+	 * @property {String | Number | Array}	rowsHeight	段落的高度 (默认 18 )
+	 * @property {Boolean}					title		是否展示标题占位图 (默认 true )
+	 * @property {String | Number}			titleWidth	标题的宽度 (默认 '50%' )
+	 * @property {String | Number}			titleHeight	标题的高度 (默认 18 )
+	 * @property {Boolean}					avatar		是否展示头像占位图 (默认 false )
+	 * @property {String | Number}			avatarSize	头像占位图大小 (默认 32 )
+	 * @property {String}					avatarShape	头像占位图的形状,circle-圆形,square-方形 (默认 'circle' )
+	 * @example <u-search placeholder="日照香炉生紫烟" v-model="keyword"></u-search>
+	 */
+	export default {
+		name: 'u-skeleton',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		data() {
+			return {
+				width: 0,
+			}
+		},
+		watch: {
+			loading() {
+				this.getComponentWidth()
+			}
+		},
+		computed: {
+			rowsArray() {
+				if (/%$/.test(this.rowsHeight)) {
+					uni.$u.error('rowsHeight参数不支持百分比单位')
+				}
+				const rows = []
+				for (let i = 0; i < this.rows; i++) {
+					let item = {},
+						// 需要预防超出数组边界的情况
+						rowWidth = uni.$u.test.array(this.rowsWidth) ? (this.rowsWidth[i] || (i === this.row - 1 ? '70%' : '100%')) : i ===
+						this.rows - 1 ? '70%' : this.rowsWidth,
+						rowHeight = uni.$u.test.array(this.rowsHeight) ? (this.rowsHeight[i] || '18px') : this.rowsHeight
+					// 如果有title占位图,第一个段落占位图的外边距需要大一些,如果没有title占位图,第一个段落占位图则无需外边距
+					// 之所以需要这么做,是因为weex的无能,以提升性能为借口不支持css的一些伪类
+					item.marginTop = !this.title && i === 0 ? 0 : this.title && i === 0 ? '20px' : '12px'
+					// 如果设置的为百分比的宽度,转换为px值,因为nvue不支持百分比单位
+					if (/%$/.test(rowWidth)) {
+						// 通过parseInt提取出百分比单位中的数值部分,除以100得到百分比的小数值
+						item.width = uni.$u.addUnit(this.width * parseInt(rowWidth) / 100)
+					} else {
+						item.width = uni.$u.addUnit(rowWidth)
+					}
+					item.height = uni.$u.addUnit(rowHeight)
+					rows.push(item)
+				}
+				// console.log(rows);
+				return rows
+			},
+			uTitleWidth() {
+				let tWidth = 0
+				if (/%$/.test(this.titleWidth)) {
+					// 通过parseInt提取出百分比单位中的数值部分,除以100得到百分比的小数值
+					tWidth = uni.$u.addUnit(this.width * parseInt(this.titleWidth) / 100)
+				} else {
+					tWidth = uni.$u.addUnit(this.titleWidth)
+				}
+				return uni.$u.addUnit(tWidth)
+			},
+			
+		},
+		mounted() {
+			this.init()
+		},
+		methods: {
+			init() {
+				this.getComponentWidth()
+				// #ifdef APP-NVUE
+				this.loading && this.animate && this.setNvueAnimation()
+				// #endif
+			},
+			async setNvueAnimation() {
+				// #ifdef APP-NVUE
+				// 为了让opacity:1的状态保持一定时间,这里做一个延时
+				await uni.$u.sleep(500)
+				const skeleton = this.$refs['u-skeleton__wrapper'];
+				skeleton && this.loading && this.animate && animation.transition(skeleton, {
+					styles: {
+						opacity: 0.5
+					},
+					duration: 600,
+				}, () => {
+					// 这里无需判断是否loading和开启动画状态,因为最终的状态必须达到opacity: 1,否则可能
+					// 会停留在opacity: 0.5的状态中
+					animation.transition(skeleton, {
+						styles: {
+							opacity: 1
+						},
+						duration: 600,
+					}, () => {
+						// 只有在loading中时,才执行动画
+						this.loading && this.animate && this.setNvueAnimation()
+					})
+				})
+				// #endif
+			},
+			// 获取组件的宽度
+			async getComponentWidth() {
+				// 延时一定时间,以获取dom尺寸
+				await uni.$u.sleep(20)
+				// #ifndef APP-NVUE
+				this.$uGetRect('.u-skeleton__wrapper__content').then(size => {
+					this.width = size.width
+				})
+				// #endif
+
+				// #ifdef APP-NVUE
+				const ref = this.$refs['u-skeleton__wrapper__content']
+				ref && dom.getComponentRect(ref, (res) => {
+					this.width = res.size.width
+				})
+				// #endif
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+
+	@mixin background {
+		/* #ifdef APP-NVUE */
+		background-color: #F1F2F4;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		background: linear-gradient(90deg, #F1F2F4 25%, #e6e6e6 37%, #F1F2F4 50%);
+		background-size: 400% 100%;
+		/* #endif */
+	}
+
+	.u-skeleton {
+		flex: 1;
+		
+		&__wrapper {
+			@include flex(row);
+			
+			&__avatar {
+				@include background;
+				margin-right: 15px;
+			
+				&--circle {
+					border-radius: 100px;
+				}
+			
+				&--square {
+					border-radius: 4px;
+				}
+			}
+			
+			&__content {
+				flex: 1;
+			
+				&__rows,
+				&__title {
+					@include background;
+					border-radius: 3px;
+				}
+			}
+		}
+	}
+
+	/* #ifndef APP-NVUE */
+	.animate {
+		animation: skeleton 1.8s ease infinite
+	}
+
+	@keyframes skeleton {
+		0% {
+			background-position: 100% 50%
+		}
+
+		100% {
+			background-position: 0 50%
+		}
+	}
+
+	/* #endif */
+</style>

+ 113 - 0
uni_modules/uview-ui/components/u-slider/mpother.js

@@ -0,0 +1,113 @@
+/**
+ * 使用普通的js方案实现slider
+ */
+export default {
+    watch: {
+        value(n) {
+            // 只有在非滑动状态时,才可以通过value更新滑块值,这里监听,是为了让用户触发
+            if (this.status === 'end') {
+                this.updateSliderPlacement(n, true)
+            }
+        }
+    },
+    mounted() {
+        this.init()
+    },
+    methods: {
+        init() {
+            this.getSliderRect()
+        },
+        // 获取slider尺寸
+        getSliderRect() {
+            // 获取滑块条的尺寸信息
+            setTimeout(() => {
+                this.$uGetRect('.u-slider').then((rect) => {
+                    this.sliderRect = rect
+                    this.updateSliderPlacement(this.value, true)
+                })
+            }, 10)
+        },
+        // 是否可以操作
+        canNotDo() {
+            return this.disabled
+        },
+        // 获取当前手势点的X轴位移值
+        getTouchX(e) {
+            return e.touches[0].clientX
+        },
+        formatStep(value) {
+            // 移动点占总长度的百分比
+            return Math.round(Math.max(this.min, Math.min(value, this.max)) / this.step) * this.step
+        },
+        // 发出事件
+        emitEvent(event, value) {
+            this.$emit(event, value || this.value)
+        },
+        // 标记当前手势的状态
+        setTouchStatus(status) {
+            this.status = status
+        },
+        onTouchStart(e) {
+            if (this.canNotDo()) {
+                return
+            }
+            // 标示当前的状态为开始触摸滑动
+            this.emitEvent('start')
+            this.setTouchStatus('start')
+        },
+        onTouchMove(e) {
+            if (this.canNotDo()) {
+                return
+            }
+            // 滑块的左边不一定跟屏幕左边接壤,所以需要减去最外层父元素的左边值
+            const x = this.getTouchX(e)
+            const { left, width } = this.sliderRect
+            const distanceX = x - left
+            // 获得移动距离对整个滑块的百分比值,此为带有多位小数的值,不能用此更新视图
+            // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
+            const percent = (distanceX / width) * 100
+            this.setTouchStatus('moving')
+            this.updateSliderPlacement(percent, true, 'moving')
+        },
+        onTouchEnd() {
+            if (this.canNotDo()) {
+                return
+            }
+            this.emitEvent('end')
+            this.setTouchStatus('end')
+        },
+        // 设置滑点的位置
+        updateSliderPlacement(value, drag, event) {
+            // 去掉小数部分,同时也是对step步进的处理
+            const { width } = this.sliderRect
+            const percent = this.formatStep(value)
+            // 设置移动的值
+            const barStyle = {
+                width: `${percent / 100 * width}px`
+            }
+            // 移动期间无需过渡动画
+            if (drag === true) {
+                barStyle.transition = 'none'
+            } else {
+                // 非移动期间,删掉对过渡为空的声明,让css中的声明起效
+                delete barStyle.transition
+            }
+            // 修改value值
+            this.$emit('input', percent)
+            // 事件的名称
+            if (event) {
+                this.emitEvent(event, percent)
+            }
+            this.barStyle = barStyle
+        },
+        onClick(e) {
+            if (this.canNotDo()) {
+                return
+            }
+            // 直接点击滑块的情况,计算方式与onTouchMove方法相同
+            const { left, width } = this.sliderRect
+            const value = ((e.detail.x - left) / width) * 100
+            this.updateSliderPlacement(value, false, 'click')
+        }
+    }
+}

+ 42 - 0
uni_modules/uview-ui/components/u-slider/mpwxs.js

@@ -0,0 +1,42 @@
+export default {
+    data() {
+        return {
+            sliderRect: {},
+            info: {
+                width: null,
+                left: null,
+                step: this.step,
+                disabled: this.disabled,
+                min: this.min,
+                max: this.max,
+                value: this.value
+            }
+        }
+    },
+    mounted() {
+        this.init()
+    },
+    methods: {
+        init() {
+            this.getSliderRect()
+        },
+        // 获取slider尺寸
+        getSliderRect() {
+            // 获取滑块条的尺寸信息
+            uni.$u.sleep().then(() => {
+                this.$uGetRect('.u-slider').then((rect) => {
+                    this.info.width = rect.width
+                    this.info.left = rect.left
+                })
+            })
+        },
+        // 此方法由wxs调用,用于修改v-model绑定的值
+        updateValue(value) {
+            this.$emit('input', value)
+        },
+        // 此方法由wxs调用,发出事件
+        emitEvent(e) {
+            this.$emit(e.event, e.value ? e.value : this.value)
+        }
+    }
+}

+ 121 - 0
uni_modules/uview-ui/components/u-slider/mpwxs.wxs

@@ -0,0 +1,121 @@
+/**
+ * 使用wxs方案实现slider
+ * 兼容微信,QQ,H5,Vue版的安卓和iOS
+ */
+/**
+ * 开始滑动操作
+ * @param {Object} e
+ * @param {Object} ownerInstance
+ */
+function onTouchMove(e, ownerInstance) {
+	// wxs事件对象下有一个instance属性,表示当前触发此事件的组件的实例,通过该实例,可以获取相关的dataset,设置样式等信息
+	// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
+	var instance = e.instance;
+	// getState()为一个对象,挂载在instance上,类似组件的data一样,可以存放一些变量,供以后的触发事件中使用
+	var state = instance.getState()
+
+	// 滑块组件的整体尺寸信息
+	var mp = state.mp
+	if(mp.disabled) {
+		return
+	}
+	
+	var distanceX = getTouchX(e) - mp.left
+	// 获得移动距离对整个滑块的百分比值,此为带有多位小数的值,step大于1时,不能用此更新视图
+	var percent = (distanceX / mp.width) * 100
+
+	updateSliderPlacement(instance, ownerInstance, percent, 'moving')
+	
+	// 阻止页面滚动,可以保证在滑动过程中,不让页面可以上下滚动,造成不好的体验
+	e.stopPropagation && e.stopPropagation() 
+	e.preventDefault && e.preventDefault()
+}
+
+function onClick(e, ownerInstance) {
+	var instance = e.instance
+	var state = instance.getState()
+	var mp = state.mp
+	if(mp.disabled) {
+		return
+	}
+	
+	// 直接点击滑块的情况,计算方式与onTouchMove方法相同
+	var value = ((e.detail.x - mp.left) / mp.width) * 100
+	updateSliderPlacement(instance, ownerInstance, value, 'click')
+}
+
+function sizeReady(newValue, oldValue, ownerInstance, instance) {
+	// 页面初始化时候,也会触发此方法,传递的值为空,这里不执行往后的逻辑
+	if(!newValue || newValue.disabled) {
+		return 
+	}
+	var state = instance.getState()
+	state.mp = newValue
+	updateSliderPlacement(instance, ownerInstance, newValue.value)
+}
+
+// 设置滑点的位置
+function updateSliderPlacement(instance, ownerInstance, value, event) {
+	var state = instance.getState()
+	var mp = state.mp
+	if(mp.disabled) {
+		return
+	}
+
+	var percent = 0
+	if (mp.step > 1) {
+		// 如果step步进大于1,需要跳步,所以需要使用Math.round进行取整
+		percent = Math.round(Math.max(mp.min, Math.min(value, mp.max)) / mp.step) * mp.step
+	} else {
+		// 当step=1时,无需跳步,充分利用wxs性能,滑块实时跟随手势,达到丝滑的效果
+		percent = Math.max(mp.min, Math.min(value, mp.max))
+	}
+	// 返回组件的实例
+	var gapInstance = ownerInstance.selectComponent('.u-slider__gap')
+	// 在移动期间,不允许transition动画,否则会造成卡顿
+	gapInstance[event === 'click' ? 'addClass' : 'removeClass']('u-slider__gap--ani')
+	// 调用逻辑层的方法,修改v-model绑定的值
+	ownerInstance.callMethod('updateValue', Math.round(percent))
+	if(event) {
+		ownerInstance.callMethod('emitEvent', {
+			event: event,
+			value: Math.round(percent)
+		})
+	}
+	
+	// 设置移动的值
+	gapInstance.requestAnimationFrame(function() {
+		gapInstance.setStyle({
+			width: percent / 100 * mp.width + 'px',
+		})
+	})
+}
+
+// 开始滑动
+function onTouchStart(e, ownerInstance) {
+	ownerInstance.callMethod('emitEvent', {
+		event: 'start', 
+		value: null
+	})
+}
+
+// 停止滑动
+function onTouchEnd(e, ownerInstance) {
+	ownerInstance.callMethod('emitEvent', {
+		event: 'end', 
+		value: null
+	})
+}
+
+// 获取当前手势点的X轴位移值
+function getTouchX(e) {
+	return e.touches[0].clientX
+}
+
+module.exports = {
+	onTouchStart: onTouchStart,
+	onTouchMove: onTouchMove,
+	onTouchEnd: onTouchEnd,
+	sizeReady: sizeReady,
+	onClick: onClick
+}

+ 180 - 0
uni_modules/uview-ui/components/u-slider/nvue - 副本.js

@@ -0,0 +1,180 @@
+/**
+ * 使用bindingx方案实现slider
+ * 只能使用于nvue下
+ */
+// 引入bindingx,此库类似于微信小程序wxs,目的是让js运行在视图层,减少视图层和逻辑层的通信折损
+const BindingX = uni.requireNativePlugin('bindingx')
+// nvue操作dom的库,用于获取dom的尺寸信息
+const dom = uni.requireNativePlugin('dom')
+// nvue中用于操作元素动画的库,类似于uni.animation,只不过uni.animation不能用于nvue
+const animation = uni.requireNativePlugin('animation')
+
+export default {
+	data() {
+		return {
+			// bindingx的回调值,用于取消绑定
+			panEvent: null,
+			// 标记是否移动状态
+			moving: false,
+			// 位移的偏移量
+			x: 0,
+			// 是否正在触摸过程中,用于标记动画类是否添加或移除
+			touching: false,
+			changeFromInside: false
+		}
+	},
+	watch: {
+		// 监听vlaue的变化,此变化可能是由于内部修改v-model的值,或者外部
+		// 从服务端获取一个值后,赋值给slider的v-model而导致的
+		value(n) {
+			if (!this.changeFromInside) {
+				this.initX()
+			} else {
+				this.changeFromInside = false
+			}
+		}
+	},
+	mounted() {
+		this.init()
+	},
+	methods: {
+		init() {
+			this.getSliderRect()
+		},
+		// 获取节点信息
+		// 获取slider尺寸
+		getSliderRect() {
+			// 获取滑块条的尺寸信息
+			// 通过nvue的dom模块,查询节点信息
+			setTimeout(() => {
+				dom.getComponentRect(this.$refs['slider'], res => {
+					this.sliderRect = res.size
+					this.initX()
+				})
+			}, 10)
+		},
+		// 初始化按钮位置
+		initButtonStyle({
+			barStyle,
+			buttonWrapperStyle
+		}) {
+			this.barStyle = barStyle
+			this.buttonWrapperStyle = buttonWrapperStyle
+		},
+		emitEvent(event, value) {
+			this.$emit(event, value ? value : this.value)
+		},
+		formatStep(value) {
+			// 移动点占总长度的百分比
+			return Math.round(Math.max(this.min, Math.min(value, this.max)) / this.step) * this.step
+		},
+		// 滑动开始
+		onTouchStart(e) {
+			// 阻止页面滚动,可以保证在滑动过程中,不让页面可以上下滚动,造成不好的体验
+			e.stopPropagation && e.stopPropagation()
+			e.preventDefault && e.preventDefault()
+			if (this.moving || this.disabled) {
+				// 释放上一次的资源
+				if (this.panEvent?.token != 0) {
+					BindingX.unbind({
+						token: this.panEvent.token,
+						// pan为手势事件
+						eventType: 'pan'
+					})
+					this.gesToken = 0
+				}
+				return
+			}
+
+			this.moving = true
+			this.touching = true
+
+			// 获取元素ref
+			const button = this.$refs['nvue-button'].ref
+			const gap = this.$refs['nvue-gap'].ref
+
+			const {
+				min,
+				max,
+				step
+			} = this
+			const {
+				left,
+				width
+			} = this.sliderRect
+
+			// 初始值为本次偏移量x,加上次停止滑动时的结束值
+			let exporession = `(${this.x} + x)`
+			// 将偏移的x值,转为总位移的百分比值,为了和min和max进行判断
+			exporession = `(${exporession} / ${width}) * 100`
+			if (step > 1) {
+				// 如果step步进大于1,需要跳步,所以需要使用Math.round进行取整
+				exporession = `round(max(${min}, min(${exporession}, ${max})) / ${step}) * ${step}`
+			} else {
+				// 当step=1时,无需跳步,充分利用bindingx性能,滑块实时跟随手势,达到丝滑的效果
+				exporession = `max(${min}, min(${exporession}, ${max}))`
+			}
+			// 将百分比最后转化为对应的px值
+			exporession = `${exporession} / 100 * ${width}`
+			// 最大值不允许超过轨迹的宽度
+			const {
+				sliderWidth
+			} = this.sliderRect
+			exporession = `min(${sliderWidth}, ${exporession})`
+			// 滑块点总是需要一个左偏移的值,为自身宽度的一半
+			const buttonExpression = `${exporession} - ${this.blockHeight / 2}`
+			// 阿里为了KPI而开源的BindingX
+			this.panEvent = BindingX.bind({
+				anchor: button,
+				eventType: 'pan',
+				props: [{
+					element: gap,
+					// 绑定width属性,设置其宽度值
+					property: 'width',
+					expression
+				}, {
+					element: button,
+					// 绑定width属性,设置其宽度值
+					property: 'transform.translateX',
+					expression: buttonExpression
+				}]
+			}, (e) => {
+				if (e.state === 'end' || e.state === 'exit') {
+					// 
+					this.x = uni.$u.range(0, left + width, e.deltaX + this.x)
+					// 根据偏移值,得出移动的百分比,进而修改双向绑定的v-model的值
+					const value = (this.x / width) * 100
+					const percent = this.formatStep(value)
+					// 修改value值
+					this.$emit('input', percent)
+					// 标记下一次触发value的watch时,这个值的变化,是由内部改变的
+					this.changeFromInside = true
+					this.moving = false
+					this.touching = false
+				}
+			})
+		},
+		// 从value的变化,倒推得出x的值该为多少
+		initX() {
+			const {
+				left,
+				width
+			} = this.sliderRect
+			// 得出x的初始偏移值,之所以需要这么做,是因为在bindingX中,触摸滑动时,只能的值本次移动的偏移值
+			// 而无法的值准确的前后移动的两个点的坐标值,weex纯粹为阿里巴巴的KPI(部门业绩考核)产物,也就这样了
+			this.x = this.value / 100 * width
+			// 设置移动的值
+			const barStyle = {
+				width: this.x + 'px'
+			}
+			// 按钮的初始值
+			const buttonWrapperStyle = {
+				transform: `translateX(${this.x - this.blockHeight / 2}px)`
+			}
+			this.initButtonStyle({
+				barStyle,
+				buttonWrapperStyle
+			})
+		}
+	}
+}

+ 193 - 0
uni_modules/uview-ui/components/u-slider/nvue.js

@@ -0,0 +1,193 @@
+/**
+ * 使用bindingx方案实现slider
+ * 只能使用于nvue下
+ */
+// 引入bindingx,此库类似于微信小程序wxs,目的是让js运行在视图层,减少视图层和逻辑层的通信折损
+const BindingX = uni.requireNativePlugin('bindingx')
+// nvue操作dom的库,用于获取dom的尺寸信息
+const dom = uni.requireNativePlugin('dom')
+// nvue中用于操作元素动画的库,类似于uni.animation,只不过uni.animation不能用于nvue
+const animation = uni.requireNativePlugin('animation')
+
+export default {
+    data() {
+        return {
+            // 位移的偏移量
+            x: 0,
+            // 是否正在触摸过程中,用于标记动画类是否添加或移除
+            touching: false,
+            changeFromInside: false
+        }
+    },
+    watch: {
+        // 监听vlaue的变化,此变化可能是由于内部修改v-model的值,或者外部
+        // 从服务端获取一个值后,赋值给slider的v-model而导致的
+        value(n) {
+            if (!this.changeFromInside) {
+                this.initX()
+            } else {
+                this.changeFromInside = false
+            }
+        }
+    },
+    mounted() {
+        this.init()
+    },
+    methods: {
+        init() {
+            // 更新滑块尺寸信息
+            this.getSliderRect().then((size) => {
+                this.sliderRect = size
+                this.initX()
+            })
+        },
+        // 获取节点信息
+        // 获取slider尺寸
+        getSliderRect() {
+            // 获取滑块条的尺寸信息
+            // 通过nvue的dom模块,查询节点信息
+            return new Promise((resolve) => {
+                this.$nextTick(() => {
+                    dom.getComponentRect(this.$refs.slider, (res) => {
+                        resolve(res.size)
+                    })
+                })
+            })
+        },
+        // 初始化按钮位置
+        initButtonStyle({
+            barStyle,
+            buttonWrapperStyle
+        }) {
+            this.barStyle = barStyle
+            this.buttonWrapperStyle = buttonWrapperStyle
+        },
+        emitEvent(event, value) {
+            this.$emit(event, value || this.value)
+        },
+        // 滑动开始
+        async onTouchStart(e) {
+            // if (this.disabled) return
+            // // 阻止页面滚动,可以保证在滑动过程中,不让页面可以上下滚动,造成不好的体验
+            // e.stopPropagation && e.stopPropagation()
+            // e.preventDefault && e.preventDefault()
+            // // 更新滑块的尺寸信息
+            // this.sliderRect = await this.getSliderRect()
+            // // 标记滑动过程中触摸点的信息
+            // this.touchStart(e)
+            // this.startValue = this.format(this.value)
+            // this.dragStatus = 'start'
+
+            // 标记滑动过程中触摸点的信息
+            // this.touchStart(e)
+        },
+        // 开始滑动
+        onTouchMove(e) {
+            // if (this.disabled) return;
+            // if (this.dragStatus === 'start') {
+            // 	this.$emit('drag-start')
+            // }
+            // // 标记当前滑动过程中的触点信息,此方法在touch mixin中
+            // this.touchMove(e)
+            // this.dragStatus = 'draging'
+            // const {
+            // 	width: sliderWidth
+            // } = this.sliderRect
+            // const diff = (this.deltaX / sliderWidth) * this.getRange()
+            // this.newValue = this.startValue + diff
+            // this.updateValue(this.newValue, false, true)
+            // 获取元素ref
+            // const button = this.$refs['nvue-button'].ref
+            // const gap = this.$refs['nvue-gap'].ref
+
+            //          animation.transition(gap, {
+            // 	styles: {
+            //                  width: `${this.startX + this.deltaX}px`
+            // 	}
+            // })
+            // // console.log(this.startX + this.deltaX);
+            // animation.transition(button, {
+            // 	styles: {
+            //         transform: `translateX(${this.startX + this.deltaX}px)`
+            // 	}
+            // })
+            // this.barStyle = {
+            // 	width: `${this.startX + this.deltaX}px`
+            // }
+            const {
+                x
+            } = this.getTouchPoint(e)
+            this.buttonWrapperStyle = {
+                transform: `translateX(${x}px)`
+            }
+            // this.buttonWrapperStyle = {
+            // 	transform: `translateX(${this.format(this.startX + this.deltaX)}px)`
+            // }
+        },
+        // onTouchEnd() {
+        // 	if (this.disabled) return;
+        // 	if (this.dragStatus === 'draging') {
+        // 		this.updateValue(this.newValue, true)
+        // 		this.$emit('drag-end');
+        // 	}
+        // },
+        updateValue(value, end, drag) {
+            value = this.format(value)
+            const {
+                width: sliderWidth
+            } = this.sliderRect
+            const width = `${((value - this.min) * sliderWidth) / this.getRange()}`
+            this.value = value
+            this.barStyle = {
+                width: `${width}px`
+            }
+            // console.log('width', width);
+            if (drag) {
+                this.$emit('drag', {
+                    value
+                })
+            }
+            if (end) {
+                this.$emit('change', value)
+            }
+            if ((drag || end)) {
+                this.changeFromInside = true
+                this.$emit('update', value)
+            }
+        },
+        // 从value的变化,倒推得出x的值该为多少
+        initX() {
+            const {
+                left,
+                width
+            } = this.sliderRect
+            // 得出x的初始偏移值,之所以需要这么做,是因为在bindingX中,触摸滑动时,只能的值本次移动的偏移值
+            // 而无法的值准确的前后移动的两个点的坐标值,weex纯粹为阿里巴巴的KPI(部门业绩考核)产物,也就这样了
+            this.x = this.value / 100 * width
+            // 设置移动的值
+            const barStyle = {
+                width: `${this.x}px`
+            }
+            // 按钮的初始值
+            const buttonWrapperStyle = {
+                transform: `translateX(${this.x - this.blockHeight / 2}px)`
+            }
+            this.initButtonStyle({
+                barStyle,
+                buttonWrapperStyle
+            })
+        },
+        // 移动点占总长度的百分比,此处需要先除以step,是为了保证step大于1时,比如10,那么在滑动11,12px这样的
+        // 距离时,实际上滑块是不会滑动的,到了16,17px,经过四舍五入后,就变成了20px,进行了下一个跳变
+        format(value) {
+            return Math.round(uni.$u.range(this.min, this.max, value) / this.step) * this.step
+        },
+        getRange() {
+            const {
+                max,
+                min
+            } = this
+            return max - min
+        }
+    }
+}

+ 54 - 0
uni_modules/uview-ui/components/u-slider/props.js

@@ -0,0 +1,54 @@
+export default {
+    props: {
+        // 最小可选值
+        min: {
+            type: [Number, String],
+            default: uni.$u.props.slider.min
+        },
+        // 最大可选值
+        max: {
+            type: [Number, String],
+            default: uni.$u.props.slider.max
+        },
+        // 步长,取值必须大于 0,并且可被(max - min)整除
+        step: {
+            type: [Number, String],
+            default: uni.$u.props.slider.step
+        },
+        // 当前取值
+        value: {
+            type: [Number, String],
+            default: uni.$u.props.slider.value
+        },
+        // 滑块右侧已选择部分的背景色
+        activeColor: {
+            type: String,
+            default: uni.$u.props.slider.activeColor
+        },
+        // 滑块左侧未选择部分的背景色
+        inactiveColor: {
+            type: String,
+            default: uni.$u.props.slider.inactiveColor
+        },
+        // 滑块的大小,取值范围为 12 - 28
+        blockSize: {
+            type: [Number, String],
+            default: uni.$u.props.slider.blockSize
+        },
+        // 滑块的颜色
+        blockColor: {
+            type: String,
+            default: uni.$u.props.slider.blockColor
+        },
+		// 禁用状态
+		disabled: {
+			type: Boolean,
+			default: uni.$u.props.slider.disabled
+		},
+        // 是否显示当前的选择值
+        showValue: {
+            type: Boolean,
+            default: uni.$u.props.slider.showValue
+        }
+    }
+}

+ 55 - 0
uni_modules/uview-ui/components/u-slider/u-slider.vue

@@ -0,0 +1,55 @@
+<template>
+	<view
+		class="u-slider"
+		:style="[$u.addStyle(customStyle)]"
+	>
+		<slider
+			:min="min"
+			:max="max"
+			:step="step"
+			:value="value"
+			:activeColor="activeColor"
+			:inactiveColor="inactiveColor"
+			:blockSize="$u.getPx(blockSize)"
+			:blockColor="blockColor"
+			:showValue="showValue"
+			:disabled="disabled"
+			@changing="changingHandler"
+			@change="changeHandler"
+		></slider>
+	</view>
+</template>
+
+<script>
+	import props from './props.js'
+	export default {
+		name: 'u--slider',
+		mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
+		methods: {
+			// 拖动过程中触发
+			changingHandler(e) {
+				const {
+					value
+				} = e.detail
+				// 更新v-model的值
+				this.$emit('input', value)
+				// 触发事件
+				this.$emit('changing', value)
+			},
+			// 滑动结束时触发
+			changeHandler(e) {
+				const {
+					value
+				} = e.detail
+				// 更新v-model的值
+				this.$emit('input', value)
+				// 触发事件
+				this.$emit('change', value)
+			}
+		},
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/components.scss";
+</style>

+ 0 - 0
uni_modules/uview-ui/components/u-status-bar/props.js


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