@@ -0,0 +1,9 @@
+const UNI_APP = {
+ APP_DEV_URL: 'http://192.168.11.77:6500',
+ APP_PROD_URL: 'http://192.168.11.77:6500',
+
+ AES_KEY: "675I3123J3Toq10L",
+ AES_IV: "AdC01PoIU3LnSh10",
+}
+module.exports = UNI_APP;
@@ -0,0 +1,26 @@
+{
+ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+ // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+ "version" : "0.0",
+ "configurations" : [
+ {
+ "app-plus" :
+ "launchtype" : "local"
+ },
+ "default" :
+ "mp-weixin" :
+ "type" : "uniCloud"
+ "playground" : "standard",
+ "type" : "uni-app:app-android"
+ }
+ ]
@@ -0,0 +1,27 @@
+<script>
+ // 燃气瓶app
+ export default {
+ onLaunch: function() {
+ var token = this.$cache.getToken()
+ if (token) {
+ uni.redirectTo({
+ url: '/pages/indexRouter'
+ })
+ console.log('App Launch')
+ onShow: function() {
+ console.log('App Show')
+ onHide: function() {
+ console.log('App Hide')
+</script>
+<style lang="scss">
+ /*每个页面公共css */
+ @import "uview-ui/index.scss";
+ @import "common/index.scss";
+ @import url(@/static/fonts/iconfont.css);
+</style>
@@ -0,0 +1,54 @@
+// 颜色
+$color-blue:#3387FF;
+$color-white:#FFFFFF;
+// 字体大小
+$font_size_34:34rpx;
+$font_size_36:36rpx;
+page{
+ background-color: #fff;
+.but_button690{
+ margin: 10rpx;
+ width: 690rpx;
+ height: 88rpx;
+ font-size: $font_size_34;
+ font-weight: normal;
+.but_button630{
+ width: 630rpx;
+.center_in_sequence{
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+.center_in{
+ width: 100%;
+ justify-content: center;
+.space_between{
+ justify-content: space-between;
+.up_and_down{
+.font36{
+ font-size: $font_size_36;
+ font-weight: bold;
+ text-align: left;
+ color: #333;
@@ -0,0 +1,115 @@
+<template>
+ <view>
+ <u-steps dot :current="stepList.length" direction="column">
+ <template v-for="(item,index) in stepList">
+ <view class="card_step">
+ <view class="card_time">{{item.optTime}}</view>
+ <u-steps-item :title="initDictvalue(optTypeList,item.optType)">
+ <template slot="desc">
+ <view class="title" v-if="item.optUserObj.nickName">
+ 操作人:{{item.optUserObj.nickName}}
+ </view>
+ <view class="title" v-if="item.objectCustomerObj.name">
+ 交互客户:{{item.objectCustomerObj.name}}
+ <view class="title" v-if="item.optCustomerObj.name">
+ 操作客户:{{item.optCustomerObj.name}}
+ <view class="title" v-if="item.currentEnterpriseObj.name">
+ 当前企业:{{item.currentEnterpriseObj.name}}
+ <view class="title" v-if="item.currentAddressObj.name">
+ 当前客户:{{item.currentAddressObj.name}}
+ <view class="title" v-if="item.currentTruckObj.name">
+ 当前司机:{{item.currentTruckObj.name}}
+ <view class="title" v-if="item.currentMotor.name">
+ 当前车辆:{{item.currentMotor.name}}
+ </template>
+ </u-steps-item>
+ </u-steps>
+</template>
+ import {
+ process
+ } from '@/static/js/blockSort.js'
+ props: {
+ stepList: {
+ type: Array,
+ default: []
+ data() {
+ return {
+ optTypeList: process(),
+ computed: {},
+ methods: {
+ // 普通类型文字匹配
+ initDictvalue(list, type) {
+ let name = ''
+ if (list) {
+ list.forEach(item => {
+ if (type === item.value) {
+ name = item.label
+ return name
+<style lang="scss" scoped>
+ ::v-deep .u-text__value {
+ font-size: 32rpx !important;
+ font-weight: bold !important;
+ ::v-deep .u-steps-item__wrapper {
+ border-radius: 50%;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, .12);
+ background-color: #3c9cff !important;
+ ::v-deep .u-steps-item__wrapper__dot {
+ background-color: #fff !important;
+ ::v-deep .u-steps-item__content {
+ margin-left: 20rpx !important;
+ .title_name {
+ font-size: 32rpx;
+ .title {
+ font-size: 26rpx;
+ color: #606266;
+ .card_step {
+ .card_time {
+ text-align: center;
+ width: 25%;
+ font-size: 25rpx;
+ color: #000;
@@ -0,0 +1,64 @@
+<!-- TreeItem.vue -->
+ <view class="item_tree" @click="toggleNode">
+ <view class="tree_title">
+ <view v-if="item.children.length > 0">
+ <u-icon name="arrow-right" v-if="!expanded"></u-icon>
+ <u-icon name="arrow-down" v-else></u-icon>
+ <view class="name_tree_item">{{ item.name }}</view>
+ <u-button type="primary" text="确定" @click="choice(item)"></u-button>
+ <view v-if="item.children && item.children.length && expanded">
+ <tree-item @confirm="confirm" v-for="child in item.children" :key="child.id" :item="child"></tree-item>
+ name: 'TreeItem',
+ props: ['item'],
+ expanded: false // 初始状态为折叠
+ };
+ toggleNode() {
+ this.expanded = !this.expanded; // 切换展开和折叠状态
+ choice(value) {
+ this.$emit('confirm', value.cmpCode)
+ confirm(cmpCode){
+ this.$emit('confirm', cmpCode)
+ .item_tree {
+ padding: 20rpx;
+ border-bottom: 1rpx solid #dfdfdf;
+ .tree_title {
+ padding-left: 20rpx;
+ .name_tree_item {
+ margin-left: 10rpx;
+ font-size: 30rpx;
@@ -0,0 +1,138 @@
+ <view class="card_order_management">
+ <view class="card_order" v-for="(item,index) in datalist" :key="index" v-if="datalist.length > 0">
+ <view @click="goDetails(item)">
+ <view class="space_between">
+ <view class="item_headline" v-if="item.goods && item.spec">{{item.goods.name}}{{item.spec.name}}
+ <view v-if="item.state == 5">
+ <view class="item_tag" v-if="item.customer.isSyncProv">
+ <u-tag text="已完善" plain size="mini" type="success"></u-tag>
+ <view class="item_tag" v-else>
+ <u-tag text="补全客户信息" plain size="mini" @click="goCompletionTasks(item)"></u-tag>
+ <view v-if="item.state == 3 || item.state == 4">
+ <view class="details_title" style="color: #5ac725;" v-if="item.state == 3">已送达</view>
+ <view class="details_title" style="color: #f56c6c;" v-else-if="item.state == 4">已取消</view>
+ <view class="item_num">×{{item.quantity}}</view>
+ <view class="item_address">配送地址:{{item.address}}</view>
+ <view class="item_address" v-if="item.state == 5">联系电话:{{item.phone}}
+ <u-icon color="#70B603" size="26" name="phone-fill" @click.native.stop="phone(item.phone)"></u-icon>
+ <view class="item_address">下单时间:{{item.orderTime}}</view>
+ <view class="item_address" v-if="item.state == 3">送达时间:{{item.arriveTime}}</view>
+ <view v-if="item.state == 2">
+ <u-button type="primary" @click="toDeliver(item)">去配送</u-button>
+ <view class="card_delivery" v-if="item.state == 5">
+ <view style="margin-right: 10rpx;flex: 1;">
+ <u-button type="warning" @click="goSecurityCheck(item)">入户安全检查</u-button>
+ <view style="margin-left: 10rpx;flex: 1;">
+ <u-button type="primary" @click="confirmedDelivery(item)">确认送达</u-button>
+ datalist: {
+ return {}
+ goDetails(value) {
+ uni.setStorageSync('detailsData', value);
+ uni.navigateTo({
+ url: '/pages/order/orderDetails'
+ });
+ // 入户安全检查
+ goSecurityCheck(row) {
+ url: '/pages/order/securityCheck?orderId=' + row.orderId + '&customerId=' + row.customerId
+ // 确认送达
+ confirmedDelivery(row) {
+ if (row.customer.isSyncProv) {
+ url: '/pages/order/delivery?id=' + '99' + '&title=' + '确定送达' + '&orderId=' + row.id
+ } else {
+ uni.$u.toast('请先完善客户信息')
+ // 补全用户信息
+ goCompletionTasks(row) {
+ url: '/pages/order/completionTasks?customerId=' + row.customerId
+ // 去配送
+ toDeliver(row) {
+ this.$emit('toDeliver', row)
+ phone(phone) {
+ console.log(phone, 66)
+ .card_order_management {
+ margin: 0rpx 20rpx;
+ .card_order {
+ width: calc(100% - 40rpx);
+ margin-top: 20rpx;
+ border: 1rpx solid rgba(215, 215, 215, 1);
+ border-radius: 8rpx;
+ padding: 30rpx 20rpx;
+ .item_headline {
+ font-size: 40rpx;
+ font-weight: 800;
+ .item_num {
+ margin: 20rpx 0rpx;
+ .details_title {
+ font-weight: 500;
+ .item_address {
+ color: #7f7f7f;
+ margin-bottom: 20rpx;
+ .card_delivery {
@@ -0,0 +1,261 @@
+ <u--form labelPosition="top" labelWidth="auto" :model="model" :rules="rules" ref="uForm">
+ <view v-for="(item,index) in list" :key="index">
+ <u-form-item :required="item.required ? true : false" :label="item.label" :prop="item.field"
+ v-if="item.type == 'input' && !item.visible">
+ <view class="card_form_item">
+ <u--input v-model="model[`${item.field}`]" :disabled="item.disabled ? true : false"
+ :placeholder="item.placeholder"></u--input>
+ </u-form-item>
+ v-else-if="item.type == 'radio' && !item.visible">
+ <view class="card_form_item" style="margin-top: 10rpx;">
+ <u-radio-group v-model="model[`${item.field}`]" placement="row">
+ <u-radio shape="circle" :customStyle="{marginRight: '20px'}"
+ v-for="(item1, index2) in item.options" :key="index2" :label="item1.label"
+ :name="item1.value"></u-radio>
+ </u-radio-group>
+ v-else-if="item.type == 'cascader' && !item.visible">
+ <u--input :placeholder="item.placeholder" suffixIcon="arrow-down"
+ v-model="model[`${item.field}`]" @focus="change"></u--input>
+ v-else-if="item.type == 'upload' && !item.visible">
+ <u-upload :fileList="fileList1" name="1" multiple :maxCount="10" @afterRead="afterRead"
+ @delete="deletePic"></u-upload>
+ v-else-if="item.type == 'textarea' && !item.visible">
+ <u--textarea v-model="model[`${item.field}`]" autoHeight
+ :placeholder="item.placeholder"></u--textarea>
+ v-else-if="item.type == 'line' && !item.visible">
+ <u-line></u-line>
+ </u--form>
+ <u-picker :show="show" ref="uPicker" :columns="addressColumns" :defaultIndex="areaDetault" keyName="label"
+ @confirm="confirm" @change="changeHandler" @cancel="cancel"></u-picker>
+ <view style="width: 100%;height: 170rpx;"></view>
+ <view class="check_btn">
+ <view class="btn_submit">
+ <u-button type="primary" text="提交" @click="clientele"></u-button>
+ const ENV = require('@/.env.js')
+ urbanArea
+ } from '@/static/js/districtCode.js'
+ name: 'x-form',
+ list: {
+ default () {
+ return []
+ model: {
+ type: Object,
+ rules: {
+ userType: {
+ type: Number,
+ return null
+ show: false,
+ areaData: urbanArea(), //原始数据
+ addressColumns: [], //数据数组
+ areaDetault: [], //默认选中数据
+ fileList1: [],
+ mounted() {},
+ change() {
+ // console.log(this.columns, 23)
+ this.show = true
+ this.addressColumns = [
+ this.areaData,
+ this.areaData[0].children,
+ ];
+ this.$nextTick(() => {
+ if (this.model.hr) {
+ this.setDefault()
+ confirm(value) {
+ this.show = false
+ this.$emit('confirm', value)
+ cancel() {
+ // 补全客户资料
+ clientele() {
+ this.$refs.uForm.validate().then(res => {
+ this.$emit('clientele')
+ }).catch(errors => {
+ // uni.$u.toast('校验失败')
+ // 新增图片
+ async afterRead(event) {
+ // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
+ let lists = [].concat(event.file)
+ let fileListLen = this[`fileList${event.name}`].length
+ lists.map((item) => {
+ this[`fileList${event.name}`].push({
+ ...item,
+ status: 'uploading',
+ message: '上传中'
+ for (let i = 0; i < lists.length; i++) {
+ const result = await this.uploadFilePromise(lists[i].url)
+ let item = this[`fileList${event.name}`][fileListLen]
+ this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
+ status: 'success',
+ message: '',
+ url: result
+ }))
+ fileListLen++
+ var arr = []
+ this.fileList1.forEach(item1 => {
+ arr.push(item1.url)
+ this.model.addressImg = arr.join()
+ this.$refs.uForm.validateField('addressImg')
+ // 删除图片
+ deletePic(event) {
+ this[`fileList${event.name}`].splice(event.index, 1)
+ uploadFilePromise(url) {
+ return new Promise((resolve, reject) => {
+ let a = uni.uploadFile({
+ url: ENV.APP_DEV_URL + '/api/upload', // 仅为示例,非真实的接口地址
+ filePath: url,
+ name: 'file',
+ // formData: {
+ // user: 'test'
+ // },
+ header: {
+ 'Authorization': 'Bearer ' + uni.getStorageSync('access_token'),
+ success: (res) => {
+ let state = JSON.parse(res.data)
+ setTimeout(() => {
+ if (state.code == 200) {
+ resolve(state.data)
+ }, 1000)
+ // 根据回显数据设置默认选中项
+ setDefault() {
+ // 户籍所在地
+ let temp = []
+ temp = this.model.hr.split('/')
+ let index, index1 = 0
+ let arr, arr1 = []
+ //市区对应每列的第几个
+ this.areaData.forEach((item, i) => {
+ if (item.value == temp[0]) {
+ index = i
+ item.children.forEach((val, ind) => {
+ if (val.value == temp[1]) {
+ index1 = ind
+ //匹配省市区对应每列的第几个
+ arr = this.areaData.map(e => {
+ return e.label
+ arr1 = this.areaData[index].children.map(e => {
+ this.addressColumns = [arr, arr1]
+ this.areaDetault = [index, index1]
+ // 选中时执行
+ changeHandler(e) {
+ const {
+ columnIndex,
+ index,
+ indexs,
+ picker = this.$refs.uPicker
+ } = e
+ if (columnIndex === 0) {
+ const children1 = this.areaData[index].children.map(e => {
+ picker.setColumnValues(1, children1)
+ .card_form_item {
+ .check_btn {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-top: 1rpx solid #f7f7f7;
+ padding-bottom: constant(safe-area-inset-bottom); //兼容 IOS<11.2
+ padding-bottom: env(safe-area-inset-bottom); //兼容 IOS>11.2
+ .btn_submit {
+ margin-top: 10rpx;
+ padding-bottom: 20rpx;
+ width: calc(100% - 80rpx);
@@ -0,0 +1,119 @@
+ <!-- 底部tabbar -->
+ <view class="navmainbox">
+ <view class="navigation">
+ <view class="navcont" v-for="(item,index) in dataList" :key="index" @click="gonavs(item,index)">
+ <span class="iconfont icon_image" :class="[item.icon,nowchos == index ? 'activeName' :'']"></span>
+ <view class="navchosbox" :class="nowchos == index ? 'activeName' :''">
+ {{item.title}}
+ <!-- //防止 固定的底部菜单遮挡页面上的内容 -->
+ <view class="navmainbox" style="position: relative;opacity: 0;z-index: 0;">
+ name: 'x-navbottom',
+ nowchos: {
+ return 0
+ dataList: [],
+ list: [{
+ icon: 'icon-shouye',
+ title: '首页',
+ }, {
+ icon: 'icon-dingdan',
+ title: '订单',
+ icon: 'icon-a-xiaoxi',
+ title: '消息',
+ icon: 'icon-wode',
+ title: '我的',
+ }],
+ gasList: [{
+ mounted() {
+ var userInfo = this.$cache.getCache('userInfo')
+ if(userInfo.provUser){
+ if (userInfo.provUser.isorders == 0 && userInfo.provUser.userType == 3) {
+ this.dataList = this.list
+ this.dataList = this.gasList
+ }else{
+ gonavs(e, cindex) {
+ this.$emit('botomchos', cindex)
+ .navmainbox {
+ z-index: 5;
+ background-color: #ffffff;
+ box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
+ height: 100rpx;
+ .navigation {
+ z-index: 2024;
+ position: relative;
+ padding-bottom: var(--window-bottom);
+ .navigation .navcont {
+ flex: 1;
+ .navchosbox {
+ color: #727289 !important;
+ margin-top: 2rpx;
+ .activeName {
+ color: #3E80FF !important;
+ .icon_image {
+ font-size: 44rpx;
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <script>
+ var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+ CSS.supports('top: constant(a)'))
+ document.write(
+ '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+ (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+ </script>
+ <title></title>
+ <!--preload-links-->
+ <!--app-context-->
+ </head>
+ <body>
+ <div id="app"><!--app-html--></div>
+ <script type="module" src="/main.js"></script>
+ </body>
+</html>
@@ -0,0 +1,36 @@
+import Vue from 'vue'
+import App from './App'
+// 引入全局uView
+import uView from 'uview-ui';
+Vue.use(uView);
+import request from './utils/request.js'
+import Storage from './store/storage.js'
+Vue.prototype.$api = request
+Vue.prototype.$cache = Storage
+// 图片拼接地址
+Vue.prototype.$baseUrl = 'https://bzdcdn.baozhida.cn/';
+// #ifndef VUE3
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+ ...App
+})
+app.$mount()
+// #endif
+// #ifdef VUE3
+import {
+ createSSRApp
+} from 'vue'
+export function createApp() {
+ const app = createSSRApp(App)
+ app
@@ -0,0 +1,86 @@
+ "name" : "气瓶安全追溯",
+ "appid" : "__UNI__10C0A5F",
+ "description" : "",
+ "versionName" : "1.0.0",
+ "versionCode" : "100",
+ "transformPx" : false,
+ /* 5+App特有相关 */
+ "app-plus" : {
+ "usingComponents" : true,
+ "nvueStyleCompiler" : "uni-app",
+ "compilerVersion" : 3,
+ "splashscreen" : {
+ "alwaysShowBeforeRender" : true,
+ "waiting" : true,
+ "autoclose" : true,
+ "delay" : 0
+ /* 模块配置 */
+ "modules" : {},
+ /* 应用发布信息 */
+ "distribute" : {
+ /* android打包配置 */
+ "android" : {
+ "permissions" : [
+ "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+ "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+ "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+ "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+ "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+ "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+ "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+ "<uses-feature android:name=\"android.hardware.camera\"/>",
+ "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+ /* ios打包配置 */
+ "ios" : {},
+ /* SDK配置 */
+ "sdkConfigs" : {
+ "maps" : {
+ "amap" : {
+ "name" : "",
+ "appkey_ios" : "",
+ "appkey_android" : ""
+ /* 快应用特有相关 */
+ "quickapp" : {},
+ /* 小程序特有相关 */
+ "mp-weixin" : {
+ "appid" : "wx7d2f59a537c6ffdd",
+ "setting" : {
+ "urlCheck" : false
+ "permission" : {
+ "scope.userLocation" : {
+ "desc" : "你的位置信息将用于小程序位置接口的效果展示"
+ "lazyCodeLoading" : "requiredComponents"
+ "mp-alipay" : {
+ "usingComponents" : true
+ "mp-baidu" : {
+ "mp-toutiao" : {
+ "uniStatistics" : {
+ "enable" : false
+ "vueVersion" : "2"
@@ -0,0 +1,15 @@
+ "name": "baozhida-gas-cylinder",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "node_modules/uview-ui": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmmirror.com/uview-ui/-/uview-ui-2.0.36.tgz",
+ "integrity": "sha512-ASSZT6M8w3GTO1eFPbsgEFV0U5UujK+8pTNr+MSUbRNcRMC1u63DDTLJVeArV91kWM0bfAexK3SK9pnTqF9TtA==",
+ "engines": {
+ "HBuilderX": "^3.1.0"
@@ -0,0 +1,21 @@
+MIT License
+Copyright (c) 2023 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.
@@ -0,0 +1,66 @@
+<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">多平台快速开发的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生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
+## [官方文档:https://uviewui.com](https://uviewui.com)
+## 预览
+您可以通过**微信**扫码,查看最佳的演示效果。
+<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
+> 我们非常乐意接受各位的优质PR,但在此之前我希望您了解uView2.0是一个需要兼容多个平台的(小程序、h5、ios app、android app)包括nvue页面、vue页面。
+> 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢!
+## 安装
+#### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?id=1593](https://ext.dcloud.net.cn/plugin?id=1593)
+请通过[官网安装文档](https://www.uviewui.com/components/install.html)了解更详细的内容
+## 快速上手
+请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
+## 使用方法
+配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
+```html
+ <u-button text="按钮"></u-button>
+```
+## 版权信息
+uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。
@@ -0,0 +1,362 @@
+## 2.0.36(2023-03-27)
+# uView2.0重磅发布,利剑出鞘,一统江湖
+1. 重构`deepClone` & `deepMerge`方法
+2. 其他优化
+## 2.0.34(2022-09-24)
+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)
@@ -0,0 +1,78 @@
+ <uvForm
+ ref="uForm"
+ :model="model"
+ :rules="rules"
+ :errorType="errorType"
+ :borderBottom="borderBottom"
+ :labelPosition="labelPosition"
+ :labelWidth="labelWidth"
+ :labelAlign="labelAlign"
+ :labelStyle="labelStyle"
+ :customStyle="customStyle"
+ >
+ <slot />
+ </uvForm>
+ /**
+ * 此组件存在的理由是,在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'
+ // #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 = []
+ // 手动设置校验的规则,如果规则中有函数的话,微信小程序中会过滤掉,所以只能手动调用设置规则
+ 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
@@ -0,0 +1,47 @@
+ <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 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
@@ -0,0 +1,73 @@
+ <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
@@ -0,0 +1,44 @@
+ <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,
+};
@@ -0,0 +1,48 @@
+ <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
+ // 操作菜单是否展示 (默认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: {
+ 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
@@ -0,0 +1,278 @@
+ <u-popup
+ mode="bottom"
+ @close="closeHandler"
+ :safeAreaInsetBottom="safeAreaInsetBottom"
+ :round="round"
+ <view class="u-action-sheet">
+ <view
+ class="u-action-sheet__header"
+ v-if="title"
+ <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>
+ <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>
+ </u-popup>
+ import openType from '../../libs/mixin/openType'
+ import button from '../../libs/mixin/button'
+ import props from './props.js';
+ * 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>
+ name: "u-action-sheet",
+ // 一些props参数和methods方法,通过mixin混入,因为其他文件也会用到
+ mixins: [openType, button, uni.$u.mixin, props],
+ computed: {
+ // 操作项目的样式
+ 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.actions[index].disabled) style.color = '#c0c4cc'
+ return style;
+ closeHandler() {
+ // 允许点击遮罩关闭时,才发出close事件
+ if(this.closeOnClickOverlay) {
+ this.$emit('close')
+ // 点击取消按钮
+ selectHandler(index) {
+ const item = this.actions[index]
+ if (item && !item.disabled && !item.loading) {
+ this.$emit('select', item)
+ if (this.closeOnClickAction) {
+ @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;
+ .u-reset-button {
+ width: $u-action-sheet-reset-button-width;
+ .u-action-sheet {
+ &__header {
+ padding: $u-action-sheet-title-padding;
+ &__title {
+ font-size: $u-action-sheet-title-font-size;
+ color: $u-action-sheet-title-color;
+ &__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;
+ &__name {
+ font-size: $u-action-sheet-item-wrap-name-font-size;
+ color: $u-main-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;
+ &__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;
+ &--hover {
+ background-color: $u-action-sheet-cancel-text-hover-background-color;
@@ -0,0 +1,59 @@
+ // 图片地址,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
@@ -0,0 +1,259 @@
+ <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')
+ * 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()
+ 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
+ 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,
+ // 判断图片横向还是竖向展示方式
+ 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
+@import '../../libs/css/components.scss';
+.u-album {
+ @include flex(column);
+ &__row {
+ @include flex(row);
+ flex-wrap: wrap;
+ &__wrapper {
+ &__text {
+ top: 0;
+ background-color: rgba(0, 0, 0, 0.3);
+ // 显示文字
+ 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
@@ -0,0 +1,243 @@
+ <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')
+ // 点击关闭按钮
+ .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 {
+ 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,52 @@
+ // 头像图片组
+ 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
@@ -0,0 +1,103 @@
+ <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: {
+ default: uni.$u.props.backtop.iconStyle
@@ -0,0 +1,129 @@
+ :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>
+ <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,
+ show() {
+ return uni.$u.getPx(this.scrollTop) > uni.$u.getPx(this.top)
+ contentStyle() {
+ const style = {}
+ let radius = 0
+ // 是否圆形
+ if(this.mode === 'circle') {
+ radius = '100px'
+ radius = '4px'
+ // 为了兼容安卓nvue,只能这么分开写
+ style.borderTopLeftRadius = radius
+ style.borderTopRightRadius = radius
+ style.borderBottomLeftRadius = radius
+ style.borderBottomRightRadius = radius
+ return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
+ 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 {
+ flex:$u-back-top-flex;
+ height: $u-back-top-height;
+ background-color: $u-back-top-background-color;
+ &__tips {
+ font-size:$u-back-top-tips-font-size;
+ transform: scale(0.8);
@@ -0,0 +1,72 @@
+ // 是否显示圆点
+ 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
@@ -0,0 +1,171 @@
+ 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>
+ * 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>
+ name: 'u-badge',
+ // 是否将badge中心与父组件右上角重合
+ boxStyle() {
+ // 整个组件的样式
+ badgeStyle() {
+ if(this.color) {
+ style.color = this.color
+ if (this.bgColor && !this.inverted) {
+ style.backgroundColor = this.bgColor
+ 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)
+ 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)
+ $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;
+ &--inverted {
+ font-size: 13px;
+ &--not-dot {
+ padding: $u-badge-text-padding;
+ &--horn {
+ border-bottom-left-radius: 0;
+ &--primary {
+ background-color: $u-badge-primary;
+ &--primary--inverted {
+ color: $u-badge-primary;
+ &--error {
+ background-color: $u-badge-error;
+ &--error--inverted {
+ color: $u-badge-error;
+ &--success {
+ background-color: $u-badge-success;
+ &--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;
@@ -0,0 +1,46 @@
+$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;
@@ -0,0 +1,161 @@
+/*
+ * @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
@@ -0,0 +1,490 @@
+ <!-- #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,
+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 成功的回调
+ * @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() {
+ 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);
+@import "../../libs/css/components.scss";
+/* #ifndef APP-NVUE */
+@import "./vue.scss";
+/* #endif */
+/* #ifdef APP-NVUE */
+@import "./nvue.scss";
+$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;
+ height: $u-button-u-button-height;
+ /* #ifndef APP-NVUE */
+ box-sizing: border-box;
+ /* #endif */
+ flex-direction: row;
+ font-size: $u-button-text-font-size;
+ font-size: $u-button-loading-text-font-size;
+ margin-left: $u-button-loading-text-margin-left;
+ &--large {
+ width: $u-button-large-width;
+ height: $u-button-large-height;
+ padding: $u-button-large-padding;
+ &--normal {
+ padding: $u-button-normal-padding;
+ font-size: $u-button-normal-font-size;
+ &--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;
+ &--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;
+ &--disabled {
+ opacity: $u-button-disabled-opacity;
+ 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;
+ 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;
+ 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;
+ 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;
+ 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;
+ &--block {
+ width: $u-button-block-width;
+ &--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;
+ &--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;
+ min-width: $u-button-icon-min-width;
+ line-height: inherit !important;
+ vertical-align: top;
+ &--plain {
+ background-color: $u-button-plain-background-color;
+ &--hairline {
+ border-width: $u-button-hairline-border-width !important;
@@ -0,0 +1,80 @@
+// 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;
+ 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;
@@ -0,0 +1,99 @@
+ <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 {
+ &__weekday {
+ line-height: 30px;
@@ -0,0 +1,579 @@
+ <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
+ this.init()
+ init() {
+ // 初始化默认选中
+ this.$emit('monthSelected', this.selected)
+ // 这里需要另一个延时,因为获取宽度后,会进行月份数据渲染,只有渲染完成之后,才有真正的高度
+ // 因为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;
@@ -0,0 +1,144 @@
+ // 日历顶部标题
+ 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
@@ -0,0 +1,384 @@
+ closeable
+ @close="close"
+ :closeOnClickOverlay="closeOnClickOverlay"
+ <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>
+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()
+ // 打开弹窗时,设置月份数据
+ // 由于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
+ return [this.innerMinDate, this.innerMaxDate, this.defaultDate]
+ subtitle() {
+ // 初始化时,this.months为空数组,所以需要特别判断处理
+ if (this.months.length) {
+ return `${this.months[this.monthIndex].year}年${
+ this.months[this.monthIndex].month
+ }月`
+ return ''
+ buttonDisabled() {
+ // 如果为range类型,且选择的日期个数不足1个时,让底部的按钮出于disabled状态
+ if (this.selected.length <= 1) {
+ return true
+ return false
+ 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)
+ // 校验maxDate,不能小于minDate
+ this.innerMaxDate &&
+ this.innerMinDate &&
+ new Date(this.innerMaxDate).getTime() < new Date(this.innerMinDate).getTime()
+ return uni.$u.error('maxDate不能小于minDate')
+ // 滚动区域的高度
+ this.listHeight = this.rowHeight * 5 + 30
+ close() {
+ // 点击确定按钮
+ confirm() {
+ if (!this.buttonDisabled) {
+ // 获得两个日期之间的月份数
+ 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()
+ // 滚动到默认设置的月份
+ 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;
+ // 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
+ // 更新月份的top值
+ updateMonthTop(topArr = []) {
+ // 设置对应月份的top值,用于onScroll方法更新月份
+ topArr.map((item, index) => {
+ this.months[index].top = item
+ // 获取默认日期的下标
+ const selected = dayjs().format("YYYY-MM")
+ this.scrollIntoDefaultMonth(selected)
+ let selected = dayjs().format("YYYY-MM");
+ selected = dayjs(this.defaultDate).format("YYYY-MM")
+ selected = dayjs(this.defaultDate[0]).format("YYYY-MM");
+.u-calendar {
+ &__confirm {
+ padding: 7px 18px;
@@ -0,0 +1,85 @@
+ // 月初是周几
+ 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) {
+ IDayCn,
+ IMonthCn
+ } = this.getLunar(e.date)
+ e.lunar = IDayCn == '初一' ? IMonthCn : IDayCn
+ ...e,
+ ...custom
@@ -0,0 +1,14 @@
+ // 是否打乱键盘按键的顺序
+ random: {
+ // 输入一个中文后,是否自动切换到英文
+ autoChange: {
@@ -0,0 +1,311 @@
+ 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 {
+ // 分组标题
+ default: uni.$u.props.cellGroup.title
+ // 是否显示外边框
+ border: {
+ default: uni.$u.props.cellGroup.border
@@ -0,0 +1,61 @@
+ <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>
+ * cellGroup 单元格
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。
+ * @tutorial https://uviewui.com/components/cell.html
+ * @property {String} title 分组标题
+ * @property {Boolean} border 是否显示外边框 (默认 true )
+ * @event {Function} click 点击cell列表时触发
+ * @example <u-cell-group title="设置喜好">
+ name: 'u-cell-group',
+ $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-group {
+ padding: $u-cell-group-title-padding;
+ font-size: $u-cell-group-title-font-size;
+ line-height: $u-cell-group-title-line-height;
+ color: $u-cell-group-title-color;
@@ -0,0 +1,110 @@
+ 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
@@ -0,0 +1,229 @@
+ <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 {
@@ -0,0 +1,82 @@
+ // 标识符
+ 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
+ class="u-checkbox-group"
+ <slot></slot>
+ * checkboxGroup 复选框组
+ * @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绑定的值时触发,回调为一个对象
+ * @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
+ return this.bem('checkbox-group', ['placement'])
+ // 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
+ if (this.children.length) {
+ this.children.map(child => {
+ // 判断子组件(u-checkbox)如果有init方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+ typeof(child.init) === 'function' && child.init()
+ // 将其他的checkbox设置为未选中的状态
+ unCheckedOther(childInstance) {
+ const values = []
+ // 将被选中的checkbox,放到数组中返回
+ if (child.isChecked) {
+ values.push(child.name)
+ // 发出事件
+ this.$emit('change', values)
+ // 修改通过v-model绑定的值
+ this.$emit('input', values)
+ .u-checkbox-group {
+ &--row {
+ &--column {
@@ -0,0 +1,69 @@
+ // 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
@@ -0,0 +1,344 @@
+ 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"
+ @tap.stop="labelClickHandler"
+ color: elDisabled ? elInactiveColor : elLabelColor,
+ fontSize: elLabelSize,
+ lineHeight: elLabelSize
+ >{{label}}</text>
+ * checkbox 复选框
+ * @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 是否禁止点击提示语选中复选框
+ * @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
+ name: "u-checkbox",
+ 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'
+ // 是否禁用,如果父组件u-raios-group禁用的话,将会忽略子组件的配置
+ elDisabled() {
+ return this.disabled !== '' ? this.disabled : this.parentData.disabled !== null ? this.parentData.disabled : false;
+ // 是否禁用label点击
+ elLabelDisabled() {
+ return this.labelDisabled !== '' ? this.labelDisabled : this.parentData.labelDisabled !== null ? this.parentData.labelDisabled :
+ false;
+ // 组件尺寸,对应size的值,默认值为21px
+ elSize() {
+ return this.size ? this.size : (this.parentData.size ? this.parentData.size : 21);
+ // 组件的勾选图标的尺寸,默认12px
+ elIconSize() {
+ return this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 12);
+ // 组件选中激活时的颜色
+ elActiveColor() {
+ return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : '#2979ff');
+ // 组件选未中激活时的颜色
+ elInactiveColor() {
+ return this.inactiveColor ? this.inactiveColor : (this.parentData.inactiveColor ? this.parentData.inactiveColor :
+ '#c8c9cc');
+ elLabelColor() {
+ return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : '#606266')
+ // 组件的形状
+ elShape() {
+ return this.shape ? this.shape : (this.parentData.shape ? this.parentData.shape : 'circle');
+ // label大小
+ elLabelSize() {
+ return uni.$u.addUnit(this.labelSize ? this.labelSize : (this.parentData.labelSize ? this.parentData.labelSize :
+ '15'))
+ elIconColor() {
+ const iconColor = this.iconColor ? this.iconColor : (this.parentData.iconColor ? this.parentData.iconColor :
+ '#ffffff');
+ // 图标的颜色
+ if (this.elDisabled) {
+ // disabled状态下,已勾选的checkbox图标改为elInactiveColor
+ return this.isChecked ? this.elInactiveColor : 'transparent'
+ return this.isChecked ? iconColor : 'transparent'
+ 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
+ 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') {
+ checkboxStyle() {
+ if (this.parentData.borderBottom && this.parentData.placement === 'row') {
+ uni.$u.error('检测到您将borderBottom设置为true,需要同时将u-checkbox-group的placement设置为column才有效')
+ // 当父组件设置了显示下边框并且排列形式为纵向时,给内容和边框之间加上一定间隔
+ if (this.parentData.borderBottom && this.parentData.placement === 'column') {
+ style.paddingBottom = '8px'
+ // 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
+ this.updateParentData()
+ if (!this.parent) {
+ uni.$u.error('u-checkbox必须搭配u-checkbox-group组件使用')
+ // 设置初始化时,是否默认选中的状态,父组件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) {
+ emitEvent() {
+ this.$emit('change', this.isChecked)
+ // 尝试调用u-form的验证方法,进行一定延迟,否则微信小程序更新可能会不及时
+ uni.$u.formValidate(this, 'change')
+ // 改变组件选中状态
+ // 这里的改变的依据是,更改本组件的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
+ $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 {
+ overflow: hidden;
+ &-label--left {
+ flex-direction: row
+ &-label--right {
+ flex-direction: row-reverse;
+ justify-content: space-between
+ // nvue下,border-color过渡有问题
+ transition-property: border-color, background-color, color;
+ transition-duration: 0.2s;
+ 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;
+ /* #ifdef MP-TOUTIAO */
+ // 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
+ line-height: $u-checkbox-icon-wrap-icon-line-height;
+ border-radius: $u-checkbox-icon-wrap-circle-border-radius;
+ border-radius: $u-checkbox-icon-wrap-square-border-radius;
+ &--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;
+ background-color: $u-checkbox-icon-wrap-disabled-background-color !important;
+ &--disabled--checked {
+ color: $u-checkbox-icon-wrap-disabled-checked-color !important;
+ 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;
+ color: $u-checkbox-label-disabled-color;
@@ -0,0 +1,8 @@
+ percentage: {
+ default: uni.$u.props.circleProgress.percentage
@@ -0,0 +1,198 @@
+ <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">
+ 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)',
+ leftSyle() {
+ style.borderTopColor = this.leftBorderColor
+ style.borderRightColor = this.leftBorderColor
+ rightSyle() {
+ style.borderLeftColor = this.rightBorderColor
+ style.borderBottomColor = this.rightBorderColor
+ uni.$u.sleep().then(() => {
+ this.rightBorderColor = 'rgb(66, 185, 131)'
+ // this.init()
+ 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,
+ // })
+ .u-circle-progress {
+ height: 100px;
+ width: 100px;
+ // transform: rotate(0deg);
+ // background-color: rgb(66, 185, 131);
+ background-color: rgb(200, 200, 200);
+ &__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;
@@ -0,0 +1,79 @@
+ // 键盘弹起时,是否自动上推页面
+ 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
@@ -0,0 +1,252 @@
+ <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的样式
+ 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;
+ @keyframes u-cursor-flicker {
+ 0% {
+ opacity: 0;
+ 50% {
+ opacity: 1;
+ 100% {
@@ -0,0 +1,34 @@
+ // 倒计时总秒数
+ 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
+ <view class="u-code">
+ <!-- 此组件功能由js完成,无需写html逻辑 -->
+ * Code 验证码输入框
+ * @description 考虑到用户实际发送验证码的场景,可能是一个按钮,也可能是一段文字,提示语各有不同,所以本组件 不提供界面显示,只提供提示语,由用户将提示语嵌入到具体的场景
+ * @tutorial https://www.uviewui.com/components/code.html
+ * @property {String | Number} seconds 倒计时所需的秒数(默认 60 )
+ * @property {String} startText 开始前的提示语,见官网说明(默认 '获取验证码' )
+ * @property {String} changeText 倒计时期间的提示语,必须带有字母"x",见官网说明(默认 'X秒重新获取' )
+ * @property {String} endText 倒计结束的提示语,见官网说明(默认 '重新获取' )
+ * @property {Boolean} keepRunning 是否在H5刷新或各端返回再进入时继续倒计时( 默认false )
+ * @property {String} uniqueKey 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
+ * @event {Function} change 倒计时期间,每秒触发一次
+ * @event {Function} start 开始倒计时触发
+ * @event {Function} end 结束倒计时触发
+ * @example <u-code ref="uCode" @change="codeChange" seconds="20"></u-code>
+ name: "u-code",
+ secNum: this.seconds,
+ timer: null,
+ canGetCode: true, // 是否可以执行验证码操作
+ this.checkKeepRunning()
+ this.secNum = n
+ checkKeepRunning() {
+ // 获取上一次退出页面(H5还包括刷新)时的时间戳,如果没有上次的保存,此值可能为空
+ let lastTimestamp = Number(uni.getStorageSync(this.uniqueKey + '_$uCountDownTimestamp'))
+ if(!lastTimestamp) return this.changeEvent(this.startText)
+ // 当前秒的时间戳
+ let nowTimestamp = Math.floor((+ new Date()) / 1000)
+ // 判断当前的时间戳,是否小于上一次的本该按设定结束,却提前结束的时间戳
+ if(this.keepRunning && lastTimestamp && lastTimestamp > nowTimestamp) {
+ // 剩余尚未执行完的倒计秒数
+ this.secNum = lastTimestamp - nowTimestamp
+ // 清除本地保存的变量
+ uni.removeStorageSync(this.uniqueKey + '_$uCountDownTimestamp')
+ // 开始倒计时
+ this.start()
+ // 如果不存在需要继续上一次的倒计时,执行正常的逻辑
+ this.changeEvent(this.startText)
+ start() {
+ // 防止快速点击获取验证码的按钮而导致内部产生多个定时器导致混乱
+ if(this.timer) {
+ clearInterval(this.timer)
+ this.timer = null
+ this.$emit('start')
+ this.canGetCode = false
+ // 这里放这句,是为了一开始时就提示,否则要等setInterval的1秒后才会有提示
+ this.changeEvent(this.changeText.replace(/x|X/, this.secNum))
+ if (--this.secNum) {
+ // 用当前倒计时的秒数替换提示字符串中的"x"字母
+ this.changeEvent(this.endText)
+ this.secNum = this.seconds
+ this.$emit('end')
+ this.canGetCode = true
+ this.setTimeToStorage()
+ // 重置,可以让用户再次获取验证码
+ reset() {
+ changeEvent(text) {
+ this.$emit('change', text)
+ // 保存时间戳,为了防止倒计时尚未结束,H5刷新或者各端的右上角返回上一页再进来
+ setTimeToStorage() {
+ if(!this.keepRunning || !this.timer) return
+ // 记录当前的时间戳,为了下次进入页面,如果还在倒计时内的话,继续倒计时
+ // 倒计时尚未结束,结果大于0;倒计时已经开始,就会小于初始值,如果等于初始值,说明没有开始倒计时,无需处理
+ if(this.secNum > 0 && this.secNum <= this.seconds) {
+ // 获取当前时间戳(+ new Date()为特殊写法),除以1000变成秒,再去除小数部分
+ // 将本该结束时候的时间戳保存起来 => 当前时间戳 + 剩余的秒数
+ uni.setStorage({
+ key: this.uniqueKey + '_$uCountDownTimestamp',
+ data: nowTimestamp + Number(this.secNum)
+ // 组件销毁的时候,清除定时器,否则定时器会继续存在,系统不会自动清除
+ beforeDestroy() {
+ clearTimeout(this.timer)
@@ -0,0 +1,29 @@
+ // 占父容器宽度的多少等分,总分为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
@@ -0,0 +1,162 @@
+ class="u-col"
+ ref="u-col"
+ :class="[
+ 'u-col-' + span
+ :style="[colStyle]"
+ * 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>
+ name: 'u-col',
+ gutter: 0
+ gridNum: 12
+ uJustify() {
+ if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify
+ else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify
+ else return this.justify
+ uAlignItem() {
+ if (this.align == 'top') return 'flex-start'
+ if (this.align == 'bottom') return 'flex-end'
+ else return this.align
+ 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))),
+ 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;
+ // nvue下百分比无效
+ .u-col-0 {
+ width: 0;
+ .u-col-1 {
+ width: calc(100%/12);
+ .u-col-2 {
+ width: calc(100%/12 * 2);
+ .u-col-3 {
+ width: calc(100%/12 * 3);
+ .u-col-4 {
+ width: calc(100%/12 * 4);
+ .u-col-5 {
+ width: calc(100%/12 * 5);
+ .u-col-6 {
+ width: calc(100%/12 * 6);
+ .u-col-7 {
+ width: calc(100%/12 * 7);
+ .u-col-8 {
+ width: calc(100%/12 * 8);
+ .u-col-9 {
+ width: calc(100%/12 * 9);
+ .u-col-10 {
+ width: calc(100%/12 * 10);
+ .u-col-11 {
+ width: calc(100%/12 * 11);
+ .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
+ default: uni.$u.props.collapseItem.duration
@@ -0,0 +1,225 @@
+ <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) {
+ // 这里根据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;
@@ -0,0 +1,19 @@
+ // 当前展开面板的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
@@ -0,0 +1,90 @@
+ <view class="u-collapse">
+ * collapse 折叠面板
+ * @description 通过折叠面板收纳内容区域
+ * @property {String | Number | Array} value 当前展开面板的name,非手风琴模式:[<string | number>],手风琴模式:string | number
+ * @property {Boolean} accordion 是否手风琴模式( 默认 false )
+ * @property {Boolean} border 是否显示外边框 ( 默认 true )
+ * @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
+ * @example <u-collapse></u-collapse>
+ name: "u-collapse",
+ needInit() {
+ // 通过computed,同时监听accordion和value值的变化
+ // 再通过watch去执行init()方法,进行再一次的初始化
+ return [this.accordion, this.value]
+ // 判断子组件(u-checkbox)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+ typeof(child.updateParentData) === 'function' && child.updateParentData()
+ // 重新初始化一次内部的所有子元素
+ child.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
+ // 拼接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)
@@ -0,0 +1,55 @@
+ // 显示的内容,字符串
+ 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
@@ -0,0 +1,160 @@
+ class="u-notice"
+ 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"
+ :style="[textStyle]"
+ >{{ item }}</text>
+ </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"
+ * 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
+ noticeChange(e){
+ this.index = e.detail.current
+ // 点击通告栏
+ this.$emit('click', this.index)
+ .u-notice {
+ &__left-icon {
+ &__right-icon {
+ margin-left: 5px;
+ &__swiper {
+ height: 16px;
@@ -0,0 +1,24 @@
+ // 倒计时时长,单位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
@@ -0,0 +1,163 @@
+ <view class="u-count-down">
+ <text class="u-count-down__text">{{ formattedTime }}</text>
+ 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',
+ // 各单位(天,时,分等)剩余时间
+ timeData: parseTimeData(0),
+ // 格式化后的时间,如"03:23:21"
+ formattedTime: '0',
+ // 倒计时是否正在进行中
+ runing: false,
+ endTime: 0, // 结束的毫秒时间戳
+ remainTime: 0, // 剩余的毫秒时间
+ time(n) {
+ this.reset()
+ 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')
+ // 重置倒计时
+ this.remainTime = this.time
+ this.setRemainTime(this.remainTime)
+ if (this.autoStart) {
+ // 暂停倒计时
+ pause() {
+ this.runing = false;
+ // 清空定时器
+ clearTimeout() {
+<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;
@@ -0,0 +1,62 @@
+// 补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
@@ -0,0 +1,184 @@
+ class="u-count-num"
+ >{{ displayValue }}</text>
+ * 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 千位分隔符,见官网说明
+ * @event {Function} end 数值滚动到目标值时触发
+ * @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
+ name: 'u-count-to',
+ localStartVal: this.startVal,
+ displayValue: this.formatNumber(this.startVal),
+ printVal: null,
+ paused: false, // 是否暂停
+ localDuration: Number(this.duration),
+ startTime: null, // 开始的时间
+ timestamp: null, // 时间戳
+ remaining: null, // 停留的时间
+ rAF: null,
+ lastTime: 0 // 上一次的时间
+ countDown() {
+ return this.startVal > this.endVal;
+ startVal() {
+ this.autoplay && this.start();
+ endVal() {
+ easingFn(t, b, c, d) {
+ return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
+ requestAnimationFrame(callback) {
+ const currTime = new Date().getTime();
+ // 为了使setTimteout的尽可能的接近每秒60帧的效果
+ const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
+ const id = setTimeout(() => {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ this.lastTime = currTime + timeToCall;
+ return id;
+ cancelAnimationFrame(id) {
+ clearTimeout(id);
+ // 开始滚动数字
+ this.localStartVal = this.startVal;
+ this.startTime = null;
+ this.localDuration = this.duration;
+ this.paused = false;
+ this.rAF = this.requestAnimationFrame(this.count);
+ // 暂定状态,重新再开始滚动;或者滚动状态下,暂停
+ reStart() {
+ if (this.paused) {
+ this.resume();
+ this.stop();
+ this.paused = true;
+ // 暂停
+ stop() {
+ this.cancelAnimationFrame(this.rAF);
+ // 重新开始(暂停的情况下)
+ resume() {
+ if (!this.remaining) return
+ this.startTime = 0;
+ this.localDuration = this.remaining;
+ this.localStartVal = this.printVal;
+ this.requestAnimationFrame(this.count);
+ // 重置
+ this.displayValue = this.formatNumber(this.startVal);
+ count(timestamp) {
+ if (!this.startTime) this.startTime = timestamp;
+ this.timestamp = timestamp;
+ const progress = timestamp - this.startTime;
+ this.remaining = this.localDuration - progress;
+ if (this.useEasing) {
+ if (this.countDown) {
+ this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
+ this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
+ this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
+ this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
+ this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
+ this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
+ this.displayValue = this.formatNumber(this.printVal) || 0;
+ if (progress < this.localDuration) {
+ this.$emit('end');
+ // 判断是否数字
+ isNumber(val) {
+ return !isNaN(parseFloat(val));
+ formatNumber(num) {
+ // 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
+ num = Number(num);
+ num = num.toFixed(Number(this.decimals));
+ num += '';
+ const x = num.split('.');
+ let x1 = x[0];
+ const x2 = x.length > 1 ? this.decimal + x[1] : '';
+ const rgx = /(\d+)(\d{3})/;
+ if (this.separator && !this.isNumber(this.separator)) {
+ while (rgx.test(x1)) {
+ x1 = x1.replace(rgx, '$1' + this.separator + '$2');
+ return x1 + x2;
+ destroyed() {
+.u-count-num {
+ display: inline-flex;
@@ -0,0 +1,116 @@
+ // 是否打开组件
+ 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
@@ -0,0 +1,360 @@
+ <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("请勿在过滤或格式化函数时添加数字")
+ }else if(type&&judge[0].length==4){//判断是否是年份
+ return judge[0]
+ }else if(judge[0].length>2){
+ // 列发生变化时触发
+ 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
+ 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"
+ :customStyle="rightLineStyle"
+ * 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组件被点击时触发
+ click() {
+ $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 {
+ 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;
+ // 当前选中项的value值
+ type: [Number, String, Array],
+ // 菜单项标题
+ // 选项数据,如果传入了默认slot,此参数无效
+ options: {
+ default() {
+ // 是否禁用此菜单项
+ // 下拉弹窗的高度
+ height: {
+ default: 'auto'
+ // 点击遮罩是否可以收起弹窗
@@ -0,0 +1,127 @@
+ <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 => {
+.u-dropdown {
+ &__menu {
@@ -0,0 +1,65 @@
+ // 标题选中时的样式
+ 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: {
+ // 内置图标名称,或图片路径,建议绝对路径
+ 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
@@ -0,0 +1,128 @@
+ class="u-empty"
+ :style="[emptyStyle]"
+ v-if="show"
+ v-if="!isSrc"
+ :name="mode === 'message' ? 'chat' : `empty-${mode}`"
+ :size="iconSize"
+ margin-top="14"
+ 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">
+ * 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 )
+ * @event {Function} close 点击关闭按钮时触发
+ * @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
+ name: "u-empty",
+ icons: {
+ car: '购物车为空',
+ page: '页面不存在',
+ search: '没有搜索结果',
+ address: '没有收货地址',
+ wifi: '没有WiFi',
+ order: '订单为空',
+ coupon: '没有优惠券',
+ favor: '暂无收藏',
+ permission: '无权限',
+ history: '无历史记录',
+ 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
+ $u-empty-text-margin-top:20rpx !default;
+ $u-empty-slot-margin-top:20rpx !default;
+ .u-empty {
+ margin-top: $u-empty-text-margin-top;
+ .u-slot-wrap {
+ margin-top:$u-empty-slot-margin-top;
+ // 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,
@@ -0,0 +1,235 @@
+ <view class="u-form-item">
+ class="u-form-item__body"
+ :style="[$u.addStyle(customStyle), {
+ flexDirection: (labelPosition || parentData.labelPosition) === '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__body__right">
+ <view class="u-form-item__body__right__content">
+ <view class="u-form-item__body__right__content__slot">
+ class="item__body__right__content__icon"
+ v-if="$slots.right"
+ <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}`"
+ * Form 表单
+ * @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>
+ name: 'u-form-item',
+ // 错误提示语
+ // 提示文本的位置
+ labelPosition: 'left',
+ // 提示文本对齐方式
+ labelAlign: 'left',
+ // 提示文本的样式
+ labelStyle: {},
+ // 提示文本的宽度
+ labelWidth: 45,
+ // 错误提示方式
+ errorType: 'message'
+ // 组件创建完成时,将当前实例保存到u-form中
+ propsLine() {
+ return uni.$u.props.line
+ // 父组件的实例
+ uni.$u.error('u-form-item需要结合u-form组件使用')
+ // 获取父组件的参数
+ // 此方法写在mixin中
+ this.getParentData('u-form');
+ // 移除u-form-item的校验结果
+ clearValidate() {
+ this.message = null
+ // 清空当前的组件的校验结果,并重置为初始值
+ resetField() {
+ // 找到原始值
+ const value = uni.$u.getProperty(this.parent.originalModel, this.prop)
+ // 将u-form的model的prop属性链还原原始值
+ uni.$u.setProperty(this.parent.model, this.prop, value)
+ // 移除校验结果
+ // 点击组件
+ .u-form-item {
+ padding: 10px 0;
+ padding-right: 10rpx;
+ margin-right: 8rpx;
+ &__required {
+ left: -9px;
+ line-height: 20px;
+ font-size: 20px;
+ top: 3px;
+ &__slot {
+ /* #ifndef MP */
+ color: $u-light-color;
+ &__message {
+ font-size: 12px;
+ line-height: 12px;
@@ -0,0 +1,45 @@
+ // 当前form的需要验证字段的集合
+ default: uni.$u.props.form.model
+ // 验证规则
+ 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
@@ -0,0 +1,214 @@
+ <view class="u-form">
+ import props from "./props.js";
+ import Schema from "../../libs/util/async-validator";
+ // 去除警告信息
+ Schema.warning = function() {};
+ * @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,请看文档说明!如果已经设置,请刷新页面。');
+ // $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)
+ // 背景颜色(默认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
@@ -0,0 +1,38 @@
+ <view class="u-gap" :style="[gapStyle]"></view>
+ * 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),
+ // 宫格的name
+ type: [String, Number, null],
+ default: uni.$u.props.gridItem.name
+ default: uni.$u.props.gridItem.bgColor
@@ -0,0 +1,209 @@
+ class="u-grid-item"
+ hover-class="u-grid-item--hover-class"
+ :class="classes"
+ :style="[itemStyle]"
+ * 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 自定义样式,对象形式
+ * @event {Function} click 点击宫格触发
+ * @example <u-grid-item></u-grid-item>
+ name: "u-grid-item",
+ col: 3, // 父组件划分的宫格数
+ border: true, // 是否显示边框,根据父组件决定
+ width: 0, // nvue下才这么计算,vue下放到computed中,否则会因为延时造成闪烁
+ classes: [], // 类名集合,用于判断是否显示右边和下边框
+ // vue下放到computed中,否则会因为延时造成闪烁
+ width() {
+ 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')
+ 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
+ // 移除事件监听,释放性能
+ 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 {
+ float: left;
+ /* #ifdef MP-WEIXIN */
+ margin-top:$u-grid-item-margin-top;
+ &--hover-class {
+ opacity:$u-grid-item-hover-class-opcatiy;
+ /* #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-border-bottom {
+ border-bottom-width:$u-grid-item-border-bottom-width;
+ border-color:$u-grid-item-border-bottom-color;
+ // 分成几列
+ col: {
+ default: uni.$u.props.grid.col
+ // 是否显示边框
+ default: uni.$u.props.grid.border
+ // 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
+ default: uni.$u.props.grid.align
@@ -0,0 +1,97 @@
+ class="u-grid"
+ ref='u-grid'
+ :style="[gridStyle]"
+ * grid 宫格布局
+ * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
+ * @property {String | Number} col 宫格的列数(默认 3 )
+ * @property {Boolean} border 是否显示宫格的边框(默认 false )
+ * @property {String} align 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 (默认 'left' )
+ * @example <u-grid :col="3" @click="click"></u-grid>
+ name: 'u-grid',
+ index: 0,
+ width: 0
+ // 判断子组件(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));
+ // 此方法由u-grid-item触发,用于在u-grid发出事件
+ childClick(name) {
+ $u-grid-width:100% !default;
+ .u-grid {
+ width: $u-grid-width;
+ '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'
@@ -0,0 +1,89 @@
+ // 图标类名
+ 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
@@ -0,0 +1,234 @@
+ 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>
+ <!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 -->
+ v-if="label !== ''"
+ class="u-icon__label"
+ 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,
+ // 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',
+ 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
+ // 是否阻止事件冒泡
+ // 变量定义
+ $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');
+ .u-icon {
+ &--left {
+ &--right {
+ &--top {
+ flex-direction: column-reverse;
+ &--bottom {
+ font-family: uicon-iconfont;
+ color: $u-icon-primary;
+ color: $u-icon-success;
+ color: $u-icon-error;
+ color: $u-icon-warning;
+ color: $u-icon-info;
+ &__img {
+ height: auto;
+ will-change: transform;
+ line-height: $u-icon-label-line-height;
@@ -0,0 +1,84 @@
+ // 图片地址
+ 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
@@ -0,0 +1,232 @@
+ :duration="fade ? 1000 : 0"
+ class="u-image"
+ @tap="onClick"
+ :style="[wrapStyle, backgroundStyle]"
+ 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"
+ * 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
+ if (!n) {
+ // 如果传入null或者'',或者false,或者undefined,标记为错误状态
+ this.isError = true
+ 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'
+ // 点击图片
+ 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'
+ $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;
+ &__image {
+ height: 100%;
+ &__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;
+ // 列表锚点文本内容
+ 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
@@ -0,0 +1,91 @@
+ <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>
+ </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>
+ name: 'u-index-anchor',
+ // 此处会活动父组件实例,并赋值给实例的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)
+ .u-index-anchor {
+ position: sticky;
+ padding-left: 15px;
+ z-index: 1;