@@ -1,21 +1,21 @@
-MIT License
-
-Copyright (c) 2020 www.uviewui.com
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+MIT License
+
+Copyright (c) 2020 www.uviewui.com
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,66 +1,106 @@
<p align="center">
<img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
</p>
-<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView 2.0</h3>
+<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3>
<h3 align="center">多平台快速开发的UI框架</h3>
-[](https://github.com/umicro/uView2.0)
-[](https://github.com/umicro/uView2.0)
-[](https://github.com/umicro/uView2.0/issues)
-[](https://uviewui.com)
-[](https://gitee.com/umicro/uView2.0/releases)
-[](https://en.wikipedia.org/wiki/MIT_License)
## 说明
-uView UI,是[uni-app](https://uniapp.dcloud.io/)全面兼容nvue的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
+uView UI,是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
-## [官方文档:https://uviewui.com](https://uviewui.com)
+## 特性
+- 兼容安卓,iOS,微信小程序,H5,QQ小程序,百度小程序,支付宝小程序,头条小程序
+- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用
+- 众多贴心的JS利器,让您飞镖在手,召之即来,百步穿杨
+- 众多的常用页面和布局,让您专注逻辑,事半功倍
+- 详尽的文档支持,现代化的演示效果
+- 按需引入,精简打包体积
-## 预览
-您可以通过**微信**扫码,查看最佳的演示效果。
-<br>
-<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
-## 链接
-- [官方文档](https://www.uviewui.com/)
-- [更新日志](https://www.uviewui.com/components/changelog.html)
-- [升级指南](https://www.uviewui.com/components/changeGuide.html)
-- [关于我们](https://www.uviewui.com/cooperation/about.html)
-## 交流反馈
-欢迎加入我们的QQ群交流反馈:[点此跳转](https://www.uviewui.com/components/addQQGroup.html)
+## 安装
-## 关于PR
+```bash
+# npm方式安装
+npm i uview-ui
+```
-> 我们非常乐意接受各位的优质PR,但在此之前我希望您了解uView2.0是一个需要兼容多个平台的(小程序、h5、ios app、android app)包括nvue页面、vue页面。
-> 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢!
+## 快速上手
-## 安装
+1. `main.js`引入uView库
+```js
+// main.js
+import uView from 'uview-ui';
+Vue.use(uView);
-#### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?id=1593](https://ext.dcloud.net.cn/plugin?id=1593)
+2. `App.vue`引入基础样式(注意style标签需声明scss属性支持)
+```css
+/* App.vue */
+<style lang="scss">
+@import "uview-ui/index.scss";
+</style>
-请通过[官网安装文档](https://www.uviewui.com/components/install.html)了解更详细的内容
+3. `uni.scss`引入全局scss变量文件
+/* uni.scss */
+@import "uview-ui/theme.scss";
-## 快速上手
+4. `pages.json`配置easycom规则(按需引入)
+// pages.json
+{
+ "easycom": {
+ // npm安装的方式不需要前面的"@/",下载安装的方式需要"@/"
+ // npm安装方式
+ "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
+ // 下载安装方式
+ // "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+ },
+ // 此为本身已有的内容
+ "pages": [
+ // ......
+ ]
+}
-请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
+请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
## 使用方法
配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
```html
<template>
- <u-button text="按钮"></u-button>
+ <u-button>按钮</u-button>
</template>
```
+## 链接
+- [官方文档](https://uviewui.com/)
+- [更新日志](https://uviewui.com/components/changelog.html)
+- [升级指南](https://uviewui.com/components/changelog.html)
+- [关于我们](https://uviewui.com/cooperation/about.html)
+## 预览
+您可以通过**微信**扫码,查看最佳的演示效果。
+<br>
+<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
+<!-- ## 捐赠uView的研发
+uView文档和源码全部开源免费,如果您认为uView帮到了您的开发工作,您可以捐赠uView的研发工作,捐赠无门槛,哪怕是一杯可乐也好(相信这比打赏主播更有意义)。
+<img src="https://uviewui.com/common/wechat.png" width="220" >
+<img style="margin-left: 100px;" src="https://uviewui.com/common/alipay.png" width="220" >
+ -->
## 版权信息
uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。
@@ -1,357 +0,0 @@
-## 2.0.34(2022-09-25)
-# uView2.0重磅发布,利剑出鞘,一统江湖
-1. `u-input`、`u-textarea`增加`ignoreCompositionEvent`属性
-2. 修复`route`方法调用可能报错的问题
-3. 修复`u-no-network`组件`z-index`无效的问题
-4. 修复`textarea`组件在h5上confirmType=""报错的问题
-5. `u-rate`适配`nvue`
-6. 优化验证手机号码的正则表达式(根据工信部发布的《电信网编号计划(2017年版)》进行修改。)
-7. `form-item`添加`labelPosition`属性
-8. `u-calendar`修复`maxDate`设置为当前日期,并且当前时间大于08:00时无法显示日期列表的问题 (#724)
-9. `u-radio`增加一个默认插槽用于自定义修改label内容 (#680)
-10. 修复`timeFormat`函数在safari重的兼容性问题 (#664)
-## 2.0.33(2022-06-17)
-1. 修复`loadmore`组件`lineColor`类型错误问题
-2. 修复`u-parse`组件`imgtap`、`linktap`不生效问题
-## 2.0.32(2022-06-16)
-1. `u-loadmore`新增自定义颜色、虚/实线
-2. 修复`u-swiper-action`组件部分平台不能上下滑动的问题
-3. 修复`u-list`回弹问题
-4. 修复`notice-bar`组件动画在低端安卓机可能会抖动的问题
-5. `u-loading-page`添加控制图标大小的属性`iconSize`
-6. 修复`u-tooltip`组件`color`参数不生效的问题
-7. 修复`u--input`组件使用`blur`事件输出为`undefined`的bug
-8. `u-code-input`组件新增键盘弹起时,是否自动上推页面参数`adjustPosition`
-9. 修复`image`组件`load`事件无回调对象问题
-10. 修复`button`组件`loadingSize`设置无效问题
-10. 其他修复
-## 2.0.31(2022-04-19)
-1. 修复`upload`在`vue`页面上传成功后没有成功标志的问题
-2. 解决演示项目中微信小程序模拟上传图片一直出于上传中问题
-3. 修复`u-code-input`组件在`nvue`页面编译到`app`平台上光标异常问题(`app`去除此功能)
-4. 修复`actionSheet`组件标题关闭按钮点击事件名称错误的问题
-5. 其他修复
-## 2.0.30(2022-04-04)
-1. `u-rate`增加`readonly`属性
-2. `tabs`滑块支持设置背景图片
-3. 修复`u-subsection` `mode`为`subsection`时,滑块样式不正确的问题
-4. `u-code-input`添加光标效果动画
-5. 修复`popup`的`open`事件不触发
-6. 修复`u-flex-column`无效的问题
-7. 修复`u-datetime-picker`索引在特定场合异常问题
-8. 修复`u-datetime-picker`最小时间字符串模板错误问题
-9. `u-swiper`添加`m3u8`验证
-10. `u-swiper`修改判断image和video逻辑
-11. 修复`swiper`无法使用本地图片问题,增加`type`参数
-12. 修复`u-row-notice`格式错误问题
-13. 修复`u-switch`组件当`unit`为`rpx`时,`nodeStyle`消失的问题
-14. 修复`datetime-picker`组件`showToolbar`与`visibleItemCount`属性无效的问题
-15. 修复`upload`组件条件编译位置判断错误,导致`previewImage`属性设置为`false`时,整个组件都会被隐藏的问题
-16. 修复`u-checkbox-group`设置`shape`属性无效的问题
-17. 修复`u-upload`的`capture`传入字符串的时候不生效的问题
-18. 修复`u-action-sheet`组件,关闭事件逻辑错误的问题
-19. 修复`u-list`触顶事件的触发错误的问题
-20. 修复`u-text`只有手机号可拨打的问题
-21. 修复`u-textarea`不能换行的问题
-22. 其他修复
-## 2.0.29(2022-03-13)
-1. 修复`u--text`组件设置`decoration`属性未生效的问题
-2. 修复`u-datetime-picker`使用`formatter`后返回值不正确
-3. 修复`u-datetime-picker` `intercept` 可能为undefined
-4. 修复已设置单位 uni..config.unit = 'rpx'时,线型指示器 `transform` 的位置翻倍,导致指示器超出宽度
-5. 修复mixin中bem方法生成的类名在支付宝和字节小程序中失效
-6. 修复默认值传值为空的时候,打开`u-datetime-picker`报错,不能选中第一列时间的bug
-7. 修复`u-datetime-picker`使用`formatter`后返回值不正确
-8. 修复`u-image`组件`loading`无效果的问题
-9. 修复`config.unit`属性设为`rpx`时,导航栏占用高度不足导致塌陷的问题
-10. 修复`u-datetime-picker`组件`itemHeight`无效问题
-11. 其他修复
-## 2.0.28(2022-02-22)
-1. search组件新增searchIconSize属性
-2. 兼容Safari/Webkit中传入时间格式如2022-02-17 12:00:56
-3. 修复text value.js 判断日期出format错误问题
-4. priceFormat格式化金额出现精度错误
-5. priceFormat在部分情况下出现精度损失问题
-6. 优化表单rules提示
-7. 修复avatar组件src为空时,展示状态不对
-8. 其他修复
-## 2.0.27(2022-01-28)
-1.样式修复
-## 2.0.26(2022-01-28)
-## 2.0.25(2022-01-27)
-1. 修复text组件mode=price时,可能会导致精度错误的问题
-2. 添加$u.setConfig()方法,可设置uView内置的config, props, zIndex, color属性,详见:[修改uView内置配置方案](https://uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE)
-3. 优化form组件在errorType=toast时,如果输入错误页面会有抖动的问题
-4. 修复$u.addUnit()对配置默认单位可能无效的问题
-## 2.0.24(2022-01-25)
-1. 修复swiper在current指定非0时缩放有误
-2. 修复u-icon添加stop属性的时候报错
-3. 优化遗留的通过正则判断rpx单位的问题
-4. 优化Layout布局 vue使用gutter时,会超出固定区域
-5. 优化search组件高度单位问题(rpx -> px)
-6. 修复u-image slot 加载和错误的图片失去了高度
-7. 修复u-index-list中footer插槽与header插槽存在性判断错误
-8. 修复部分机型下u-popup关闭时会闪烁
-9. 修复u-image在nvue-app下失去宽高
-10. 修复u-popup运行报错
-11. 修复u-tooltip报错
-12. 修复box-sizing在app下的警告
-13. 修复u-navbar在小程序中报运行时错误
-14. 其他修复
-## 2.0.23(2022-01-24)
-1. 修复image组件在hx3.3.9的nvue下可能会显示异常的问题
-2. 修复col组件gutter参数带rpx单位处理不正确的问题
-3. 修复text组件单行时无法显示省略号的问题
-4. navbar添加titleStyle参数
-5. 升级到hx3.3.9可消除nvue下控制台样式警告的问题
-## 2.0.22(2022-01-19)
-1. $u.page()方法优化,避免在特殊场景可能报错的问题
-2. picker组件添加immediateChange参数
-3. 新增$u.pages()方法
-## 2.0.21(2022-01-19)
-1. 优化:form组件在用户设置rules的时候提示用户model必传
-2. 优化遗留的通过正则判断rpx单位的问题
-3. 修复微信小程序环境中tabbar组件开启safeAreaInsetBottom属性后,placeholder高度填充不正确
-4. 修复swiper在current指定非0时缩放有误
-5. 修复u-icon添加stop属性的时候报错
-6. 修复upload组件在accept=all的时候没有作用
-7. 修复在text组件mode为phone时call属性无效的问题
-8. 处理u-form clearValidate方法
-9. 其他修复
-## 2.0.20(2022-01-14)
-1. 修复calendar默认会选择一个日期,如果直接点确定的话,无法取到值的问题
-2. 修复Slider缺少disabled props 还有注释
-3. 修复u-notice-bar点击事件无法拿到index索引值的问题
-4. 修复u-collapse-item在vue文件下,app端自定义插槽不生效的问题
-5. 优化头像为空时显示默认头像
-6. 修复图片地址赋值后判断加载状态为完成问题
-7. 修复日历滚动到默认日期月份区域
-8. search组件暴露点击左边icon事件
-9. 修复u-form clearValidate方法不生效
-10. upload h5端增加返回文件参数(文件的name参数)
-11. 处理upload选择文件后url为blob类型无法预览的问题
-12. u-code-input 修复输入框没有往左移出一半屏幕
-13. 修复Upload上传 disabled为true时,控制台报hoverClass类型错误
-14. 临时处理ios app下grid点击坍塌问题
-15. 其他修复
-## 2.0.19(2021-12-29)
-1. 优化微信小程序包体积可在微信中预览,请升级HbuilderX3.3.4,同时在“运行->运行到小程序模拟器”中勾选“运行时是否压缩代码”
-2. 优化微信小程序setData性能,处理某些方法如$u.route()无法在模板中使用的问题
-3. navbar添加autoBack参数
-4. 允许avatar组件的事件冒泡
-5. 修复cell组件报错问题
-6. 其他修复
-## 2.0.18(2021-12-28)
-1. 修复app端编译报错问题
-2. 重新处理微信小程序端setData过大的性能问题
-3. 修复边框问题
-4. 修复最大最小月份不大于0则没有数据出现的问题
-5. 修复SwipeAction微信小程序端无法上下滑动问题
-6. 修复input的placeholder在小程序端默认显示为true问题
-7. 修复divider组件click事件无效问题
-8. 修复u-code-input maxlength 属性值为 String 类型时显示异常
-9. 修复当 grid只有 1到2时 在小程序端algin设置无效的问题
-10. 处理form-item的label为top时,取消错误提示的左边距
-## 2.0.17(2021-12-26)
-## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583)
-1. 解决HBuilderX3.3.3.20211225版本导致的样式问题
-2. calendar日历添加monthNum参数
-3. navbar添加center slot
-## 2.0.16(2021-12-25)
-1. 解决微信小程序setData性能问题
-2. 修复count-down组件change事件不触发问题
-## 2.0.15(2021-12-21)
-1. 修复Cell单元格titleWidth无效
-2. 修复cheakbox组件ischecked不更新
-3. 修复keyboard是否显示"."按键默认值问题
-4. 修复number-keyboard是否显示键盘的"."符号问题
-5. 修复Input输入框 readonly无效
-6. 修复u-avatar 导致打包app、H5时候报错问题
-7. 修复Upload上传deletable无效
-8. 修复upload当设置maxSize时无效的问题
-9. 修复tabs lineWidth传入带单位的字符串的时候偏移量计算错误问题
-10. 修复rate组件在有padding的view内,显示的星星位置和可触摸区域不匹配,无法正常选中星星
-## 2.0.13(2021-12-14)
-## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY)
-1. 修复配置默认单位为rpx可能会导致自定义导航栏高度异常的问题
-## 2.0.12(2021-12-14)
-1. 修复tabs组件在vue环境下划线消失的问题
-2. 修复upload组件在安卓小程序无法选择视频的问题
-3. 添加uni.$u.config.unit配置,用于配置参数默认单位,详见:[默认单位配置](https://www.uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE)
-4. 修复textarea组件在没绑定v-model时,字符统计不生效问题
-5. 修复nvue下控制是否出现滚动条失效问题
-## 2.0.11(2021-12-13)
-1. text组件align参数无效的问题
-2. subsection组件添加keyName参数
-3. upload组件无法判断[Object file]类型的问题
-4. 处理notify层级过低问题
-5. codeInput组件添加disabledDot参数
-6. 处理actionSheet组件round参数无效的问题
-7. calendar组件添加round参数用于控制圆角值
-8. 处理swipeAction组件在vue环境下默认被打开的问题
-9. button组件的throttleTime节流参数无效的问题
-10. 解决u-notify手动关闭方法close()无效的问题
-11. input组件readonly不生效问题
-12. tag组件type参数为info不生效问题
-## 2.0.10(2021-12-08)
-1. 修复button sendMessagePath属性不生效
-2. 修复DatetimePicker选择器title无效
-3. 修复u-toast设置loading=true不生效
-4. 修复u-text金额模式传0报错
-5. 修复u-toast组件的icon属性配置不生效
-6. button的icon在特殊场景下的颜色优化
-7. IndexList优化,增加#
-## 2.0.9(2021-12-01)
-## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
-1. 优化swiper的height支持100%值(仅vue有效),修复嵌入视频时click事件无法触发的问题
-2. 优化tabs组件对list值为空的判断,或者动态变化list时重新计算相关尺寸的问题
-3. 优化datetime-picker组件逻辑,让其后续打开的默认值为上一次的选中值,需要通过v-model绑定值才有效
-4. 修复upload内嵌在其他组件中,选择图片可能不会换行的问题
-## 2.0.8(2021-12-01)
-1. 修复toast的position参数无效问题
-2. 处理input在ios nvue上无法获得焦点的问题
-3. avatar-group组件添加extraValue参数,让剩余展示数量可手动控制
-4. tabs组件添加keyName参数用于配置从对象中读取的键名
-5. 处理text组件名字脱敏默认配置无效的问题
-6. 处理picker组件item文本太长换行问题
-## 2.0.7(2021-11-30)
-1. 修复radio和checkbox动态改变v-model无效的问题。
-2. 优化form规则validator在微信小程序用法
-3. 修复backtop组件mode参数在微信小程序无效的问题
-4. 处理Album的previewFullImage属性无效的问题
-5. 处理u-datetime-picker组件mode='time'在选择改变时间时,控制台报错的问题
-## 2.0.6(2021-11-27)
-1. 处理tag组件在vue下边框无效的问题。
-2. 处理popup组件圆角参数可能无效的问题。
-3. 处理tabs组件lineColor参数可能无效的问题。
-4. propgress组件在值很小时,显示异常的问题。
-## 2.0.5(2021-11-25)
-1. calendar在vue下显示异常问题。
-2. form组件labelPosition和errorType参数无效的问题
-3. input组件inputAlign无效的问题
-4. 其他一些修复
-## 2.0.4(2021-11-23)
-0. input组件缺失@confirm事件,以及subfix和prefix无效问题
-1. component.scss文件样式在vue下干扰全局布局问题
-2. 修复subsection在vue环境下表现异常的问题
-3. tag组件的bgColor等参数无效的问题
-4. upload组件不换行的问题
-5. 其他的一些修复处理
-## 2.0.3(2021-11-16)
-## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
-1. uView2.0已实现全面兼容nvue
-2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升
-3. 目前uView2.0为公测阶段,相关细节可能会有变动
-4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
-5. 处理modal的confirm回调事件拼写错误问题
-6. 处理input组件@input事件参数错误问题
-7. 其他一些修复
-## 2.0.2(2021-11-16)
-5. 修复input组件formatter参数缺失问题
-6. 优化loading-icon组件的scss写法问题,防止不兼容新版本scss
-## 2.0.0(2020-11-15)
@@ -1,78 +0,0 @@
-<template>
- <uvForm
- ref="uForm"
- :model="model"
- :rules="rules"
- :errorType="errorType"
- :borderBottom="borderBottom"
- :labelPosition="labelPosition"
- :labelWidth="labelWidth"
- :labelAlign="labelAlign"
- :labelStyle="labelStyle"
- :customStyle="customStyle"
- >
- <slot />
- </uvForm>
-</template>
-<script>
- /**
- * 此组件存在的理由是,在nvue下,u-form被uni-app官方占用了,u-form在nvue中相当于form组件
- * 所以在nvue下,取名为u--form,内部其实还是u-form.vue,只不过做一层中转
- */
- import uvForm from '../u-form/u-form.vue';
- import props from '../u-form/props.js'
- export default {
- // #ifdef MP-WEIXIN
- name: 'u-form',
- // #endif
- // #ifndef MP-WEIXIN
- name: 'u--form',
- mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
- components: {
- uvForm
- },
- created() {
- this.children = []
- methods: {
- // 手动设置校验的规则,如果规则中有函数的话,微信小程序中会过滤掉,所以只能手动调用设置规则
- setRules(rules) {
- this.$refs.uForm.setRules(rules)
- validate() {
- * 在微信小程序中,通过this.$parent拿到的父组件是u--form,而不是其内嵌的u-form
- * 导致在u-form组件中,拿不到对应的children数组,从而校验无效,所以这里每次调用u-form组件中的
- * 对应方法的时候,在小程序中都先将u--form的children赋值给u-form中的children
- this.setMpData()
- return this.$refs.uForm.validate()
- validateField(value, callback) {
- return this.$refs.uForm.validateField(value, callback)
- resetFields() {
- return this.$refs.uForm.resetFields()
- clearValidate(props) {
- return this.$refs.uForm.clearValidate(props)
- setMpData() {
- this.$refs.uForm.children = this.children
- }
-</script>
@@ -1,47 +0,0 @@
- <uvImage
- :src="src"
- :mode="mode"
- :width="width"
- :height="height"
- :shape="shape"
- :radius="radius"
- :lazyLoad="lazyLoad"
- :showMenuByLongpress="showMenuByLongpress"
- :loadingIcon="loadingIcon"
- :errorIcon="errorIcon"
- :showLoading="showLoading"
- :showError="showError"
- :fade="fade"
- :webp="webp"
- :duration="duration"
- :bgColor="bgColor"
- @click="$emit('click')"
- @error="$emit('error')"
- @load="$emit('load')"
- <template v-slot:loading>
- <slot name="loading"></slot>
- </template>
- <template v-slot:error>
- <slot name="error"></slot>
- </uvImage>
- * 此组件存在的理由是,在nvue下,u-image被uni-app官方占用了,u-image在nvue中相当于image组件
- * 所以在nvue下,取名为u--image,内部其实还是u-iamge.vue,只不过做一层中转
- import uvImage from '../u-image/u-image.vue';
- import props from '../u-image/props.js';
- name: 'u--image',
- uvImage
@@ -1,73 +0,0 @@
- <uvInput
- :value="value"
- :type="type"
- :fixed="fixed"
- :disabled="disabled"
- :disabledColor="disabledColor"
- :clearable="clearable"
- :password="password"
- :maxlength="maxlength"
- :placeholder="placeholder"
- :placeholderClass="placeholderClass"
- :placeholderStyle="placeholderStyle"
- :showWordLimit="showWordLimit"
- :confirmType="confirmType"
- :confirmHold="confirmHold"
- :holdKeyboard="holdKeyboard"
- :focus="focus"
- :autoBlur="autoBlur"
- :disableDefaultPadding="disableDefaultPadding"
- :cursor="cursor"
- :cursorSpacing="cursorSpacing"
- :selectionStart="selectionStart"
- :selectionEnd="selectionEnd"
- :adjustPosition="adjustPosition"
- :inputAlign="inputAlign"
- :fontSize="fontSize"
- :color="color"
- :prefixIcon="prefixIcon"
- :suffixIcon="suffixIcon"
- :suffixIconStyle="suffixIconStyle"
- :prefixIconStyle="prefixIconStyle"
- :border="border"
- :readonly="readonly"
- :formatter="formatter"
- :ignoreCompositionEvent="ignoreCompositionEvent"
- @focus="$emit('focus')"
- @blur="e => $emit('blur', e)"
- @keyboardheightchange="$emit('keyboardheightchange')"
- @change="e => $emit('change', e)"
- @input="e => $emit('input', e)"
- @confirm="e => $emit('confirm', e)"
- @clear="$emit('clear')"
- <!-- #ifdef MP -->
- <slot name="prefix"></slot>
- <slot name="suffix"></slot>
- <!-- #endif -->
- <!-- #ifndef MP -->
- <slot name="prefix" slot="prefix"></slot>
- <slot name="suffix" slot="suffix"></slot>
- </uvInput>
- * 此组件存在的理由是,在nvue下,u-input被uni-app官方占用了,u-input在nvue中相当于input组件
- * 所以在nvue下,取名为u--input,内部其实还是u-input.vue,只不过做一层中转
- import uvInput from '../u-input/u-input.vue';
- import props from '../u-input/props.js'
- name: 'u--input',
- uvInput
@@ -1,44 +0,0 @@
- <uvText
- :show="show"
- :text="text"
- :href="href"
- :format="format"
- :call="call"
- :openType="openType"
- :bold="bold"
- :block="block"
- :lines="lines"
- :decoration="decoration"
- :size="size"
- :iconStyle="iconStyle"
- :margin="margin"
- :lineHeight="lineHeight"
- :align="align"
- :wordWrap="wordWrap"
- ></uvText>
-/**
- * 此组件存在的理由是,在nvue下,u-text被uni-app官方占用了,u-text在nvue中相当于input组件
- * 所以在nvue下,取名为u--input,内部其实还是u-text.vue,只不过做一层中转
- * 不使用v-bind="$attrs",而是分开独立写传参,是因为微信小程序不支持此写法
-import uvText from "../u-text/u-text.vue";
-import props from "../u-text/props.js";
-export default {
- name: "u--text",
- uvText,
-};
@@ -1,48 +0,0 @@
- <uvTextarea
- :count="count"
- :autoHeight="autoHeight"
- :showConfirmBar="showConfirmBar"
- @focus="e => $emit('focus')"
- @blur="e => $emit('blur')"
- @linechange="e => $emit('linechange', e)"
- @confirm="e => $emit('confirm')"
- @keyboardheightchange="e => $emit('keyboardheightchange')"
- ></uvTextarea>
- * 此组件存在的理由是,在nvue下,u--textarea被uni-app官方占用了,u-textarea在nvue中相当于textarea组件
- * 所以在nvue下,取名为u--textarea,内部其实还是u-textarea.vue,只不过做一层中转
- import uvTextarea from '../u-textarea/u-textarea.vue';
- import props from '../u-textarea/props.js'
- name: 'u--textarea',
- uvTextarea
@@ -1,54 +0,0 @@
- props: {
- // 操作菜单是否展示 (默认false)
- show: {
- type: Boolean,
- default: uni.$u.props.actionSheet.show
- // 标题
- title: {
- type: String,
- default: uni.$u.props.actionSheet.title
- // 选项上方的描述信息
- description: {
- default: uni.$u.props.actionSheet.description
- // 数据
- actions: {
- type: Array,
- default: uni.$u.props.actionSheet.actions
- // 取消按钮的文字,不为空时显示按钮
- cancelText: {
- default: uni.$u.props.actionSheet.cancelText
- // 点击某个菜单项时是否关闭弹窗
- closeOnClickAction: {
- default: uni.$u.props.actionSheet.closeOnClickAction
- // 处理底部安全区(默认true)
- safeAreaInsetBottom: {
- default: uni.$u.props.actionSheet.safeAreaInsetBottom
- // 小程序的打开方式
- openType: {
- default: uni.$u.props.actionSheet.openType
- // 点击遮罩是否允许关闭 (默认true)
- closeOnClickOverlay: {
- default: uni.$u.props.actionSheet.closeOnClickOverlay
- // 圆角值
- round: {
- type: [Boolean, String, Number],
- default: uni.$u.props.actionSheet.round
-}
@@ -1,278 +1,190 @@
- <u-popup
- mode="bottom"
- @close="closeHandler"
- :safeAreaInsetBottom="safeAreaInsetBottom"
- :round="round"
- <view class="u-action-sheet">
- <view
- class="u-action-sheet__header"
- v-if="title"
+ <u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="value" :maskCloseAble="maskCloseAble"
+ length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex">
+ <view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
+ {{tips.text}}
+ </view>
+ <block v-for="(item, index) in list" :key="index">
+ <view
+ @touchmove.stop.prevent
+ @tap="itemClick(index)"
+ :style="[itemStyle(index)]"
+ class="u-action-sheet-item u-line-1"
+ :class="[index < list.length - 1 ? 'u-border-bottom' : '']"
+ :hover-stay-time="150"
>
- <text class="u-action-sheet__header__title u-line-1">{{title}}</text>
- class="u-action-sheet__header__icon-wrap"
- @tap.stop="cancel"
- <u-icon
- name="close"
- size="17"
- color="#c8c9cc"
- bold
- ></u-icon>
- </view>
- <text
- class="u-action-sheet__description"
- :style="[{
- marginTop: `${title && description ? 0 : '18px'}`
- }]"
- v-if="description"
- >{{description}}</text>
- <slot>
- <u-line v-if="description"></u-line>
- <view class="u-action-sheet__item-wrap">
- <template v-for="(item, index) in actions">
- <button
- :key="index"
- class="u-reset-button"
- :openType="item.openType"
- @getuserinfo="onGetUserInfo"
- @contact="onContact"
- @getphonenumber="onGetPhoneNumber"
- @error="onError"
- @launchapp="onLaunchApp"
- @opensetting="onOpenSetting"
- :lang="lang"
- :session-from="sessionFrom"
- :send-message-title="sendMessageTitle"
- :send-message-path="sendMessagePath"
- :send-message-img="sendMessageImg"
- :show-message-card="showMessageCard"
- :app-parameter="appParameter"
- @tap="selectHandler(index)"
- :hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
- class="u-action-sheet__item-wrap__item"
- @tap.stop="selectHandler(index)"
- :hover-stay-time="150"
- <template v-if="!item.loading">
- class="u-action-sheet__item-wrap__item__name"
- :style="[itemStyle(index)]"
- >{{ item.name }}</text>
- v-if="item.subname"
- class="u-action-sheet__item-wrap__item__subname"
- >{{ item.subname }}</text>
- <u-loading-icon
- v-else
- custom-class="van-action-sheet__loading"
- size="18"
- mode="circle"
- />
- </button>
- <u-line v-if="index !== actions.length - 1"></u-line>
- </slot>
- <u-gap
- bgColor="#eaeaec"
- height="6"
- v-if="cancelText"
- ></u-gap>
- <view hover-class="u-action-sheet--hover">
- @touchmove.stop.prevent
- class="u-action-sheet__cancel-text"
- @tap="cancel"
- >{{cancelText}}</text>
+ <text>{{item.text}}</text>
+ <text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{item.subText}}</text>
</view>
+ </block>
+ <view class="u-gab" v-if="cancelBtn">
+ <view @touchmove.stop.prevent class="u-actionsheet-cancel u-action-sheet-item" hover-class="u-hover-class"
+ :hover-stay-time="150" v-if="cancelBtn" @tap="close">{{cancelText}}</view>
</u-popup>
<script>
- import openType from '../../libs/mixin/openType'
- import button from '../../libs/mixin/button'
- import props from './props.js';
/**
- * ActionSheet 操作菜单
+ * actionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
* @tutorial https://www.uviewui.com/components/actionSheet.html
- *
- * @property {Boolean} show 操作菜单是否展示 (默认 false )
- * @property {String} title 操作菜单标题
- * @property {String} description 选项上方的描述信息
- * @property {Array<Object>} actions 按钮的文字数组,见官方文档示例
- * @property {String} cancelText 取消按钮的提示文字,不为空时显示按钮
- * @property {Boolean} closeOnClickAction 点击某个菜单项时是否关闭弹窗 (默认 true )
- * @property {Boolean} safeAreaInsetBottom 处理底部安全区 (默认 true )
- * @property {String} openType 小程序的打开方式 (contact | launchApp | getUserInfo | openSetting |getPhoneNumber |error )
- * @property {Boolean} closeOnClickOverlay 点击遮罩是否允许关闭 (默认 true )
- * @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
- * @property {String} lang 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文
- * @property {String} sessionFrom 会话来源,openType="contact"时有效
- * @property {String} sendMessageTitle 会话内消息卡片标题,openType="contact"时有效
- * @property {String} sendMessagePath 会话内消息卡片点击跳转小程序路径,openType="contact"时有效
- * @property {String} sendMessageImg 会话内消息卡片图片,openType="contact"时有效
- * @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效 (默认 false )
- * @property {String} appParameter 打开 APP 时,向 APP 传递的参数,openType=launchApp 时有效
- * @event {Function} select 点击ActionSheet列表项时触发
- * @event {Function} close 点击取消按钮时触发
- * @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与 wx.getUserInfo 返回的一致,openType="getUserInfo"时有效
- * @event {Function} contact 客服消息回调,openType="contact"时有效
- * @event {Function} getphonenumber 获取用户手机号回调,openType="getPhoneNumber"时有效
- * @event {Function} error 当使用开放能力时,发生错误的回调,openType="error"时有效
- * @event {Function} launchapp 打开 APP 成功的回调,openType="launchApp"时有效
- * @event {Function} opensetting 在打开授权设置页后回调,openType="openSetting"时有效
- * @example <u-action-sheet :actions="list" :title="title" :show="show"></u-action-sheet>
+ * @property {Array<Object>} list 按钮的文字数组,见官方文档示例
+ * @property {Object} tips 顶部的提示文字,见官方文档示例
+ * @property {String} cancel-text 取消按钮的提示文字
+ * @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
+ * @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
+ * @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Number String} z-index z-index值(默认1075)
+ * @event {Function} click 点击ActionSheet列表项时触发
+ * @event {Function} close 点击取消按钮时触发
+ * @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
*/
export default {
name: "u-action-sheet",
- // 一些props参数和methods方法,通过mixin混入,因为其他文件也会用到
- mixins: [openType, button, uni.$u.mixin, props],
- data() {
- return {
+ props: {
+ // 点击遮罩是否可以关闭actionsheet
+ maskCloseAble: {
+ type: Boolean,
+ default: true
+ // 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
+ list: {
+ type: Array,
+ default () {
+ // 如下
+ // return [{
+ // text: '确定',
+ // color: '',
+ // fontSize: ''
+ // }]
+ return [];
+ }
+ // 顶部的提示文字
+ tips: {
+ type: Object,
+ return {
+ text: '',
+ color: '',
+ fontSize: '26'
+ // 底部的取消按钮
+ cancelBtn: {
+ // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+ safeAreaInsetBottom: {
+ default: false
+ // 通过双向绑定控制组件的弹出与收起
+ value: {
+ // 弹出的顶部圆角值
+ borderRadius: {
+ type: [String, Number],
+ default: 0
+ // 弹出的z-index值
+ zIndex: {
+ // 取消按钮的文字提示
+ cancelText: {
+ type: String,
+ default: '取消'
}
},
computed: {
+ // 顶部提示的样式
+ tipsStyle() {
+ let style = {};
+ if (this.tips.color) style.color = this.tips.color;
+ if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx';
+ return style;
// 操作项目的样式
itemStyle() {
return (index) => {
let style = {};
- if (this.actions[index].color) style.color = this.actions[index].color
- if (this.actions[index].fontSize) style.fontSize = uni.$u.addUnit(this.actions[index].fontSize)
+ if (this.list[index].color) style.color = this.list[index].color;
+ if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx';
// 选项被禁用的样式
- if (this.actions[index].disabled) style.color = '#c0c4cc'
+ if (this.list[index].disabled) style.color = '#c0c4cc';
return style;
+ uZIndex() {
+ // 如果用户有传递z-index值,优先使用
+ return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
methods: {
- closeHandler() {
- // 允许点击遮罩关闭时,才发出close事件
- if(this.closeOnClickOverlay) {
- this.$emit('close')
// 点击取消按钮
- cancel() {
+ close() {
+ // 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
+ // 这是一个vue发送事件的特殊用法
+ this.popupClose();
+ this.$emit('close');
- selectHandler(index) {
- const item = this.actions[index]
- if (item && !item.disabled && !item.loading) {
- this.$emit('select', item)
- if (this.closeOnClickAction) {
+ // 弹窗关闭
+ popupClose() {
+ this.$emit('input', false);
+ // 点击某一个item
+ itemClick(index) {
+ // disabled的项禁止点击
+ if(this.list[index].disabled) return;
+ this.$emit('click', index);
</script>
<style lang="scss" scoped>
- @import "../../libs/css/components.scss";
- $u-action-sheet-reset-button-width:100% !default;
- $u-action-sheet-title-font-size: 16px !default;
- $u-action-sheet-title-padding: 12px 30px !default;
- $u-action-sheet-title-color: $u-main-color !default;
- $u-action-sheet-header-icon-wrap-right:15px !default;
- $u-action-sheet-header-icon-wrap-top:15px !default;
- $u-action-sheet-description-font-size:13px !default;
- $u-action-sheet-description-color:14px !default;
- $u-action-sheet-description-margin: 18px 15px !default;
- $u-action-sheet-item-wrap-item-padding:15px !default;
- $u-action-sheet-item-wrap-name-font-size:16px !default;
- $u-action-sheet-item-wrap-subname-font-size:13px !default;
- $u-action-sheet-item-wrap-subname-color: #c0c4cc !default;
- $u-action-sheet-item-wrap-subname-margin-top:10px !default;
- $u-action-sheet-cancel-text-font-size:16px !default;
- $u-action-sheet-cancel-text-color:$u-content-color !default;
- $u-action-sheet-cancel-text-font-size:15px !default;
- $u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
+ @import "../../libs/css/style.components.scss";
- .u-reset-button {
- width: $u-action-sheet-reset-button-width;
- .u-action-sheet {
+ .u-tips {
+ font-size: 26rpx;
text-align: center;
- &__header {
- position: relative;
- padding: $u-action-sheet-title-padding;
- &__title {
- font-size: $u-action-sheet-title-font-size;
- color: $u-action-sheet-title-color;
- font-weight: bold;
- text-align: center;
- &__icon-wrap {
- position: absolute;
- right: $u-action-sheet-header-icon-wrap-right;
- top: $u-action-sheet-header-icon-wrap-top;
- &__description {
- font-size: $u-action-sheet-description-font-size;
- color: $u-tips-color;
- margin: $u-action-sheet-description-margin;
- &__item-wrap {
- &__item {
- padding: $u-action-sheet-item-wrap-item-padding;
- @include flex;
- align-items: center;
- justify-content: center;
- flex-direction: column;
- &__name {
- font-size: $u-action-sheet-item-wrap-name-font-size;
- color: $u-main-color;
+ padding: 34rpx 0;
+ line-height: 1;
+ color: $u-tips-color;
- &__subname {
- font-size: $u-action-sheet-item-wrap-subname-font-size;
- color: $u-action-sheet-item-wrap-subname-color;
- margin-top: $u-action-sheet-item-wrap-subname-margin-top;
+ .u-action-sheet-item {
+ @include vue-flex;;
+ justify-content: center;
+ align-items: center;
+ font-size: 32rpx;
+ flex-direction: column;
+ .u-action-sheet-item__subtext {
+ font-size: 24rpx;
+ margin-top: 20rpx;
- &__cancel-text {
- font-size: $u-action-sheet-cancel-text-font-size;
- color: $u-action-sheet-cancel-text-color;
- padding: $u-action-sheet-cancel-text-font-size;
+ .u-gab {
+ height: 12rpx;
+ background-color: rgb(234, 234, 236);
- &--hover {
- background-color: $u-action-sheet-cancel-text-hover-background-color;
+ .u-actionsheet-cancel {
+ color: $u-main-color;
</style>
@@ -1,59 +0,0 @@
- // 图片地址,Array<String>|Array<Object>形式
- urls: {
- default: uni.$u.props.album.urls
- // 指定从数组的对象元素中读取哪个属性作为图片地址
- keyName: {
- default: uni.$u.props.album.keyName
- // 单图时,图片长边的长度
- singleSize: {
- type: [String, Number],
- default: uni.$u.props.album.singleSize
- // 多图时,图片边长
- multipleSize: {
- default: uni.$u.props.album.multipleSize
- // 多图时,图片水平和垂直之间的间隔
- space: {
- default: uni.$u.props.album.space
- // 单图时,图片缩放裁剪的模式
- singleMode: {
- default: uni.$u.props.album.singleMode
- // 多图时,图片缩放裁剪的模式
- multipleMode: {
- default: uni.$u.props.album.multipleMode
- // 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量
- maxCount: {
- default: uni.$u.props.album.maxCount
- // 是否可以预览图片
- previewFullImage: {
- default: uni.$u.props.album.previewFullImage
- // 每行展示图片数量,如设置,singleSize和multipleSize将会无效
- rowCount: {
- default: uni.$u.props.album.rowCount
- // 超出maxCount时是否显示查看更多的提示
- showMore: {
- default: uni.$u.props.album.showMore
@@ -1,259 +0,0 @@
- <view class="u-album">
- class="u-album__row"
- ref="u-album__row"
- v-for="(arr, index) in showUrls"
- :forComputedUse="albumWidth"
- class="u-album__row__wrapper"
- v-for="(item, index1) in arr"
- :key="index1"
- :style="[imageStyle(index + 1, index1 + 1)]"
- @tap="previewFullImage ? onPreviewTap(getSrc(item)) : ''"
- <image
- :src="getSrc(item)"
- :mode="
- urls.length === 1
- ? imageHeight > 0
- ? singleMode
- : 'widthFix'
- : multipleMode
- "
- :style="[
- {
- width: imageWidth,
- height: imageHeight
- ]"
- ></image>
- v-if="
- showMore &&
- urls.length > rowCount * showUrls.length &&
- index === showUrls.length - 1 &&
- index1 === showUrls[showUrls.length - 1].length - 1
- class="u-album__row__wrapper__text"
- <u--text
- :text="`+${urls.length - maxCount}`"
- color="#fff"
- :size="multipleSize * 0.3"
- align="center"
- customStyle="justify-content: center"
- ></u--text>
-import props from './props.js'
-// #ifdef APP-NVUE
-// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
-const dom = uni.requireNativePlugin('dom')
-// #endif
- * Album 相册
- * @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
- * @tutorial https://www.uviewui.com/components/album.html
- * @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
- * @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
- * @property {String | Number} singleSize 单图时,图片长边的长度 (默认 180 )
- * @property {String | Number} multipleSize 多图时,图片边长 (默认 70 )
- * @property {String | Number} space 多图时,图片水平和垂直之间的间隔 (默认 6 )
- * @property {String} singleMode 单图时,图片缩放裁剪的模式 (默认 'scaleToFill' )
- * @property {String} multipleMode 多图时,图片缩放裁剪的模式 (默认 'aspectFill' )
- * @property {String | Number} maxCount 取消按钮的提示文字 (默认 9 )
- * @property {Boolean} previewFullImage 是否可以预览图片 (默认 true )
- * @property {String | Number} rowCount 每行展示图片数量,如设置,singleSize和multipleSize将会无效 (默认 3 )
- * @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true )
- * @event {Function} albumWidth 某些特殊的情况下,需要让文字与相册的宽度相等,这里事件的形式对外发送 (回调参数 width )
- * @example <u-album :urls="urls2" @albumWidth="width => albumWidth = width" multipleSize="68" ></u-album>
- name: 'u-album',
- mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
- // 单图的宽度
- singleWidth: 0,
- // 单图的高度
- singleHeight: 0,
- // 单图时,如果无法获取图片的尺寸信息,让图片宽度默认为容器的一定百分比
- singlePercent: 0.6
- watch: {
- immediate: true,
- handler(newVal) {
- if (newVal.length === 1) {
- this.getImageRect()
- computed: {
- imageStyle() {
- return (index1, index2) => {
- const { space, rowCount, multipleSize, urls } = this,
- { addUnit, addStyle } = uni.$u,
- rowLen = this.showUrls.length,
- allLen = this.urls.length
- const style = {
- marginRight: addUnit(space),
- marginBottom: addUnit(space)
- // 如果为最后一行,则每个图片都无需下边框
- if (index1 === rowLen) style.marginBottom = 0
- // 每行的最右边一张和总长度的最后一张无需右边框
- if (
- index2 === rowCount ||
- (index1 === rowLen &&
- index2 === this.showUrls[index1 - 1].length)
- )
- style.marginRight = 0
- return style
- // 将数组划分为二维数组
- showUrls() {
- const arr = []
- this.urls.map((item, index) => {
- // 限制最大展示数量
- if (index + 1 <= this.maxCount) {
- // 计算该元素为第几个素组内
- const itemIndex = Math.floor(index / this.rowCount)
- // 判断对应的索引是否存在
- if (!arr[itemIndex]) {
- arr[itemIndex] = []
- arr[itemIndex].push(item)
- })
- return arr
- imageWidth() {
- return uni.$u.addUnit(
- this.urls.length === 1 ? this.singleWidth : this.multipleSize
- imageHeight() {
- this.urls.length === 1 ? this.singleHeight : this.multipleSize
- // 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
- // 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
- albumWidth() {
- let width = 0
- if (this.urls.length === 1) {
- width = this.singleWidth
- } else {
- width =
- this.showUrls[0].length * this.multipleSize +
- this.space * (this.showUrls[0].length - 1)
- this.$emit('albumWidth', width)
- return width
- // 预览图片
- onPreviewTap(url) {
- const urls = this.urls.map((item) => {
- return this.getSrc(item)
- uni.previewImage({
- current: url,
- urls
- // 获取图片的路径
- getSrc(item) {
- return uni.$u.test.object(item)
- ? (this.keyName && item[this.keyName]) || item.src
- : item
- // 单图时,获取图片的尺寸
- // 在小程序中,需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
- // 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
- getImageRect() {
- const src = this.getSrc(this.urls[0])
- uni.getImageInfo({
- src,
- success: (res) => {
- // 判断图片横向还是竖向展示方式
- const isHorizotal = res.width >= res.height
- this.singleWidth = isHorizotal
- ? this.singleSize
- : (res.width / res.height) * this.singleSize
- this.singleHeight = !isHorizotal
- : (res.height / res.width) * this.singleWidth
- fail: () => {
- this.getComponentWidth()
- // 获取组件的宽度
- async getComponentWidth() {
- // 延时一定时间,以获取dom尺寸
- await uni.$u.sleep(30)
- // #ifndef APP-NVUE
- this.$uGetRect('.u-album__row').then((size) => {
- this.singleWidth = size.width * this.singlePercent
- // #ifdef APP-NVUE
- // 这里ref="u-album__row"所在的标签为通过for循环出来,导致this.$refs['u-album__row']是一个数组
- const ref = this.$refs['u-album__row'][0]
- ref &&
- dom.getComponentRect(ref, (res) => {
- this.singleWidth = res.size.width * this.singlePercent
-<style lang="scss" scoped>
-@import '../../libs/css/components.scss';
-.u-album {
- @include flex(column);
- &__row {
- @include flex(row);
- flex-wrap: wrap;
- &__wrapper {
- &__text {
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.3);
-</style>
@@ -0,0 +1,256 @@
+<template>
+ <view class="u-alert-tips" v-if="show" :class="[
+ !show ? 'u-close-alert-tips': '',
+ type ? 'u-alert-tips--bg--' + type + '-light' : '',
+ type ? 'u-alert-tips--border--' + type + '-disabled' : '',
+ ]" :style="{
+ backgroundColor: bgColor,
+ borderColor: borderColor
+ }">
+ <view class="u-icon-wrap">
+ <u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
+ <view class="u-alert-content" @tap.stop="click">
+ <view class="u-alert-title" :style="[uTitleStyle]">
+ {{title}}
+ <view v-if="description" class="u-alert-desc" :style="[descStyle]">
+ {{description}}
+ <u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
+ :size="22" class="u-close-icon" :style="{
+ top: description ? '18rpx' : '24rpx'
+ }"></u-icon>
+ <text v-if="closeAble && closeText" class="u-close-text" :style="{
+ }">{{closeText}}</text>
+</template>
+<script>
+ /**
+ * alertTips 警告提示
+ * @description 警告提示,展现需要关注的信息
+ * @tutorial https://uviewui.com/components/alertTips.html
+ * @property {String} title 显示的标题文字
+ * @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
+ * @property {String} type 关闭按钮(默认为叉号icon图标)
+ * @property {String} icon 图标名称
+ * @property {Object} icon-style 图标的样式,对象形式
+ * @property {Object} title-style 标题的样式,对象形式
+ * @property {Object} desc-style 描述的样式,对象形式
+ * @property {String} close-able 用文字替代关闭图标,close-able为true时有效
+ * @property {Boolean} show-icon 是否显示左边的辅助图标
+ * @property {Boolean} show 显示或隐藏组件
+ * @event {Function} click 点击组件时触发
+ * @event {Function} close 点击关闭按钮时触发
+ */
+ export default {
+ name: 'u-alert-tips',
+ // 显示文字
+ title: {
+ default: ''
+ // 主题,success/warning/info/error
+ type: {
+ default: 'warning'
+ // 辅助性文字
+ description: {
+ // 是否可关闭
+ closeAble: {
+ // 关闭按钮自定义文本
+ closeText: {
+ // 是否显示图标
+ showIcon: {
+ // 文字颜色,如果定义了color值,icon会失效
+ color: {
+ // 背景颜色
+ bgColor: {
+ // 边框颜色
+ borderColor: {
+ // 是否显示
+ show: {
+ // 左边显示的icon
+ icon: {
+ // icon的样式
+ iconStyle: {
+ default() {
+ return {}
+ // 标题的样式
+ titleStyle: {
+ // 描述文字的样式
+ descStyle: {
+ data() {
+ computed: {
+ uTitleStyle() {
+ // 如果有描述文字的话,标题进行加粗
+ style.fontWeight = this.description ? 500 : 'normal';
+ // 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
+ return this.$u.deepMerge(style, this.titleStyle);
+ uIcon() {
+ // 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
+ return this.icon ? this.icon : this.$u.type2icon(this.type);
+ uIconType() {
+ // 如果有设置图标的样式,优先使用,没有的话,则用type的样式
+ return Object.keys(this.iconStyle).length ? '' : this.type;
+ methods: {
+ // 点击内容
+ click() {
+ this.$emit('click');
+ // 点击关闭按钮
+</script>
+<style lang="scss" scoped>
+ .u-alert-tips {
+ @include vue-flex;
+ padding: 16rpx 30rpx;
+ border-radius: 8rpx;
+ position: relative;
+ transition: all 0.3s linear;
+ border: 1px solid #fff;
+ &--bg--primary-light {
+ background-color: $u-type-primary-light;
+ &--bg--info-light {
+ background-color: $u-type-info-light;
+ &--bg--success-light {
+ background-color: $u-type-success-light;
+ &--bg--warning-light {
+ background-color: $u-type-warning-light;
+ &--bg--error-light {
+ background-color: $u-type-error-light;
+ &--border--primary-disabled {
+ border-color: $u-type-primary-disabled;
+ &--border--success-disabled {
+ border-color: $u-type-success-disabled;
+ &--border--error-disabled {
+ border-color: $u-type-error-disabled;
+ &--border--warning-disabled {
+ border-color: $u-type-warning-disabled;
+ &--border--info-disabled {
+ border-color: $u-type-info-disabled;
+ .u-close-alert-tips {
+ opacity: 0;
+ visibility: hidden;
+ .u-icon {
+ margin-right: 16rpx;
+ .u-alert-title {
+ font-size: 28rpx;
+ .u-alert-desc {
+ text-align: left;
+ color: $u-content-color;
+ .u-close-icon {
+ position: absolute;
+ top: 20rpx;
+ right: 20rpx;
+ .u-close-hover {
+ color: red;
+ .u-close-text {
- // 显示文字
- default: uni.$u.props.alert.title
- // 主题,success/warning/info/error
- type: {
- default: uni.$u.props.alert.type
- // 辅助性文字
- default: uni.$u.props.alert.description
- // 是否可关闭
- closable: {
- default: uni.$u.props.alert.closable
- // 是否显示图标
- showIcon: {
- default: uni.$u.props.alert.showIcon
- // 浅或深色调,light-浅色,dark-深色
- effect: {
- default: uni.$u.props.alert.effect
- // 文字是否居中
- center: {
- default: uni.$u.props.alert.center
- // 字体大小
- fontSize: {
- default: uni.$u.props.alert.fontSize
@@ -1,243 +0,0 @@
- <u-transition
- mode="fade"
- class="u-alert"
- :class="[`u-alert--${type}--${effect}`]"
- @tap.stop="clickHandler"
- :style="[$u.addStyle(customStyle)]"
- class="u-alert__icon"
- v-if="showIcon"
- :name="iconName"
- :color="iconColor"
- class="u-alert__content"
- paddingRight: closable ? '20px' : 0
- class="u-alert__content__title"
- fontSize: $u.addUnit(fontSize),
- textAlign: center ? 'center' : 'left'
- :class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
- >{{ title }}</text>
- class="u-alert__content__desc"
- >{{ description }}</text>
- class="u-alert__close"
- v-if="closable"
- @tap.stop="closeHandler"
- size="15"
- </u-transition>
- * Alert 警告提示
- * @description 警告提示,展现需要关注的信息。
- * @tutorial https://www.uviewui.com/components/alertTips.html
- * @property {String} title 显示的文字
- * @property {String} type 使用预设的颜色 (默认 'warning' )
- * @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
- * @property {Boolean} closable 关闭按钮(默认为叉号icon图标) (默认 false )
- * @property {Boolean} showIcon 是否显示左边的辅助图标 ( 默认 false )
- * @property {String} effect 多图时,图片缩放裁剪的模式 (默认 'light' )
- * @property {Boolean} center 文字是否居中 (默认 false )
- * @property {String | Number} fontSize 字体大小 (默认 14 )
- * @property {Object} customStyle 定义需要用到的外部样式
- * @event {Function} click 点击组件时触发
- * @example <u-alert :title="title" type = "warning" :closable="closable" :description = "description"></u-alert>
- name: 'u-alert',
- show: true
- iconColor() {
- return this.effect === 'light' ? this.type : '#fff'
- // 不同主题对应不同的图标
- iconName() {
- switch (this.type) {
- case 'success':
- return 'checkmark-circle-fill';
- break;
- case 'error':
- return 'close-circle-fill';
- case 'warning':
- return 'error-circle-fill';
- case 'info':
- return 'info-circle-fill';
- case 'primary':
- return 'more-circle-fill';
- default:
- // 点击内容
- clickHandler() {
- this.$emit('click')
- // 点击关闭按钮
- this.show = false
- .u-alert {
- background-color: $u-primary;
- padding: 8px 10px;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- &--primary--dark {
- &--primary--light {
- background-color: #ecf5ff;
- &--error--dark {
- background-color: $u-error;
- &--error--light {
- background-color: #FEF0F0;
- &--success--dark {
- background-color: $u-success;
- &--success--light {
- background-color: #f5fff0;
- &--warning--dark {
- background-color: $u-warning;
- &--warning--light {
- background-color: #FDF6EC;
- &--info--dark {
- background-color: $u-info;
- &--info--light {
- background-color: #f4f4f5;
- &__icon {
- margin-right: 5px;
- &__content {
- flex: 1;
- font-size: 14px;
- color: #fff;
- margin-bottom: 2px;
- &__desc {
- &__title--dark,
- &__desc--dark {
- color: #FFFFFF;
- &__text--primary--light,
- &__text--primary--light {
- color: $u-primary;
- &__text--success--light,
- &__text--success--light {
- color: $u-success;
- &__text--warning--light,
- &__text--warning--light {
- color: $u-warning;
- &__text--error--light,
- &__text--error--light {
- color: $u-error;
- &__text--info--light,
- &__text--info--light {
- color: $u-info;
- &__close {
- top: 11px;
- right: 10px;
@@ -0,0 +1,290 @@
+ <view class="content">
+ <view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
+ <canvas
+ class="cropper"
+ :disable-scroll="true"
+ @touchstart="touchStart"
+ @touchmove="touchMove"
+ @touchend="touchEnd"
+ :style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
+ canvas-id="cropper"
+ id="cropper"
+ ></canvas>
+ :style="{
+ position: 'fixed',
+ top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
+ left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
+ width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
+ height: `${cropperOpt.height * cropperOpt.pixelRatio}`
+ }"
+ canvas-id="targetId"
+ id="targetId"
+ <view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
+ <!-- #ifdef H5 -->
+ <view class="upload" @tap="uploadTap">选择图片</view>
+ <!-- #endif -->
+ <!-- #ifndef H5 -->
+ <view class="upload" @tap="uploadTap">重新选择</view>
+ <view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
+import WeCropper from './weCropper.js';
+export default {
+ // 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
+ // mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
+ boundStyle: {
+ lineWidth: 4,
+ borderColor: 'rgb(245, 245, 245)',
+ mask: 'rgba(0, 0, 0, 0.35)'
+ };
+ // // 裁剪框宽度,单位rpx
+ // rectWidth: {
+ // type: [String, Number],
+ // default: 400
+ // },
+ // // 裁剪框高度,单位rpx
+ // rectHeight: {
+ // // 输出图片宽度,单位rpx
+ // destWidth: {
+ // // 输出图片高度,单位rpx
+ // destHeight: {
+ // // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
+ // fileType: {
+ // type: String,
+ // default: 'jpg',
+ // // 生成的图片质量
+ // // H5上无效,目前不考虑使用此参数
+ // quality: {
+ // type: [Number, String],
+ // default: 1
+ // }
+ // 底部导航的高度
+ bottomNavHeight: 50,
+ originWidth: 200,
+ width: 0,
+ height: 0,
+ cropperOpt: {
+ id: 'cropper',
+ targetId: 'targetCropper',
+ pixelRatio: 1,
+ scale: 2.5,
+ zoom: 8,
+ cut: {
+ x: (this.width - this.originWidth) / 2,
+ y: (this.height - this.originWidth) / 2,
+ width: this.originWidth,
+ height: this.originWidth
+ lineWidth: uni.upx2px(this.boundStyle.lineWidth),
+ mask: this.boundStyle.mask,
+ color: this.boundStyle.borderColor
+ // 裁剪框和输出图片的尺寸,高度默认等于宽度
+ // 输出图片宽度,单位px
+ destWidth: 200,
+ // 裁剪框宽度,单位px
+ rectWidth: 200,
+ // 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
+ fileType: 'jpg',
+ src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
+ onLoad(option) {
+ let rectInfo = uni.getSystemInfoSync();
+ this.width = rectInfo.windowWidth;
+ this.height = rectInfo.windowHeight - this.bottomNavHeight;
+ this.cropperOpt.width = this.width;
+ this.cropperOpt.height = this.height;
+ this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
+ if (option.destWidth) this.destWidth = option.destWidth;
+ if (option.rectWidth) {
+ let rectWidth = Number(option.rectWidth);
+ this.cropperOpt.cut = {
+ x: (this.width - rectWidth) / 2,
+ y: (this.height - rectWidth) / 2,
+ width: rectWidth,
+ height: rectWidth
+ this.rectWidth = option.rectWidth;
+ if (option.fileType) this.fileType = option.fileType;
+ // 初始化
+ this.cropper = new WeCropper(this.cropperOpt)
+ .on('ready', ctx => {
+ // wecropper is ready for work!
+ })
+ .on('beforeImageLoad', ctx => {
+ // before picture loaded, i can do something
+ .on('imageLoad', ctx => {
+ // picture loaded
+ .on('beforeDraw', (ctx, instance) => {
+ // before canvas draw,i can do something
+ });
+ // 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
+ uni.setNavigationBarColor({
+ frontColor: '#ffffff',
+ backgroundColor: '#000000'
+ uni.chooseImage({
+ count: 1, // 默认9
+ sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
+ sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
+ success: res => {
+ this.src = res.tempFilePaths[0];
+ // 获取裁剪图片资源后,给data添加src属性及其值
+ this.cropper.pushOrign(this.src);
+ touchStart(e) {
+ this.cropper.touchStart(e);
+ touchMove(e) {
+ this.cropper.touchMove(e);
+ touchEnd(e) {
+ this.cropper.touchEnd(e);
+ getCropperImage(isPre = false) {
+ if(!this.src) return this.$u.toast('请先选择图片再裁剪');
+ let cropper_opt = {
+ destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
+ destWidth: Number(this.destWidth),
+ fileType: this.fileType
+ this.cropper.getCropperImage(cropper_opt, (path, err) => {
+ if (err) {
+ uni.showModal({
+ title: '温馨提示',
+ content: err.message
+ } else {
+ if (isPre) {
+ uni.previewImage({
+ current: '', // 当前显示图片的 http 链接
+ urls: [path] // 需要预览的图片 http 链接列表
+ uni.$emit('uAvatarCropper', path);
+ this.$u.route({
+ type: 'back'
+ uploadTap() {
+ const self = this;
+ sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
+ success: (res) => {
+ self.src = res.tempFilePaths[0];
+ self.cropper.pushOrign(this.src);
+};
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+.content {
+ background: rgba(255, 255, 255, 1);
+.cropper {
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 11;
+.cropper-buttons {
+ background-color: #000000;
+ color: #eee;
+.cropper-wrapper {
+ flex-direction: row;
+ justify-content: space-between;
+ background-color: #000;
+ width: 100vw;
+ position: fixed;
+ bottom: 0;
+.cropper-buttons .upload,
+.cropper-buttons .getCropperImage {
+ width: 50%;
+ text-align: center;
+.cropper-buttons .upload {
+ padding-left: 50rpx;
+ text-align: right;
+ padding-right: 50rpx;
@@ -0,0 +1,1265 @@
+/**
+ * we-cropper v1.3.9
+ * (c) 2020 dlhandsome
+ * @license MIT
+(function(global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.WeCropper = factory());
+}(this, (function() {
+ 'use strict';
+ var device = void 0;
+ var TOUCH_STATE = ['touchstarted', 'touchmoved', 'touchended'];
+ function firstLetterUpper(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+ function setTouchState(instance) {
+ var arg = [],
+ len = arguments.length - 1;
+ while (len-- > 0) arg[len] = arguments[len + 1];
+ TOUCH_STATE.forEach(function(key, i) {
+ if (arg[i] !== undefined) {
+ instance[key] = arg[i];
+ function validator(instance, o) {
+ Object.defineProperties(instance, o);
+ function getDevice() {
+ if (!device) {
+ device = uni.getSystemInfoSync();
+ return device
+ var tmp = {};
+ var ref = getDevice();
+ var pixelRatio = ref.pixelRatio;
+ var DEFAULT = {
+ id: {
+ default: 'cropper',
+ get: function get() {
+ return tmp.id
+ set: function set(value) {
+ if (typeof(value) !== 'string') {
+ console.error(("id:" + value + " is invalid"));
+ tmp.id = value;
+ width: {
+ default: 750,
+ return tmp.width
+ if (typeof(value) !== 'number') {
+ console.error(("width:" + value + " is invalid"));
+ tmp.width = value;
+ height: {
+ return tmp.height
+ console.error(("height:" + value + " is invalid"));
+ tmp.height = value;
+ pixelRatio: {
+ default: pixelRatio,
+ return tmp.pixelRatio
+ console.error(("pixelRatio:" + value + " is invalid"));
+ tmp.pixelRatio = value;
+ scale: {
+ default: 2.5,
+ return tmp.scale
+ console.error(("scale:" + value + " is invalid"));
+ tmp.scale = value;
+ zoom: {
+ default: 5,
+ return tmp.zoom
+ console.error(("zoom:" + value + " is invalid"));
+ } else if (value < 0 || value > 10) {
+ console.error("zoom should be ranged in 0 ~ 10");
+ tmp.zoom = value;
+ src: {
+ default: '',
+ return tmp.src
+ console.error(("src:" + value + " is invalid"));
+ tmp.src = value;
+ default: {},
+ return tmp.cut
+ if (typeof(value) !== 'object') {
+ console.error(("cut:" + value + " is invalid"));
+ tmp.cut = value;
+ return tmp.boundStyle
+ console.error(("boundStyle:" + value + " is invalid"));
+ tmp.boundStyle = value;
+ onReady: {
+ default: null,
+ return tmp.ready
+ tmp.ready = value;
+ onBeforeImageLoad: {
+ return tmp.beforeImageLoad
+ tmp.beforeImageLoad = value;
+ onImageLoad: {
+ return tmp.imageLoad
+ tmp.imageLoad = value;
+ onBeforeDraw: {
+ return tmp.beforeDraw
+ tmp.beforeDraw = value;
+ var ref$1 = getDevice();
+ var windowWidth = ref$1.windowWidth;
+ function prepare() {
+ var self = this;
+ // v1.4.0 版本中将不再自动绑定we-cropper实例
+ self.attachPage = function() {
+ var pages = getCurrentPages();
+ // 获取到当前page上下文
+ var pageContext = pages[pages.length - 1];
+ // 把this依附在Page上下文的wecropper属性上,便于在page钩子函数中访问
+ Object.defineProperty(pageContext, 'wecropper', {
+ console.warn(
+ 'Instance will not be automatically bound to the page after v1.4.0\n\n' +
+ 'Please use a custom instance name instead\n\n' +
+ 'Example: \n' +
+ 'this.mycropper = new WeCropper(options)\n\n' +
+ '// ...\n' +
+ 'this.mycropper.getCropperImage()'
+ );
+ return self
+ configurable: true
+ self.createCtx = function() {
+ var id = self.id;
+ var targetId = self.targetId;
+ if (id) {
+ self.ctx = self.ctx || uni.createCanvasContext(id);
+ self.targetCtx = self.targetCtx || uni.createCanvasContext(targetId);
+ console.error("constructor: create canvas context failed, 'id' must be valuable");
+ self.deviceRadio = windowWidth / 750;
+ var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !==
+ 'undefined' ? self : {};
+ function createCommonjsModule(fn, module) {
+ return module = {
+ exports: {}
+ }, fn(module, module.exports), module.exports;
+ var tools = createCommonjsModule(function(module, exports) {
+ * String type check
+ exports.isStr = function(v) {
+ return typeof v === 'string';
+ * Number type check
+ exports.isNum = function(v) {
+ return typeof v === 'number';
+ * Array type check
+ exports.isArr = Array.isArray;
+ * undefined type check
+ exports.isUndef = function(v) {
+ return v === undefined;
+ exports.isTrue = function(v) {
+ return v === true;
+ exports.isFalse = function(v) {
+ return v === false;
+ * Function type check
+ exports.isFunc = function(v) {
+ return typeof v === 'function';
+ * Quick object check - this is primarily used to tell
+ * Objects from primitive values when we know the value
+ * is a JSON-compliant type.
+ exports.isObj = exports.isObject = function(obj) {
+ return obj !== null && typeof obj === 'object'
+ * Strict object type check. Only returns true
+ * for plain JavaScript objects.
+ var _toString = Object.prototype.toString;
+ exports.isPlainObject = function(obj) {
+ return _toString.call(obj) === '[object Object]'
+ * Check whether the object has the property.
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ exports.hasOwn = function(obj, key) {
+ return hasOwnProperty.call(obj, key)
+ * Perform no operation.
+ * Stubbing args to make Flow happy without leaving useless transpiled code
+ * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/)
+ exports.noop = function(a, b, c) {};
+ * Check if val is a valid array index.
+ exports.isValidArrayIndex = function(val) {
+ var n = parseFloat(String(val));
+ return n >= 0 && Math.floor(n) === n && isFinite(val)
+ var tools_7 = tools.isFunc;
+ var tools_10 = tools.isPlainObject;
+ var EVENT_TYPE = ['ready', 'beforeImageLoad', 'beforeDraw', 'imageLoad'];
+ function observer() {
+ self.on = function(event, fn) {
+ if (EVENT_TYPE.indexOf(event) > -1) {
+ if (tools_7(fn)) {
+ event === 'ready' ?
+ fn(self) :
+ self[("on" + (firstLetterUpper(event)))] = fn;
+ console.error(("event: " + event + " is invalid"));
+ function wxPromise(fn) {
+ return function(obj) {
+ var args = [],
+ while (len-- > 0) args[len] = arguments[len + 1];
+ if (obj === void 0) obj = {};
+ return new Promise(function(resolve, reject) {
+ obj.success = function(res) {
+ resolve(res);
+ obj.fail = function(err) {
+ reject(err);
+ fn.apply(void 0, [obj].concat(args));
+ function draw(ctx, reserve) {
+ if (reserve === void 0) reserve = false;
+ return new Promise(function(resolve) {
+ ctx.draw(reserve, resolve);
+ var getImageInfo = wxPromise(uni.getImageInfo);
+ var canvasToTempFilePath = wxPromise(uni.canvasToTempFilePath);
+ var base64 = createCommonjsModule(function(module, exports) {
+ /*! http://mths.be/base64 v0.1.0 by @mathias | MIT license */
+ (function(root) {
+ // Detect free variables `exports`.
+ var freeExports = 'object' == 'object' && exports;
+ // Detect free variable `module`.
+ var freeModule = 'object' == 'object' && module &&
+ module.exports == freeExports && module;
+ // Detect free variable `global`, from Node.js or Browserified code, and use
+ // it as `root`.
+ var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal;
+ if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
+ root = freeGlobal;
+ /*--------------------------------------------------------------------------*/
+ var InvalidCharacterError = function(message) {
+ this.message = message;
+ InvalidCharacterError.prototype = new Error;
+ InvalidCharacterError.prototype.name = 'InvalidCharacterError';
+ var error = function(message) {
+ // Note: the error messages used throughout this file match those used by
+ // the native `atob`/`btoa` implementation in Chromium.
+ throw new InvalidCharacterError(message);
+ var TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ // http://whatwg.org/html/common-microsyntaxes.html#space-character
+ var REGEX_SPACE_CHARACTERS = /[\t\n\f\r ]/g;
+ // `decode` is designed to be fully compatible with `atob` as described in the
+ // HTML Standard. http://whatwg.org/html/webappapis.html#dom-windowbase64-atob
+ // The optimized base64-decoding algorithm used is based on @atk’s excellent
+ // implementation. https://gist.github.com/atk/1020396
+ var decode = function(input) {
+ input = String(input)
+ .replace(REGEX_SPACE_CHARACTERS, '');
+ var length = input.length;
+ if (length % 4 == 0) {
+ input = input.replace(/==?$/, '');
+ length = input.length;
+ if (
+ length % 4 == 1 ||
+ // http://whatwg.org/C#alphanumeric-ascii-characters
+ /[^+a-zA-Z0-9/]/.test(input)
+ ) {
+ error(
+ 'Invalid character: the string to be decoded is not correctly encoded.'
+ var bitCounter = 0;
+ var bitStorage;
+ var buffer;
+ var output = '';
+ var position = -1;
+ while (++position < length) {
+ buffer = TABLE.indexOf(input.charAt(position));
+ bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
+ // Unless this is the first of a group of 4 characters…
+ if (bitCounter++ % 4) {
+ // …convert the first 8 bits to a single ASCII character.
+ output += String.fromCharCode(
+ 0xFF & bitStorage >> (-2 * bitCounter & 6)
+ return output;
+ // `encode` is designed to be fully compatible with `btoa` as described in the
+ // HTML Standard: http://whatwg.org/html/webappapis.html#dom-windowbase64-btoa
+ var encode = function(input) {
+ input = String(input);
+ if (/[^\0-\xFF]/.test(input)) {
+ // Note: no need to special-case astral symbols here, as surrogates are
+ // matched, and the input is supposed to only contain ASCII anyway.
+ 'The string to be encoded contains characters outside of the ' +
+ 'Latin1 range.'
+ var padding = input.length % 3;
+ var a;
+ var b;
+ var c;
+ // Make sure any padding is handled outside of the loop.
+ var length = input.length - padding;
+ // Read three bytes, i.e. 24 bits.
+ a = input.charCodeAt(position) << 16;
+ b = input.charCodeAt(++position) << 8;
+ c = input.charCodeAt(++position);
+ buffer = a + b + c;
+ // Turn the 24 bits into four chunks of 6 bits each, and append the
+ // matching character for each of them to the output.
+ output += (
+ TABLE.charAt(buffer >> 18 & 0x3F) +
+ TABLE.charAt(buffer >> 12 & 0x3F) +
+ TABLE.charAt(buffer >> 6 & 0x3F) +
+ TABLE.charAt(buffer & 0x3F)
+ if (padding == 2) {
+ a = input.charCodeAt(position) << 8;
+ b = input.charCodeAt(++position);
+ buffer = a + b;
+ TABLE.charAt(buffer >> 10) +
+ TABLE.charAt((buffer >> 4) & 0x3F) +
+ TABLE.charAt((buffer << 2) & 0x3F) +
+ '='
+ } else if (padding == 1) {
+ buffer = input.charCodeAt(position);
+ TABLE.charAt(buffer >> 2) +
+ TABLE.charAt((buffer << 4) & 0x3F) +
+ '=='
+ var base64 = {
+ 'encode': encode,
+ 'decode': decode,
+ 'version': '0.1.0'
+ // Some AMD build optimizers, like r.js, check for specific condition patterns
+ // like the following:
+ typeof undefined == 'function' &&
+ typeof undefined.amd == 'object' &&
+ undefined.amd
+ undefined(function() {
+ return base64;
+ } else if (freeExports && !freeExports.nodeType) {
+ if (freeModule) { // in Node.js or RingoJS v0.8.0+
+ freeModule.exports = base64;
+ } else { // in Narwhal or RingoJS v0.7.0-
+ for (var key in base64) {
+ base64.hasOwnProperty(key) && (freeExports[key] = base64[key]);
+ } else { // in Rhino or a web browser
+ root.base64 = base64;
+ }(commonjsGlobal));
+ function makeURI(strData, type) {
+ return 'data:' + type + ';base64,' + strData
+ function fixType(type) {
+ type = type.toLowerCase().replace(/jpg/i, 'jpeg');
+ var r = type.match(/png|jpeg|bmp|gif/)[0];
+ return 'image/' + r
+ function encodeData(data) {
+ var str = '';
+ if (typeof data === 'string') {
+ str = data;
+ for (var i = 0; i < data.length; i++) {
+ str += String.fromCharCode(data[i]);
+ return base64.encode(str)
+ * 获取图像区域隐含的像素数据
+ * @param canvasId canvas标识
+ * @param x 将要被提取的图像数据矩形区域的左上角 x 坐标
+ * @param y 将要被提取的图像数据矩形区域的左上角 y 坐标
+ * @param width 将要被提取的图像数据矩形区域的宽度
+ * @param height 将要被提取的图像数据矩形区域的高度
+ * @param done 完成回调
+ function getImageData(canvasId, x, y, width, height, done) {
+ uni.canvasGetImageData({
+ canvasId: canvasId,
+ x: x,
+ y: y,
+ width: width,
+ height: height,
+ success: function success(res) {
+ done(res, null);
+ fail: function fail(res) {
+ done(null, res);
+ * 生成bmp格式图片
+ * 按照规则生成图片响应头和响应体
+ * @param oData 用来描述 canvas 区域隐含的像素数据 { data, width, height } = oData
+ * @returns {*} base64字符串
+ function genBitmapImage(oData) {
+ //
+ // BITMAPFILEHEADER: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183374(v=vs.85).aspx
+ // BITMAPINFOHEADER: http://msdn.microsoft.com/en-us/library/dd183376.aspx
+ var biWidth = oData.width;
+ var biHeight = oData.height;
+ var biSizeImage = biWidth * biHeight * 3;
+ var bfSize = biSizeImage + 54; // total header size = 54 bytes
+ // typedef struct tagBITMAPFILEHEADER {
+ // WORD bfType;
+ // DWORD bfSize;
+ // WORD bfReserved1;
+ // WORD bfReserved2;
+ // DWORD bfOffBits;
+ // } BITMAPFILEHEADER;
+ var BITMAPFILEHEADER = [
+ // WORD bfType -- The file type signature; must be "BM"
+ 0x42, 0x4D,
+ // DWORD bfSize -- The size, in bytes, of the bitmap file
+ bfSize & 0xff, bfSize >> 8 & 0xff, bfSize >> 16 & 0xff, bfSize >> 24 & 0xff,
+ // WORD bfReserved1 -- Reserved; must be zero
+ 0, 0,
+ // WORD bfReserved2 -- Reserved; must be zero
+ // DWORD bfOffBits -- The offset, in bytes, from the beginning of the BITMAPFILEHEADER structure to the bitmap bits.
+ 54, 0, 0, 0
+ ];
+ // typedef struct tagBITMAPINFOHEADER {
+ // DWORD biSize;
+ // LONG biWidth;
+ // LONG biHeight;
+ // WORD biPlanes;
+ // WORD biBitCount;
+ // DWORD biCompression;
+ // DWORD biSizeImage;
+ // LONG biXPelsPerMeter;
+ // LONG biYPelsPerMeter;
+ // DWORD biClrUsed;
+ // DWORD biClrImportant;
+ // } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
+ var BITMAPINFOHEADER = [
+ // DWORD biSize -- The number of bytes required by the structure
+ 40, 0, 0, 0,
+ // LONG biWidth -- The width of the bitmap, in pixels
+ biWidth & 0xff, biWidth >> 8 & 0xff, biWidth >> 16 & 0xff, biWidth >> 24 & 0xff,
+ // LONG biHeight -- The height of the bitmap, in pixels
+ biHeight & 0xff, biHeight >> 8 & 0xff, biHeight >> 16 & 0xff, biHeight >> 24 & 0xff,
+ // WORD biPlanes -- The number of planes for the target device. This value must be set to 1
+ 1, 0,
+ // WORD biBitCount -- The number of bits-per-pixel, 24 bits-per-pixel -- the bitmap
+ // has a maximum of 2^24 colors (16777216, Truecolor)
+ 24, 0,
+ // DWORD biCompression -- The type of compression, BI_RGB (code 0) -- uncompressed
+ 0, 0, 0, 0,
+ // DWORD biSizeImage -- The size, in bytes, of the image. This may be set to zero for BI_RGB bitmaps
+ biSizeImage & 0xff, biSizeImage >> 8 & 0xff, biSizeImage >> 16 & 0xff, biSizeImage >> 24 & 0xff,
+ // LONG biXPelsPerMeter, unused
+ // LONG biYPelsPerMeter, unused
+ // DWORD biClrUsed, the number of color indexes of palette, unused
+ // DWORD biClrImportant, unused
+ 0, 0, 0, 0
+ var iPadding = (4 - ((biWidth * 3) % 4)) % 4;
+ var aImgData = oData.data;
+ var strPixelData = '';
+ var biWidth4 = biWidth << 2;
+ var y = biHeight;
+ var fromCharCode = String.fromCharCode;
+ do {
+ var iOffsetY = biWidth4 * (y - 1);
+ var strPixelRow = '';
+ for (var x = 0; x < biWidth; x++) {
+ var iOffsetX = x << 2;
+ strPixelRow += fromCharCode(aImgData[iOffsetY + iOffsetX + 2]) +
+ fromCharCode(aImgData[iOffsetY + iOffsetX + 1]) +
+ fromCharCode(aImgData[iOffsetY + iOffsetX]);
+ for (var c = 0; c < iPadding; c++) {
+ strPixelRow += String.fromCharCode(0);
+ strPixelData += strPixelRow;
+ } while (--y)
+ var strEncoded = encodeData(BITMAPFILEHEADER.concat(BITMAPINFOHEADER)) + encodeData(strPixelData);
+ return strEncoded
+ * 转换为图片base64
+ * @param type 转换图片类型
+ function convertToImage(canvasId, x, y, width, height, type, done) {
+ if (done === void 0) done = function() {};
+ if (type === undefined) {
+ type = 'png';
+ type = fixType(type);
+ if (/bmp/.test(type)) {
+ getImageData(canvasId, x, y, width, height, function(data, err) {
+ var strData = genBitmapImage(data);
+ tools_7(done) && done(makeURI(strData, 'image/' + type), err);
+ console.error('暂不支持生成\'' + type + '\'类型的base64图片');
+ var CanvasToBase64 = {
+ convertToImage: convertToImage,
+ // convertToPNG: function (width, height, done) {
+ // return convertToImage(width, height, 'png', done)
+ // convertToJPEG: function (width, height, done) {
+ // return convertToImage(width, height, 'jpeg', done)
+ // convertToGIF: function (width, height, done) {
+ // return convertToImage(width, height, 'gif', done)
+ convertToBMP: function(ref, done) {
+ if (ref === void 0) ref = {};
+ var canvasId = ref.canvasId;
+ var x = ref.x;
+ var y = ref.y;
+ var width = ref.width;
+ var height = ref.height;
+ return convertToImage(canvasId, x, y, width, height, 'bmp', done)
+ function methods() {
+ var boundWidth = self.width; // 裁剪框默认宽度,即整个画布宽度
+ var boundHeight = self.height; // 裁剪框默认高度,即整个画布高度
+ var pixelRatio = self.pixelRatio;
+ var ref = self.cut;
+ if (x === void 0) x = 0;
+ if (y === void 0) y = 0;
+ if (width === void 0) width = boundWidth;
+ if (height === void 0) height = boundHeight;
+ self.updateCanvas = function(done) {
+ if (self.croperTarget) {
+ // 画布绘制图片
+ self.ctx.drawImage(
+ self.croperTarget,
+ self.imgLeft,
+ self.imgTop,
+ self.scaleWidth,
+ self.scaleHeight
+ tools_7(self.onBeforeDraw) && self.onBeforeDraw(self.ctx, self);
+ self.setBoundStyle(self.boundStyle); // 设置边界样式
+ self.ctx.draw(false, done);
+ self.pushOrigin = self.pushOrign = function(src) {
+ self.src = src;
+ tools_7(self.onBeforeImageLoad) && self.onBeforeImageLoad(self.ctx, self);
+ return getImageInfo({
+ src: src
+ .then(function(res) {
+ var innerAspectRadio = res.width / res.height;
+ var customAspectRadio = width / height;
+ self.croperTarget = res.path;
+ if (innerAspectRadio < customAspectRadio) {
+ self.rectX = x;
+ self.baseWidth = width;
+ self.baseHeight = width / innerAspectRadio;
+ self.rectY = y - Math.abs((height - self.baseHeight) / 2);
+ self.rectY = y;
+ self.baseWidth = height * innerAspectRadio;
+ self.baseHeight = height;
+ self.rectX = x - Math.abs((width - self.baseWidth) / 2);
+ self.imgLeft = self.rectX;
+ self.imgTop = self.rectY;
+ self.scaleWidth = self.baseWidth;
+ self.scaleHeight = self.baseHeight;
+ self.update();
+ self.updateCanvas(resolve);
+ .then(function() {
+ tools_7(self.onImageLoad) && self.onImageLoad(self.ctx, self);
+ self.removeImage = function() {
+ self.src = '';
+ self.croperTarget = '';
+ return draw(self.ctx)
+ self.getCropperBase64 = function(done) {
+ CanvasToBase64.convertToBMP({
+ canvasId: id,
+ height: height
+ }, done);
+ self.getCropperImage = function(opt, fn) {
+ var customOptions = opt;
+ var canvasOptions = {
+ var task = function() {
+ return Promise.resolve();
+ tools_10(customOptions) &&
+ customOptions.original
+ // original mode
+ task = function() {
+ self.targetCtx.drawImage(
+ self.imgLeft * pixelRatio,
+ self.imgTop * pixelRatio,
+ self.scaleWidth * pixelRatio,
+ self.scaleHeight * pixelRatio
+ canvasOptions = {
+ canvasId: targetId,
+ x: x * pixelRatio,
+ y: y * pixelRatio,
+ width: width * pixelRatio,
+ height: height * pixelRatio
+ return draw(self.targetCtx)
+ return task()
+ if (tools_10(customOptions)) {
+ canvasOptions = Object.assign({}, canvasOptions, customOptions);
+ if (tools_7(customOptions)) {
+ fn = customOptions;
+ var arg = canvasOptions.componentContext ?
+ [canvasOptions, canvasOptions.componentContext] :
+ [canvasOptions];
+ return canvasToTempFilePath.apply(null, arg)
+ var tempFilePath = res.tempFilePath;
+ return tools_7(fn) ?
+ fn.call(self, tempFilePath, null) :
+ tempFilePath
+ .catch(function(err) {
+ fn.call(self, null, err);
+ throw err
+ * 获取最新缩放值
+ * @param oldScale 上一次触摸结束后的缩放值
+ * @param oldDistance 上一次触摸结束后的双指距离
+ * @param zoom 缩放系数
+ * @param touch0 第一指touch对象
+ * @param touch1 第二指touch对象
+ * @returns {*}
+ var getNewScale = function(oldScale, oldDistance, zoom, touch0, touch1) {
+ var xMove, yMove, newDistance;
+ // 计算二指最新距离
+ xMove = Math.round(touch1.x - touch0.x);
+ yMove = Math.round(touch1.y - touch0.y);
+ newDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
+ return oldScale + 0.001 * zoom * (newDistance - oldDistance)
+ function update() {
+ if (!self.src) {
+ return
+ self.__oneTouchStart = function(touch) {
+ self.touchX0 = Math.round(touch.x);
+ self.touchY0 = Math.round(touch.y);
+ self.__oneTouchMove = function(touch) {
+ var xMove, yMove;
+ // 计算单指移动的距离
+ if (self.touchended) {
+ return self.updateCanvas()
+ xMove = Math.round(touch.x - self.touchX0);
+ yMove = Math.round(touch.y - self.touchY0);
+ var imgLeft = Math.round(self.rectX + xMove);
+ var imgTop = Math.round(self.rectY + yMove);
+ self.outsideBound(imgLeft, imgTop);
+ self.updateCanvas();
+ self.__twoTouchStart = function(touch0, touch1) {
+ var xMove, yMove, oldDistance;
+ self.touchX1 = Math.round(self.rectX + self.scaleWidth / 2);
+ self.touchY1 = Math.round(self.rectY + self.scaleHeight / 2);
+ // 计算两指距离
+ oldDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
+ self.oldDistance = oldDistance;
+ self.__twoTouchMove = function(touch0, touch1) {
+ var oldScale = self.oldScale;
+ var oldDistance = self.oldDistance;
+ var scale = self.scale;
+ var zoom = self.zoom;
+ self.newScale = getNewScale(oldScale, oldDistance, zoom, touch0, touch1);
+ // 设定缩放范围
+ self.newScale <= 1 && (self.newScale = 1);
+ self.newScale >= scale && (self.newScale = scale);
+ self.scaleWidth = Math.round(self.newScale * self.baseWidth);
+ self.scaleHeight = Math.round(self.newScale * self.baseHeight);
+ var imgLeft = Math.round(self.touchX1 - self.scaleWidth / 2);
+ var imgTop = Math.round(self.touchY1 - self.scaleHeight / 2);
+ self.__xtouchEnd = function() {
+ self.oldScale = self.newScale;
+ self.rectX = self.imgLeft;
+ self.rectY = self.imgTop;
+ var handle = {
+ // 图片手势初始监测
+ touchStart: function touchStart(e) {
+ var ref = e.touches;
+ var touch0 = ref[0];
+ var touch1 = ref[1];
+ setTouchState(self, true, null, null);
+ // 计算第一个触摸点的位置,并参照改点进行缩放
+ self.__oneTouchStart(touch0);
+ // 两指手势触发
+ if (e.touches.length >= 2) {
+ self.__twoTouchStart(touch0, touch1);
+ // 图片手势动态缩放
+ touchMove: function touchMove(e) {
+ setTouchState(self, null, true);
+ // 单指手势时触发
+ if (e.touches.length === 1) {
+ self.__oneTouchMove(touch0);
+ self.__twoTouchMove(touch0, touch1);
+ touchEnd: function touchEnd(e) {
+ setTouchState(self, false, false, true);
+ self.__xtouchEnd();
+ function cut() {
+ var boundHeight = self.height;
+ // 裁剪框默认高度,即整个画布高度
+ * 设置边界
+ * @param imgLeft 图片左上角横坐标值
+ * @param imgTop 图片左上角纵坐标值
+ self.outsideBound = function(imgLeft, imgTop) {
+ self.imgLeft = imgLeft >= x ?
+ x :
+ self.scaleWidth + imgLeft - x <= width ?
+ x + width - self.scaleWidth :
+ imgLeft;
+ self.imgTop = imgTop >= y ?
+ y :
+ self.scaleHeight + imgTop - y <= height ?
+ y + height - self.scaleHeight :
+ imgTop;
+ * 设置边界样式
+ * @param color 边界颜色
+ self.setBoundStyle = function(ref) {
+ var color = ref.color;
+ if (color === void 0) color = '#04b00f';
+ var mask = ref.mask;
+ if (mask === void 0) mask = 'rgba(0, 0, 0, 0.3)';
+ var lineWidth = ref.lineWidth;
+ if (lineWidth === void 0) lineWidth = 1;
+ var half = lineWidth / 2;
+ var boundOption = [{
+ start: {
+ x: x - half,
+ y: y + 10 - half
+ step1: {
+ y: y - half
+ step2: {
+ x: x + 10 - half,
+ {
+ y: y + height - 10 + half
+ y: y + height + half
+ x: x + width - 10 + half,
+ x: x + width + half,
+ // 绘制半透明层
+ self.ctx.beginPath();
+ self.ctx.setFillStyle(mask);
+ self.ctx.fillRect(0, 0, x, boundHeight);
+ self.ctx.fillRect(x, 0, width, y);
+ self.ctx.fillRect(x, y + height, width, boundHeight - y - height);
+ self.ctx.fillRect(x + width, 0, boundWidth - x - width, boundHeight);
+ self.ctx.fill();
+ boundOption.forEach(function(op) {
+ self.ctx.setStrokeStyle(color);
+ self.ctx.setLineWidth(lineWidth);
+ self.ctx.moveTo(op.start.x, op.start.y);
+ self.ctx.lineTo(op.step1.x, op.step1.y);
+ self.ctx.lineTo(op.step2.x, op.step2.y);
+ self.ctx.stroke();
+ var version = "1.3.9";
+ var WeCropper = function WeCropper(params) {
+ var _default = {};
+ validator(self, DEFAULT);
+ Object.keys(DEFAULT).forEach(function(key) {
+ _default[key] = DEFAULT[key].default;
+ Object.assign(self, _default, params);
+ self.prepare();
+ self.attachPage();
+ self.createCtx();
+ self.observer();
+ self.cutt();
+ self.methods();
+ self.init();
+ WeCropper.prototype.init = function init() {
+ var src = self.src;
+ self.version = version;
+ typeof self.onReady === 'function' && self.onReady(self.ctx, self);
+ if (src) {
+ self.pushOrign(src);
+ setTouchState(self, false, false, false);
+ self.oldScale = 1;
+ self.newScale = 1;
+ Object.assign(WeCropper.prototype, handle);
+ WeCropper.prototype.prepare = prepare;
+ WeCropper.prototype.observer = observer;
+ WeCropper.prototype.methods = methods;
+ WeCropper.prototype.cutt = cut;
+ WeCropper.prototype.update = update;
+ return WeCropper;
+})));
@@ -1,52 +0,0 @@
- // 头像图片组
- default: uni.$u.props.avatarGroup.urls
- // 最多展示的头像数量
- default: uni.$u.props.avatarGroup.maxCount
- // 头像形状
- shape: {
- default: uni.$u.props.avatarGroup.shape
- // 图片裁剪模式
- mode: {
- default: uni.$u.props.avatarGroup.mode
- default: uni.$u.props.avatarGroup.showMore
- // 头像大小
- size: {
- default: uni.$u.props.avatarGroup.size
- default: uni.$u.props.avatarGroup.keyName
- // 头像之间的遮挡比例
- gap: {
- validator(value) {
- return value >= 0 && value <= 1
- default: uni.$u.props.avatarGroup.gap
- // 需额外显示的值
- extraValue: {
- type: [Number, String],
- default: uni.$u.props.avatarGroup.extraValue
@@ -1,103 +0,0 @@
- <view class="u-avatar-group">
- class="u-avatar-group__item"
- v-for="(item, index) in showUrl"
- :style="{
- marginLeft: index === 0 ? 0 : $u.addUnit(-size * gap)
- }"
- <u-avatar
- :src="$u.test.object(item) ? keyName && item[keyName] || item.url : item"
- ></u-avatar>
- class="u-avatar-group__item__show-more"
- v-if="showMore && index === showUrl.length - 1 && (urls.length > maxCount || extraValue > 0)"
- @tap="clickHandler"
- color="#ffffff"
- :size="size * 0.4"
- :text="`+${extraValue || urls.length - showUrl.length}`"
- * AvatarGroup 头像组
- * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
- * @tutorial https://www.uviewui.com/components/avatar.html
- * @property {Array} urls 头像图片组 (默认 [] )
- * @property {String | Number} maxCount 最多展示的头像数量 ( 默认 5 )
- * @property {String} shape 头像形状( 'circle' (默认) | 'square' )
- * @property {String} mode 图片裁剪模式(默认 'scaleToFill' )
- * @property {String | Number} size 头像大小 (默认 40 )
- * @property {String | Number} gap 头像之间的遮挡比例(0.4代表遮挡40%) (默认 0.5 )
- * @property {String | Number} extraValue 需额外显示的值
- * @event {Function} showMore 头像组更多点击
- * @example <u-avatar-group:urls="urls" size="35" gap="0.4" ></u-avatar-group:urls=>
- name: 'u-avatar-group',
- showUrl() {
- return this.urls.slice(0, this.maxCount)
- this.$emit('showMore')
- .u-avatar-group {
- margin-left: -10px;
- &--no-indent {
- // 如果你想质疑作者不会使用:first-child,说明你太年轻,因为nvue不支持
- margin-left: 0;
- &__show-more {
- border-radius: 100px;
- // 头像图片路径(不能为相对路径)
- src: {
- default: uni.$u.props.avatar.src
- // 头像形状,circle-圆形,square-方形
- default: uni.$u.props.avatar.shape
- // 头像尺寸
- default: uni.$u.props.avatar.size
- // 裁剪模式
- default: uni.$u.props.avatar.mode
- // 显示的文字
- text: {
- default: uni.$u.props.avatar.text
- // 背景色
- bgColor: {
- default: uni.$u.props.avatar.bgColor
- // 文字颜色
- color: {
- default: uni.$u.props.avatar.color
- // 文字大小
- default: uni.$u.props.avatar.fontSize
- // 显示的图标
- icon: {
- default: uni.$u.props.avatar.icon
- // 显示小程序头像,只对百度,微信,QQ小程序有效
- mpAvatar: {
- default: uni.$u.props.avatar.mpAvatar
- // 是否使用随机背景色
- randomBgColor: {
- default: uni.$u.props.avatar.randomBgColor
- // 加载失败的默认头像(组件有内置默认图片)
- defaultUrl: {
- default: uni.$u.props.avatar.defaultUrl
- // 如果配置了randomBgColor为true,且配置了此值,则从默认的背景色数组中取出对应索引的颜色值,取值0-19之间
- colorIndex: {
- // 校验参数规则,索引在0-19之间
- validator(n) {
- return uni.$u.test.range(n, [0, 19]) || n === ''
- default: uni.$u.props.avatar.colorIndex
- // 组件标识符
- name: {
- default: uni.$u.props.avatar.name
- // 返回顶部的形状,circle-圆形,square-方形
- default: uni.$u.props.backtop.mode
- // 自定义图标
- default: uni.$u.props.backtop.icon
- // 提示文字
- default: uni.$u.props.backtop.text
- // 返回顶部滚动时间
- duration: {
- default: uni.$u.props.backtop.duration
- // 滚动距离
- scrollTop: {
- default: uni.$u.props.backtop.scrollTop
- // 距离顶部多少距离显示,单位px
- top: {
- default: uni.$u.props.backtop.top
- // 返回顶部按钮到底部的距离,单位px
- bottom: {
- default: uni.$u.props.backtop.bottom
- // 返回顶部按钮到右边的距离,单位px
- right: {
- default: uni.$u.props.backtop.right
- // 层级
- zIndex: {
- default: uni.$u.props.backtop.zIndex
- // 图标的样式,对象形式
- iconStyle: {
- type: Object,
- default: uni.$u.props.backtop.iconStyle
@@ -1,129 +1,153 @@
- :customStyle="backTopStyle"
- class="u-back-top"
- :style="[contentStyle]"
- v-if="!$slots.default && !$slots.$default"
- @click="backToTop"
- :name="icon"
- :custom-style="iconStyle"
- v-if="text"
- class="u-back-top__text"
- >{{text}}</text>
+ <view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
+ bottom: bottom + 'rpx',
+ right: right + 'rpx',
+ borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
+ zIndex: uZIndex,
+ opacity: opacity
+ }, customStyle]">
+ <view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
+ <u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
+ <view class="u-back-top__content__tips">
+ {{tips}}
<slot v-else />
- const dom = weex.requireModule('dom')
- * backTop 返回顶部
- * @description 本组件一个用于长页面,滑动一定距离后,出现返回顶部按钮,方便快速返回顶部的场景。
- * @tutorial https://uviewui.com/components/backTop.html
- * @property {String} mode 返回顶部的形状,circle-圆形,square-方形 (默认 'circle' )
- * @property {String} icon 自定义图标 (默认 'arrow-upward' ) 见官方文档示例
- * @property {String} text 提示文字
- * @property {String | Number} duration 返回顶部滚动时间 (默认 100)
- * @property {String | Number} scrollTop 滚动距离 (默认 0 )
- * @property {String | Number} top 距离顶部多少距离显示,单位px (默认 400 )
- * @property {String | Number} bottom 返回顶部按钮到底部的距离,单位px (默认 100 )
- * @property {String | Number} right 返回顶部按钮到右边的距离,单位px (默认 20 )
- * @property {String | Number} zIndex 层级 (默认 9 )
- * @property {Object<Object>} iconStyle 图标的样式,对象形式 (默认 {color: '#909399',fontSize: '19px'})
- * @example <u-back-top :scrollTop="scrollTop"></u-back-top>
name: 'u-back-top',
- mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
- backTopStyle() {
- // 动画组件样式
- bottom: uni.$u.addUnit(this.bottom),
- right: uni.$u.addUnit(this.right),
- width: '40px',
- height: '40px',
- position: 'fixed',
- zIndex: 10,
+ // 返回顶部的形状,circle-圆形,square-方形
+ mode: {
+ default: 'circle'
+ // 自定义图标
+ default: 'arrow-upward'
+ // 提示文字
+ // 返回顶部滚动时间
+ duration: {
+ type: [Number, String],
+ default: 100
+ // 滚动距离
+ scrollTop: {
- show() {
- return uni.$u.getPx(this.scrollTop) > uni.$u.getPx(this.top)
+ // 距离顶部多少距离显示,单位rpx
+ top: {
+ default: 400
- contentStyle() {
- const style = {}
- let radius = 0
- // 是否圆形
- if(this.mode === 'circle') {
- radius = '100px'
+ // 返回顶部按钮到底部的距离,单位rpx
+ bottom: {
+ default: 200
+ // 返回顶部按钮到右边的距离,单位rpx
+ right: {
+ default: 40
+ // 层级
+ default: '9'
+ // 图标的样式,对象形式
+ color: '#909399',
+ fontSize: '38rpx'
+ // 整个组件的样式
+ customStyle: {
+ watch: {
+ showBackTop(nVal, oVal) {
+ // 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
+ // 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
+ if(nVal) {
+ this.uZIndex = this.zIndex;
+ this.opacity = 1;
} else {
- radius = '4px'
+ this.uZIndex = -1;
+ this.opacity = 0;
- // 为了兼容安卓nvue,只能这么分开写
- style.borderTopLeftRadius = radius
- style.borderTopRightRadius = radius
- style.borderBottomLeftRadius = radius
- style.borderBottomRightRadius = radius
- return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
+ showBackTop() {
+ // 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
+ // 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
+ return this.scrollTop > uni.upx2px(this.top);
+ // 不透明度,为了让组件有一个显示和隐藏的过渡动画
+ opacity: 0,
+ // 组件的z-index值,隐藏时设置为-1,就会看不到
+ uZIndex: -1
backToTop() {
- if (!this.$parent.$refs['u-back-top']) {
- uni.$u.error(`nvue页面需要给页面最外层元素设置"ref='u-back-top'`)
- dom.scrollToElement(this.$parent.$refs['u-back-top'], {
- offset: 0
uni.pageScrollTo({
scrollTop: 0,
duration: this.duration
});
- @import '../../libs/css/components.scss';
- $u-back-top-flex:1 !default;
- $u-back-top-height:100% !default;
- $u-back-top-background-color:#E1E1E1 !default;
- $u-back-top-tips-font-size:12px !default;
.u-back-top {
+ width: 80rpx;
+ height: 80rpx;
+ z-index: 9;
flex-direction: column;
- flex:$u-back-top-flex;
- height: $u-back-top-height;
justify-content: center;
- background-color: $u-back-top-background-color;
- &__tips {
- font-size:$u-back-top-tips-font-size;
- transform: scale(0.8);
+ background-color: #E1E1E1;
+ transition: opacity 0.4s;
+ &__content {
+ &__tips {
+ transform: scale(0.8);
@@ -1,72 +0,0 @@
- // 是否显示圆点
- isDot: {
- default: uni.$u.props.badge.isDot
- // 显示的内容
- value: {
- default: uni.$u.props.badge.value
- // 是否显示
- default: uni.$u.props.badge.show
- // 最大值,超过最大值会显示 '{max}+'
- max: {
- default: uni.$u.props.badge.max
- // 主题类型,error|warning|success|primary
- default: uni.$u.props.badge.type
- // 当数值为 0 时,是否展示 Badge
- showZero: {
- default: uni.$u.props.badge.showZero
- // 背景颜色,优先级比type高,如设置,type参数会失效
- type: [String, null],
- default: uni.$u.props.badge.bgColor
- // 字体颜色
- default: uni.$u.props.badge.color
- // 徽标形状,circle-四角均为圆角,horn-左下角为直角
- default: uni.$u.props.badge.shape
- // 设置数字的显示方式,overflow|ellipsis|limit
- // overflow会根据max字段判断,超出显示`${max}+`
- // ellipsis会根据max判断,超出显示`${max}...`
- // limit会依据1000作为判断条件,超出1000,显示`${value/1000}K`,比如2.2k、3.34w,最多保留2位小数
- numberType: {
- default: uni.$u.props.badge.numberType
- // 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效
- offset: {
- default: uni.$u.props.badge.offset
- // 是否反转背景和字体颜色
- inverted: {
- default: uni.$u.props.badge.inverted
- // 是否绝对定位
- absolute: {
- default: uni.$u.props.badge.absolute
@@ -1,171 +1,216 @@
- v-if="show && ((Number(value) === 0 ? showZero : true) || isDot)"
- :class="[isDot ? 'u-badge--dot' : 'u-badge--not-dot', inverted && 'u-badge--inverted', shape === 'horn' && 'u-badge--horn', `u-badge--${type}${inverted ? '--inverted' : ''}`]"
- :style="[$u.addStyle(customStyle), badgeStyle]"
- class="u-badge"
- >{{ isDot ? '' :showValue }}</text>
+ <view v-if="show" class="u-badge" :class="[
+ isDot ? 'u-badge-dot' : '',
+ size == 'mini' ? 'u-badge-mini' : '',
+ type ? 'u-badge--bg--' + type : ''
+ ]" :style="[{
+ top: offset[0] + 'rpx',
+ right: offset[1] + 'rpx',
+ fontSize: fontSize + 'rpx',
+ position: absolute ? 'absolute' : 'static',
+ color: color,
+ backgroundColor: bgColor
+ }, boxStyle]"
+ >
+ {{showText}}
- * badge 徽标数
- * @description 该组件一般用于图标右上角显示未读的消息数量,提示用户点击,有圆点和圆包含文字两种形式。
- * @tutorial https://uviewui.com/components/badge.html
- * @property {Boolean} isDot 是否显示圆点 (默认 false )
- * @property {String | Number} value 显示的内容
- * @property {Boolean} show 是否显示 (默认 true )
- * @property {String | Number} max 最大值,超过最大值会显示 '{max}+' (默认999)
- * @property {String} type 主题类型,error|warning|success|primary (默认 'error' )
- * @property {Boolean} showZero 当数值为 0 时,是否展示 Badge (默认 false )
- * @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
- * @property {String} color 字体颜色 (默认 '#ffffff' )
- * @property {String} shape 徽标形状,circle-四角均为圆角,horn-左下角为直角 (默认 'circle' )
- * @property {String} numberType 设置数字的显示方式,overflow|ellipsis|limit (默认 'overflow' )
- * @property {Array}} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,absolute为true时有效
- * @property {Boolean} inverted 是否反转背景和字体颜色(默认 false )
- * @property {Boolean} absolute 是否绝对定位(默认 false )
- * @example <u-badge :type="type" :count="count"></u-badge>
+ * badge 角标
+ * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
+ * @tutorial https://www.uviewui.com/components/badge.html
+ * @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏
+ * @property {Boolean} is-dot 不展示数字,只有一个小点(默认false)
+ * @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true)
+ * @property {String Number} overflow-count 展示封顶的数字值(默认99)
+ * @property {String} type 使用预设的背景颜色(默认error)
+ * @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false)
+ * @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default)
+ * @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20])
+ * @property {String} color 字体颜色(默认#ffffff)
+ * @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
+ * @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
+ * @example <u-badge type="error" count="7"></u-badge>
name: 'u-badge',
+ // primary,warning,success,error,info
+ default: 'error'
+ // default, mini
+ size: {
+ default: 'default'
+ //是否是圆点
+ isDot: {
+ // 显示的数值内容
+ count: {
+ // 展示封顶的数字值
+ overflowCount: {
+ type: Number,
+ default: 99
+ // 当数值为 0 时,是否展示 Badge
+ showZero: {
+ // 位置偏移
+ offset: {
+ default: () => {
+ return [20, 20]
+ // 是否开启绝对定位,开启了offset才会起作用
+ absolute: {
+ // 字体大小
+ fontSize: {
+ default: '24'
+ // 字体演示
+ default: '#ffffff'
+ // badge的背景颜色
+ // 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效
+ isCenter: {
// 是否将badge中心与父组件右上角重合
boxStyle() {
- return style;
- // 整个组件的样式
- badgeStyle() {
- if(this.color) {
- style.color = this.color
- if (this.bgColor && !this.inverted) {
- style.backgroundColor = this.bgColor
+ if(this.isCenter) {
+ style.top = 0;
+ style.right = 0;
+ // Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
+ style.transform = "translateY(-50%) translateX(50%)";
+ style.top = this.offset[0] + 'rpx';
+ style.right = this.offset[1] + 'rpx';
+ style.transform = "translateY(0) translateX(0)";
- if (this.absolute) {
- style.position = 'absolute'
- // 如果有设置offset参数
- if(this.offset.length) {
- // top和right分为为offset的第一个和第二个值,如果没有第二个值,则right等于top
- const top = this.offset[0]
- const right = this.offset[1] || top
- style.top = uni.$u.addUnit(top)
- style.right = uni.$u.addUnit(right)
+ // 如果尺寸为mini,后接上scal()
+ if(this.size == 'mini') {
+ style.transform = style.transform + " scale(0.8)";
- showValue() {
- switch (this.numberType) {
- case "overflow":
- return Number(this.value) > Number(this.max) ? this.max + "+" : this.value
- case "ellipsis":
- return Number(this.value) > Number(this.max) ? "..." : this.value
- case "limit":
- return Number(this.value) > 999 ? Number(this.value) >= 9999 ?
- Math.floor(this.value / 1e4 * 100) / 100 + "w" : Math.floor(this.value /
- 1e3 * 100) / 100 + "k" : this.value
- return Number(this.value)
+ // isDot类型时,不显示文字
+ showText() {
+ if(this.isDot) return '';
+ else {
+ if(this.count > this.overflowCount) return `${this.overflowCount}+`;
+ else return this.count;
+ // 是否显示组件
+ show() {
+ // 如果count的值为0,并且showZero设置为false,不显示组件
+ if(this.count == 0 && this.showZero == false) return false;
+ else return true;
- $u-badge-primary: $u-primary !default;
- $u-badge-error: $u-error !default;
- $u-badge-success: $u-success !default;
- $u-badge-info: $u-info !default;
- $u-badge-warning: $u-warning !default;
- $u-badge-dot-radius: 100px !default;
- $u-badge-dot-size: 8px !default;
- $u-badge-dot-right: 4px !default;
- $u-badge-dot-top: 0 !default;
- $u-badge-text-font-size: 11px !default;
- $u-badge-text-right: 10px !default;
- $u-badge-text-padding: 2px 5px !default;
- $u-badge-text-align: center !default;
- $u-badge-text-color: #FFFFFF !default;
.u-badge {
- border-top-right-radius: $u-badge-dot-radius;
- border-top-left-radius: $u-badge-dot-radius;
- border-bottom-left-radius: $u-badge-dot-radius;
- border-bottom-right-radius: $u-badge-dot-radius;
- line-height: $u-badge-text-font-size;
- text-align: $u-badge-text-align;
- font-size: $u-badge-text-font-size;
- color: $u-badge-text-color;
- &--dot {
- height: $u-badge-dot-size;
- width: $u-badge-dot-size;
+ /* #ifndef APP-NVUE */
+ display: inline-flex;
+ /* #endif */
+ line-height: 24rpx;
+ padding: 4rpx 8rpx;
+ border-radius: 100rpx;
- &--inverted {
- font-size: 13px;
+ &--bg--primary {
+ background-color: $u-type-primary;
- &--not-dot {
- padding: $u-badge-text-padding;
- &--horn {
- border-bottom-left-radius: 0;
- &--primary {
- background-color: $u-badge-primary;
+ &--bg--error {
+ background-color: $u-type-error;
- &--primary--inverted {
- color: $u-badge-primary;
- &--error {
- background-color: $u-badge-error;
+ &--bg--success {
+ background-color: $u-type-success;
- &--error--inverted {
- color: $u-badge-error;
- &--success {
- background-color: $u-badge-success;
+ &--bg--info {
+ background-color: $u-type-info;
- &--success--inverted {
- color: $u-badge-success;
- &--info {
- background-color: $u-badge-info;
- &--info--inverted {
- color: $u-badge-info;
- &--warning {
- background-color: $u-badge-warning;
- &--warning--inverted {
- color: $u-badge-warning;
+ &--bg--warning {
+ background-color: $u-type-warning;
+ .u-badge-dot {
+ height: 16rpx;
+ width: 16rpx;
+ .u-badge-mini {
+ transform-origin: center center;
+ // .u-primary {
+ // background: $u-type-primary;
+ // color: #fff;
+ // .u-error {
+ // background: $u-type-error;
+ // .u-warning {
+ // background: $u-type-warning;
+ // .u-success {
+ // background: $u-type-success;
+ // .u-black {
+ // background: #585858;
+ .u-info {
+ color: #fff;
@@ -1,46 +0,0 @@
-$u-button-active-opacity:0.75 !default;
-$u-button-loading-text-margin-left:4px !default;
-$u-button-text-color: #FFFFFF !default;
-$u-button-text-plain-error-color:$u-error !default;
-$u-button-text-plain-warning-color:$u-warning !default;
-$u-button-text-plain-success-color:$u-success !default;
-$u-button-text-plain-info-color:$u-info !default;
-$u-button-text-plain-primary-color:$u-primary !default;
-.u-button {
- &--active {
- opacity: $u-button-active-opacity;
- &--active--plain {
- background-color: rgb(217, 217, 217);
- &__loading-text {
- margin-left:$u-button-loading-text-margin-left;
- &__text,
- color:$u-button-text-color;
- &__text--plain--error {
- color:$u-button-text-plain-error-color;
- &__text--plain--warning {
- color:$u-button-text-plain-warning-color;
- &__text--plain--success{
- color:$u-button-text-plain-success-color;
- &__text--plain--info {
- color:$u-button-text-plain-info-color;
- &__text--plain--primary {
- color:$u-button-text-plain-primary-color;
@@ -1,161 +0,0 @@
-/*
- * @Author : LQ
- * @Description :
- * @version : 1.0
- * @Date : 2021-08-16 10:04:04
- * @LastAuthor : LQ
- * @lastTime : 2021-08-16 10:04:24
- * @FilePath : /u-view2.0/uview-ui/components/u-button/props.js
- // 是否细边框
- hairline: {
- default: uni.$u.props.button.hairline
- // 按钮的预置样式,info,primary,error,warning,success
- default: uni.$u.props.button.type
- // 按钮尺寸,large,normal,small,mini
- default: uni.$u.props.button.size
- // 按钮形状,circle(两边为半圆),square(带圆角)
- default: uni.$u.props.button.shape
- // 按钮是否镂空
- plain: {
- default: uni.$u.props.button.plain
- // 是否禁止状态
- disabled: {
- default: uni.$u.props.button.disabled
- // 是否加载中
- loading: {
- default: uni.$u.props.button.loading
- // 加载中提示文字
- loadingText: {
- default: uni.$u.props.button.loadingText
- // 加载状态图标类型
- loadingMode: {
- default: uni.$u.props.button.loadingMode
- // 加载图标大小
- loadingSize: {
- default: uni.$u.props.button.loadingSize
- // 开放能力,具体请看uniapp稳定关于button组件部分说明
- // https://uniapp.dcloud.io/component/button
- default: uni.$u.props.button.openType
- // 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
- // 取值为submit(提交表单),reset(重置表单)
- formType: {
- default: uni.$u.props.button.formType
- // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
- // 只微信小程序、QQ小程序有效
- appParameter: {
- default: uni.$u.props.button.appParameter
- // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
- hoverStopPropagation: {
- default: uni.$u.props.button.hoverStopPropagation
- // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
- lang: {
- default: uni.$u.props.button.lang
- // 会话来源,open-type="contact"时有效。只微信小程序有效
- sessionFrom: {
- default: uni.$u.props.button.sessionFrom
- // 会话内消息卡片标题,open-type="contact"时有效
- // 默认当前标题,只微信小程序有效
- sendMessageTitle: {
- default: uni.$u.props.button.sendMessageTitle
- // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
- // 默认当前分享路径,只微信小程序有效
- sendMessagePath: {
- default: uni.$u.props.button.sendMessagePath
- // 会话内消息卡片图片,open-type="contact"时有效
- // 默认当前页面截图,只微信小程序有效
- sendMessageImg: {
- default: uni.$u.props.button.sendMessageImg
- // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
- // 用户点击后可以快速发送小程序消息,open-type="contact"时有效
- showMessageCard: {
- default: uni.$u.props.button.showMessageCard
- // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
- dataName: {
- default: uni.$u.props.button.dataName
- // 节流,一定时间内只能触发一次
- throttleTime: {
- default: uni.$u.props.button.throttleTime
- // 按住后多久出现点击态,单位毫秒
- hoverStartTime: {
- default: uni.$u.props.button.hoverStartTime
- // 手指松开后点击态保留时间,单位毫秒
- hoverStayTime: {
- default: uni.$u.props.button.hoverStayTime
- // 按钮文字,之所以通过props传入,是因为slot传入的话
- // nvue中无法控制文字的样式
- default: uni.$u.props.button.text
- // 按钮图标
- default: uni.$u.props.button.icon
- iconColor: {
- // 按钮颜色,支持传入linear-gradient渐变色
- default: uni.$u.props.button.color
@@ -1,490 +1,596 @@
- <!-- #ifndef APP-NVUE -->
- :hover-start-time="Number(hoverStartTime)"
- :hover-stay-time="Number(hoverStayTime)"
- :form-type="formType"
- :open-type="openType"
- :hover-stop-propagation="hoverStopPropagation"
- :data-name="dataName"
- @getphonenumber="getphonenumber"
- @getuserinfo="getuserinfo"
- @error="error"
- @opensetting="opensetting"
- @launchapp="launchapp"
- :hover-class="!disabled && !loading ? 'u-button--active' : ''"
- class="u-button u-reset-button"
- :style="[baseColor, $u.addStyle(customStyle)]"
- :class="bemClass"
- <template v-if="loading">
- :mode="loadingMode"
- :size="loadingSize * 1.15"
- :color="loadingColor"
- ></u-loading-icon>
- class="u-button__loading-text"
- :style="[{ fontSize: textSize + 'px' }]"
- >{{ loadingText || text }}</text
- <template v-else>
- v-if="icon"
- :color="iconColorCom"
- :size="textSize * 1.35"
- :customStyle="{ marginRight: '2px' }"
- class="u-button__text"
- >{{ text }}</text
- <!-- #ifdef APP-NVUE -->
- class="u-button"
- :hover-class="
- !disabled && !loading && !color && (plain || type === 'info')
- ? 'u-button--active--plain'
- : !disabled && !loading && !plain
- ? 'u-button--active'
- : ''
- :style="[nvueTextStyle]"
- :class="[plain && `u-button__text--plain--${type}`]"
- marginLeft: icon ? '2px' : 0,
- nvueTextStyle,
+ <button
+ id="u-wave-btn"
+ class="u-btn u-line-1 u-fix-ios-appearance"
+ :class="[
+ 'u-size-' + size,
+ plain ? 'u-btn--' + type + '--plain' : '',
+ loading ? 'u-loading' : '',
+ shape == 'circle' ? 'u-round-circle' : '',
+ hairLine ? showHairLineBorder : 'u-btn--bold-border',
+ 'u-btn--' + type,
+ disabled ? `u-btn--${type}--disabled` : '',
+ ]"
+ :hover-start-time="Number(hoverStartTime)"
+ :hover-stay-time="Number(hoverStayTime)"
+ :disabled="disabled"
+ :form-type="formType"
+ :open-type="openType"
+ :app-parameter="appParameter"
+ :hover-stop-propagation="hoverStopPropagation"
+ :send-message-title="sendMessageTitle"
+ send-message-path="sendMessagePath"
+ :lang="lang"
+ :data-name="dataName"
+ :session-from="sessionFrom"
+ :send-message-img="sendMessageImg"
+ :show-message-card="showMessageCard"
+ @getphonenumber="getphonenumber"
+ @getuserinfo="getuserinfo"
+ @error="error"
+ @opensetting="opensetting"
+ @launchapp="launchapp"
+ :style="[customStyle, {
+ overflow: ripple ? 'hidden' : 'visible'
+ }]"
+ @tap.stop="click($event)"
+ :hover-class="getHoverClass"
+ :loading="loading"
+ <slot></slot>
+ v-if="ripple"
+ class="u-wave-ripple"
+ :class="[waveActive ? 'u-wave-active' : '']"
+ top: rippleTop + 'px',
+ left: rippleLeft + 'px',
+ width: fields.targetWidth + 'px',
+ height: fields.targetWidth + 'px',
+ 'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
+ ></view>
+ </button>
-import button from "../../libs/mixin/button.js";
-import openType from "../../libs/mixin/openType.js";
-import props from "./props.js";
* button 按钮
* @description Button 按钮
* @tutorial https://www.uviewui.com/components/button.html
- * @property {Boolean} hairline 是否显示按钮的细边框 (默认 true )
- * @property {String} type 按钮的预置样式,info,primary,error,warning,success (默认 'info' )
- * @property {String} size 按钮尺寸,large,normal,mini (默认 normal)
- * @property {String} shape 按钮形状,circle(两边为半圆),square(带圆角) (默认 'square' )
- * @property {Boolean} plain 按钮是否镂空,背景色透明 (默认 false)
- * @property {Boolean} disabled 是否禁用 (默认 false)
- * @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈) (默认 false)
- * @property {String | Number} loadingText 加载中提示文字
- * @property {String} loadingMode 加载状态图标类型 (默认 'spinner' )
- * @property {String | Number} loadingSize 加载图标大小 (默认 15 )
- * @property {String} openType 开放能力,具体请看uniapp稳定关于button组件部分说明
- * @property {String} formType 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
- * @property {String} appParameter 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 (注:只微信小程序、QQ小程序有效)
- * @property {Boolean} hoverStopPropagation 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效(默认 true )
- * @property {String} lang 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文(默认 en )
- * @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效(默认false)
- * @property {String} dataName 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
- * @property {String | Number} throttleTime 节流,一定时间内只能触发一次 (默认 0 )
- * @property {String | Number} hoverStartTime 按住后多久出现点击态,单位毫秒 (默认 0 )
- * @property {String | Number} hoverStayTime 手指松开后点击态保留时间,单位毫秒 (默认 200 )
- * @property {String | Number} text 按钮文字,之所以通过props传入,是因为slot传入的话(注:nvue中无法控制文字的样式)
- * @property {String} icon 按钮图标
- * @property {String} iconColor 按钮图标颜色
- * @property {String} color 按钮颜色,支持传入linear-gradient渐变色
- * @event {Function} click 非禁止并且非加载中,才能点击
- * @event {Function} getphonenumber open-type="getPhoneNumber"时有效
- * @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
- * @event {Function} error 当使用开放能力时,发生错误的回调
- * @event {Function} opensetting 在打开授权设置页并关闭后回调
- * @event {Function} launchapp 打开 APP 成功的回调
+ * @property {String} size 按钮的大小
+ * @property {Boolean} ripple 是否开启点击水波纹效果
+ * @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效
+ * @property {String} type 按钮的样式类型
+ * @property {Boolean} plain 按钮是否镂空,背景色透明
+ * @property {Boolean} disabled 是否禁用
+ * @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
+ * @property {Boolean} shape 按钮外观形状,见文档说明
+ * @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈)
+ * @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ * @property {String} open-type 开放能力
+ * @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ * @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
+ * @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
+ * @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
+ * @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
+ * @event {Function} click 按钮点击
+ * @event {Function} getphonenumber open-type="getPhoneNumber"时有效
+ * @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
+ * @event {Function} error 当使用开放能力时,发生错误的回调
+ * @event {Function} opensetting 在打开授权设置页并关闭后回调
+ * @event {Function} launchapp 打开 APP 成功的回调
* @example <u-button>月落</u-button>
- name: "u-button",
- // #ifdef MP
- mixins: [uni.$u.mpMixin, uni.$u.mixin, button, openType, props],
- // #ifndef MP
- return {};
- // 生成bem风格的类名
- bemClass() {
- // this.bem为一个computed变量,在mixin中
- if (!this.color) {
- return this.bem(
- "button",
- ["type", "shape", "size"],
- ["disabled", "plain", "hairline"]
- );
- // 由于nvue的原因,在有color参数时,不需要传入type,否则会生成type相关的类型,影响最终的样式
- ["shape", "size"],
- loadingColor() {
- if (this.plain) {
- // 如果有设置color值,则用color值,否则使用type主题颜色
- return this.color
- ? this.color
- : uni.$u.config.color[`u-${this.type}`];
- if (this.type === "info") {
- return "#c9c9c9";
- return "rgb(200, 200, 200)";
- iconColorCom() {
- // 如果是镂空状态,设置了color就用color值,否则使用主题颜色,
- // u-icon的color能接受一个主题颜色的值
- if (this.iconColor) return this.iconColor;
- return this.color ? this.color : this.type;
- return this.type === "info" ? "#000000" : "#ffffff";
- baseColor() {
- let style = {};
- if (this.color) {
- // 针对自定义了color颜色的情况,镂空状态下,就是用自定义的颜色
- style.color = this.plain ? this.color : "white";
- if (!this.plain) {
- // 非镂空,背景色使用自定义的颜色
- style["background-color"] = this.color;
- if (this.color.indexOf("gradient") !== -1) {
- // 如果自定义的颜色为渐变色,不显示边框,以及通过backgroundImage设置渐变色
- // weex文档说明可以写borderWidth的形式,为什么这里需要分开写?
- // 因为weex是阿里巴巴为了部门业绩考核而做的你懂的东西,所以需要这么写才有效
- style.borderTopWidth = 0;
- style.borderRightWidth = 0;
- style.borderBottomWidth = 0;
- style.borderLeftWidth = 0;
- style.backgroundImage = this.color;
- // 非渐变色,则设置边框相关的属性
- style.borderColor = this.color;
- style.borderWidth = "1px";
- style.borderStyle = "solid";
- // nvue版本按钮的字体不会继承父组件的颜色,需要对每一个text组件进行单独的设置
- nvueTextStyle() {
- style.color = "#323233";
- style.fontSize = this.textSize + "px";
- textSize() {
- let fontSize = 14,
- { size } = this;
- if (size === "large") fontSize = 16;
- if (size === "normal") fontSize = 14;
- if (size === "small") fontSize = 12;
- if (size === "mini") fontSize = 10;
- return fontSize;
- // 非禁止并且非加载中,才能点击
- if (!this.disabled && !this.loading) {
- // 进行节流控制,每this.throttle毫秒内,只在开始处执行
- uni.$u.throttle(() => {
- this.$emit("click");
- }, this.throttleTime);
- // 下面为对接uniapp官方按钮开放能力事件回调的对接
- getphonenumber(res) {
- this.$emit("getphonenumber", res);
- getuserinfo(res) {
- this.$emit("getuserinfo", res);
- error(res) {
- this.$emit("error", res);
- opensetting(res) {
- this.$emit("opensetting", res);
- launchapp(res) {
- this.$emit("launchapp", res);
+ name: 'u-button',
+ // 是否细边框
+ hairLine: {
+ // 按钮的预置样式,default,primary,error,warning,success
+ // 按钮尺寸,default,medium,mini
+ // 按钮形状,circle(两边为半圆),square(带圆角)
+ shape: {
+ default: 'square'
+ // 按钮是否镂空
+ plain: {
+ // 是否禁止状态
+ disabled: {
+ // 是否加载中
+ loading: {
+ // 开放能力,具体请看uniapp稳定关于button组件部分说明
+ // https://uniapp.dcloud.io/component/button
+ openType: {
+ // 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ // 取值为submit(提交表单),reset(重置表单)
+ formType: {
+ // 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
+ // 只微信小程序、QQ小程序有效
+ appParameter: {
+ // 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
+ hoverStopPropagation: {
+ // 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
+ lang: {
+ default: 'en'
+ // 会话来源,open-type="contact"时有效。只微信小程序有效
+ sessionFrom: {
+ // 会话内消息卡片标题,open-type="contact"时有效
+ // 默认当前标题,只微信小程序有效
+ sendMessageTitle: {
+ // 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
+ // 默认当前分享路径,只微信小程序有效
+ sendMessagePath: {
+ // 会话内消息卡片图片,open-type="contact"时有效
+ // 默认当前页面截图,只微信小程序有效
+ sendMessageImg: {
+ // 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
+ // 用户点击后可以快速发送小程序消息,open-type="contact"时有效
+ showMessageCard: {
+ // 手指按(触摸)按钮时按钮时的背景颜色
+ hoverBgColor: {
+ // 水波纹的背景颜色
+ rippleBgColor: {
+ // 是否开启水波纹效果
+ ripple: {
+ // 按下的类名
+ hoverClass: {
+ // 自定义样式,对象形式
+ return {};
+ // 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ dataName: {
+ // 节流,一定时间内只能触发一次
+ throttleTime: {
+ default: 1000
+ // 按住后多久出现点击态,单位毫秒
+ hoverStartTime: {
+ default: 20
+ // 手指松开后点击态保留时间,单位毫秒
+ hoverStayTime: {
+ default: 150
+ // 当没有传bgColor变量时,按钮按下去的颜色类名
+ getHoverClass() {
+ // 如果开启水波纹效果,则不启用hover-class效果
+ if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
+ let hoverClass = '';
+ hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
+ return hoverClass;
+ // 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
+ showHairLineBorder() {
+ if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
+ return '';
+ return 'u-hairline-border';
+ rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
+ rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
+ fields: {}, // 波纹按钮节点信息
+ waveActive: false // 激活水波纹
+ // 按钮点击
+ click(e) {
+ // 进行节流控制,每this.throttle毫秒内,只在开始处执行
+ this.$u.throttle(() => {
+ // 如果按钮时disabled和loading状态,不触发水波纹效果
+ if (this.loading === true || this.disabled === true) return;
+ if (this.ripple) {
+ // 每次点击时,移除上一次的类,再次添加,才能触发动画效果
+ this.waveActive = false;
+ this.$nextTick(function() {
+ this.getWaveQuery(e);
+ this.$emit('click', e);
+ }, this.throttleTime);
+ // 查询按钮的节点信息
+ getWaveQuery(e) {
+ this.getElQuery().then(res => {
+ // 查询返回的是一个数组节点
+ let data = res[0];
+ // 查询不到节点信息,不操作
+ if (!data.width || !data.width) return;
+ // 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
+ // 最终的方形(变换后的圆形)才能覆盖整个按钮
+ data.targetWidth = data.height > data.width ? data.height : data.width;
+ if (!data.targetWidth) return;
+ this.fields = data;
+ let touchesX = '',
+ touchesY = '';
+ // #ifdef MP-BAIDU
+ touchesX = e.changedTouches[0].clientX;
+ touchesY = e.changedTouches[0].clientY;
+ // #endif
+ // #ifdef MP-ALIPAY
+ touchesX = e.detail.clientX;
+ touchesY = e.detail.clientY;
+ // #ifndef MP-BAIDU || MP-ALIPAY
+ touchesX = e.touches[0].clientX;
+ touchesY = e.touches[0].clientY;
+ // 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
+ // 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
+ // 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
+ this.rippleTop = touchesY - data.top - data.targetWidth / 2;
+ this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
+ this.$nextTick(() => {
+ this.waveActive = true;
+ // 获取节点信息
+ getElQuery() {
+ return new Promise(resolve => {
+ let queryInfo = '';
+ // 获取元素节点信息,请查看uniapp相关文档
+ // https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
+ queryInfo = uni.createSelectorQuery().in(this);
+ //#ifdef MP-ALIPAY
+ queryInfo = uni.createSelectorQuery();
+ //#endif
+ queryInfo.select('.u-btn').boundingClientRect();
+ queryInfo.exec(data => {
+ resolve(data);
+ // 下面为对接uniapp官方按钮开放能力事件回调的对接
+ getphonenumber(res) {
+ this.$emit('getphonenumber', res);
+ getuserinfo(res) {
+ this.$emit('getuserinfo', res);
+ error(res) {
+ this.$emit('error', res);
+ opensetting(res) {
+ this.$emit('opensetting', res);
+ launchapp(res) {
+ this.$emit('launchapp', res);
};
-@import "../../libs/css/components.scss";
+.u-btn::after {
+ border: none;
-/* #ifndef APP-NVUE */
-@import "./vue.scss";
-/* #endif */
+.u-btn {
+ border: 0;
+ //border-radius: 10rpx;
+ // 避免边框某些场景可能被“裁剪”,不能设置为hidden
+ overflow: visible;
+ cursor: pointer;
+ padding: 0 40rpx;
+ z-index: 1;
+ box-sizing: border-box;
+ transition: all 0.15s;
+ &--bold-border {
+ border: 1px solid #ffffff;
+ &--default {
+ border-color: #c0c4cc;
+ background-color: #ffffff;
+ &--primary {
+ color: #ffffff;
+ border-color: $u-type-primary;
+ &--success {
+ border-color: $u-type-success;
+ &--error {
+ border-color: $u-type-error;
+ &--warning {
+ border-color: $u-type-warning;
+ &--default--disabled {
+ border-color: #e4e7ed;
+ &--primary--disabled {
+ color: #ffffff!important;
+ border-color: $u-type-primary-disabled!important;
+ background-color: $u-type-primary-disabled!important;
+ &--success--disabled {
+ border-color: $u-type-success-disabled!important;
+ background-color: $u-type-success-disabled!important;
+ &--error--disabled {
+ border-color: $u-type-error-disabled!important;
+ background-color: $u-type-error-disabled!important;
+ &--warning--disabled {
+ border-color: $u-type-warning-disabled!important;
+ background-color: $u-type-warning-disabled!important;
+ &--primary--plain {
+ color: $u-type-primary!important;
+ background-color: $u-type-primary-light!important;
+ &--success--plain {
+ color: $u-type-success!important;
+ background-color: $u-type-success-light!important;
+ &--error--plain {
+ color: $u-type-error!important;
+ background-color: $u-type-error-light!important;
+ &--warning--plain {
+ color: $u-type-warning!important;
+ background-color: $u-type-warning-light!important;
-/* #ifdef APP-NVUE */
-@import "./nvue.scss";
+.u-hairline-border:after {
+ content: ' ';
+ pointer-events: none;
+ // 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border)
+ // 中心点作为变形(scale())的原点
+ -webkit-transform-origin: 0 0;
+ transform-origin: 0 0;
+ width: 199.8%;
+ height: 199.7%;
+ -webkit-transform: scale(0.5, 0.5);
+ transform: scale(0.5, 0.5);
+ border: 1px solid currentColor;
-$u-button-u-button-height: 40px !default;
-$u-button-text-font-size: 15px !default;
-$u-button-loading-text-font-size: 15px !default;
-$u-button-loading-text-margin-left: 4px !default;
-$u-button-large-width: 100% !default;
-$u-button-large-height: 50px !default;
-$u-button-normal-padding: 0 12px !default;
-$u-button-large-padding: 0 15px !default;
-$u-button-normal-font-size: 14px !default;
-$u-button-small-min-width: 60px !default;
-$u-button-small-height: 30px !default;
-$u-button-small-padding: 0px 8px !default;
-$u-button-mini-padding: 0px 8px !default;
-$u-button-small-font-size: 12px !default;
-$u-button-mini-height: 22px !default;
-$u-button-mini-font-size: 10px !default;
-$u-button-mini-min-width: 50px !default;
-$u-button-disabled-opacity: 0.5 !default;
-$u-button-info-color: #323233 !default;
-$u-button-info-background-color: #fff !default;
-$u-button-info-border-color: #ebedf0 !default;
-$u-button-info-border-width: 1px !default;
-$u-button-info-border-style: solid !default;
-$u-button-success-color: #fff !default;
-$u-button-success-background-color: $u-success !default;
-$u-button-success-border-color: $u-button-success-background-color !default;
-$u-button-success-border-width: 1px !default;
-$u-button-success-border-style: solid !default;
-$u-button-primary-color: #fff !default;
-$u-button-primary-background-color: $u-primary !default;
-$u-button-primary-border-color: $u-button-primary-background-color !default;
-$u-button-primary-border-width: 1px !default;
-$u-button-primary-border-style: solid !default;
-$u-button-error-color: #fff !default;
-$u-button-error-background-color: $u-error !default;
-$u-button-error-border-color: $u-button-error-background-color !default;
-$u-button-error-border-width: 1px !default;
-$u-button-error-border-style: solid !default;
-$u-button-warning-color: #fff !default;
-$u-button-warning-background-color: $u-warning !default;
-$u-button-warning-border-color: $u-button-warning-background-color !default;
-$u-button-warning-border-width: 1px !default;
-$u-button-warning-border-style: solid !default;
-$u-button-block-width: 100% !default;
-$u-button-circle-border-top-right-radius: 100px !default;
-$u-button-circle-border-top-left-radius: 100px !default;
-$u-button-circle-border-bottom-left-radius: 100px !default;
-$u-button-circle-border-bottom-right-radius: 100px !default;
-$u-button-square-border-top-right-radius: 3px !default;
-$u-button-square-border-top-left-radius: 3px !default;
-$u-button-square-border-bottom-left-radius: 3px !default;
-$u-button-square-border-bottom-right-radius: 3px !default;
-$u-button-icon-min-width: 1em !default;
-$u-button-plain-background-color: #fff !default;
-$u-button-hairline-border-width: 0.5px !default;
+.u-wave-ripple {
+ z-index: 0;
+ border-radius: 100%;
+ background-clip: padding-box;
+ user-select: none;
+ transform: scale(0);
+ opacity: 1;
+ transform-origin: center;
- height: $u-button-u-button-height;
- /* #ifndef APP-NVUE */
- box-sizing: border-box;
- /* #endif */
- flex-direction: row;
+.u-wave-ripple.u-wave-active {
+ transform: scale(2);
+ transition: opacity 1s linear, transform 0.4s linear;
- font-size: $u-button-text-font-size;
+.u-round-circle {
- font-size: $u-button-loading-text-font-size;
- margin-left: $u-button-loading-text-margin-left;
+.u-round-circle::after {
- &--large {
- width: $u-button-large-width;
- height: $u-button-large-height;
- padding: $u-button-large-padding;
+.u-loading::after {
+ background-color: hsla(0, 0%, 100%, 0.35);
- &--normal {
- padding: $u-button-normal-padding;
- font-size: $u-button-normal-font-size;
+.u-size-default {
+ font-size: 30rpx;
+ line-height: 80rpx;
- &--small {
- min-width: $u-button-small-min-width;
- height: $u-button-small-height;
- padding: $u-button-small-padding;
- font-size: $u-button-small-font-size;
+.u-size-medium {
+ width: auto;
+ height: 70rpx;
+ line-height: 70rpx;
+ padding: 0 80rpx;
- &--mini {
- height: $u-button-mini-height;
- font-size: $u-button-mini-font-size;
- min-width: $u-button-mini-min-width;
- padding: $u-button-mini-padding;
+.u-size-mini {
+ font-size: 22rpx;
+ padding-top: 1px;
+ height: 50rpx;
+ line-height: 50rpx;
+ padding: 0 20rpx;
- &--disabled {
- opacity: $u-button-disabled-opacity;
+.u-primary-plain-hover {
+ color: #ffffff !important;
+ background: $u-type-primary-dark !important;
- color: $u-button-info-color;
- background-color: $u-button-info-background-color;
- border-color: $u-button-info-border-color;
- border-width: $u-button-info-border-width;
- border-style: $u-button-info-border-style;
+.u-default-plain-hover {
+ color: $u-type-primary-dark !important;
+ background: $u-type-primary-light !important;
- color: $u-button-success-color;
- background-color: $u-button-success-background-color;
- border-color: $u-button-success-border-color;
- border-width: $u-button-success-border-width;
- border-style: $u-button-success-border-style;
+.u-success-plain-hover {
+ background: $u-type-success-dark !important;
- color: $u-button-primary-color;
- background-color: $u-button-primary-background-color;
- border-color: $u-button-primary-border-color;
- border-width: $u-button-primary-border-width;
- border-style: $u-button-primary-border-style;
+.u-warning-plain-hover {
+ background: $u-type-warning-dark !important;
- color: $u-button-error-color;
- background-color: $u-button-error-background-color;
- border-color: $u-button-error-border-color;
- border-width: $u-button-error-border-width;
- border-style: $u-button-error-border-style;
+.u-error-plain-hover {
+ background: $u-type-error-dark !important;
- color: $u-button-warning-color;
- background-color: $u-button-warning-background-color;
- border-color: $u-button-warning-border-color;
- border-width: $u-button-warning-border-width;
- border-style: $u-button-warning-border-style;
+.u-info-plain-hover {
+ background: $u-type-info-dark !important;
- &--block {
- width: $u-button-block-width;
+.u-default-hover {
+ border-color: $u-type-primary-dark !important;
+ background-color: $u-type-primary-light !important;
- &--circle {
- border-top-right-radius: $u-button-circle-border-top-right-radius;
- border-top-left-radius: $u-button-circle-border-top-left-radius;
- border-bottom-left-radius: $u-button-circle-border-bottom-left-radius;
- border-bottom-right-radius: $u-button-circle-border-bottom-right-radius;
+.u-primary-hover {
- &--square {
- border-bottom-left-radius: $u-button-square-border-top-right-radius;
- border-bottom-right-radius: $u-button-square-border-top-left-radius;
- border-top-left-radius: $u-button-square-border-bottom-left-radius;
- border-top-right-radius: $u-button-square-border-bottom-right-radius;
+.u-success-hover {
- min-width: $u-button-icon-min-width;
- line-height: inherit !important;
- vertical-align: top;
+.u-info-hover {
- &--plain {
- background-color: $u-button-plain-background-color;
+.u-warning-hover {
- &--hairline {
- border-width: $u-button-hairline-border-width !important;
+.u-error-hover {
@@ -1,80 +0,0 @@
-// nvue下hover-class无效
-$u-button-before-top:50% !default;
-$u-button-before-left:50% !default;
-$u-button-before-width:100% !default;
-$u-button-before-height:100% !default;
-$u-button-before-transform:translate(-50%, -50%) !default;
-$u-button-before-opacity:0 !default;
-$u-button-before-background-color:#000 !default;
-$u-button-before-border-color:#000 !default;
-$u-button-active-before-opacity:.15 !default;
-$u-button-icon-margin-left:4px !default;
-$u-button-plain-u-button-info-color:$u-info;
-$u-button-plain-u-button-success-color:$u-success;
-$u-button-plain-u-button-error-color:$u-error;
-$u-button-plain-u-button-warning-color:$u-error;
- width: 100%;
- white-space: nowrap;
- line-height: 1;
- &:before {
- top:$u-button-before-top;
- left:$u-button-before-left;
- width:$u-button-before-width;
- height:$u-button-before-height;
- border: inherit;
- border-radius: inherit;
- transform:$u-button-before-transform;
- opacity:$u-button-before-opacity;
- content: " ";
- background-color:$u-button-before-background-color;
- border-color:$u-button-before-border-color;
- opacity: .15
- &__icon+&__text:not(:empty),
- margin-left:$u-button-icon-margin-left;
- &.u-button--primary {
- &.u-button--info {
- color:$u-button-plain-u-button-info-color;
- &.u-button--success {
- color:$u-button-plain-u-button-success-color;
- &.u-button--error {
- color:$u-button-plain-u-button-error-color;
- &.u-button--warning {
- color:$u-button-plain-u-button-warning-color;
@@ -1,99 +0,0 @@
- <view class="u-calendar-header u-border-bottom">
- class="u-calendar-header__title"
- v-if="showTitle"
- class="u-calendar-header__subtitle"
- v-if="showSubtitle"
- >{{ subtitle }}</text>
- <view class="u-calendar-header__weekdays">
- <text class="u-calendar-header__weekdays__weekday">一</text>
- <text class="u-calendar-header__weekdays__weekday">二</text>
- <text class="u-calendar-header__weekdays__weekday">三</text>
- <text class="u-calendar-header__weekdays__weekday">四</text>
- <text class="u-calendar-header__weekdays__weekday">五</text>
- <text class="u-calendar-header__weekdays__weekday">六</text>
- <text class="u-calendar-header__weekdays__weekday">日</text>
- name: 'u-calendar-header',
- mixins: [uni.$u.mpMixin, uni.$u.mixin],
- default: ''
- // 副标题
- subtitle: {
- // 是否显示标题
- showTitle: {
- default: true
- // 是否显示副标题
- showSubtitle: {
- name() {
- .u-calendar-header {
- padding-bottom: 4px;
- font-size: 16px;
- height: 42px;
- line-height: 42px;
- &__subtitle {
- height: 40px;
- line-height: 40px;
- &__weekdays {
- justify-content: space-between;
- &__weekday {
- line-height: 30px;
@@ -1,579 +0,0 @@
- <view class="u-calendar-month-wrapper" ref="u-calendar-month-wrapper">
- <view v-for="(item, index) in months" :key="index" :class="[`u-calendar-month-${index}`]"
- :ref="`u-calendar-month-${index}`" :id="`month-${index}`">
- <text v-if="index !== 0" class="u-calendar-month__title">{{ item.year }}年{{ item.month }}月</text>
- <view class="u-calendar-month__days">
- <view v-if="showMark" class="u-calendar-month__days__month-mark-wrapper">
- <text class="u-calendar-month__days__month-mark-wrapper__text">{{ item.month }}</text>
- <view class="u-calendar-month__days__day" v-for="(item1, index1) in item.date" :key="index1"
- :style="[dayStyle(index, index1, item1)]" @tap="clickHandler(index, index1, item1)"
- :class="[item1.selected && 'u-calendar-month__days__day__select--selected']">
- <view class="u-calendar-month__days__day__select" :style="[daySelectStyle(index, index1, item1)]">
- <text class="u-calendar-month__days__day__select__info"
- :class="[item1.disabled && 'u-calendar-month__days__day__select__info--disabled']"
- :style="[textStyle(item1)]">{{ item1.day }}</text>
- <text v-if="getBottomInfo(index, index1, item1)"
- class="u-calendar-month__days__day__select__buttom-info"
- :class="[item1.disabled && 'u-calendar-month__days__day__select__buttom-info--disabled']"
- :style="[textStyle(item1)]">{{ getBottomInfo(index, index1, item1) }}</text>
- <text v-if="item1.dot" class="u-calendar-month__days__day__select__dot"></text>
- // 由于nvue不支持百分比单位,需要查询宽度来计算每个日期的宽度
- const dom = uni.requireNativePlugin('dom')
- import dayjs from '../../libs/util/dayjs.js';
- name: 'u-calendar-month',
- // 是否显示月份背景色
- showMark: {
- // 主题色,对底部按钮和选中日期有效
- default: '#3c9cff'
- // 月份数据
- months: {
- default: () => []
- // 日期选择类型
- default: 'single'
- // 日期行高
- rowHeight: {
- default: 58
- // mode=multiple时,最多可选多少个日期
- default: Infinity
- // mode=range时,第一个日期底部的提示文字
- startText: {
- default: '开始'
- // mode=range时,最后一个日期底部的提示文字
- endText: {
- default: '结束'
- // 默认选中的日期,mode为multiple或range是必须为数组格式
- defaultDate: {
- type: [Array, String, Date],
- default: null
- // 最小的可选日期
- minDate: {
- default: 0
- // 最大可选日期
- maxDate: {
- // 如果没有设置maxDate,则往后推多少个月
- maxMonth: {
- default: 2
- // 是否为只读状态,只读状态下禁止选择日期
- readonly: {
- default: uni.$u.props.calendar.readonly
- // 日期区间最多可选天数,默认无限制,mode = range时有效
- maxRange: {
- // 范围选择超过最多可选天数时的提示文案,mode = range时有效
- rangePrompt: {
- // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效
- showRangePrompt: {
- // 是否允许日期范围的起止时间为同一天,mode = range时有效
- allowSameDay: {
- default: false
- // 每个日期的宽度
- width: 0,
- // 当前选中的日期item
- item: {},
- selected: []
- selectedChange: {
- handler(n) {
- this.setDefaultDate()
- // 多个条件的变化,会引起选中日期的变化,这里统一管理监听
- selectedChange() {
- return [this.minDate, this.maxDate, this.defaultDate]
- dayStyle(index1, index2, item) {
- return (index1, index2, item) => {
- let week = item.week
- // 不进行四舍五入的形式保留2位小数
- const dayWidth = Number(parseFloat(this.width / 7).toFixed(3).slice(0, -1))
- // 得出每个日期的宽度
- style.width = uni.$u.addUnit(dayWidth)
- style.height = uni.$u.addUnit(this.rowHeight)
- if (index2 === 0) {
- // 获取当前为星期几,如果为0,则为星期天,减一为每月第一天时,需要向左偏移的item个数
- week = (week === 0 ? 7 : week) - 1
- style.marginLeft = uni.$u.addUnit(week * dayWidth)
- if (this.mode === 'range') {
- // 之所以需要这么写,是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
- style.paddingLeft = 0
- style.paddingRight = 0
- style.paddingBottom = 0
- style.paddingTop = 0
- daySelectStyle() {
- let date = dayjs(item.date).format("YYYY-MM-DD"),
- style = {}
- // 判断date是否在selected数组中,因为月份可能会需要补0,所以使用dateSame判断,而不用数组的includes判断
- if (this.selected.some(item => this.dateSame(item, date))) {
- style.backgroundColor = this.color
- if (this.mode === 'single') {
- if (date === this.selected[0]) {
- // 因为需要对nvue的兼容,只能这么写,无法缩写,也无法通过类名控制等等
- style.borderTopLeftRadius = '3px'
- style.borderBottomLeftRadius = '3px'
- style.borderTopRightRadius = '3px'
- style.borderBottomRightRadius = '3px'
- } else if (this.mode === 'range') {
- if (this.selected.length >= 2) {
- const len = this.selected.length - 1
- // 第一个日期设置左上角和左下角的圆角
- if (this.dateSame(date, this.selected[0])) {
- // 最后一个日期设置右上角和右下角的圆角
- if (this.dateSame(date, this.selected[len])) {
- // 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
- if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
- .selected[len]))) {
- style.backgroundColor = uni.$u.colorGradient(this.color, '#ffffff', 100)[90]
- // 增加一个透明度,让范围区间的背景色也能看到底部的mark水印字符
- style.opacity = 0.7
- } else if (this.selected.length === 1) {
- // 进行还原操作,否则在nvue的iOS,uni-app有bug,会导致诡异的表现
- // 某个日期是否被选中
- textStyle() {
- return (item) => {
- const date = dayjs(item.date).format("YYYY-MM-DD"),
- // 选中的日期,提示文字设置白色
- style.color = '#ffffff'
- // 如果是范围选择模式,第一个和最后一个之间的日期,文字颜色设置为高亮的主题色
- // 获取底部的提示文字
- getBottomInfo() {
- const date = dayjs(item.date).format("YYYY-MM-DD")
- const bottomInfo = item.bottomInfo
- // 当为日期范围模式时,且选择的日期个数大于0时
- if (this.mode === 'range' && this.selected.length > 0) {
- if (this.selected.length === 1) {
- // 选择了一个日期时,如果当前日期为数组中的第一个日期,则显示底部文字为“开始”
- if (this.dateSame(date, this.selected[0])) return this.startText
- else return bottomInfo
- // 如果数组中的日期大于2个时,第一个和最后一个显示为开始和结束日期
- if (this.dateSame(date, this.selected[0]) && this.dateSame(date, this.selected[1]) &&
- len === 1) {
- // 如果长度为2,且第一个等于第二个日期,则提示语放在同一个item中
- return `${this.startText}/${this.endText}`
- } else if (this.dateSame(date, this.selected[0])) {
- return this.startText
- } else if (this.dateSame(date, this.selected[len])) {
- return this.endText
- return bottomInfo
- mounted() {
- this.init()
- init() {
- // 初始化默认选中
- this.$emit('monthSelected', this.selected)
- this.$nextTick(() => {
- // 这里需要另一个延时,因为获取宽度后,会进行月份数据渲染,只有渲染完成之后,才有真正的高度
- // 因为nvue下,$nextTick并不是100%可靠的
- uni.$u.sleep(10).then(() => {
- this.getWrapperWidth()
- this.getMonthRect()
- // 判断两个日期是否相等
- dateSame(date1, date2) {
- return dayjs(date1).isSame(dayjs(date2))
- // 获取月份数据区域的宽度,因为nvue不支持百分比,所以无法通过css设置每个日期item的宽度
- getWrapperWidth() {
- dom.getComponentRect(this.$refs['u-calendar-month-wrapper'], res => {
- this.width = res.size.width
- this.$uGetRect('.u-calendar-month-wrapper').then(size => {
- this.width = size.width
- getMonthRect() {
- // 获取每个月份数据的尺寸,用于父组件在scroll-view滚动事件中,监听当前滚动到了第几个月份
- const promiseAllArr = this.months.map((item, index) => this.getMonthRectByPromise(
- `u-calendar-month-${index}`))
- // 一次性返回
- Promise.all(promiseAllArr).then(
- sizes => {
- let height = 1
- const topArr = []
- for (let i = 0; i < this.months.length; i++) {
- // 添加到months数组中,供scroll-view滚动事件中,判断当前滚动到哪个月份
- topArr[i] = height
- height += sizes[i].height
- // 由于微信下,无法通过this.months[i].top的形式(引用类型)去修改父组件的month的top值,所以使用事件形式对外发出
- this.$emit('updateMonthTop', topArr)
- // 获取每个月份区域的尺寸
- getMonthRectByPromise(el) {
- // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
- // 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
- return new Promise(resolve => {
- this.$uGetRect(`.${el}`).then(size => {
- resolve(size)
- // nvue下,使用dom模块查询元素高度
- // 返回一个promise,让调用此方法的主体能使用then回调
- dom.getComponentRect(this.$refs[el][0], res => {
- resolve(res.size)
- // 点击某一个日期
- clickHandler(index1, index2, item) {
- if (this.readonly) {
- return;
- this.item = item
- if (item.disabled) return
- // 对上一次选择的日期数组进行深度克隆
- let selected = uni.$u.deepClone(this.selected)
- // 单选情况下,让数组中的元素为当前点击的日期
- selected = [date]
- } else if (this.mode === 'multiple') {
- if (selected.some(item => this.dateSame(item, date))) {
- // 如果点击的日期已在数组中,则进行移除操作,也就是达到反选的效果
- const itemIndex = selected.findIndex(item => item === date)
- selected.splice(itemIndex, 1)
- // 如果点击的日期不在数组中,且已有的长度小于总可选长度时,则添加到数组中去
- if (selected.length < this.maxCount) selected.push(date)
- // 选择区间形式
- if (selected.length === 0 || selected.length >= 2) {
- // 如果原来就为0或者大于2的长度,则当前点击的日期,就是开始日期
- } else if (selected.length === 1) {
- // 如果已经选择了开始日期
- const existsDate = selected[0]
- // 如果当前选择的日期小于上一次选择的日期,则当前的日期定为开始日期
- if (dayjs(date).isBefore(existsDate)) {
- } else if (dayjs(date).isAfter(existsDate)) {
- // 当前日期减去最大可选的日期天数,如果大于起始时间,则进行提示
- if(dayjs(dayjs(date).subtract(this.maxRange, 'day')).isAfter(dayjs(selected[0])) && this.showRangePrompt) {
- if(this.rangePrompt) {
- uni.$u.toast(this.rangePrompt)
- uni.$u.toast(`选择天数不能超过 ${this.maxRange} 天`)
- return
- // 如果当前日期大于已有日期,将当前的添加到数组尾部
- selected.push(date)
- const startDate = selected[0]
- const endDate = selected[1]
- let i = 0
- do {
- // 将开始和结束日期之间的日期添加到数组中
- arr.push(dayjs(startDate).add(i, 'day').format("YYYY-MM-DD"))
- i++
- // 累加的日期小于结束日期时,继续下一次的循环
- } while (dayjs(startDate).add(i, 'day').isBefore(dayjs(endDate)))
- // 为了一次性修改数组,避免computed中多次触发,这里才用arr变量一次性赋值的方式,同时将最后一个日期添加近来
- arr.push(endDate)
- selected = arr
- // 选择区间时,只有一个日期的情况下,且不允许选择起止为同一天的话,不允许选择自己
- if (selected[0] === date && !this.allowSameDay) return
- this.setSelected(selected)
- // 设置默认日期
- setDefaultDate() {
- if (!this.defaultDate) {
- // 如果没有设置默认日期,则将当天日期设置为默认选中的日期
- const selected = [dayjs().format("YYYY-MM-DD")]
- return this.setSelected(selected, false)
- let defaultDate = []
- const minDate = this.minDate || dayjs().format("YYYY-MM-DD")
- const maxDate = this.maxDate || dayjs(minDate).add(this.maxMonth - 1, 'month').format("YYYY-MM-DD")
- // 单选模式,可以是字符串或数组,Date对象等
- if (!uni.$u.test.array(this.defaultDate)) {
- defaultDate = [dayjs(this.defaultDate).format("YYYY-MM-DD")]
- defaultDate = [this.defaultDate[0]]
- // 如果为非数组,则不执行
- if (!uni.$u.test.array(this.defaultDate)) return
- defaultDate = this.defaultDate
- // 过滤用户传递的默认数组,取出只在可允许最大值与最小值之间的元素
- defaultDate = defaultDate.filter(item => {
- return dayjs(item).isAfter(dayjs(minDate).subtract(1, 'day')) && dayjs(item).isBefore(dayjs(
- maxDate).add(1, 'day'))
- this.setSelected(defaultDate, false)
- setSelected(selected, event = true) {
- this.selected = selected
- event && this.$emit('monthSelected', this.selected)
- .u-calendar-month-wrapper {
- margin-top: 4px;
- .u-calendar-month {
- &__days {
- &__month-mark-wrapper {
- font-size: 155px;
- color: rgba(231, 232, 234, 0.83);
- &__day {
- padding: 2px;
- // vue下使用css进行宽度计算,因为某些安卓机会无法进行js获取父元素宽度进行计算得出,会有偏移
- width: calc(100% / 7);
- &__select {
- &__dot {
- width: 7px;
- height: 7px;
- top: 12px;
- right: 7px;
- &__buttom-info {
- color: $u-content-color;
- bottom: 5px;
- font-size: 10px;
- &--selected {
- color: #ffffff;
- color: #cacbcd;
- &__info {
- border-radius: 3px;
- &--range-selected {
- opacity: 0.3;
- border-radius: 0;
- &--range-start-selected {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- &--range-end-selected {
- border-top-left-radius: 0;
@@ -1,144 +0,0 @@
- // 日历顶部标题
- default: uni.$u.props.calendar.title
- default: uni.$u.props.calendar.showTitle
- default: uni.$u.props.calendar.showSubtitle
- // 日期类型选择,single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围
- default: uni.$u.props.calendar.mode
- default: uni.$u.props.calendar.startText
- default: uni.$u.props.calendar.endText
- // 自定义列表
- customList: {
- default: uni.$u.props.calendar.customList
- default: uni.$u.props.calendar.color
- default: uni.$u.props.calendar.minDate
- default: uni.$u.props.calendar.maxDate
- type: [Array, String, Date, null],
- default: uni.$u.props.calendar.defaultDate
- default: uni.$u.props.calendar.maxCount
- default: uni.$u.props.calendar.rowHeight
- // 日期格式化函数
- formatter: {
- type: [Function, null],
- default: uni.$u.props.calendar.formatter
- // 是否显示农历
- showLunar: {
- default: uni.$u.props.calendar.showLunar
- default: uni.$u.props.calendar.showMark
- // 确定按钮的文字
- confirmText: {
- default: uni.$u.props.calendar.confirmText
- // 确认按钮处于禁用状态时的文字
- confirmDisabledText: {
- default: uni.$u.props.calendar.confirmDisabledText
- // 是否显示日历弹窗
- default: uni.$u.props.calendar.show
- // 是否允许点击遮罩关闭日历
- default: uni.$u.props.calendar.closeOnClickOverlay
- // 是否展示确认按钮
- showConfirm: {
- default: uni.$u.props.calendar.showConfirm
- default: uni.$u.props.calendar.maxRange
- default: uni.$u.props.calendar.rangePrompt
- default: uni.$u.props.calendar.showRangePrompt
- default: uni.$u.props.calendar.allowSameDay
- default: uni.$u.props.calendar.round
- // 最多展示月份数量
- monthNum: {
- default: 3
@@ -1,384 +1,643 @@
- closeable
- @close="close"
- :closeOnClickOverlay="closeOnClickOverlay"
+ <u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
+ :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
<view class="u-calendar">
- <uHeader
- :title="title"
- :subtitle="subtitle"
- :showSubtitle="showSubtitle"
- :showTitle="showTitle"
- ></uHeader>
- <scroll-view
- height: $u.addUnit(listHeight)
- scroll-y
- @scroll="onScroll"
- :scroll-top="scrollTop"
- :scrollIntoView="scrollIntoView"
- <uMonth
- :rowHeight="rowHeight"
- :showMark="showMark"
- :months="months"
- :maxCount="maxCount"
- :startText="startText"
- :endText="endText"
- :defaultDate="defaultDate"
- :minDate="innerMinDate"
- :maxDate="innerMaxDate"
- :maxMonth="monthNum"
- :maxRange="maxRange"
- :rangePrompt="rangePrompt"
- :showRangePrompt="showRangePrompt"
- :allowSameDay="allowSameDay"
- ref="month"
- @monthSelected="monthSelected"
- @updateMonthTop="updateMonthTop"
- ></uMonth>
- </scroll-view>
- <slot name="footer" v-if="showConfirm">
- <view class="u-calendar__confirm">
- <u-button
- shape="circle"
- :text="
- buttonDisabled ? confirmDisabledText : confirmText
- @click="confirm"
- :disabled="buttonDisabled"
- ></u-button>
+ <view class="u-calendar__header">
+ <view class="u-calendar__header__text" v-if="!$slots['tooltip']">
+ {{toolTip}}
+ <slot v-else name="tooltip" />
+ <view class="u-calendar__action u-flex u-row-center">
+ <view class="u-calendar__action__icon">
+ <u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
+ <u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
+ <view class="u-calendar__action__text">{{ showTitle }}</view>
+ <u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
+ <u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
+ <view class="u-calendar__week-day">
+ <view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
+ <view class="u-calendar__content">
+ <!-- 前置空白部分 -->
+ <block v-for="(item, index) in weekdayArr" :key="index">
+ <view class="u-calendar__content__item"></view>
+ <view class="u-calendar__content__item" :class="{
+ 'u-hover-class':openDisAbled(year,month,index+1),
+ 'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
+ 'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
+ }" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
+ @tap="dateClick(index)">
+ <view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
+ <view>{{ index + 1 }}</view>
+ <view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
+ <view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
+ <view class="u-calendar__content__bg-month">{{month}}</view>
+ <view class="u-calendar__bottom">
+ <view class="u-calendar__bottom__choose">
+ <text>{{mode == 'date' ? activeDate : startDate}}</text>
+ <text v-if="endDate">至{{endDate}}</text>
+ <view class="u-calendar__bottom__btn">
+ <u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
-import uHeader from './header.vue'
-import uMonth from './month.vue'
-import util from './util.js'
-import dayjs from '../../libs/util/dayjs.js'
-import Calendar from '../../libs/util/calendar.js'
- * Calendar 日历
- * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中.
- * @tutorial https://www.uviewui.com/components/calendar.html
- * @property {String} title 标题内容 (默认 日期选择 )
- * @property {Boolean} showTitle 是否显示标题 (默认 true )
- * @property {Boolean} showSubtitle 是否显示副标题 (默认 true )
- * @property {String} mode 日期类型选择 single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 ( 默认 'single' )
- * @property {String} startText mode=range时,第一个日期底部的提示文字 (默认 '开始' )
- * @property {String} endText mode=range时,最后一个日期底部的提示文字 (默认 '结束' )
- * @property {Array} customList 自定义列表
- * @property {String} color 主题色,对底部按钮和选中日期有效 (默认 ‘#3c9cff' )
- * @property {String | Number} minDate 最小的可选日期 (默认 0 )
- * @property {String | Number} maxDate 最大可选日期 (默认 0 )
- * @property {Array | String| Date} defaultDate 默认选中的日期,mode为multiple或range是必须为数组格式
- * @property {String | Number} maxCount mode=multiple时,最多可选多少个日期 (默认 Number.MAX_SAFE_INTEGER )
- * @property {String | Number} rowHeight 日期行高 (默认 56 )
- * @property {Function} formatter 日期格式化函数
- * @property {Boolean} showLunar 是否显示农历 (默认 false )
- * @property {Boolean} showMark 是否显示月份背景色 (默认 true )
- * @property {String} confirmText 确定按钮的文字 (默认 '确定' )
- * @property {String} confirmDisabledText 确认按钮处于禁用状态时的文字 (默认 '确定' )
- * @property {Boolean} show 是否显示日历弹窗 (默认 false )
- * @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭日历 (默认 false )
- * @property {Boolean} readonly 是否为只读状态,只读状态下禁止选择日期 (默认 false )
- * @property {String | Number} maxRange 日期区间最多可选天数,默认无限制,mode = range时有效
- * @property {String} rangePrompt 范围选择超过最多可选天数时的提示文案,mode = range时有效
- * @property {Boolean} showRangePrompt 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 (默认 true )
- * @property {Boolean} allowSameDay 是否允许日期范围的起止时间为同一天,mode = range时有效 (默认 false )
- * @property {Number|String} monthNum 最多展示的月份数量 (默认 3 )
- * @event {Function()} confirm 点击确定按钮时触发 选择日期相关的返回参数
- * @event {Function()} close 日历关闭时触发 可定义页面关闭时的回调事件
- * @example <u-calendar :defaultDate="defaultDateMultiple" :show="show" mode="multiple" @confirm="confirm">
- </u-calendar>
- * */
- name: 'u-calendar',
- uHeader,
- uMonth
- // 需要显示的月份的数组
- months: [],
- // 在月份滚动区域中,当前视图中月份的index索引
- monthIndex: 0,
- // 月份滚动区域的高度
- listHeight: 0,
- // month组件中选择的日期数组
- selected: [],
- scrollIntoView: '',
- scrollTop:0,
- // 过滤处理方法
- innerFormatter: (value) => value
- this.setMonth()
- // 打开弹窗时,设置月份数据
+ * calendar 日历
+ * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
+ * @tutorial http://uviewui.com/components/calendar.html
+ * @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围
+ * @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
+ * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+ * @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
+ * @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
+ * @property {String Number} max-year 可切换的最大年份(默认2050)
+ * @property {String Number} min-year 可切换的最小年份(默认1950)
+ * @property {String Number} min-date 最小可选日期(默认1950-01-01)
+ * @property {String Number} max-date 最大可选日期(默认当前日期)
+ * @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
+ * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
+ * @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
+ * @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
+ * @property {String} color 日期字体的默认颜色(默认#303133)
+ * @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
+ * @property {String Number} z-index 弹出时的z-index值(默认10075)
+ * @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
+ * @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
+ * @property {String} range-color 选择范围内字体颜色(默认#2979ff)
+ * @property {String} start-text 起始日期底部的提示文字(默认 '开始')
+ * @property {String} end-text 结束日期底部的提示文字(默认 '结束')
+ * @property {String} btn-type 底部确定按钮的主题(默认 'primary')
+ * @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期')
+ * @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
+ * @example <u-calendar v-model="show" :mode="mode"></u-calendar>
+ name: 'u-calendar',
+ // 是否允许通过点击遮罩关闭Picker
+ // 是否允许切换年份
+ changeYear: {
+ // 是否允许切换月份
+ changeMonth: {
+ // date-单个日期选择,range-开始日期+结束日期选择
+ default: 'date'
+ // 可切换的最大年份
+ maxYear: {
+ default: 2050
+ // 可切换的最小年份
+ minYear: {
+ default: 1950
+ // 最小可选日期(不在范围内日期禁用不可选)
+ minDate: {
+ default: '1950-01-01'
+ * 最大可选日期
+ * 默认最大值为今天,之后的日期不可选
+ * 2030-12-31
+ * */
+ maxDate: {
+ // 弹窗顶部左右两边的圆角值
+ // 月份切换按钮箭头颜色
+ monthArrowColor: {
+ default: '#606266'
+ // 年份切换按钮箭头颜色
+ yearArrowColor: {
+ default: '#909399'
+ // 默认日期字体颜色
+ default: '#303133'
+ // 选中|起始结束日期背景色
+ activeBgColor: {
+ default: '#2979ff'
+ // 选中|起始结束日期字体颜色
+ activeColor: {
+ // 范围内日期背景色
+ rangeBgColor: {
+ default: 'rgba(41,121,255,0.13)'
+ // 范围内日期字体颜色
+ rangeColor: {
+ // mode=range时生效,起始日期自定义文案
+ startText: {
+ default: '开始'
+ // mode=range时生效,结束日期自定义文案
+ endText: {
+ default: '结束'
+ //按钮样式类型
+ btnType: {
+ default: 'primary'
+ // 当前选中日期带选中效果
+ isActiveCurrent: {
+ // 切换年月是否触发事件 mode=date时生效
+ isChange: {
+ // 是否显示右上角的关闭图标
+ closeable: {
+ toolTip: {
+ default: '选择日期'
- // 由于maxDate和minDate可以为字符串(2021-10-10),或者数值(时间戳),但是dayjs如果接受字符串形式的时间戳会有问题,这里进行处理
- innerMaxDate() {
- return uni.$u.test.number(this.maxDate)
- ? Number(this.maxDate)
- : this.maxDate
- innerMinDate() {
- return uni.$u.test.number(this.minDate)
- ? Number(this.minDate)
- : this.minDate
+ // 星期几,值为1-7
+ weekday: 1,
+ weekdayArr:[],
+ // 当前月有多少天
+ days: 0,
+ daysArr:[],
+ showTitle: '',
+ year: 2020,
+ month: 0,
+ day: 0,
+ startYear: 0,
+ startMonth: 0,
+ startDay: 0,
+ endYear: 0,
+ endMonth: 0,
+ endDay: 0,
+ today: '',
+ activeDate: '',
+ startDate: '',
+ endDate: '',
+ isStart: true,
+ min: null,
+ max: null,
+ weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
- return [this.innerMinDate, this.innerMaxDate, this.defaultDate]
+ dataChange() {
+ return `${this.mode}-${this.minDate}-${this.maxDate}`;
- subtitle() {
- // 初始化时,this.months为空数组,所以需要特别判断处理
- if (this.months.length) {
- return `${this.months[this.monthIndex].year}年${
- this.months[this.monthIndex].month
- }月`
- return ''
+ dataChange(val) {
+ this.init()
- buttonDisabled() {
- // 如果为range类型,且选择的日期个数不足1个时,让底部的按钮出于disabled状态
- if (this.selected.length <= 1) {
- return true
+ created() {
+ getColor(index, type) {
+ let color = type == 1 ? '' : this.color;
+ let day = index + 1
+ let date = `${this.year}-${this.month}-${day}`
+ let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
+ let start = this.startDate.replace(/\-/g, '/')
+ let end = this.endDate.replace(/\-/g, '/')
+ if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
+ color = type == 1 ? this.activeBgColor : this.activeColor;
+ } else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
+ color = type == 1 ? this.rangeBgColor : this.rangeColor;
+ return color;
+ init() {
+ let now = new Date();
+ let minDate = new Date(this.minDate);
+ let maxDate = new Date(this.maxDate);
+ if (now < minDate) now = minDate;
+ if (now > maxDate) now = maxDate;
+ this.year = now.getFullYear();
+ this.month = now.getMonth() + 1;
+ this.day = now.getDate();
+ this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
+ this.activeDate = this.today;
+ this.min = this.initDate(this.minDate);
+ this.max = this.initDate(this.maxDate || this.today);
+ this.startDate = "";
+ this.startYear = 0;
+ this.startMonth = 0;
+ this.startDay = 0;
+ this.endYear = 0;
+ this.endMonth = 0;
+ this.endDay = 0;
+ this.endDate = "";
+ this.isStart = true;
+ this.changeData();
+ //日期处理
+ initDate(date) {
+ let fdate = date.split('-');
+ year: Number(fdate[0] || 1920),
+ month: Number(fdate[1] || 1),
+ day: Number(fdate[2] || 1)
+ openDisAbled: function(year, month, day) {
+ let bool = true;
+ let date = `${year}/${month}/${day}`;
+ // let today = this.today.replace(/\-/g, '/');
+ let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
+ let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
+ let timestamp = new Date(date).getTime();
+ if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
+ bool = false;
+ return bool;
+ generateArray: function(start, end) {
+ return Array.from(new Array(end + 1).keys()).slice(start);
+ formatNum: function(num) {
+ return num < 10 ? '0' + num : num + '';
+ //一个月有多少天
+ getMonthDay(year, month) {
+ let days = new Date(year, month, 0).getDate();
+ return days;
+ getWeekday(year, month) {
+ let date = new Date(`${year}/${month}/01 00:00:00`);
+ return date.getDay();
+ checkRange(year) {
+ let overstep = false;
+ if (year < this.minYear || year > this.maxYear) {
+ uni.showToast({
+ title: "日期超出范围啦~",
+ icon: 'none'
+ overstep = true;
+ return overstep;
+ changeMonthHandler(isAdd) {
+ if (isAdd) {
+ let month = this.month + 1;
+ let year = month > 12 ? this.year + 1 : this.year;
+ if (!this.checkRange(year)) {
+ this.month = month > 12 ? 1 : month;
+ this.year = year;
- return false
+ let month = this.month - 1;
+ let year = month < 1 ? this.year - 1 : this.year;
+ this.month = month < 1 ? 12 : month;
+ changeYearHandler(isAdd) {
+ let year = isAdd ? this.year + 1 : this.year - 1;
+ changeData() {
+ this.days = this.getMonthDay(this.year, this.month);
+ this.daysArr=this.generateArray(1,this.days)
+ this.weekday = this.getWeekday(this.year, this.month);
+ this.weekdayArr=this.generateArray(1,this.weekday)
+ this.showTitle = `${this.year}年${this.month}月`;
+ if (this.isChange && this.mode == 'date') {
+ this.btnFix(true);
+ dateClick: function(day) {
+ day += 1;
+ if (!this.openDisAbled(this.year, this.month, day)) {
+ this.day = day;
+ let date = `${this.year}-${this.month}-${day}`;
+ if (this.mode == 'date') {
+ this.activeDate = date;
+ let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
+ if (this.isStart || compare) {
+ this.startDate = date;
+ this.startYear = this.year;
+ this.startMonth = this.month;
+ this.startDay = this.day;
+ this.activeDate = "";
+ this.isStart = false;
+ this.endDate = date;
+ this.endYear = this.year;
+ this.endMonth = this.month;
+ this.endDay = this.day;
+ // 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
+ getWeekText(date) {
+ date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
+ let week = date.getDay();
+ return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
+ btnFix(show) {
+ if (!show) {
+ this.close();
+ let arr = this.activeDate.split('-')
+ let year = this.isChange ? this.year : Number(arr[0]);
+ let month = this.isChange ? this.month : Number(arr[1]);
+ let day = this.isChange ? this.day : Number(arr[2]);
+ //当前月有多少天
+ let days = this.getMonthDay(year, month);
+ let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
+ let weekText = this.getWeekText(result);
+ let isToday = false;
+ if (`${year}-${month}-${day}` == this.today) {
+ //今天
+ isToday = true;
+ this.$emit('change', {
+ year: year,
+ month: month,
+ day: day,
+ days: days,
+ result: result,
+ week: weekText,
+ isToday: isToday,
+ // switch: show //是否是切换年月操作
+ if (!this.startDate || !this.endDate) return;
+ let startMonth = this.formatNum(this.startMonth);
+ let startDay = this.formatNum(this.startDay);
+ let startDate = `${this.startYear}-${startMonth}-${startDay}`;
+ let startWeek = this.getWeekText(startDate)
+ let endMonth = this.formatNum(this.endMonth);
+ let endDay = this.formatNum(this.endDay);
+ let endDate = `${this.endYear}-${endMonth}-${endDay}`;
+ let endWeek = this.getWeekText(endDate);
+ startYear: this.startYear,
+ startMonth: this.startMonth,
+ startDay: this.startDay,
+ startDate: startDate,
+ startWeek: startWeek,
+ endYear: this.endYear,
+ endMonth: this.endMonth,
+ endDay: this.endDay,
+ endDate: endDate,
+ endWeek: endWeek
- this.start = Date.now()
- // 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用
- setFormatter(e) {
- this.innerFormatter = e
- // month组件内部选择日期后,通过事件通知给父组件
- monthSelected(e) {
- this.selected = e
- if (!this.showConfirm) {
- // 在不需要确认按钮的情况下,如果为单选,或者范围多选且已选长度大于2,则直接进行返还
- this.mode === 'multiple' ||
- this.mode === 'single' ||
- (this.mode === 'range' && this.selected.length >= 2)
- ) {
- this.$emit('confirm', this.selected)
+ .u-calendar {
+ &__header {
+ background-color: #fff;
+ &__text {
+ margin-top: 30rpx;
+ padding: 0 60rpx;
- // 校验maxDate,不能小于minDate
- this.innerMaxDate &&
- this.innerMinDate &&
- new Date(this.innerMaxDate).getTime() < new Date(this.innerMinDate).getTime()
- return uni.$u.error('maxDate不能小于minDate')
+ &__action {
+ padding: 40rpx 0 40rpx 0;
+ &__icon {
+ margin: 0 16rpx;
- // 滚动区域的高度
- this.listHeight = this.rowHeight * 5 + 30
- close() {
- // 点击确定按钮
- confirm() {
- if (!this.buttonDisabled) {
+ padding: 0 16rpx;
+ line-height: 32rpx;
+ font-weight: bold;
- // 获得两个日期之间的月份数
- getMonths(minDate, maxDate) {
- const minYear = dayjs(minDate).year()
- const minMonth = dayjs(minDate).month() + 1
- const maxYear = dayjs(maxDate).year()
- const maxMonth = dayjs(maxDate).month() + 1
- return (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1
- // 设置月份数据
- setMonth() {
- // 最小日期的毫秒数
- const minDate = this.innerMinDate || dayjs().valueOf()
- // 如果没有指定最大日期,则往后推3个月
- const maxDate =
- this.innerMaxDate ||
- dayjs(minDate)
- .add(this.monthNum - 1, 'month')
- .valueOf()
- // 最大最小月份之间的共有多少个月份,
- const months = uni.$u.range(
- 1,
- this.monthNum,
- this.getMonths(minDate, maxDate)
- // 先清空数组
- this.months = []
- for (let i = 0; i < months; i++) {
- this.months.push({
- date: new Array(
- dayjs(minDate).add(i, 'month').daysInMonth()
- .fill(1)
- .map((item, index) => {
- // 日期,取值1-31
- let day = index + 1
- // 星期,0-6,0为周日
- const week = dayjs(minDate)
- .add(i, 'month')
- .date(day)
- .day()
- const date = dayjs(minDate)
- .format('YYYY-MM-DD')
- let bottomInfo = ''
- if (this.showLunar) {
- // 将日期转为农历格式
- const lunar = Calendar.solar2lunar(
- dayjs(date).year(),
- dayjs(date).month() + 1,
- dayjs(date).date()
- bottomInfo = lunar.IDayCn
- let config = {
- day,
- week,
- // 小于最小允许的日期,或者大于最大的日期,则设置为disabled状态
- disabled:
- dayjs(date).isBefore(
- dayjs(minDate).format('YYYY-MM-DD')
- ) ||
- dayjs(date).isAfter(
- dayjs(maxDate).format('YYYY-MM-DD')
- ),
- // 返回一个日期对象,供外部的formatter获取当前日期的年月日等信息,进行加工处理
- date: new Date(date),
- bottomInfo,
- dot: false,
- month:
- dayjs(minDate).add(i, 'month').month() + 1
- const formatter =
- this.formatter || this.innerFormatter
- return formatter(config)
- }),
- // 当前所属的月份
- month: dayjs(minDate).add(i, 'month').month() + 1,
- // 当前年份
- year: dayjs(minDate).add(i, 'month').year()
+ &__week-day {
+ padding: 6px 0;
+ overflow: hidden;
+ flex: 1;
- // 滚动到默认设置的月份
- scrollIntoDefaultMonth(selected) {
- // 查询默认日期在可选列表的下标
- const _index = this.months.findIndex(({
- year,
- month
- }) => {
- month = uni.$u.padZero(month)
- return `${year}-${month}` === selected
- if (_index !== -1) {
- this.scrollIntoView = `month-${_index}`
- this.scrollTop = this.months[_index].top || 0;
+ flex-wrap: wrap;
+ &--end-date {
+ border-top-right-radius: 8rpx;
+ border-bottom-right-radius: 8rpx;
- // scroll-view滚动监听
- onScroll(event) {
- // 不允许小于0的滚动值,如果scroll-view到顶了,继续下拉,会出现负数值
- const scrollTop = Math.max(0, event.detail.scrollTop)
- // 将当前滚动条数值,除以滚动区域的高度,可以得出当前滚动到了哪一个月份的索引
- if (scrollTop >= (this.months[i].top || this.listHeight)) {
- this.monthIndex = i
+ &--start-date {
+ border-top-left-radius: 8rpx;
+ border-bottom-left-radius: 8rpx;
+ &__item {
+ width: 14.2857%;
+ z-index: 2;
+ &__inner {
+ height: 84rpx;
+ border-radius: 50%;
+ &__desc {
+ transform: scale(0.75);
+ bottom: 2rpx;
+ bottom: 8rpx;
- // 更新月份的top值
- updateMonthTop(topArr = []) {
- // 设置对应月份的top值,用于onScroll方法更新月份
- topArr.map((item, index) => {
- this.months[index].top = item
- // 获取默认日期的下标
- const selected = dayjs().format("YYYY-MM")
- this.scrollIntoDefaultMonth(selected)
+ &__bg-month {
+ font-size: 130px;
+ line-height: 130px;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ color: #e4e7ed;
- let selected = dayjs().format("YYYY-MM");
- selected = dayjs(this.defaultDate).format("YYYY-MM")
- selected = dayjs(this.defaultDate[0]).format("YYYY-MM");
+ &__bottom {
+ padding: 0 40rpx 30rpx;
+ &__choose {
+ &__btn {
-.u-calendar {
- &__confirm {
- padding: 7px 18px;
@@ -1,85 +0,0 @@
- // 月初是周几
- const day = dayjs(this.date).date(1).day()
- const start = day == 0 ? 6 : day - 1
- // 本月天数
- const days = dayjs(this.date).endOf('month').format('D')
- // 上个月天数
- const prevDays = dayjs(this.date).endOf('month').subtract(1, 'month').format('D')
- // 日期数据
- // 清空表格
- this.month = []
- // 添加上月数据
- arr.push(
- ...new Array(start).fill(1).map((e, i) => {
- const day = prevDays - start + i + 1
- value: day,
- disabled: true,
- date: dayjs(this.date).subtract(1, 'month').date(day).format('YYYY-MM-DD')
- // 添加本月数据
- ...new Array(days - 0).fill(1).map((e, i) => {
- const day = i + 1
- date: dayjs(this.date).date(day).format('YYYY-MM-DD')
- // 添加下个月
- ...new Array(42 - days - start).fill(1).map((e, i) => {
- date: dayjs(this.date).add(1, 'month').date(day).format('YYYY-MM-DD')
- // 分割数组
- for (let n = 0; n < arr.length; n += 7) {
- this.month.push(
- arr.slice(n, n + 7).map((e, i) => {
- e.index = i + n
- // 自定义信息
- const custom = this.customList.find((c) => c.date == e.date)
- // 农历
- if (this.lunar) {
- const {
- IDayCn,
- IMonthCn
- } = this.getLunar(e.date)
- e.lunar = IDayCn == '初一' ? IMonthCn : IDayCn
- ...e,
- ...custom
@@ -1,14 +0,0 @@
- // 是否打乱键盘按键的顺序
- random: {
- // 输入一个中文后,是否自动切换到英文
- autoChange: {
@@ -1,311 +1,257 @@
- class="u-keyboard"
- @touchmove.stop.prevent="noop"
- v-for="(group, i) in abc ? engKeyBoardList : areaList"
- :key="i"
- class="u-keyboard__button"
- :index="i"
- :class="[i + 1 === 4 && 'u-keyboard__button--center']"
- v-if="i === 3"
- class="u-keyboard__button__inner-wrapper"
- class="u-keyboard__button__inner-wrapper__left"
- hover-class="u-hover-class"
- :hover-stay-time="200"
- @tap="changeCarInputMode"
- class="u-keyboard__button__inner-wrapper__left__lang"
- :class="[!abc && 'u-keyboard__button__inner-wrapper__left__lang--active']"
- >中</text>
- <text class="u-keyboard__button__inner-wrapper__left__line">/</text>
- :class="[abc && 'u-keyboard__button__inner-wrapper__left__lang--active']"
- >英</text>
- v-for="(item, j) in group"
- :key="j"
- class="u-keyboard__button__inner-wrapper__inner"
- @tap="carInputClick(i, j)"
- <text class="u-keyboard__button__inner-wrapper__inner__text">{{ item }}</text>
- @touchstart="backspaceClick"
- @touchend="clearTimer"
- class="u-keyboard__button__inner-wrapper__right"
- size="28"
- name="backspace"
- color="#303133"
- * keyboard 键盘组件
- * @description 此为uView自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3种模式,都有可以打乱按键顺序的选项。
- * @tutorial https://uviewui.com/components/keyboard.html
- * @property {Boolean} random 是否打乱键盘的顺序
- * @event {Function} change 点击键盘触发
- * @event {Function} backspace 点击退格键触发
- * @example <u-keyboard ref="uKeyboard" mode="car" v-model="show"></u-keyboard>
- name: "u-keyboard",
- // 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
- abc: false
- };
- areaList() {
- let data = [
- '京',
- '沪',
- '粤',
- '津',
- '冀',
- '豫',
- '云',
- '辽',
- '黑',
- '湘',
- '皖',
- '鲁',
- '苏',
- '浙',
- '赣',
- '鄂',
- '桂',
- '甘',
- '晋',
- '陕',
- '蒙',
- '吉',
- '闽',
- '贵',
- '渝',
- '川',
- '青',
- '琼',
- '宁',
- '挂',
- '藏',
- '港',
- '澳',
- '新',
- '使',
- '学'
- ];
- let tmp = [];
- // 打乱顺序
- if (this.random) data = uni.$u.randomArray(data);
- // 切割成二维数组
- tmp[0] = data.slice(0, 10);
- tmp[1] = data.slice(10, 20);
- tmp[2] = data.slice(20, 30);
- tmp[3] = data.slice(30, 36);
- return tmp;
- engKeyBoardList() {
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9,
- 0,
- 'Q',
- 'W',
- 'E',
- 'R',
- 'T',
- 'Y',
- 'U',
- 'I',
- 'O',
- 'P',
- 'A',
- 'S',
- 'D',
- 'F',
- 'G',
- 'H',
- 'J',
- 'K',
- 'L',
- 'Z',
- 'X',
- 'C',
- 'V',
- 'B',
- 'N',
- 'M'
- // 点击键盘按钮
- carInputClick(i, j) {
- let value = '';
- // 不同模式,获取不同数组的值
- if (this.abc) value = this.engKeyBoardList[i][j];
- else value = this.areaList[i][j];
- // 如果允许自动切换,则将中文状态切换为英文
- if (!this.abc && this.autoChange) uni.$u.sleep(200).then(() => this.abc = true)
- this.$emit('change', value);
- // 修改汽车牌键盘的输入模式,中文|英文
- changeCarInputMode() {
- this.abc = !this.abc;
- // 点击退格键
- backspaceClick() {
- this.$emit('backspace');
- clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
- this.timer = null;
- this.timer = setInterval(() => {
- }, 250);
- clearTimer() {
- clearInterval(this.timer);
- $u-car-keyboard-background-color: rgb(224, 228, 230) !default;
- $u-car-keyboard-padding:6px 0 6px !default;
- $u-car-keyboard-button-inner-width:64rpx !default;
- $u-car-keyboard-button-inner-background-color:#FFFFFF !default;
- $u-car-keyboard-button-height:80rpx !default;
- $u-car-keyboard-button-inner-box-shadow:0 1px 0px #999992 !default;
- $u-car-keyboard-button-border-radius:4px !default;
- $u-car-keyboard-button-inner-margin:8rpx 5rpx !default;
- $u-car-keyboard-button-text-font-size:16px !default;
- $u-car-keyboard-button-text-color:$u-main-color !default;
- $u-car-keyboard-center-inner-margin: 0 4rpx !default;
- $u-car-keyboard-special-button-width:134rpx !default;
- $u-car-keyboard-lang-font-size:16px !default;
- $u-car-keyboard-lang-color:$u-main-color !default;
- $u-car-keyboard-active-color:$u-primary !default;
- $u-car-keyboard-line-font-size:15px !default;
- $u-car-keyboard-line-color:$u-main-color !default;
- $u-car-keyboard-line-margin:0 1px !default;
- $u-car-keyboard-u-hover-class-background-color:#BBBCC6 !default;
- .u-keyboard {
- justify-content: space-around;
- background-color: $u-car-keyboard-background-color;
- align-items: stretch;
- padding: $u-car-keyboard-padding;
- &__button {
- &__inner-wrapper {
- box-shadow: $u-car-keyboard-button-inner-box-shadow;
- margin: $u-car-keyboard-button-inner-margin;
- border-radius: $u-car-keyboard-button-border-radius;
- &__inner {
- width: $u-car-keyboard-button-inner-width;
- background-color: $u-car-keyboard-button-inner-background-color;
- height: $u-car-keyboard-button-height;
- font-size: $u-car-keyboard-button-text-font-size;
- color: $u-car-keyboard-button-text-color;
- &__left,
- &__right {
- width: $u-car-keyboard-special-button-width;
- background-color: $u-car-keyboard-u-hover-class-background-color;
- &__left {
- &__line {
- font-size: $u-car-keyboard-line-font-size;
- color: $u-car-keyboard-line-color;
- margin: $u-car-keyboard-line-margin;
- &__lang {
- font-size: $u-car-keyboard-lang-font-size;
- color: $u-car-keyboard-lang-color;
- color: $u-car-keyboard-active-color;
- .u-hover-class {
+ <view class="u-keyboard" @touchmove.stop.prevent="() => {}">
+ <view class="u-keyboard-grids">
+ <block>
+ <view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
+ <view :hover-stay-time="100" @tap="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
+ v-for="(item, j) in group" :key="j">
+ {{ item }}
+ <view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
+ hover-class="u-hover-class">
+ <u-icon :size="38" name="backspace" :bold="true"></u-icon>
+ <view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @tap="changeCarInputMode">
+ <text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
+ /
+ <text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
+ name: "u-keyboard",
+ // 是否打乱键盘按键的顺序
+ random: {
+ // 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
+ abc: false
+ areaList() {
+ let data = [
+ '京',
+ '沪',
+ '粤',
+ '津',
+ '冀',
+ '豫',
+ '云',
+ '辽',
+ '黑',
+ '湘',
+ '皖',
+ '鲁',
+ '苏',
+ '浙',
+ '赣',
+ '鄂',
+ '桂',
+ '甘',
+ '晋',
+ '陕',
+ '蒙',
+ '吉',
+ '闽',
+ '贵',
+ '渝',
+ '川',
+ '青',
+ '琼',
+ '宁',
+ '挂',
+ '藏',
+ '港',
+ '澳',
+ '新',
+ '使',
+ '学'
+ let tmp = [];
+ // 打乱顺序
+ if (this.random) data = this.$u.randomArray(data);
+ // 切割成二维数组
+ tmp[0] = data.slice(0, 10);
+ tmp[1] = data.slice(10, 20);
+ tmp[2] = data.slice(20, 30);
+ tmp[3] = data.slice(30, 36);
+ return tmp;
+ EngKeyBoardList() {
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 0,
+ 'Q',
+ 'W',
+ 'E',
+ 'R',
+ 'T',
+ 'Y',
+ 'U',
+ 'I',
+ 'O',
+ 'P',
+ 'A',
+ 'S',
+ 'D',
+ 'F',
+ 'G',
+ 'H',
+ 'J',
+ 'K',
+ 'L',
+ 'Z',
+ 'X',
+ 'C',
+ 'V',
+ 'B',
+ 'N',
+ 'M'
+ // 点击键盘按钮
+ carInputClick(i, j) {
+ let value = '';
+ // 不同模式,获取不同数组的值
+ if (this.abc) value = this.EngKeyBoardList[i][j];
+ else value = this.areaList[i][j];
+ this.$emit('change', value);
+ // 修改汽车牌键盘的输入模式,中文|英文
+ changeCarInputMode() {
+ this.abc = !this.abc;
+ // 点击退格键
+ backspaceClick() {
+ this.$emit('backspace');
+ clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+ this.timer = null;
+ this.timer = setInterval(() => {
+ }, 250);
+ clearTimer() {
+ clearInterval(this.timer);
+ .u-keyboard-grids {
+ background: rgb(215, 215, 217);
+ padding: 24rpx 0;
+ .u-keyboard-grids-item {
+ .u-keyboard-grids-btn {
+ text-decoration: none;
+ width: 62rpx;
+ flex: 0 0 64rpx;
+ font-size: 36rpx;
+ margin: 8rpx 5rpx;
+ box-shadow: 0 2rpx 0rpx #888992;
+ font-weight: 500;
+ .u-carinput-hover {
+ background-color: rgb(185, 188, 195) !important;
+ .u-keyboard-back {
+ width: 96rpx;
+ right: 22rpx;
+ bottom: 32rpx;
+ background-color: rgb(185, 188, 195);
+ .u-keyboard-change {
+ left: 22rpx;
+ .u-keyboard-change .inactive.zh {
+ transform: scale(0.85) translateY(-10rpx);
+ .u-keyboard-change .inactive.en {
+ transform: scale(0.85) translateY(10rpx);
+ .u-keyboard-change .active {
+ color: rgb(237, 112, 64);
+ .u-keyboard-change .zh {
+ transform: translateY(-10rpx);
+ .u-keyboard-change .en {
+ transform: translateY(10rpx);
@@ -0,0 +1,299 @@
+ class="u-card"
+ @tap.stop="click"
+ :class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
+ borderRadius: borderRadius + 'rpx',
+ margin: margin,
+ boxShadow: boxShadow
+ v-if="showHead"
+ class="u-card__head"
+ :style="[{padding: padding + 'rpx'}, headStyle]"
+ :class="{
+ 'u-border-bottom': headBorderBottom
+ @tap="headClick"
+ <view v-if="!$slots.head" class="u-flex u-row-between">
+ <view class="u-card__head--left u-flex u-line-1" v-if="title">
+ <image
+ :src="thumb"
+ class="u-card__head--left__thumb"
+ mode="aspectfull"
+ v-if="thumb"
+ height: thumbWidth + 'rpx',
+ width: thumbWidth + 'rpx',
+ borderRadius: thumbCircle ? '100rpx' : '6rpx'
+ ></image>
+ <text
+ class="u-card__head--left__title u-line-1"
+ fontSize: titleSize + 'rpx',
+ color: titleColor
+ {{ title }}
+ </text>
+ <view class="u-card__head--right u-line-1" v-if="subTitle">
+ class="u-card__head__title__text"
+ fontSize: subTitleSize + 'rpx',
+ color: subTitleColor
+ {{ subTitle }}
+ <slot name="head" v-else />
+ <view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
+ v-if="showFoot"
+ class="u-card__foot"
+ @tap="footClick"
+ :style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
+ 'u-border-top': footBorderTop
+ <slot name="foot" />
+ * card 卡片
+ * @description 卡片组件一般用于多个列表条目,且风格统一的场景
+ * @tutorial https://www.uviewui.com/components/card.html
+ * @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
+ * @property {String} title 头部左边的标题
+ * @property {String} title-color 标题颜色(默认#303133)
+ * @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
+ * @property {String} sub-title 头部右边的副标题
+ * @property {String} sub-title-color 副标题颜色(默认#909399)
+ * @property {String | Number} sub-title-size 副标题字体大小(默认26)
+ * @property {Boolean} border 是否显示边框(默认true)
+ * @property {String | Number} index 用于标识点击了第几个卡片
+ * @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
+ * @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx)
+ * @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16)
+ * @property {Object} head-style 头部自定义样式,对象形式
+ * @property {Object} body-style 中部自定义样式,对象形式
+ * @property {Object} foot-style 底部自定义样式,对象形式
+ * @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
+ * @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
+ * @property {Boolean} show-head 是否显示头部(默认true)
+ * @property {Boolean} show-head 是否显示尾部(默认true)
+ * @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
+ * @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60)
+ * @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
+ * @event {Function} click 整个卡片任意位置被点击时触发
+ * @event {Function} head-click 卡片头部被点击时触发
+ * @event {Function} body-click 卡片主体部分被点击时触发
+ * @event {Function} foot-click 卡片底部部分被点击时触发
+ * @example <u-card padding="30" title="card"></u-card>
+ name: 'u-card',
+ // 与屏幕两侧是否留空隙
+ full: {
+ // 标题
+ // 标题颜色
+ titleColor: {
+ // 标题字体大小,单位rpx
+ titleSize: {
+ default: '30'
+ // 副标题
+ subTitle: {
+ // 副标题颜色
+ subTitleColor: {
+ // 副标题字体大小,单位rpx
+ subTitleSize: {
+ default: '26'
+ // 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
+ border: {
+ // 用于标识点击了第几个
+ index: {
+ type: [Number, String, Object],
+ // 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx"
+ margin: {
+ default: '30rpx'
+ // card卡片的圆角
+ default: '16'
+ // 头部自定义样式,对象形式
+ headStyle: {
+ // 主体自定义样式,对象形式
+ bodyStyle: {
+ // 底部自定义样式,对象形式
+ footStyle: {
+ // 头部是否下边框
+ headBorderBottom: {
+ // 底部是否有上边框
+ footBorderTop: {
+ // 标题左边的缩略图
+ thumb: {
+ // 缩略图宽高,单位rpx
+ thumbWidth: {
+ default: '60'
+ // 缩略图是否为圆形
+ thumbCircle: {
+ // 给head,body,foot的内边距
+ padding: {
+ // 是否显示头部
+ showHead: {
+ // 是否显示尾部
+ showFoot: {
+ // 卡片外围阴影,字符串形式
+ boxShadow: {
+ default: 'none'
+ this.$emit('click', this.index);
+ headClick() {
+ this.$emit('head-click', this.index);
+ bodyClick() {
+ this.$emit('body-click', this.index);
+ footClick() {
+ this.$emit('foot-click', this.index);
+@import "../../libs/css/style.components.scss";
+.u-card {
+ &-full {
+ // 如果是与屏幕之间不留空隙,应该设置左右边距为0
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ &--border:after {
+ border-radius: 16rpx;
+ &__head {
+ &--left {
+ &__thumb {
+ &__title {
+ max-width: 400rpx;
+ &--right {
+ margin-left: 6rpx;
+ &__body {
+ &__foot {
- // 分组标题
- default: uni.$u.props.cellGroup.title
- // 是否显示外边框
- border: {
- default: uni.$u.props.cellGroup.border
@@ -1,61 +1,70 @@
- <view :style="[$u.addStyle(customStyle)]" :class="[customClass]" class="u-cell-group">
- <view v-if="title" class="u-cell-group__title">
- <slot name="title">
- <text class="u-cell-group__title__text">{{ title }}</text>
- <view class="u-cell-group__wrapper">
- <u-line v-if="border"></u-line>
+ <view class="u-cell-box">
+ <view class="u-cell-title" v-if="title" :style="[titleStyle]">
+ <view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
+ <slot />
- * cellGroup 单元格
- * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。
- * @tutorial https://uviewui.com/components/cell.html
- * @property {String} title 分组标题
- * @property {Boolean} border 是否显示外边框 (默认 true )
- * @event {Function} click 点击cell列表时触发
+ * cellGroup 单元格父组件Group
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
+ * @tutorial https://www.uviewui.com/components/cell.html
+ * @property {String} title 分组标题
+ * @property {Boolean} border 是否显示外边框(默认true)
+ * @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
* @example <u-cell-group title="设置喜好">
- name: 'u-cell-group',
+ name: "u-cell-group",
+ // 分组标题
+ // 是否显示分组list上下边框
+ // 分组标题的样式,对象形式,注意驼峰属性写法
+ // 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
+ index: 0,
- $u-cell-group-title-padding: 16px 16px 8px !default;
- $u-cell-group-title-font-size: 15px !default;
- $u-cell-group-title-line-height: 16px !default;
- $u-cell-group-title-color: $u-main-color !default;
+ .u-cell-box {
- .u-cell-group {
- padding: $u-cell-group-title-padding;
+ .u-cell-title {
+ padding: 30rpx 32rpx 10rpx 32rpx;
- font-size: $u-cell-group-title-font-size;
- line-height: $u-cell-group-title-line-height;
- color: $u-cell-group-title-color;
+ .u-cell-item-box {
+ background-color: #FFFFFF;
@@ -0,0 +1,316 @@
+ @tap="click"
+ class="u-cell"
+ :class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
+ hover-stay-time="150"
+ :hover-class="hoverClass"
+ <u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
+ <view class="u-flex" v-else>
+ <slot name="icon"></slot>
+ class="u-cell_title"
+ :style="[
+ width: titleWidth ? titleWidth + 'rpx' : 'auto'
+ titleStyle
+ <block v-if="title !== ''">{{ title }}</block>
+ <slot name="title" v-else></slot>
+ <view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
+ <block v-if="label !== ''">{{ label }}</block>
+ <slot name="label" v-else></slot>
+ <view class="u-cell__value" :style="[valueStyle]">
+ <block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
+ <slot v-else></slot>
+ <view class="u-flex u-cell_right" v-if="$slots['right-icon']">
+ <slot name="right-icon"></slot>
+ <u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
+ * cellItem 单元格Item
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
+ * @property {String} title 左侧标题
+ * @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标
+ * @property {Object} icon-style 左边图标的样式,对象形式
+ * @property {String} value 右侧内容
+ * @property {String} label 标题下方的描述信息
+ * @property {Boolean} border-bottom 是否显示cell的下边框(默认true)
+ * @property {Boolean} border-top 是否显示cell的上边框(默认false)
+ * @property {Boolean} center 是否使内容垂直居中(默认false)
+ * @property {String} hover-class 是否开启点击反馈,none为无效果(默认true)
+ * // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true)
+ * @property {Boolean} arrow 是否显示右侧箭头(默认true)
+ * @property {Boolean} required 箭头方向,可选值(默认right)
+ * @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false)
+ * @property {Object} title-style 标题样式,对象形式
+ * @property {Object} value-style 右侧内容样式,对象形式
+ * @property {Object} label-style 标题下方描述信息的样式,对象形式
+ * @property {String} bg-color 背景颜色(默认transparent)
+ * @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item
+ * @property {String Number} title-width 标题的宽度,单位rpx
+ * @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
+ name: 'u-cell-item',
+ // 左侧图标名称(只能uView内置图标),或者图标src
+ // 左侧标题
+ // 右侧内容
+ // 标题下方的描述信息
+ label: {
+ // 是否显示下边框
+ borderBottom: {
+ // 是否显示上边框
+ borderTop: {
+ // 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离
+ // 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰
+ // borderGap: {
+ // type: Boolean,
+ // default: true
+ // 是否开启点击反馈,即点击时cell背景为灰色,none为无效果
+ default: 'u-cell-hover'
+ // 是否显示右侧箭头
+ arrow: {
+ // 内容是否垂直居中
+ center: {
+ // 是否显示左边表示必填的星号
+ required: {
+ // 标题的宽度,单位rpx
+ titleWidth: {
+ // 右侧箭头方向,可选值:right|up|down,默认为right
+ arrowDirection: {
+ default: 'right'
+ // 控制标题的样式
+ // 右侧显示内容的样式
+ valueStyle: {
+ // 描述信息的样式
+ labelStyle: {
+ default: 'transparent'
+ // 用于识别被点击的是第几个cell
+ // 是否使用lable插槽
+ useLabelSlot: {
+ // 左边图标的大小,单位rpx,只对传入icon字段时有效
+ iconSize: {
+ default: 34
+ // 左边图标的样式,对象形式
+ arrowStyle() {
+ if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
+ else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
+ else style.transform = 'rotate(0deg)';
+.u-cell {
+ padding: 26rpx 32rpx;
+ line-height: 54rpx;
+.u-cell_title {
+.u-cell__left-icon-wrap {
+ margin-right: 10rpx;
+.u-cell__right-icon-wrap {
+ margin-left: 10rpx;
+ color: #969799;
+.u-cell__left-icon-wrap,
+ height: 48rpx;
+.u-cell-border:after {
+ border-bottom: 1px solid $u-border-color;
+ right: 0;
+ transform: scaleY(0.5);
+.u-cell-border {
+.u-cell__label {
+ margin-top: 6rpx;
+ line-height: 36rpx;
+ word-wrap: break-word;
+.u-cell__value {
+ vertical-align: middle;
+.u-cell__title,
+.u-cell--required {
+.u-cell--required:before {
+ content: '*';
+ left: 8px;
+ margin-top: 4rpx;
+ font-size: 14px;
+ color: $u-type-error;
+.u-cell_right {
@@ -1,110 +0,0 @@
- default: uni.$u.props.cell.title
- // 标题下方的描述信息
- label: {
- default: uni.$u.props.cell.label
- // 右侧的内容
- default: uni.$u.props.cell.value
- // 左侧图标名称,或者图片链接(本地文件建议使用绝对地址)
- default: uni.$u.props.cell.icon
- // 是否禁用cell
- default: uni.$u.props.cell.disabled
- // 是否显示下边框
- default: uni.$u.props.cell.border
- // 内容是否垂直居中(主要是针对右侧的value部分)
- default: uni.$u.props.cell.center
- // 点击后跳转的URL地址
- url: {
- default: uni.$u.props.cell.url
- // 链接跳转的方式,内部使用的是uView封装的route方法,可能会进行拦截操作
- linkType: {
- default: uni.$u.props.cell.linkType
- // 是否开启点击反馈(表现为点击时加上灰色背景)
- clickable: {
- default: uni.$u.props.cell.clickable
- // 是否展示右侧箭头并开启点击反馈
- isLink: {
- default: uni.$u.props.cell.isLink
- // 是否显示表单状态下的必填星号(此组件可能会内嵌入input组件)
- required: {
- default: uni.$u.props.cell.required
- // 右侧的图标箭头
- rightIcon: {
- default: uni.$u.props.cell.rightIcon
- // 右侧箭头的方向,可选值为:left,up,down
- arrowDirection: {
- default: uni.$u.props.cell.arrowDirection
- // 左侧图标样式
- type: [Object, String],
- default: () => {
- return uni.$u.props.cell.iconStyle
- // 右侧箭头图标的样式
- rightIconStyle: {
- return uni.$u.props.cell.rightIconStyle
- // 标题的样式
- titleStyle: {
- return uni.$u.props.cell.titleStyle
- // 单位元的大小,可选值为large
- default: uni.$u.props.cell.size
- // 点击cell是否阻止事件传播
- stop: {
- default: uni.$u.props.cell.stop
- // 标识符,cell被点击时返回
- default: uni.$u.props.cell.name
@@ -1,229 +0,0 @@
- <view class="u-cell" :class="[customClass]" :style="[$u.addStyle(customStyle)]"
- :hover-class="(!disabled && (clickable || isLink)) ? 'u-cell--clickable' : ''" :hover-stay-time="250"
- @tap="clickHandler">
- <view class="u-cell__body" :class="[ center && 'u-cell--center', size === 'large' && 'u-cell__body--large']">
- <view class="u-cell__body__content">
- <view class="u-cell__left-icon-wrap" v-if="$slots.icon || icon">
- <slot name="icon" v-if="$slots.icon">
- <u-icon v-else :name="icon" :custom-style="iconStyle" :size="size === 'large' ? 22 : 18"></u-icon>
- <view class="u-cell__title">
- <text v-if="title" class="u-cell__title-text" :style="[titleTextStyle]"
- :class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__title-text--large']">{{ title }}</text>
- <slot name="label">
- <text class="u-cell__label" v-if="label"
- :class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__label--large']">{{ label }}</text>
- <slot name="value">
- <text class="u-cell__value"
- :class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__value--large']"
- v-if="!$u.test.empty(value)">{{ value }}</text>
- <view class="u-cell__right-icon-wrap" v-if="$slots['right-icon'] || isLink"
- :class="[`u-cell__right-icon-wrap--${arrowDirection}`]">
- <slot name="right-icon" v-if="$slots['right-icon']">
- <u-icon v-else :name="rightIcon" :custom-style="rightIconStyle" :color="disabled ? '#c8c9cc' : 'info'"
- :size="size === 'large' ? 18 : 16"></u-icon>
- * cell 单元格
- * @property {String | Number} title 标题
- * @property {String | Number} label 标题下方的描述信息
- * @property {String | Number} value 右侧的内容
- * @property {String} icon 左侧图标名称,或者图片链接(本地文件建议使用绝对地址)
- * @property {Boolean} disabled 是否禁用cell
- * @property {Boolean} border 是否显示下边框 (默认 true )
- * @property {Boolean} center 内容是否垂直居中(主要是针对右侧的value部分) (默认 false )
- * @property {String} url 点击后跳转的URL地址
- * @property {String} linkType 链接跳转的方式,内部使用的是uView封装的route方法,可能会进行拦截操作 (默认 'navigateTo' )
- * @property {Boolean} clickable 是否开启点击反馈(表现为点击时加上灰色背景) (默认 false )
- * @property {Boolean} isLink 是否展示右侧箭头并开启点击反馈 (默认 false )
- * @property {Boolean} required 是否显示表单状态下的必填星号(此组件可能会内嵌入input组件) (默认 false )
- * @property {String} rightIcon 右侧的图标箭头 (默认 'arrow-right')
- * @property {String} arrowDirection 右侧箭头的方向,可选值为:left,up,down
- * @property {Object | String} rightIconStyle 右侧箭头图标的样式
- * @property {Object | String} titleStyle 标题的样式
- * @property {Object | String} iconStyle 左侧图标样式
- * @property {String} size 单位元的大小,可选值为 large,normal,mini
- * @property {Boolean} stop 点击cell是否阻止事件传播 (默认 true )
- * @example 该组件需要搭配cell-group组件使用,见官方文档示例
- name: 'u-cell',
- titleTextStyle() {
- return uni.$u.addStyle(this.titleStyle)
- // 点击cell
- clickHandler(e) {
- if (this.disabled) return
- this.$emit('click', {
- name: this.name
- // 如果配置了url(此props参数通过mixin引入)参数,跳转页面
- this.openPage()
- // 是否阻止事件传播
- this.stop && this.preventEvent(e)
- $u-cell-padding: 10px 15px !default;
- $u-cell-font-size: 15px !default;
- $u-cell-line-height: 24px !default;
- $u-cell-color: $u-main-color !default;
- $u-cell-icon-size: 16px !default;
- $u-cell-title-font-size: 15px !default;
- $u-cell-title-line-height: 22px !default;
- $u-cell-title-color: $u-main-color !default;
- $u-cell-label-font-size: 12px !default;
- $u-cell-label-color: $u-tips-color !default;
- $u-cell-label-line-height: 18px !default;
- $u-cell-value-font-size: 14px !default;
- $u-cell-value-color: $u-content-color !default;
- $u-cell-clickable-color: $u-bg-color !default;
- $u-cell-disabled-color: #c8c9cc !default;
- $u-cell-padding-top-large: 13px !default;
- $u-cell-padding-bottom-large: 13px !default;
- $u-cell-value-font-size-large: 15px !default;
- $u-cell-label-font-size-large: 14px !default;
- $u-cell-title-font-size-large: 16px !default;
- $u-cell-left-icon-wrap-margin-right: 4px !default;
- $u-cell-right-icon-wrap-margin-left: 4px !default;
- $u-cell-title-flex:1 !default;
- $u-cell-label-margin-top:5px !default;
- .u-cell {
- &__body {
- @include flex();
- padding: $u-cell-padding;
- font-size: $u-cell-font-size;
- color: $u-cell-color;
- // line-height: $u-cell-line-height;
- padding-top: $u-cell-padding-top-large;
- padding-bottom: $u-cell-padding-bottom-large;
- &__left-icon-wrap,
- &__right-icon-wrap {
- // height: $u-cell-line-height;
- font-size: $u-cell-icon-size;
- &__left-icon-wrap {
- margin-right: $u-cell-left-icon-wrap-margin-right;
- margin-left: $u-cell-right-icon-wrap-margin-left;
- transition: transform 0.3s;
- &--up {
- transform: rotate(-90deg);
- &--down {
- transform: rotate(90deg);
- flex: $u-cell-title-flex;
- &-text {
- font-size: $u-cell-title-font-size;
- line-height: $u-cell-title-line-height;
- color: $u-cell-title-color;
- font-size: $u-cell-title-font-size-large;
- &__label {
- margin-top: $u-cell-label-margin-top;
- font-size: $u-cell-label-font-size;
- color: $u-cell-label-color;
- line-height: $u-cell-label-line-height;
- font-size: $u-cell-label-font-size-large;
- &__value {
- text-align: right;
- font-size: $u-cell-value-font-size;
- line-height: $u-cell-line-height;
- color: $u-cell-value-color;
- font-size: $u-cell-value-font-size-large;
- &--clickable {
- background-color: $u-cell-clickable-color;
- color: $u-cell-disabled-color;
- cursor: not-allowed;
- &--center {
@@ -1,82 +0,0 @@
- // 标识符
- default: uni.$u.props.checkboxGroup.name
- // 绑定的值
- default: uni.$u.props.checkboxGroup.value
- // 形状,circle-圆形,square-方形
- default: uni.$u.props.checkboxGroup.shape
- // 是否禁用全部checkbox
- default: uni.$u.props.checkboxGroup.disabled
- // 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值
- activeColor: {
- default: uni.$u.props.checkboxGroup.activeColor
- // 未选中的颜色
- inactiveColor: {
- default: uni.$u.props.checkboxGroup.inactiveColor
- // 整个组件的尺寸,默认px
- default: uni.$u.props.checkboxGroup.size
- // 布局方式,row-横向,column-纵向
- placement: {
- default: uni.$u.props.checkboxGroup.placement
- // label的字体大小,px单位
- labelSize: {
- default: uni.$u.props.checkboxGroup.labelSize
- // label的字体颜色
- labelColor: {
- type: [String],
- default: uni.$u.props.checkboxGroup.labelColor
- // 是否禁止点击文本操作
- labelDisabled: {
- default: uni.$u.props.checkboxGroup.labelDisabled
- // 图标颜色
- default: uni.$u.props.checkboxGroup.iconColor
- // 图标的大小,单位px
- iconSize: {
- default: uni.$u.props.checkboxGroup.iconSize
- // 勾选图标的对齐方式,left-左边,right-右边
- iconPlacement: {
- default: uni.$u.props.checkboxGroup.iconPlacement
- // 竖向配列时,是否显示下划线
- borderBottom: {
- default: uni.$u.props.checkboxGroup.borderBottom
@@ -1,103 +1,123 @@
- class="u-checkbox-group"
+ <view class="u-checkbox-group u-clearfix">
<slot></slot>
+ import Emitter from '../../libs/util/emitter.js';
- * checkboxGroup 复选框组
+ * checkboxGroup 开关选择器父组件Group
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
* @tutorial https://www.uviewui.com/components/checkbox.html
- * @property {String} name 标识符
- * @property {Array} value 绑定的值
- * @property {String} shape 形状,circle-圆形,square-方形 (默认 'square' )
- * @property {Boolean} disabled 是否禁用全部checkbox (默认 false )
- * @property {String} activeColor 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值 (默认 '#2979ff' )
- * @property {String} inactiveColor 未选中的颜色 (默认 '#c8c9cc' )
- * @property {String | Number} size 整个组件的尺寸 单位px (默认 18 )
- * @property {String} placement 布局方式,row-横向,column-纵向 (默认 'row' )
- * @property {String | Number} labelSize label的字体大小,px单位 (默认 14 )
- * @property {String} labelColor label的字体颜色 (默认 '#303133' )
- * @property {Boolean} labelDisabled 是否禁止点击文本操作 (默认 false )
- * @property {String} iconColor 图标颜色 (默认 '#ffffff' )
- * @property {String | Number} iconSize 图标的大小,单位px (默认 12 )
- * @property {String} iconPlacement 勾选图标的对齐方式,left-左边,right-右边 (默认 'left' )
- * @property {Boolean} borderBottom placement为row时,是否显示下边框 (默认 false )
- * @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
- * @event {Function} input 修改通过v-model绑定的值时触发,回调为一个对象
+ * @property {String Number} max 最多能选中多少个checkbox(默认999)
+ * @property {String Number} size 组件整体的大小,单位rpx(默认40)
+ * @property {Boolean} disabled 是否禁用所有checkbox(默认false)
+ * @property {String Number} icon-size 图标大小,单位rpx(默认20)
+ * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
+ * @property {String} width 宽度,需带单位
+ * @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
+ * @property {Boolean} wrap 是否每个checkbox都换行(默认false)
+ * @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff)
+ * @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
* @example <u-checkbox-group></u-checkbox-group>
name: 'u-checkbox-group',
- // 这里computed的变量,都是子组件u-checkbox需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
- // 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(u-checkbox-group)
- // 拉取父组件新的变化后的参数
- parentData() {
- return [this.value, this.disabled, this.inactiveColor, this.activeColor, this.size, this.labelDisabled, this.shape,
- this.iconSize, this.borderBottom, this.placement
- ]
+ mixins: [Emitter],
+ // 最多能选中多少个checkbox
+ max: {
+ default: 999
- return this.bem('checkbox-group', ['placement'])
+ // 所有选中项的 name
+ // value: {
+ // default: Array,
+ // default() {
+ // return []
+ // 是否禁用所有复选框
- // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
- if (this.children.length) {
- this.children.map(child => {
- // 判断子组件(u-checkbox)如果有init方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
- typeof(child.init) === 'function' && child.init()
+ // 在表单内提交时的标识符
+ name: {
+ type: [Boolean, String],
+ // 是否禁止点击提示语选中复选框
+ labelDisabled: {
+ // 形状,square为方形,circle为原型
+ // 选中状态下的颜色
+ // 组件的整体大小
+ // 每个checkbox占u-checkbox-group的宽度
+ default: 'auto'
+ // 是否每个checkbox都换行
+ wrap: {
+ // 图标的大小,单位rpx
data() {
return {
created() {
+ // 如果将children定义在data中,在微信小程序会造成循环引用而报错
+ this.children = [];
- // 将其他的checkbox设置为未选中的状态
- unCheckedOther(childInstance) {
- const values = []
- // 将被选中的checkbox,放到数组中返回
- if (child.isChecked) {
- values.push(child.name)
+ emitEvent() {
+ let values = [];
+ this.children.map(val => {
+ if(val.value) values.push(val.name);
})
- // 发出事件
- this.$emit('change', values)
- // 修改通过v-model绑定的值
- this.$emit('input', values)
+ this.$emit('change', values);
+ // 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
+ // 由于头条小程序执行迟钝,故需要用几十毫秒的延时
+ setTimeout(() => {
+ // 将当前的值发送到 u-form-item 进行校验
+ this.dispatch('u-form-item', 'on-form-change', values);
+ }, 60)
.u-checkbox-group {
- &--row {
- &--column {
+ /* #ifndef MP || APP-NVUE */
@@ -1,69 +0,0 @@
- // checkbox的名称
- type: [String, Number, Boolean],
- default: uni.$u.props.checkbox.name
- // 形状,square为方形,circle为圆型
- default: uni.$u.props.checkbox.shape
- // 整体的大小
- default: uni.$u.props.checkbox.size
- // 是否默认选中
- checked: {
- default: uni.$u.props.checkbox.checked
- // 是否禁用
- type: [String, Boolean],
- default: uni.$u.props.checkbox.disabled
- default: uni.$u.props.checkbox.activeColor
- default: uni.$u.props.checkbox.inactiveColor
- default: uni.$u.props.checkbox.iconSize
- default: uni.$u.props.checkbox.iconColor
- // label提示文字,因为nvue下,直接slot进来的文字,由于特殊的结构,无法修改样式
- default: uni.$u.props.checkbox.label
- default: uni.$u.props.checkbox.labelSize
- // label的颜色
- default: uni.$u.props.checkbox.labelColor
- // 是否禁止点击提示语选中复选框
- default: uni.$u.props.checkbox.labelDisabled
@@ -1,343 +1,283 @@
- class="u-checkbox"
- :style="[checkboxStyle]"
- @tap.stop="wrapperClickHandler"
- :class="[`u-checkbox-label--${parentData.iconPlacement}`, parentData.borderBottom && parentData.placement === 'column' && 'u-border-bottom']"
- class="u-checkbox__icon-wrap"
- @tap.stop="iconClickHandler"
- :class="iconClasses"
- :style="[iconWrapStyle]"
- <slot name="icon">
- class="u-checkbox__icon-wrap__icon"
- name="checkbox-mark"
- :size="elIconSize"
- :color="elIconColor"
+ <view class="u-checkbox" :style="[checkboxStyle]">
+ <view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
+ <u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor"/>
+ <view class="u-checkbox__label" @tap="onClickLabel" :style="{
+ fontSize: $u.addUnit(labelSize)
- @tap.stop="labelClickHandler"
- color: elDisabled ? elInactiveColor : elLabelColor,
- fontSize: elLabelSize,
- lineHeight: elLabelSize
- >{{label}}</text>
- * checkbox 复选框
- * @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
- * @tutorial https://uviewui.com/components/checkbox.html
- * @property {String | Number | Boolean} name checkbox组件的标示符
- * @property {String} shape 形状,square为方形,circle为圆型
- * @property {String | Number} size 整体的大小
- * @property {Boolean} checked 是否默认选中
- * @property {String | Boolean} disabled 是否禁用
- * @property {String} activeColor 选中状态下的颜色,如设置此值,将会覆盖parent的activeColor值
- * @property {String} inactiveColor 未选中的颜色
- * @property {String | Number} iconSize 图标的大小,单位px
- * @property {String} iconColor 图标颜色
- * @property {String | Number} label label提示文字,因为nvue下,直接slot进来的文字,由于特殊的结构,无法修改样式
- * @property {String} labelColor label的颜色
- * @property {String | Number} labelSize label的字体大小,px单位
- * @property {String | Boolean} labelDisabled 是否禁止点击提示语选中复选框
+ * checkbox 复选框
+ * @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
+ * @tutorial https://www.uviewui.com/components/checkbox.html
+ * @property {String Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String Number} name checkbox组件的标示符
+ * @property {String} shape 形状,见官网说明(默认circle)
+ * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
+ * @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
+ * @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
name: "u-checkbox",
+ // checkbox的名称
+ // 是否为选中状态
+ // 是否禁用
+ type: [String, Boolean],
+ // 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值
+ // label的字体大小,rpx单位
+ labelSize: {
- isChecked: false,
- // 父组件的默认值,因为头条小程序不支持在computed中使用this.parent.shape的形式
- // 故只能使用如此方法
- parentData: {
- iconSize: 12,
- labelDisabled: null,
- disabled: null,
- shape: 'square',
- activeColor: null,
- inactiveColor: null,
- size: 18,
- value: null,
- iconColor: null,
- placement: 'row',
- borderBottom: false,
- iconPlacement: 'left'
+ parentDisabled: false,
+ newParams: {},
+ // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+ this.parent = this.$u.$parent.call(this, 'u-checkbox-group');
+ // 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
+ this.parent && this.parent.children.push(this);
- // 是否禁用,如果父组件u-raios-group禁用的话,将会忽略子组件的配置
- elDisabled() {
- return this.disabled !== '' ? this.disabled : this.parentData.disabled !== null ? this.parentData.disabled : false;
+ // 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
+ isDisabled() {
+ return this.disabled !== '' ? this.disabled : this.parent ? this.parent.disabled : false;
// 是否禁用label点击
- elLabelDisabled() {
- return this.labelDisabled !== '' ? this.labelDisabled : this.parentData.labelDisabled !== null ? this.parentData.labelDisabled :
- false;
+ isLabelDisabled() {
+ return this.labelDisabled !== '' ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
- // 组件尺寸,对应size的值,默认值为21px
- elSize() {
- return this.size ? this.size : (this.parentData.size ? this.parentData.size : 21);
+ // 组件尺寸,对应size的值,默认值为34rpx
+ checkboxSize() {
+ return this.size ? this.size : (this.parent ? this.parent.size : 34);
- // 组件的勾选图标的尺寸,默认12px
- elIconSize() {
- return this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 12);
+ // 组件的勾选图标的尺寸,默认20
+ checkboxIconSize() {
+ return this.iconSize ? this.iconSize : (this.parent ? this.parent.iconSize : 20);
// 组件选中激活时的颜色
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');
- elLabelColor() {
- return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : '#606266')
+ return this.activeColor ? this.activeColor : (this.parent ? this.parent.activeColor : 'primary');
// 组件的形状
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'))
+ return this.shape ? this.shape : (this.parent ? this.parent.shape : 'square');
- elIconColor() {
- const iconColor = this.iconColor ? this.iconColor : (this.parentData.iconColor ? this.parentData.iconColor :
- '#ffffff');
- // 图标的颜色
- if (this.elDisabled) {
- // disabled状态下,已勾选的checkbox图标改为elInactiveColor
- return this.isChecked ? this.elInactiveColor : 'transparent'
- return this.isChecked ? iconColor : 'transparent'
+ iconStyle() {
+ // 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中
+ if (this.elActiveColor && this.value && !this.isDisabled) {
+ style.borderColor = this.elActiveColor;
+ style.backgroundColor = this.elActiveColor;
+ style.width = this.$u.addUnit(this.checkboxSize);
+ style.height = this.$u.addUnit(this.checkboxSize);
- iconClasses() {
- let classes = []
- // 组件的形状
- classes.push('u-checkbox__icon-wrap--' + this.elShape)
- classes.push('u-checkbox__icon-wrap--disabled')
- if (this.isChecked && this.elDisabled) {
- classes.push('u-checkbox__icon-wrap--disabled--checked')
- // 支付宝,头条小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
- // #ifdef MP-ALIPAY || MP-TOUTIAO
- classes = classes.join(' ')
- return classes
+ // checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
+ iconColor() {
+ return this.value ? '#ffffff' : 'transparent';
- iconWrapStyle() {
- // checkbox的整体样式
- style.backgroundColor = this.isChecked && !this.elDisabled ? this.elActiveColor : '#ffffff'
- style.borderColor = this.isChecked && !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') {
+ iconClass() {
+ let classes = [];
+ classes.push('u-checkbox__icon-wrap--' + this.elShape);
+ if (this.value == true) classes.push('u-checkbox__icon-wrap--checked');
+ if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled');
+ if (this.value && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked');
+ // 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
+ return classes.join(' ');
checkboxStyle() {
- if (this.parentData.borderBottom && this.parentData.placement === 'row') {
- uni.$u.error('检测到您将borderBottom设置为true,需要同时将u-checkbox-group的placement设置为column才有效')
+ if(this.parent && this.parent.width) {
+ style.width = this.parent.width;
+ // #ifdef MP
+ // 各家小程序因为它们特殊的编译结构,使用float布局
+ style.float = 'left';
+ // #ifndef MP
+ // H5和APP使用flex布局
+ style.flex = `0 0 ${this.parent.width}`;
- // 当父组件设置了显示下边框并且排列形式为纵向时,给内容和边框之间加上一定间隔
- if (this.parentData.borderBottom && this.parentData.placement === 'column') {
- style.paddingBottom = '8px'
+ if(this.parent && this.parent.wrap) {
+ style.width = '100%';
+ // H5和APP使用flex布局,将宽度设置100%,即可自动换行
+ style.flex = '0 0 100%';
- // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
- this.updateParentData()
- if (!this.parent) {
- uni.$u.error('u-checkbox必须搭配u-checkbox-group组件使用')
+ onClickLabel() {
+ if (!this.isLabelDisabled && !this.isDisabled) {
+ this.setValue();
- // 设置初始化时,是否默认选中的状态,父组件u-checkbox-group的value可能是array,所以额外判断
- if (this.checked) {
- this.isChecked = true
- } else if (uni.$u.test.array(this.parentData.value)) {
- // 查找数组是是否存在this.name元素值
- this.isChecked = this.parentData.value.some(item => {
- return item === this.name
- updateParentData() {
- this.getParentData('u-checkbox-group')
- // 横向两端排列时,点击组件即可触发选中事件
- wrapperClickHandler(e) {
- this.parentData.iconPlacement === 'right' && this.iconClickHandler(e)
- // 点击图标
- iconClickHandler(e) {
- this.preventEvent(e)
- // 如果整体被禁用,不允许被点击
- if (!this.elDisabled) {
- this.setRadioCheckedStatus()
- // 点击label
- labelClickHandler(e) {
- // 如果按钮整体被禁用或者label被禁用,则不允许点击文字修改状态
- if (!this.elLabelDisabled && !this.elDisabled) {
+ toggle() {
+ if (!this.isDisabled) {
emitEvent() {
- this.$emit('change', this.isChecked)
- // 尝试调用u-form的验证方法,进行一定延迟,否则微信小程序更新可能会不及时
- uni.$u.formValidate(this, 'change')
+ value: !this.value,
+ name: this.name
+ // 执行父组件u-checkbox-group的事件方法
+ // 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
+ if(this.parent && this.parent.emitEvent) this.parent.emitEvent();
+ }, 80);
- // 改变组件选中状态
- // 这里的改变的依据是,更改本组件的checked值为true,同时通过父组件遍历所有u-checkbox实例
- // 将本组件外的其他u-checkbox的checked都设置为false(都被取消选中状态),因而只剩下一个为选中状态
- setRadioCheckedStatus() {
- // 将本组件标记为与原来相反的状态
- this.isChecked = !this.isChecked
- this.emitEvent()
- typeof this.parent.unCheckedOther === 'function' && this.parent.unCheckedOther(this)
- watch:{
- checked(){
- this.isChecked = this.checked
+ // 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值
+ setValue() {
+ // 判断是否超过了可选的最大数量
+ let checkedNum = 0;
+ if(this.parent && this.parent.children) {
+ // 只要父组件的某一个子元素的value为true,就加1(已有的选中数量)
+ this.parent.children.map(val => {
+ if (val.value) checkedNum++;
+ // 如果原来为选中状态,那么可以取消
+ if (this.value == true) {
+ this.emitEvent();
+ this.$emit('input', !this.value);
+ // 如果超出最多可选项,提示
+ if(this.parent && checkedNum >= this.parent.max) {
+ return this.$u.toast(`最多可选${this.parent.max}项`);
+ // 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
- $u-checkbox-icon-wrap-margin-right:6px !default;
- $u-checkbox-icon-wrap-font-size:6px !default;
- $u-checkbox-icon-wrap-border-width:1px !default;
- $u-checkbox-icon-wrap-border-color:#c8c9cc !default;
- $u-checkbox-icon-wrap-icon-line-height:0 !default;
- $u-checkbox-icon-wrap-circle-border-radius:100% !default;
- $u-checkbox-icon-wrap-square-border-radius:3px !default;
- $u-checkbox-icon-wrap-checked-color:#fff !default;
- $u-checkbox-icon-wrap-checked-background-color:red !default;
- $u-checkbox-icon-wrap-checked-border-color:#2979ff !default;
- $u-checkbox-icon-wrap-disabled-background-color:#ebedf0 !default;
- $u-checkbox-icon-wrap-disabled-checked-color:#c8c9cc !default;
- $u-checkbox-label-margin-left:5px !default;
- $u-checkbox-label-margin-right:12px !default;
- $u-checkbox-label-color:$u-content-color !default;
- $u-checkbox-label-font-size:15px !default;
- $u-checkbox-label-disabled-color:#c8c9cc !default;
.u-checkbox {
/* #ifndef APP-NVUE */
/* #endif */
- overflow: hidden;
align-items: center;
- &-label--left {
- flex-direction: row
- &-label--right {
- flex-direction: row-reverse;
- justify-content: space-between
+ line-height: 1.8;
&__icon-wrap {
- // nvue下,border-color过渡有问题
- transition-property: border-color, background-color, color;
- transition-duration: 0.2s;
color: $u-content-color;
+ flex: none;
+ display: -webkit-flex;
+ width: 42rpx;
+ height: 42rpx;
color: transparent;
- margin-right: $u-checkbox-icon-wrap-margin-right;
- font-size: $u-checkbox-icon-wrap-font-size;
- border-width: $u-checkbox-icon-wrap-border-width;
- border-color: $u-checkbox-icon-wrap-border-color;
- border-style: solid;
+ transition-property: color, border-color, background-color;
+ font-size: 20px;
+ border: 1px solid #c8c9cc;
+ transition-duration: 0.2s;
/* #ifdef MP-TOUTIAO */
// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
&__icon {
- line-height: $u-checkbox-icon-wrap-icon-line-height;
+ line-height: 0;
&--circle {
- border-radius: $u-checkbox-icon-wrap-circle-border-radius;
&--square {
- border-radius: $u-checkbox-icon-wrap-square-border-radius;
+ border-radius: 6rpx;
&--checked {
- color: $u-checkbox-icon-wrap-checked-color;
- background-color: $u-checkbox-icon-wrap-checked-background-color;
- border-color: $u-checkbox-icon-wrap-checked-border-color;
&--disabled {
- background-color: $u-checkbox-icon-wrap-disabled-background-color !important;
+ background-color: #ebedf0;
+ border-color: #c8c9cc;
&--disabled--checked {
- color: $u-checkbox-icon-wrap-disabled-checked-color !important;
+ color: #c8c9cc !important;
&__label {
word-wrap: break-word;
- margin-left: $u-checkbox-label-margin-left;
- margin-right: $u-checkbox-label-margin-right;
- color: $u-checkbox-label-color;
- font-size: $u-checkbox-label-font-size;
+ margin-right: 24rpx;
- color: $u-checkbox-label-disabled-color;
+ color: #c8c9cc;
@@ -1,8 +0,0 @@
- percentage: {
- default: uni.$u.props.circleProgress.percentage
@@ -1,198 +1,220 @@
- <view class="u-circle-progress">
- <view class="u-circle-progress__left">
- class="u-circle-progress__left__circle"
- :style="[leftSyle]"
- ref="left-circle"
- class="u-circle-progress__right"
- class="u-circle-progress__right__circle"
- ref="right-circle"
- :style="[rightSyle]"
- <view class="u-circle-progress__circle">
+ class="u-circle-progress"
+ width: widthPx + 'px',
+ height: widthPx + 'px',
+ <!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
+ class="u-canvas-bg"
+ :canvas-id="elBgId"
+ :id="elBgId"
+ height: widthPx + 'px'
+ class="u-canvas"
+ :canvas-id="elId"
+ :id="elId"
- const animation = uni.requireNativePlugin('animation')
- * CircleProgress 圆形进度条 TODO: 待完善
- * @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度环。
- * @tutorial https://www.uviewui.com/components/circleProgress.html
- * @property {String | Number} percentage 圆环进度百分比值,为数值类型,0-100 (默认 30 )
- * @example
- name: 'u-circle-progress',
- leftBorderColor: 'rgb(200, 200, 200)',
- rightBorderColor: 'rgb(200, 200, 200)',
+ * circleProgress 环形进度条
+ * @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
+ * @tutorial https://www.uviewui.com/components/circleProgress.html
+ * @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
+ * @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec)
+ * @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b)
+ * @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200)
+ * @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14)
+ * @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500)
+ * @property {String} type 如设置,active-color值将会失效
+ * @property {String} bg-color 整个组件背景颜色,默认为白色
+ * @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
+ name: 'u-circle-progress',
+ // 圆环进度百分比值
+ percent: {
+ default: 0,
+ // 限制值在0到100之间
+ validator: val => {
+ return val >= 0 && val <= 100;
- leftSyle() {
- style.borderTopColor = this.leftBorderColor
- style.borderRightColor = this.leftBorderColor
- rightSyle() {
- style.borderLeftColor = this.rightBorderColor
- style.borderBottomColor = this.rightBorderColor
+ // 底部圆环的颜色(灰色的圆环)
+ inactiveColor: {
+ default: '#ececec'
- uni.$u.sleep().then(() => {
- this.rightBorderColor = 'rgb(66, 185, 131)'
- // this.init()
+ // 圆环激活部分的颜色
+ default: '#19be6b'
- animation.transition(this.$refs['right-circle'].ref, {
- styles: {
- transform: 'rotate(45deg)',
- transformOrigin: 'center center'
- }, () => {
- // animation.transition(this.$refs['right-circle'].ref, {
- // styles: {
- // transform: 'rotate(225deg)',
- // transformOrigin: 'center center'
- // },
- // duration: 3000,
- // }, () => {
- // animation.transition(this.$refs['left-circle'].ref, {
- // transform: 'rotate(45deg)',
- // this.leftBorderColor = 'rgb(66, 185, 131)'
- // duration: 1500,
- // })
+ // 圆环线条的宽度,单位rpx
+ borderWidth: {
+ default: 14
+ // 整个圆形的宽度,单位rpx
+ // 整个圆环执行一圈的时间,单位ms
+ default: 1500
+ // 主题类型
+ // 整个圆环进度区域的背景色
+ // #ifdef MP-WEIXIN
+ elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错
+ elId: 'uCircleProgressElId',
+ // #ifndef MP-WEIXIN
+ elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
+ elId: this.$u.guid(),
+ widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
+ borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
+ startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
+ progressContext: null, // 活动圆的canvas上下文
+ newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+ oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+ percent(nVal, oVal = 0) {
+ if (nVal > 100) nVal = 100;
+ if (nVal < 0) oVal = 0;
+ // 此值其实等于this.percent,命名一个新
+ this.newPercent = nVal;
+ this.oldPercent = oVal;
+ // 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
+ // 将此值减少或者新增到新的百分比值
+ this.drawCircleByProgress(oVal);
+ }, 50);
+ // 赋值,用于加载后第一个画圆使用
+ this.newPercent = this.percent;
+ this.oldPercent = 0;
+ // 有type主题时,优先起作用
+ circleColor() {
+ if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
+ else return this.activeColor;
+ mounted() {
+ // 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
+ this.drawProgressBg();
+ this.drawCircleByProgress(this.oldPercent);
+ drawProgressBg() {
+ let ctx = uni.createCanvasContext(this.elBgId, this);
+ ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
+ ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
+ ctx.beginPath(); // 开始描绘路径
+ // 设置一个原点(110,110),半径为100的圆的路径到当前路径
+ let radius = this.widthPx / 2;
+ ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
+ ctx.stroke(); // 对路径进行描绘
+ ctx.draw();
+ drawCircleByProgress(progress) {
+ // 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
+ let ctx = this.progressContext;
+ if (!ctx) {
+ ctx = uni.createCanvasContext(this.elId, this);
+ this.progressContext = ctx;
+ // 表示进度的两端为圆形
+ ctx.setLineCap('round');
+ // 设置线条的宽度和颜色
+ ctx.setLineWidth(this.borderWidthPx);
+ ctx.setStrokeStyle(this.circleColor);
+ // 将总过渡时间除以100,得出每修改百分之一进度所需的时间
+ let time = Math.floor(this.duration / 100);
+ // 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
+ // 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
+ let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
+ ctx.beginPath();
+ // 半径为整个canvas宽度的一半
+ ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
+ ctx.stroke();
+ // 如果变更后新值大于旧值,意味着增大了百分比
+ if (this.newPercent > this.oldPercent) {
+ // 每次递增百分之一
+ progress++;
+ // 如果新增后的值,大于需要设置的值百分比值,停止继续增加
+ if (progress > this.newPercent) return;
+ // 同理于上面
+ progress--;
+ if (progress < this.newPercent) return;
+ // 定时器,每次操作间隔为time值,为了让进度条有动画效果
+ this.drawCircleByProgress(progress);
+ }, time);
+.u-circle-progress {
- .u-circle-progress {
- height: 100px;
- width: 100px;
- // transform: rotate(0deg);
- // background-color: rgb(66, 185, 131);
- background-color: rgb(200, 200, 200);
+.u-canvas-bg {
- &__circle {
- height: 90px;
- width: 90px;
- transform: translate(-50%, -50%);
- background-color: rgb(255, 255, 255);
- left: 50px;
- top: 50px;
- width: 50px;
- // background-color: rgb(200, 200, 200);
- // transform-origin: left center;
- // background-color: red;
- border-left-color: transparent;
- border-bottom-color: transparent;
- border-top-left-radius: 50px;
- border-top-right-radius: 50px;
- border-bottom-right-radius: 50px;
- // border-left-color: rgb(66, 185, 131);
- // border-bottom-color: rgb(66, 185, 131);
- border-top-color: rgb(66, 185, 131);
- border-right-color: rgb(66, 185, 131);
- border-width: 5px;
- transform: rotate(225deg);
- // border-radius: 100px;
- border-top-color: transparent;
- border-right-color: transparent;
- border-bottom-left-radius: 50px;
- border-left-color: rgb(200, 200, 200);
- border-bottom-color: rgb(200, 200, 200);
- transform: rotate(45deg);
- transform-origin: center center;
+.u-canvas {
@@ -0,0 +1,147 @@
+ <view class="u-progress" :style="{
+ borderRadius: round ? '100rpx' : 0,
+ height: height + 'rpx',
+ backgroundColor: inactiveColor
+ <view :class="[
+ type ? `u-type-${type}-bg` : '',
+ striped ? 'u-striped' : '',
+ striped && stripedActive ? 'u-striped-active' : ''
+ ]" class="u-active" :style="[progressStyle]">
+ <slot v-if="$slots.default || $slots.$default" />
+ <block v-else-if="showPercent">
+ {{percent + '%'}}
+ * lineProgress 线型进度条
+ * @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
+ * @tutorial https://www.uviewui.com/components/lineProgress.html
+ * @property {String Number} percent 进度条百分比值,为数值类型,0-100
+ * @property {Boolean} round 进度条两端是否为半圆(默认true)
+ * @property {String} active-color 进度条激活部分的颜色(默认#19be6b)
+ * @property {String} inactive-color 进度条的底色(默认#ececec)
+ * @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true)
+ * @property {String Number} height 进度条的高度,单位rpx(默认28)
+ * @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false)
+ * @property {Boolean} striped-active 条纹是否具有动态效果(默认false)
+ * @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
+ name: "u-line-progress",
+ // 两端是否显示半圆形
+ round: {
+ // 主题颜色
+ // 激活部分的颜色
+ // 进度百分比,数值
+ // 是否在进度条内部显示百分比的值
+ showPercent: {
+ // 进度条的高度,单位rpx
+ default: 28
+ // 是否显示条纹
+ striped: {
+ // 条纹是否显示活动状态
+ stripedActive: {
+ progressStyle() {
+ style.width = this.percent + '%';
+ if(this.activeColor) style.backgroundColor = this.activeColor;
+ .u-progress {
+ height: 15px;
+ .u-active {
+ width: 0;
+ justify-items: flex-end;
+ justify-content: space-around;
+ font-size: 20rpx;
+ transition: all 0.4s ease;
+ .u-striped {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 39px 39px;
+ .u-striped-active {
+ animation: progress-stripes 2s linear infinite;
+ @keyframes progress-stripes {
+ 0% {
+ background-position: 0 0;
+ 100% {
+ background-position: 39px 0;
@@ -1,79 +0,0 @@
- // 键盘弹起时,是否自动上推页面
- adjustPosition: {
- default: uni.$u.props.codeInput.adjustPosition
- // 最大输入长度
- maxlength: {
- default: uni.$u.props.codeInput.maxlength
- // 是否用圆点填充
- dot: {
- default: uni.$u.props.codeInput.dot
- // 显示模式,box-盒子模式,line-底部横线模式
- default: uni.$u.props.codeInput.mode
- default: uni.$u.props.codeInput.hairline
- // 字符间的距离
- default: uni.$u.props.codeInput.space
- // 预置值
- default: uni.$u.props.codeInput.value
- // 是否自动获取焦点
- focus: {
- default: uni.$u.props.codeInput.focus
- // 字体是否加粗
- bold: {
- default: uni.$u.props.codeInput.bold
- default: uni.$u.props.codeInput.color
- default: uni.$u.props.codeInput.fontSize
- // 输入框的大小,宽等于高
- default: uni.$u.props.codeInput.size
- // 是否隐藏原生键盘,如果想用自定义键盘的话,需设置此参数为true
- disabledKeyboard: {
- default: uni.$u.props.codeInput.disabledKeyboard
- // 边框和线条颜色
- borderColor: {
- default: uni.$u.props.codeInput.borderColor
- // 是否禁止输入"."符号
- disabledDot: {
- default: uni.$u.props.codeInput.disabledDot
@@ -1,252 +0,0 @@
- <view class="u-code-input">
- class="u-code-input__item"
- v-for="(item, index) in codeLength"
- class="u-code-input__item__dot"
- v-if="dot && codeArray.length > index"
- ></view>
- fontWeight: bold ? 'bold' : 'normal',
- color: color
- >{{codeArray[index]}}</text>
- class="u-code-input__item__line"
- v-if="mode === 'line'"
- :style="[lineStyle]"
- <!-- #ifndef APP-PLUS -->
- <view v-if="isFocus && codeArray.length === index" :style="{backgroundColor: color}" class="u-code-input__item__cursor"></view>
- <input
- :disabled="disabledKeyboard"
- type="number"
- :value="inputValue"
- class="u-code-input__input"
- @input="inputHandler"
- height: $u.addUnit(size)
- @focus="isFocus = true"
- @blur="isFocus = false"
- * CodeInput 验证码输入
- * @description 该组件一般用于验证用户短信验证码的场景,也可以结合uView的键盘组件使用
- * @tutorial https://www.uviewui.com/components/codeInput.html
- * @property {String | Number} maxlength 最大输入长度 (默认 6 )
- * @property {Boolean} dot 是否用圆点填充 (默认 false )
- * @property {String} mode 显示模式,box-盒子模式,line-底部横线模式 (默认 'box' )
- * @property {Boolean} hairline 是否细边框 (默认 false )
- * @property {String | Number} space 字符间的距离 (默认 10 )
- * @property {String | Number} value 预置值
- * @property {Boolean} focus 是否自动获取焦点 (默认 false )
- * @property {Boolean} bold 字体和输入横线是否加粗 (默认 false )
- * @property {String} color 字体颜色 (默认 '#606266' )
- * @property {String | Number} fontSize 字体大小,单位px (默认 18 )
- * @property {String | Number} size 输入框的大小,宽等于高 (默认 35 )
- * @property {Boolean} disabledKeyboard 是否隐藏原生键盘,如果想用自定义键盘的话,需设置此参数为true (默认 false )
- * @property {String} borderColor 边框和线条颜色 (默认 '#c9cacc' )
- * @property {Boolean} disabledDot 是否禁止输入"."符号 (默认 true )
- * @event {Function} change 输入内容发生改变时触发,具体见上方说明 value:当前输入的值
- * @event {Function} finish 输入字符个数达maxlength值时触发,见上方说明 value:当前输入的值
- * @example <u-code-input v-model="value4" :focus="true"></u-code-input>
- name: 'u-code-input',
- inputValue: '',
- isFocus: this.focus
- handler(val) {
- // 转为字符串,超出部分截掉
- this.inputValue = String(val).substring(0, this.maxlength)
- // 根据长度,循环输入框的个数,因为头条小程序数值不能用于v-for
- codeLength() {
- return new Array(Number(this.maxlength))
- // 循环item的样式
- itemStyle() {
- return index => {
- const addUnit = uni.$u.addUnit
- width: addUnit(this.size),
- height: addUnit(this.size)
- // 盒子模式下,需要额外进行处理
- if (this.mode === 'box') {
- // 设置盒子的边框,如果是细边框,则设置为0.5px宽度
- style.border = `${this.hairline ? 0.5 : 1}px solid ${this.borderColor}`
- // 如果盒子间距为0的话
- if (uni.$u.getPx(this.space) === 0) {
- // 给第一和最后一个盒子设置圆角
- if (index === 0) {
- if (index === this.codeLength.length - 1) {
- // 最后一个盒子的右边框需要保留
- if (index !== this.codeLength.length - 1) {
- style.borderRight = 'none'
- // 设置验证码字符之间的距离,通过margin-right设置,最后一个字符,无需右边框
- style.marginRight = addUnit(this.space)
- // 最后一个盒子的有边框需要保留
- // 将输入的值,转为数组,给item历遍时,根据当前的索引显示数组的元素
- codeArray() {
- return String(this.inputValue).split('')
- // 下划线模式下,横线的样式
- lineStyle() {
- style.height = this.hairline ? '2px' : '4px'
- style.width = uni.$u.addUnit(this.size)
- // 线条模式下,背景色即为边框颜色
- style.backgroundColor = this.borderColor
- // 监听输入框的值发生变化
- inputHandler(e) {
- const value = e.detail.value
- this.inputValue = value
- // 是否允许输入“.”符号
- if(this.disabledDot) {
- this.inputValue = value.replace('.', '')
- // 未达到maxlength之前,发送change事件,达到后发送finish事件
- this.$emit('change', value)
- // 修改通过v-model双向绑定的值
- this.$emit('input', value)
- // 达到用户指定输入长度时,发出完成事件
- if (String(value).length >= Number(this.maxlength)) {
- this.$emit('finish', value)
- $u-code-input-cursor-width: 1px;
- $u-code-input-cursor-height: 40%;
- $u-code-input-cursor-animation-duration: 1s;
- $u-code-input-cursor-animation-name: u-cursor-flicker;
- .u-code-input {
- font-size: 15px;
- background-color: $u-content-color;
- height: 4px;
- width: 40px;
- /* #ifndef APP-PLUS */
- &__cursor {
- top: 50%;
- left: 50%;
- transform: translate(-50%,-50%);
- width: $u-code-input-cursor-width;
- height: $u-code-input-cursor-height;
- animation: $u-code-input-cursor-animation-duration u-cursor-flicker infinite;
- &__input {
- // 之所以需要input输入框,是因为有它才能唤起键盘
- // 这里将它设置为两倍的屏幕宽度,再将左边的一半移出屏幕,为了不让用户看到输入的内容
- left: -750rpx;
- width: 1500rpx;
- background-color: transparent;
- text-align: left;
- @keyframes u-cursor-flicker {
- 0% {
- opacity: 0;
- 50% {
- opacity: 1;
- 100% {
@@ -1,34 +0,0 @@
- // 倒计时总秒数
- seconds: {
- default: uni.$u.props.code.seconds
- // 尚未开始时提示
- default: uni.$u.props.code.startText
- // 正在倒计时中的提示
- changeText: {
- default: uni.$u.props.code.changeText
- // 倒计时结束时的提示
- default: uni.$u.props.code.endText
- // 是否在H5刷新或各端返回再进入时继续倒计时
- keepRunning: {
- default: uni.$u.props.code.keepRunning
- // 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
- uniqueKey: {
- default: uni.$u.props.code.uniqueKey
@@ -1,29 +0,0 @@
- // 占父容器宽度的多少等分,总分为12份
- span: {
- default: uni.$u.props.col.span
- // 指定栅格左侧的间隔数(总12栏)
- default: uni.$u.props.col.offset
- // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
- justify: {
- default: uni.$u.props.col.justify
- // 垂直对齐方式,可选值为top、center、bottom、stretch
- align: {
- default: uni.$u.props.col.align
- // 文字对齐方式
- textAlign: {
- default: uni.$u.props.col.textAlign
@@ -1,111 +1,107 @@
- class="u-col"
- ref="u-col"
- :class="[
- 'u-col-' + span
- :style="[colStyle]"
+ <view class="u-col" :class="[
+ 'u-col-' + span
+ padding: `0 ${Number(gutter)/2 + 'rpx'}`,
+ marginLeft: 100 / 12 * offset + '%',
+ flex: `0 0 ${100 / 12 * span}%`,
+ alignItems: uAlignItem,
+ justifyContent: uJustify,
+ textAlign: textAlign
+ @tap="click">
- * CodeInput 栅格系统的列
- * @description 该组件一般用于Layout 布局 通过基础的 12 分栏,迅速简便地创建布局
- * @tutorial https://www.uviewui.com/components/Layout.html
- * @property {String | Number} span 栅格占据的列数,总12等份 (默认 12 )
- * @property {String | Number} offset 分栏左边偏移,计算方式与span相同 (默认 0 )
- * @property {String} justify 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`) (默认 'start' )
- * @property {String} align 垂直对齐方式,可选值为top、center、bottom、stretch (默认 'stretch' )
- * @property {String} textAlign 文字水平对齐方式 (默认 'left' )
- * @event {Function} click col被点击,会阻止事件冒泡到row
- * @example <u-col span="3" offset="3" > <view class="demo-layout bg-purple"></view> </u-col>
+ * col 布局单元格
+ * @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
+ * @tutorial https://www.uviewui.com/components/layout.html
+ * @property {String Number} span 栅格占据的列数,总12等分(默认0)
+ * @property {String} text-align 文字水平对齐方式(默认left)
+ * @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0)
+ * @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
- name: 'u-col',
+ name: "u-col",
+ // 占父容器宽度的多少等分,总分为12份
+ span: {
+ default: 12
+ // 指定栅格左侧的间隔数(总12栏)
+ // 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
+ justify: {
+ default: 'start'
+ // 垂直对齐方式,可选值为top、center、bottom
+ align: {
+ default: 'center'
+ // 文字对齐方式
+ textAlign: {
+ default: 'left'
+ // 是否阻止事件传播
+ stop: {
- gutter: 0
- gridNum: 12
+ gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取
+ this.parent = false;
+ // 获取父组件实例,并赋值给对应的参数
+ this.parent = this.$u.$parent.call(this, 'u-row');
+ if (this.parent) {
+ this.gutter = this.parent.gutter;
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
+ 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
- colStyle() {
- // 这里写成"padding: 0 10px"的形式是因为nvue的需要
- paddingLeft: uni.$u.addUnit(uni.$u.getPx(this.parentData.gutter)/2),
- paddingRight: uni.$u.addUnit(uni.$u.getPx(this.parentData.gutter)/2),
- alignItems: this.uAlignItem,
- justifyContent: this.uJustify,
- textAlign: this.textAlign,
- // 在非nvue上,使用百分比形式
- flex: `0 0 ${100 / this.gridNum * this.span}%`,
- marginLeft: 100 / 12 * this.offset + '%',
- // 在nvue上,由于无法使用百分比单位,这里需要获取父组件的宽度,再计算得出该有对应的百分比尺寸
- width: uni.$u.addUnit(Math.floor(this.width / this.gridNum * Number(this.span))),
- marginLeft: uni.$u.addUnit(Math.floor(this.width / this.gridNum * Number(this.offset))),
+ if (this.align == 'top') return 'flex-start';
+ if (this.align == 'bottom') return 'flex-end';
+ else return this.align;
- async init() {
- this.width = await this.parent.getComponentWidth()
- this.getParentData('u-row')
this.$emit('click');
.u-col {
- padding: 0;
- box-sizing:border-box;
- /* #ifdef MP */
- display: block;
+ /* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
+ float: left;
- // nvue下百分比无效
.u-col-0 {
width: 0;
@@ -157,6 +153,4 @@
.u-col-12 {
width: calc(100%/12 * 12);
- default: uni.$u.props.collapseItem.title
- // 标题右侧内容
- default: uni.$u.props.collapseItem.value
- default: uni.$u.props.collapseItem.label
- // 是否禁用折叠面板
- default: uni.$u.props.collapseItem.disabled
- default: uni.$u.props.collapseItem.isLink
- // 是否开启点击反馈
- default: uni.$u.props.collapseItem.clickable
- // 是否显示内边框
- default: uni.$u.props.collapseItem.border
- // 标题的对齐方式
- default: uni.$u.props.collapseItem.align
- // 唯一标识符
- default: uni.$u.props.collapseItem.name
- // 标题左侧图片,可为绝对路径的图片或内置图标
- default: uni.$u.props.collapseItem.icon
- // 面板展开收起的过渡时间,单位ms
- type: Number,
- default: uni.$u.props.collapseItem.duration
@@ -1,225 +1,205 @@
- <view class="u-collapse-item">
- <u-cell
- :label="label"
- :icon="icon"
- :isLink="isLink"
- :clickable="clickable"
- :border="parentData.border && showBorder"
- @click="clickHandler"
- :arrowDirection="expanded ? 'up' : 'down'"
- <!-- #ifndef MP-WEIXIN -->
- <!-- 微信小程序不支持,因为微信中不支持 <slot name="title" slot="title" />的写法 -->
- <template slot="title">
- <slot name="title"></slot>
- <template slot="icon">
- <slot name="icon"></slot>
- <template slot="value">
- <slot name="value"></slot>
- <template slot="right-icon">
- <slot name="right-icon"></slot>
- </u-cell>
- class="u-collapse-item__content"
- :animation="animationData"
- ref="animation"
- class="u-collapse-item__content__text content-class"
- :id="elId"
- :ref="elId"
- ><slot /></view>
- <u-line v-if="parentData.border"></u-line>
- * collapseItem 折叠面板Item
- * @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
- * @tutorial https://www.uviewui.com/components/collapse.html
- * @property {String} title 标题
- * @property {String} value 标题右侧内容
- * @property {String} label 标题下方的描述信息
- * @property {Boolean} disbled 是否禁用折叠面板 ( 默认 false )
- * @property {Boolean} isLink 是否展示右侧箭头并开启点击反馈 ( 默认 true )
- * @property {Boolean} clickable 是否开启点击反馈 ( 默认 true )
- * @property {Boolean} border 是否显示内边框 ( 默认 true )
- * @property {String} align 标题的对齐方式 ( 默认 'left' )
- * @property {String | Number} name 唯一标识符
- * @property {String} icon 标题左侧图片,可为绝对路径的图片或内置图标
- * @event {Function} change 某个item被打开或者收起时触发
- * @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
- name: "u-collapse-item",
- elId: uni.$u.guid(),
- // uni.createAnimation的导出数据
- animationData: {},
- // 是否展开状态
- expanded: false,
- // 根据expanded确定是否显示border,为了控制展开时,cell的下划线更好的显示效果,进行一定时间的延时
- showBorder: false,
- // 是否动画中,如果是则不允许继续触发点击
- animating: false,
- // 父组件u-collapse的参数
- accordion: false,
- border: false
- expanded(n) {
- clearTimeout(this.timer)
- this.timer = null
- // 这里根据expanded的值来进行一定的延时,是为了cell的下划线更好的显示效果
- this.timer = setTimeout(() => {
- this.showBorder = n
- }, n ? 10 : 290)
- // 异步获取内容,或者动态修改了内容时,需要重新初始化
- // 初始化数据
- return uni.$u.error('u-collapse-item必须要搭配u-collapse组件使用')
- value,
- accordion,
- children = []
- } = this.parent
- if (accordion) {
- if (uni.$u.test.array(value)) {
- return uni.$u.error('手风琴模式下,u-collapse组件的value参数不能为数组')
- this.expanded = this.name == value
- if (!uni.$u.test.array(value) && value !== null) {
- return uni.$u.error('非手风琴模式下,u-collapse组件的value参数必须为数组')
- this.expanded = (value || []).some(item => item == this.name)
- // 设置组件的展开或收起状态
- this.$nextTick(function() {
- this.setContentAnimate()
- // 此方法在mixin中
- this.getParentData('u-collapse')
- async setContentAnimate() {
- // 每次面板打开或者收起时,都查询元素尺寸
- // 好处是,父组件从服务端获取内容后,变更折叠面板后可以获得最新的高度
- const rect = await this.queryRect()
- const height = this.expanded ? rect.height : 0
- this.animating = true
- const ref = this.$refs['animation'].ref
- animation.transition(ref, {
- height: height + 'px'
- duration: this.duration,
- // 必须设置为true,否则会到面板收起或展开时,页面其他元素不会随之调整它们的布局
- needLayout: true,
- timingFunction: 'ease-in-out',
- this.animating = false
- const animation = uni.createAnimation({
- });
- animation
- .height(height)
- .step({
- .step()
- // 导出动画数据给面板的animationData值
- this.animationData = animation.export()
- // 标识动画结束
- uni.$u.sleep(this.duration).then(() => {
- // 点击collapsehead头部
- if (this.disabled && this.animating) return
- // 设置本组件为相反的状态
- this.parent && this.parent.onChange(this)
- // 查询内容高度
- queryRect() {
- this.$uGetRect(`#${this.elId}`).then(size => {
- dom.getComponentRect(this.$refs[this.elId], res => {
- .u-collapse-item {
- height: 0;
- padding: 12px 15px;
- line-height: 18px;
+ <view class="u-collapse-item" :style="[itemStyle]">
+ <view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
+ <block v-if="!$slots['title-all']">
+ <view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
+ isShow && activeStyle && !arrow ? activeStyle : '']">
+ <slot v-else name="title" />
+ <u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
+ class="u-arrow-down-icon" name="arrow-down"></u-icon>
+ <slot v-else name="title-all" />
+ <view class="u-collapse-body" :style="[{
+ height: isShow ? height + 'px' : '0'
+ }]">
+ <view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
+ * collapseItem 手风琴Item
+ * @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
+ * @tutorial https://www.uviewui.com/components/collapse.html
+ * @property {String} title 面板标题
+ * @property {String Number} index 主要用于事件的回调,标识那个Item被点击
+ * @property {Boolean} disabled 面板是否可以打开或收起(默认false)
+ * @property {Boolean} open 设置某个面板的初始状态是否打开(默认false)
+ * @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值
+ * @property {String} align 标题的对齐方式(默认left)
+ * @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式
+ * @event {Function} change 某个item被打开或者收起时触发
+ * @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
+ name: "u-collapse-item",
+ // 标题的对齐方式
+ // 是否可以点击收起
+ // collapse显示与否
+ open: {
+ // 唯一标识符
+ //活动样式
+ activeStyle: {
+ // 标识当前为第几个
+ isShow: false,
+ height: 0, // body内容的高度
+ headStyle: {}, // 头部样式,对象形式
+ bodyStyle: {}, // 主体部分样式
+ itemStyle: {}, // 每个item的整体样式
+ arrowColor: '', // 箭头的颜色
+ hoverClass: '', // 头部按下时的效果样式类
+ arrow: true, // 是否显示右侧箭头
+ open(val) {
+ this.isShow = val;
+ // 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍
+ this.isShow = this.open;
+ // 异步获取内容,或者动态修改了内容时,需要重新初始化
+ this.parent = this.$u.$parent.call(this, 'u-collapse');
+ if(this.parent) {
+ this.nameSync = this.name ? this.name : this.parent.childrens.length;
+ // 不存在时才添加本实例
+ !this.parent.childrens.includes(this) && this.parent.childrens.push(this);
+ this.headStyle = this.parent.headStyle;
+ this.bodyStyle = this.parent.bodyStyle;
+ this.arrowColor = this.parent.arrowColor;
+ this.hoverClass = this.parent.hoverClass;
+ this.arrow = this.parent.arrow;
+ this.itemStyle = this.parent.itemStyle;
+ this.queryRect();
+ // 点击collapsehead头部
+ if (this.disabled) return;
+ if (this.parent && this.parent.accordion == true) {
+ this.parent.childrens.map(val => {
+ // 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了
+ if (this != val) {
+ val.isShow = false;
+ this.isShow = !this.isShow;
+ // 触发本组件的事件
+ index: this.index,
+ show: this.isShow
+ // 只有在打开时才发出事件
+ if (this.isShow) this.parent && this.parent.onChange();
+ this.$forceUpdate();
+ // 查询内容高度
+ queryRect() {
+ // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
+ // 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
+ this.$uGetRect('#' + this.elId).then(res => {
+ this.height = res.height;
+ this.init();
+ .u-collapse-head {
+ .u-collapse-title {
+ .u-arrow-down-icon {
+ transition: all 0.3s;
+ margin-right: 20rpx;
+ margin-left: 14rpx;
+ .u-arrow-down-icon-active {
+ transform: rotate(180deg);
+ .u-collapse-body {
+ .u-collapse-content {
@@ -1,19 +0,0 @@
- // 当前展开面板的name,非手风琴模式:[<string | number>],手风琴模式:string | number
- type: [String, Number, Array, null],
- default: uni.$u.props.collapse.value
- // 是否手风琴模式
- accordion: {
- default: uni.$u.props.collapse.accordion
- default: uni.$u.props.collapse.border
@@ -1,90 +1,99 @@
<view class="u-collapse">
<slot />
- * collapse 折叠面板
+ * collapse 手风琴
* @description 通过折叠面板收纳内容区域
* @tutorial https://www.uviewui.com/components/collapse.html
- * @property {String | Number | Array} value 当前展开面板的name,非手风琴模式:[<string | number>],手风琴模式:string | number
- * @property {Boolean} accordion 是否手风琴模式( 默认 false )
- * @property {Boolean} border 是否显示外边框 ( 默认 true )
- * @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
+ * @property {Boolean} accordion 是否手风琴模式(默认true)
+ * @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
+ * @property {String} arrow-color 标题右侧箭头的颜色(默认#909399)
+ * @property {Object} head-style 标题自定义样式,对象形式
+ * @property {Object} body-style 主体自定义样式,对象形式
+ * @property {String} hover-class 样式类名,按下时有效(默认u-hover-class)
+ * @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
* @example <u-collapse></u-collapse>
- name: "u-collapse",
- needInit() {
+ name:"u-collapse",
+ // 是否手风琴模式
+ accordion: {
+ // 头部的样式
+ // 主体的样式
+ // 每一个item的样式
+ itemStyle: {
+ // 是否显示右侧的箭头
+ // 箭头的颜色
+ arrowColor: {
+ // 标题部分按压时的样式类,"none"为无效果
+ default: 'u-hover-class'
+ this.childrens = []
- // 通过computed,同时监听accordion和value值的变化
- // 再通过watch去执行init()方法,进行再一次的初始化
- return [this.accordion, this.value]
- // 判断子组件(u-checkbox)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
- typeof(child.updateParentData) === 'function' && child.updateParentData()
- // 重新初始化一次内部的所有子元素
+ // 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
init() {
- child.init()
+ this.childrens.forEach((vm, index) => {
+ vm.init();
- * collapse-item被点击时触发,由collapse统一处理各子组件的状态
- * @param {Object} target 被操作的面板的实例
- onChange(target) {
- let changeArr = []
- this.children.map((child, index) => {
- // 如果是手风琴模式,将其他的折叠面板收起来
- if (this.accordion) {
- child.expanded = child === target ? !target.expanded : false
- child.setContentAnimate()
- if(child === target) {
- child.expanded = !child.expanded
+ // collapse item被点击,由collapse item调用父组件方法
+ onChange() {
+ let activeItem = [];
+ if (vm.isShow) {
+ activeItem.push(vm.nameSync);
- // 拼接change事件中,数组元素的状态
- changeArr.push({
- // 如果没有定义name属性,则默认返回组件的index索引
- name: child.name || index,
- status: child.expanded ? 'open' : 'close'
- this.$emit('change', changeArr)
- this.$emit(target.expanded ? 'open' : 'close', target.name)
+ // 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串
+ if (this.accordion) activeItem = activeItem.join('');
+ this.$emit('change', activeItem);
@@ -1,55 +0,0 @@
- // 显示的内容,字符串
- type: [Array],
- default: uni.$u.props.columnNotice.text
- // 是否显示左侧的音量图标
- default: uni.$u.props.columnNotice.icon
- // 通告模式,link-显示右箭头,closable-显示右侧关闭图标
- default: uni.$u.props.columnNotice.mode
- // 文字颜色,各图标也会使用文字颜色
- default: uni.$u.props.columnNotice.color
- // 背景颜色
- default: uni.$u.props.columnNotice.bgColor
- // 字体大小,单位px
- default: uni.$u.props.columnNotice.fontSize
- // 水平滚动时的滚动速度,即每秒滚动多少px(px),这有利于控制文字无论多少时,都能有一个恒定的速度
- speed: {
- default: uni.$u.props.columnNotice.speed
- // direction = row时,是否使用步进形式滚动
- step: {
- default: uni.$u.props.columnNotice.step
- // 滚动一个周期的时间长,单位ms
- default: uni.$u.props.columnNotice.duration
- // 是否禁止用手滑动切换
- // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
- disableTouch: {
- default: uni.$u.props.columnNotice.disableTouch
@@ -1,160 +1,237 @@
<view
- class="u-notice"
+ class="u-notice-bar"
+ background: computeBgColor,
+ padding: padding
+ type ? `u-type-${type}-light-bg` : ''
- class="u-notice__left-icon"
- size="19"
- <swiper
- :disable-touch="disableTouch"
- :vertical="step ? false : true"
- circular
- :interval="duration"
- :autoplay="true"
- class="u-notice__swiper"
- @change="noticeChange"
- <swiper-item
- v-for="(item, index) in text"
- class="u-notice__swiper__item"
- class="u-notice__swiper__item__text u-line-1"
+ <u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
+ <swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
+ <swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
+ class="u-news-item u-line-1"
:style="[textStyle]"
- >{{ item }}</text>
+ @tap="click(index)"
+ :class="['u-type-' + type]"
</swiper-item>
</swiper>
- class="u-notice__right-icon"
- v-if="['link', 'closable'].includes(mode)"
- v-if="mode === 'link'"
- name="arrow-right"
- :size="17"
- v-if="mode === 'closable'"
- :size="16"
- @click="close"
+ <u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
+ <u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
- * ColumnNotice 滚动通知中的垂直滚动 内部组件
- * @description 该组件用于滚动通告场景,是其中的垂直滚动方式
- * @tutorial https://www.uviewui.com/components/noticeBar.html
- * @property {Array} 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 )
- * @property {Boolean} step direction = row时,是否使用步进形式滚动 ( 默认 false )
- * @property {String | Number} duration 滚动一个周期的时间长,单位ms ( 默认 1500 )
- * @property {Boolean} disableTouch 是否禁止用手滑动切换 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 ( 默认 true )
- handler(newValue, oldValue) {
- if(!uni.$u.test.array(newValue)) {
- uni.$u.error('noticebar组件direction为column时,要求text参数为数组形式')
+ // 显示的内容,数组
- // 文字内容的样式
- let style = {}
- style.fontSize = uni.$u.addUnit(this.fontSize)
- // 垂直或者水平滚动
- vertical() {
- if (this.mode == 'horizontal') return false
- else return true
- index:0
+ // 显示的主题,success|error|primary|info|warning
+ // 是否显示左侧的音量图标
+ volumeIcon: {
+ // 是否显示右侧的右箭头图标
+ moreIcon: {
+ // 是否显示右侧的关闭图标
+ closeIcon: {
+ // 是否自动播放
+ autoplay: {
+ // 文字颜色,各图标也会使用文字颜色
+ // 滚动方向,row-水平滚动,column-垂直滚动
+ direction: {
+ default: 'row'
+ // 字体大小,单位rpx
+ default: 26
+ // 滚动一个周期的时间长,单位ms
+ default: 2000
+ // 音量喇叭的大小
+ volumeSize: {
+ // 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
+ speed: {
+ default: 160
+ // 水平滚动时,是否采用衔接形式滚动
+ isCircular: {
+ // 滚动方向,horizontal-水平滚动,vertical-垂直滚动
+ default: 'horizontal'
- noticeChange(e){
- this.index = e.detail.current
- // 点击通告栏
- this.$emit('click', this.index)
+ // 播放状态,play-播放,paused-暂停
+ playState: {
+ default: 'play'
+ // 是否禁止用手滑动切换
+ // 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
+ disableTouch: {
+ // 通知的边距
+ default: '18rpx 24rpx'
+ // 计算字体颜色,如果没有自定义的,就用uview主题颜色
+ computeColor() {
+ if (this.color) return this.color;
+ // 如果是无主题,就默认使用content-color
+ else if(this.type == 'none') return '#606266';
+ else return this.type;
+ // 文字内容的样式
+ textStyle() {
+ if (this.color) style.color = this.color;
+ else if(this.type == 'none') style.color = '#606266';
+ style.fontSize = this.fontSize + 'rpx';
+ // 垂直或者水平滚动
+ vertical() {
+ if(this.mode == 'horizontal') return false;
+ // 计算背景颜色
+ computeBgColor() {
+ if (this.bgColor) return this.bgColor;
+ else if(this.type == 'none') return 'transparent';
+ // animation: false
+ // 点击通告栏
+ click(index) {
+ // 点击更多箭头按钮
+ getMore() {
+ this.$emit('getMore');
+ change(e) {
+ let index = e.detail.current;
+ if(index == this.list.length - 1) {
+ this.$emit('end');
- .u-notice {
+.u-notice-bar {
+ flex-wrap: nowrap;
+ padding: 18rpx 24rpx;
- &__left-icon {
+.u-swiper {
+ height: 32rpx;
+ margin-left: 12rpx;
- &__right-icon {
- margin-left: 5px;
+.u-swiper-item {
- &__swiper {
- height: 16px;
+.u-news-item {
+.u-right-icon {
+.u-left-icon {
@@ -1,24 +0,0 @@
- // 倒计时时长,单位ms
- time: {
- default: uni.$u.props.countDown.time
- // 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒
- format: {
- default: uni.$u.props.countDown.format
- // 是否自动开始倒计时
- autoStart: {
- default: uni.$u.props.countDown.autoStart
- // 是否展示毫秒倒计时
- millisecond: {
- default: uni.$u.props.countDown.millisecond
@@ -1,163 +1,318 @@
- <view class="u-count-down">
- <text class="u-count-down__text">{{ formattedTime }}</text>
- import {
- isSameSecond,
- parseFormat,
- parseTimeData
- } from './utils';
- * u-count-down 倒计时
- * @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
- * @tutorial https://uviewui.com/components/countDown.html
- * @property {String | Number} time 倒计时时长,单位ms (默认 0 )
- * @property {String} format 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 (默认 'HH:mm:ss' )
- * @property {Boolean} autoStart 是否自动开始倒计时 (默认 true )
- * @property {Boolean} millisecond 是否展示毫秒倒计时 (默认 false )
- * @event {Function} finish 倒计时结束时触发
- * @event {Function} change 倒计时变化时触发
- * @event {Function} start 开始倒计时
- * @event {Function} pause 暂停倒计时
- * @event {Function} reset 重设倒计时,若 auto-start 为 true,重设后会自动开始倒计时
- * @example <u-count-down :time="time"></u-count-down>
- name: 'u-count-down',
- timer: null,
- // 各单位(天,时,分等)剩余时间
- timeData: parseTimeData(0),
- // 格式化后的时间,如"03:23:21"
- formattedTime: '0',
- // 倒计时是否正在进行中
- runing: false,
- endTime: 0, // 结束的毫秒时间戳
- remainTime: 0, // 剩余的毫秒时间
- time(n) {
- this.reset()
- // 开始倒计时
- start() {
- if (this.runing) return
- // 标识为进行中
- this.runing = true
- // 结束时间戳 = 此刻时间戳 + 剩余的时间
- this.endTime = Date.now() + this.remainTime
- this.toTick()
- // 根据是否展示毫秒,执行不同操作函数
- toTick() {
- if (this.millisecond) {
- this.microTick()
- this.macroTick()
- macroTick() {
- this.clearTimeout()
- // 每隔一定时间,更新一遍定时器的值
- // 同时此定时器的作用也能带来毫秒级的更新
- // 获取剩余时间
- const remain = this.getRemainTime()
- // 重设剩余时间
- if (!isSameSecond(remain, this.remainTime) || remain === 0) {
- this.setRemainTime(remain)
- // 如果剩余时间不为0,则继续检查更新倒计时
- if (this.remainTime !== 0) {
- }, 30)
- microTick() {
- this.setRemainTime(this.getRemainTime())
- }, 50)
- // 获取剩余的时间
- getRemainTime() {
- // 取最大值,防止出现小于0的剩余时间值
- return Math.max(this.endTime - Date.now(), 0)
- // 设置剩余的时间
- setRemainTime(remain) {
- this.remainTime = remain
- // 根据剩余的毫秒时间,得出该有天,小时,分钟等的值,返回一个对象
- const timeData = parseTimeData(remain)
- this.$emit('change', timeData)
- // 得出格式化后的时间
- this.formattedTime = parseFormat(this.format, timeData)
- // 如果时间已到,停止倒计时
- if (remain <= 0) {
- this.pause()
- this.$emit('finish')
- // 重置倒计时
- reset() {
- this.remainTime = this.time
- this.setRemainTime(this.remainTime)
- if (this.autoStart) {
- this.start()
- // 暂停倒计时
- pause() {
- this.runing = false;
- // 清空定时器
- clearTimeout() {
- beforeDestroy() {
-<style
- lang="scss"
- scoped
->
- $u-count-down-text-color:$u-content-color !default;
- $u-count-down-text-font-size:15px !default;
- $u-count-down-text-line-height:22px !default;
- .u-count-down {
- color: $u-count-down-text-color;
- font-size: $u-count-down-text-font-size;
- line-height: $u-count-down-text-line-height;
+ <view class="u-countdown">
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))">
+ <view class="u-countdown-time" :style="[letterStyle]">
+ {{ d }}
+ class="u-countdown-colon"
+ :style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+ v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
+ {{ separator == 'colon' ? ':' : '天' }}
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showHours">
+ <view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
+ {{ h }}
+ v-if="showHours"
+ {{ separator == 'colon' ? ':' : '时' }}
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showMinutes">
+ {{ i }}
+ v-if="showMinutes"
+ {{ separator == 'colon' ? ':' : '分' }}
+ <view class="u-countdown-item" :style="[itemStyle]" v-if="showSeconds">
+ {{ s }}
+ v-if="showSeconds && separator == 'zh'"
+ 秒
+ * countDown 倒计时
+ * @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
+ * @tutorial https://www.uviewui.com/components/countDown.html
+ * @property {String Number} timestamp 倒计时,单位为秒
+ * @property {Boolean} autoplay 是否自动开始倒计时,如果为false,需手动调用开始方法。见官网说明(默认true)
+ * @property {String} separator 分隔符,colon为英文冒号,zh为中文(默认colon)
+ * @property {String Number} separator-size 分隔符的字体大小,单位rpx(默认30)
+ * @property {String} separator-color 分隔符的颜色(默认#303133)
+ * @property {String Number} font-size 倒计时字体大小,单位rpx(默认30)
+ * @property {Boolean} show-border 是否显示倒计时数字的边框(默认false)
+ * @property {Boolean} hide-zero-day 当"天"的部分为0时,隐藏该字段 (默认true)
+ * @property {String} border-color 数字边框的颜色(默认#303133)
+ * @property {String} bg-color 倒计时数字的背景颜色(默认#ffffff)
+ * @property {String} color 倒计时数字的颜色(默认#303133)
+ * @property {String} height 数字高度值(宽度等同此值),设置边框时看情况是否需要设置此值,单位rpx(默认auto)
+ * @property {Boolean} show-days 是否显示倒计时的"天"部分(默认true)
+ * @property {Boolean} show-hours 是否显示倒计时的"时"部分(默认true)
+ * @property {Boolean} show-minutes 是否显示倒计时的"分"部分(默认true)
+ * @property {Boolean} show-seconds 是否显示倒计时的"秒"部分(默认true)
+ * @event {Function} end 倒计时结束
+ * @event {Function} change 每秒触发一次,回调为当前剩余的倒计秒数
+ * @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down>
+ name: 'u-count-down',
+ // 倒计时的时间,秒为单位
+ timestamp: {
+ // 是否自动开始倒计时
+ // 用英文冒号(colon)或者中文(zh)当做分隔符,false的时候为中文,如:"11:22"或"11时22秒"
+ separator: {
+ default: 'colon'
+ // 分隔符的大小,单位rpx
+ separatorSize: {
+ default: 30
+ // 分隔符颜色
+ separatorColor: {
+ default: "#303133"
+ // 字体颜色
+ default: '#fff'
+ // 数字框高度,单位rpx
+ // 是否显示数字框
+ showBorder: {
+ // 是否显示秒
+ showSeconds: {
+ // 是否显示分钟
+ showMinutes: {
+ // 是否显示小时
+ showHours: {
+ // 是否显示“天”
+ showDays: {
+ // 当"天"的部分为0时,不显示
+ hideZeroDay: {
+ // 监听时间戳的变化
+ timestamp(newVal, oldVal) {
+ // 如果倒计时间发生变化,清除定时器,重新开始倒计时
+ this.clearTimer();
+ this.start();
+ d: '00', // 天的默认值
+ h: '00', // 小时的默认值
+ i: '00', // 分钟的默认值
+ s: '00', // 秒的默认值
+ timer: null ,// 定时器
+ seconds: 0, // 记录不停倒计过程中变化的秒数
+ // 倒计时item的样式,item为分别的时分秒部分的数字
+ itemStyle() {
+ if(this.height) {
+ style.height = this.height + 'rpx';
+ style.width = this.height + 'rpx';
+ if(this.showBorder) {
+ style.borderStyle = 'solid';
+ style.borderColor = this.borderColor;
+ style.borderWidth = '1px';
+ if(this.bgColor) {
+ style.backgroundColor = this.bgColor;
+ // 倒计时数字的样式
+ letterStyle() {
+ if(this.fontSize) style.fontSize = this.fontSize + 'rpx';
+ if(this.color) style.color = this.color;
+ // 如果自动倒计时
+ this.autoplay && this.timestamp && this.start();
+ // 倒计时
+ start() {
+ // 避免可能出现的倒计时重叠情况
+ if (this.timestamp <= 0) return;
+ this.seconds = Number(this.timestamp);
+ this.formatTime(this.seconds);
+ this.seconds--;
+ // 发出change事件
+ this.$emit('change', this.seconds);
+ if (this.seconds < 0) {
+ return this.end();
+ }, 1000);
+ // 格式化时间
+ formatTime(seconds) {
+ // 小于等于0的话,结束倒计时
+ seconds <= 0 && this.end();
+ let [day, hour, minute, second] = [0, 0, 0, 0];
+ day = Math.floor(seconds / (60 * 60 * 24));
+ // 判断是否显示“天”参数,如果不显示,将天部分的值,加入到小时中
+ // hour为给后面计算秒和分等用的(基于显示天的前提下计算)
+ hour = Math.floor(seconds / (60 * 60)) - day * 24;
+ // showHour为需要显示的小时
+ let showHour = null;
+ if(this.showDays) {
+ showHour = hour;
+ // 如果不显示天数,将“天”部分的时间折算到小时中去
+ showHour = Math.floor(seconds / (60 * 60));
+ minute = Math.floor(seconds / 60) - hour * 60 - day * 24 * 60;
+ second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
+ // 如果小于10,在前面补上一个"0"
+ showHour = showHour < 10 ? '0' + showHour : showHour;
+ minute = minute < 10 ? '0' + minute : minute;
+ second = second < 10 ? '0' + second : second;
+ day = day < 10 ? '0' + day : day;
+ this.d = day;
+ this.h = showHour;
+ this.i = minute;
+ this.s = second;
+ // 停止倒计时
+ end() {
+ this.$emit('end', {});
+ // 清除定时器
+ if(this.timer) {
+ beforeDestroy() {
+ .u-countdown {
+ .u-countdown-item {
+ padding: 2rpx;
+ white-space: nowrap;
+ transform: translateZ(0);
+ .u-countdown-time {
+ margin: 0;
+ padding: 0;
+ .u-countdown-colon {
+ padding: 0 5rpx;
+ padding-bottom: 4rpx;
+ .u-countdown-scale {
+ transform: scale(0.9);
@@ -1,62 +0,0 @@
-// 补0,如1 -> 01
-function padZero(num, targetLength = 2) {
- let str = `${num}`
- while (str.length < targetLength) {
- str = `0${str}`
- return str
-const SECOND = 1000
-const MINUTE = 60 * SECOND
-const HOUR = 60 * MINUTE
-const DAY = 24 * HOUR
-export function parseTimeData(time) {
- const days = Math.floor(time / DAY)
- const hours = Math.floor((time % DAY) / HOUR)
- const minutes = Math.floor((time % HOUR) / MINUTE)
- const seconds = Math.floor((time % MINUTE) / SECOND)
- const milliseconds = Math.floor(time % SECOND)
- days,
- hours,
- minutes,
- seconds,
- milliseconds
-export function parseFormat(format, timeData) {
- let {
- } = timeData
- // 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去
- if (format.indexOf('DD') === -1) {
- hours += days * 24
- // 对天补0
- format = format.replace('DD', padZero(days))
- // 其他同理于DD的格式化处理方式
- if (format.indexOf('HH') === -1) {
- minutes += hours * 60
- format = format.replace('HH', padZero(hours))
- if (format.indexOf('mm') === -1) {
- seconds += minutes * 60
- format = format.replace('mm', padZero(minutes))
- if (format.indexOf('ss') === -1) {
- milliseconds += seconds * 1000
- format = format.replace('ss', padZero(seconds))
- return format.replace('SSS', padZero(milliseconds, 3))
-export function isSameSecond(time1, time2) {
- return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
- // 开始的数值,默认从0增长到某一个数
- startVal: {
- default: uni.$u.props.countTo.startVal
- // 要滚动的目标数值,必须
- endVal: {
- default: uni.$u.props.countTo.endVal
- // 滚动到目标数值的动画持续时间,单位为毫秒(ms)
- default: uni.$u.props.countTo.duration
- // 设置数值后是否自动开始滚动
- autoplay: {
- default: uni.$u.props.countTo.autoplay
- // 要显示的小数位数
- decimals: {
- default: uni.$u.props.countTo.decimals
- // 是否在即将到达目标数值的时候,使用缓慢滚动的效果
- useEasing: {
- default: uni.$u.props.countTo.useEasing
- // 十进制分割
- decimal: {
- default: uni.$u.props.countTo.decimal
- default: uni.$u.props.countTo.color
- default: uni.$u.props.countTo.fontSize
- // 是否加粗字体
- default: uni.$u.props.countTo.bold
- // 千位分隔符,类似金额的分割(¥23,321.05中的",")
- separator: {
- default: uni.$u.props.countTo.separator
@@ -1,36 +1,94 @@
class="u-count-num"
:style="{
fontWeight: bold ? 'bold' : 'normal',
color: color
}"
- >{{ displayValue }}</text>
+ {{ displayValue }}
* countTo 数字滚动
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
* @tutorial https://www.uviewui.com/components/countTo.html
- * @property {String | Number} startVal 开始的数值,默认从0增长到某一个数(默认 0 )
- * @property {String | Number} endVal 要滚动的目标数值,必须 (默认 0 )
- * @property {String | Number} duration 滚动到目标数值的动画持续时间,单位为毫秒(ms) (默认 2000 )
- * @property {Boolean} autoplay 设置数值后是否自动开始滚动 (默认 true )
- * @property {String | Number} decimals 要显示的小数位数,见官网说明(默认 0 )
- * @property {Boolean} useEasing 滚动结束时,是否缓动结尾,见官网说明(默认 true )
- * @property {String} decimal 十进制分割 ( 默认 "." )
- * @property {String} color 字体颜色( 默认 '#606266' )
- * @property {String | Number} fontSize 字体大小,单位px( 默认 22 )
- * @property {Boolean} bold 字体是否加粗(默认 false )
- * @property {String} separator 千位分隔符,见官网说明
+ * @property {String Number} start-val 开始值
+ * @property {String Number} end-val 结束值
+ * @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
+ * @property {Boolean} autoplay 是否自动开始滚动(默认true)
+ * @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
+ * @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
+ * @property {String} separator 千位分隔符,见官网说明
+ * @property {String} color 字体颜色(默认#303133)
+ * @property {String Number} font-size 字体大小,单位rpx(默认50)
+ * @property {Boolean} bold 字体是否加粗(默认false)
* @event {Function} end 数值滚动到目标值时触发
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
name: 'u-count-to',
+ // 开始的数值,默认从0增长到某一个数
+ startVal: {
+ // 要滚动的目标数值,必须
+ endVal: {
+ required: true
+ // 滚动到目标数值的动画持续时间,单位为毫秒(ms)
+ // 设置数值后是否自动开始滚动
+ // 要显示的小数位数
+ decimals: {
+ // 是否在即将到达目标数值的时候,使用缓慢滚动的效果
+ useEasing: {
+ // 十进制分割
+ decimal: {
+ default: '.'
+ default: 50
+ // 是否加粗字体
+ bold: {
+ // 千位分隔符,类似金额的分割(¥23,321.05中的",")
localStartVal: this.startVal,
@@ -45,7 +103,6 @@ export default {
lastTime: 0 // 上一次的时间
countDown() {
return this.startVal > this.endVal;
@@ -76,6 +133,7 @@ export default {
this.lastTime = currTime + timeToCall;
return id;
cancelAnimationFrame(id) {
clearTimeout(id);
@@ -103,8 +161,7 @@ export default {
// 重新开始(暂停的情况下)
resume() {
- if (!this.remaining) return
- this.startTime = 0;
+ this.startTime = null;
this.localDuration = this.remaining;
this.localStartVal = this.printVal;
this.requestAnimationFrame(this.count);
@@ -138,7 +195,7 @@ export default {
this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
- this.displayValue = this.formatNumber(this.printVal) || 0;
+ this.displayValue = this.formatNumber(this.printVal);
if (progress < this.localDuration) {
this.rAF = this.requestAnimationFrame(this.count);
@@ -173,11 +230,11 @@ export default {
.u-count-num {
- display: inline-flex;
@@ -1,116 +0,0 @@
- // 是否打开组件
- default: uni.$u.props.datetimePicker.show
- // 是否展示顶部的操作栏
- showToolbar: {
- default: uni.$u.props.datetimePicker.showToolbar
- // 绑定值
- default: uni.$u.props.datetimePicker.value
- // 顶部标题
- default: uni.$u.props.datetimePicker.title
- // 展示格式,mode=date为日期选择,mode=time为时间选择,mode=year-month为年月选择,mode=datetime为日期时间选择
- default: uni.$u.props.datetimePicker.mode
- // 可选的最大时间
- // 最大默认值为后10年
- default: uni.$u.props.datetimePicker.maxDate
- // 可选的最小时间
- // 最小默认值为前10年
- default: uni.$u.props.datetimePicker.minDate
- // 可选的最小小时,仅mode=time有效
- minHour: {
- default: uni.$u.props.datetimePicker.minHour
- // 可选的最大小时,仅mode=time有效
- maxHour: {
- default: uni.$u.props.datetimePicker.maxHour
- // 可选的最小分钟,仅mode=time有效
- minMinute: {
- default: uni.$u.props.datetimePicker.minMinute
- // 可选的最大分钟,仅mode=time有效
- maxMinute: {
- default: uni.$u.props.datetimePicker.maxMinute
- // 选项过滤函数
- filter: {
- default: uni.$u.props.datetimePicker.filter
- // 选项格式化函数
- default: uni.$u.props.datetimePicker.formatter
- // 是否显示加载中状态
- default: uni.$u.props.datetimePicker.loading
- // 各列中,单个选项的高度
- itemHeight: {
- default: uni.$u.props.datetimePicker.itemHeight
- // 取消按钮的文字
- default: uni.$u.props.datetimePicker.cancelText
- // 确认按钮的文字
- default: uni.$u.props.datetimePicker.confirmText
- // 取消按钮的颜色
- cancelColor: {
- default: uni.$u.props.datetimePicker.cancelColor
- // 确认按钮的颜色
- confirmColor: {
- default: uni.$u.props.datetimePicker.confirmColor
- // 每列中可见选项的数量
- visibleItemCount: {
- default: uni.$u.props.datetimePicker.visibleItemCount
- // 是否允许点击遮罩关闭选择器
- default: uni.$u.props.datetimePicker.closeOnClickOverlay
- // 各列的默认索引
- defaultIndex: {
- default: uni.$u.props.datetimePicker.defaultIndex
@@ -1,360 +0,0 @@
- <u-picker
- ref="picker"
- :columns="columns"
- :itemHeight="itemHeight"
- :showToolbar="showToolbar"
- :visibleItemCount="visibleItemCount"
- :defaultIndex="innerDefaultIndex"
- :cancelText="cancelText"
- :confirmText="confirmText"
- :cancelColor="cancelColor"
- :confirmColor="confirmColor"
- @cancel="cancel"
- @confirm="confirm"
- @change="change"
- </u-picker>
- function times(n, iteratee) {
- let index = -1
- const result = Array(n < 0 ? 0 : n)
- while (++index < n) {
- result[index] = iteratee(index)
- return result
- * DatetimePicker 时间日期选择器
- * @description 此选择器用于时间日期
- * @tutorial https://www.uviewui.com/components/datetimePicker.html
- * @property {Boolean} show 用于控制选择器的弹出与收起 ( 默认 false )
- * @property {Boolean} showToolbar 是否显示顶部的操作栏 ( 默认 true )
- * @property {String | Number} value 绑定值
- * @property {String} title 顶部标题
- * @property {String} mode 展示格式 mode=date为日期选择,mode=time为时间选择,mode=year-month为年月选择,mode=datetime为日期时间选择 ( 默认 ‘datetime )
- * @property {Number} maxDate 可选的最大时间 默认值为后10年
- * @property {Number} minDate 可选的最小时间 默认值为前10年
- * @property {Number} minHour 可选的最小小时,仅mode=time有效 ( 默认 0 )
- * @property {Number} maxHour 可选的最大小时,仅mode=time有效 ( 默认 23 )
- * @property {Number} minMinute 可选的最小分钟,仅mode=time有效 ( 默认 0 )
- * @property {Number} maxMinute 可选的最大分钟,仅mode=time有效 ( 默认 59 )
- * @property {Function} filter 选项过滤函数
- * @property {Function} formatter 选项格式化函数
- * @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 {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭选择器 ( 默认 false )
- * @property {Array} defaultIndex 各列的默认索引
- * @event {Function} close 关闭选择器时触发
- * @event {Function} confirm 点击确定按钮,返回当前选择的值
- * @event {Function} change 当选择值变化时触发
- * @event {Function} cancel 点击取消按钮
- * @example <u-datetime-picker :show="show" :value="value1" mode="datetime" ></u-datetime-picker>
- name: 'datetime-picker',
- columns: [],
- innerDefaultIndex: [],
- innerFormatter: (type, value) => value
- show(newValue, oldValue) {
- if (newValue) {
- this.updateColumnValue(this.innerValue)
- propsChange() {
- // 如果以下这些变量发生了变化,意味着需要重新初始化各列的值
- return [this.mode, this.maxDate, this.minDate, this.minHour, this.maxHour, this.minMinute, this.maxMinute, this.filter, ]
- this.innerValue = this.correctValue(this.value)
- // 关闭选择器
- if (this.closeOnClickOverlay) {
- // 点击工具栏的取消按钮
- this.$emit('cancel')
- // 点击工具栏的确定按钮
- this.$emit('confirm', {
- value: this.innerValue,
- mode: this.mode
- this.$emit('input', this.innerValue)
- //用正则截取输出值,当出现多组数字时,抛出错误
- intercept(e,type){
- let judge = e.match(/\d+/g)
- //判断是否掺杂数字
- if(judge.length>1){
- uni.$u.error("请勿在过滤或格式化函数时添加数字")
- return 0
- }else if(type&&judge[0].length==4){//判断是否是年份
- return judge[0]
- }else if(judge[0].length>2){
- }else{
- // 列发生变化时触发
- change(e) {
- const { indexs, values } = e
- let selectValue = ''
- if(this.mode === 'time') {
- // 根据value各列索引,从各列数组中,取出当前时间的选中值
- selectValue = `${this.intercept(values[0][indexs[0]])}:${this.intercept(values[1][indexs[1]])}`
- // 将选择的值转为数值,比如'03'转为数值的3,'2019'转为数值的2019
- const year = parseInt(this.intercept(values[0][indexs[0]],'year'))
- const month = parseInt(this.intercept(values[1][indexs[1]]))
- let date = parseInt(values[2] ? this.intercept(values[2][indexs[2]]) : 1)
- let hour = 0, minute = 0
- // 此月份的最大天数
- const maxDate = dayjs(`${year}-${month}`).daysInMonth()
- // year-month模式下,date不会出现在列中,设置为1,为了符合后边需要减1的需求
- if (this.mode === 'year-month') {
- date = 1
- // 不允许超过maxDate值
- date = Math.min(maxDate, date)
- if (this.mode === 'datetime') {
- hour = parseInt(this.intercept(values[3][indexs[3]]))
- minute = parseInt(this.intercept(values[4][indexs[4]]))
- // 转为时间模式
- selectValue = Number(new Date(year, month - 1, date, hour, minute))
- // 取出准确的合法值,防止超越边界的情况
- selectValue = this.correctValue(selectValue)
- this.innerValue = selectValue
- this.updateColumnValue(selectValue)
- // 发出change时间,value为当前选中的时间戳
- this.$emit('change', {
- value: selectValue,
- // 微信小程序不能传递this实例,会因为循环引用而报错
- picker: this.$refs.picker,
- // 更新各列的值,进行补0、格式化等操作
- updateColumnValue(value) {
- this.innerValue = value
- this.updateColumns()
- this.updateIndexs(value)
- // 更新索引
- updateIndexs(value) {
- let values = []
- const formatter = this.formatter || this.innerFormatter
- const padZero = uni.$u.padZero
- if (this.mode === 'time') {
- // 将time模式的时间用:分隔成数组
- const timeArr = value.split(':')
- // 使用formatter格式化方法进行管道处理
- values = [formatter('hour', timeArr[0]), formatter('minute', timeArr[1])]
- const date = new Date(value)
- values = [
- formatter('year', `${dayjs(value).year()}`),
- // 月份补0
- formatter('month', padZero(dayjs(value).month() + 1))
- if (this.mode === 'date') {
- // date模式,需要添加天列
- values.push(formatter('day', padZero(dayjs(value).date())))
- // 数组的push方法,可以写入多个参数
- values.push(formatter('day', padZero(dayjs(value).date())), formatter('hour', padZero(dayjs(value).hour())), formatter('minute', padZero(dayjs(value).minute())))
- // 根据当前各列的所有值,从各列默认值中找到默认值在各列中的索引
- const indexs = this.columns.map((column, index) => {
- // 通过取大值,可以保证不会出现找不到索引的-1情况
- return Math.max(0, column.findIndex(item => item === values[index]))
- this.innerDefaultIndex = indexs
- // 更新各列的值
- updateColumns() {
- // 获取各列的值,并且map后,对各列的具体值进行补0操作
- const results = this.getOriginColumns().map((column) => column.values.map((value) => formatter(column.type, value)))
- this.columns = results
- getOriginColumns() {
- // 生成各列的值
- const results = this.getRanges().map(({ type, range }) => {
- let values = times(range[1] - range[0] + 1, (index) => {
- let value = range[0] + index
- value = type === 'year' ? `${value}` : uni.$u.padZero(value)
- return value
- // 进行过滤
- if (this.filter) {
- values = this.filter(type, values)
- return { type, values }
- return results
- // 通过最大值和最小值生成数组
- generateArray(start, end) {
- return Array.from(new Array(end + 1).keys()).slice(start)
- // 得出合法的时间
- correctValue(value) {
- const isDateMode = this.mode !== 'time'
- if (isDateMode && !uni.$u.test.date(value)) {
- // 如果是日期类型,但是又没有设置合法的当前时间的话,使用最小时间为当前时间
- value = this.minDate
- } else if (!isDateMode && !value) {
- // 如果是时间类型,而又没有默认值的话,就用最小时间
- value = `${uni.$u.padZero(this.minHour)}:${uni.$u.padZero(this.minMinute)}`
- // 时间类型
- if (!isDateMode) {
- if (String(value).indexOf(':') === -1) return uni.$u.error('时间错误,请传递如12:24的格式')
- let [hour, minute] = value.split(':')
- // 对时间补零,同时控制在最小值和最大值之间
- hour = uni.$u.padZero(uni.$u.range(this.minHour, this.maxHour, Number(hour)))
- minute = uni.$u.padZero(uni.$u.range(this.minMinute, this.maxMinute, Number(minute)))
- return `${ hour }:${ minute }`
- // 如果是日期格式,控制在最小日期和最大日期之间
- value = dayjs(value).isBefore(dayjs(this.minDate)) ? this.minDate : value
- value = dayjs(value).isAfter(dayjs(this.maxDate)) ? this.maxDate : value
- // 获取每列的最大和最小值
- getRanges() {
- return [
- type: 'hour',
- range: [this.minHour, this.maxHour],
- type: 'minute',
- range: [this.minMinute, this.maxMinute],
- const { maxYear, maxDate, maxMonth, maxHour, maxMinute, } = this.getBoundary('max', this.innerValue);
- const { minYear, minDate, minMonth, minHour, minMinute, } = this.getBoundary('min', this.innerValue);
- const result = [
- type: 'year',
- range: [minYear, maxYear],
- type: 'month',
- range: [minMonth, maxMonth],
- type: 'day',
- range: [minDate, maxDate],
- range: [minHour, maxHour],
- range: [minMinute, maxMinute],
- if (this.mode === 'date')
- result.splice(3, 2);
- if (this.mode === 'year-month')
- result.splice(2, 3);
- return result;
- // 根据minDate、maxDate、minHour、maxHour等边界值,判断各列的开始和结束边界值
- getBoundary(type, innerValue) {
- const value = new Date(innerValue)
- const boundary = new Date(this[`${type}Date`])
- const year = dayjs(boundary).year()
- let month = 1
- let date = 1
- let hour = 0
- let minute = 0
- if (type === 'max') {
- month = 12
- // 月份的天数
- date = dayjs(value).daysInMonth()
- hour = 23
- minute = 59
- // 获取边界值,逻辑是:当年达到了边界值(最大或最小年),就检查月允许的最大和最小值,以此类推
- if (dayjs(value).year() === year) {
- month = dayjs(boundary).month() + 1
- if (dayjs(value).month() + 1 === month) {
- date = dayjs(boundary).date()
- if (dayjs(value).date() === date) {
- hour = dayjs(boundary).hour()
- if (dayjs(value).hour() === hour) {
- minute = dayjs(boundary).minute()
- [`${type}Year`]: year,
- [`${type}Month`]: month,
- [`${type}Date`]: date,
- [`${type}Hour`]: hour,
- [`${type}Minute`]: minute
- // 是否虚线
- dashed: {
- default: uni.$u.props.divider.dashed
- // 是否细线
- default: uni.$u.props.divider.hairline
- // 是否以点替代文字,优先于text字段起作用
- default: uni.$u.props.divider.dot
- // 内容文本的位置,left-左边,center-中间,right-右边
- textPosition: {
- default: uni.$u.props.divider.textPosition
- // 文本内容
- default: uni.$u.props.divider.text
- // 文本大小
- textSize: {
- default: uni.$u.props.divider.textSize
- // 文本颜色
- textColor: {
- default: uni.$u.props.divider.textColor
- // 线条颜色
- lineColor: {
- default: uni.$u.props.divider.lineColor
@@ -1,116 +1,153 @@
- class="u-divider"
- @tap="click"
- <u-line
- :color="lineColor"
- :customStyle="leftLineStyle"
- :hairline="hairline"
- :dashed="dashed"
- ></u-line>
- v-if="dot"
- class="u-divider__dot"
- >●</text>
- v-else-if="text"
- class="u-divider__text"
- :style="[textStyle]"
- :customStyle="rightLineStyle"
+ <view class="u-divider" :style="{
+ height: height == 'auto' ? 'auto' : height + 'rpx',
+ marginBottom: marginBottom + 'rpx',
+ marginTop: marginTop + 'rpx'
+ }" @tap="click">
+ <view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
+ <view v-if="useSlot" class="u-divider-text" :style="{
+ fontSize: fontSize + 'rpx'
+ }"><slot /></view>
- * divider 分割线
- * @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
- * @tutorial https://www.uviewui.com/components/divider.html
- * @property {Boolean} dashed 是否虚线 (默认 false )
- * @property {Boolean} hairline 是否细线 (默认 true )
- * @property {Boolean} dot 是否以点替代文字,优先于text字段起作用 (默认 false )
- * @property {String} textPosition 内容文本的位置,left-左边,center-中间,right-右边 (默认 'center' )
- * @property {String | Number} text 文本内容
- * @property {String | Number} textSize 文本大小 (默认 14)
- * @property {String} textColor 文本颜色 (默认 '#909399' )
- * @property {String} lineColor 线条颜色 (默认 '#dcdfe6' )
- * @event {Function} click divider组件被点击时触发
- * @example <u-divider :color="color">锦瑟无端五十弦</u-divider>
- name:'u-divider',
- style.fontSize = uni.$u.addUnit(this.textSize)
- style.color = this.textColor
- // 左边线条的的样式
- leftLineStyle() {
- // 如果是在左边,设置左边的宽度为固定值
- if (this.textPosition === 'left') {
- style.width = '80rpx'
- style.flex = 1
- // 右边线条的的样式
- rightLineStyle() {
- // 如果是在右边,设置右边的宽度为固定值
- if (this.textPosition === 'right') {
+ * divider 分割线
+ * @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
+ * @tutorial https://www.uviewui.com/components/divider.html
+ * @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
+ * @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6)
+ * @property {String} color 文字颜色(默认#909399)
+ * @property {String Number} fontSize 字体大小,单位rpx(默认26)
+ * @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff)
+ * @property {String Number} height 整个divider的高度,单位rpx(默认40)
+ * @property {String} type 将线条设置主题色(默认primary)
+ * @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
+ * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
+ * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
+ * @event {Function} click divider组件被点击时触发
+ * @example <u-divider color="#fa3534">长河落日圆</u-divider>
+ name: 'u-divider',
+ // 单一边divider横线的宽度(数值),单位rpx。或者百分比
+ halfWidth: {
- // divider组件被点击时触发
- click() {
- this.$emit('click');
+ // divider横线的颜色,如设置,
+ default: '#dcdfe6'
+ // 主题色,可以是primary|info|success|warning|error之一值
+ // 文字颜色
+ // 文字大小,单位rpx
+ // 整个divider的背景颜色
+ // 整个divider的高度单位rpx
+ // 上边距
+ marginTop: {
+ // 下边距
+ marginBottom: {
+ // 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙
+ useSlot: {
+ lineStyle() {
+ if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
+ else style.width = this.halfWidth + 'rpx';
+ // borderColor优先级高于type值
+ if(this.borderColor) style.borderColor = this.borderColor;
- $u-divider-margin:15px 0 !default;
- $u-divider-text-margin:0 15px !default;
- $u-divider-dot-font-size:12px !default;
- $u-divider-dot-margin:0 12px !default;
- $u-divider-dot-color: #c0c4cc !default;
+.u-divider {
- .u-divider {
- margin: $u-divider-margin;
- margin: $u-divider-text-margin;
- font-size: $u-divider-dot-font-size;
- margin: $u-divider-dot-margin;
- color: $u-divider-dot-color;
+.u-divider-line {
+ transform: scale(1, 0.5);
+ &--bordercolor--primary {
+ &--bordercolor--success {
+ &--bordercolor--error {
+ &--bordercolor--info {
+ border-color: $u-type-info;
+ &--bordercolor--warning {
+.u-divider-text {
@@ -1,36 +0,0 @@
- // 当前选中项的value值
- type: [Number, String, Array],
- // 菜单项标题
- // 选项数据,如果传入了默认slot,此参数无效
- options: {
- default() {
- return []
- // 是否禁用此菜单项
- // 下拉弹窗的高度
- height: {
- default: 'auto'
- // 点击遮罩是否可以收起弹窗
@@ -1,127 +1,132 @@
- <view class="u-drawdown">
- class="u-dropdown__menu"
- height: $u.addUnit(height)
- ref="u-dropdown__menu"
- class="u-dropdown__menu__item"
- v-for="(item, index) in menuList"
- @tap.stop="clickHandler(item, index)"
- <view class="u-dropdown__menu__item__content">
- class="u-dropdown__menu__item__content__text"
- :style="[index === current ? activeStyle : inactiveStyle]"
- >{{item.title}}</text>
- class="u-dropdown__menu__item__content__arrow"
- :class="[index === current && 'u-dropdown__menu__item__content__arrow--rotate']"
- :name="menuIcon"
- :size="$u.addUnit(menuIconSize)"
- <view class="u-dropdown__content">
-import props from './props.js';
- * Dropdown
- * @description
- * @tutorial url
- * @property {String}
- * @event {Function}
- name: 'u-dropdown',
- mixins: [uni.$u.mixin, props],
- // �˵�����
- menuList: [],
- current: 0
- // �������������(u-dropdown-item)��this��������data��������������������С��������ѭ�����ö�����
- this.children = [];
- clickHandler(item, index) {
- if(child.title === item.title) {
- // this.queryRect('u-dropdown__menu').then(size => {
- child.$emit('click')
- child.setContentAnimate(child.show ? 0 : 300)
- child.show = !child.show
- child.show = false
- child.setContentAnimate(0)
- // ��ȡ��ǩ�ijߴ�λ��
- queryRect(el) {
- // $uGetRectΪuView�Դ��Ľڵ��ѯ����������ĵ����ܣ�https://www.uviewui.com/js/getRect.html
- // ����ڲ�һ����this.$uGetRect�������Ϊthis.$u.getRect�����߹���һ�£����Ʋ�ͬ
- // nvue�£�ʹ��domģ���ѯԪ�ظ߶�
- // ����һ��promise���õ��ô˷�����������ʹ��then�ص�
- dom.getComponentRect(this.$refs[el], res => {
-<style lang="scss">
-.u-dropdown {
- &__menu {
+ <view class="u-dropdown-item" v-if="active" @touchmove.stop.prevent="() => {}" @tap.stop.prevent="() => {}">
+ <block v-if="!$slots.default && !$slots.$default">
+ <scroll-view scroll-y="true" :style="{
+ height: $u.addUnit(height)
+ <view class="u-dropdown-item__options">
+ <u-cell-group>
+ <u-cell-item @click="cellClick(item.value)" :arrow="false" :title="item.label" v-for="(item, index) in options"
+ :key="index" :title-style="{
+ color: value == item.value ? activeColor : inactiveColor
+ <u-icon v-if="value == item.value" name="checkbox-mark" :color="activeColor" size="32"></u-icon>
+ </u-cell-item>
+ </u-cell-group>
+ </scroll-view>
+ <slot v-else />
+ * dropdown-item 下拉菜单
+ * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
+ * @tutorial http://uviewui.com/components/dropdown.html
+ * @property {String | Number} v-model 双向绑定选项卡选择值
+ * @property {String} title 菜单项标题
+ * @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效
+ * @property {Boolean} disabled 是否禁用此选项卡(默认false)
+ * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
+ * @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
+ * @example <u-dropdown-item title="标题"></u-dropdown-item>
+ name: 'u-dropdown-item',
+ // 当前选中项的value值
+ type: [Number, String, Array],
+ // 菜单项标题
+ // 选项数据,如果传入了默认slot,此参数无效
+ options: {
+ return []
+ // 是否禁用此菜单项
+ // 下拉弹窗的高度
+ active: false, // 当前项是否处于展开状态
+ activeColor: '#2979ff', // 激活时左边文字和右边对勾图标的颜色
+ inactiveColor: '#606266', // 未激活时左边文字和右边对勾图标的颜色
+ // 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定
+ propsChange() {
+ return `${this.title}-${this.disabled}`;
+ propsChange(n) {
+ // 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法
+ // 将所有子组件数据重新整理一遍
+ if (this.parent) this.parent.init();
+ // 父组件的实例
+ // 获取父组件u-dropdown
+ let parent = this.$u.$parent.call(this, 'u-dropdown');
+ if (parent) {
+ this.parent = parent;
+ // 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
+ this.activeColor = parent.activeColor;
+ this.inactiveColor = parent.inactiveColor;
+ // 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性
+ // push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件
+ let exist = parent.children.find(val => {
+ return this === val;
+ if (!exist) parent.children.push(this);
+ if (parent.children.length == 1) this.active = true;
+ // 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中
+ parent.menuList.push({
+ title: this.title,
+ disabled: this.disabled
+ // cell被点击
+ cellClick(value) {
+ // 修改通过v-model绑定的值
+ this.$emit('input', value);
+ // 通知父组件(u-dropdown)收起菜单
+ this.parent.close();
+ // 发出事件,抛出当前勾选项的value
@@ -1,65 +0,0 @@
- // 标题选中时的样式
- activeStyle: {
- type: [String, Object],
- default: () => ({
- color: '#2979ff',
- fontSize: '14px'
- // 标题未选中时的样式
- inactiveStyle: {
- color: '#606266',
- // 点击遮罩是否关闭菜单
- closeOnClickMask: {
- // 点击当前激活项标题是否关闭菜单
- closeOnClickSelf: {
- // 过渡时间
- default: 300
- // 标题菜单的高度
- default: 40
- // 标题的字体大小
- titleSize: {
- default: 14
- // 下拉出来的内容部分的圆角值
- borderRadius: {
- // 菜单右侧的icon图标
- menuIcon: {
- default: 'arrow-down'
- // 菜单右侧图标的大小
- menuIconSize: {
@@ -1,127 +1,298 @@
+ <view class="u-dropdown">
+ <view class="u-dropdown__menu" :style="{
+ }" :class="{
+ 'u-border-bottom': borderBottom
+ <view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
+ <view class="u-flex">
+ <text class="u-dropdown__menu__item__text" :style="{
+ color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
+ fontSize: $u.addUnit(titleSize)
+ }">{{item.title}}</text>
+ <view class="u-dropdown__menu__item__arrow" :class="{
+ 'u-dropdown__menu__item__arrow--rotate': index === current
+ <u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
+ <view class="u-dropdown__content" :style="[contentStyle, {
+ transition: `opacity ${duration / 1000}s linear`,
+ top: $u.addUnit(height),
+ height: contentHeight + 'px'
+ @tap="maskClick" @touchmove.stop.prevent>
+ <view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
+ <view class="u-dropdown__content__mask"></view>
+ * dropdown 下拉菜单
+ * @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
+ * @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
+ * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
+ * @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
+ * @property {String | Number} height 标题菜单的高度,单位任意(默认80)
+ * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
+ * @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
+ * @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
+ * @event {Function} open 下拉菜单被打开时触发
+ * @event {Function} close 下拉菜单被关闭时触发
+ * @example <u-dropdown></u-dropdown>
+ name: 'u-dropdown',
+ // 菜单标题和选项的激活态颜色
+ // 菜单标题和选项的未激活态颜色
+ // 点击遮罩是否关闭菜单
+ closeOnClickMask: {
+ // 点击当前激活项标题是否关闭菜单
+ closeOnClickSelf: {
+ // 过渡时间
+ default: 300
+ // 标题菜单的高度,单位任意,数值默认为rpx单位
+ default: 80
+ // 标题的字体大小
+ // 下拉出来的内容部分的圆角值
+ // 菜单右侧的icon图标
+ menuIcon: {
+ default: 'arrow-down'
+ // 菜单右侧图标的大小
+ menuIconSize: {
+ showDropdown: true, // 是否打开下来菜单,
+ menuList: [], // 显示的菜单
+ active: false, // 下拉菜单的状态
+ // 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
+ // 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
+ current: 99999,
+ // 外层内容的样式,初始时处于底层,且透明
+ contentStyle: {
+ zIndex: -1,
+ opacity: 0
+ // 让某个菜单保持高亮的状态
+ highlightIndex: 99999,
+ contentHeight: 0
+ // 下拉出来部分的样式
+ popupStyle() {
+ // 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
+ style.transform = `translateY(${this.active ? 0 : '-100%'})`
+ style['transition-duration'] = this.duration / 1000 + 's';
+ style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
+ // 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
+ this.getContentHeight();
+ // 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
+ // 以保证数据的正确性
+ this.menuList = [];
+ this.children.map(child => {
+ child.init();
+ // 点击菜单
+ menuClick(index) {
+ // 判断是否被禁用
+ if (this.menuList[index].disabled) return;
+ // 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
+ if (index === this.current && this.closeOnClickSelf) {
+ // 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
+ this.children[index].active = false;
+ }, this.duration)
+ return;
+ this.open(index);
+ // 打开下拉菜单
+ open(index) {
+ // 重置高亮索引,否则会造成多个菜单同时高亮
+ // this.highlightIndex = 9999;
+ // 展开时,设置下拉内容的样式
+ this.contentStyle = {
+ zIndex: 11,
+ // 标记展开状态以及当前展开项的索引
+ this.active = true;
+ this.current = index;
+ // 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
+ // 之所以不是因display: none,是因为nvue没有display这个属性
+ this.children.map((val, idx) => {
+ val.active = index == idx ? true : false;
+ this.$emit('open', this.current);
+ // 设置下拉菜单处于收起状态
+ this.$emit('close', this.current);
+ // 设置为收起状态,同时current归位,设置为空字符串
+ this.active = false;
+ this.current = 99999;
+ // 下拉内容的样式进行调整,不透明度设置为0
+ // 点击遮罩
+ maskClick() {
+ // 如果不允许点击遮罩,直接返回
+ if (!this.closeOnClickMask) return;
+ // 外部手动设置某个菜单高亮
+ highlight(index = undefined) {
+ this.highlightIndex = index !== undefined ? index : 99999;
+ // 获取下拉菜单内容的高度
+ getContentHeight() {
+ // 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
+ // 才能让遮罩占满菜单一下,直到屏幕底部的高度
+ // this.$u.sys()为uView封装的获取设备信息的方法
+ let windowHeight = this.$u.sys().windowHeight;
+ this.$uGetRect('.u-dropdown__menu').then(res => {
+ // 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
+ // H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
+ // 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
+ // 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
+ this.contentHeight = windowHeight - res.bottom;
+ .u-dropdown {
+ &__menu {
+ &__arrow {
+ transition: transform .3s;
+ &--rotate {
+ z-index: 8;
+ left: 0px;
+ &__mask {
+ background: rgba(0, 0, 0, .3);
+ &__popup {
+ z-index: 10;
+ transform: translate3D(0, -100%, 0);
- // 内置图标名称,或图片路径,建议绝对路径
- default: uni.$u.props.empty.icon
- default: uni.$u.props.empty.text
- default: uni.$u.props.empty.textColor
- default: uni.$u.props.empty.textSize
- default: uni.$u.props.empty.iconColor
- // 图标的大小
- default: uni.$u.props.empty.iconSize
- // 选择预置的图标类型
- default: uni.$u.props.empty.mode
- // 图标宽度,单位px
- width: {
- default: uni.$u.props.empty.width
- // 图标高度,单位px
- default: uni.$u.props.empty.height
- // 是否显示组件
- default: uni.$u.props.empty.show
- // 组件距离上一个元素之间的距离,默认px单位
- marginTop: {
- default: uni.$u.props.empty.marginTop
@@ -1,62 +1,107 @@
- class="u-empty"
- :style="[emptyStyle]"
- v-if="show"
+ <view class="u-empty" v-if="show" :style="{
<u-icon
- v-if="!isSrc"
- :name="mode === 'message' ? 'chat' : `empty-${mode}`"
- :size="iconSize"
- margin-top="14"
+ :name="src ? src : 'empty-' + mode"
+ :custom-style="iconStyle"
+ :label="text ? text : icons[mode]"
+ label-pos="bottom"
+ :label-color="color"
+ :label-size="fontSize"
+ :size="iconSize"
+ :color="iconColor"
+ margin-top="14"
></u-icon>
- width: $u.addUnit(width),
- height: $u.addUnit(height),
- :src="icon"
- mode="widthFix"
- class="u-empty__text"
- >{{text ? text : icons[mode]}}</text>
- <view class="u-empty__wrap" v-if="$slots.default || $slots.$default">
+ <view class="u-slot-wrap">
+ <slot name="bottom"></slot>
* empty 内容为空
* @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
* @tutorial https://www.uviewui.com/components/empty.html
- * @property {String} icon 内置图标名称,或图片路径,建议绝对路径
- * @property {String} textColor 文字颜色 (默认 '#c0c4cc' )
- * @property {String | Number} textSize 文字大小 (默认 14 )
- * @property {String} iconColor 图标的颜色 (默认 '#c0c4cc' )
- * @property {String | Number} iconSize 图标的大小 (默认 90 )
- * @property {String} mode 选择预置的图标类型 (默认 'data' )
- * @property {String | Number} width 图标宽度,单位px (默认 160 )
- * @property {String | Number} height 图标高度,单位px (默认 160 )
- * @property {Boolean} show 是否显示组件 (默认 true )
- * @property {String | Number} marginTop 组件距离上一个元素之间的距离,默认px单位 (默认 0 )
+ * @property {String} color 文字颜色(默认#c0c4cc)
+ * @property {String} text 文字提示(默认“无内容”)
+ * @property {String} src 自定义图标路径,如定义,mode参数会失效
+ * @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
+ * @property {String} mode 内置的图标,见官网说明(默认data)
+ * @property {String Number} img-width 图标的宽度,单位rpx(默认240)
+ * @property {String} img-height 图标的高度,单位rpx(默认auto)
+ * @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0)
+ * @property {Boolean} show 是否显示组件(默认true)
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
name: "u-empty",
+ // 图标路径
+ text: {
+ default: '#c0c4cc'
+ // 图标的颜色
+ iconColor: {
+ // 图标的大小
+ default: 120
+ // 选择预置的图标类型
+ default: 'data'
+ // 图标宽度,单位rpx
+ imgWidth: {
+ // 图标高度,单位rpx
+ imgHeight: {
+ // 组件距离上一个元素之间的距离
icons: {
@@ -73,56 +118,76 @@
news: '无新闻列表',
message: '消息列表为空',
list: '列表为空',
- data: '数据为空',
- comment: '暂无评论',
- // 组件样式
- emptyStyle() {
- style.marginTop = uni.$u.addUnit(this.marginTop)
- // 合并customStyle样式,此参数通过mixin中的props传递
- return uni.$u.deepMerge(uni.$u.addStyle(this.customStyle), style)
- // 文本样式
- // 判断icon是否图片路径
- isSrc() {
- return this.icon.indexOf('/') >= 0
+ data: '数据为空'
+ // icons: [{
+ // icon: 'car',
+ // text: '购物车为空'
+ // },{
+ // icon: 'page',
+ // text: '页面不存在'
+ // icon: 'search',
+ // text: '没有搜索结果'
+ // icon: 'address',
+ // text: '没有收货地址'
+ // icon: 'wifi',
+ // text: '没有WiFi'
+ // icon: 'order',
+ // text: '订单为空'
+ // icon: 'coupon',
+ // text: '没有优惠券'
+ // icon: 'favor',
+ // text: '暂无收藏'
+ // icon: 'permission',
+ // text: '无权限'
+ // icon: 'history',
+ // text: '无历史记录'
+ // icon: 'news',
+ // text: '无新闻列表'
+ // icon: 'message',
+ // text: '消息列表为空'
+ // icon: 'list',
+ // text: '列表为空'
+ // icon: 'data',
+ // text: '数据为空'
+ // }],
- $u-empty-text-margin-top:20rpx !default;
- $u-empty-slot-margin-top:20rpx !default;
.u-empty {
- margin-top: $u-empty-text-margin-top;
+ .u-image {
+ margin-bottom: 20rpx;
+ .u-slot-wrap {
- .u-slot-wrap {
- margin-top:$u-empty-slot-margin-top;
@@ -0,0 +1,384 @@
+ <view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
+ <view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
+ <view class="u-label" :class="[required ? 'u-required' : '']" :style="{
+ justifyContent: justifyContent,
+ flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
+ <view class="u-icon-wrap" v-if="icon">
+ <u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
+ <text class="u-label-text" :class="[this.$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
+ <view class="fild-body">
+ <view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
+ <textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="value"
+ :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
+ :focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
+ @tap="fieldClick" />
+ <input
+ v-else
+ :style="[fieldStyle]"
+ :type="type"
+ class="u-flex-1 u-field__input-wrap"
+ :value="value"
+ :password="password || this.type === 'password'"
+ :placeholder="placeholder"
+ :placeholderStyle="placeholderStyle"
+ :maxlength="inputMaxlength"
+ :focus="focus"
+ :confirmType="confirmType"
+ @focus="onFocus"
+ @blur="onBlur"
+ @input="onInput"
+ @confirm="onConfirm"
+ @tap="fieldClick"
+ />
+ <u-icon :size="clearSize" v-if="clearable && value != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
+ <view class="u-button-wrap"><slot name="right" /></view>
+ <u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
+ <view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
+ paddingLeft: labelWidth + 'rpx'
+ }">{{ errorMessage }}</view>
+ * field 输入框
+ * @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
+ * @tutorial https://www.uviewui.com/components/field.html
+ * @property {String} type 输入框的类型(默认text)
+ * @property {String} icon label左边的图标,限uView的图标名称
+ * @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false)
+ * @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false)
+ * @property {String} label 输入框左边的文字提示
+ * @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false)
+ * @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true)
+ * @property {Number String} label-width label的宽度,单位rpx(默认130)
+ * @property {String} label-align label的文字对齐方式(默认left)
+ * @property {Object} field-style 自定义输入框的样式,对象形式
+ * @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30)
+ * @property {String} input-align 输入框内容对齐方式(默认left)
+ * @property {Boolean} border-bottom 是否显示field的下边框(默认true)
+ * @property {Boolean} border-top 是否显示field的上边框(默认false)
+ * @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
+ * @property {String} placeholder 输入框的提示文字
+ * @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} disabled 是否不可输入(默认false)
+ * @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
+ * @event {Function} input 输入框内容发生变化时触发
+ * @event {Function} focus 输入框获得焦点时触发
+ * @event {Function} blur 输入框失去焦点时触发
+ * @event {Function} confirm 点击完成按钮时触发
+ * @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
+ * @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
+ * @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
+ name:"u-field",
+ icon: String,
+ rightIcon: String,
+ // arrowDirection: {
+ // default: 'right'
+ required: Boolean,
+ label: String,
+ password: Boolean,
+ clearable: {
+ // 左边标题的宽度单位rpx
+ labelWidth: {
+ default: 130
+ // 对齐方式,left|center|right
+ labelAlign: {
+ inputAlign: {
+ autoHeight: {
+ errorMessage: {
+ placeholder: String,
+ placeholderStyle: String,
+ focus: Boolean,
+ fixed: Boolean,
+ value: [Number, String],
+ default: 'text'
+ maxlength: {
+ default: 140
+ confirmType: {
+ default: 'done'
+ // lable的位置,可选为 left-左边,top-上边
+ labelPosition: {
+ // 输入框的自定义样式
+ fieldStyle: {
+ // 清除按钮的大小
+ clearSize: {
+ // lable左边的图标样式,对象形式
+ // 是否自动去除两端的空格
+ trim: {
+ focused: false,
+ itemIndex: 0,
+ inputWrapStyle() {
+ style.textAlign = this.inputAlign;
+ // 判断lable的位置,如果是left的话,让input左边两边有间隙
+ if(this.labelPosition == 'left') {
+ style.margin = `0 8rpx`;
+ // 如果lable是top的,input的左边就没必要有间隙了
+ style.marginRight = `8rpx`;
+ rightIconStyle() {
+ if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)';
+ if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)';
+ else style.transform = 'roate(0deg)';
+ labelStyle() {
+ if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
+ if(this.labelAlign == 'center') style.justifyContent = 'center';
+ if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
+ // uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
+ justifyContent() {
+ if(this.labelAlign == 'left') return 'flex-start';
+ if(this.labelAlign == 'center') return 'center';
+ if(this.labelAlign == 'right') return 'flex-end';
+ // 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
+ inputMaxlength() {
+ return Number(this.maxlength)
+ // label的位置
+ fieldInnerStyle() {
+ style.flexDirection = 'row';
+ style.flexDirection = 'column';
+ onInput(event) {
+ let value = event.detail.value;
+ // 判断是否去除空格
+ if(this.trim) value = this.$u.trim(value);
+ onFocus(event) {
+ this.focused = true;
+ this.$emit('focus', event);
+ onBlur(event) {
+ // 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+ // 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+ this.focused = false;
+ }, 100)
+ this.$emit('blur', event);
+ onConfirm(e) {
+ this.$emit('confirm', e.detail.value);
+ onClear(event) {
+ this.$emit('input', '');
+ rightIconClick() {
+ this.$emit('right-icon-click');
+ fieldClick() {
+.u-field {
+ padding: 20rpx 28rpx;
+.u-field-inner {
+.u-textarea-inner {
+ align-items: flex-start;
+.u-textarea-class {
+ min-height: 96rpx;
+.fild-body {
+.u-arror-right {
+ margin-left: 8rpx;
+.u-label-text {
+.u-label-left-gap {
+.u-label-postion-top {
+.u-label {
+ width: 130rpx;
+ flex: 1 1 130rpx;
+.u-required::before {
+ left: -16rpx;
+ height: 9px;
+.u-field__input-wrap {
+.u-clear-icon {
+.u-error-message {
+.placeholder-style {
+ color: rgb(150, 151, 153);
+.u-input-class {
+.u-button-wrap {
- // input的label提示语
- default: uni.$u.props.formItem.label
- prop: {
- default: uni.$u.props.formItem.prop
- // 是否显示表单域的下划线边框
- default: uni.$u.props.formItem.borderBottom
- // label的位置,left-左边,top-上边
- labelPosition: {
- default: uni.$u.props.formItem.labelPosition
- // label的宽度,单位px
- labelWidth: {
- default: uni.$u.props.formItem.labelWidth
- // 右侧图标
- default: uni.$u.props.formItem.rightIcon
- // 左侧图标
- leftIcon: {
- default: uni.$u.props.formItem.leftIcon
- // 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置
- default: uni.$u.props.formItem.required
- leftIconStyle: {
- default: uni.$u.props.formItem.leftIconStyle,
@@ -1,235 +1,431 @@
- <view class="u-form-item">
- class="u-form-item__body"
- :style="[$u.addStyle(customStyle), {
- flexDirection: (labelPosition || parentData.labelPosition) === 'left' ? 'row' : 'column'
+ <view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
+ <view class="u-form-item__body" :style="{
+ flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
<!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" -->
- <!-- {{required}} -->
- class="u-form-item__body__left"
- v-if="required || leftIcon || label"
- width: $u.addUnit(labelWidth || parentData.labelWidth),
- marginBottom: parentData.labelPosition === 'left' ? 0 : '5px',
- <!-- 为了块对齐 -->
- <view class="u-form-item__body__left__content">
- <!-- nvue不支持伪元素before -->
- v-if="required"
- class="u-form-item__body__left__content__required"
- >*</text>
- class="u-form-item__body__left__content__icon"
- v-if="leftIcon"
- :name="leftIcon"
- :custom-style="leftIconStyle"
- class="u-form-item__body__left__content__label"
- :style="[parentData.labelStyle, {
- justifyContent: parentData.labelAlign === 'left' ? 'flex-start' : parentData.labelAlign === 'center' ? 'center' : 'flex-end'
- >{{ label }}</text>
+ <view class="u-form-item--left" :style="{
+ width: uLabelWidth,
+ flex: `0 0 ${uLabelWidth}`,
+ marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
+ <!-- 为了块对齐 -->
+ <view class="u-form-item--left__content" v-if="required || leftIcon || label">
+ <!-- nvue不支持伪元素before -->
+ <text v-if="required" class="u-form-item--left__content--required">*</text>
+ <view class="u-form-item--left__content__icon" v-if="leftIcon">
+ <u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
+ <view class="u-form-item--left__content__label" :style="[elLabelStyle, {
+ 'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
+ {{label}}
- <view class="u-form-item__body__right">
- <view class="u-form-item__body__right__content">
- <view class="u-form-item__body__right__content__slot">
+ <view class="u-form-item--right u-flex">
+ <view class="u-form-item--right__content">
+ <view class="u-form-item--right__content__slot ">
- class="item__body__right__content__icon"
- v-if="$slots.right"
+ <view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
+ <u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
<slot name="right" />
- <slot name="error">
- v-if="!!message && parentData.errorType === 'message'"
- class="u-form-item__body__right__message"
- marginLeft: $u.addUnit(parentData.labelPosition === 'top' ? 0 : (labelWidth || parentData.labelWidth))
- >{{ message }}</text>
- v-if="borderBottom"
- :color="message && parentData.errorType === 'border-bottom' ? $u.color.error : propsLine.color"
- :customStyle="`margin-top: ${message && parentData.errorType === 'message' ? '5px' : 0}`"
+ <view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
+ paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
+ }">{{validateMessage}}</view>
+ import schema from '../../libs/util/async-validator';
+ // 去除警告信息
+ schema.warning = function() {};
- * Form 表单
+ * form-item 表单item
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
- * @tutorial https://www.uviewui.com/components/form.html
- * @property {String} label input的label提示语
- * @property {String} prop 绑定的值
- * @property {String | Boolean} borderBottom 是否显示表单域的下划线边框
- * @property {String | Number} labelWidth label的宽度,单位px
- * @property {String} rightIcon 右侧图标
- * @property {String} leftIcon 左侧图标
- * @property {String | Object} leftIconStyle 左侧图标的样式
- * @property {Boolean} required 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置 (默认 false )
- * @example <u-form-item label="姓名" prop="userInfo.name" borderBottom ref="item1"></u-form-item>
+ * @tutorial http://uviewui.com/components/form.html
+ * @property {String} label 左侧提示文字
+ * @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的
+ * @property {Boolean} border-bottom 是否显示表单域的下划线边框
+ * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
+ * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
+ * @property {Object} label-style lable的样式,对象形式
+ * @property {String} label-align lable的对齐方式
+ * @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
+ * @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
+ * @property {Object} left-icon-style 左侧图标的样式,对象形式
+ * @property {Object} right-icon-style 右侧图标的样式,对象形式
+ * @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false)
+ * @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
name: 'u-form-item',
+ inject: {
+ uForm: {
+ return null
+ // input的label提示语
+ // 绑定的值
+ prop: {
+ // 是否显示表单域的下划线边框
+ // label的位置,left-左边,top-上边
+ // label的宽度,单位rpx
+ // lable的样式,对象形式
+ // lable字体的对齐方式
+ // 右侧图标
+ rightIcon: {
+ // 左侧图标
+ leftIcon: {
+ // 左侧图标的样式
+ leftIconStyle: {
+ rightIconStyle: {
+ // 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置
- // 错误提示语
- message: '',
+ initialValue: '', // 存储的默认值
+ // isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+ validateState: '', // 是否校验成功
+ validateMessage: '', // 校验失败的提示语
+ // 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
+ errorType: ['message'],
+ fieldValue: '', // 获取当前子组件input的输入的值
+ // 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中
parentData: {
- // 提示文本的位置
+ borderBottom: true,
+ labelWidth: 90,
labelPosition: 'left',
- // 提示文本对齐方式
- labelAlign: 'left',
- // 提示文本的样式
labelStyle: {},
- // 提示文本的宽度
- labelWidth: 45,
- // 错误提示方式
- errorType: 'message'
+ labelAlign: 'left',
+ validateState(val) {
+ this.broadcastInputError();
+ // 监听u-form组件的errorType的变化
+ "uForm.errorType"(val) {
+ this.errorType = val;
- // 组件创建完成时,将当前实例保存到u-form中
- propsLine() {
- return uni.$u.props.line
+ // 计算后的label宽度,由于需要多个判断,故放到computed中
+ uLabelWidth() {
+ // 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
+ return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
+ .elLabelWidth)) : '100%';
+ showError() {
+ return type => {
+ // 如果errorType数组中含有none,或者toast提示类型
+ if (this.errorType.indexOf('none') >= 0) return false;
+ else if (this.errorType.indexOf(type) >= 0) return true;
+ else return false;
+ // label的宽度
+ elLabelWidth() {
+ // label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值
+ return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
+ .labelWidth :
+ 90);
+ // label的样式
+ elLabelStyle() {
+ return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
+ {});
+ // label的位置,左侧或者上方
+ elLabelPosition() {
+ return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
+ 'left');
+ // label的对齐方式
+ elLabelAlign() {
+ return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
+ // label的下划线
+ elBorderBottom() {
+ // 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值
+ return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
+ true;
- // 父组件的实例
- uni.$u.error('u-form-item需要结合u-form组件使用')
+ broadcastInputError() {
+ // 子组件发出事件,第三个参数为true或者false,true代表有错误
+ this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'));
- // 获取父组件的参数
- // 此方法写在mixin中
- this.getParentData('u-form');
+ // 判断是否需要required校验
+ setRules() {
+ let that = this;
+ // 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+ // 从父组件u-form拿到当前u-form-item需要验证 的规则
+ // let rules = this.getRules();
+ // if (rules.length) {
+ // this.isRequired = rules.some(rule => {
+ // // 如果有必填项,就返回,没有的话,就是undefined
+ // return rule.required;
+ // });
+ // blur事件
+ this.$on('on-form-blur', that.onFieldBlur);
+ // change事件
+ this.$on('on-form-change', that.onFieldChange);
+ // 从u-form的rules属性中,取出当前u-form-item的校验规则
+ getRules() {
+ // 父组件的所有规则
+ let rules = this.parent.rules;
+ rules = rules ? rules[this.prop] : [];
+ // 保证返回的是一个数组形式
+ return [].concat(rules || []);
+ // blur事件时进行表单校验
+ onFieldBlur() {
+ this.validation('blur');
- // 移除u-form-item的校验结果
- clearValidate() {
- this.message = null
+ // change事件进行表单校验
+ onFieldChange() {
+ this.validation('change');
+ // 过滤出符合要求的rule规则
+ getFilteredRule(triggerType = '') {
+ let rules = this.getRules();
+ // 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
+ if (!triggerType) return rules;
+ // 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
+ // 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
+ // 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
+ return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
+ // 校验数据
+ validation(trigger, callback = () => {}) {
+ // 检验之间,先获取需要校验的值
+ this.fieldValue = this.parent.model[this.prop];
+ // blur和change是否有当前方式的校验规则
+ let rules = this.getFilteredRule(trigger);
+ // 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
+ // 对count变量的统计错误而无法进入上一层的回调
+ if (!rules || rules.length === 0) {
+ return callback('');
+ // 设置当前的装填,标识为校验中
+ this.validateState = 'validating';
+ // 调用async-validator的方法
+ let validator = new schema({
+ [this.prop]: rules
+ validator.validate({
+ [this.prop]: this.fieldValue
+ }, {
+ firstFields: true
+ }, (errors, fields) => {
+ // 记录状态和报错信息
+ this.validateState = !errors ? 'success' : 'error';
+ this.validateMessage = errors ? errors[0].message : '';
+ // 调用回调方法
+ callback(this.validateMessage);
- // 清空当前的组件的校验结果,并重置为初始值
+ // 清空当前的u-form-item
resetField() {
- // 找到原始值
- const value = uni.$u.getProperty(this.parent.originalModel, this.prop)
- // 将u-form的model的prop属性链还原原始值
- uni.$u.setProperty(this.parent.model, this.prop, value)
- // 移除校验结果
- // 点击组件
+ this.parent.model[this.prop] = this.initialValue;
+ // 设置为`success`状态,只是为了清空错误标记
+ this.validateState = 'success';
+ // 组件创建完成时,将当前实例保存到u-form中
+ // 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+ this.parent = this.$u.$parent.call(this, 'u-form');
+ // 历遍parentData中的属性,将parent中的同名属性赋值给parentData
+ Object.keys(this.parentData).map(key => {
+ this.parentData[key] = this.parent[key];
+ // 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
+ if (this.prop) {
+ // 将本实例添加到父组件中
+ this.parent.fields.push(this);
+ this.errorType = this.parent.errorType;
+ // 设置初始值
+ this.initialValue = this.fieldValue;
+ // 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的
+ // 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空
+ this.setRules();
+ // 组件销毁前,将实例从u-form的缓存中移除
+ // 如果当前没有prop的话表示当前不要进行删除(因为没有注入)
+ if (this.parent && this.prop) {
+ this.parent.fields.map((item, index) => {
+ if (item === this) this.parent.fields.splice(index, 1);
.u-form-item {
+ // align-items: flex-start;
+ padding: 20rpx 0;
color: $u-main-color;
+ line-height: $u-form-item-height;
+ &__border-bottom--error:after {
&__body {
- padding: 10px 0;
+ padding-right: 10rpx;
+ margin-right: 8rpx;
+ &--required {
+ padding-top: 6rpx;
+ &__label {
- padding-right: 10rpx;
flex: 1;
- margin-right: 8rpx;
- &__required {
- left: -9px;
- line-height: 20px;
- font-size: 20px;
- top: 3px;
+ &__slot {
- &__slot {
- /* #ifndef MP */
- margin-left: 10rpx;
- color: $u-light-color;
- font-size: 30rpx;
+ /* #ifndef MP */
- &__message {
- font-size: 12px;
- line-height: 12px;
+ color: $u-light-color;
+ &__message {
+ margin-top: 12rpx;
@@ -1,45 +0,0 @@
- // 当前form的需要验证字段的集合
- model: {
- default: uni.$u.props.form.model
- // 验证规则
- rules: {
- type: [Object, Function, Array],
- default: uni.$u.props.form.rules
- // 有错误时的提示方式,message-提示信息,toast-进行toast提示
- // border-bottom-下边框呈现红色,none-无提示
- errorType: {
- default: uni.$u.props.form.errorType
- default: uni.$u.props.form.borderBottom
- default: uni.$u.props.form.labelPosition
- default: uni.$u.props.form.labelWidth
- // lable字体的对齐方式
- labelAlign: {
- default: uni.$u.props.form.labelAlign
- // lable的样式,对象形式
- labelStyle: {
- default: uni.$u.props.form.labelStyle
@@ -1,214 +1,134 @@
- <view class="u-form">
- import props from "./props.js";
- import Schema from "../../libs/util/async-validator";
- // 去除警告信息
- Schema.warning = function() {};
- * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
- * @property {Object} model 当前form的需要验证字段的集合
- * @property {Object | Function | Array} rules 验证规则
- * @property {String} errorType 错误的提示方式,见上方说明 ( 默认 message )
- * @property {Boolean} borderBottom 是否显示表单域的下划线边框 ( 默认 true )
- * @property {String} labelPosition 表单域提示文字的位置,left-左侧,top-上方 ( 默认 'left' )
- * @property {String | Number} labelWidth 提示文字的宽度,单位px ( 默认 45 )
- * @property {String} labelAlign lable字体的对齐方式 ( 默认 ‘left' )
- * @property {Object} labelStyle lable的样式,对象形式
- * @example <u--formlabelPosition="left" :model="model1" :rules="rules" ref="form1"></u--form>
- name: "u-form",
- provide() {
- uForm: this,
- formRules: {},
- // 规则校验器
- validator: {},
- // 原始的model快照,用于resetFields方法重置表单时使用
- originalModel: null,
- // 监听规则的变化
- this.setRules(n);
- // 监听属性的变化,通知子组件u-form-item重新获取信息
- propsChange(n) {
- if (this.children?.length) {
- this.children.map((child) => {
- // 判断子组件(u-form-item)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
- typeof child.updateParentData == "function" &&
- child.updateParentData();
- // 监听model的初始值作为重置表单的快照
- if (!this.originalModel) {
- this.originalModel = uni.$u.deepClone(n);
- this.errorType,
- this.borderBottom,
- this.labelPosition,
- this.labelWidth,
- this.labelAlign,
- this.labelStyle,
- // 存储当前form下的所有u-form-item的实例
- // 不能定义在data中,否则微信小程序会造成循环引用而报错
- // 判断是否有规则
- if (Object.keys(rules).length === 0) return;
- if (process.env.NODE_ENV === 'development' && Object.keys(this.model).length === 0) {
- uni.$u.error('设置rules,model必须设置!如果已经设置,请刷新页面。');
- this.formRules = rules;
- // 重新将规则赋予Validator
- this.validator = new Schema(rules);
- // 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
- this.resetModel();
- // 重置model为初始值的快照
- resetModel(obj) {
- // 历遍所有u-form-item,根据其prop属性,还原model的原始快照
- const prop = child?.prop;
- const value = uni.$u.getProperty(this.originalModel, prop);
- uni.$u.setProperty(this.model, prop, value);
- // 清空校验结果
- props = [].concat(props);
- // 如果u-form-item的prop在props数组中,则清除对应的校验结果信息
- if (props[0] === undefined || props.includes(child.prop)) {
- child.message = null;
- // 对部分表单字段进行校验
- async validateField(value, callback, event = null) {
- // $nextTick是必须的,否则model的变更,可能会延后于此方法的执行
- // 校验错误信息,返回给回调方法,用于存放所有form-item的错误信息
- const errorsRes = [];
- // 如果为字符串,转为数组
- value = [].concat(value);
- // 历遍children所有子form-item
- // 用于存放form-item的错误信息
- const childErrors = [];
- if (value.includes(child.prop)) {
- // 获取对应的属性,通过类似'a.b.c'的形式
- const propertyVal = uni.$u.getProperty(
- this.model,
- child.prop
- // 属性链数组
- const propertyChain = child.prop.split(".");
- const propertyName =
- propertyChain[propertyChain.length - 1];
- const rule = this.formRules[child.prop];
- // 如果不存在对应的规则,直接返回,否则校验器会报错
- if (!rule) return;
- // rule规则可为数组形式,也可为对象形式,此处拼接成为数组
- const rules = [].concat(rule);
- // 对rules数组进行校验
- for (let i = 0; i < rules.length; i++) {
- const ruleItem = rules[i];
- // 将u-form-item的触发器转为数组形式
- const trigger = [].concat(ruleItem?.trigger);
- // 如果是有传入触发事件,但是此form-item却没有配置此触发器的话,不执行校验操作
- if (event && !trigger.includes(event)) continue;
- // 实例化校验对象,传入构造规则
- const validator = new Schema({
- [propertyName]: ruleItem,
- validator.validate({
- [propertyName]: propertyVal,
- (errors, fields) => {
- if (uni.$u.test.array(errors)) {
- errorsRes.push(...errors);
- childErrors.push(...errors);
- child.message =
- childErrors[0]?.message ?? null;
- // 执行回调函数
- typeof callback === "function" && callback(errorsRes);
- // 校验全部数据
- validate(callback) {
- // 开发环境才提示,生产环境不会提示
- if (process.env.NODE_ENV === 'development' && Object.keys(this.formRules).length === 0) {
- uni.$u.error('未设置rules,请看文档说明!如果已经设置,请刷新页面。');
- return new Promise((resolve, reject) => {
- // $nextTick是必须的,否则model的变更,可能会延后于validate方法
- // 获取所有form-item的prop,交给validateField方法进行校验
- const formItemProps = this.children.map(
- (item) => item.prop
- this.validateField(formItemProps, (errors) => {
- if(errors.length) {
- // 如果错误提示方式为toast,则进行提示
- this.errorType === 'toast' && uni.$u.toast(errors[0].message)
- reject(errors)
- resolve(true)
+ <view class="u-form"><slot /></view>
+ * form 表单
+ * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
+ * @property {Object} model 表单数据对象
+ * @property {Object} rules 通过ref设置,见官网说明
+ * @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
+ * @example <u-form :model="form" ref="uForm"></u-form>
+ name: 'u-form',
+ // 当前form的需要验证字段的集合
+ model: {
+ // 验证规则
+ // rules: {
+ // type: [Object, Function, Array],
+ // return {};
+ // border-bottom-下边框呈现红色,none-无提示
+ errorType: {
+ return ['message', 'toast']
+ default: 90
+ provide() {
+ uForm: this
+ rules: {}
+ // 存储当前form下的所有u-form-item的实例
+ // 不能定义在data中,否则微信小程序会造成循环引用而报错
+ this.fields = [];
+ setRules(rules) {
+ this.rules = rules;
+ // 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
+ resetFields() {
+ this.fields.map(field => {
+ field.resetField();
+ // 校验全部数据
+ validate(callback) {
+ // 对所有的u-form-item进行校验
+ let valid = true; // 默认通过
+ let count = 0; // 用于标记是否检查完毕
+ let errorArr = []; // 存放错误信息
+ // 调用每一个u-form-item实例的validation的校验方法
+ field.validation('', error => {
+ // 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
+ if (error) {
+ valid = false;
+ errorArr.push(error);
+ // 当历遍了所有的u-form-item时,调用promise的then方法
+ if (++count === this.fields.length) {
+ resolve(valid); // 进入promise的then方法
+ // 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
+ if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
+ this.$u.toast(errorArr[0]);
+ if (typeof callback == 'function') callback(valid);
@@ -0,0 +1,52 @@
+ <u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
+ <view class="u-update-content">
+ <rich-text :nodes="content"></rich-text>
+ </u-modal>
+ show: false,
+ content: `
+ 1. 修复badge组件的size参数无效问题<br>
+ 2. 新增Modal模态框组件<br>
+ 3. 新增压窗屏组件,可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
+ 4. 修复键盘组件在微信小程序上遮罩无效的问题
+ `,
+ onReady() {
+ this.show = true;
+ cancel() {
+ this.closeModal();
+ confirm() {
+ closeModal() {
+ uni.navigateBack();
+ .u-full-content {
+ background-color: #00C777;
+ .u-update-content {
+ line-height: 1.7;
+ padding: 30rpx;
- // 背景颜色(默认transparent)
- default: uni.$u.props.gap.bgColor
- // 分割槽高度,单位px(默认30)
- default: uni.$u.props.gap.height
- // 与上一个组件的距离
- default: uni.$u.props.gap.marginTop
- // 与下一个组件的距离
- marginBottom: {
- default: uni.$u.props.gap.marginBottom
@@ -3,36 +3,52 @@
- * gap 间隔槽
- * @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
- * @tutorial https://www.uviewui.com/components/gap.html
- * @property {String} bgColor 背景颜色 (默认 'transparent' )
- * @property {String | Number} height 分割槽高度,单位px (默认 20 )
- * @property {String | Number} marginTop 与前一个组件的距离,单位px( 默认 0 )
- * @property {String | Number} marginBottom 与后一个组件的距离,单位px (默认 0 )
- * @example <u-gap height="80" bg-color="#bbb"></u-gap>
- name: "u-gap",
- gapStyle() {
- backgroundColor: this.bgColor,
- height: uni.$u.addUnit(this.height),
- marginTop: uni.$u.addUnit(this.marginTop),
- marginBottom: uni.$u.addUnit(this.marginBottom),
+ * gap 间隔槽
+ * @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
+ * @tutorial https://www.uviewui.com/components/gap.html
+ * @property {String} bg-color 背景颜色(默认#f3f4f6)
+ * @property {String Number} height 分割槽高度,单位rpx(默认30)
+ * @example <u-gap height="80" bg-color="#bbb"></u-gap>
+ name: "u-gap",
+ default: 'transparent ' // 背景透明
+ // 高度
+ // 与上一个组件的距离
+ // 与下一个组件的距离
+ gapStyle() {
+ backgroundColor: this.bgColor,
+ height: this.height + 'rpx',
+ marginTop: this.marginTop + 'rpx',
+ marginBottom: this.marginBottom + 'rpx'
- // 宫格的name
- type: [String, Number, null],
- default: uni.$u.props.gridItem.name
- default: uni.$u.props.gridItem.bgColor
@@ -1,209 +1,126 @@
- class="u-grid-item"
- hover-class="u-grid-item--hover-class"
- :class="classes"
- :style="[itemStyle]"
+ <view class="u-grid-item" :hover-class="parentData.hoverClass"
+ :hover-stay-time="200" @tap="click" :style="{
+ background: bgColor,
+ <view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
* gridItem 提示
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用
* @tutorial https://www.uviewui.com/components/grid.html
- * @property {String | Number} name 宫格的name ( 默认 null )
- * @property {String} bgColor 宫格的背景颜色 (默认 'transparent' )
- * @property {Object} customStyle 自定义样式,对象形式
+ * @property {String} bg-color 宫格的背景颜色(默认#ffffff)
+ * @property {String Number} index 点击宫格时,返回的值
+ * @property {Object} custom-style 自定义样式,对象形式
* @event {Function} click 点击宫格触发
* @example <u-grid-item></u-grid-item>
name: "u-grid-item",
+ // 点击时返回的index
+ padding: '30rpx 0'
+ hoverClass: '', // 按下去的时候,是否显示背景灰色
col: 3, // 父组件划分的宫格数
border: true, // 是否显示边框,根据父组件决定
- width: 0, // nvue下才这么计算,vue下放到computed中,否则会因为延时造成闪烁
- classes: [], // 类名集合,用于判断是否显示右边和下边框
+ this.updateParentData();
+ // this.parent在updateParentData()中定义
+ this.parent.children.push(this);
- // vue下放到computed中,否则会因为延时造成闪烁
+ // 每个grid-item的宽度
width() {
- return 100 / Number(this.parentData.col) + '%'
+ return 100 / Number(this.parentData.col) + '%';
- background: this.bgColor,
- width: this.width
- // 用于在父组件u-grid的children中被添加入子组件时,
- // 重新计算item的边框
- uni.$on('$uGridItem', () => {
- this.gridItemClasses()
- // 获取元素该有的长度,nvue下要延时才准确
- this.$nextTick(function(){
- this.getItemWidth()
- // 发出事件,通知所有的grid-item都重新计算自己的边框
- uni.$emit('$uGridItem')
// 获取父组件的参数
updateParentData() {
// 此方法写在mixin中
this.getParentData('u-grid');
- let name = this.name
- // 如果没有设置name属性,历遍父组件的children数组,判断当前的元素是否和本实例this相等,找出当前组件的索引
- const children = this.parent?.children
- if(children && this.name === null) {
- name = children.findIndex(child => child === this)
- // 调用父组件方法,发出事件
- this.parent && this.parent.childClick(name)
- this.$emit('click', name)
- async getItemWidth() {
- // 如果是nvue,不能使用百分比,只能使用固定宽度
- if(this.parent) {
- // 获取父组件宽度后,除以栅格数,得出每个item的宽度
- const parentWidth = await this.getParentWidth()
- width = parentWidth / Number(this.parentData.col) + 'px'
- this.width = width
- // 获取父元素的尺寸
- getParentWidth() {
- // 返回一个promise,让调用者可以用await同步获取
- // 调用父组件的ref
- dom.getComponentRect(this.parent.$refs['u-grid'], res => {
- resolve(res.size.width)
- gridItemClasses() {
- if(this.parentData.border) {
- const classes = []
- this.parent.children.map((child, index) =>{
- if(this === child) {
- const len = this.parent.children.length
- // 贴近右边屏幕边沿的child,并且最后一个(比如只有横向2个的时候),无需右边框
- if((index + 1) % this.parentData.col !== 0 && index + 1 !== len) {
- classes.push('u-border-right')
- // 总的宫格数量对列数取余的值
- // 如果取余后,值为0,则意味着要将最后一排的宫格,都不需要下边框
- const lessNum = len % this.parentData.col === 0 ? this.parentData.col : len % this.parentData.col
- // 最下面的一排child,无需下边框
- if(index < len - lessNum) {
- classes.push('u-border-bottom')
- this.classes = classes
+ this.parent && this.parent.click(this.index);
- // 移除事件监听,释放性能
- uni.$off('$uGridItem')
- $u-grid-item-hover-class-opcatiy:.5 !default;
- $u-grid-item-margin-top:1rpx !default;
- $u-grid-item-border-right-width:0.5px !default;
- $u-grid-item-border-bottom-width:0.5px !default;
- $u-grid-item-border-right-color:$u-border-color !default;
- $u-grid-item-border-bottom-color:$u-border-color !default;
.u-grid-item {
+ background: #fff;
position: relative;
- display: flex;
/* #ifdef MP */
float: left;
- /* #ifdef MP-WEIXIN */
- margin-top:$u-grid-item-margin-top;
- &--hover-class {
- opacity:$u-grid-item-hover-class-opcatiy;
+ .u-grid-item-hover {
+ background: #f7f7f7 !important;
- /* #ifdef APP-NVUE */
- // 由于nvue不支持组件内引入app.vue中再引入的样式,所以需要写在这里
- .u-border-right {
- border-right-width:$u-grid-item-border-right-width;
- border-color: $u-grid-item-border-right-color;
+ .u-grid-marker-box {
- .u-border-bottom {
- border-bottom-width:$u-grid-item-border-bottom-width;
- border-color:$u-grid-item-border-bottom-color;
+ .u-grid-marker-wrap {
+ .u-grid-item-box {
+ padding: 30rpx 0;
- // 分成几列
- col: {
- default: uni.$u.props.grid.col
- // 是否显示边框
- default: uni.$u.props.grid.border
- // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
- default: uni.$u.props.grid.align
@@ -1,97 +1,108 @@
- class="u-grid"
- ref='u-grid'
- :style="[gridStyle]"
+ <view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
- * grid 宫格布局
- * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
- * @tutorial https://www.uviewui.com/components/grid.html
- * @property {String | Number} col 宫格的列数(默认 3 )
- * @property {Boolean} border 是否显示宫格的边框(默认 false )
- * @property {String} align 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 (默认 'left' )
- * @event {Function} click 点击宫格触发
- * @example <u-grid :col="3" @click="click"></u-grid>
- name: 'u-grid',
- index: 0,
- width: 0
+ * grid 宫格布局
+ * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
+ * @tutorial https://www.uviewui.com/components/grid.html
+ * @property {String Number} col 宫格的列数(默认3)
+ * @property {Boolean} border 是否显示宫格的边框(默认true)
+ * @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false)
+ * @event {Function} click 点击宫格触发
+ * @example <u-grid :col="3" @click="click"></u-grid>
+ name: 'u-grid',
+ // 分成几列
+ col: {
+ default: 3
- // 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
- typeof(child.updateParentData) == 'function' && child.updateParentData();
+ // 是否显示边框
- // 如果将children定义在data中,在微信小程序会造成循环引用而报错
+ // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
- // 计算父组件的值是否发生变化
- return [this.hoverClass, this.col, this.size, this.border];
- // 宫格对齐方式
- gridStyle() {
- switch (this.align) {
- case 'left':
- style.justifyContent = 'flex-start';
- case 'center':
- style.justifyContent = 'center';
- case 'right':
- style.justifyContent = 'flex-end';
- return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle));
+ // 宫格按压时的样式类,"none"为无效果
+ // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
+ parentData() {
+ if(this.children.length) {
+ // 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+ typeof(child.updateParentData) == 'function' && child.updateParentData();
- // 此方法由u-grid-item触发,用于在u-grid发出事件
- childClick(name) {
+ // 计算父组件的值是否发生变化
+ return [this.hoverClass, this.col, this.size, this.border];
+ // 宫格对齐方式
+ gridStyle() {
+ switch(this.align) {
+ case 'left':
+ style.justifyContent = 'flex-start';
+ break;
+ case 'center':
+ style.justifyContent = 'center';
+ case 'right':
+ style.justifyContent = 'flex-end';
+ default: style.justifyContent = 'flex-start';
- $u-grid-width:100% !default;
- .u-grid {
- width: $u-grid-width;
+.u-grid {
+ /* #ifdef MP */
@@ -1,214 +0,0 @@
- 'uicon-level': '\ue693',
- 'uicon-column-line': '\ue68e',
- 'uicon-checkbox-mark': '\ue807',
- 'uicon-folder': '\ue7f5',
- 'uicon-movie': '\ue7f6',
- 'uicon-star-fill': '\ue669',
- 'uicon-star': '\ue65f',
- 'uicon-phone-fill': '\ue64f',
- 'uicon-phone': '\ue622',
- 'uicon-apple-fill': '\ue881',
- 'uicon-chrome-circle-fill': '\ue885',
- 'uicon-backspace': '\ue67b',
- 'uicon-attach': '\ue632',
- 'uicon-cut': '\ue948',
- 'uicon-empty-car': '\ue602',
- 'uicon-empty-coupon': '\ue682',
- 'uicon-empty-address': '\ue646',
- 'uicon-empty-favor': '\ue67c',
- 'uicon-empty-permission': '\ue686',
- 'uicon-empty-news': '\ue687',
- 'uicon-empty-search': '\ue664',
- 'uicon-github-circle-fill': '\ue887',
- 'uicon-rmb': '\ue608',
- 'uicon-person-delete-fill': '\ue66a',
- 'uicon-reload': '\ue788',
- 'uicon-order': '\ue68f',
- 'uicon-server-man': '\ue6bc',
- 'uicon-search': '\ue62a',
- 'uicon-fingerprint': '\ue955',
- 'uicon-more-dot-fill': '\ue630',
- 'uicon-scan': '\ue662',
- 'uicon-share-square': '\ue60b',
- 'uicon-map': '\ue61d',
- 'uicon-map-fill': '\ue64e',
- 'uicon-tags': '\ue629',
- 'uicon-tags-fill': '\ue651',
- 'uicon-bookmark-fill': '\ue63b',
- 'uicon-bookmark': '\ue60a',
- 'uicon-eye': '\ue613',
- 'uicon-eye-fill': '\ue641',
- 'uicon-mic': '\ue64a',
- 'uicon-mic-off': '\ue649',
- 'uicon-calendar': '\ue66e',
- 'uicon-calendar-fill': '\ue634',
- 'uicon-trash': '\ue623',
- 'uicon-trash-fill': '\ue658',
- 'uicon-play-left': '\ue66d',
- 'uicon-play-right': '\ue610',
- 'uicon-minus': '\ue618',
- 'uicon-plus': '\ue62d',
- 'uicon-info': '\ue653',
- 'uicon-info-circle': '\ue7d2',
- 'uicon-info-circle-fill': '\ue64b',
- 'uicon-question': '\ue715',
- 'uicon-error': '\ue6d3',
- 'uicon-close': '\ue685',
- 'uicon-checkmark': '\ue6a8',
- 'uicon-android-circle-fill': '\ue67e',
- 'uicon-android-fill': '\ue67d',
- 'uicon-ie': '\ue87b',
- 'uicon-IE-circle-fill': '\ue889',
- 'uicon-google': '\ue87a',
- 'uicon-google-circle-fill': '\ue88a',
- 'uicon-setting-fill': '\ue872',
- 'uicon-setting': '\ue61f',
- 'uicon-minus-square-fill': '\ue855',
- 'uicon-plus-square-fill': '\ue856',
- 'uicon-heart': '\ue7df',
- 'uicon-heart-fill': '\ue851',
- 'uicon-camera': '\ue7d7',
- 'uicon-camera-fill': '\ue870',
- 'uicon-more-circle': '\ue63e',
- 'uicon-more-circle-fill': '\ue645',
- 'uicon-chat': '\ue620',
- 'uicon-chat-fill': '\ue61e',
- 'uicon-bag-fill': '\ue617',
- 'uicon-bag': '\ue619',
- 'uicon-error-circle-fill': '\ue62c',
- 'uicon-error-circle': '\ue624',
- 'uicon-close-circle': '\ue63f',
- 'uicon-close-circle-fill': '\ue637',
- 'uicon-checkmark-circle': '\ue63d',
- 'uicon-checkmark-circle-fill': '\ue635',
- 'uicon-question-circle-fill': '\ue666',
- 'uicon-question-circle': '\ue625',
- 'uicon-share': '\ue631',
- 'uicon-share-fill': '\ue65e',
- 'uicon-shopping-cart': '\ue621',
- 'uicon-shopping-cart-fill': '\ue65d',
- 'uicon-bell': '\ue609',
- 'uicon-bell-fill': '\ue640',
- 'uicon-list': '\ue650',
- 'uicon-list-dot': '\ue616',
- 'uicon-zhihu': '\ue6ba',
- 'uicon-zhihu-circle-fill': '\ue709',
- 'uicon-zhifubao': '\ue6b9',
- 'uicon-zhifubao-circle-fill': '\ue6b8',
- 'uicon-weixin-circle-fill': '\ue6b1',
- 'uicon-weixin-fill': '\ue6b2',
- 'uicon-twitter-circle-fill': '\ue6ab',
- 'uicon-twitter': '\ue6aa',
- 'uicon-taobao-circle-fill': '\ue6a7',
- 'uicon-taobao': '\ue6a6',
- 'uicon-weibo-circle-fill': '\ue6a5',
- 'uicon-weibo': '\ue6a4',
- 'uicon-qq-fill': '\ue6a1',
- 'uicon-qq-circle-fill': '\ue6a0',
- 'uicon-moments-circel-fill': '\ue69a',
- 'uicon-moments': '\ue69b',
- 'uicon-qzone': '\ue695',
- 'uicon-qzone-circle-fill': '\ue696',
- 'uicon-baidu-circle-fill': '\ue680',
- 'uicon-baidu': '\ue681',
- 'uicon-facebook-circle-fill': '\ue68a',
- 'uicon-facebook': '\ue689',
- 'uicon-car': '\ue60c',
- 'uicon-car-fill': '\ue636',
- 'uicon-warning-fill': '\ue64d',
- 'uicon-warning': '\ue694',
- 'uicon-clock-fill': '\ue638',
- 'uicon-clock': '\ue60f',
- 'uicon-edit-pen': '\ue612',
- 'uicon-edit-pen-fill': '\ue66b',
- 'uicon-email': '\ue611',
- 'uicon-email-fill': '\ue642',
- 'uicon-minus-circle': '\ue61b',
- 'uicon-minus-circle-fill': '\ue652',
- 'uicon-plus-circle': '\ue62e',
- 'uicon-plus-circle-fill': '\ue661',
- 'uicon-file-text': '\ue663',
- 'uicon-file-text-fill': '\ue665',
- 'uicon-pushpin': '\ue7e3',
- 'uicon-pushpin-fill': '\ue86e',
- 'uicon-grid': '\ue673',
- 'uicon-grid-fill': '\ue678',
- 'uicon-play-circle': '\ue647',
- 'uicon-play-circle-fill': '\ue655',
- 'uicon-pause-circle-fill': '\ue654',
- 'uicon-pause': '\ue8fa',
- 'uicon-pause-circle': '\ue643',
- 'uicon-eye-off': '\ue648',
- 'uicon-eye-off-outline': '\ue62b',
- 'uicon-gift-fill': '\ue65c',
- 'uicon-gift': '\ue65b',
- 'uicon-rmb-circle-fill': '\ue657',
- 'uicon-rmb-circle': '\ue677',
- 'uicon-kefu-ermai': '\ue656',
- 'uicon-server-fill': '\ue751',
- 'uicon-coupon-fill': '\ue8c4',
- 'uicon-coupon': '\ue8ae',
- 'uicon-integral': '\ue704',
- 'uicon-integral-fill': '\ue703',
- 'uicon-home-fill': '\ue964',
- 'uicon-home': '\ue965',
- 'uicon-hourglass-half-fill': '\ue966',
- 'uicon-hourglass': '\ue967',
- 'uicon-account': '\ue628',
- 'uicon-plus-people-fill': '\ue626',
- 'uicon-minus-people-fill': '\ue615',
- 'uicon-account-fill': '\ue614',
- 'uicon-thumb-down-fill': '\ue726',
- 'uicon-thumb-down': '\ue727',
- 'uicon-thumb-up': '\ue733',
- 'uicon-thumb-up-fill': '\ue72f',
- 'uicon-lock-fill': '\ue979',
- 'uicon-lock-open': '\ue973',
- 'uicon-lock-opened-fill': '\ue974',
- 'uicon-lock': '\ue97a',
- 'uicon-red-packet-fill': '\ue690',
- 'uicon-photo-fill': '\ue98b',
- 'uicon-photo': '\ue98d',
- 'uicon-volume-off-fill': '\ue659',
- 'uicon-volume-off': '\ue644',
- 'uicon-volume-fill': '\ue670',
- 'uicon-volume': '\ue633',
- 'uicon-red-packet': '\ue691',
- 'uicon-download': '\ue63c',
- 'uicon-arrow-up-fill': '\ue6b0',
- 'uicon-arrow-down-fill': '\ue600',
- 'uicon-play-left-fill': '\ue675',
- 'uicon-play-right-fill': '\ue676',
- 'uicon-rewind-left-fill': '\ue679',
- 'uicon-rewind-right-fill': '\ue67a',
- 'uicon-arrow-downward': '\ue604',
- 'uicon-arrow-leftward': '\ue601',
- 'uicon-arrow-rightward': '\ue603',
- 'uicon-arrow-upward': '\ue607',
- 'uicon-arrow-down': '\ue60d',
- 'uicon-arrow-right': '\ue605',
- 'uicon-arrow-left': '\ue60e',
- 'uicon-arrow-up': '\ue606',
- 'uicon-skip-back-left': '\ue674',
- 'uicon-skip-forward-right': '\ue672',
- 'uicon-rewind-right': '\ue66f',
- 'uicon-rewind-left': '\ue671',
- 'uicon-arrow-right-double': '\ue68d',
- 'uicon-arrow-left-double': '\ue68c',
- 'uicon-wifi-off': '\ue668',
- 'uicon-wifi': '\ue667',
- 'uicon-empty-data': '\ue62f',
- 'uicon-empty-history': '\ue684',
- 'uicon-empty-list': '\ue68b',
- 'uicon-empty-page': '\ue627',
- 'uicon-empty-order': '\ue639',
- 'uicon-man': '\ue697',
- 'uicon-woman': '\ue69c',
- 'uicon-man-add': '\ue61c',
- 'uicon-man-add-fill': '\ue64c',
- 'uicon-man-delete': '\ue61a',
- 'uicon-man-delete-fill': '\ue66a',
- 'uicon-zh': '\ue70a',
- 'uicon-en': '\ue692'
@@ -1,89 +0,0 @@
- // 图标类名
- default: uni.$u.props.icon.name
- // 图标颜色,可接受主题色
- default: uni.$u.props.icon.color
- default: uni.$u.props.icon.size
- // 是否显示粗体
- default: uni.$u.props.icon.bold
- // 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
- index: {
- default: uni.$u.props.icon.index
- // 触摸图标时的类名
- hoverClass: {
- default: uni.$u.props.icon.hoverClass
- // 自定义扩展前缀,方便用户扩展自己的图标库
- customPrefix: {
- default: uni.$u.props.icon.customPrefix
- // 图标右边或者下面的文字
- default: uni.$u.props.icon.label
- // label的位置,只能右边或者下边
- labelPos: {
- default: uni.$u.props.icon.labelPos
- // label的大小
- default: uni.$u.props.icon.labelSize
- default: uni.$u.props.icon.labelColor
- // label与图标的距离
- default: uni.$u.props.icon.space
- // 图片的mode
- imgMode: {
- default: uni.$u.props.icon.imgMode
- // 用于显示图片小图标时,图片的宽度
- default: uni.$u.props.icon.width
- // 用于显示图片小图标时,图片的高度
- default: uni.$u.props.icon.height
- // 用于解决某些情况下,让图标垂直居中的用途
- default: uni.$u.props.icon.top
- default: uni.$u.props.icon.stop
@@ -1,234 +1,336 @@
- class="u-icon"
- :class="['u-icon--' + labelPos]"
- class="u-icon__img"
- v-if="isImg"
- :src="name"
- :mode="imgMode"
- :style="[imgStyle, $u.addStyle(customStyle)]"
- class="u-icon__icon"
- :class="uClasses"
- :style="[iconStyle, $u.addStyle(customStyle)]"
- :hover-class="hoverClass"
- >{{icon}}</text>
+ <view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
+ <image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
+ <text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass"
+ @touchstart="touchstart">
+ <text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass"
+ class="u-icon__decimal">
<!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 -->
- v-if="label !== ''"
- class="u-icon__label"
+ <text v-if="label !== ''" class="u-icon__label" :style="{
color: labelColor,
fontSize: $u.addUnit(labelSize),
- marginLeft: labelPos == 'right' ? $u.addUnit(space) : 0,
- marginTop: labelPos == 'bottom' ? $u.addUnit(space) : 0,
- marginRight: labelPos == 'left' ? $u.addUnit(space) : 0,
- marginBottom: labelPos == 'top' ? $u.addUnit(space) : 0,
+ marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
+ marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
+ marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
+ marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0,
+ }">{{ label }}
- // nvue通过weex的dom模块引入字体,相关文档地址如下:
- // https://weex.apache.org/zh/docs/modules/dom.html#addrule
- const fontUrl = 'https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf'
- const domModule = weex.requireModule('dom')
- domModule.addRule('fontFace', {
- 'fontFamily': "uicon-iconfont",
- 'src': `url('${fontUrl}')`
- // 引入图标名称,已经对应的unicode
- import icons from './icons'
- import props from './props.js';;
- * icon 图标
- * @description 基于字体的图标集,包含了大多数常见场景的图标。
- * @tutorial https://www.uviewui.com/components/icon.html
- * @property {String} name 图标名称,见示例图标集
- * @property {String} color 图标颜色,可接受主题色 (默认 color['u-content-color'] )
- * @property {String | Number} size 图标字体大小,单位px (默认 '16px' )
- * @property {Boolean} bold 是否显示粗体 (默认 false )
- * @property {String | Number} index 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
- * @property {String} hoverClass 图标按下去的样式类,用法同uni的view组件的hoverClass参数,详情见官网
- * @property {String} customPrefix 自定义扩展前缀,方便用户扩展自己的图标库 (默认 'uicon' )
- * @property {String | Number} label 图标右侧的label文字
- * @property {String} labelPos label相对于图标的位置,只能right或bottom (默认 'right' )
- * @property {String | Number} labelSize label字体大小,单位px (默认 '15px' )
- * @property {String} labelColor 图标右侧的label文字颜色 ( 默认 color['u-content-color'] )
- * @property {String | Number} space label与图标的距离,单位px (默认 '3px' )
- * @property {String} imgMode 图片的mode
- * @property {String | Number} width 显示图片小图标时的宽度
- * @property {String | Number} height 显示图片小图标时的高度
- * @property {String | Number} top 图标在垂直方向上的定位 用于解决某些情况下,让图标垂直居中的用途 (默认 0 )
- * @property {Boolean} stop 是否阻止事件传播 (默认 false )
- * @property {Object} customStyle icon的样式,对象形式
- * @event {Function} click 点击图标时触发
- * @event {Function} touchstart 事件触摸时触发
- * @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
- name: 'u-icon',
+ * icon 图标
+ * @description 基于字体的图标集,包含了大多数常见场景的图标。
+ * @tutorial https://www.uviewui.com/components/icon.html
+ * @property {String} name 图标名称,见示例图标集
+ * @property {String} color 图标颜色(默认inherit)
+ * @property {String | Number} size 图标字体大小,单位rpx(默认32)
+ * @property {String | Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String} label 图标右侧的label文字(默认28)
+ * @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} label-color label字体颜色(默认#606266)
+ * @property {Object} custom-style icon的样式,对象形式
+ * @property {String} custom-prefix 自定义字体图标库时,需要写上此值
+ * @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6)
+ * @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出
+ * @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网
+ * @property {String} width 显示图片小图标时的宽度
+ * @property {String} height 显示图片小图标时的高度
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {Boolean} show-decimal-icon 是否为DecimalIcon
+ * @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效
+ * @property {String | Number} percent 显示的百分比,仅Decimal时有效
+ * @event {Function} click 点击图标时触发
+ * @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
+ name: 'u-icon',
+ // 图标类名
+ // 图标颜色,可接受主题色
+ default: 'inherit'
+ // 是否显示粗体
+ // 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
+ // 触摸图标时的类名
+ // 自定义扩展前缀,方便用户扩展自己的图标库
+ customPrefix: {
+ default: 'uicon'
+ // 图标右边或者下面的文字
+ // label的位置,只能右边或者下边
+ labelPos: {
+ // label的大小
+ default: '28'
+ // label的颜色
+ labelColor: {
+ // label与图标的距离(横向排列)
+ marginLeft: {
+ default: '6'
+ // label与图标的距离(竖向排列)
+ marginRight: {
+ // 图片的mode
+ imgMode: {
+ default: 'widthFix'
+ // 自定义样式
+ // 用于显示图片小图标时,图片的宽度
+ // 用于显示图片小图标时,图片的高度
+ // 用于解决某些情况下,让图标垂直居中的用途
+ // 是否为DecimalIcon
+ showDecimalIcon: {
+ // 背景颜色,可接受主题色,仅Decimal时有效
+ // 显示的百分比,仅Decimal时有效
+ default: '50'
+ customClass() {
+ let classes = []
+ classes.push(this.customPrefix + '-' + this.name)
+ // uView的自定义图标类名为u-iconfont
+ if (this.customPrefix == 'uicon') {
+ classes.push('u-iconfont')
+ classes.push(this.customPrefix)
+ // 主题色,通过类配置
+ if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) {
+ classes.push('u-icon__icon--' + this.inactiveColor)
+ } else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
+ // 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
+ // 故需将其拆成一个字符串的形式,通过空格隔开各个类名
+ //#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
+ classes = classes.join(' ')
+ return classes
+ let style = {}
+ style = {
+ fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
+ fontWeight: this.bold ? 'bold' : 'normal',
+ // 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
+ top: this.$u.addUnit(this.top)
+ // 非主题色值时,才当作颜色值
+ if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) {
+ style.color = this.inactiveColor
+ } else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
+ return style
+ // 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
+ isImg() {
+ return this.name.indexOf('/') !== -1
- uClasses() {
- classes.push(this.customPrefix + '-' + this.name)
- // // uView的自定义图标类名为u-iconfont
- // if (this.customPrefix == 'uicon') {
- // classes.push('u-iconfont')
- // } else {
- // classes.push(this.customPrefix)
- // }
- // 主题色,通过类配置
- if (this.color && uni.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
- // 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
- // 故需将其拆成一个字符串的形式,通过空格隔开各个类名
- //#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
- //#endif
- iconStyle() {
- style = {
- fontSize: uni.$u.addUnit(this.size),
- lineHeight: uni.$u.addUnit(this.size),
- fontWeight: this.bold ? 'bold' : 'normal',
- // 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
- top: uni.$u.addUnit(this.top)
- // 非主题色值时,才当作颜色值
- if (this.color && !uni.$u.config.type.includes(this.color)) style.color = this.color
- // 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
- isImg() {
- return this.name.indexOf('/') !== -1
- imgStyle() {
- // 如果设置width和height属性,则优先使用,否则使用size属性
- style.width = this.width ? uni.$u.addUnit(this.width) : uni.$u.addUnit(this.size)
- style.height = this.height ? uni.$u.addUnit(this.height) : uni.$u.addUnit(this.size)
- // 通过图标名,查找对应的图标
- icon() {
- // 如果内置的图标中找不到对应的图标,就直接返回name值,因为用户可能传入的是unicode代码
- return icons['uicon-' + this.name] || this.name
+ imgStyle() {
+ // 如果设置width和height属性,则优先使用,否则使用size属性
+ style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size)
+ style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size)
+ decimalIconStyle() {
+ top: this.$u.addUnit(this.top),
+ width: this.percent + '%'
+ if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
- // 是否阻止事件冒泡
+ decimalIconClass() {
+ if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
+ else classes.push('u-icon__icon--primary')
+ this.$emit('click', this.index)
+ touchstart() {
+ this.$emit('touchstart', this.index)
- // 变量定义
- $u-icon-primary: $u-primary !default;
- $u-icon-success: $u-success !default;
- $u-icon-info: $u-info !default;
- $u-icon-warning: $u-warning !default;
- $u-icon-error: $u-error !default;
- $u-icon-label-line-height:1 !default;
- // 非nvue下加载字体
- @font-face {
- font-family: 'uicon-iconfont';
- src: url('https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf') format('truetype');
+@import '../../iconfont.css';
+.u-icon {
- .u-icon {
+ flex-direction: row-reverse;
- &--left {
- &--right {
- &--top {
- flex-direction: column-reverse;
- &--bottom {
- font-family: uicon-iconfont;
+ &--top {
+ flex-direction: column-reverse;
- color: $u-icon-primary;
+ &--bottom {
- color: $u-icon-success;
- color: $u-icon-error;
+ color: $u-type-primary;
- color: $u-icon-warning;
+ color: $u-type-success;
- color: $u-icon-info;
- &__img {
- height: auto;
- will-change: transform;
+ color: $u-type-warning;
- line-height: $u-icon-label-line-height;
+ &--info {
+ color: $u-type-info;
+ &__decimal {
+ display: inline-block;
+ &__img {
+ height: auto;
+ will-change: transform;
@@ -1,84 +0,0 @@
- // 图片地址
- default: uni.$u.props.image.src
- default: uni.$u.props.image.mode
- // 宽度,单位任意
- default: uni.$u.props.image.width
- // 高度,单位任意
- default: uni.$u.props.image.height
- // 图片形状,circle-圆形,square-方形
- default: uni.$u.props.image.shape
- // 圆角,单位任意
- radius: {
- default: uni.$u.props.image.radius
- // 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
- lazyLoad: {
- default: uni.$u.props.image.lazyLoad
- // 开启长按图片显示识别微信小程序码菜单
- showMenuByLongpress: {
- default: uni.$u.props.image.showMenuByLongpress
- // 加载中的图标,或者小图片
- loadingIcon: {
- default: uni.$u.props.image.loadingIcon
- // 加载失败的图标,或者小图片
- errorIcon: {
- default: uni.$u.props.image.errorIcon
- // 是否显示加载中的图标或者自定义的slot
- showLoading: {
- default: uni.$u.props.image.showLoading
- // 是否显示加载错误的图标或者自定义的slot
- showError: {
- default: uni.$u.props.image.showError
- // 是否需要淡入效果
- fade: {
- default: uni.$u.props.image.fade
- // 只支持网络资源,只对微信小程序有效
- webp: {
- default: uni.$u.props.image.webp
- // 过渡时间,单位ms
- default: uni.$u.props.image.duration
- // 背景颜色,用于深色页面加载图片时,为了和背景色融合
- default: uni.$u.props.image.bgColor
@@ -1,232 +1,268 @@
- :duration="fade ? 1000 : 0"
+ <view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
+ v-if="!isError"
+ :src="src"
+ :mode="mode"
+ @error="onErrorHandler"
+ @load="onLoadHandler"
+ :lazy-load="lazyLoad"
+ class="u-image__image"
+ :show-menu-by-longpress="showMenuByLongpress"
+ borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
- class="u-image"
- @tap="onClick"
- :style="[wrapStyle, backgroundStyle]"
+ v-if="showLoading && loading"
+ class="u-image__loading"
+ borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
+ backgroundColor: this.bgColor
- v-if="!isError"
- @error="onErrorHandler"
- @load="onLoadHandler"
- :show-menu-by-longpress="showMenuByLongpress"
- :lazy-load="lazyLoad"
- class="u-image__image"
- borderRadius: shape == 'circle' ? '10000px' : $u.addUnit(radius),
- v-if="showLoading && loading"
- class="u-image__loading"
- borderRadius: shape == 'circle' ? '50%' : $u.addUnit(radius),
- <slot name="loading">
- :name="loadingIcon"
- v-if="showError && isError && !loading"
- class="u-image__error"
- :name="errorIcon"
+ <slot v-if="$slots.loading" name="loading" />
+ <u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
+ v-if="showError && isError && !loading"
+ class="u-image__error"
+ <slot v-if="$slots.error" name="error" />
+ <u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
- * Image 图片
- * @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
- * @tutorial https://uviewui.com/components/image.html
- * @property {String} src 图片地址
- * @property {String} mode 裁剪模式,见官网说明 (默认 'aspectFill' )
- * @property {String | Number} width 宽度,单位任意,如果为数值,则为px单位 (默认 '300' )
- * @property {String | Number} height 高度,单位任意,如果为数值,则为px单位 (默认 '225' )
- * @property {String} shape 图片形状,circle-圆形,square-方形 (默认 'square' )
- * @property {String | Number} radius 圆角值,单位任意,如果为数值,则为px单位 (默认 0 )
- * @property {Boolean} lazyLoad 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效 (默认 true )
- * @property {Boolean} showMenuByLongpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效 (默认 true )
- * @property {String} loadingIcon 加载中的图标,或者小图片 (默认 'photo' )
- * @property {String} errorIcon 加载失败的图标,或者小图片 (默认 'error-circle' )
- * @property {Boolean} showLoading 是否显示加载中的图标或者自定义的slot (默认 true )
- * @property {Boolean} showError 是否显示加载错误的图标或者自定义的slot (默认 true )
- * @property {Boolean} fade 是否需要淡入效果 (默认 true )
- * @property {Boolean} webp 只支持网络资源,只对微信小程序有效 (默认 false )
- * @property {String | Number} duration 搭配fade参数的过渡时间,单位ms (默认 500 )
- * @property {String} bgColor 背景颜色,用于深色页面加载图片时,为了和背景色融合 (默认 '#f3f4f6' )
- * @event {Function} click 点击图片时触发
- * @event {Function} error 图片加载失败时触发
- * @event {Function} load 图片加载成功时触发
- * @example <u-image width="100%" height="300px" :src="src"></u-image>
- name: 'u-image',
- // 图片是否加载错误,如果是,则显示错误占位图
- isError: false,
- // 初始化组件时,默认为加载中状态
- loading: true,
- // 不透明度,为了实现淡入淡出的效果
- opacity: 1,
- // 过渡时间,因为props的值无法修改,故需要一个中间值
- durationTime: this.duration,
- // 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
- backgroundStyle: {},
- // 用于fade模式的控制组件显示与否
- show: false
+ * Image 图片
+ * @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
+ * @tutorial https://uviewui.com/components/image.html
+ * @property {String} src 图片地址
+ * @property {String} mode 裁剪模式,见官网说明
+ * @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%)
+ * @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto)
+ * @property {String} shape 图片形状,circle-圆形,square-方形(默认square)
+ * @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0)
+ * @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true)
+ * @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false)
+ * @property {String} loading-icon 加载中的图标,或者小图片(默认 photo)
+ * @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle)
+ * @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true)
+ * @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true)
+ * @property {Boolean} fade 是否需要淡入效果(默认 true)
+ * @property {String Number} width 传入图片路径时图片的宽度
+ * @property {String Number} height 传入图片路径时图片的高度
+ * @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
+ * @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
+ * @event {Function} click 点击图片时触发
+ * @event {Function} error 图片加载失败时触发
+ * @event {Function} load 图片加载成功时触发
+ * @example <u-image width="100%" height="300rpx" :src="src"></u-image>
+ name: 'u-image',
+ // 图片地址
+ // 裁剪模式
+ default: 'aspectFill'
+ // 宽度,单位任意
+ default: '100%'
+ // 高度,单位任意
+ // 图片形状,circle-圆形,square-方形
+ // 圆角,单位任意
+ // 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
+ lazyLoad: {
+ // 开启长按图片显示识别微信小程序码菜单
+ showMenuByLongpress: {
+ // 加载中的图标,或者小图片
+ loadingIcon: {
+ default: 'photo'
+ // 加载失败的图标,或者小图片
+ errorIcon: {
+ default: 'error-circle'
+ // 是否显示加载中的图标或者自定义的slot
+ showLoading: {
+ // 是否显示加载错误的图标或者自定义的slot
+ showError: {
+ // 是否需要淡入效果
+ fade: {
- if (!n) {
- // 如果传入null或者'',或者false,或者undefined,标记为错误状态
- this.isError = true
- this.isError = false;
- this.loading = true;
+ // 只支持网络资源,只对微信小程序有效
+ webp: {
+ // 过渡时间,单位ms
+ default: 500
+ // 背景颜色,用于深色页面加载图片时,为了和背景色融合
+ default: '#f3f4f6'
+ // 图片是否加载错误,如果是,则显示错误占位图
+ isError: false,
+ // 初始化组件时,默认为加载中状态
+ loading: true,
+ // 不透明度,为了实现淡入淡出的效果
+ opacity: 1,
+ // 过渡时间,因为props的值无法修改,故需要一个中间值
+ durationTime: this.duration,
+ // 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
+ backgroundStyle: {}
+ immediate: true,
+ handler (n) {
+ if(!n) {
+ // 如果传入null或者'',或者false,或者undefined,标记为错误状态
+ this.isError = true;
+ this.loading = false;
+ this.isError = false;
+ this.loading = true;
- wrapStyle() {
- // 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
- style.width = this.$u.addUnit(this.width);
- style.height = this.$u.addUnit(this.height);
- // 如果是显示圆形,设置一个很多的半径值即可
- style.borderRadius = this.shape == 'circle' ? '10000px' : uni.$u.addUnit(this.radius)
- // 如果设置圆角,必须要有hidden,否则可能圆角无效
- style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible'
- // if (this.fade) {
- // style.opacity = this.opacity
- // // nvue下,这几个属性必须要分开写
- // style.transitionDuration = `${this.durationTime}ms`
- // style.transitionTimingFunction = 'ease-in-out'
- // style.transitionProperty = 'opacity'
+ wrapStyle() {
+ // 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
+ style.width = this.$u.addUnit(this.width);
+ style.height = this.$u.addUnit(this.height);
+ // 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
+ style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
+ // 如果设置圆角,必须要有hidden,否则可能圆角无效
+ style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
+ if (this.fade) {
+ style.opacity = this.opacity;
+ style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
+ // 点击图片
+ onClick() {
- this.show = true
- // 点击图片
- onClick() {
- // 图片加载失败
- onErrorHandler(err) {
- this.loading = false
- this.$emit('error', err)
- // 图片加载完成,标记loading结束
- onLoadHandler(event) {
- this.isError = false
- this.$emit('load', event)
- this.removeBgColor()
- // 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
- // 否则无需fade效果时,png图片依然能看到下方的背景色
- // if (!this.fade) return this.removeBgColor();
- // // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
- // this.opacity = 0;
- // // 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
- // // 到图片展示的过程中的淡入效果
- // this.durationTime = 0;
- // // 延时50ms,否则在浏览器H5,过渡效果无效
- // setTimeout(() => {
- // this.durationTime = this.duration;
- // this.opacity = 1;
- // this.removeBgColor();
- // }, this.durationTime);
- // }, 50);
- // 移除图片的背景色
- removeBgColor() {
- // 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
- this.backgroundStyle = {
- backgroundColor: 'transparent'
+ // 图片加载失败
+ onErrorHandler(err) {
+ this.$emit('error', err);
+ // 图片加载完成,标记loading结束
+ onLoadHandler() {
+ this.$emit('load');
+ // 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
+ // 否则无需fade效果时,png图片依然能看到下方的背景色
+ if (!this.fade) return this.removeBgColor();
+ // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
+ // 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
+ // 到图片展示的过程中的淡入效果
+ this.durationTime = 0;
+ // 延时50ms,否则在浏览器H5,过渡效果无效
+ this.durationTime = this.duration;
+ this.removeBgColor();
+ }, this.durationTime);
+ // 移除图片的背景色
+ removeBgColor() {
+ // 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
+ this.backgroundStyle = {
+ backgroundColor: 'transparent'
- $u-image-error-top:0px !default;
- $u-image-error-left:0px !default;
- $u-image-error-width:100% !default;
- $u-image-error-hight:100% !default;
- $u-image-error-background-color:$u-bg-color !default;
- $u-image-error-color:$u-tips-color !default;
- $u-image-error-font-size: 46rpx !default;
+.u-image {
+ transition: opacity 0.5s ease-in-out;
- .u-image {
- transition: opacity 0.5s ease-in-out;
- &__image {
- height: 100%;
+ &__image {
- &__loading,
- &__error {
- top: $u-image-error-top;
- left: $u-image-error-left;
- width: $u-image-error-width;
- height: $u-image-error-hight;
- background-color: $u-image-error-background-color;
- color: $u-image-error-color;
- font-size: $u-image-error-font-size;
+ &__loading,
+ &__error {
+ background-color: $u-bg-color;
+ font-size: 46rpx;
- // 列表锚点文本内容
- default: uni.$u.props.indexAnchor.text
- // 列表锚点文字颜色
- default: uni.$u.props.indexAnchor.color
- // 列表锚点文字大小,单位默认px
- default: uni.$u.props.indexAnchor.size
- // 列表锚点背景颜色
- default: uni.$u.props.indexAnchor.bgColor
- // 列表锚点高度,单位默认px
- default: uni.$u.props.indexAnchor.height
@@ -1,91 +1,89 @@
- <header>
- class="u-index-anchor u-border-bottom"
- :ref="`u-index-anchor-${text}`"
- backgroundColor: bgColor
- class="u-index-anchor__text"
- fontSize: $u.addUnit(size),
- >{{ text }}</text>
+ <!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+ <view>
+ <view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]">
+ <view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]">
+ <slot v-if="useSlot" />
+ <block v-else>
+ <text>{{ index }}</text>
- </header>
- * IndexAnchor 列表锚点
- * @tutorial https://uviewui.com/components/indexList.html
- * @property {String | Number} text 列表锚点文本内容
- * @property {String} color 列表锚点文字颜色 ( 默认 '#606266' )
- * @property {String | Number} size 列表锚点文字大小,单位默认px ( 默认 14 )
- * @property {String} bgColor 列表锚点背景颜色 ( 默认 '#dedede' )
- * @property {String | Number} height 列表锚点高度,单位默认px ( 默认 32 )
- * @example <u-index-anchor :text="indexList[index]"></u-index-anchor>
+ * indexAnchor 索引列表锚点
+ * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
+ * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
+ * @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false)
+ * @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效
+ * @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}"
+ * @event {Function} default 锚点位置显示内容,默认为索引字符
+ * @example <u-index-anchor :index="item" />
- name: 'u-index-anchor',
+ name: "u-index-anchor",
+ active: false,
+ wrapperStyle: {},
+ anchorStyle: {}
- // 此处会活动父组件实例,并赋值给实例的parent属性
- const indexList = uni.$u.$parent.call(this, 'u-index-list')
- if (!indexList) {
- return uni.$u.error('u-index-anchor必须要搭配u-index-list组件使用')
- // 将当前实例放入到u-index-list中
- indexList.anchors.push(this)
- const indexListItem = uni.$u.$parent.call(this, 'u-index-item')
- // 只有在非nvue下,u-index-anchor才是嵌套在u-index-item中的
- if (!indexListItem) {
- return uni.$u.error('u-index-anchor必须要搭配u-index-item组件使用')
- // 设置u-index-item的id为anchor的text标识符,因为非nvue下滚动列表需要依赖scroll-view滚动到元素的特性
- indexListItem.id = this.text.charCodeAt(0)
+ this.parent = this.$u.$parent.call(this, 'u-index-list');
+ this.parent.updateData();
+ customAnchorStyle() {
+ return Object.assign(this.anchorStyle, this.customStyle);
.u-index-anchor {
- position: sticky;
- padding-left: 15px;
- z-index: 1;
+ padding: 14rpx 24rpx;
+ color: #606266;
+ line-height: 1.2;
+ background-color: rgb(245, 245, 245);
+ .u-index-anchor--active {
+ color: #2979ff;
@@ -1,5 +0,0 @@
@@ -1,87 +0,0 @@
- <cell ref="u-index-item">
- class="u-index-item"
- :id="`u-index-item-${id}`"
- :class="[`u-index-item-${id}`]"
- </cell>
- // 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
- * IndexItem
- name: 'u-index-item',
- // 本组件到滚动条顶部的距离
- top: 0,
- height: 0,
- id: ''
- // 子组件u-index-anchor的实例
- this.anchor = {}
- this.getParentData('u-index-list')
- return uni.$u.error('u-index-item必须要搭配u-index-list组件使用')
- uni.$u.sleep().then(() =>{
- this.getIndexItemRect().then(size => {
- // 由于对象的引用特性,此处会同时生效到父组件的children数组的本实例的top属性中,供父组件判断读取
- this.top = Math.ceil(size.top)
- this.height = Math.ceil(size.height)
- getIndexItemRect() {
- this.$uGetRect('.u-index-item').then(size => {
- const ref = this.$refs['u-index-item']
- dom.getComponentRect(ref, res => {
- // 右边锚点非激活的颜色
- default: uni.$u.props.indexList.inactiveColor
- // 右边锚点激活的颜色
- default: uni.$u.props.indexList.activeColor
- // 索引字符列表,数组形式
- indexList: {
- default: uni.$u.props.indexList.indexList
- // 是否开启锚点自动吸顶
- sticky: {
- default: uni.$u.props.indexList.sticky
- // 自定义导航栏的高度
- customNavHeight: {
- default: uni.$u.props.indexList.customNavHeight
@@ -1,440 +1,315 @@
- <view class="u-index-list">
- <list
- :scrollTop="scrollTop"
- enable-back-to-top
- :offset-accuracy="1"
- maxHeight: $u.addUnit(scrollViewHeight)
- @scroll="scrollHandler"
- ref="uList"
- <cell
- v-if="$slots.header"
- ref="header"
- <slot name="header" />
+ <view class="u-index-bar">
- <cell v-if="$slots.footer">
- <slot name="footer" />
- </list>
- <view v-if="$slots.header">
+ <view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove"
+ @touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop">
+ <view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}"
+ :data-index="index">
- <view v-if="$slots.footer">
- class="u-index-list__letter"
- ref="u-index-list__letter"
- :style="{ top: $u.addUnit(letterInfo.top || 100) }"
- @touchstart="touchStart"
- @touchmove.stop.prevent="touchMove"
- @touchend.stop.prevent="touchEnd"
- @touchcancel.stop.prevent="touchEnd"
- class="u-index-list__letter__item"
- v-for="(item, index) in uIndexList"
- backgroundColor: activeIndex === index ? activeColor : 'transparent'
- class="u-index-list__letter__item__index"
- :style="{color: activeIndex === index ? '#fff' : inactiveColor}"
+ <view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{
+ zIndex: alertZIndex
+ <text>{{indexList[touchmoveIndex]}}</text>
- :show="touching"
- :customStyle="{
- right: '50px',
- top: $u.addUnit(indicatorTop),
- zIndex: 2
- class="u-index-list__indicator"
- :class="['u-index-list__indicator--show']"
- height: $u.addUnit(indicatorHeight),
- width: $u.addUnit(indicatorHeight)
- <text class="u-index-list__indicator__text">{{ uIndexList[activeIndex] }}</text>
- const indexList = () => {
- const indexList = [];
- const charCodeOfA = 'A'.charCodeAt(0);
- for (let i = 0; i < 26; i++) {
+ var indexList = function() {
+ var indexList = [];
+ var charCodeOfA = 'A'.charCodeAt(0);
+ for (var i = 0; i < 26; i++) {
indexList.push(String.fromCharCode(charCodeOfA + i));
return indexList;
- * IndexList 索引列表
- * @description 通过折叠面板收纳内容区域
- * @property {String} inactiveColor 右边锚点非激活的颜色 ( 默认 '#606266' )
- * @property {String} activeColor 右边锚点激活的颜色 ( 默认 '#5677fc' )
- * @property {Array} indexList 索引字符列表,数组形式
- * @property {Boolean} sticky 是否开启锚点自动吸顶 ( 默认 true )
- * @property {String | Number} customNavHeight 自定义导航栏的高度 ( 默认 0 )
+ * indexList 索引列表
+ * @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入
+ * @property {Array} index-list 索引字符列表,数组(默认A-Z)
+ * @property {Number String} z-index 锚点吸顶时的层级(默认965)
+ * @property {Boolean} sticky 是否开启锚点自动吸顶(默认true)
+ * @property {Number String} offset-top 锚点自动吸顶时与顶部的距离(默认0)
+ * @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff)
+ * @event {Function} select 选中右边索引字符时触发
+ * @example <u-index-list :scrollTop="scrollTop"></u-index-list>
- name: 'u-index-list',
- // 将自定义节点设置成虚拟的,更加接近Vue组件的表现,能更好的使用flex属性
- virtualHost: true
+ name: "u-index-list",
+ sticky: {
+ offsetTop: {
+ indexList: {
+ return indexList()
+ // #ifdef H5
+ this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44;
+ // #ifndef H5
+ this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0;
+ // 只能在created生命周期定义children,如果在data定义,会因为循环引用而报错
- // 当前正在被选中的字母索引
- activeIndex: -1,
- touchmoveIndex: 1,
- // 索引字母的信息
- letterInfo: {
- itemHeight: 0,
- top: 0
- // 设置字母指示器的高度,后面为了让指示器跟随字母,并将尖角部分指向字母的中部,需要依赖此值
- indicatorHeight: 50,
- // 字母放大指示器的top值,为了让其指向当前激活的字母
- // indicatorTop: 0
- // 当前是否正在被触摸状态
- touching: false,
- // 滚动条顶部top值
- scrollTop: 0,
- // scroll-view的高度
- scrollViewHeight: 0,
- // 系统信息
- sys: uni.$u.sys(),
- scrolling: false,
- // 如果有传入外部的indexList锚点数组则使用,否则使用内部生成A-Z字母
- uIndexList() {
- return this.indexList.length ? this.indexList : indexList()
- indicatorTop() {
- top,
- itemHeight
- } = this.letterInfo
- return Math.floor(top + itemHeight * this.activeIndex + itemHeight / 2 - this.indicatorHeight / 2)
+ activeAnchorIndex: 0,
+ showSidebar: true,
+ // children: [],
+ touchmove: false,
+ touchmoveIndex: 0,
watch: {
- // 监听字母索引的变化,重新设置尺寸
- uIndexList: {
- handler() {
- this.setIndexListLetterInfo()
+ scrollTop() {
+ this.updateData()
- this.anchors = []
+ // 弹出toast的z-index值
+ alertZIndex() {
+ return this.$u.zIndex.toast;
- // 设置列表的高度为整个屏幕的高度
- //减去this.customNavHeight,并将this.scrollViewHeight设置为maxHeight
- //解决当u-index-list组件放在tabbar页面时,scroll-view内容较少时,还能滚动
- this.scrollViewHeight = this.sys.windowHeight - this.customNavHeight
+ updateData() {
+ this.timer && clearTimeout(this.timer);
+ this.timer = setTimeout(() => {
+ this.showSidebar = !!this.children.length;
+ this.setRect().then(() => {
+ this.onScroll();
+ }, 0);
- // 索引列表被触摸
- touchStart(e) {
- // 获取触摸点信息
- const touchStart = e.changedTouches[0]
- if (!touchStart) return
- this.touching = true
- pageY
- } = touchStart
- // 根据当前触摸点的坐标,获取当前触摸的为第几个字母
- const currentIndex = this.getIndexListLetter(pageY)
- this.setValueForTouch(currentIndex)
+ setRect() {
+ return Promise.all([
+ this.setAnchorsRect(),
+ this.setListRect(),
+ this.setSiderbarRect()
+ ]);
- // 索引字母列表被触摸滑动中
- touchMove(e) {
- let touchMove = e.changedTouches[0]
- if (!touchMove) return;
- // 滑动结束后迅速开始第二次滑动时候 touching 为 false 造成不显示 indicator 问题
- if (!this.touching) {
- } = touchMove
+ setAnchorsRect() {
+ return Promise.all(this.children.map((anchor, index) => anchor
+ .$uGetRect('.u-index-anchor-wrapper')
+ .then((rect) => {
+ Object.assign(anchor, {
+ height: rect.height,
+ top: rect.top
+ })));
- // 触摸结束
- touchEnd(e) {
- // 延时一定时间后再隐藏指示器,为了让用户看的更直观,同时也是为了消除快速切换u-transition的show带来的影响
- uni.$u.sleep(300).then(() => {
- this.touching = false
+ setListRect() {
+ return this.$uGetRect('.u-index-bar').then((rect) => {
+ Object.assign(this, {
+ top: rect.top + this.scrollTop
- // 获取索引列表的尺寸以及单个字符的尺寸信息
- getIndexListLetterRect() {
- this.$uGetRect('.u-index-list__letter').then(size => {
- const ref = this.$refs['u-index-list__letter']
+ setSiderbarRect() {
+ return this.$uGetRect('.u-index-bar__sidebar').then(rect => {
+ this.sidebar = {
- // 设置indexList索引的尺寸信息
- setIndexListLetterInfo() {
- this.getIndexListLetterRect().then(size => {
- height
- } = size
- const sys = uni.$u.sys()
- const windowHeight = sys.windowHeight
- let customNavHeight = 0
- // 消除各端导航栏非原生和原生导致的差异,让索引列表字母对屏幕垂直居中
- if (this.customNavHeight == 0) {
- // #ifdef H5
- customNavHeight = sys.windowTop
- // #ifndef H5
- // 在非H5中,为原生导航栏,其高度不算在windowHeight内,这里设置为负值,后面相加时变成减去其高度的一半
- customNavHeight = -(sys.statusBarHeight + 44)
- customNavHeight = uni.$u.getPx(this.customNavHeight)
- this.letterInfo = {
- height,
- // 为了让字母列表对屏幕绝对居中,让其对导航栏进行修正,也即往上偏移导航栏的一半高度
- top: (windowHeight - height) / 2 + customNavHeight / 2,
- itemHeight: Math.floor(height / this.uIndexList.length)
+ getActiveAnchorIndex() {
+ const {
+ children
+ } = this;
+ sticky
+ for (let i = this.children.length - 1; i >= 0; i--) {
+ const preAnchorHeight = i > 0 ? children[i - 1].height : 0;
+ const reachTop = sticky ? preAnchorHeight : 0;
+ if (reachTop >= children[i].top) {
+ return i;
+ return -1;
- // 获取当前被触摸的索引字母
- getIndexListLetter(pageY) {
+ onScroll() {
const {
- // 对H5的pageY进行修正,这是由于uni-app自作多情在H5中将触摸点的坐标跟H5的导航栏结合导致的问题
- pageY += uni.$u.sys().windowTop
- // 对第一和最后一个字母做边界处理,因为用户可能在字母列表上触摸到两端的尽头后依然继续滑动
- if (pageY < top) {
- } else if (pageY >= top + height) {
- // 如果超出了,取最后一个字母
- return this.uIndexList.length - 1
- // 将触摸点的Y轴偏移值,减去索引字母的top值,除以每个字母的高度,即可得到当前触摸点落在哪个字母上
- return Math.floor((pageY - top) / itemHeight);
+ children = []
+ if (!children.length) {
+ sticky,
+ stickyOffsetTop,
+ zIndex,
+ scrollTop,
+ activeColor
+ const active = this.getActiveAnchorIndex();
+ this.activeAnchorIndex = active;
+ if (sticky) {
+ let isActiveAnchorSticky = false;
+ if (active !== -1) {
+ isActiveAnchorSticky =
+ children[active].top <= 0;
+ children.forEach((item, index) => {
+ if (index === active) {
+ let wrapperStyle = '';
+ let anchorStyle = {
+ color: `${activeColor}`
+ if (isActiveAnchorSticky) {
+ wrapperStyle = {
+ height: `${children[index].height}px`
+ anchorStyle = {
+ top: `${stickyOffsetTop}px`,
+ zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
+ item.active = active;
+ item.wrapperStyle = wrapperStyle;
+ item.anchorStyle = anchorStyle;
+ } else if (index === active - 1) {
+ const currentAnchor = children[index];
+ const currentOffsetTop = currentAnchor.top;
+ const targetOffsetTop = index === children.length - 1 ?
+ this.top :
+ children[index + 1].top;
+ const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
+ const translateY = parentOffsetHeight - currentAnchor.height;
+ const anchorStyle = {
+ position: 'relative',
+ transform: `translate3d(0, ${translateY}px, 0)`,
+ item.active = false;
+ item.anchorStyle = '';
+ item.wrapperStyle = '';
- // 设置各项由触摸而导致变化的值
- setValueForTouch(currentIndex) {
- // 如果偏移量太小,前后得出的会是同一个索引字母,为了防抖,进行返回
- if (currentIndex === this.activeIndex) return
- this.activeIndex = currentIndex
- // #ifndef APP-NVUE || MP-WEIXIN
- // 在非nvue中,由于anchor和item都在u-index-item中,所以需要对index-item进行偏移
- this.scrollIntoView = `u-index-item-${this.uIndexList[currentIndex].charCodeAt(0)}`
- // 微信小程序下,scroll-view的scroll-into-view属性无法对slot中的内容的id生效,只能通过设置scrollTop的形式去移动滚动条
- this.scrollTop = this.children[currentIndex].top
- // 在nvue中,由于cell和header为同级元素,所以实际是需要对header(anchor)进行偏移
- const anchor = `u-index-anchor-${this.uIndexList[currentIndex]}`
- dom.scrollToElement(this.anchors[currentIndex].$refs[anchor], {
- offset: 0,
- animated: false
+ onTouchMove(event) {
+ this.touchmove = true;
+ const sidebarLength = this.children.length;
+ const touch = event.touches[0];
+ const itemHeight = this.sidebar.height / sidebarLength;
+ let clientY = 0;
+ clientY = touch.clientY;
+ let index = Math.floor((clientY - this.sidebar.top) / itemHeight);
+ if (index < 0) {
+ index = 0;
+ } else if (index > sidebarLength - 1) {
+ index = sidebarLength - 1;
+ this.touchmoveIndex = index;
+ this.scrollToAnchor(index);
- getHeaderRect() {
- // 获取header slot的高度,因为list组件中获取元素的尺寸是没有top值的
- dom.getComponentRect(this.$refs.header, res => {
+ onTouchStop() {
+ this.touchmove = false;
+ this.scrollToAnchorIndex = null;
- // scroll-view的滚动事件
- async scrollHandler(e) {
- if (this.touching || this.scrolling) return
- // 每过一定时间取样一次,减少资源损耗以及可能带来的卡顿
- this.scrolling = true
- this.scrolling = false
- let scrollTop = 0
- const len = this.children.length
- let children = this.children
- const anchors = this.anchors
- // nvue下获取的滚动条偏移为负数,需要转为正数
- scrollTop = Math.abs(e.contentOffset.y)
- // 获取header slot的尺寸信息
- const header = await this.getHeaderRect()
- // item的top值,在nvue下,模拟出的anchor的top,类似非nvue下的index-item的top
- let top = header.height
- // 由于list组件无法获取cell的top值,这里通过header slot和各个item之间的height,模拟出类似非nvue下的位置信息
- children = this.children.map((item, index) => {
- const child = {
- height: item.height,
- top
- // 进行累加,给下一个item提供计算依据
- top += item.height + anchors[index].height
- return child
- // 非nvue通过detail获取滚动条位移
- scrollTop = e.detail.scrollTop
- for (let i = 0; i < len; i++) {
- const item = children[i],
- nextItem = children[i + 1]
- // 如果滚动条高度小于第一个item的top值,此时无需设置任意字母为高亮
- if (scrollTop <= children[0].top || scrollTop >= children[len - 1].top + children[len -
- 1].height) {
- this.activeIndex = -1
- break
- } else if (!nextItem) {
- // 当不存在下一个item时,意味着历遍到了最后一个
- this.activeIndex = len - 1
- } else if (scrollTop > item.top && scrollTop < nextItem.top) {
- this.activeIndex = i
+ scrollToAnchor(index) {
+ if (this.scrollToAnchorIndex === index) {
+ this.scrollToAnchorIndex = index;
+ const anchor = this.children.find((item) => item.index === this.indexList[index]);
+ if (anchor) {
+ this.$emit('select', anchor.index);
+ uni.pageScrollTo({
+ duration: 0,
+ scrollTop: anchor.top + this.scrollTop
- .u-index-list {
- &__letter {
- position: fixed;
- z-index: 3;
- padding: 0 6px;
- width: 16px;
- margin: 1px 0;
+ .u-index-bar {
+ position: relative
+ .u-index-bar__sidebar {
+ transform: translateY(-50%);
+ z-index: 99;
- &__index {
+ .u-index-bar__index {
+ padding: 8rpx 18rpx;
+ line-height: 1
- &__indicator {
- height: 50px;
- border-radius: 100px 100px 0 100px;
- background-color: #c9c9c9;
- transform: rotate(-45deg);
+ .u-indexed-list-alert {
+ width: 120rpx;
+ height: 120rpx;
+ right: 90rpx;
+ margin-top: -60rpx;
+ border-radius: 24rpx;
+ font-size: 50rpx;
+ background-color: rgba(0, 0, 0, 0.65);
+ z-index: 9999999;
- font-size: 28px;
- line-height: 28px;
+ .u-indexed-list-alert text {
@@ -1,187 +0,0 @@
- // 输入的值
- default: uni.$u.props.input.value
- // 输入框类型
- // number-数字输入键盘,app-vue下可以输入浮点数,app-nvue和小程序平台下只能输入整数
- // idcard-身份证输入键盘,微信、支付宝、百度、QQ小程序
- // digit-带小数点的数字键盘,App的nvue页面、微信、支付宝、百度、头条、QQ小程序
- // text-文本输入键盘
- default: uni.$u.props.input.type
- // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true,
- // 兼容性:微信小程序、百度小程序、字节跳动小程序、QQ小程序
- fixed: {
- default: uni.$u.props.input.fixed
- // 是否禁用输入框
- default: uni.$u.props.input.disabled
- // 禁用状态时的背景色
- disabledColor: {
- default: uni.$u.props.input.disabledColor
- // 是否显示清除控件
- clearable: {
- default: uni.$u.props.input.clearable
- // 是否密码类型
- password: {
- default: uni.$u.props.input.password
- // 最大输入长度,设置为 -1 的时候不限制最大长度
- default: uni.$u.props.input.maxlength
- // 输入框为空时的占位符
- placeholder: {
- default: uni.$u.props.input.placeholder
- // 指定placeholder的样式类,注意页面或组件的style中写了scoped时,需要在类名前写/deep/
- placeholderClass: {
- default: uni.$u.props.input.placeholderClass
- // 指定placeholder的样式
- placeholderStyle: {
- default: uni.$u.props.input.placeholderStyle
- // 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效
- showWordLimit: {
- default: uni.$u.props.input.showWordLimit
- // 设置右下角按钮的文字,有效值:send|search|next|go|done,兼容性详见uni-app文档
- // https://uniapp.dcloud.io/component/input
- // https://uniapp.dcloud.io/component/textarea
- confirmType: {
- default: uni.$u.props.input.confirmType
- // 点击键盘右下角按钮时是否保持键盘不收起,H5无效
- confirmHold: {
- default: uni.$u.props.input.confirmHold
- // focus时,点击页面的时候不收起键盘,微信小程序有效
- holdKeyboard: {
- default: uni.$u.props.input.holdKeyboard
- // 自动获取焦点
- // 在 H5 平台能否聚焦以及软键盘是否跟随弹出,取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点
- default: uni.$u.props.input.focus
- // 键盘收起时,是否自动失去焦点,目前仅App3.0.0+有效
- autoBlur: {
- default: uni.$u.props.input.autoBlur
- // 是否去掉 iOS 下的默认内边距,仅微信小程序,且type=textarea时有效
- disableDefaultPadding: {
- default: uni.$u.props.input.disableDefaultPadding
- // 指定focus时光标的位置
- cursor: {
- default: uni.$u.props.input.cursor
- // 输入框聚焦时底部与键盘的距离
- cursorSpacing: {
- default: uni.$u.props.input.cursorSpacing
- // 光标起始位置,自动聚集时有效,需与selection-end搭配使用
- selectionStart: {
- default: uni.$u.props.input.selectionStart
- // 光标结束位置,自动聚集时有效,需与selection-start搭配使用
- selectionEnd: {
- default: uni.$u.props.input.selectionEnd
- default: uni.$u.props.input.adjustPosition
- // 输入框内容对齐方式,可选值为:left|center|right
- inputAlign: {
- default: uni.$u.props.input.inputAlign
- // 输入框字体的大小
- default: uni.$u.props.input.fontSize
- // 输入框字体颜色
- default: uni.$u.props.input.color
- // 输入框前置图标
- prefixIcon: {
- default: uni.$u.props.input.prefixIcon
- // 前置图标样式,对象或字符串
- prefixIconStyle: {
- default: uni.$u.props.input.prefixIconStyle
- // 输入框后置图标
- suffixIcon: {
- default: uni.$u.props.input.suffixIcon
- // 后置图标样式,对象或字符串
- suffixIconStyle: {
- default: uni.$u.props.input.suffixIconStyle
- // 边框类型,surround-四周边框,bottom-底部边框,none-无边框
- default: uni.$u.props.input.border
- // 是否只读,与disabled不同之处在于disabled会置灰组件,而readonly则不会
- default: uni.$u.props.input.readonly
- // 输入框形状,circle-圆形,square-方形
- default: uni.$u.props.input.shape
- // 用于处理或者过滤输入框内容的方法
- default: uni.$u.props.input.formatter
- // 是否忽略组件内对文本合成系统事件的处理
- ignoreCompositionEvent: {
@@ -1,354 +1,394 @@
- <view class="u-input" :class="inputClass" :style="[wrapperStyle]">
- <view class="u-input__content">
- class="u-input__content__prefix-icon"
- v-if="prefixIcon || $slots.prefix"
- <slot name="prefix">
- :name="prefixIcon"
- :customStyle="prefixIconStyle"
- <view class="u-input__content__field-wrapper" @tap="clickHandler">
- <!-- 根据uni-app的input组件文档,H5和APP中只要声明了password参数(无论true还是false),type均失效,此时
- 为了防止type=number时,又存在password属性,type无效,此时需要设置password为undefined
- -->
- class="u-input__content__field-wrapper__field"
- :style="[inputStyle]"
- :value="innerValue"
- :auto-blur="autoBlur"
- :disabled="disabled || readonly"
- :placeholder-style="placeholderStyle"
- :placeholder-class="placeholderClass"
- :confirm-type="confirmType"
- :confirm-hold="confirmHold"
- :hold-keyboard="holdKeyboard"
- :cursor-spacing="cursorSpacing"
- :adjust-position="adjustPosition"
- :selection-end="selectionEnd"
- :selection-start="selectionStart"
- :password="password || type === 'password' || undefined"
- @input="onInput"
- @blur="onBlur"
- @focus="onFocus"
- @confirm="onConfirm"
- @keyboardheightchange="onkeyboardheightchange"
- class="u-input__content__clear"
- v-if="isShowClear"
- @tap="onClear"
- size="11"
- customStyle="line-height: 12px"
- class="u-input__content__subfix-icon"
- v-if="suffixIcon || $slots.suffix"
- <slot name="suffix">
- :name="suffixIcon"
- :customStyle="suffixIconStyle"
+ class="u-input"
+ 'u-input--border': border,
+ 'u-input--error': validateState
+ padding: `0 ${border ? 20 : 0}rpx`,
+ borderColor: borderColor,
+ textAlign: inputAlign
+ @tap.stop="inputClick"
+ <textarea
+ v-if="type == 'textarea'"
+ class="u-input__input u-input__textarea"
+ :style="[getStyle]"
+ :value="defaultValue"
+ :fixed="fixed"
+ :autoHeight="autoHeight"
+ :selection-end="uSelectionEnd"
+ :selection-start="uSelectionStart"
+ :cursor-spacing="getCursorSpacing"
+ :show-confirm-bar="showConfirmbar"
+ @input="handleInput"
+ @blur="handleBlur"
+ class="u-input__input"
+ :type="type == 'password' ? 'text' : type"
+ :password="type == 'password' && !showPassword"
+ :disabled="disabled || type === 'select'"
+ :adjust-position="adjustPosition"
+ <view class="u-input__right-icon u-flex">
+ <view class="u-input__right-icon__clear u-input__right-icon__item" @tap="onClear" v-if="clearable && value != '' && focused">
+ <u-icon size="32" name="close-circle-fill" color="#c0c4cc"/>
+ <view class="u-input__right-icon__clear u-input__right-icon__item" v-if="passwordIcon && type == 'password'">
+ <u-icon size="32" :name="!showPassword ? 'eye' : 'eye-fill'" color="#c0c4cc" @click="showPassword = !showPassword"/>
+ <view class="u-input__right-icon--select u-input__right-icon__item" v-if="type == 'select'" :class="{
+ 'u-input__right-icon--select--reverse': selectOpen
+ <u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
+import Emitter from '../../libs/util/emitter.js';
- * Input 输入框
- * @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
- * @tutorial https://uviewui.com/components/input.html
- * @property {String | Number} value 输入的值
- * @property {String} type 输入框类型,见上方说明 ( 默认 'text' )
- * @property {Boolean} fixed 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true,兼容性:微信小程序、百度小程序、字节跳动小程序、QQ小程序 ( 默认 false )
- * @property {Boolean} disabled 是否禁用输入框 ( 默认 false )
- * @property {String} disabledColor 禁用状态时的背景色( 默认 '#f5f7fa' )
- * @property {Boolean} clearable 是否显示清除控件 ( 默认 false )
- * @property {Boolean} password 是否密码类型 ( 默认 false )
- * @property {String | Number} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度 ( 默认 -1 )
- * @property {String} placeholder 输入框为空时的占位符
- * @property {String} placeholderClass 指定placeholder的样式类,注意页面或组件的style中写了scoped时,需要在类名前写/deep/ ( 默认 'input-placeholder' )
- * @property {String | Object} placeholderStyle 指定placeholder的样式,字符串/对象形式,如"color: red;"
- * @property {Boolean} showWordLimit 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效 ( 默认 false )
- * @property {String} confirmType 设置右下角按钮的文字,兼容性详见uni-app文档 ( 默认 'done' )
- * @property {Boolean} confirmHold 点击键盘右下角按钮时是否保持键盘不收起,H5无效 ( 默认 false )
- * @property {Boolean} holdKeyboard focus时,点击页面的时候不收起键盘,微信小程序有效 ( 默认 false )
- * @property {Boolean} focus 自动获取焦点,在 H5 平台能否聚焦以及软键盘是否跟随弹出,取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点 ( 默认 false )
- * @property {Boolean} autoBlur 键盘收起时,是否自动失去焦点,目前仅App3.0.0+有效 ( 默认 false )
- * @property {Boolean} disableDefaultPadding 是否去掉 iOS 下的默认内边距,仅微信小程序,且type=textarea时有效 ( 默认 false )
- * @property {String | Number} cursor 指定focus时光标的位置( 默认 -1 )
- * @property {String | Number} cursorSpacing 输入框聚焦时底部与键盘的距离 ( 默认 30 )
- * @property {String | Number} selectionStart 光标起始位置,自动聚集时有效,需与selection-end搭配使用 ( 默认 -1 )
- * @property {String | Number} selectionEnd 光标结束位置,自动聚集时有效,需与selection-start搭配使用 ( 默认 -1 )
- * @property {Boolean} adjustPosition 键盘弹起时,是否自动上推页面 ( 默认 true )
- * @property {String} inputAlign 输入框内容对齐方式( 默认 'left' )
- * @property {String | Number} fontSize 输入框字体的大小 ( 默认 '15px' )
- * @property {String} color 输入框字体颜色 ( 默认 '#303133' )
- * @property {Function} formatter 内容式化函数
- * @property {String} prefixIcon 输入框前置图标
- * @property {String | Object} prefixIconStyle 前置图标样式,对象或字符串
- * @property {String} suffixIcon 输入框后置图标
- * @property {String | Object} suffixIconStyle 后置图标样式,对象或字符串
- * @property {String} border 边框类型,surround-四周边框,bottom-底部边框,none-无边框 ( 默认 'surround' )
- * @property {Boolean} readonly 是否只读,与disabled不同之处在于disabled会置灰组件,而readonly则不会 ( 默认 false )
- * @property {String} shape 输入框形状,circle-圆形,square-方形 ( 默认 'square' )
- * @property {Boolean} ignoreCompositionEvent 是否忽略组件内对文本合成系统事件的处理。
- * @example <u-input v-model="value" :password="true" suffix-icon="lock-fill" />
+ * input 输入框
+ * @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
+ * @tutorial http://uviewui.com/components/input.html
+ * @property {String} type 模式选择,见官网说明
+ * @property {Boolean} clearable 是否显示右侧的清除图标(默认true)
+ * @property {} v-model 用于双向绑定输入框的值
+ * @property {String} input-align 输入框文字的对齐方式(默认left)
+ * @property {String} placeholder placeholder显示值(默认 '请输入内容')
+ * @property {Boolean} disabled 是否禁用输入框(默认false)
+ * @property {String Number} maxlength 输入框的最大可输入长度(默认140)
+ * @property {String Number} selection-start 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1)
+ * @property {String Number} maxlength 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1)
+ * @property {String Number} cursor-spacing 指定光标与键盘的距离,单位px(默认0)
+ * @property {String} placeholderStyle placeholder的样式,字符串形式,如"color: red;"(默认 "color: #c0c4cc;")
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type为text时生效(默认done)
+ * @property {Object} custom-style 自定义输入框的样式,对象形式
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} password-icon type为password时,是否显示右侧的密码查看图标(默认true)
+ * @property {Boolean} border 是否显示边框(默认false)
+ * @property {String} border-color 输入框的边框颜色(默认#dcdfe6)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Number} height 高度,单位rpx(text类型时为70,textarea时为100)
+ * @example <u-input v-model="value" :type="type" :border="border" />
- name: "u-input",
- // 输入框的值
- innerValue: "",
- // 是否处于获得焦点状态
- focused: false,
- // value是否第一次变化,在watch中,由于加入immediate属性,会在第一次触发,此时不应该认为value发生了变化
- firstChange: true,
- // value绑定值的变化是由内部还是外部引起的
- changeFromInner: false,
- innerFormatter: value => value
- handler(newVal, oldVal) {
- this.innerValue = newVal;
- /* #ifdef H5 */
- // 在H5中,外部value变化后,修改input中的值,不会触发@input事件,此时手动调用值变化方法
- this.firstChange === false &&
- this.changeFromInner === false
- this.valueChange();
- this.firstChange = false;
- // 重置changeFromInner的值为false,标识下一次引起默认为外部引起的
- this.changeFromInner = false;
- isShowClear() {
- const { clearable, readonly, focused, innerValue } = this;
- return !!clearable && !readonly && !!focused && innerValue !== "";
- // 组件的类名
- inputClass() {
- let classes = [],
- { border, disabled, shape } = this;
- border === "surround" &&
- (classes = classes.concat(["u-border", "u-input--radius"]));
- classes.push(`u-input--${shape}`);
- border === "bottom" &&
- (classes = classes.concat([
- "u-border-bottom",
- "u-input--no-radius",
- ]));
- return classes.join(" ");
- // 组件的样式
- wrapperStyle() {
- const style = {};
- // 禁用状态下,被背景色加上对应的样式
- if (this.disabled) {
- style.backgroundColor = this.disabledColor;
- // 无边框时,去除内边距
- if (this.border === "none") {
- style.padding = "0";
- // 由于uni-app的iOS开发者能力有限,导致需要分开写才有效
- style.paddingTop = "6px";
- style.paddingBottom = "6px";
- style.paddingLeft = "9px";
- style.paddingRight = "9px";
- // 输入框的样式
- inputStyle() {
- color: this.color,
- fontSize: uni.$u.addUnit(this.fontSize),
- textAlign: this.inputAlign
- // 当键盘输入时,触发input事件
- onInput(e) {
- let { value = "" } = e.detail || {};
- // 格式化过滤方法
- const formatValue = formatter(value)
- // 为了避免props的单向数据流特性,需要先将innerValue值设置为当前值,再在$nextTick中重新赋予设置后的值才有效
- this.innerValue = formatValue;
- // 输入框失去焦点时触发
- onBlur(event) {
- this.$emit("blur", event.detail.value);
- // H5端的blur会先于点击清除控件的点击click事件触发,导致focused
- // 瞬间为false,从而隐藏了清除控件而无法被点击到
- uni.$u.sleep(50).then(() => {
- this.focused = false;
- // 尝试调用u-form的验证方法
- uni.$u.formValidate(this, "blur");
- // 输入框聚焦时触发
- onFocus(event) {
- this.focused = true;
- this.$emit("focus");
- // 点击完成按钮时触发
- onConfirm(event) {
- this.$emit("confirm", this.innerValue);
- // 键盘高度发生变化的时候触发此事件
- // 兼容性:微信小程序2.7.0+、App 3.1.0+
- onkeyboardheightchange() {
- this.$emit("keyboardheightchange");
- // 内容发生变化,进行处理
- valueChange() {
- const value = this.innerValue;
- this.$emit("input", value);
- // 标识value值的变化是由内部引起的
- this.changeFromInner = true;
- this.$emit("change", value);
- uni.$u.formValidate(this, "change");
- // 点击清除控件
- onClear() {
- this.innerValue = "";
- this.$emit("clear");
- * 在安卓nvue上,事件无法冒泡
- * 在某些时间,我们希望监听u-from-item的点击事件,此时会导致点击u-form-item内的u-input后
- * 无法触发u-form-item的点击事件,这里通过手动调用u-form-item的方法进行触发
- if (uni.$u.os() === "android") {
- const formItem = uni.$u.$parent.call(this, "u-form-item");
- if (formItem) {
- formItem.clickHandler();
+ name: 'u-input',
+ // 输入框的类型,textarea,text,number
+ placeholder: {
+ default: '请输入内容'
+ placeholderStyle: {
+ default: 'color: #c0c4cc;'
+ // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
+ fixed: {
+ // 是否自动获得焦点
+ focus: {
+ // 密码类型时,是否显示右侧的密码图标
+ passwordIcon: {
+ // input|textarea是否显示边框
+ // 输入框的边框颜色
+ // type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态
+ // open-打开,close-关闭
+ selectOpen: {
+ // 高度,单位rpx
+ // 是否可清空
+ // 指定光标与键盘的距离,单位 px
+ cursorSpacing: {
+ // 光标起始位置,自动聚焦时有效,需与selection-end搭配使用
+ selectionStart: {
+ default: -1
+ // 光标结束位置,自动聚焦时有效,需与selection-start搭配使用
+ selectionEnd: {
+ // 是否显示键盘上方带有”完成“按钮那一栏
+ showConfirmbar:{
+ type:Boolean,
+ default:true
+ // 弹出键盘时是否自动调节高度,uni-app默认值是true
+ adjustPosition: {
+ defaultValue: this.value,
+ inputHeight: 70, // input的高度
+ textareaHeight: 100, // textarea的高度
+ validateState: false, // 当前input的验证状态,用于错误时,边框是否改为红色
+ focused: false, // 当前是否处于获得焦点的状态
+ showPassword: false, // 是否预览密码
+ lastValue: '', // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input时间
+ value(nVal, oVal) {
+ this.defaultValue = nVal;
+ // 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件
+ if(nVal != oVal && this.type == 'select') this.handleInput({
+ detail: {
+ value: nVal
+ return Number(this.maxlength);
+ getStyle() {
+ // 如果没有自定义高度,就根据type为input还是textare来分配一个默认的高度
+ style.minHeight = this.height ? this.height + 'rpx' : this.type == 'textarea' ?
+ this.textareaHeight + 'rpx' : this.inputHeight + 'rpx';
+ style = Object.assign(style, this.customStyle);
+ getCursorSpacing() {
+ return Number(this.cursorSpacing);
+ // 光标起始位置
+ uSelectionStart() {
+ return String(this.selectionStart);
+ // 光标结束位置
+ uSelectionEnd() {
+ return String(this.selectionEnd);
+ // 监听u-form-item发出的错误事件,将输入框边框变红色
+ this.$on('on-form-item-error', this.onFormItemError);
+ * change 事件
+ * @param event
+ handleInput(event) {
+ // vue 原生的方法 return 出去
+ // 当前model 赋值
+ this.defaultValue = value;
+ // 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
+ // 尚未更新到u-form-item,导致获取的值为空,从而校验混论
+ // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
+ // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+ // #ifdef MP-TOUTIAO
+ if(this.$u.trim(value) == this.lastValue) return ;
+ this.lastValue = value;
+ this.dispatch('u-form-item', 'on-form-change', value);
+ }, 40)
+ * blur 事件
+ handleBlur(event) {
+ this.$emit('blur', value);
+ this.dispatch('u-form-item', 'on-form-blur', value);
+ onFormItemError(status) {
+ this.validateState = status;
+ this.$emit('focus');
+ inputClick() {
.u-input {
- &--radius,
- border-radius: 4px;
+ &__input {
+ //height: $u-form-item-height;
- &--no-radius {
+ &__textarea {
+ padding: 10rpx 0;
+ line-height: normal;
+ &--border {
+ border-radius: 4px;
+ border: 1px solid $u-form-item-border-color;
+ border-color: $u-type-error!important;
- &__field-wrapper {
- margin: 0;
- &__field {
- line-height: 26px;
- height: 24px;
+ &__right-icon {
- &__clear {
- width: 20px;
- height: 20px;
- background-color: #c6c7cb;
- transform: scale(0.82);
- margin-left: 4px;
- &__subfix-icon {
+ &--select {
+ transition: transform .4s;
- &__prefix-icon {
- margin-right: 4px;
+ &--reverse {
+ transform: rotate(-180deg);