@@ -0,0 +1,17 @@
+.DS_Store
+
+unpackage/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+node_modules/
+# Editor directories and files
+.hbuilderx
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
@@ -0,0 +1,43 @@
+<script>
+ export default {
+ globalData: {
+ statusBarHeight: 0, // 状态导航栏高度
+ navHeight: 44, // 总体高度
+ navigationBarHeight: 0, // 导航栏高度(标题栏高度)
+ },
+ onLaunch: function() {
+ // console.log('App Launch')
+ // 状态栏高度
+ this.globalData.statusBarHeight = uni.getSystemInfoSync().statusBarHeight
+ // #ifdef MP-WEIXIN
+ // 获取微信胶囊的位置信息 width,height,top,right,left,bottom
+ const custom = wx.getMenuButtonBoundingClientRect()
+ // console.log(custom)
+ // 导航栏高度(标题栏高度) = 胶囊高度 + (顶部距离 - 状态栏高度) * 2
+ this.globalData.navigationBarHeight = custom.height + (custom.top - this.globalData.statusBarHeight) * 2
+ // console.log("导航栏高度:"+this.globalData.navigationBarHeight)
+ // 总体高度 = 状态栏高度 + 导航栏高度
+ this.globalData.navHeight = this.globalData.navigationBarHeight + this.globalData.statusBarHeight
+ // #endif
+ // console.log('this.globalData==========',this.globalData)
+ // this.$wxApi.config()
+ onShow: function() {
+ // console.log('App Show')
+ onHide: function() {
+ // console.log('App Hide')
+ }
+</script>
+<style lang="scss">
+ /*每个页面公共css */
+ @import "@/uni_modules/uview-ui/index.scss";
+ @import "@/static/css/common.scss";
+</style>
@@ -0,0 +1,278 @@
+<template>
+ <view class="pages">
+ <!-- <view class="" :style="{height: navHeight+'px' }"></view> -->
+ <!-- <view class="page-bg">
+ <img class="img" :src="staticUrl+'/img/center-index-bg.png'" alt="">
+ </view> -->
+ <view class="box">
+ <view class="home_top">
+ <view class="base-info block-wrap u-flex u-row-between" @click="$u.route('/center/memberinfo',{type:'redirectTo'})" v-if="vuex_member_info.name">
+ <view class="left u-flex">
+ <!-- <u-avatar :src="avatar||staticUrl+'/img/avatar.png'" size="140rpx"></u-avatar> -->
+ <view class="info">
+ <view class="name ellipsis-1">{{vuex_member_info.contact||vuex_member_info.name}}</view>
+ <view class="mobile u-flex">
+ <text class="mobile">{{vuex_member_info.name}}</text>
+ <!-- <text class="mobile">{{vuex_member_info.mobile|hidePhoneNumber}}</text> -->
+ </view>
+ <!-- <u-icon @click="$u.route('/center/memberinfo',{type:'redirectTo'})" name="setting-fill" color="#333333" size="38rpx"></u-icon> -->
+ <view class="base-info block-wrap u-flex u-row-between" @click="goLogin" v-else>
+ <u-avatar :src="staticUrl+'/img/avatar.png'" size="140rpx"></u-avatar>
+ <view class="name ellipsis-1">登录/注册</view>
+ <text class="mobile">登录查看更多</text>
+ <!-- <u-icon name="setting-fill" color="#333333" size="38rpx"></u-icon> -->
+ <view class="tools block-wrap">
+ <!-- <view class="title">常用工具</view> -->
+ <view class="tools-wrap">
+ <view class=""
+ v-for="(item,index) in tools"
+ @click="toolsClick(item)"
+ :key="index">
+ <view class="tool-item u-flex u-row-between" v-if="!item.chat">
+ <u-icon :name="item.ico" color="#333333" size="55rpx"></u-icon>
+ <text class="name">{{item.name}}</text>
+ <u-icon name="arrow-right" color="#ddd" size="40rpx"></u-icon>
+ <view class="tool-item" v-if="item.chat==1">
+ <button class="button-item u-flex u-row-between" type="default" open-type="contact">
+ </button>
+ <tabbar :tabbarIndexProps="2" />
+</template>
+ import { systemInfo } from "@/mixin.js";
+ import tabbar from "../components/tabbar.vue"
+ components:{
+ tabbar
+ mixins:[systemInfo],
+ data() {
+ return {
+ staticUrl:this.$commonConfig.staticUrl,
+ avatar:this.$commonConfig.staticUrl+'/img/avatar.png',
+ memberInfo:{},
+ customerMobile:'',
+ tools:[
+ {name:'修改密码',url:'/center/resetpass',ico:this.$commonConfig.staticUrl+'/img/center-coupon.png',checkauth:true},
+ // {name:'领券中心',url:'/center/mycoupon',ico:this.$commonConfig.staticUrl+'/img/center-coupon.png',checkauth:true},
+ // {name:'开具发票',url:'center/invoice',ico:this.$commonConfig.staticUrl+'/img/center-ticket.png',checkauth:true},
+ {name:'报名记录',url:'center/applylist',ico:this.$commonConfig.staticUrl+'/img/center-order.png',checkauth:true},
+ {name:'订单记录',url:'center/order',ico:this.$commonConfig.staticUrl+'/img/center-order.png',checkauth:true},
+ // {name:'客服热线',url:'',ico:this.$commonConfig.staticUrl+'/img/center-call.png',checkauth:true,phone:''},
+ // {name:'在线客服',chat:'1',ico:this.$commonConfig.staticUrl+'/img/center-call.png'},
+ ]
+ onShow() {
+ // if(this.vuex_member_info.name){
+ this.getMemberInfo();
+ // }
+ onLoad() {
+ this.getSystemInfo();
+ methods: {
+ getMemberInfo(){
+ this.$u.api.getInfo({userid:this.vuex_member_info.id}).then(res=>{
+ this.memberInfo = res.data;
+ this.avatar = res.data.contractImg;
+ this.tools.forEach(item => {
+ if (item.name === '客服热线') {
+ item.phone = res.data.customerMobile;
+ });
+ this.$u.vuex('vuex_member_info', res.data);
+ const isExist = this.tools.some(item => item.name === '实名认证');
+ // if(!res.data.memberInfo.isAuth&&!isExist){
+ // this.tools.push({name:'实名认证',url:'center/factorauth',ico:this.$commonConfig.staticUrl+'/img/center-ticket.png',checkauth:true})
+ // console.log('memberInfo',this.memberInfo);
+ }).catch(err=>{
+ console.log('memberInfo',err.data);
+ })
+ toolsClick(item){
+ let that = this;
+ console.log('item',item);
+ // if(item.name!=='在线客服'&&item.name!=='客服热线'&&item.name!=='实名认证'&&item.name!=='我的订单'){
+ // uni.showToast({
+ // title:'开发中',
+ // icon:"none"
+ // })
+ // return
+ if ('phone' in item) {
+ if(!item.phone){
+ uni.showToast({
+ title:'电话号码为空',
+ icon:"none"
+ return
+ uni.makePhoneCall({
+ phoneNumber: item.phone,
+ success() {
+ console.log('success');
+ fail() {
+ console.log('fail');
+ if ('chat' in item) {
+ console.log('item.checkauth',item.checkauth);
+ if(item.checkauth){
+ this.checkauth(item.url)
+ }else{
+ uni.$u.route(item.url);
+ goLogin(){
+ uni.setStorage({
+ key: 'backUrl',
+ data:'center/center' ,
+ success: function () {
+ uni.$u.route('/pages/login/login')
+ checkauth(pageUrl){
+ if(this.vuex_member_info.name){
+ uni.$u.route(pageUrl)
+ uni.showModal({
+ title: '提示',
+ content: '你需要登录后,才可使用此功能!',
+ success: res => {
+ if (res.confirm) {
+ data:pageUrl ,
+<style>
+page{
+ background-color: #fff;
+ padding-top: 0;
+}
+<style lang="scss" scoped>
+$boxHeight:418rpx;
+.box { width: 100%;height:$boxHeight; margin: auto; overflow: hidden; }
+.home_top {
+ position: relative;
+ width: 100%;
+ z-index: 2;
+ height: $boxHeight;
+ .base-info{
+ position: absolute;
+ left: 32rpx;
+ right: 32rpx;
+ bottom: 90rpx;
+ color: #fff;
+.home_top:after { width: 180%; height: $boxHeight; position: absolute; left: -40%; top: 0; z-index: -1; content: ''; border-radius: 0 0 50% 50%; background: linear-gradient(180deg, #EE0C0C 0%, #F39D9F 100%); }
+.block-wrap{
+ margin: 24rpx 32rpx;
+.title{
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333333;
+ line-height: 45rpx;
+ margin-bottom: 30rpx;
+.base-info{
+ margin-bottom: 10rpx;
+ .info{
+ margin-left: 30rpx;
+ .name{
+ font-size: 36rpx;
+ font-weight: bold;
+ color: #FFFFFF;
+ line-height: 54rpx;
+ .mobile{
+ width: fit-content;
+ font-size: 28rpx;
+ font-weight: 400;
+ line-height: 42rpx;
+.tools{
+ .tools-wrap{
+ .tool-item{
+ padding: 12rpx 18rpx;
+ background-color: #FBFBFB;
+ margin-bottom: 20rpx;
+ border-radius: 20rpx;
+ overflow: hidden;
+ color: #363636;
+ margin-left: 12rpx;
+ .button-item{
+ background-color: transparent;
+ line-height: 1;
+ font-size: 24rpx;
+ color: #666666;
+ border: 0;
+ padding: 0;
+ top: -2px;
+ &::after{border: initial;}
@@ -0,0 +1,57 @@
+/*
+ 接口统一管理
+*/
+const apiurl = {
+ // 小程序获取openid
+ wxinfo: {
+ url: '/member/wechat/appletGetOpenId',
+ type: 'get'
+ // 授权获取微信手机号
+ getMobile: {
+ url: '/member/wechat/appletGetMobileV1',
+ // 小程序openid登录
+ login: {
+ url: '/member/auth/appletLogin',
+ type: 'post'
+ // 重新登录
+ reLogin: {
+ url: '/auth/login',
+ // 个人中心首页
+ personalIndex: {
+ url: '/system/client/personalIndex',
+ /**
+ * @author ygh
+ * @date 2023-12-14
+ * 公众号获取openid
+ *
+ * */
+ wxinfoH5: {
+ url: '/member/wechat/h5/code/',
+ type: 'get',
+ addUrl: true
+* 特殊处理接口
+const otherApiUrl = {
+ // 文件上传
+ uploadFile: '/file/upload/single/minio',
+export {
+ apiurl,
+ otherApiUrl
@@ -0,0 +1,32 @@
+/**
+ * 配置通用
+ */
+const node_dev = process.env.H_NODE_ENV;
+let baseUrl = '/api/serviceapi',
+ wxAppid = 'wx6490eaa0d20d2be2', //
+ staticUrl = 'https://minio.wdzzgs.com/greattransition/staticfile',
+ redirectUri = encodeURIComponent('https://greath5.dev.gztjy.top/greatgroup/pages/login/login'),
+ authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxAppid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`,
+ upFileUrl = 'https://serviceapi.wdzzgs.com/thirdapi/upload/single/minio';
+if (node_dev) {
+ if(process.env.H_NODE_ENV=='production'){
+ redirectUri = encodeURIComponent('https://h5.wdzzgs.com/greatgroup/pages/login/login')
+ };
+ (baseUrl = process.env.H_BASE_URL), (upFileUrl = process.env.H_UP_FILE_URL), (authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxAppid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`);
+const commonConfig = {
+ wxAppid: wxAppid, // 测试wxAppid
+ baseUrl: baseUrl, // 服务器地址
+ uploadFileUrl: upFileUrl, // 上传文件路径
+ staticUrl:staticUrl,
+ authUrl:authUrl,
+ paginationConfig:{
+ pageNum: 1,
+ pageSize: 10
+ },//分页参数
+ successCode:200,//接口返回状态
+ commonConfig
@@ -0,0 +1,37 @@
+import {
+ apiurl
+} from "./apiurl.js"
+import queryParams from "../utils/queryParams.js"
+// 此处第二个参数vm,就是我们在页面使用的this,你可以通过vm获取vuex等操作,更多内容详见uView对拦截器的介绍部分:
+const install = (Vue, vm) => {
+ let httpMap = {}
+ const http = uni.$u.http
+ // 循环请求路径对象生成对应的方式请求
+ Object.keys(apiurl).forEach((key) => {
+ if(apiurl[key]?.type=='get'){
+ if(apiurl[key].addUrl){
+ httpMap[key] = (data = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url+data.code, {}, config);
+ }else {
+ httpMap[key] = (data = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url, {params:data}, config);
+ } else if (apiurl[key]?.type == 'delete') {
+ httpMap[key] = (data = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url + `${queryParams(data)}`, {}, config);
+ } else{
+ httpMap[key] = (params = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url, params, config);
+ // 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
+ vm.$u.api = {
+ ...httpMap
+export default {
+ install
@@ -0,0 +1,170 @@
+import { commonConfig } from '@/common/config.js';
+import {againToken} from '../utils/againToken.js'
+import { showFullScreenLoading , tryHideFullScreenLoading } from '../utils/loading.js'
+let showModal = false;
+// 此vm参数为页面的实例,可以通过它引用vuex中的变量
+module.exports = (vm) => {
+ // 初始化请求配置
+ uni.$u.http.setConfig((config) => {
+ /* config 为默认全局配置*/
+ config.baseURL = commonConfig.baseUrl; /* 根域名 */
+ return config
+ // 请求拦截
+ uni.$u.http.interceptors.request.use((config) => { // 可使用async await 做异步操作
+ // console.log('config========',config);
+ // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{}
+ config.data = config.data || {}
+ // 根据custom参数中配置的是否需要token,添加对应的请求头
+ if(!config?.custom?.auth) {
+ // 可以在此通过vm引用vuex中的变量,具体值在vm.$store.state中
+ // config.header.token = vm.vuex_user_info.token;
+ config.header.Authorization = `Bearer ${vm.vuex_user_info.accessToken}`;
+ if(!config?.custom?.noload){
+ showFullScreenLoading()
+ }, config => { // 可使用async await 做异步操作
+ return Promise.reject(config)
+ let unlogin = function(){
+ if(vm.vuex_user_info.userid&&vm.vuex_wechatOpenid) {
+ againToken(vm.$u,vm.vuex_wechatOpenid,vm.vuex_user_info.userid)
+ } else {
+ let pages = getCurrentPages();
+ // console.log('pages',pages);
+ let backUrl = pages[pages.length - 1].route;
+ let options =uni.$u.queryParams( pages[pages.length - 1].options);
+ let fullBackUrl = backUrl+options;
+ // const backArr = ['productdetails'];//需要登录返回的页面
+ // const hasBackArr = backArr.some(backPage => backUrl.includes(backPage));
+ // if(hasBackArr){
+ // console.log('包含');
+ // uni.setStorage({
+ // key: 'backUrl',
+ // data: fullBackUrl,
+ // success: function () {
+ // // console.log('setStorage success');
+ // });
+ // }else{
+ // console.log('不包含');
+ // uni.removeStorage({
+ // success: function (res) {
+ // // console.log('success');
+ data: fullBackUrl,
+ // console.log('setStorage success');
+ // console.log('commonConfig.authUrl',commonConfig.authUrl);
+ tryHideFullScreenLoading()
+ if(showModal){return}
+ showModal = true;
+ title: '你需要登录后,才可使用!',
+ icon:'none',
+ duration: 2000,
+ complete:function(){
+ showModal = false;
+ uni.$u.vuex('vuex_member_info', {});
+ window.location.href = commonConfig.authUrl
+ // uni.$u.route(commonConfig.authUrl);
+ // uni.showModal({
+ // title: '提示',
+ // content: '你需要登录后,才可使用此功能!',
+ // success: res => {
+ // if (res.confirm) {
+ // window.location.href = commonConfig.authUrl;
+ // // uni.$u.route(commonConfig.authUrl);
+ // let pages = getCurrentPages();
+ // // console.log('pages',pages);
+ // if(pages.length>1){
+ // uni.navigateBack()
+ // },
+ // complete() {
+ // showModal = false
+ // uni.$u.vuex('vuex_member_info', {});
+ // 响应拦截
+ uni.$u.http.interceptors.response.use((response) => {/* 对响应成功做点什么 可使用async await 做异步操作*/
+ // console.log('response====response',response);
+ const data = response.data
+ // 自定义参数
+ const custom = response.config?.custom
+ if (data.code !== 200) {
+ console.log('data====',data);
+ // 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示
+ if (custom.toast !== false) {
+ const unshowmsg = ['令牌不能为空'];
+ if (!unshowmsg.includes(data.msg)) {
+ uni.$u.toast(data.msg)
+ // uni.$u.toast(data.msg)
+ if(data.msg == "令牌验证失败" || data.msg == "令牌不能为空" || data.code == 401){
+ unlogin()
+ if(data.msg == "用户不存在!"||data.msg == "用户未注册"||data.msg == "用户信息不能为空"||data.msg == "团队用户信息不能为空"){
+ uni.clearStorage();
+ return Promise.reject(data)
+ // 如果需要catch返回,则进行reject
+ // if (custom?.catch) {
+ // return Promise.reject(data)
+ // } else {
+ // // 否则返回一个pending中的promise,请求不会进入catch中
+ // return new Promise(() => { })
+ // console.log('data--',data);
+ return data === undefined ? {} : data
+ }, (response) => {
+ // console.log('response==',response);
+ const data = response.data;
+ // console.log('data==',data);
+ // 对响应错误做点什么 (statusCode !== 200)
+ let errMap = {
+ '404':'接口不存在'
+ if (response.statusCode in errMap) {
+ uni.$u.toast(errMap[response.statusCode])
+ if(data.msg == "令牌不能为空" || data.code == 401){
+ return Promise.reject(response)
@@ -0,0 +1,412 @@
+ <view :class="modal?'show-qrcode':'hide-qrcode'">
+ <view class="box-qrcode" :style="{'margin-left': marginLeft + 'px'}" @longtap="longtapCode">
+ <!-- style="width: 550rpx;height: 550rpx;" -->
+ <canvas class="canvas-qrcode" :style="style_w_h" :canvas-id="qrcode_id">
+ <!-- #ifndef MP -->
+ <view v-if="modal&&is_themeImg" :style="style_w_h" class="box-img-qrcode">
+ <image :style="style_w_h_img" mode="scaleToFill" :src="themeImg"></image>
+ <!-- #endif -->
+ </canvas>
+ <!-- <image mode="scaleToFill" :src="imagePath"></image> -->
+ var qr_we = require("./qrcode_wx.js");
+ const qrCode = require('./weapp-qrcode.js')
+ isAndroid : false ,
+ show: true,
+ imagePath: '',
+ // qrcode_id: 'qrcode_id',
+ marginLeft: 0,
+ //一般的安卓app只需加30就能显示全
+ //苹果app的不加就能显示全,加了就要弄margin-left
+ //有些安卓app显示不全
+ add_num : 30 ,
+ add_num_key : 'rectify_code_key',
+ props: {
+ modal: {
+ type: Boolean,
+ default: false
+ url: {
+ type: String,
+ default: ''
+ height: {
+ type: Number,
+ default: 260
+ width: {
+ themeColor: {
+ default: '#333333',
+ qrcode_id: {
+ default: 'qrcode_id',
+ is_themeImg: {
+ default: false,
+ themeImg: {
+ default: 'https://cdn.pixabay.com/photo/2016/11/29/13/24/balloons-1869816__340.jpg',
+ h_w_img: {
+ default: 30
+ watch:{
+ computed: {
+ style_w_h() {
+ return this.set_style_w_h();
+ style_w_h_img() {
+ var height = parseInt(that.h_w_img);
+ var width = parseInt(that.h_w_img);
+ var style = '';
+ if (height > 0) {
+ style = `height:${height*2}rpx;`;
+ if (width > 0) {
+ style += `width:${width*2}rpx;z-index: 2;`;
+ return style;
+ created: function() {
+ try {
+ //app苹果二维码不居中
+ //#ifndef MP
+ let isAndroid = false ;
+ const res = uni.getSystemInfoSync();
+ if(res.platform == 'android'){
+ isAndroid = true ;
+ isAndroid = false ;
+ if (!isAndroid) {
+ // that.marginLeft = 46;
+ that.marginLeft = 0;
+ that.isAndroid = isAndroid ;
+ const add_num = uni.getStorageSync(that.add_num_key);
+ if (add_num) {
+ that.add_num = add_num;
+ } catch (e) {
+ // error
+ //#ifdef MP
+ //that.marginLeft = 40;
+ set_style_w_h(){
+ var height = parseInt(that.height);
+ var width = parseInt(that.width);
+ var height = height*2 ;
+ var width = width*2 ;
+ var add = that.add_num ;
+ height += add;
+ width += add;
+ style = `height:${height}rpx;`;
+ style += `width:${width}rpx;`;
+ hideQrcode() {
+ this.$emit("hideQrcode")
+ // 二维码生成工具
+ crtQrCode() {
+ new qrCode(that.qrcode_id, {
+ text: this.url,
+ width: that.width,
+ height: that.height,
+ colorDark: that.themeColor,//#333333
+ colorLight: "#FFFFFF",
+ correctLevel: qrCode.CorrectLevel.H,
+ that.createQrCode(this.url, that.qrcode_id, that.width, that.height,that.themeColor,that.is_themeImg,that.themeImg,that.h_w_img);
+ //that.createQrCode(this.url, that.qrcode_id, that.width, that.height);
+ createQrCode: function(url, canvasId, cavW, cavH,cavColor,haveImg,imgurl,imgsize) {
+ //调用插件中的draw方法,绘制二维码图片
+ qr_we.api.draw(url, canvasId, cavW, cavH,cavColor,haveImg,imgurl,imgsize, this, this.canvasToTempImage);
+ // setTimeout(() => { this.canvasToTempImage();},100);
+ //获取临时缓存照片路径,存入data中
+ canvasToTempImage: function() {
+ var that = this;
+ saveImage: function() {
+ uni.canvasToTempFilePath({
+ canvasId: that.qrcode_id,
+ success: function(res) {
+ var tempFilePath = res.tempFilePath;
+ console.log(tempFilePath);
+ that.imagePath = tempFilePath;
+ //保存到相册
+ // uni.saveFile({
+ // tempFilePath: tempFilePath,
+ // success: function (res2) {
+ // var savedFilePath = res2.savedFilePath;
+ uni.saveImageToPhotosAlbum({
+ filePath : tempFilePath ,
+ success: function (res3) {
+ content: '保存成功',
+ confirmText: '确定',
+ showCancel: false,
+ confirmColor: '#33CCCC',
+ success(res4) {
+ fail: function(res) {
+ console.log(res);
+ }, that);
+ //微信小程序支持:长按二维码,提示是否保存相册
+ //安卓APP长按校正二维码
+ longtapCode(){
+ title: '校正二维码',
+ content: '二维码是否异常',
+ success(res) {
+ that.rectify_code();
+ //#ifdef MP-WEIXIN
+ content: '是否保存到相册',
+ that.saveImage();
+ //安卓有些手机不正常,长按可选择矫正
+ rectify_code(){
+ let add_num = that.add_num ;
+ add_num += 30 ;
+ that.crtQrCode();//重新生成才会立即覆盖
+ //第一次长按校正设置了就不用在设置
+ key: that.add_num_key,
+ data: add_num,
+ success: function() {
+ mounted() {}
+<style scoped lang="scss">
+ // .qrcode-box {
+ // position: fixed;
+ // left: 0;
+ // top: 0;
+ // right: 0;
+ // bottom: 0;
+ // height: 100vh;
+ // width: 100vw;
+ // background-color: rgba(59, 59, 59, 0.6);
+ // // opacity: 0.8;
+ // text-align: center;
+ // display: flex;
+ // align-items: center;
+ // display: none;
+ // .qrcode-item {
+ // flex: 1;
+ // position: relative;
+ // .item-box {
+ // width: 90%;
+ // margin: auto;
+ // display: inline-block;
+ // margin-top: 30%;
+ // padding-bottom: 30rpx;
+ // // animation: show 0.7s;
+ // .title {
+ // font-size: 46rpx;
+ // margin-bottom: 24rpx;
+ // .canvas {
+ // background-color: #FFFFFF;
+ .box-qrcode{
+ text-align: center;
+ .box-img-qrcode{
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ image{
+ width: 60upx;
+ height: 60upx;
+ border-radius: 50%;
+ .canvas-qrcode {
+ margin: auto;
+ display: inline-block;
+ float: left;
+ .opacity-qrcode {
+ opacity: 0;
+ display: block;
+ .show-qrcode {
+ animation: fade 0.7s;
+ // -moz-animation: fade 0.5s; /* Firefox */
+ // -webkit-animation: fade 0.5s; /* Safari 和 Chrome */
+ // -o-animation: fade 0.5s;
+ .hide-qrcode {
+ animation: hide 0.7s;
+ @keyframes fade {
+ from {
+ opacity: 0.8;
+ to {
+ opacity: 1;
+ @keyframes hide {
@@ -0,0 +1,872 @@
+!(function() {
+ // alignment pattern
+ var adelta = [
+ 0, 11, 15, 19, 23, 27, 31,
+ 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,
+ 26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28
+ ];
+ // version block
+ var vpat = [
+ 0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d,
+ 0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9,
+ 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75,
+ 0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64,
+ 0x541, 0xc69
+ // final format bits with mask: level << 3 | mask
+ var fmtword = [
+ 0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, //L
+ 0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, //M
+ 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, //Q
+ 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b //H
+ // 4 per version: number of blocks 1,2; data width; ecc width
+ var eccblocks = [
+ 1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17,
+ 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28,
+ 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22,
+ 1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16,
+ 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22,
+ 2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28,
+ 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4, 1, 13, 26,
+ 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26,
+ 2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24,
+ 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28,
+ 4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24,
+ 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28,
+ 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22,
+ 3, 1, 115, 30, 4, 5, 40, 24, 11, 5, 16, 20, 11, 5, 12, 24,
+ 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24,
+ 5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30,
+ 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28,
+ 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28,
+ 3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26,
+ 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,
+ 4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30,
+ 2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13, 24,
+ 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30,
+ 6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
+ 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30,
+ 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30,
+ 8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30,
+ 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30,
+ 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30,
+ 5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30,
+ 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30,
+ 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30,
+ 17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30,
+ 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30,
+ 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30,
+ 6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30,
+ 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30,
+ 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30,
+ 20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30,
+ 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
+ // Galois field log table
+ var glog = [
+ 0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,
+ 0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,
+ 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,
+ 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,
+ 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
+ 0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,
+ 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,
+ 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,
+ 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,
+ 0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
+ 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,
+ 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,
+ 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,
+ 0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,
+ 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
+ 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
+ // Galios field exponent table
+ var gexp = [
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,
+ 0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,
+ 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,
+ 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,
+ 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
+ 0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,
+ 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,
+ 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,
+ 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,
+ 0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
+ 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,
+ 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,
+ 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,
+ 0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,
+ 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
+ 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00
+ // Working buffers:
+ // data input and ecc append, image working buffer, fixed part of image, run lengths for badness
+ var strinbuf = [],
+ eccbuf = [],
+ qrframe = [],
+ framask = [],
+ rlens = [];
+ // Control values - width is based on version, last 4 are from table.
+ var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
+ var ecclevel = 2;
+ // set bit to indicate cell in qrframe is immutable. symmetric around diagonal
+ function setmask(x, y) {
+ var bt;
+ if (x > y) {
+ bt = x;
+ x = y;
+ y = bt;
+ // y*y = 1+3+5...
+ bt = y;
+ bt *= y;
+ bt += y;
+ bt >>= 1;
+ bt += x;
+ framask[bt] = 1;
+ // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
+ function putalign(x, y) {
+ var j;
+ qrframe[x + width * y] = 1;
+ for (j = -2; j < 2; j++) {
+ qrframe[(x + j) + width * (y - 2)] = 1;
+ qrframe[(x - 2) + width * (y + j + 1)] = 1;
+ qrframe[(x + 2) + width * (y + j)] = 1;
+ qrframe[(x + j + 1) + width * (y + 2)] = 1;
+ for (j = 0; j < 2; j++) {
+ setmask(x - 1, y + j);
+ setmask(x + 1, y - j);
+ setmask(x - j, y - 1);
+ setmask(x + j, y + 1);
+ //========================================================================
+ // Reed Solomon error correction
+ // exponentiation mod N
+ function modnn(x) {
+ while (x >= 255) {
+ x -= 255;
+ x = (x >> 8) + (x & 255);
+ return x;
+ var genpoly = [];
+ // Calculate and append ECC data to data block. Block is in strinbuf, indexes to buffers given.
+ function appendrs(data, dlen, ecbuf, eclen) {
+ var i, j, fb;
+ for (i = 0; i < eclen; i++)
+ strinbuf[ecbuf + i] = 0;
+ for (i = 0; i < dlen; i++) {
+ fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
+ if (fb != 255) /* fb term is non-zero */
+ for (j = 1; j < eclen; j++)
+ strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
+ else
+ for (j = ecbuf; j < ecbuf + eclen; j++)
+ strinbuf[j] = strinbuf[j + 1];
+ strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
+ // Frame data insert following the path rules
+ // check mask - since symmetrical use half.
+ function ismasked(x, y) {
+ bt += y * y;
+ return framask[bt];
+ // Apply the selected mask out of the 8.
+ function applymask(m) {
+ var x, y, r3x, r3y;
+ switch (m) {
+ case 0:
+ for (y = 0; y < width; y++)
+ for (x = 0; x < width; x++)
+ if (!((x + y) & 1) && !ismasked(x, y))
+ qrframe[x + y * width] ^= 1;
+ break;
+ case 1:
+ if (!(y & 1) && !ismasked(x, y))
+ case 2:
+ for (r3x = 0, x = 0; x < width; x++, r3x++) {
+ if (r3x == 3)
+ r3x = 0;
+ if (!r3x && !ismasked(x, y))
+ case 3:
+ for (r3y = 0, y = 0; y < width; y++, r3y++) {
+ if (r3y == 3)
+ r3y = 0;
+ for (r3x = r3y, x = 0; x < width; x++, r3x++) {
+ case 4:
+ for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++, r3x++) {
+ if (r3x == 3) {
+ r3y = !r3y;
+ if (!r3y && !ismasked(x, y))
+ case 5:
+ if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y))
+ case 6:
+ if (!(((x & y & 1) + (r3x && (r3x == r3y))) & 1) && !ismasked(x, y))
+ case 7:
+ if (!(((r3x && (r3x == r3y)) + ((x + y) & 1)) & 1) && !ismasked(x, y))
+ return;
+ // Badness coefficients.
+ var N1 = 3,
+ N2 = 3,
+ N3 = 40,
+ N4 = 10;
+ // Using the table of the length of each run, calculate the amount of bad image
+ // - long runs or those that look like finders; called twice, once each for X and Y
+ function badruns(length) {
+ var i;
+ var runsbad = 0;
+ for (i = 0; i <= length; i++)
+ if (rlens[i] >= 5)
+ runsbad += N1 + rlens[i] - 5;
+ // BwBBBwB as in finder
+ for (i = 3; i < length - 1; i += 2)
+ if (rlens[i - 2] == rlens[i + 2] &&
+ rlens[i + 2] == rlens[i - 1] &&
+ rlens[i - 1] == rlens[i + 1] &&
+ rlens[i - 1] * 3 == rlens[i]
+ // white around the black pattern? Not part of spec
+ &&
+ (rlens[i - 3] == 0 // beginning
+ ||
+ i + 3 > length // end
+ rlens[i - 3] * 3 >= rlens[i] * 4 || rlens[i + 3] * 3 >= rlens[i] * 4)
+ )
+ runsbad += N3;
+ return runsbad;
+ // Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
+ function badcheck() {
+ var x, y, h, b, b1;
+ var thisbad = 0;
+ var bw = 0;
+ // blocks of same color.
+ for (y = 0; y < width - 1; y++)
+ for (x = 0; x < width - 1; x++)
+ if ((qrframe[x + width * y] && qrframe[(x + 1) + width * y] &&
+ qrframe[x + width * (y + 1)] && qrframe[(x + 1) + width * (y + 1)]) // all black
+ !(qrframe[x + width * y] || qrframe[(x + 1) + width * y] ||
+ qrframe[x + width * (y + 1)] || qrframe[(x + 1) + width * (y + 1)])) // all white
+ thisbad += N2;
+ // X runs
+ for (y = 0; y < width; y++) {
+ rlens[0] = 0;
+ for (h = b = x = 0; x < width; x++) {
+ if ((b1 = qrframe[x + width * y]) == b)
+ rlens[h]++;
+ rlens[++h] = 1;
+ b = b1;
+ bw += b ? 1 : -1;
+ thisbad += badruns(h);
+ // black/white imbalance
+ if (bw < 0)
+ bw = -bw;
+ var big = bw;
+ var count = 0;
+ big += big << 2;
+ big <<= 1;
+ while (big > width * width)
+ big -= width * width, count++;
+ thisbad += count * N4;
+ // Y runs
+ for (x = 0; x < width; x++) {
+ for (h = b = y = 0; y < width; y++) {
+ return thisbad;
+ function genframe(instring) {
+ var x, y, k, t, v, i, j, m;
+ // find the smallest version that fits the string
+ t = instring.length;
+ version = 0;
+ do {
+ version++;
+ k = (ecclevel - 1) * 4 + (version - 1) * 16;
+ neccblk1 = eccblocks[k++];
+ neccblk2 = eccblocks[k++];
+ datablkw = eccblocks[k++];
+ eccblkwid = eccblocks[k];
+ k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
+ if (t <= k)
+ } while (version < 40);
+ // FIXME - insure that it fits insted of being truncated
+ width = 17 + 4 * version;
+ // allocate, clear and setup data structures
+ v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+ for (t = 0; t < v; t++)
+ eccbuf[t] = 0;
+ strinbuf = instring.slice(0);
+ for (t = 0; t < width * width; t++)
+ qrframe[t] = 0;
+ for (t = 0; t < (width * (width + 1) + 1) / 2; t++)
+ framask[t] = 0;
+ // insert finders - black to frame, white to mask
+ for (t = 0; t < 3; t++) {
+ k = 0;
+ y = 0;
+ if (t == 1)
+ k = (width - 7);
+ if (t == 2)
+ y = (width - 7);
+ qrframe[(y + 3) + width * (k + 3)] = 1;
+ for (x = 0; x < 6; x++) {
+ qrframe[(y + x) + width * k] = 1;
+ qrframe[y + width * (k + x + 1)] = 1;
+ qrframe[(y + 6) + width * (k + x)] = 1;
+ qrframe[(y + x + 1) + width * (k + 6)] = 1;
+ for (x = 1; x < 5; x++) {
+ setmask(y + x, k + 1);
+ setmask(y + 1, k + x + 1);
+ setmask(y + 5, k + x);
+ setmask(y + x + 1, k + 5);
+ for (x = 2; x < 4; x++) {
+ qrframe[(y + x) + width * (k + 2)] = 1;
+ qrframe[(y + 2) + width * (k + x + 1)] = 1;
+ qrframe[(y + 4) + width * (k + x)] = 1;
+ qrframe[(y + x + 1) + width * (k + 4)] = 1;
+ // alignment blocks
+ if (version > 1) {
+ t = adelta[version];
+ y = width - 7;
+ for (;;) {
+ x = width - 7;
+ while (x > t - 3) {
+ putalign(x, y);
+ if (x < t)
+ x -= t;
+ if (y <= t + 9)
+ y -= t;
+ putalign(6, y);
+ putalign(y, 6);
+ // single black
+ qrframe[8 + width * (width - 8)] = 1;
+ // timing gap - mask only
+ for (y = 0; y < 7; y++) {
+ setmask(7, y);
+ setmask(width - 8, y);
+ setmask(7, y + width - 7);
+ for (x = 0; x < 8; x++) {
+ setmask(x, 7);
+ setmask(x + width - 8, 7);
+ setmask(x, width - 8);
+ // reserve mask-format area
+ for (x = 0; x < 9; x++)
+ setmask(x, 8);
+ setmask(x + width - 8, 8);
+ setmask(8, x);
+ for (y = 0; y < 7; y++)
+ setmask(8, y + width - 7);
+ // timing row/col
+ for (x = 0; x < width - 14; x++)
+ if (x & 1) {
+ setmask(8 + x, 6);
+ setmask(6, 8 + x);
+ else {
+ qrframe[(8 + x) + width * 6] = 1;
+ qrframe[6 + width * (8 + x)] = 1;
+ if (version > 6) {
+ t = vpat[version - 7];
+ k = 17;
+ for (x = 0; x < 6; x++)
+ for (y = 0; y < 3; y++, k--)
+ if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
+ qrframe[(5 - x) + width * (2 - y + width - 11)] = 1;
+ qrframe[(2 - y + width - 11) + width * (5 - x)] = 1;
+ setmask(5 - x, 2 - y + width - 11);
+ setmask(2 - y + width - 11, 5 - x);
+ // sync mask bits - only set above for white spaces, so add in black bits
+ for (x = 0; x <= y; x++)
+ if (qrframe[x + width * y])
+ setmask(x, y);
+ // convert string to bitstream
+ // 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
+ v = strinbuf.length;
+ // string to array
+ for (i = 0; i < v; i++)
+ eccbuf[i] = strinbuf.charCodeAt(i);
+ strinbuf = eccbuf.slice(0);
+ // calculate max string length
+ x = datablkw * (neccblk1 + neccblk2) + neccblk2;
+ if (v >= x - 2) {
+ v = x - 2;
+ if (version > 9)
+ v--;
+ // shift and repack to insert length prefix
+ i = v;
+ if (version > 9) {
+ strinbuf[i + 2] = 0;
+ strinbuf[i + 3] = 0;
+ while (i--) {
+ t = strinbuf[i];
+ strinbuf[i + 3] |= 255 & (t << 4);
+ strinbuf[i + 2] = t >> 4;
+ strinbuf[2] |= 255 & (v << 4);
+ strinbuf[1] = v >> 4;
+ strinbuf[0] = 0x40 | (v >> 12);
+ strinbuf[i + 1] = 0;
+ strinbuf[i + 2] |= 255 & (t << 4);
+ strinbuf[i + 1] = t >> 4;
+ strinbuf[1] |= 255 & (v << 4);
+ strinbuf[0] = 0x40 | (v >> 4);
+ // fill to end with pad pattern
+ i = v + 3 - (version < 10);
+ while (i < x) {
+ strinbuf[i++] = 0xec;
+ // buffer has room if (i == x) break;
+ strinbuf[i++] = 0x11;
+ // calculate and append ECC
+ // calculate generator polynomial
+ genpoly[0] = 1;
+ for (i = 0; i < eccblkwid; i++) {
+ genpoly[i + 1] = 1;
+ for (j = i; j > 0; j--)
+ genpoly[j] = genpoly[j] ?
+ genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
+ genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
+ for (i = 0; i <= eccblkwid; i++)
+ genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step
+ // append ecc to data buffer
+ k = x;
+ for (i = 0; i < neccblk1; i++) {
+ appendrs(y, datablkw, k, eccblkwid);
+ y += datablkw;
+ k += eccblkwid;
+ for (i = 0; i < neccblk2; i++) {
+ appendrs(y, datablkw + 1, k, eccblkwid);
+ y += datablkw + 1;
+ // interleave blocks
+ for (i = 0; i < datablkw; i++) {
+ for (j = 0; j < neccblk1; j++)
+ eccbuf[y++] = strinbuf[i + j * datablkw];
+ for (j = 0; j < neccblk2; j++)
+ eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
+ for (i = 0; i < eccblkwid; i++)
+ for (j = 0; j < neccblk1 + neccblk2; j++)
+ eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
+ strinbuf = eccbuf;
+ // pack bits into frame avoiding masked area.
+ x = y = width - 1;
+ k = v = 1; // up, minus
+ /* inteleaved data and ecc codes */
+ m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+ for (i = 0; i < m; i++) {
+ for (j = 0; j < 8; j++, t <<= 1) {
+ if (0x80 & t)
+ do { // find next fill position
+ if (v)
+ x--;
+ x++;
+ if (k) {
+ if (y != 0)
+ y--;
+ x -= 2;
+ k = !k;
+ if (x == 6) {
+ y = 9;
+ if (y != width - 1)
+ y++;
+ y -= 8;
+ v = !v;
+ } while (ismasked(x, y));
+ // save pre-mask copy of frame
+ strinbuf = qrframe.slice(0);
+ t = 0; // best
+ y = 30000; // demerit
+ // for instead of while since in original arduino code
+ // if an early mask was "good enough" it wouldn't try for a better one
+ // since they get more complex and take longer.
+ for (k = 0; k < 8; k++) {
+ applymask(k); // returns black-white imbalance
+ x = badcheck();
+ if (x < y) { // current mask better than previous best?
+ y = x;
+ t = k;
+ if (t == 7)
+ break; // don't increment i to a void redoing mask
+ qrframe = strinbuf.slice(0); // reset for next pass
+ if (t != k) // redo best mask - none good enough, last wasn't t
+ applymask(t);
+ // add in final mask/ecclevel bytes
+ y = fmtword[t + ((ecclevel - 1) << 3)];
+ // low byte
+ for (k = 0; k < 8; k++, y >>= 1)
+ if (y & 1) {
+ qrframe[(width - 1 - k) + width * 8] = 1;
+ if (k < 6)
+ qrframe[8 + width * k] = 1;
+ qrframe[8 + width * (k + 1)] = 1;
+ // high byte
+ for (k = 0; k < 7; k++, y >>= 1)
+ qrframe[8 + width * (width - 7 + k)] = 1;
+ if (k)
+ qrframe[(6 - k) + width * 8] = 1;
+ qrframe[7 + width * 8] = 1;
+ return qrframe;
+ var _canvas = null;
+ var api = {
+ get ecclevel() {
+ return ecclevel;
+ set ecclevel(val) {
+ ecclevel = val;
+ get size() {
+ return _size;
+ set size(val) {
+ _size = val
+ get canvas() {
+ return _canvas;
+ set canvas(el) {
+ _canvas = el;
+ getFrame: function(string) {
+ return genframe(string);
+ //这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文
+ utf16to8: function(str) {
+ var out, i, len, c;
+ out = "";
+ len = str.length;
+ for (i = 0; i < len; i++) {
+ c = str.charCodeAt(i);
+ if ((c >= 0x0001) && (c <= 0x007F)) {
+ out += str.charAt(i);
+ } else if (c > 0x07FF) {
+ out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
+ out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
+ out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+ out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
+ return out;
+ * 新增$this参数,传入组件的this,兼容在组件中生成
+ draw: function(str, canvas, cavW, cavH, cavColor, haveImg, imageUrl, imageSize, $this, cb = function() {}, ecc) {
+ ecclevel = ecc || ecclevel;
+ canvas = canvas || _canvas;
+ if (!canvas) {
+ console.warn('No canvas provided to draw QR code in!')
+ let pre_background = "#ffffff";
+ var size = Math.min(cavW, cavH);
+ str = that.utf16to8(str); //增加中文显示
+ var frame = that.getFrame(str);
+ // 组件中生成qrcode需要绑定this
+ var ctx = uni.createCanvasContext(canvas, $this);
+ var px = Math.round(size / (width ));
+ var roundedSize = px * (width);
+ // var px = 1 ;
+ // var roundedSize = px * (width + 8) ;
+ //var roundedSize = 0 ;
+ //var offset = Math.floor((size - roundedSize) / 2);
+ var offset = 0 ;
+ size = roundedSize;
+ //ctx.clearRect(0, 0, cavW, cavW);
+ ctx.setFillStyle(pre_background)
+ ctx.fillRect(0, 0, cavW, cavW);
+ ctx.setFillStyle(cavColor);
+ for (var i = 0; i < width; i++) {
+ for (var j = 0; j < width; j++) {
+ if (frame[j * width + i]) {
+ ctx.fillRect(px * ( i) + offset, px * ( j) + offset, px, px);
+ //画图片
+ if (haveImg) {
+ var x = Number(((cavW - imageSize - 14) / 2).toFixed(2));
+ var y = Number(((cavH - imageSize -14) / 2).toFixed(2));
+ drawRoundedRect(ctx, x, y, imageSize, imageSize, imageSize / 2, 6, true, true)
+ let isNetImg = false;
+ isNetImg = imageUrl.substr(0, 4) == 'http' ? true : false;
+ if (isNetImg) {
+ //网络图片下载到本地
+ uni.getImageInfo({
+ src: imageUrl,
+ ctx.drawImage(res.path, x, y, imageSize, imageSize);
+ //--增加绘制完成回调
+ ctx.draw(false, function() {
+ cb();
+ ctx.drawImage(imageUrl, x, y, imageSize, imageSize);
+ // 画圆角矩形
+ function drawRoundedRect(ctxi, x, y, width, height, r, lineWidth, fill, stroke) {
+ ctxi.setLineWidth(lineWidth);
+ ctxi.setFillStyle(pre_background);
+ ctxi.setStrokeStyle(pre_background);
+ ctxi.beginPath(); // draw top and top right corner
+ ctxi.moveTo(x + r, y);
+ ctxi.arcTo(x + width, y, x + width, y + r, r); // draw right side and bottom right corner
+ ctxi.arcTo(x + width, y + height, x + width - r, y + height, r); // draw bottom and bottom left corner
+ ctxi.arcTo(x, y + height, x, y + height - r, r); // draw left and top left corner
+ ctxi.arcTo(x, y, x + r, y, r);
+ ctxi.closePath();
+ if (fill) {
+ ctxi.fill();
+ if (stroke) {
+ ctxi.stroke();
+ //TODO handle the exception
+ module.exports = {
+ api
+})();
@@ -0,0 +1,83 @@
+ <view class="">
+ <view v-if="vuex_member_info.name" class="cart u-flex u-row-center" @click="$u.route('/shopping/cart',{type:'reLaunch'})">
+ <u-icon size="52rpx" class="icon" :name="staticUrl+'/img/cartico.png'"></u-icon>
+ <u-badge type="error" max="99" :value="cartTotal" :absolute="true" :offset="[0,5]"></u-badge>
+ <u-toast ref="uToast"></u-toast>
+ name:'cartfixed',
+ cartTotal:0,
+ mounted(){
+ this.getCartList()
+ getCartList(isAdd){
+ if(!this.vuex_member_info.name){
+ this.$u.api.cartList().then(res=>{
+ if(isAdd){
+ if(res.data.total==this.cartTotal){
+ title:'添加成功',
+ icon:'success',
+ duration:1000
+ // this.$refs.uToast.show({
+ // type:"success",
+ // message:'已在购物车'
+ // message:'加入成功'
+ this.cartTotal = res.data.total;
+ console.log('getCartList',res);
+ console.log('getCartList',err.data);
+ addToCart(num){
+ // this.cartTotal += num;
+ console.log('addToCart',num);
+.cart{
+ position: fixed;
+ width: 100rpx;
+ height: 100rpx;
+ right: 20rpx;
+ bottom: 300rpx;
+ background: #FFFFFF;
+ box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.12);
@@ -0,0 +1,131 @@
+ <view class="tabbar">
+ <u-tabbar
+ :value="tabbarValue"
+ @change="tabbarChange"
+ :fixed="true"
+ :placeholder="true"
+ :border="false"
+ inactiveColor="#666"
+ activeColor="#EE0808"
+ :customStyle="{'padding-top':'5px','padding-bottom':'5px','z-index':'30','margin':'0 48rpx 40rpx','border-radius':'50rpx','box-shadow':'rgba(100, 100, 111, 0.2) 0px 7px 29px 0px'}"
+ :safeAreaInsetBottom="false"
+ >
+ <u-tabbar-item text="首页" >
+ <image
+ class="u-page__item__slot-icon"
+ slot="active-icon"
+ :src="staticUrl+'/img/tabbar-home.png'"
+ ></image>
+ slot="inactive-icon"
+ :src="staticUrl+'/img/tabbar-home-gray.png'"
+ </u-tabbar-item>
+ <u-tabbar-item text="报名记录" >
+ :src="staticUrl+'/img/tabbar-tuan-order.png'"
+ :src="staticUrl+'/img/tabbar-tuan-order-y.png'"
+ <u-tabbar-item text="我的" >
+ :src="staticUrl+'/img/tabbar-my.png'"
+ :src="staticUrl+'/img/tabbar-my-gray.png'"
+ </u-tabbar>
+ name: 'tabbar',
+ props:{
+ tabbarIndexProps:{
+ type:Number,
+ default:0
+ tabbarValue:0,
+ 'tabbarIndexProps': {
+ handler(newVal, oldVal) {
+ // console.log('pages============',pages);
+ this.tabbarValue = newVal
+ immediate: true
+ console.log('onLoad tabbarIndex',this.tabbarIndex);
+ console.log('tabbarIndex',this.tabbarIndex);
+ this.tabbarValue = this.tabbarIndex
+ tabbarChange(name){
+ console.log('name====',name);
+ const tabBarRoutes = {
+ 0: '/pages/index/index',
+ // 1: {
+ // url: '/shopping/producTypetList',
+ // query: {
+ // navType: 'navigateTo'
+ 1: '/center/applylist',
+ // 2: '/shopping/shoppingindex',
+ 2: '/center/center'
+ const targetRoute = tabBarRoutes[name];
+ if (typeof targetRoute === 'string') {
+ uni.reLaunch({url: targetRoute});
+ } else if (typeof targetRoute === 'object') {
+ if(targetRoute.query.navType=='navigateTo'){
+ this.$u.route(targetRoute.url, targetRoute.query);
+ let queryParams = uni.$u.queryParams(targetRoute.query);
+ uni.reLaunch({url: targetRoute.url+queryParams});
+ // this.tabbarValue = name
+.u-page__item__slot-icon{
+ width: 62rpx;
+ height: 62rpx;
+ &.big{
+ transform: scale(1.5) translateY(-0.5em);
+.tabbar /deep/ .u-tabbar-item{
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href="/logo.svg" />
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+ <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,88 @@
+import App from './App'
+// import $wxApi from "./wxapi.js";
+// Vue.prototype.$wxApi = $wxApi;
+//微信支付封装
+// import $pay from "./pay.js";
+// Vue.prototype.$pay = $pay
+// #ifndef VUE3
+import Vue from 'vue'
+Vue.config.productionTip = false
+App.mpType = 'app'
+try {
+ function isPromise(obj) {
+ return (
+ !!obj &&
+ (typeof obj === "object" || typeof obj === "function") &&
+ typeof obj.then === "function"
+ );
+ // 统一 vue2 API Promise 化返回格式与 vue3 保持一致
+ uni.addInterceptor({
+ returnValue(res) {
+ if (!isPromise(res)) {
+ return res;
+ return new Promise((resolve, reject) => {
+ res.then((res) => {
+ if (res[0]) {
+ reject(res[0]);
+ resolve(res[1]);
+} catch (error) { }
+import uView from '@/uni_modules/uview-ui'
+Vue.use(uView)
+import { commonConfig } from './common/config';
+Vue.prototype.$commonConfig = commonConfig;
+import './utils/filter'
+import store from '@/store';
+// 引入uView提供的对vuex的简写法文件
+let vuexStore = require('@/store/$u.mixin.js');
+Vue.mixin(vuexStore);
+const app = new Vue({
+ ...App,
+ store
+})
+//uviewui v1
+// // http拦截器,将此部分放在new Vue()和app.$mount()之间,才能App.vue中正常使用
+// import httpInterceptor from '@/common/http.interceptor.js';
+// Vue.use(httpInterceptor, app);
+// 引入请求封装,将app参数传递到配置中
+require('./common/request.js')(app)
+// http接口API抽离,免于写url或者一些固定的参数
+import httpApi from '@/common/http.api.js';
+Vue.use(httpApi, app);
+app.$mount()
+// #endif
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+ const app = createSSRApp(App)
+ app
@@ -0,0 +1,112 @@
+{
+ "name" : "great_group",
+ "appid" : "__UNI__67D09D5",
+ "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" : {}
+ /* 快应用特有相关 */
+ "quickapp" : {},
+ /* 小程序特有相关 */
+ "mp-weixin" : {
+ "appid" : "wx1a07584f15abf1ff",
+ "setting" : {
+ "urlCheck" : false,
+ "minified" : true,
+ "postcss" : true
+ "permission" : {},
+ // "scope.userLocation" : {
+ // "desc" : "你的位置信息将用于小程序位置接口的效果展示"
+ // "scope.writePhotosAlbum" : {
+ // "desc" : "保存图片到相册"
+ "requiredPrivateInfos" : [ "getLocation", "chooseLocation" ],
+ "lazyCodeLoading" : "requiredComponents"
+ "mp-alipay" : {
+ "usingComponents" : true
+ "mp-baidu" : {
+ "mp-toutiao" : {
+ "uniStatistics" : {
+ "enable" : false
+ "vueVersion" : "2",
+ "h5" : {
+ "devServer" : {
+ "port" : 8080, //浏览器运行端口
+ "disableHostCheck" : true,
+ "proxy" : {
+ "/api" : {
+ // "disableHostCheck" : true,
+ "target" : "https://greatadmin.dev.gztjy.top/", //请求的目标域名
+ "changeOrigin" : true,
+ "secure" : false,
+ "pathRewrite" : {
+ //使用代理; 告诉他你这个连接要用代理
+ "^/api" : ""
+ "https" : false
+ "sdkConfigs" : {
+ "maps" : {}
+ "title" : "伟大转折",
+ "router" : {
+ "mode" : "history",
+ "base" : ""
+ "template" : ""
+ "fallbackLocale" : "zh-Hans"
@@ -0,0 +1,18 @@
+export const systemInfo = {
+ data: () => ({
+ statusBarHeight: 0,
+ navigationBarHeight: 0,
+ navHeight: 0,
+ windowHeight: 0, // 可使用窗口高度
+ }),
+ // 获取设备信息
+ getSystemInfo() {
+ this.statusBarHeight = getApp().globalData.statusBarHeight
+ this.navigationBarHeight = getApp().globalData.navigationBarHeight
+ this.windowHeight = uni.getSystemInfoSync().windowHeight
+ this.navHeight = getApp().globalData.navHeight
@@ -0,0 +1,16 @@
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "moment": {
+ "version": "2.29.4",
+ "resolved": "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
+ "weixin-jsapi": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/weixin-jsapi/-/weixin-jsapi-1.1.0.tgz",
+ "integrity": "sha512-986drpgKs1yf8gK1A/hrdF2U5cjp/zW/7bhoL37JLoePhNiO14JqZpa+wlNslge0Hlw7gEXMTnEntFvvSXz8Bw=="
@@ -0,0 +1,28 @@
+ "uni-app": {
+ "scripts": {
+ "build:build64": {
+ "title": "build:build64",
+ "env": {
+ "UNI_PLATFORM": "h5",
+ "H_NODE_ENV": "development",
+ "H_BASE_URL": "https://greath5.dev.gztjy.top/serviceapi",
+ "H_UP_FILE_URL": "https://serviceapi.wdzzgs.com/thirdapi/upload/single/minio"
+ "build:buildOnline": {
+ "title": "build:online",
+ "H_NODE_ENV": "production",
+ "H_BASE_URL": "https://h5.wdzzgs.com/serviceapi",
+ "moment": "^2.29.4",
+ "weixin-jsapi": "^1.1.0"
@@ -0,0 +1,41 @@
+ "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+ {
+ "path": "pages/index/index",
+ "style": {
+ "navigationBarTitleText": "首页",
+ "navigationStyle": "custom"
+ "path": "pages/login/login",
+ "navigationBarTitleText": "登录",
+ ],
+ "subPackages": [
+ "root": "center",
+ "pages": [
+ "path": "center",
+ "navigationBarTitleText": "我的",
+ "preloadRule": {
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "uni-app",
+ "navigationBarBackgroundColor": "#F8F8F8",
+ "backgroundColor": "#F8F8F8"
+ "uniIdRouter": {}
@@ -0,0 +1,53 @@
+ <image class="main-img" src="../../static/main.png"></image>
+ import tabbar from "../../components/tabbar.vue";
+ tabbar,
+ // cartfixed
+ beforeRouteLeave() {
+ onLoad(query) {
+ onReady() {
+ onUnload() {
+background-color: #fff;
+.main-img{
+ height: 100vh;
+ height: 100dvh;
@@ -0,0 +1,291 @@
+ <view class="login-content">
+ <view class="login-content-info">
+ <view class="login-bgm":style="{backgroundImage:`url(${staticUrl}/img/tuan-index-bg.png)`}">
+ <image class="login-logo" :src="staticUrl+'/img/logo.png'" mode="scaleToFill"></image>
+ <text>{{ title }}</text>
+ <view class="login-info">
+ <view class="login-info-box">
+ <text class="login-info-title">账号密码登陆</text>
+ <view class="login-info-form">
+ <u--form
+ labelWidth="0"
+ :borderBottom="false"
+ :model="form"
+ :rules="rules"
+ ref="uForm">
+ <u-form-item prop="mobile">
+ <u--input
+ v-model="form.mobile"
+ placeholder="请输入账号"
+ border="surround"
+ shape="circle"
+ prefixIcon="account-fill"
+ prefixIconStyle="font-size: 22px;color: #909399"
+ ></u--input>
+ </u-form-item>
+ <u-form-item prop="password">
+ v-model="form.password"
+ placeholder="请输入账号密码"
+ prefixIcon="lock-fill"
+ :password="true"
+ </u--form>
+ <view class="login-info-submit">
+ <u-button
+ class="login-info-submit-but"
+ @click="submit()"
+ :loading="loading"
+ loadingText="登录中..."
+ >登录</u-button>
+ <view class="login-info-tip">暂不支持自行注册</view>
+ code:'',
+ title: '《伟大转折》剧目团购系统',
+ logoUrl: this.$commonConfig.staticUrl + "login/logo.png",
+ loading: false,
+ backUrl:null,
+ form: {
+ mobile: '',
+ password: '',
+ rules: {
+ 'mobile': {
+ type: 'string',
+ required: true,
+ message: '请填写账号',
+ trigger: ['blur', 'change']
+ 'password': {
+ message: '请填写密码',
+ //如果需要兼容微信小程序,并且校验规则中含有方法等,只能通过setRules方法设置规则。
+ this.$refs.uForm.setRules(this.rules)
+ onLoad(e) {
+ uni.getStorage({
+ success: function (res) {
+ // console.log('getStorage',res);
+ complete(res) {
+ if(res.data){
+ that.backUrl = '/'+res.data;
+ that.backUrl = '/pages/index/index';
+ console.log('backUrl',that.backUrl);
+ key:'mobile',
+ success: (res) => {
+ that.form.mobile = res.data
+ // console.log('accessToken=====',this.vuex_user_info.accessToken);
+ let accessToken = this.vuex_user_info?.accessToken;
+ if(accessToken){
+ this.$u.api.teamLoginCheck().then(res=>{
+ if(this.backUrl.includes('login/login')||this.backUrl.includes('main/index')){
+ this.backUrl = '/pages/index/index'
+ uni.reLaunch({url: this.backUrl});
+ console.log('teamLoginCheck',err);
+ uni.$u.vuex('vuex_user_info', {});
+ this.$u.toast(err.msg)
+ this.redirectToAuth()
+ if(!e.code) { // 微信第三方登录失败
+ this.code = e.code
+ // 测试环境填充用户名密码
+ if(process.env.NODE_ENV=='development'){
+ this.form.mobile = '13682277062';
+ this.form.password = '123456';
+ /** 公众号 微信授权登录 */
+ redirectToAuth() {
+ try{
+ window.location.href = this.$commonConfig.authUrl;
+ }catch(e){
+ alert(`redirectToAuth e:${e}`)
+ * 提交登录
+ async submit(e) {
+ let _this = this;
+ let wxinfo = null
+ let data = {}
+ wxinfo = await this.$u.api.wxinfoH5({code:this.code});
+ let { openid } = wxinfo.data;
+ this.form.h5OpenId = openid;
+ this.loading = true
+ this.$refs.uForm.validate().then(res => {
+ this.$u.toast('校验通过');
+ data:this.form.mobile
+ this.$u.api.teamLogin(this.form).then(res=>{
+ this.loading = false
+ // console.log('res',res.data);
+ this.$u.vuex('vuex_user_info', res.data);
+ console.log('login',err);
+ this.$u.toast(err.msg);
+ this.loading = false;
+ setTimeout(()=>{
+ this.redirectToAuth();
+ },1500)
+ }).catch(errors => {
+ this.$u.toast('校验失败')
+ .login-content {
+ box-sizing: border-box;
+ --bgm-height: 630rpx;
+ /** 背景 */
+ .login-content-box {
+ height: 100%;
+ .login-bgm {
+ height: 630rpx;
+ // background-image: url("#{$image-beas-url}login/bgm.png");
+ background-size: 100% auto;
+ background-repeat: no-repeat;
+ .login-logo {
+ width: 240rpx;
+ height: 172rpx;
+ padding: 50rpx 0;
+ >text {
+ font-family: SourceHanSansCN, SourceHanSansCN;
+ color: #FFD788;
+ .login-info {
+ height: calc(100% - var(--bgm-height) + 40rpx);
+ border-radius: 28rpx 28rpx 0rpx 0rpx;
+ bottom: 0;
+ left: 0;
+ z-index: 22;
+ padding: 80rpx;
+ .login-info-box {
+ .login-info-title {
+ color: #2D2D2D;
+ padding-bottom: 40rpx;
+ .login-info-form {
+ .login-info-submit {
+ height: 80rpx;
+ padding: 40rpx 0 20rpx;
+ flex-shrink: 0;
+ .login-info-submit-but {
+ border-radius: 40rpx;
+ width: 100% !important;
+ height: 100% !important;
+ background-color: #ed0000;
+ .login-info-tip {
+ color: #C2C2C2;
@@ -0,0 +1,332 @@
+ <view class="body" :style="{height:screenHeight}">
+ <image class="login-bg" :src="staticUrl+'/img/login-bg.png'" mode="widthFix"></image>
+ <view style="height: 40%;position: relative;z-index: 10;">
+ <view class="logo-wrap">
+ <img :src="logoSrc" class="logo" alt="">
+ <view class="btn-wrap" style="margin:94rpx">
+ :hair-line='false'
+ type="error"
+ color="#ED0000"
+ @click="disabledClick"
+ shape="circle">登录
+ </u-button>
+ <view class="rule-wrap u-flex u-flex-wrap u-row-center">
+ <u-checkbox-group v-model="checked" @change="checkboxChange" placement="row">
+ <u-checkbox activeColor="#1677FF" name="同意" labelSize="24rpx" shape="circle" label="我已阅读并同意"></u-checkbox>
+ </u-checkbox-group>
+ <text class="link" @click="$u.route('/pages/login/regulation',{regulationName:'用户服务协议',type:1})">《用户服务协议》</text>
+ 和<text class="link" @click="$u.route('/pages/login/regulation',{regulationName:'用户隐私政策',type:2})">《用户隐私政策》</text>
+ checked:false,
+ checkboxVal:null,
+ loginBtn:true,
+ bname:'旭烁集团',
+ //屏幕高度
+ screenHeight: "",
+ logoSrc: "/static/logo.png",
+ // sitename:"/static/sitename.png",
+ miniappLoginInfo:null,
+ hasUserInfo:false,
+ userInfo: null,
+ user:{
+ avatar:'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
+ name:'',
+ mobile:'',
+ code: '',
+ showAuthorizeUser: false,
+ showAuthorizePhone: false,
+ customStyleUnOk:{},
+ customStyleOk:{'margin-left': '30px',color:'#00A447'},
+ scene:'',
+ console.log("回调参数=======",e)
+ if(e&&e.code) { // 微信第三方登录成功
+ this.login(e)
+ //获取屏幕高度,我的项目再store里已经取到了
+ uni.getSystemInfo({
+ this.screenHeight = res.windowHeight + "px"
+ let pages = getCurrentPages(); //当前页面栈
+ console.log("pages=====",pages)
+ let userInfo = uni.getStorageSync('userInfo');
+ this.user.name = userInfo.name;
+ openAuth(){
+ this.showAuthorizePhone = true;
+ //获取昵称输入内容
+ // userNameInput(e){
+ // this.user.nickName = e.detail.value
+ onChooseAvatar(e) {
+ console.log('头像信息》')
+ console.log(e)
+ this.user.avatar = e.detail.avatarUrl;
+ async login(e){
+ // console.log('e',e);
+ let wxinfo = await this.$u.api.wxinfoH5({code:this.code});
+ let {openid,nickname} = wxinfo.data;
+ // console.log('----------登陆中',data)
+ this.$u.api.login({
+ "openId": "",
+ "h5OpenId":"openid",
+ "mobile": ""
+ }).then(res=>{
+ // console.log('微信登录返回结果========',res.data)
+ _this.$u.vuex('vuex_user_info', res.data);
+ // _this.$u.vuex('vuex_member_info',res.data);
+ // uni.setStorageSync('userInfo', res.data)
+ this.showAuthorizePhone = false;
+ uni.removeStorage({
+ key:'scene'
+ // console.log('res.data.userid',res.data.userid);
+ this.getMemberInfo(res.data.userId);
+ console.log('err',err);
+ this.showAuthorizePhone = false
+ * @param {Object} userid
+ * 根据userId 获取用户信息
+ getMemberInfo(userid){
+ // console.log('userid',userid);
+ this.$u.api.memberInfo({id:userid}).then(res=> {
+ if(res.code ===200) {
+ _this.userInfo = res.data;
+ // this.$u.vuex('vuex_member_info.avatar', 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0');
+ if(!res.data.name){
+ this.$u.vuex('vuex_member_info.name', '微信用户');
+ this.updateMemberInfo();
+ this.goBack();
+ * 更新用户信息
+ updateMemberInfo(){
+ let params ={
+ id:this.userInfo.id,
+ // avatar:'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
+ name:'微信用户'
+ this.$u.api.updateMemberInfo(params).then(res=>{
+ goBack(){
+ let url = this.backUrl&&this.backUrl.length>0?this.backUrl:'/pages/index/index';
+ // console.log('url',url);
+ // console.log('success');
+ uni.reLaunch({url: decodeURIComponent(url)});
+ checkboxChange(e){
+ this.checkboxVal = e[0];
+ disabledClick(){
+ console.log("this.checked====",this.checked)
+ // console.log('checked',this.checked?.length);
+ // console.log('loginBtn',this.loginBtn);
+ if(!this.checked||this.checked?.length<=0){
+ title:'请先同意使用条款!',
+ icon:'none'
+ title:'登录中!',
+ /** 微信授权登录 */
+ const appid = 'wx6490eaa0d20d2be2';
+ const redirectUri = encodeURIComponent('https://greath5.dev.gztjy.top/pages/login/loginh5');
+ const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`;
+ window.location.href = authUrl;
+ getAccessToken() {
+ const code = (new URLSearchParams(window.location.search)).get('code');
+ if (code) {
+ const requestUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=YOUR_APP_SECRET&code=${code}&grant_type=authorization_code`;
+ fetch(requestUrl)
+ .then(response => response.json())
+ .then(data => {
+ if (data && data.openid) {
+ const openid = data.openid;
+ // 在这里可以存储或使用用户的openid
+ // ...
+ .catch(error => {
+ console.error(error);
+.login-bg{
+ right: 0;
+.body {
+ .logo-wrap {
+ padding-top: 248rpx;
+ img{
+ margin: 0 auto;
+ .logo{
+ width: 306rpx;
+ height: 258rpx;
+ .sitename{
+ width: 460rpx;
+ height: 142rpx;
+//
+.auth-btncard{
+ margin-top: 20px;
+ .btn-unok{
+ width: 45%;
+ .btn-ok{
+ margin: 0;
+ border: 0px solid transparent; //自定义边框
+ outline: none; //消除默认点击蓝色边框效果
+ u-button{
+ font-size: 16px;
+ // border: 0px solid transparent; //自定义边框
+.auth-card{
+ .avatar-img{
+ width: 150rpx;
+ height: 150rpx;
+ border-radius: 100%;
+ margin-top: 30rpx;
+ .title{
+ font-size: 30rpx;
+ .content{
+ margin-top: 10rpx;
+.avatar-wrapper{
+ color: #333 !important;
+ text-align: center !important;
+ border: none !important;
+ border-radius:0 !important;
+ background-color:transparent !important;
+.avatar-wrapper::after {
+.avatar{
+.rule-wrap{
+ bottom: 20px;
+ margin: 40rpx auto;
+ line-height: 1.5;
+ .link{
+ white-space: nowrap;
+ color: #1677FF;
@@ -0,0 +1,60 @@
+ <u-navbar
+ :title="title"
+ :autoBack="true"
+ :safeAreaInsetTop="true"
+ </u-navbar>
+ <!-- <view class="title">积分规则</view> -->
+ <view class="page-wrap parse-content">
+ <u-parse :content="content"></u-parse>
+ title:'',
+ regulationName:'',
+ content:'',
+ type:'',
+ onLoad(page) {
+ this.title = page.regulationName;;
+ this.regulationName = page.regulationName;
+ this.type = page.type;
+ // console.log('page',page);
+ this.getPageData();
+ getPageData(){
+ this.$u.api.getAgreement({type:this.type}).then(res=>{
+ this.content = res.data.content
+ console.log('res',res.data);
+ console.log('memberCreditDesc',err);
+ background-color: #f5f5f5;
@@ -0,0 +1,389 @@
+$pagegap:32rpx;
+.page-wrap{padding: $pagegap;}
+.top-search{
+ z-index: 31;
+ background-color: rgba(0,0,0,0.04);
+ border-radius: 100rpx;
+ margin-left: $pagegap;
+.search-wrap{
+ padding: 20rpx;
+ margin-bottom: 24rpx;
+.search-wrap::after {
+ content: "";
+ height: 14rpx;
+ bottom: -20rpx;
+ background: linear-gradient(to bottom, #F5F5F5, #fff);
+ z-index: -1;
+ .position{
+ /deep/ .u-icon{
+ top: 5rpx;
+ margin-left: 8rpx;
+ .search-out{
+ flex: 1;
+ border: 1px solid #DADADA;
+ border-radius: 50rpx;
+ margin-left: 24rpx;
+ .conditions{
+ // margin-top: 48rpx;
+ .item{
+ margin: 0 24rpx;
+ padding: 48rpx 0 20rpx;
+ .text{
+ margin-right: 10rpx;
+.gray{
+ color: #999;
+.red{
+ color: #FF3C3F;
+.view-wrap{
+ padding: 30rpx 20rpx;
+.full-btn{
+ background-color: #F01414;
+ border-radius: 44rpx;
+ padding: 22rpx 0;
+ margin-bottom: 40rpx;
+ margin-top: 20rpx;
+ &.gray{
+ background-color: #ddd;
+ &.white{
+ color: #606060;
+ &.red{
+ background-color: #ED0000;
+.single-til{
+ color: #333;
+ .sub-title{
+ margin-left: 20rpx;
+ color: #999999;
+ .more-text{
+.date-list{
+ .date-item{
+ border-radius: 16rpx;
+ border: 1rpx solid #7F7F7F;
+ margin: 0 8rpx;
+ &.active{
+ border-color: #ED0000;
+ background-color: #FFC8C8 ;
+ .name,.date{
+ color: #ED0000;
+ .selected-img{
+ width: 32rpx;
+ height: 32rpx;
+ display: none;
+ &.dot::after{
+ content: '';
+ width: 4px;
+ height: 4px;
+ right: 10rpx;
+ top: 5px; }
+ padding-top: 26rpx;
+ margin-bottom: 12rpx;
+ color: #7F7F7F;
+ line-height: 36rpx;
+ .date{
+ font-weight: 500;
+ padding-bottom: 28rpx;
+ .more-date{
+ height: 148rpx;
+.date-block.generic-block{
+ .date-list{
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 10rpx;
+ margin-bottom: 0;
+ align-content: center;
+ height: 52px;
+ // padding: 34rpx 0;
+// 分享 海报
+.share-option{
+ transform: translateY(100%);
+ .overlay{
+ top: 0;
+ width: 100vw;
+ background-color: rgba(0, 0, 0, 0.35);
+ transform: translateY(-100%);
+ &.shareShow{
+ transform: translateY(0);
+ z-index: 50;
+ .share-option-item{
+ height: 46px;
+ line-height: 46px;
+ border-radius: 0;
+ border-bottom: 1px solid #eee;
+ .wx-share{
+ border-bottom: 0;
+.poster-wrap{
+ .poster-inner{
+ margin: 0 75rpx;
+ .close-wrap{
+ justify-content: flex-end;
+ margin-bottom: 16rpx;
+ .poster{
+ // padding-bottom: 90rpx;
+ .posterBg{
+ z-index: 1;
+ .placard{
+ z-index: 10;
+ .bottom{
+ z-index: 20;
+ margin: 33rpx 40rpx 0;
+ .left{
+ margin-right: 58rpx;
+ .price{
+ font-size: 22rpx;
+ color: #FF3538;
+ line-height: 30rpx;
+ .num{
+ font-size: 40rpx;
+ margin-left: 5rpx;
+ .goodsName{
+ line-height: 38rpx;
+ .slogan{
+ font-size: 26rpx;
+ line-height: 37rpx;
+ .right{
+ .imgTip{
+ margin-top: 12rpx;
+ font-size: 20rpx;
+ line-height: 28rpx;
+ .qrcode{
+ width: 108rpx;
+ height:108rpx;
+ .poster-btn{
+ height: 88rpx;
+ line-height: 88rpx;
+ margin-top: 40rpx;
+ background: linear-gradient(90deg, #FF7979 0%, #ED0000 100%);
+// 富文本
+.parse-content{
+ color: #4E4E4E;
+ line-height: 40rpx;
+ rich-text,p{
+ margin-bottom: 8px;
+// 剧场
+.programme{
+ border-radius: 30rpx;
+ // background: radial-gradient(circle at -26rpx 230rpx, transparent 10%, #fff 4%) left, radial-gradient(circle at calc( 100% + 26rpx ) 232rpx, transparent 10%, #fff 4%) right;
+ background-size: 50% 100%;
+ .img{
+ height: 242rpx;
+ padding: 32rpx 30rpx;
+ // margin-bottom: 18rpx;
+ .addr{
+ line-height: 34rpx;
+ .btn{
+ height: 51rpx;
+ line-height: 51rpx;
+ padding: 0 24rpx;
+ border-radius: 25rpx;
+ .share{
+ right: 16rpx;
+ top: 16rpx;
+ padding: 10rpx;
+ background-color: rgba(0,0,0,0.4);
+ .icon{
@@ -0,0 +1,71 @@
+.u-flex {
+ flex-direction: row;
+.u-flex-wrap {
+ flex-wrap: wrap;
+.u-flex-nowrap {
+ flex-wrap: nowrap;
+.u-col-center {
+.u-col-top {
+ align-items: flex-start;
+.u-col-bottom {
+ align-items: flex-end;
+.u-row-center {
+.u-row-left {
+ justify-content: flex-start;
+.u-row-right {
+.u-row-between {
+ justify-content: space-between;
+.u-row-around {
+ justify-content: space-around;
+.u-text-left {
+ text-align: left;
+.u-text-center {
+.u-text-right {
+ text-align: right;
+.u-flex-col {
+ /* #ifndef APP-NVUE */
+ /* #endif */
+// 定义flex等分
+@for $i from 0 through 12 {
+ .u-flex-#{$i} {
+ flex: $i;
@@ -0,0 +1,419 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 750 422" style="enable-background:new 0 0 750 422;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:url(#SVGID_1_);}
+ .st1{fill:url(#SVGID_00000140000486157882826870000007221579918701196449_);}
+ .st2{fill:url(#SVGID_00000036931254733988851740000009953821978700827530_);}
+ .st3{fill:url(#SVGID_00000158731152278408604530000000055075175893880453_);}
+ .st4{fill:url(#SVGID_00000080928632844849270810000017537741479220234144_);}
+ .st5{fill:url(#SVGID_00000091015095995676809280000009079845705063013795_);}
+ .st6{fill:url(#SVGID_00000151545769631247894970000005004757807824389025_);}
+ .st7{fill:url(#SVGID_00000128475466696929793450000010403830378602305940_);}
+ .st8{fill:url(#SVGID_00000036248896964300413530000004462419411256015785_);}
+ .st9{fill:url(#SVGID_00000022548248585355353180000005833023100168402343_);}
+ .st10{fill:url(#SVGID_00000099640502816314516610000015645311528596044965_);}
+ .st11{fill:url(#SVGID_00000143599212459675872200000007240227900190114986_);}
+ .st12{fill:url(#SVGID_00000153699669491735575770000010502572629283511947_);}
+ .st13{fill:url(#SVGID_00000139271213937430465080000012685673228554600854_);}
+ .st14{fill:url(#SVGID_00000160165960824655412760000004520554627207360142_);}
+ .st15{fill:url(#SVGID_00000030488478326947421440000017588227068186671240_);}
+ .st16{fill:url(#SVGID_00000053527995893557589750000003927760824008387238_);}
+ .st17{fill:url(#SVGID_00000100355412462811441330000011505244038520995482_);}
+ .st18{fill:url(#SVGID_00000178199887920956096310000018072506848948699044_);}
+ .st19{fill:url(#SVGID_00000140727996621273254330000008246246362703009698_);}
+ .st20{fill:url(#SVGID_00000053507488083599379620000009634741184751434682_);}
+<g>
+ <g>
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="527.5886" y1="139.4055" x2="527.5886" y2="278.2872">
+ <stop offset="0" style="stop-color:#DEA54E"/>
+ <stop offset="0.1421" style="stop-color:#FCFADF"/>
+ <stop offset="0.1594" style="stop-color:#FBF4CF"/>
+ <stop offset="0.1982" style="stop-color:#F9E9B0"/>
+ <stop offset="0.2409" style="stop-color:#F7E097"/>
+ <stop offset="0.2882" style="stop-color:#F6DA83"/>
+ <stop offset="0.3426" style="stop-color:#F5D575"/>
+ <stop offset="0.4104" style="stop-color:#F4D26D"/>
+ <stop offset="0.5334" style="stop-color:#F4D16B"/>
+ <stop offset="0.8344" style="stop-color:#C26719"/>
+ <stop offset="1" style="stop-color:#F4CD9C"/>
+ </linearGradient>
+ <path class="st0" d="M538.99,272.42c-0.19-1.2,0.01-2.05,0.62-2.89c1.6-2.19,2.46-4.71,2.34-7.75
+ c-0.24-6.09,0.25-12.15,0.67-18.2c0.01-0.13,0-0.27-0.13-0.54c-1.01,1.35-2.07,2.65-3.02,4.06c-2.26,3.38-3.72,7.21-4.18,11.57
+ c-0.06,0.54-0.07,1.09-0.1,1.63c-0.11,1.78-1.07,2.61-2.38,1.85c-1.19-0.69-2.31-1.71-2.71-3.34c-0.21-0.86-0.13-1.88-0.04-2.81
+ c0.68-6.73,1.4-13.45,2.12-20.17c0.2-1.89,0.42-3.78,0.62-5.67c0.03-0.25,0-0.52,0-0.89c-0.27,0.29-0.49,0.51-0.7,0.74
+ c-3.36,3.83-6.72,7.67-10.09,11.49c-0.3,0.34-0.41,0.65-0.36,1.19c0.49,4.73,0.93,9.46,1.38,14.2c0.02,0.21,0,0.43,0,0.69
+ c-0.92,0.3-1.42,1.17-1.76,2.17c-0.36,1.05-0.63,2.15-0.95,3.22c-0.12,0.38-0.25,1.02-0.44,1.05c-0.56,0.1-1.17,0.26-1.71-0.32
+ c-1.33-1.45-2.7-2.85-4.05-4.28c-0.93-0.99-0.92-0.99-0.78-2.65c0.27-0.04,0.55-0.08,0.83-0.13c0.93-0.18,1.38-0.79,1.38-1.93
+ c0.01-1.71,0-3.42,0-5.36c-1.08,1.28-2.05,2.38-2.98,3.54c-2.2,2.76-4.34,5.59-5.88,8.97c-0.65,1.41-1.05,2.98-1.6,4.47
+ c-0.08,0.21-0.3,0.46-0.47,0.48c-0.51,0.06-1.03,0.04-1.54,0.02c-0.54-0.02-2.55-2.5-2.58-3.16c0-0.03,0-0.06,0-0.08
+ c-0.18-1.56,0.27-2.72,1.17-3.91c4.58-6.01,9.07-12.11,13.57-18.2c0.18-0.25,0.28-0.67,0.29-1.02c0.02-2.43,0.06-4.86,0-7.28
+ c-0.06-2.28,0.18-4.5,0.61-6.71c0.06-0.31,0.09-0.63,0.14-0.95c-0.58-0.32-0.92,0.1-1.28,0.47c-2.77,2.82-5.76,5.22-9.14,6.82
+ c-2.28,1.08-4.64,1.79-7.09,1.77c-1.37-0.01-2.46-0.9-3.34-2.11c-0.49-0.68-0.28-1.47,0.37-1.91c4.7-3.16,9.41-6.31,14.11-9.49
+ c1.32-0.89,2.69-1.73,3.88-2.86c2.7-2.58,3.83-6.17,4.03-10.27c0.03-0.64,0.1-1.31,0.29-1.9c0.85-2.8,1.73-5.6,2.64-8.37
+ c0.26-0.77,0.65-1.49,1.05-2.17c0.35-0.59,1.09-0.61,1.54-0.15c3.17,3.3,4.56,4.13,8.8,5.27c0.23,1.96,0.48,3.95,0.67,5.94
+ c0.03,0.27-0.15,0.65-0.33,0.87c-2.15,2.69-4.39,5.28-6.46,8.05c-1.7,2.28-2.91,4.91-3.05,8.16c-0.09,2.11-0.44,4.21-0.67,6.31
+ c-0.23,2.1-0.45,4.21-0.68,6.31c0.07,0.02,0.12,0.06,0.14,0.05c4.06-3.31,7.9-6.95,10.9-11.73c1.88-3,2.89-6.34,3.33-10.15
+ c0.5-4.4,1.55-8.72,3.28-12.7c0.19-0.43,0.44-0.82,0.69-1.2c0.4-0.62,0.91-0.95,1.57-0.85c0.09,0.01,0.18,0.01,0.27,0
+ c0.98-0.1,1.76,0.16,2.6,0.97c1.95,1.89,4.35,2.7,6.73,3.45c2.22,0.7,4.47,1.23,6.71,1.84c0.21,0.06,0.43,0.13,0.62,0.19
+ c0.19,1.12,0.01,1.97-0.68,2.81c-2,2.46-3.96,4.96-6.3,6.96c-1.25,1.06-2.55,1.99-4.26,2.19c0.82,1,1.53,1.97,2.33,2.82
+ c0.54,0.58,0.65,1.14,0.49,1.98c-0.34,1.82-0.74,3.65-0.6,5.56c0.01,0.16,0.02,0.31,0.03,0.64c1.54-1.25,2.99-2.43,4.49-3.65
+ c-0.42-0.78-0.79-1.55-1.23-2.26c-0.26-0.43-0.23-0.77-0.02-1.18c0.71-1.39,1.84-1.82,3.07-2c0.51-0.07,1.03-0.02,1.54-0.1
+ c0.85-0.13,1.43,0.29,2.05,1c0.69,0.79,0.73,1.62,0.56,2.58c-0.39,2.25-1.46,4.06-2.77,5.58c-2.08,2.42-4.31,4.65-6.46,6.98
+ c-0.28,0.3-0.5,0.74-0.62,1.16c-0.58,2-0.71,4.08-0.65,6.19c0.05,2.02,0.09,4.04-0.01,6.05c-0.15,3.23-0.39,6.46-0.62,9.69
+ c-0.05,0.75-0.04,1.34,0.4,2.01c0.29,0.43,0.3,1.41,0.11,1.98c-0.37,1.08-0.86,2.18-1.51,3.03c-2.04,2.66-4.62,4.43-7.24,6.08
+ C540.15,272.51,539.55,272.37,538.99,272.42z M544.91,220.07c-0.06-0.06-0.12-0.13-0.17-0.19c-0.67,0.4-1.38,0.73-2.02,1.21
+ c-1.72,1.31-2.73,3.33-3.46,5.56c-1.17,3.55-1.57,7.3-1.74,11.09c-0.06,1.37-0.01,2.74-0.01,4.19c0.32-0.26,0.64-0.5,0.94-0.76
+ c3.37-3.01,4.93-7.26,5.48-12.15c0.24-2.1,0.35-4.22,0.56-6.32C544.58,221.81,544.77,220.94,544.91,220.07z M541.17,217.23
+ c0.43-0.09,0.71-0.07,0.94-0.2c2.66-1.61,5.33-3.17,7.65-5.48c0.46-0.46,0.83-1.07,1.32-1.71c-2.63-1.29-5.08-2.5-7.54-3.68
+ c-0.23-0.11-0.52-0.05-0.84-0.08C542.18,209.77,541.69,213.41,541.17,217.23z M524.55,203.52c-0.21,2.04-0.87,3.98-0.68,5.94
+ c0.95-1.15,1.9-2.31,2.77-3.37C525.99,205.29,525.3,204.43,524.55,203.52z"/>
+ </g>
+ <linearGradient id="SVGID_00000155830357604304887680000002910475876380659645_" gradientUnits="userSpaceOnUse" x1="461.3712" y1="139.4055" x2="461.3712" y2="278.2872">
+ <path style="fill:url(#SVGID_00000155830357604304887680000002910475876380659645_);" d="M484.35,197.92
+ c-1.26,0.58-0.8,2.06-0.58,3.19c0.43,2.22,1.63,3.84,3,5.28c0.4,0.41,0.6,0.84,0.69,1.49c0.21,1.58,0.97,2.85,1.78,4.06
+ c0.21,0.32,0.44,0.62,0.66,0.92c0.63,0.85,0.66,1.12,0.21,2.17c-1.35,3.13-2.84,6.14-5.14,8.38c-0.86,0.84-1.85,1.47-2.85,2.24
+ c0.4,3.42,0.85,6.97,1.22,10.53c0.38,3.67,1.08,7.29,1.03,11.02c-0.02,1.35-0.44,2.36-1.29,3.15c-1.36,1.27-2.96,1.87-4.6,2.32
+ c-2.94,0.81-5.92,1.31-8.94,1.31c-0.73,0-1.34,0.17-1.91,0.73c-0.4,0.39-0.85,0.7-1.29,1.06c-1.71-1.5-3.72-1.5-5.69-1.71
+ c-0.67-0.07-1.34-0.04-2.01-0.05c-0.94-0.02-1.4-0.59-1.4-1.76c-0.01-3.34,1.53-5.35,4.29-5.56c0.96-0.07,1.92-0.06,2.88-0.04
+ c0.27,0.01,0.56,0.15,0.79,0.33c1.24,0.98,2.45,1.99,3.69,2.97c0.23,0.18,0.54,0.33,0.79,0.29c2.83-0.45,5.66-0.9,8.36-2.1
+ c1.01-0.45,2-1.08,2.9-1.8c0.97-0.78,1.28-1.92,1.05-3.41c-0.74-4.76-1.39-9.54-2.08-14.31c-0.01-0.07-0.05-0.14-0.11-0.28
+ c-0.16,0.11-0.3,0.2-0.44,0.32c-1.7,1.49-3.62,2.18-5.69,2.3c-1.13,0.06-1.7-1.06-1.17-2.28c1.79-4.11,3.57-8.22,5.39-12.31
+ c0.38-0.86,0.53-1.7,0.4-2.74c-0.17,0.18-0.32,0.31-0.45,0.47c-8.49,10.38-16.98,20.75-25.48,31.12
+ c-0.27,0.33-0.4,0.62-0.32,1.13c0.72,4.46,0.78,8.98,0.69,13.51c-0.01,0.31-0.08,0.66-0.22,0.9c-2.43,3.99-5.62,6.19-9.77,6.1
+ c-0.12,0-0.25-0.03-0.47-0.05c0.23-0.3,0.39-0.52,0.56-0.72c2.4-2.93,4.79-5.87,7.2-8.79c0.3-0.37,0.46-0.73,0.45-1.27
+ c-0.02-2.75-0.01-5.51-0.01-8.32c-0.12,0.04-0.21,0.04-0.27,0.09c-3.86,3.59-7.61,7.32-10.78,11.85
+ c-1.15,1.64-2.19,3.39-2.81,5.43c-0.25,0.83-0.38,1.72-0.62,2.86c1.85-0.35,3.5-0.66,5.15-0.97c0.02,0.04,0.04,0.09,0.06,0.13
+ c-1.06,1.11-2.11,2.22-3.17,3.33c-0.47,0.5-0.93,1.03-1.44,1.47c-0.28,0.25-0.65,0.4-0.99,0.49c-0.72,0.19-1.19-0.31-1.74-0.85
+ c-1.21-1.18-1.61-2.58-1.51-4.45c0.11-2.19,0.64-4.16,1.49-6.03c1.48-3.3,3.42-6.21,5.64-8.79c3.07-3.57,6.28-6.96,9.46-10.37
+ c0.55-0.59,0.8-1.08,0.6-1.97c-0.25-1.07-0.51-2.3-0.51-3.32c0-0.28-0.06-1.69-0.06-2.54c0-2.13-0.05-4.26,0.01-6.38
+ c0.05-1.69-0.18-3.32-0.59-4.9c-0.26-0.98-0.17-1.85,0.28-2.63c0.93-1.63,1.88-3.24,2.89-4.8c0.8-1.23,1.68-2.38,2.53-3.56
+ c-0.03-0.07-0.06-0.14-0.09-0.21c-0.32,0.15-0.68,0.24-0.95,0.47c-3.3,2.93-6.6,5.86-9.87,8.85c-0.7,0.64-1.37,1.06-2.25,0.91
+ c-0.3-0.05-0.62-0.01-1.03-0.01c-0.25-1.67-0.5-3.32-0.71-4.98c-0.03-0.27,0.08-0.67,0.25-0.87c2.01-2.38,4.24-4.34,7.06-5.02
+ c0.5-0.12,1.01-0.17,1.6-0.27c-0.16-0.23-0.29-0.41-0.42-0.58c-0.34-0.45-0.36-0.96-0.07-1.41c1.89-2.87,3.82-5.71,6.35-7.8
+ c0.56-0.47,1.23-0.76,1.86-1.08c0.41-0.21,0.75-0.11,1,0.44c0.28,0.62,0.64,1.18,0.96,1.77c0.34,0.64,0.36,1.24-0.13,1.82
+ c-0.58,0.68-0.57,1.16,0.01,1.86c0.87,1.06,1.74,2.11,2.62,3.18c-1.98,3.23-3.92,6.39-5.86,9.55c-0.96,1.57-1.94,3.13-2.88,4.72
+ c-0.16,0.27-0.29,0.63-0.3,0.95c-0.02,5.16-0.01,10.31-0.01,15.47c0,0.13,0.03,0.26,0.05,0.38c0.06,0.06,0.13,0.12,0.19,0.19
+ c0.13-0.25,0.23-0.54,0.4-0.75c4.74-5.86,9.43-11.77,14.24-17.54c2.08-2.5,4.22-5.04,6.64-6.96c2.58-2.05,4.19-5.05,6.21-7.65
+ c0.11-0.14,0.16-0.38,0.19-0.59c0.22-1.53,0.51-3.05,0.61-4.59c0.05-0.74-0.17-1.55-0.4-2.27c-0.25-0.76-0.21-1.41,0.12-2.08
+ c0.3-0.6,0.58-1.22,0.88-1.82c0.46-0.91,1.69-1.14,2.28-0.19c0.45,0.73,1.05,1.56,1.73,1.93
+ C484.46,197.65,484.52,197.85,484.35,197.92z M487.32,213.5c-1.37-0.22-1.35-1.58-1.53-2.78c-0.74,0.07-1.25,0.58-1.57,1.27
+ c-0.56,1.23-1.13,2.48-1.51,3.79c-0.55,1.88-0.94,3.83-1.42,5.85C484.5,220.42,487.28,216.65,487.32,213.5z"/>
+ <linearGradient id="SVGID_00000131327206248398065760000005869725996311911043_" gradientUnits="userSpaceOnUse" x1="352.6551" y1="139.4055" x2="352.6551" y2="278.2872">
+ <path style="fill:url(#SVGID_00000131327206248398065760000005869725996311911043_);" d="M351.45,208.97
+ c-0.66-0.13-1.26-0.19-1.84-0.37c-0.51-0.16-0.96-0.49-1.08-1.2c-0.13-0.73,0.21-1.25,0.66-1.56c1.53-1.05,3.06-2.1,4.65-3.01
+ c0.57-0.32,0.74-0.73,0.82-1.38c0.41-3.09,0.82-6.18,1.24-9.27c0.03-0.24,0.1-0.47,0.14-0.7c1.55-0.38,2.77,0.35,3.82,1.61
+ c1.07,1.28,1.4,2.92,1.42,4.85c0.46-0.15,0.86-0.2,1.2-0.41c2.68-1.61,5.33-3.26,8.01-4.86c0.36-0.22,0.76-0.34,1.16-0.37
+ c1.1-0.06,2.2-0.06,3.3-0.01c0.31,0.02,0.77,0.23,1.01,0.48c0.66,0.69,1.26,1.45,1.95,2.26c-2.53,3.06-4.99,6.04-7.55,9.14
+ c1.11,0.13,2.08,0.19,3.02,0.35c0.42,0.07,0.84,0.21,1.24,0.4c1.09,0.53,1.2,0.98,0.49,1.58c-5.4,4.59-10.8,9.18-16.2,13.77
+ c-0.18,0.16-0.36,0.32-0.55,0.47c-0.8,0.65-1.07,1.55-0.78,2.77c0.21-0.14,0.44-0.27,0.66-0.42c3.61-2.48,7.23-4.96,10.83-7.46
+ c0.31-0.22,0.69-0.17,0.97,0.1c0.8,0.78,1.64,1.52,2.52,2.34c0.71,0.63-1.17,1.2-2.29,2.04c-2.38,1.78-4.58,3.81-6.89,5.72
+ c-0.39,0.32-0.63,0.87-0.59,1.45c0.04,0.57,0.01,1.15,0.01,1.78c0,0,1.07,0.03,1.46,0.08c0.56,0.07,0.55,2.63,0,2.74
+ c-0.96,0.18-4.49,0.94-4.49,0.94c0.78,0.94,1.41,1.83,2.16,2.54c0.24,0.23,0.83,0.15,1.15-0.05c1.05-0.67,2.03-1.48,3.06-2.18
+ c0.26-0.18,0.65-0.27,0.92-0.17c1.15,0.41,2.3,0.83,3.41,1.38c0.69,0.34,1.19,0.95,1.31,1.8c0.08,0.58-1.29,0.61-1.95,0.79
+ c-2.69,0.73-5.29,1.75-7.49,3.89c-2.21,2.16-3.31,4.97-3.29,8.62c0.41-0.12,0.79-0.14,1.11-0.32c1.5-0.88,3.01-1.77,4.48-2.74
+ c0.78-0.52,1.53-0.71,2.42-0.49c1.3,0.32,2.63,0.48,4.02,0.72c0,0.81,0.03,1.57-0.02,2.33c-0.01,0.2-0.26,0.5-0.44,0.54
+ c-4.26,1.09-8.11,3.41-11.86,5.99c-0.36,0.25-0.49,0.47-0.38,1.02c0.25,1.24,0.41,2.5,0.62,3.81c0.3,2.19,0.02,4.83-0.07,5.87
+ c-0.33,3.69-1.64,6.81-3.87,9.33c-0.67,0.76-1.23,0.75-1.64-0.29c-0.42-1.06-0.79-2.24-0.85-3.39
+ c-0.19-3.72-0.22-7.46-0.31-11.19c-0.02-0.95,0-1.9,0-3.02c-0.44,0.11-0.81,0.14-1.14,0.3c-0.55,0.28-1.12,0.58-1.61,0.99
+ c-0.75,0.61-1.47,0.61-2.25,0.16c-0.34-0.2-0.71-0.31-1.05-0.51c-1.77-1.06-1.47-1.4-1.46-3.53c0-0.17,0.11-0.35,0.2-0.5
+ c0.57-0.95,1.17-1.88,1.73-2.84c0.18-0.3,0.48-0.43,0.77-0.35c0.52,0.16,1.07,0.19,1.57,0.39c1.18,0.49,2.29,0.32,3.43-0.24
+ c0.47-0.23,0.63-0.52,0.55-1.08c-0.2-1.28-0.36-2.58-0.62-3.84c-0.14-0.65-0.13-1.24,0.28-1.63c0.33-0.32,0.46-0.49,0.9-0.86
+ c-1.9-0.89-1.73-2.9-2.08-4.83c0.47,0,0.87,0.02,1.28-0.01c0.99-0.07,1.77-1.1,1.77-2.31c0-1.3,0-2.61,0-4.09l-2.19,1.74
+ c-1.17,0.97-1.41-1.34-2.1-2.06c-0.23-0.24-0.28-0.66-0.1-0.96c1.17-1.89,2.55-4.05,3.73-5.96c0.88-1.44,1.78-2.86,2.64-4.33
+ c0.2-0.34,0.24-0.8,0.36-1.21c-0.07-0.06-0.13-0.12-0.2-0.18c-0.45,0.33-0.94,0.59-1.33,1c-2.41,2.59-4.86,5.14-7.19,7.84
+ c-2.8,3.25-5.48,6.64-8.23,9.94c-0.26,0.31-0.6,0.53-0.96,0.59c-2.05,0.36-2.98,1.73-3.28,4.68c-0.34,3.35-0.73,6.69-1.17,10.02
+ c-0.23,1.71-0.43,3.38,0.26,5c0.19,0.45,0.04,0.7-0.27,0.92c-0.31,0.22-0.63,0.43-0.89,0.73c-0.77,0.9-1.72,0.86-2.63,0.69
+ c-0.36-0.07-0.79-0.55-0.95-0.97c-0.24-0.65-0.24-1.42-0.4-2.13c-0.06-0.25-0.23-0.56-0.41-0.66c-0.57-0.3-1.17-0.51-1.82-0.78
+ c0.32-2.05,0.63-4.07,0.95-6.09c1.14-7.21,2.27-14.42,3.45-21.63c0.09-0.54,0.38-1.08,0.69-1.5c0.39-0.53,0.88-0.3,1.06,0.4
+ c0.52,2.05,1.03,4.11,1.59,6.14c0.19,0.69,0.5,1.33,0.8,1.97c0.22,0.48,0.48,0.5,0.85,0.1c3.61-3.95,7.23-7.87,10.85-11.8
+ c0.13-0.14,0.25-0.32,0.4-0.44c1.98-1.57,2.55-3.93,2.35-7.34c-0.14-2.42,0.43-4.75,1.04-7.05
+ C351.47,209.35,351.45,209.18,351.45,208.97z M366.64,198.96c-0.06-0.05-0.12-0.1-0.18-0.15c-0.53,0.28-1.06,0.54-1.58,0.84
+ c-3.52,2.04-6.26,5.07-7.86,9.48c-0.16,0.43-0.19,0.93-0.29,1.48c0.2-0.05,0.25-0.05,0.29-0.08c0.19-0.14,0.38-0.29,0.56-0.44
+ c3.37-2.8,6.15-6.36,8.78-10.13C366.53,199.71,366.55,199.3,366.64,198.96z"/>
+ <linearGradient id="SVGID_00000052819573255971274660000006754478074310558641_" gradientUnits="userSpaceOnUse" x1="334.9821" y1="139.4055" x2="334.9821" y2="278.2872">
+ <path style="fill:url(#SVGID_00000052819573255971274660000006754478074310558641_);" d="M340.21,199.73
+ c0.82-0.12,1.52-0.14,2.18,0.63c1.05,1.22,2.21,2.29,3.32,3.42c0.71,0.72,0.74,1.11,0.14,1.94c-2.31,3.17-4.63,6.34-6.95,9.51
+ c-3.62,4.96-7.25,9.92-10.87,14.88c-0.88,1.2-1.83,1.25-2.76,0.13c-0.52-0.63-1.06-1.24-1.58-1.86
+ c3.16-3.55,5.72-7.61,8.36-11.57c2.65-3.97,5.21-8.02,7.84-12.01c0.21-0.32,0.57-0.48,1-0.83
+ C340.68,202.72,340.45,201.25,340.21,199.73z"/>
+ <linearGradient id="SVGID_00000104666638876684332600000004035634833883296189_" gradientUnits="userSpaceOnUse" x1="398.4344" y1="139.4055" x2="398.4344" y2="278.2872">
+ <path style="fill:url(#SVGID_00000104666638876684332600000004035634833883296189_);" d="M384.52,233.27
+ c-0.17-1.82-0.17-1.8,1.11-2.13c4.23-1.08,8.46-2.18,12.45-4.26c1.46-0.76,2.85-1.78,4.19-2.84c0.97-0.77,1.64-1.91,1.78-3.41
+ c0.02-0.24,0.19-0.48,0.33-0.68c1.94-2.67,2.64-5.87,2.67-9.34c0-0.32-0.22-0.69-0.4-0.97c-1.97-3.09-3.98-6.15-5.92-9.28
+ c-0.53-0.84-0.86-1.87-1.29-2.84c1.65-0.62,7.15,3.08,9.34,6.07c2.52,3.44,3.55,7.53,3.56,12.11c0.01,0.71,0.57,0.88,0.72,0
+ c0.14-1.39,0.89-2.26,1.81-2.94c1.72-1.27,3.63-1.9,5.56-2.48c0.13-0.04,0.31,0.01,0.44,0.08c0.9,0.54,1.79,1.09,2.83,1.73
+ c-0.41,0.73-0.76,1.44-1.19,2.07c-2.02,2.95-4.46,5.35-6.98,7.61c-3.76,3.36-7.61,6.57-11.39,9.89
+ c-0.52,0.46-1.02,1.06-1.35,1.73c-3.91,7.69-7.96,15.26-12.6,22.32c-1.41,2.14-2.93,4.18-4.4,6.26c-1.85,2.63-4.29,4.23-6.8,5.65
+ c-1.47,0.83-3.02,1.44-4.52,2.23c-0.65,0.34-0.96-0.15-1.19-0.62c-0.23-0.47-0.17-0.97,0.29-1.4c4.3-3.96,8.42-8.17,12.19-12.89
+ c3.5-4.39,6.71-9.07,8.89-14.64c0.71-1.8,1.22-3.73,1.75-5.63c0.35-1.24,0.2-1.31-0.75-1.83c-0.97-0.53-1.83-0.49-2.67,0.26
+ c-1.16,1.03-2.38,0.99-3.73,0.59c-1.02-0.31-2.11-0.48-3.17-0.43C385.57,233.31,384.6,233.81,384.52,233.27z"/>
+ <linearGradient id="SVGID_00000006698553838715441150000006468526626636131720_" gradientUnits="userSpaceOnUse" x1="419.8334" y1="139.4055" x2="419.8334" y2="278.2872">
+ <path style="fill:url(#SVGID_00000006698553838715441150000006468526626636131720_);" d="M412.17,263.54
+ c0.24-0.55,0.37-1,0.59-1.35c1.6-2.44,3.24-4.85,4.83-7.3c0.86-1.33,0.97-2.83,0.48-4.4c-0.29-0.91-0.53-1.86-0.94-2.7
+ c-0.92-1.9-1.92-3.73-2.88-5.59c-0.09-0.18-0.19-0.35-0.4-0.74c0.53,0.11,0.92,0.14,1.28,0.28c3.34,1.23,6.05,3.69,8.37,6.77
+ c1.31,1.74,2.47,3.65,3.67,5.51c0.54,0.84,0.39,2.43-0.3,3.1c-0.59,0.57-1.21,1.16-1.9,1.51c-3.35,1.68-6.71,3.34-10.11,4.87
+ C414.14,263.81,413.24,263.54,412.17,263.54z"/>
+ <linearGradient id="SVGID_00000047035691048151441870000014968092272306573230_" gradientUnits="userSpaceOnUse" x1="545.4971" y1="329.9934" x2="419.568" y2="310.3531">
+ <stop offset="0" style="stop-color:#9A0000"/>
+ <stop offset="0.4774" style="stop-color:#E50029"/>
+ <stop offset="0.5423" style="stop-color:#E81B22"/>
+ <stop offset="0.7009" style="stop-color:#ED5814"/>
+ <stop offset="0.8356" style="stop-color:#F18509"/>
+ <stop offset="0.9395" style="stop-color:#F4A102"/>
+ <stop offset="1" style="stop-color:#F5AB00"/>
+ <path style="fill:url(#SVGID_00000047035691048151441870000014968092272306573230_);" d="M499.16,318.17l-35.81,4.77
+ c-47.75,3.13-37.25-4.22-83.57-23.6c1.65,0.6,3.3,1.17,4.96,1.69c0.02,0,0.02,0,0.03,0.02c1.48,0.46,22.88,4.9,32.26,4.87
+ l31.65-0.1c16.17-0.05,32.15,3.56,46.73,10.55L499.16,318.17z"/>
+ <linearGradient id="SVGID_00000063609354156613872810000017220577945889751464_" gradientUnits="userSpaceOnUse" x1="353.0473" y1="318.1414" x2="401.3137" y2="298.116">
+ <stop offset="0.7601" style="stop-color:#E62C2E"/>
+ <path style="fill:url(#SVGID_00000063609354156613872810000017220577945889751464_);" d="M463.35,322.93l-52.5,7.41
+ c-4.24,0.59-8.47,0.94-12.71,1.02c-48.63-3.91-38.06-44.87-62.99-56.35c6.05,2.34,11.89,5.5,17.33,9.47l1.17,0.86
+ c8.07,5.88,16.87,10.58,26.13,13.99C426.1,318.71,415.59,326.06,463.35,322.93z"/>
+ <linearGradient id="SVGID_00000044859097233475420150000017294217224159958176_" gradientUnits="userSpaceOnUse" x1="343.198" y1="344.8954" x2="328.8208" y2="282.5085">
+ <path style="fill:url(#SVGID_00000044859097233475420150000017294217224159958176_);" d="M398.14,331.36
+ c-16.02,0.34-31.98-2.93-46.66-9.64c-2.85-1.31-5.76-2.4-8.72-3.31c-42.53-17.41-34.23-57.3-72.63-38.62
+ c11.57-6.59,24.54-9.92,37.56-9.92c9.29,0,18.59,1.69,27.42,5.13c0.02,0,0.03,0.02,0.05,0.02
+ C360.08,286.49,349.51,327.45,398.14,331.36z"/>
+ <linearGradient id="SVGID_00000136394704996483294990000018380544818187287462_" gradientUnits="userSpaceOnUse" x1="340.717" y1="141.6266" x2="491.3503" y2="156.9726">
+ <polygon style="fill:url(#SVGID_00000136394704996483294990000018380544818187287462_);" points="418.96,169.82 388.86,158.99
+ 393.13,126.74 "/>
+ <linearGradient id="SVGID_00000056406797688230079960000009773664603587219640_" gradientUnits="userSpaceOnUse" x1="451.2126" y1="177.6628" x2="347.0212" y2="33.8948">
+ <polygon style="fill:url(#SVGID_00000056406797688230079960000009773664603587219640_);" points="425.11,112.42 409.85,137.06
+ <linearGradient id="SVGID_00000007428621917166539450000015583986995171762837_" gradientUnits="userSpaceOnUse" x1="443.1714" y1="183.4904" x2="338.98" y2="39.7224">
+ <polygon style="fill:url(#SVGID_00000007428621917166539450000015583986995171762837_);" points="396.23,107.67 393.13,126.74
+ 377.09,80 "/>
+ <linearGradient id="SVGID_00000095330560046905565680000012653014443888594062_" gradientUnits="userSpaceOnUse" x1="432.4759" y1="201.5028" x2="349.4862" y2="106.3978">
+ <polygon style="fill:url(#SVGID_00000095330560046905565680000012653014443888594062_);" points="393.13,126.74 333.06,117.24
+ 366.09,109.03 "/>
+ <linearGradient id="SVGID_00000007418647100218523120000011985711963450850992_" gradientUnits="userSpaceOnUse" x1="405.4063" y1="87.2944" x2="357.3491" y2="166.0437">
+ <polygon style="fill:url(#SVGID_00000007418647100218523120000011985711963450850992_);" points="393.13,126.74 360.97,178.47
+ 360.34,141.81 "/>
+ <linearGradient id="SVGID_00000052086401443861530350000009892287849444241566_" gradientUnits="userSpaceOnUse" x1="415.0266" y1="281.0163" x2="372.0174" y2="82.9315">
+ <polygon style="fill:url(#SVGID_00000052086401443861530350000009892287849444241566_);" points="393.13,126.74 366.09,109.03
+ <linearGradient id="SVGID_00000054237655125153033930000000885783561814504864_" gradientUnits="userSpaceOnUse" x1="419.8904" y1="200.3626" x2="315.699" y2="56.5946">
+ <polygon style="fill:url(#SVGID_00000054237655125153033930000000885783561814504864_);" points="393.13,126.74 360.34,141.81
+ 333.06,117.24 "/>
+ <linearGradient id="SVGID_00000049906736961950545670000002369480488441995690_" gradientUnits="userSpaceOnUse" x1="414.494" y1="204.2735" x2="310.3026" y2="60.5055">
+ <polygon style="fill:url(#SVGID_00000049906736961950545670000002369480488441995690_);" points="393.13,126.74 388.86,158.99
+ 360.97,178.47 "/>
+ <linearGradient id="SVGID_00000032621984761622746590000016794674069364360327_" gradientUnits="userSpaceOnUse" x1="286.2739" y1="148.2623" x2="435.6957" y2="105.4549">
+ <polygon style="fill:url(#SVGID_00000032621984761622746590000016794674069364360327_);" points="425.11,112.42 393.13,126.74
+ 396.23,107.67 "/>
+ <linearGradient id="SVGID_00000108311198905665412900000004465620395145953712_" gradientUnits="userSpaceOnUse" x1="489.9962" y1="255.3554" x2="367.3289" y2="89.9818">
+ <polygon style="fill:url(#SVGID_00000108311198905665412900000004465620395145953712_);" points="418.96,169.82 393.13,126.74
+ 409.85,137.06 "/>
+ <linearGradient id="SVGID_00000132778855163012709780000008730862617147263632_" gradientUnits="userSpaceOnUse" x1="359.893" y1="149.3366" x2="187.978" y2="282.8451">
+ <path style="fill:url(#SVGID_00000132778855163012709780000008730862617147263632_);" d="M255.95,290.19
+ c0.99-0.92,1.96-1.79,3-2.64c-0.05,0.07-0.19,0.27-0.44,0.51c-0.41,0.51-1.16,1.36-2.18,2.42c-0.39,0.41-0.82,0.87-1.31,1.38
+ c-6.57,6.61-20.64,18.49-35.98,17.32l-0.24-0.02l-0.1-0.02c0,0-0.02,0-0.05,0c-0.07,0-0.24,0-0.46-0.02
+ c-3.34-0.15-22.1-2.3-23.77-30.46c0.46-10.59-0.62-39.6,36.32-83.25c32.04-37.86,68.28-60.56,99.08-75.7l25.59,23.87l1.07,35.79
+ c-55.44,12.28-124.59,33.92-135.28,92.85C216.48,298.24,242.58,302.62,255.95,290.19z"/>
+ <linearGradient id="SVGID_00000124861145368272951150000002031596967736413349_" gradientUnits="userSpaceOnUse" x1="305.2405" y1="270.0725" x2="219.861" y2="336.3778">
+ <path style="fill:url(#SVGID_00000124861145368272951150000002031596967736413349_);" d="M342.77,318.4
+ c-6.71-2.04-13.64-3.05-20.55-3.05c-9.96,0-19.87,2.11-29.08,6.3c-2.74,1.24-5.43,2.67-8.02,4.26l-1.04,0.65l-7.75,4.8
+ c-26.73,16.57-58.06,6.2-72.81-16.31v-0.02c-0.51-0.75-0.99-1.53-1.45-2.33c-0.12-0.17-0.22-0.36-0.32-0.53
+ c-0.44-0.75-0.85-1.53-1.24-2.28c-4.85-9.4-6.54-20.21-6.08-31.23c1.67,28.16,20.43,30.31,23.77,30.46
+ c0.22,0.02,0.39,0.02,0.46,0.02c0.05,0.02,0.1,0.02,0.15,0.02l0.24,0.02c15.34,1.16,29.42-10.71,35.98-17.32
+ c0.48-0.51,0.92-0.97,1.31-1.38c1.02-1.07,1.77-1.91,2.18-2.42c0.24-0.24,0.39-0.44,0.44-0.51c0.02-0.02,0.02-0.02,0.02-0.02
+ c3.54-2.96,7.24-5.55,11.15-7.73C308.53,261.09,300.22,301,342.77,318.4z"/>
+</g>
+</svg>
@@ -0,0 +1,29 @@
+// $u.mixin.js
+import { mapState } from 'vuex'
+import store from "@/store"
+// 尝试将用户在根目录中的store/index.js的vuex的state变量,全部加载到全局变量中
+let $uStoreKey = [];
+try{
+ $uStoreKey = store.state ? Object.keys(store.state) : [];
+}catch(e){
+module.exports = {
+ created() {
+ // 将vuex方法挂在到$u中
+ // 使用方法为:如果要修改vuex的state中的user.name变量为"史诗" => this.$u.vuex('user.name', '史诗')
+ // 如果要修改vuex的state的version变量为1.0.1 => this.$u.vuex('version', '1.0.1')
+ this.$u.vuex = (name, value) => {
+ this.$store.commit('$uStore', {
+ name,value
+ // 将vuex的state中的所有变量,解构到全局混入的mixin中
+ ...mapState($uStoreKey)
@@ -0,0 +1,87 @@
+import Vuex from 'vuex'
+Vue.use(Vuex)
+let lifeData = {};
+ // 尝试获取本地是否存在lifeData变量,第一次启动APP时是不存在的
+ lifeData = uni.getStorageSync('lifeData');
+// 需要永久存储,且下次APP启动需要取出的,在state中的变量名
+let saveStateKeys = ['vuex_member_info', 'vuex_user_info','vuex_wechatOpenid'];
+// 保存变量到本地存储中
+const saveLifeData = function(key, value){
+ // 判断变量名是否在需要存储的数组中
+ if(saveStateKeys.indexOf(key) != -1) {
+ // 获取本地存储的lifeData对象,将变量添加到对象中
+ let tmp = uni.getStorageSync('lifeData');
+ // 第一次打开APP,不存在lifeData变量,故放一个{}空对象
+ tmp = tmp ? tmp : {};
+ tmp[key] = value;
+ // 执行这一步后,所有需要存储的变量,都挂载在本地的lifeData对象中
+ uni.setStorageSync('lifeData', tmp);
+const store = new Vuex.Store({
+ state: {
+ // 如果上面从本地获取的lifeData对象下有对应的属性,就赋值给state中对应的变量
+ // 加上vuex_前缀,是防止变量名冲突,也让人一目了然
+ vuex_member_info: lifeData.vuex_member_info ? lifeData.vuex_member_info : {mobile:''},
+ vuex_user_info: lifeData.vuex_user_info ? lifeData.vuex_user_info : {},
+ vuex_wechatOpenid:lifeData.vuex_wechatOpenid ? lifeData.vuex_wechatOpenid : '',
+ cartGoods:[],//购物车商品
+ creditGoods:[],//积分商品
+ buyNowGoods:[],//立即购买商品
+ // vuex_version: '1.0.1',
+ mutations: {
+ $uStore(state, payload) {
+ // 判断是否多层级调用,state中为对象存在的情况,诸如user.info.score = 1
+ let nameArr = payload.name.split('.');
+ let saveKey = '';
+ let len = nameArr.length;
+ if(len >= 2) {
+ let obj = state[nameArr[0]];
+ for(let i = 1; i < len - 1; i ++) {
+ obj = obj[nameArr[i]];
+ obj[nameArr[len - 1]] = payload.value;
+ saveKey = nameArr[0];
+ // 单层级变量,在state就是一个普通变量的情况
+ state[payload.name] = payload.value;
+ saveKey = payload.name;
+ // 保存变量到本地,见顶部函数定义
+ saveLifeData(saveKey, state[saveKey])
+ actions: {
+ // 获取用户信息
+ getUserInfo({ commit },backFu) {
+ if(this.state.user_info && this.state.user_info.userId) {
+ Vue.prototype.$u.api.checkToken().then((data) => {
+ if(data && data.code ===200) {
+ backFu(this.state.user_info)
+ Vue.prototype.$u.api.userInfo().then((data) => {
+ console.log(data)
+ Vue.prototype.$u.vuex('user_info', data.data)
+ backFu(data.data)
+export default store
@@ -0,0 +1,212 @@
+ * 这里是uni-app内置的常用样式变量
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+/* 颜色变量 */
+@import '@/uni_modules/uview-ui/theme.scss';
+/* 行为相关颜色 */
+$uni-color-primary: #00A447;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+$uni-text-color-red:#FF3C3F;
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+$uni-background-color:#F5F5F5;
+/* 边框颜色 */
+$uni-border-color:#eee;
+/* 尺寸变量 */
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16;
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;
+ // &:has(.u-navbar){
+ // padding-top: 104px;
+// @import url("@/static/css/flex.scss");
+@mixin multi-ellipsis($line: 1) {
+ @if $line <= 0 {
+ $line: 1;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: $line;
+ -webkit-box-orient: vertical;
+.ellipsis-1 {
+ @include multi-ellipsis(1);
+.ellipsis-2 {
+ @include multi-ellipsis(2);
+.tabs-wrap{
+ .more{
+.page-bg{
+.pickup-info{
+ padding: 0 30rpx;
+ padding: 30rpx 0;
+ &:not(:last-of-type){
@@ -0,0 +1,6 @@
+## 1.3.7(2021-04-13)
+1. 新增`mescroll-swiper-sticky.vue`的示例, 轮播吸顶菜单导航
+2. 新增`mescroll-empty.vue`的示例, 单独使用空布局组件
+3. 简化tabs在具体项目中的使用,并简化对应的示例
+4. mescroll-uni 支持动态禁止滚动的属性 disableScroll (注: mescroll-body不支持)
+-by 小瑾同学
@@ -0,0 +1,19 @@
+.mescroll-body {
+ position: relative; /* 下拉刷新区域相对自身定位 */
+ height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
+ overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
+ box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
+.mescroll-body.mescorll-sticky{
+ overflow: unset !important
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+ .mescroll-safearea {
+ padding-bottom: constant(safe-area-inset-bottom);
+ padding-bottom: env(safe-area-inset-bottom);
@@ -0,0 +1,400 @@
+ <view
+ class="mescroll-body mescroll-render-touch"
+ :class="{'mescorll-sticky': sticky}"
+ :style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}"
+ @touchstart="wxsBiz.touchstartEvent"
+ @touchmove="wxsBiz.touchmoveEvent"
+ @touchend="wxsBiz.touchendEvent"
+ @touchcancel="wxsBiz.touchendEvent"
+ :change:prop="wxsBiz.propObserver"
+ :prop="wxsProp"
+ <!-- 状态栏 -->
+ <view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+ <view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+ <!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+ <!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+ <view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+ <view class="downwarp-content">
+ <view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+ <view class="downwarp-tip">{{downText}}</view>
+ <!-- 列表内容 -->
+ <slot></slot>
+ <!-- 空布局 -->
+ <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+ <!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+ <!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+ <view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+ <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+ <view v-show="upLoadType===1">
+ <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+ <view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+ <!-- 无数据 -->
+ <view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+ <!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+ <!-- #ifdef H5 -->
+ <view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+ <!-- 适配iPhoneX -->
+ <view v-if="safearea" class="mescroll-safearea"></view>
+ <!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+ <mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+ <!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+ <!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+ <view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+ import renderBiz from "../mescroll-uni/wxs/renderjs.js";
+ mixins: [renderBiz]
+ // 引入mescroll-uni.js,处理核心逻辑
+ import MeScroll from "../mescroll-uni/mescroll-uni.js";
+ // 引入全局配置
+ import GlobalOption from "../mescroll-uni/mescroll-uni-option.js";
+ // 引入国际化工具类
+ import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+ // 引入回到顶部组件
+ import MescrollTop from "../mescroll-uni/components/mescroll-top.vue";
+ // 引入兼容wxs(含renderjs)写法的mixins
+ import WxsMixin from "../mescroll-uni/wxs/mixins.js";
+ * mescroll-body 基于page滚动的下拉刷新和上拉加载组件, 支持嵌套原生组件, 性能好
+ * @property {Object} down 下拉刷新的参数配置
+ * @property {Object} up 上拉加载的参数配置
+ * @property {Object} i18n 国际化的参数配置
+ * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+ * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+ * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+ * @property {String, Number} height 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+ * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+ * @property {Boolean} sticky 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法隐藏
+ * @event {Function} init 初始化完成的回调
+ * @event {Function} down 下拉刷新的回调
+ * @event {Function} up 上拉加载的回调
+ * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+ * @event {Function} topclick 点击回到顶部的按钮回调
+ * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+ * @example <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-body>
+ name: 'mescroll-body',
+ mixins: [WxsMixin],
+ components: {
+ MescrollTop
+ down: Object,
+ up: Object,
+ i18n: Object,
+ top: [String, Number],
+ topbar: [Boolean, String],
+ bottom: [String, Number],
+ safearea: Boolean,
+ height: [String, Number],
+ bottombar:{
+ default: true
+ sticky: Boolean
+ mescroll: {optDown:{},optUp:{}}, // mescroll实例
+ downHight: 0, //下拉刷新: 容器高度
+ downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+ downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+ upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+ isShowEmpty: false, // 是否显示空布局
+ isShowToTop: false, // 是否显示回到顶部按钮
+ windowHeight: 0, // 可使用窗口的高度
+ windowBottom: 0, // 可使用窗口的底部位置
+ statusBarHeight: 0 // 状态栏高度
+ // mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+ minHeight(){
+ return this.toPx(this.height || '100%') + 'px'
+ // 下拉布局往下偏移的距离 (px)
+ numTop() {
+ return this.toPx(this.top)
+ padTop() {
+ return this.numTop + 'px';
+ // 上拉布局往上偏移 (px)
+ numBottom() {
+ return this.toPx(this.bottom);
+ padBottom() {
+ return this.numBottom + 'px';
+ // 是否为重置下拉的状态
+ isDownReset() {
+ return this.downLoadType === 3 || this.downLoadType === 4;
+ // 过渡
+ transition() {
+ return this.isDownReset ? 'transform 300ms' : '';
+ translateY() {
+ return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+ // 是否在加载中
+ isDownLoading(){
+ return this.downLoadType === 3
+ // 旋转的角度
+ downRotate(){
+ return 'rotate(' + 360 * this.downRate + 'deg)'
+ // 文本提示
+ downText(){
+ if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+ switch (this.downLoadType){
+ case 1: return this.mescroll.optDown.textInOffset;
+ case 2: return this.mescroll.optDown.textOutOffset;
+ case 3: return this.mescroll.optDown.textLoading;
+ case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+ default: return this.mescroll.optDown.textInOffset;
+ //number,rpx,upx,px,% --> px的数值
+ toPx(num) {
+ if (typeof num === 'string') {
+ if (num.indexOf('px') !== -1) {
+ if (num.indexOf('rpx') !== -1) {
+ // "10rpx"
+ num = num.replace('rpx', '');
+ } else if (num.indexOf('upx') !== -1) {
+ // "10upx"
+ num = num.replace('upx', '');
+ // "10px"
+ return Number(num.replace('px', ''));
+ } else if (num.indexOf('%') !== -1) {
+ // 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+ let rate = Number(num.replace('%', '')) / 100;
+ return this.windowHeight * rate;
+ return num ? uni.upx2px(Number(num)) : 0;
+ // 点击空布局的按钮回调
+ emptyClick() {
+ this.$emit('emptyclick', this.mescroll);
+ // 点击回到顶部的按钮回调
+ toTopClick() {
+ this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+ this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+ // 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+ let vm = this;
+ let diyOption = {
+ // 下拉刷新的配置
+ down: {
+ inOffset() {
+ vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+ outOffset() {
+ vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+ onMoving(mescroll, rate, downHight) {
+ // 下拉过程中的回调,滑动过程一直在执行;
+ vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+ vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+ showLoading(mescroll, downHight) {
+ vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+ beforeEndDownScroll(mescroll){
+ vm.downLoadType = 4;
+ return mescroll.optDown.beforeEndDelay // 延时结束的时长
+ endDownScroll() {
+ vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+ vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+ if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
+ vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
+ if(vm.downLoadType === 4) vm.downLoadType = 0
+ },300)
+ // 派发下拉刷新的回调
+ callback: function(mescroll) {
+ vm.$emit('down', mescroll);
+ // 上拉加载的配置
+ up: {
+ // 显示加载中的回调
+ showLoading() {
+ vm.upLoadType = 1;
+ // 显示无更多数据的回调
+ showNoMore() {
+ vm.upLoadType = 2;
+ // 隐藏上拉加载的回调
+ hideUpScroll(mescroll) {
+ vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+ // 空布局
+ empty: {
+ onShow(isShow) {
+ // 显示隐藏的回调
+ vm.isShowEmpty = isShow;
+ // 回到顶部
+ toTop: {
+ vm.isShowToTop = isShow;
+ // 派发上拉加载的回调
+ vm.$emit('up', mescroll);
+ let i18nType = mescrollI18n.getType() // 当前语言类型
+ let i18nOption = {type: i18nType} // 国际化配置
+ MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+ MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+ MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+ MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+ let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+ MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+ // 初始化MeScroll对象
+ vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+ // 挂载语言包
+ vm.mescroll.i18n = i18nOption;
+ // init回调mescroll对象
+ vm.$emit('init', vm.mescroll);
+ // 设置高度
+ const sys = uni.getSystemInfoSync();
+ if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+ if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+ if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+ // 使down的bottomOffset生效
+ vm.mescroll.setBodyHeight(sys.windowHeight);
+ // 因为使用的是page的scroll,这里需自定义scrollTo
+ vm.mescroll.resetScrollTo((y, t) => {
+ if(typeof y === 'string'){
+ // 滚动到指定view (y为css选择器)
+ setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+ let selector;
+ if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+ selector = '#'+y // 不带#和. 则默认为id选择器
+ selector = y
+ // #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+ if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+ selector = y.split('>>>')[1].trim()
+ uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+ if (rect) {
+ let top = rect.top
+ top += vm.mescroll.getScrollTop()
+ uni.pageScrollTo({
+ scrollTop: top,
+ duration: t
+ console.error(selector + ' does not exist');
+ }).exec()
+ },30)
+ // 滚动到指定位置 (y必须为数字)
+ scrollTop: y,
+ // 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+ if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+ vm.mescroll.optUp.toTop.safearea = vm.safearea;
+ // 全局配置监听
+ uni.$on("setMescrollGlobalOption", options=>{
+ if(!options) return;
+ let i18nType = options.i18n ? options.i18n.type : null
+ if(i18nType && vm.mescroll.i18n.type != i18nType){
+ vm.mescroll.i18n.type = i18nType
+ mescrollI18n.setType(i18nType)
+ MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+ if(options.down){
+ let down = MeScroll.extend({}, options.down)
+ vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+ if(options.up){
+ let up = MeScroll.extend({}, options.up)
+ vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+ destroyed() {
+ // 注销全局配置监听
+ uni.$off("setMescrollGlobalOption")
+ @import "../mescroll-body/mescroll-body.css";
+ @import "../mescroll-uni/components/mescroll-down.css";
+ @import "../mescroll-uni/components/mescroll-up.css";
@@ -0,0 +1,47 @@
+/*下拉刷新--标语*/
+.mescroll-downwarp .downwarp-slogan{
+ width: 420rpx;
+ height: 168rpx;
+/*下拉刷新--向下进度动画*/
+.mescroll-downwarp .downwarp-progress{
+ width: 40rpx;
+ height: 40rpx;
+ border: none;
+ background-size: contain;
+ background-position: center;
+ background-image: url(https://www.mescroll.com/img/beibei/mescroll-progress.png);
+ transition: all 300ms;
+/*下拉刷新--进度条*/
+.mescroll-downwarp .downwarp-loading{
+ border: 2rpx solid #FF8095;
+ border-bottom-color: transparent;
+/*下拉刷新--吉祥物*/
+.mescroll-downwarp .downwarp-mascot{
+ animation: animMascot .6s steps(1,end) infinite;
+@keyframes animMascot {
+ 0% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
+ 25% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb2.png)}
+ 50% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb3.png)}
+ 75% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb4.png)}
+ 100% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
@@ -0,0 +1,39 @@
+<!-- 下拉刷新区域 -->
+ <view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+ <image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+ <view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+ <view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+ <view class="downwarp-mascot"></view>
+ option: Object , // down的配置项
+ type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+ // 支付宝小程序需写成计算属性,prop定义default仍报错
+ mOption(){
+ return this.option || {}
+ return this.type === 3
+ return this.type === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+};
+@import "../../../mescroll-uni/components/mescroll-down.css";
+@import "./mescroll-down.css";
@@ -0,0 +1,360 @@
+ <!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
+ <!-- 底部是否偏移TabBar的高度(仅H5端生效) -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+ import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+ import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+ import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+ import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+ import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+ import GlobalOption from './mescroll-uni-option.js';
+ mescroll: null, // mescroll实例
+ down: Object, // 下拉刷新的参数配置
+ up: Object, // 上拉加载的参数配置
+ i18n: Object, // 国际化的参数配置
+ top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+ bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+ height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+ bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+ sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+ return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+ @import "../../mescroll-body/mescroll-body.css";
+ @import "../../mescroll-uni/components/mescroll-down.css";
+ @import "../../mescroll-uni/components/mescroll-up.css";
+ @import "./components/mescroll-down.css";
@@ -0,0 +1,49 @@
+// mescroll-uni和mescroll-body 的全局配置
+const GlobalOption = {
+ // 其他down的配置参数也可以写,这里只展示了常用的配置:
+ offset: uni.upx2px(140), // 在列表顶部,下拉大于140upx,松手即可触发下拉刷新的回调
+ native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+ // 其他up的配置参数也可以写,这里只展示了常用的配置:
+ offset: 150, // 距底部多远时,触发upCallback
+ // 回到顶部按钮,需配置src才显示
+ src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
+ offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
+ right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+ bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+ width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+ use: true, // 是否显示空布局
+ icon: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+ // 国际化配置
+ i18n: {
+ // 中文
+ zh: {
+ textLoading: '加载中 ...', // 加载中的提示文本
+ textNoMore: '-- END --', // 没有更多数据的提示文本
+ tip: '~ 暂无相关数据 ~' // 空提示
+ // 英文
+ en: {
+ textLoading: 'loading ...',
+ textNoMore: '-- END --',
+ tip: '~ absolutely empty ~'
+export default GlobalOption
@@ -0,0 +1,437 @@
+ <view class="mescroll-uni-warp">
+ <scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+ <view class="mescroll-uni-content mescroll-render-touch"
+ :prop="wxsProp">
+ <view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
+ </scroll-view>
+ <!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+ viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+ upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+ scrollTop: 0, // 滚动条的位置
+ scrollAnim: false, // 是否开启滚动动画
+ windowTop: 0, // 可使用窗口的顶部位置
+ fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+ height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ disableScroll: Boolean // 是否禁止滚动
+ // 是否使用fixed定位 (当height有值,则不使用)
+ isFixed(){
+ return !this.height && this.fixed
+ // mescroll的高度
+ scrollHeight(){
+ if (this.isFixed) {
+ return "auto"
+ } else if(this.height){
+ return this.toPx(this.height) + 'px'
+ return "100%"
+ fixedTop() {
+ return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+ return !this.isFixed ? this.numTop + 'px' : 0
+ return this.toPx(this.bottom)
+ fixedBottom() {
+ return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+ return !this.isFixed ? this.numBottom + 'px' : 0
+ isDownReset(){
+ return this.downLoadType===3 || this.downLoadType===4
+ return this.isDownReset ? 'transform 300ms' : ''
+ return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : '' // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+ // 列表是否可滑动
+ scrollable(){
+ if(this.disableScroll) return false
+ return this.downLoadType===0 || this.isDownReset
+ toPx(num){
+ if(typeof num === "string"){
+ if(num.indexOf('rpx') !== -1) { // "10rpx"
+ } else if(num.indexOf('upx') !== -1) { // "10upx"
+ } else { // "10px"
+ return Number(num.replace('px', ''))
+ }else if (num.indexOf('%') !== -1){
+ let rate = Number(num.replace("%","")) / 100
+ return this.windowHeight * rate
+ return num ? uni.upx2px(Number(num)) : 0
+ //注册列表滚动事件,用于下拉刷新和上拉加载
+ scroll(e) {
+ this.mescroll.scroll(e.detail, () => {
+ this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+ this.$emit('emptyclick', this.mescroll)
+ // 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+ setClientHeight() {
+ if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+ this.isExec = true; // 避免多次获取
+ this.$nextTick(() => { // 确保dom已渲染
+ this.getClientInfo(data=>{
+ this.isExec = false;
+ if (data) {
+ this.mescroll.setClientHeight(data.height);
+ } else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+ this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+ setTimeout(() => {
+ this.setClientHeight()
+ }, this.clientNum * 100)
+ // 获取滚动区域的信息
+ getClientInfo(success){
+ let query = uni.createSelectorQuery();
+ // #ifndef MP-ALIPAY || MP-DINGTALK
+ query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+ let view = query.select('#' + this.viewId);
+ view.boundingClientRect(data => {
+ success(data)
+ }).exec();
+ vm.downResetTimer && clearTimeout(vm.downResetTimer)
+ vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+ if(vm.downLoadType===4) vm.downLoadType = 0
+ vm.$emit('down', mescroll)
+ onShow(isShow) { // 显示隐藏的回调
+ // 更新容器的高度 (多mescroll的情况)
+ vm.setClientHeight()
+ let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+ vm.mescroll = new MeScroll(myOption);
+ vm.mescroll.viewId = vm.viewId; // 附带id
+ if(sys.windowTop) vm.windowTop = sys.windowTop;
+ if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+ if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+ if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+ // 因为使用的是scrollview,这里需自定义scrollTo
+ vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+ // 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+ vm.getClientInfo(function(rect){
+ let mescrollTop = rect.top // mescroll到顶部的距离
+ let curY = vm.mescroll.getScrollTop()
+ let top = rect.top - mescrollTop
+ top += curY
+ if(!vm.isFixed) top -= vm.numTop
+ vm.scrollTop = curY;
+ vm.$nextTick(function() {
+ vm.scrollTop = top
+ if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+ vm.scrollTop = y
+ vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+ vm.scrollTop = step
+ }, t)
+ mounted() {
+ // 设置容器的高度
+ @import "../../mescroll-uni/mescroll-uni.css";
@@ -0,0 +1,44 @@
+/*下拉刷新--上下箭头*/
+.mescroll-downwarp .downwarp-arrow {
+ width: 20px;
+ height: 20px;
+ margin: 10px;
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-arrow.png);
+ vertical-align: middle;
+/*下拉刷新--旋转进度条*/
+ width: 36px;
+ height: 36px;
+ animation: progressRotate 0.6s steps(6, start) infinite;
+@keyframes progressRotate {
+ 0% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+ 16% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
+ 32% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
+ 48% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
+ 64% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
+ 80% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
+ 100% {
+ <view v-if="isDownLoading" class="downwarp-progress"></view>
+ <view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view>
+ <view class="downwarp-tip">{{ downText }}</view>
+ option: Object, // down的配置项
+ mOption() {
+ return this.option || {};
+ isDownLoading() {
+ return this.type === 3;
+ downRotate() {
+ return this.type === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+ downText() {
+ switch (this.type) {
+ return this.mOption.textInOffset;
+ return this.mOption.textOutOffset;
+ return this.mOption.textLoading;
+ default:
+@import '../../../mescroll-uni/components/mescroll-down.css';
+@import './mescroll-down.css';
+/*上拉加载--旋转进度条*/
+.mescroll-upwarp .upwarp-progress {
@@ -0,0 +1,40 @@
+<!-- 上拉加载区域 -->
+ <view class="mescroll-upwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+ <view v-show="isUpLoading">
+ <view class="upwarp-progress mescroll-rotate"></view>
+ <view class="upwarp-tip">{{ mOption.textLoading }}</view>
+ <view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
+ option: Object, // up的配置项
+ type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+ // 加载中
+ isUpLoading() {
+ return this.type === 1;
+ // 没有更多了
+ isUpNoMore() {
+ return this.type === 2;
+@import '../../../mescroll-uni/components/mescroll-up.css';
+@import './mescroll-up.css';
@@ -0,0 +1,380 @@
+ <!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+ <!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+ <view class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+ return this.downLoadType === 3;
+ return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+ if(!this.mescroll) return "";
+ switch (this.downLoadType) {
+ return this.mescroll.optDown.textInOffset;
+ return this.mescroll.optDown.textOutOffset;
+ return this.mescroll.optDown.textLoading;
+ return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+ @import "./components/mescroll-up.css";
@@ -0,0 +1,64 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+ offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+ offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+ textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+ textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+ textSuccess: '加载成功', // 加载成功的文本
+ textErr: '加载失败', // 加载失败的文本
+ tip: '~ 空空如也 ~' // 空提示
+ textInOffset: 'drop down refresh',
+ textOutOffset: 'release updates',
+ textSuccess: 'loaded successfully',
+ textErr: 'loading failed'
@@ -0,0 +1,462 @@
+ let myOption = JSON.parse(JSON.stringify({
+ 'down': vm.down,
+ 'up': vm.up
+ })) // 深拷贝,避免对props的影响
@@ -0,0 +1,116 @@
+<!--空布局:
+遵循easycom规范, 可作为独立的组件, 不使用mescroll的页面也能使用:
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
+-->
+ <view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
+ <view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
+ <view v-if="tip" class="empty-tip">{{ tip }}</view>
+ <view v-if="btnText" class="empty-btn" @click="emptyClick">{{ btnText }}</view>
+// 引入全局配置
+import GlobalOption from '../mescroll-uni/mescroll-uni-option.js';
+// 引入国际化工具类
+import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+ // empty的配置项: 默认为GlobalOption.up.empty
+ option: {
+ type: Object,
+ default() {
+ return {};
+ // 使用computed获取配置,用于支持option的动态配置
+ // 图标
+ icon() {
+ if (this.option.icon != null) { // 此处不使用短路求值, 用于支持传空串不显示图标
+ return this.option.icon
+ let i18nType = mescrollI18n.getType() // 国际化配置
+ if (this.option.i18n) {
+ return this.option.i18n[i18nType].icon
+ return GlobalOption.i18n[i18nType].up.empty.icon || GlobalOption.up.empty.icon
+ tip() {
+ if (this.option.tip != null) { // 支持传空串不显示文本提示
+ return this.option.tip
+ return this.option.i18n[i18nType].tip
+ return GlobalOption.i18n[i18nType].up.empty.tip || GlobalOption.up.empty.tip
+ // 按钮文本
+ btnText() {
+ return this.option.i18n[i18nType].btnText
+ return this.option.btnText
+ // 点击按钮
+ this.$emit('emptyclick');
+/* 无任何数据的空布局 */
+.mescroll-empty {
+ padding: 100rpx 50rpx;
+.mescroll-empty.empty-fixed {
+ z-index: 99;
+ position: absolute; /*transform会使fixed失效,最终会降级为absolute */
+ top: 100rpx;
+.mescroll-empty .empty-icon {
+ width: 280rpx;
+ height: 280rpx;
+.mescroll-empty .empty-tip {
+ color: gray;
+.mescroll-empty .empty-btn {
+ min-width: 200rpx;
+ padding: 18rpx;
+ border: 1rpx solid #e04b28;
+ border-radius: 60rpx;
+ color: #e04b28;
+.mescroll-empty .empty-btn:active {
+ opacity: 0.75;
@@ -0,0 +1,55 @@
+/* 下拉刷新区域 */
+.mescroll-downwarp {
+ top: -100%;
+/* 下拉刷新--内容区,定位于区域底部 */
+.mescroll-downwarp .downwarp-content {
+ min-height: 60rpx;
+ padding: 20rpx 0;
+/* 下拉刷新--提示文本 */
+.mescroll-downwarp .downwarp-tip {
+ margin-left: 16rpx;
+ /* color: gray; 已在style设置color,此处删去*/
+/* 下拉刷新--旋转进度条 */
+.mescroll-downwarp .downwarp-progress {
+ border: 2rpx solid gray;
+ border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+/* 旋转动画 */
+.mescroll-downwarp .mescroll-rotate {
+ animation: mescrollDownRotate 0.6s linear infinite;
+@keyframes mescrollDownRotate {
+ transform: rotate(0deg);
+ transform: rotate(360deg);
+ <view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+ <view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
+ type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+ rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+ return 'rotate(' + 360 * this.rate + 'deg)'
+ switch (this.type){
+ case 1: return this.mOption.textInOffset;
+ case 2: return this.mOption.textOutOffset;
+ case 3: return this.mOption.textLoading;
+ case 4: return this.mOption.textLoading;
+ default: return this.mOption.textInOffset;
+<!-- 回到顶部的按钮 -->
+ v-if="mOption.src"
+ class="mescroll-totop"
+ :class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
+ :style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
+ :src="mOption.src"
+ mode="widthFix"
+ @click="toTopClick"
+ />
+ // up.toTop的配置项
+ option: Object,
+ // 是否显示
+ value: false
+ // 优先显示左边
+ left(){
+ return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
+ // 右边距离 (优先显示左边)
+ right() {
+ return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
+ addUnit(num){
+ if(!num) return 0;
+ if(typeof num === 'number') return num + 'rpx';
+ return num
+ this.$emit('input', false); // 使v-model生效
+ this.$emit('click'); // 派发点击事件
+/* 回到顶部的按钮 */
+.mescroll-totop {
+ z-index: 9990;
+ position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
+ bottom: 120rpx;
+ width: 72rpx;
+ height: auto;
+ transition: opacity 0.5s; /* 过渡 */
+ margin-bottom: var(--window-bottom); /* css变量 */
+ .mescroll-totop-safearea {
+ margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
+ margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
+/* 显示 -- 淡入 */
+.mescroll-totop-in {
+/* 隐藏 -- 淡出且不接收事件*/
+.mescroll-totop-out {
+ pointer-events: none;
+/* 上拉加载区域 */
+.mescroll-upwarp {
+ min-height: 110rpx;
+ clear: both;
+/*提示文本 */
+.mescroll-upwarp .upwarp-tip,
+.mescroll-upwarp .upwarp-nodata {
+.mescroll-upwarp .upwarp-tip {
+/*旋转进度条 */
+.mescroll-upwarp .mescroll-rotate {
+ animation: mescrollUpRotate 0.6s linear infinite;
+@keyframes mescrollUpRotate {
+ <view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+ <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
+ type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
@@ -0,0 +1,15 @@
+// 国际化工具类
+const mescrollI18n = {
+ // 默认语言
+ def: "zh",
+ // 获取当前语言类型
+ getType(){
+ return uni.getStorageSync("mescroll-i18n") || this.def
+ // 设置当前语言类型
+ setType(type){
+ uni.setStorageSync("mescroll-i18n", type)
+export default mescrollI18n
+const MescrollMixin = {
+ mescroll: null //mescroll实例对象
+ // 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+ onPullDownRefresh(){
+ this.mescroll && this.mescroll.onPullDownRefresh();
+ // 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+ onPageScroll(e) {
+ this.mescroll && this.mescroll.onPageScroll(e);
+ // 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+ onReachBottom() {
+ this.mescroll && this.mescroll.onReachBottom();
+ // mescroll组件初始化的回调,可获取到mescroll对象
+ mescrollInit(mescroll) {
+ this.mescroll = mescroll;
+ this.mescrollInitByRef(); // 兼容字节跳动小程序
+ // 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
+ mescrollInitByRef() {
+ if(!this.mescroll || !this.mescroll.resetUpScroll){
+ let mescrollRef = this.$refs.mescrollRef;
+ if(mescrollRef) this.mescroll = mescrollRef.mescroll
+ // 下拉刷新的回调 (mixin默认resetUpScroll)
+ downCallback() {
+ if(this.mescroll.optUp.use){
+ this.mescroll.resetUpScroll()
+ this.mescroll.endSuccess();
+ }, 500)
+ // 上拉加载的回调
+ upCallback() {
+ // mixin默认延时500自动结束加载
+ this.mescroll.endErr();
+ this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
+export default MescrollMixin;
@@ -0,0 +1,36 @@
+.mescroll-uni-warp{
+.mescroll-uni-content{
+.mescroll-uni {
+ min-height: 200rpx;
+ overflow-y: auto;
+/* 定位的方式固定高度 */
+.mescroll-uni-fixed{
+ width: auto; /* 使right生效 */
+ height: auto; /* 使bottom生效 */
@@ -0,0 +1,799 @@
+/* mescroll
+ * version 1.3.7
+ * 2021-04-12 wenju
+ * https://www.mescroll.com
+export default function MeScroll(options, isScrollBody) {
+ let me = this;
+ me.version = '1.3.7'; // mescroll版本号
+ me.options = options || {}; // 配置
+ me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
+ me.isDownScrolling = false; // 是否在执行下拉刷新的回调
+ me.isUpScrolling = false; // 是否在执行上拉加载的回调
+ let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
+ // 初始化下拉刷新
+ me.initDownScroll();
+ // 初始化上拉加载,则初始化
+ me.initUpScroll();
+ // 自动加载
+ setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+ // 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
+ if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
+ if (me.optDown.autoShowLoading) {
+ me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
+ me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
+ // 自动触发上拉加载
+ if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
+ setTimeout(function(){
+ me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
+ },100)
+ }, 30); // 需让me.optDown.inited和me.optUp.inited先执行
+/* 配置参数:下拉刷新 */
+MeScroll.prototype.extendDownScroll = function(optDown) {
+ MeScroll.extend(optDown, {
+ use: true, // 是否启用下拉刷新; 默认true
+ auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
+ native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+ autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
+ isLock: false, // 是否锁定下拉刷新,默认false;
+ startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
+ inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+ outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+ bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
+ minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
+ beforeEndDelay: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
+ bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+ textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+ inited: null, // 下拉刷新初始化完毕的回调
+ inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+ outOffset: null, // 下拉的距离大于offset那一刻的回调
+ onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+ beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+ showLoading: null, // 显示下拉刷新进度的回调
+ afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+ beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+ endDownScroll: null, // 结束下拉刷新的回调
+ afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+ // 下拉刷新的回调;默认重置上拉加载列表为第一页
+ mescroll.resetUpScroll();
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function(optUp) {
+ MeScroll.extend(optUp, {
+ use: true, // 是否启用上拉加载; 默认true
+ auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+ isLock: false, // 是否锁定上拉加载,默认false;
+ isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+ callback: null, // 上拉加载的回调;function(page,mescroll){ }
+ page: {
+ num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+ size: 10, // 每页数据的数量
+ time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+ noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+ bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+ inited: null, // 初始化完毕的回调
+ showLoading: null, // 显示加载中的回调
+ showNoMore: null, // 显示无更多数据的回调
+ hideUpScroll: null, // 隐藏上拉加载的回调
+ errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+ src: null, // 图片路径,默认null (绝对路径或网络图)
+ offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+ duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+ btnClick: null, // 点击按钮的回调
+ onShow: null, // 是否显示的回调
+ zIndex: 9990, // fixed定位z-index值
+ left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+ width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ icon: null, // 图标路径
+ tip: '~ 暂无相关数据 ~', // 提示
+ btnText: '', // 按钮
+ fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+ top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+ zIndex: 99 // fixed定位z-index值
+ onScroll: false // 是否监听滚动事件
+/* 配置参数 */
+MeScroll.extend = function(userOption, defaultOption) {
+ if (!userOption) return defaultOption;
+ for (let key in defaultOption) {
+ if (userOption[key] == null) {
+ let def = defaultOption[key];
+ if (def != null && typeof def === 'object') {
+ userOption[key] = MeScroll.extend({}, def); // 深度匹配
+ userOption[key] = def;
+ } else if (typeof userOption[key] === 'object') {
+ MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
+ return userOption;
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function(color) {
+ if(!color) return false;
+ let c = color.toLowerCase();
+ return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function() {
+ // 配置参数
+ me.optDown = me.options.down || {};
+ if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+ me.extendDownScroll(me.optDown);
+ // 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+ if(me.isScrollBody && me.optDown.native){
+ me.optDown.use = false
+ me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+ me.downHight = 0; // 下拉区域的高度
+ // 在页面中加入下拉布局
+ if (me.optDown.use && me.optDown.inited) {
+ // 初始化完毕的回调
+ me.optDown.inited(me);
+ }, 0)
+/* 列表touchstart事件 */
+MeScroll.prototype.touchstartEvent = function(e) {
+ if (!this.optDown.use) return;
+ this.startPoint = this.getPoint(e); // 记录起点
+ this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
+ this.startAngle = 0; // 初始角度
+ this.lastPoint = this.startPoint; // 重置上次move的点
+ this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+ this.inTouchend = false; // 标记不是touchend
+/* 列表touchmove事件 */
+MeScroll.prototype.touchmoveEvent = function(e) {
+ let scrollTop = me.getScrollTop(); // 当前滚动条的距离
+ let curPoint = me.getPoint(e); // 当前点
+ let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ // 向下拉 && 在顶部
+ // mescroll-body,直接判定在顶部即可
+ // scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+ // scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+ if (moveY > 0 && (
+ (me.isScrollBody && scrollTop <= 0)
+ (!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+ )) {
+ // 可下拉的条件
+ if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+ me.optUp.isBoth))) {
+ // 下拉的初始角度是否在配置的范围内
+ if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+ if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
+ // 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+ if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+ me.inTouchend = true; // 标记执行touchend
+ me.touchendEvent(); // 提前触发touchend
+ me.preventDefault(e); // 阻止默认事件
+ let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+ // 下拉距离 < 指定距离
+ if (me.downHight < me.optDown.offset) {
+ if (me.movetype !== 1) {
+ me.movetype = 1; // 加入标记,保证只执行一次
+ me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
+ me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+ me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+ me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+ // 指定距离 <= 下拉距离
+ if (me.movetype !== 2) {
+ me.movetype = 2; // 加入标记,保证只执行一次
+ me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+ if (diff > 0) { // 向下拉
+ me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+ } else { // 向上收
+ me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+ me.downHight = Math.round(me.downHight) // 取整
+ let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+ me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+ me.lastPoint = curPoint; // 记录本次移动的点
+/* 列表touchend事件 */
+MeScroll.prototype.touchendEvent = function(e) {
+ // 如果下拉区域高度已改变,则需重置回来
+ if (this.isMoveDown) {
+ if (this.downHight >= this.optDown.offset) {
+ // 符合触发刷新的条件
+ this.triggerDownScroll();
+ // 不符合的话 则重置
+ this.downHight = 0;
+ this.endDownScrollCall(this);
+ this.movetype = 0;
+ this.isMoveDown = false;
+ } else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
+ let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ // 上滑
+ if (isScrollUp) {
+ // 需检查滑动的角度
+ let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
+ if (angle > 80) {
+ // 检查并触发上拉
+ this.triggerUpScroll(true);
+/* 根据点击滑动事件获取第一个手指的坐标 */
+MeScroll.prototype.getPoint = function(e) {
+ if (!e) {
+ x: 0,
+ y: 0
+ if (e.touches && e.touches[0]) {
+ x: e.touches[0].pageX,
+ y: e.touches[0].pageY
+ } else if (e.changedTouches && e.changedTouches[0]) {
+ x: e.changedTouches[0].pageX,
+ y: e.changedTouches[0].pageY
+ x: e.clientX,
+ y: e.clientY
+/* 计算两点之间的角度: 区间 [0,90]*/
+MeScroll.prototype.getAngle = function(p1, p2) {
+ let x = Math.abs(p1.x - p2.x);
+ let y = Math.abs(p1.y - p2.y);
+ let z = Math.sqrt(x * x + y * y);
+ let angle = 0;
+ if (z !== 0) {
+ angle = Math.asin(y / z) / Math.PI * 180;
+ return angle
+/* 触发下拉刷新 */
+MeScroll.prototype.triggerDownScroll = function() {
+ if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
+ //return true则处于完全自定义状态
+ this.showDownScroll(); // 下拉刷新中...
+ !this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+/* 显示下拉进度布局 */
+MeScroll.prototype.showDownScroll = function() {
+ this.isDownScrolling = true; // 标记下拉中
+ if (this.optDown.native) {
+ uni.startPullDownRefresh(); // 系统自带的下拉刷新
+ this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+ this.downHight = this.optDown.offset; // 更新下拉区域高度
+ this.showDownLoadingCall(this.downHight); // 下拉刷新中...
+MeScroll.prototype.showDownLoadingCall = function(downHight) {
+ this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
+ this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
+/* 显示系统自带的下拉刷新时需要处理的业务 */
+MeScroll.prototype.onPullDownRefresh = function() {
+ this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+/* 结束下拉刷新 */
+MeScroll.prototype.endDownScroll = function() {
+ if (this.optDown.native) { // 结束原生下拉刷新
+ this.isDownScrolling = false;
+ uni.stopPullDownRefresh();
+ // 结束下拉刷新的方法
+ let endScroll = function() {
+ me.downHight = 0;
+ me.isDownScrolling = false;
+ me.endDownScrollCall(me);
+ if(!me.isScrollBody){
+ me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
+ me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
+ // 结束下拉刷新时的回调
+ let delay = 0;
+ if (me.optDown.beforeEndDownScroll) {
+ delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
+ if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
+ if (typeof delay === 'number' && delay > 0) {
+ setTimeout(endScroll, delay);
+ endScroll();
+MeScroll.prototype.endDownScrollCall = function() {
+ this.optDown.endDownScroll && this.optDown.endDownScroll(this);
+ this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
+/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockDownScroll = function(isLock) {
+ if (isLock == null) isLock = true;
+ this.optDown.isLock = isLock;
+/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockUpScroll = function(isLock) {
+ this.optUp.isLock = isLock;
+/* -------初始化上拉加载------- */
+MeScroll.prototype.initUpScroll = function() {
+ me.optUp = me.options.up || {use: false}
+ if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+ me.extendUpScroll(me.optUp);
+ if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
+ me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
+ me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
+ if (me.optUp.inited) {
+ me.optUp.inited(me);
+/*滚动到底部的事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onReachBottom = function() {
+ if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
+ if (!this.optUp.isLock && this.optUp.hasNext) {
+ this.triggerUpScroll();
+/*列表滚动事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onPageScroll = function(e) {
+ if (!this.isScrollBody) return;
+ // 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
+ this.setScrollTop(e.scrollTop);
+ // 顶部按钮的显示隐藏
+ if (e.scrollTop >= this.optUp.toTop.offset) {
+ this.showTopBtn();
+ this.hideTopBtn();
+/*列表滚动事件*/
+MeScroll.prototype.scroll = function(e, onScroll) {
+ // 更新滚动条的位置
+ // 更新滚动内容高度
+ this.setScrollHeight(e.scrollHeight);
+ // 向上滑还是向下滑动
+ if (this.preScrollY == null) this.preScrollY = 0;
+ this.isScrollUp = e.scrollTop - this.preScrollY > 0;
+ this.preScrollY = e.scrollTop;
+ // 上滑 && 检查并触发上拉
+ this.isScrollUp && this.triggerUpScroll(true);
+ // 滑动监听
+ this.optUp.onScroll && onScroll && onScroll()
+/* 触发上拉加载 */
+MeScroll.prototype.triggerUpScroll = function(isCheck) {
+ if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
+ // 是否校验在底部; 默认不校验
+ if (isCheck === true) {
+ let canUp = false;
+ // 还有下一页 && 没有锁定 && 不在下拉中
+ if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
+ if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
+ canUp = true; // 标记可上拉
+ if (canUp === false) return;
+ this.showUpScroll(); // 上拉加载中...
+ this.optUp.page.num++; // 预先加一页,如果失败则减回
+ this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+ this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+ this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.optUp.callback(this); // 执行回调,联网加载数据
+/* 显示上拉加载中 */
+MeScroll.prototype.showUpScroll = function() {
+ this.isUpScrolling = true; // 标记上拉加载中
+ this.optUp.showLoading && this.optUp.showLoading(this); // 回调
+/* 显示上拉无更多数据 */
+MeScroll.prototype.showNoMore = function() {
+ this.optUp.hasNext = false; // 标记无更多数据
+ this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
+/* 隐藏上拉区域**/
+MeScroll.prototype.hideUpScroll = function() {
+ this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
+/* 结束上拉加载 */
+MeScroll.prototype.endUpScroll = function(isShowNoMore) {
+ if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
+ if (isShowNoMore) {
+ this.showNoMore(); // isShowNoMore=true,显示无更多数据
+ this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
+ this.isUpScrolling = false; // 标记结束上拉加载
+/* 重置上拉加载列表为第一页
+ *isShowLoading 是否显示进度布局;
+ * 1.默认null,不传参,则显示上拉加载的进度布局
+ * 2.传参true, 则显示下拉刷新的进度布局
+ * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
+MeScroll.prototype.resetUpScroll = function(isShowLoading) {
+ if (this.optUp && this.optUp.use) {
+ let page = this.optUp.page;
+ this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
+ this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
+ page.num = this.startNum; // 重置为第一页
+ page.time = null; // 重置时间为空
+ if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
+ if (isShowLoading == null) {
+ this.removeEmpty(); // 移除空布局
+ this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
+ this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
+ this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+ this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
+/* 设置page.num的值 */
+MeScroll.prototype.setPageNum = function(num) {
+ this.optUp.page.num = num - 1;
+/* 设置page.size的值 */
+MeScroll.prototype.setPageSize = function(size) {
+ this.optUp.page.size = size;
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalPage: 总页数(必传)
+ * systime: 服务器时间 (可空)
+MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
+ let hasNext;
+ if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
+ this.endSuccess(dataSize, hasNext, systime);
+ * totalSize: 列表所有数据总数量(必传)
+MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
+ if (this.optUp.use && totalSize != null) {
+ let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
+ hasNext = loadSize < totalSize; // 是否还有下一页
+ * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
+ * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
+ * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
+MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
+ // 结束下拉刷新
+ if (me.isDownScrolling) {
+ me.isDownEndSuccess = true
+ me.endDownScroll();
+ // 结束上拉加载
+ if (me.optUp.use) {
+ let isShowNoMore; // 是否已无更多数据
+ if (dataSize != null) {
+ let pageNum = me.optUp.page.num; // 当前页码
+ let pageSize = me.optUp.page.size; // 每页长度
+ // 如果是第一页
+ if (pageNum === 1) {
+ if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
+ if (dataSize < pageSize || hasNext === false) {
+ // 返回的数据不满一页时,则说明已无更多数据
+ me.optUp.hasNext = false;
+ if (dataSize === 0 && pageNum === 1) {
+ // 如果第一页无任何数据且配置了空布局
+ isShowNoMore = false;
+ me.showEmpty();
+ // 总列表数少于配置的数量,则不显示无更多数据
+ let allDataSize = (pageNum - 1) * pageSize + dataSize;
+ if (allDataSize < me.optUp.noMoreSize) {
+ isShowNoMore = true;
+ me.removeEmpty(); // 移除空布局
+ // 还有下一页
+ me.optUp.hasNext = true;
+ // 隐藏上拉
+ me.endUpScroll(isShowNoMore);
+/* 回调失败,结束下拉刷新和上拉加载 */
+MeScroll.prototype.endErr = function(errDistance) {
+ // 结束下拉,回调失败重置回原来的页码和时间
+ if (this.isDownScrolling) {
+ this.isDownEndSuccess = false
+ if (page && this.prePageNum) {
+ page.num = this.prePageNum;
+ page.time = this.prePageTime;
+ this.endDownScroll();
+ // 结束上拉,回调失败重置回原来的页码
+ if (this.isUpScrolling) {
+ this.optUp.page.num--;
+ this.endUpScroll(false);
+ // 如果是mescroll-body,则需往回滚一定距离
+ if(this.isScrollBody && errDistance !== 0){ // 不处理0
+ if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
+ this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
+/* 显示空布局 */
+MeScroll.prototype.showEmpty = function() {
+ this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
+/* 移除空布局 */
+MeScroll.prototype.removeEmpty = function() {
+ this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
+/* 显示回到顶部的按钮 */
+MeScroll.prototype.showTopBtn = function() {
+ if (!this.topBtnShow) {
+ this.topBtnShow = true;
+ this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
+/* 隐藏回到顶部的按钮 */
+MeScroll.prototype.hideTopBtn = function() {
+ if (this.topBtnShow) {
+ this.topBtnShow = false;
+ this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
+/* 获取滚动条的位置 */
+MeScroll.prototype.getScrollTop = function() {
+ return this.scrollTop || 0
+/* 记录滚动条的位置 */
+MeScroll.prototype.setScrollTop = function(y) {
+ this.scrollTop = y;
+/* 滚动到指定位置 */
+MeScroll.prototype.scrollTo = function(y, t) {
+ this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
+/* 自定义scrollTo */
+MeScroll.prototype.resetScrollTo = function(myScrollTo) {
+ this.myScrollTo = myScrollTo
+/* 滚动条到底部的距离 */
+MeScroll.prototype.getScrollBottom = function() {
+ return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
+/* 计步器
+ star: 开始值
+ end: 结束值
+ callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
+ t: 计步时长,传0则直接回调end值;不传则默认300ms
+ rate: 周期;不传则默认30ms计步一次
+MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
+ let diff = end - star; // 差值
+ if (t === 0 || diff === 0) {
+ callback && callback(end);
+ t = t || 300; // 时长 300ms
+ rate = rate || 30; // 周期 30ms
+ let count = t / rate; // 次数
+ let step = diff / count; // 步长
+ let i = 0; // 计数
+ let timer = setInterval(function() {
+ if (i < count - 1) {
+ star += step;
+ callback && callback(star, timer);
+ i++;
+ callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
+ clearInterval(timer);
+ }, rate);
+/* 滚动容器的高度 */
+MeScroll.prototype.getClientHeight = function(isReal) {
+ let h = this.clientHeight || 0
+ if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
+ h = this.getBodyHeight()
+ return h
+MeScroll.prototype.setClientHeight = function(h) {
+ this.clientHeight = h;
+/* 滚动内容的高度 */
+MeScroll.prototype.getScrollHeight = function() {
+ return this.scrollHeight || 0;
+MeScroll.prototype.setScrollHeight = function(h) {
+ this.scrollHeight = h;
+/* body的高度 */
+MeScroll.prototype.getBodyHeight = function() {
+ return this.bodyHeight || 0;
+MeScroll.prototype.setBodyHeight = function(h) {
+ this.bodyHeight = h;
+/* 阻止浏览器默认滚动事件 */
+MeScroll.prototype.preventDefault = function(e) {
+ // 小程序不支持e.preventDefault, 已在wxs中禁止
+ // app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
+ // cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
+ if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
@@ -0,0 +1,477 @@
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+ import renderBiz from './wxs/renderjs.js';
+ mixins:[renderBiz]
+ import MeScroll from './mescroll-uni.js';
+ import mescrollI18n from './mescroll-i18n.js';
+ import MescrollTop from './components/mescroll-top.vue';
+ import WxsMixin from './wxs/mixins.js';
+ * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
+ * @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean} disableScroll 是否禁止滚动, 默认false
+ * @example <mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
+ name: 'mescroll-uni',
+ fixed: {
+ disableScroll: Boolean
+ vm.mescroll.i18n = i18nOption; // 挂载语言包
+ @import "./mescroll-uni.css";
+ @import './components/mescroll-up.css';
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
+const MescrollCompMixin = {
+ // 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
+ this.handlePageScroll(e)
+ this.handleReachBottom()
+ // 当down的native: true时, 还需传递此方法进到子组件
+ this.handlePullDownRefresh()
+ mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
+ onPageScroll: e=>{
+ onReachBottom: ()=>{
+ onPullDownRefresh: ()=>{
+ methods:{
+ handlePageScroll(e){
+ let item = this.$refs["mescrollItem"];
+ if(item && item.mescroll) item.mescroll.onPageScroll(e);
+ handleReachBottom(){
+ if(item && item.mescroll) item.mescroll.onReachBottom();
+ handlePullDownRefresh(){
+ if(item && item.mescroll) item.mescroll.onPullDownRefresh();
+export default MescrollCompMixin;
@@ -0,0 +1,66 @@
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
+const MescrollMoreItemMixin = {
+ // 支付宝小程序不支持props的mixin,需写在具体的页面中
+ i: Number, // 每个tab页的专属下标
+ index: { // 当前tab的下标
+ default(){
+ return 0
+ downOption:{
+ auto:false // 不自动加载
+ upOption:{
+ isInit: false // 当前tab是否已初始化
+ // 监听下标的变化
+ index(val){
+ if (this.i === val && !this.isInit) this.mescrollTrigger()
+ // 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
+ let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i];
+ // mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
+ this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
+ // 自动加载当前tab的数据
+ if(this.i === this.index){
+ this.mescrollTrigger()
+ // 主动触发加载
+ mescrollTrigger(){
+ this.isInit = true; // 标记为true
+ if (this.mescroll) {
+ if (this.mescroll.optDown.use) {
+ this.mescroll.triggerDownScroll();
+ this.mescroll.triggerUpScroll();
+export default MescrollMoreItemMixin;
@@ -0,0 +1,74 @@
+ * mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
+const MescrollMoreMixin = {
+ tabIndex: 0, // 当前tab下标
+ // 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
+ let mescroll = this.getMescroll(this.tabIndex);
+ mescroll && mescroll.onPageScroll(e);
+ mescroll && mescroll.onReachBottom();
+ mescroll && mescroll.onPullDownRefresh();
+ // 根据下标获取对应子组件的mescroll
+ getMescroll(i){
+ if(!this.mescrollItems) this.mescrollItems = [];
+ if(!this.mescrollItems[i]) {
+ // v-for中的refs
+ let vForItem = this.$refs["mescrollItem"];
+ if(vForItem){
+ this.mescrollItems[i] = vForItem[i]
+ // 普通的refs,不可重复
+ this.mescrollItems[i] = this.$refs["mescrollItem"+i];
+ let item = this.mescrollItems[i]
+ return item ? item.mescroll : null
+ // 切换tab,恢复滚动条位置
+ tabChange(i){
+ let mescroll = this.getMescroll(i);
+ if(mescroll){
+ // 延时(比$nextTick靠谱一些),确保元素已渲染
+ mescroll.scrollTo(mescroll.getScrollTop(),0)
+export default MescrollMoreMixin;
@@ -0,0 +1,109 @@
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
+const WxsMixin = {
+ // 传入wxs视图层的数据 (响应式)
+ wxsProp: {
+ optDown:{}, // 下拉刷新的配置
+ scrollTop:0, // 滚动条的距离
+ bodyHeight:0, // body的高度
+ isDownScrolling:false, // 是否正在下拉刷新中
+ isUpScrolling:false, // 是否正在上拉加载中
+ isScrollBody:true, // 是否为mescroll-body滚动
+ isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
+ t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+ // 标记调用wxs视图层的方法
+ callProp: {
+ callType: '', // 方法名
+ // 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
+ // #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+ wxsBiz: {
+ //注册列表touchstart事件,用于下拉刷新
+ touchstartEvent: e=> {
+ this.mescroll.touchstartEvent(e);
+ //注册列表touchmove事件,用于下拉刷新
+ touchmoveEvent: e=> {
+ this.mescroll.touchmoveEvent(e);
+ //注册列表touchend事件,用于下拉刷新
+ touchendEvent: e=> {
+ this.mescroll.touchendEvent(e);
+ propObserver(){}, // 抹平wxs的写法
+ callObserver(){} // 抹平wxs的写法
+ // 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
+ // #ifndef APP-PLUS || H5
+ renderBiz: {
+ propObserver(){} // 抹平renderjs的写法
+ // wxs视图层调用逻辑层的回调
+ wxsCall(msg){
+ if(msg.type === 'setWxsProp'){
+ // 更新wxsProp数据 (值改变才触发更新)
+ this.wxsProp = {
+ optDown: this.mescroll.optDown,
+ scrollTop: this.mescroll.getScrollTop(),
+ bodyHeight: this.mescroll.getBodyHeight(),
+ isDownScrolling: this.mescroll.isDownScrolling,
+ isUpScrolling: this.mescroll.isUpScrolling,
+ isUpBoth: this.mescroll.optUp.isBoth,
+ isScrollBody:this.mescroll.isScrollBody,
+ t: Date.now()
+ }else if(msg.type === 'setLoadType'){
+ // 设置inOffset,outOffset的状态
+ this.downLoadType = msg.downLoadType
+ // 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+ this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+ // 重置是否加载成功的状态
+ this.$set(this.mescroll, 'isDownEndSuccess', null)
+ }else if(msg.type === 'triggerDownScroll'){
+ // 主动触发下拉刷新
+ }else if(msg.type === 'endDownScroll'){
+ this.mescroll.endDownScroll();
+ }else if(msg.type === 'triggerUpScroll'){
+ // 主动触发上拉加载
+ this.mescroll.triggerUpScroll(true);
+ // #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+ // 配置主动触发wxs显示加载进度的回调
+ this.mescroll.optDown.afterLoading = ()=>{
+ this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+ // 配置主动触发wxs隐藏加载进度的回调
+ this.mescroll.optDown.afterEndDownScroll = ()=>{
+ this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+ let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
+ if(this.downLoadType === 4 || this.downLoadType === 0){
+ this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+ }, delay)
+ // 初始化wxs的数据
+ this.wxsCall({type: 'setWxsProp'})
+export default WxsMixin;
@@ -0,0 +1,92 @@
+// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
+// https://uniapp.dcloud.io/frame?id=renderjs
+// 与wxs的me实例一致
+var me = {}
+// 初始化window对象的touch事件 (仅初始化一次)
+if(window && !window.$mescrollRenderInit){
+ window.$mescrollRenderInit = true
+ window.addEventListener('touchstart', function(e){
+ if (me.disabled()) return;
+ me.startPoint = me.getPoint(e); // 记录起点
+ }, {passive: true})
+ window.addEventListener('touchmove', function(e){
+ if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
+ var curPoint = me.getPoint(e); // 当前点
+ var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ // 向下拉
+ if (moveY > 0) {
+ if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
+ // 只有touch在mescroll的view上面,才禁止bounce
+ var el = e.target;
+ var isMescrollTouch = false;
+ while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
+ var cls = el.classList;
+ if (cls && cls.contains('mescroll-render-touch')) {
+ isMescrollTouch = true
+ el = el.parentNode; // 继续检查其父元素
+ // 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
+ if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
+ }, {passive: false})
+me.getScrollTop = function() {
+ return me.scrollTop || 0
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+ return !me.optDown || !me.optDown.use || me.optDown.native
+me.getPoint = function(e) {
+ return {x: 0,y: 0}
+ return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+ return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+ return {x: e.clientX,y: e.clientY}
+ * 监听逻辑层数据的变化 (实时更新数据)
+function propObserver(wxsProp) {
+ me.optDown = wxsProp.optDown
+ me.scrollTop = wxsProp.scrollTop
+ me.isDownScrolling = wxsProp.isDownScrolling
+ me.isUpScrolling = wxsProp.isUpScrolling
+ me.isUpBoth = wxsProp.isUpBoth
+/* 导出模块 */
+const renderBiz = {
+ propObserver: propObserver,
+export default renderBiz;
@@ -0,0 +1,268 @@
+// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
+// https://uniapp.dcloud.io/frame?id=wxs
+// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
+// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
+// ------ 自定义下拉刷新动画 start ------
+/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
+me.onMoving = function (ins, rate, downHight){
+ ins.requestAnimationFrame(function () {
+ ins.selectComponent('.mescroll-wxs-content').setStyle({
+ 'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
+ 'transform': 'translateY(' + downHight + 'px)',
+ 'transition': ''
+ // 环形进度条
+ var progress = ins.selectComponent('.mescroll-wxs-progress')
+ progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
+/* 显示下拉刷新进度 */
+me.showLoading = function (ins){
+ me.downHight = me.optDown.offset
+ 'will-change': 'auto',
+ 'transform': 'translateY(' + me.downHight + 'px)',
+ 'transition': 'transform 300ms'
+/* 结束下拉 */
+me.endDownScroll = function (ins){
+ 'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
+/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
+me.clearTransform = function (ins){
+ 'will-change': '',
+ 'transform': '',
+// ------ 自定义下拉刷新动画 end ------
+ me.bodyHeight = wxsProp.bodyHeight
+ me.isScrollBody = wxsProp.isScrollBody
+ me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
+ * 监听逻辑层数据的变化 (调用wxs的方法)
+function callObserver(callProp, oldValue, ins) {
+ if(callProp.callType){
+ // 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
+ if(callProp.callType === 'showLoading'){
+ me.showLoading(ins)
+ }else if(callProp.callType === 'endDownScroll'){
+ me.endDownScroll(ins)
+ }else if(callProp.callType === 'clearTransform'){
+ me.clearTransform(ins)
+ * touch事件
+function touchstartEvent(e, ins) {
+ me.downHight = 0; // 下拉的距离
+ me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
+ me.startAngle = 0; // 初始角度
+ me.lastPoint = me.startPoint; // 重置上次move的点
+ me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+ me.inTouchend = false; // 标记不是touchend
+ me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+function touchmoveEvent(e, ins) {
+ var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+ if (me.disabled()) return isPrevent;
+ var scrollTop = me.getScrollTop(); // 当前滚动条的距离
+ me.isUpBoth))) {
+ // 下拉的角度是否在配置的范围内
+ if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
+ touchendEvent(e, ins); // 提前触发touchend
+ return isPrevent;
+ isPrevent = false // 小程序是return false
+ var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+ // me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+ me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
+ // me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+ me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
+ var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+ // me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+ me.onMoving(ins, rate, me.downHight)
+ return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+function touchendEvent(e, ins) {
+ if (me.isMoveDown) {
+ if (me.downHight >= me.optDown.offset) {
+ me.downHight = me.optDown.offset; // 更新下拉区域高度
+ // me.triggerDownScroll();
+ me.callMethod(ins, {type: 'triggerDownScroll'})
+ // me.optDown.endDownScroll && me.optDown.endDownScroll(me);
+ me.callMethod(ins, {type: 'endDownScroll'})
+ me.movetype = 0;
+ me.isMoveDown = false;
+ } else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
+ var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
+ // me.triggerUpScroll(true);
+ me.callMethod(ins, {type: 'triggerUpScroll'})
+me.getAngle = function (p1, p2) {
+ var x = Math.abs(p1.x - p2.x);
+ var y = Math.abs(p1.y - p2.y);
+ var z = Math.sqrt(x * x + y * y);
+ var angle = 0;
+/* 获取body的高度 */
+me.getBodyHeight = function() {
+ return me.bodyHeight || 0;
+/* 调用逻辑层的方法 */
+me.callMethod = function(ins, param) {
+ if(ins) ins.callMethod('wxsCall', param)
+ callObserver: callObserver,
+ touchstartEvent: touchstartEvent,
+ touchmoveEvent: touchmoveEvent,
+ touchendEvent: touchendEvent
@@ -0,0 +1,80 @@
+ "id": "mescroll-uni",
+ "displayName": "【wxs+renderjs实现】高性能的下拉刷新上拉加载组件",
+ "version": "1.3.7",
+ "description": "支持uni-app的下拉刷新和上拉加载的组件,支持原生页面和局部区域滚动,支持国际化",
+ "keywords": [
+ "mescroll",
+ "下拉刷新",
+ "上拉加载",
+ "翻页",
+ "分页"
+],
+ "repository": "https://github.com/mescroll/mescroll",
+ "engines": {
+ "HBuilderX": "^3.1.0"
+ "dcloudext": {
+ "category": [
+ "前端组件",
+ "通用组件"
+ "sale": {
+ "regular": {
+ "price": "0.00"
+ "sourcecode": {
+ "contact": {
+ "qq": ""
+ "declaration": {
+ "ads": "无",
+ "data": "无",
+ "permissions": "无"
+ "npmurl": "https://www.npmjs.com/package/mescroll-uni"
+ "uni_modules": {
+ "dependencies": [],
+ "encrypt": [],
+ "platforms": {
+ "cloud": {
+ "tcb": "y",
+ "aliyun": "y"
+ "client": {
+ "App": {
+ "app-vue": "y",
+ "app-nvue": "n"
+ "H5-mobile": {
+ "Safari": "y",
+ "Android Browser": "y",
+ "微信浏览器(Android)": "y",
+ "QQ浏览器(Android)": "y"
+ "H5-pc": {
+ "Chrome": "y",
+ "IE": "y",
+ "Edge": "y",
+ "Firefox": "y",
+ "Safari": "y"
+ "小程序": {
+ "微信": "y",
+ "阿里": "y",
+ "百度": "y",
+ "字节跳动": "y",
+ "QQ": "y"
+ "快应用": {
+ "华为": "y",
+ "联盟": "y"
@@ -0,0 +1,45 @@
+## mescroll --【wxs+renderjs实现】高性能的下拉刷新上拉加载组件
+1. mescroll的uni版本 是专门用在uni-app的下拉刷新和上拉加载的组件
+2. mescroll的uni版本 继承了mescroll.js的实用功能: 自动处理分页, 自动控制无数据, 空布局提示, 回到顶部按钮 ..
+3. mescroll的uni版本 丰富的案例, 自由灵活的api, 超详细的注释, 可让您快速自定义真正属于自己的下拉上拉组件
+<br/>
+## 最新文档(1.3.7版本): <a href="https://www.mescroll.com/uni.html">https://www.mescroll.com/uni.html</a>
+2021-04-13 by 小瑾同学 (文档可能会有缓存,建议打开时刷新一下)
+## 1.3.5版本已调整为[uni_modules](https://uniapp.dcloud.io/uni_modules)
+uni_modules版本的mescroll-body 和 mescroll-empty 支持 [easycom规范](https://uniapp.dcloud.io/collocation/pages?id=easycom)
+所以 main.js 无需再为mescroll-body注册全局组件
+所以个别页面要单独使用 mescroll-empty , 也无需手动注册
+#### 1.3.5以前的用户升级为uni_modules版本:
+```
+1. 删除原来的 @/components/mescroll-uni 组件
+2. 删除 main.js 注册的 mescroll 组件
+3. 从插件市场导入最新mescroll组件 (1.3.5+uni_modules版本)
+4. 全局搜索 '@/components/mescroll-uni/' 替换为 '@/uni_modules/mescroll-uni/components/mescroll-uni/'
+5. mescroll-empty遵循easycom规范, 若某些页面单独使用 'mescroll-empty.vue', 可删除手动导入的代码
+## 近期已更新优化的内容:
+1. 微信小程序, app, h5使用高性能wxs和renderjs, 下拉刷新更流畅丝滑, 尤其能明显解决Android小程序下拉卡顿的问题
+2. 新增`入门极简`示例, 国际化`mescroll-i18n.vue`示例, 轮播吸顶菜单`mescroll-swiper-sticky.vue`示例
+3. 新增 "局部区域滚动" 的案例: mescroll-body-part.vue 和 mescroll-uni-part.vue
+4. 新增 me-video 视频组件, 解决APP端视频下拉悬浮错位的问题, 参考 mescroll-options.vue 示例
+5. 新增 me-tabs 组件,tabs支持水平滑动; 优化mescroll-more和mescroll-swiper的案例, 顶部tab支持水平滑动
+6. 吸顶悬浮提供了原生sticky和监听滚动条实现的示例: sticky.vue 和 sticky-scroll.vue (推荐使用sticky样式实现)
+7. mescroll.scrollTo(y)的y支持css选择器, 包括跨自定义组件的后代选择器, 支持滚动到子组件的view (参考 mescroll-options.vue)
+8. topbar 顶部是否预留状态栏的高度, 默认false; 还可支持设置状态栏背景: 如 '#ffff00', 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'
+9. down.bgColor 和 up.bgColor 加载区域的背景,不仅支持色值, 而且还是支持背景图和渐变: 如 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'
+10. topbar,bgColor支持一行代码定义background: [https://www.runoob.com/cssref/css3-pr-background.html](https://www.runoob.com/cssref/css3-pr-background.html)
+<a href="https://ext.dcloud.net.cn/plugin?id=343&update_log">查看更多 ... </a>
+#### mescroll不支持nvue,也暂无支持的计划哈,so sorry~
@@ -0,0 +1,21 @@
+MIT License
+Copyright (c) 2020 www.uviewui.com
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+<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,357 @@
+## 2.0.34(2022-09-25)
+# uView2.0重磅发布,利剑出鞘,一统江湖
+1. `u-input`、`u-textarea`增加`ignoreCompositionEvent`属性
+2. 修复`route`方法调用可能报错的问题
+3. 修复`u-no-network`组件`z-index`无效的问题
+4. 修复`textarea`组件在h5上confirmType=""报错的问题
+5. `u-rate`适配`nvue`
+6. 优化验证手机号码的正则表达式(根据工信部发布的《电信网编号计划(2017年版)》进行修改。)
+7. `form-item`添加`labelPosition`属性
+8. `u-calendar`修复`maxDate`设置为当前日期,并且当前时间大于08:00时无法显示日期列表的问题 (#724)
+9. `u-radio`增加一个默认插槽用于自定义修改label内容 (#680)
+10. 修复`timeFormat`函数在safari重的兼容性问题 (#664)
+## 2.0.33(2022-06-17)
+1. 修复`loadmore`组件`lineColor`类型错误问题
+2. 修复`u-parse`组件`imgtap`、`linktap`不生效问题
+## 2.0.32(2022-06-16)
+1. `u-loadmore`新增自定义颜色、虚/实线
+2. 修复`u-swiper-action`组件部分平台不能上下滑动的问题
+3. 修复`u-list`回弹问题
+4. 修复`notice-bar`组件动画在低端安卓机可能会抖动的问题
+5. `u-loading-page`添加控制图标大小的属性`iconSize`
+6. 修复`u-tooltip`组件`color`参数不生效的问题
+7. 修复`u--input`组件使用`blur`事件输出为`undefined`的bug
+8. `u-code-input`组件新增键盘弹起时,是否自动上推页面参数`adjustPosition`
+9. 修复`image`组件`load`事件无回调对象问题
+10. 修复`button`组件`loadingSize`设置无效问题
+10. 其他修复
+## 2.0.31(2022-04-19)
+1. 修复`upload`在`vue`页面上传成功后没有成功标志的问题
+2. 解决演示项目中微信小程序模拟上传图片一直出于上传中问题
+3. 修复`u-code-input`组件在`nvue`页面编译到`app`平台上光标异常问题(`app`去除此功能)
+4. 修复`actionSheet`组件标题关闭按钮点击事件名称错误的问题
+5. 其他修复
+## 2.0.30(2022-04-04)
+1. `u-rate`增加`readonly`属性
+2. `tabs`滑块支持设置背景图片
+3. 修复`u-subsection` `mode`为`subsection`时,滑块样式不正确的问题
+4. `u-code-input`添加光标效果动画
+5. 修复`popup`的`open`事件不触发
+6. 修复`u-flex-column`无效的问题
+7. 修复`u-datetime-picker`索引在特定场合异常问题
+8. 修复`u-datetime-picker`最小时间字符串模板错误问题
+9. `u-swiper`添加`m3u8`验证
+10. `u-swiper`修改判断image和video逻辑
+11. 修复`swiper`无法使用本地图片问题,增加`type`参数
+12. 修复`u-row-notice`格式错误问题
+13. 修复`u-switch`组件当`unit`为`rpx`时,`nodeStyle`消失的问题
+14. 修复`datetime-picker`组件`showToolbar`与`visibleItemCount`属性无效的问题
+15. 修复`upload`组件条件编译位置判断错误,导致`previewImage`属性设置为`false`时,整个组件都会被隐藏的问题
+16. 修复`u-checkbox-group`设置`shape`属性无效的问题
+17. 修复`u-upload`的`capture`传入字符串的时候不生效的问题
+18. 修复`u-action-sheet`组件,关闭事件逻辑错误的问题
+19. 修复`u-list`触顶事件的触发错误的问题
+20. 修复`u-text`只有手机号可拨打的问题
+21. 修复`u-textarea`不能换行的问题
+22. 其他修复
+## 2.0.29(2022-03-13)
+1. 修复`u--text`组件设置`decoration`属性未生效的问题
+2. 修复`u-datetime-picker`使用`formatter`后返回值不正确
+3. 修复`u-datetime-picker` `intercept` 可能为undefined
+4. 修复已设置单位 uni..config.unit = 'rpx'时,线型指示器 `transform` 的位置翻倍,导致指示器超出宽度
+5. 修复mixin中bem方法生成的类名在支付宝和字节小程序中失效
+6. 修复默认值传值为空的时候,打开`u-datetime-picker`报错,不能选中第一列时间的bug
+7. 修复`u-datetime-picker`使用`formatter`后返回值不正确
+8. 修复`u-image`组件`loading`无效果的问题
+9. 修复`config.unit`属性设为`rpx`时,导航栏占用高度不足导致塌陷的问题
+10. 修复`u-datetime-picker`组件`itemHeight`无效问题
+11. 其他修复
+## 2.0.28(2022-02-22)
+1. search组件新增searchIconSize属性
+2. 兼容Safari/Webkit中传入时间格式如2022-02-17 12:00:56
+3. 修复text value.js 判断日期出format错误问题
+4. priceFormat格式化金额出现精度错误
+5. priceFormat在部分情况下出现精度损失问题
+6. 优化表单rules提示
+7. 修复avatar组件src为空时,展示状态不对
+8. 其他修复
+## 2.0.27(2022-01-28)
+1.样式修复
+## 2.0.26(2022-01-28)
+## 2.0.25(2022-01-27)
+1. 修复text组件mode=price时,可能会导致精度错误的问题
+2. 添加$u.setConfig()方法,可设置uView内置的config, props, zIndex, color属性,详见:[修改uView内置配置方案](https://uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE)
+3. 优化form组件在errorType=toast时,如果输入错误页面会有抖动的问题
+4. 修复$u.addUnit()对配置默认单位可能无效的问题
+## 2.0.24(2022-01-25)
+1. 修复swiper在current指定非0时缩放有误
+2. 修复u-icon添加stop属性的时候报错
+3. 优化遗留的通过正则判断rpx单位的问题
+4. 优化Layout布局 vue使用gutter时,会超出固定区域
+5. 优化search组件高度单位问题(rpx -> px)
+6. 修复u-image slot 加载和错误的图片失去了高度
+7. 修复u-index-list中footer插槽与header插槽存在性判断错误
+8. 修复部分机型下u-popup关闭时会闪烁
+9. 修复u-image在nvue-app下失去宽高
+10. 修复u-popup运行报错
+11. 修复u-tooltip报错
+12. 修复box-sizing在app下的警告
+13. 修复u-navbar在小程序中报运行时错误
+14. 其他修复
+## 2.0.23(2022-01-24)
+1. 修复image组件在hx3.3.9的nvue下可能会显示异常的问题
+2. 修复col组件gutter参数带rpx单位处理不正确的问题
+3. 修复text组件单行时无法显示省略号的问题
+4. navbar添加titleStyle参数
+5. 升级到hx3.3.9可消除nvue下控制台样式警告的问题
+## 2.0.22(2022-01-19)
+1. $u.page()方法优化,避免在特殊场景可能报错的问题
+2. picker组件添加immediateChange参数
+3. 新增$u.pages()方法
+## 2.0.21(2022-01-19)
+1. 优化:form组件在用户设置rules的时候提示用户model必传
+2. 优化遗留的通过正则判断rpx单位的问题
+3. 修复微信小程序环境中tabbar组件开启safeAreaInsetBottom属性后,placeholder高度填充不正确
+4. 修复swiper在current指定非0时缩放有误
+5. 修复u-icon添加stop属性的时候报错
+6. 修复upload组件在accept=all的时候没有作用
+7. 修复在text组件mode为phone时call属性无效的问题
+8. 处理u-form clearValidate方法
+9. 其他修复
+## 2.0.20(2022-01-14)
+1. 修复calendar默认会选择一个日期,如果直接点确定的话,无法取到值的问题
+2. 修复Slider缺少disabled props 还有注释
+3. 修复u-notice-bar点击事件无法拿到index索引值的问题
+4. 修复u-collapse-item在vue文件下,app端自定义插槽不生效的问题
+5. 优化头像为空时显示默认头像
+6. 修复图片地址赋值后判断加载状态为完成问题
+7. 修复日历滚动到默认日期月份区域
+8. search组件暴露点击左边icon事件
+9. 修复u-form clearValidate方法不生效
+10. upload h5端增加返回文件参数(文件的name参数)
+11. 处理upload选择文件后url为blob类型无法预览的问题
+12. u-code-input 修复输入框没有往左移出一半屏幕
+13. 修复Upload上传 disabled为true时,控制台报hoverClass类型错误
+14. 临时处理ios app下grid点击坍塌问题
+15. 其他修复
+## 2.0.19(2021-12-29)
+1. 优化微信小程序包体积可在微信中预览,请升级HbuilderX3.3.4,同时在“运行->运行到小程序模拟器”中勾选“运行时是否压缩代码”
+2. 优化微信小程序setData性能,处理某些方法如$u.route()无法在模板中使用的问题
+3. navbar添加autoBack参数
+4. 允许avatar组件的事件冒泡
+5. 修复cell组件报错问题
+6. 其他修复
+## 2.0.18(2021-12-28)
+1. 修复app端编译报错问题
+2. 重新处理微信小程序端setData过大的性能问题
+3. 修复边框问题
+4. 修复最大最小月份不大于0则没有数据出现的问题
+5. 修复SwipeAction微信小程序端无法上下滑动问题
+6. 修复input的placeholder在小程序端默认显示为true问题
+7. 修复divider组件click事件无效问题
+8. 修复u-code-input maxlength 属性值为 String 类型时显示异常
+9. 修复当 grid只有 1到2时 在小程序端algin设置无效的问题
+10. 处理form-item的label为top时,取消错误提示的左边距
+## 2.0.17(2021-12-26)
+## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583)
+1. 解决HBuilderX3.3.3.20211225版本导致的样式问题
+2. calendar日历添加monthNum参数
+3. navbar添加center slot
+## 2.0.16(2021-12-25)
+1. 解决微信小程序setData性能问题
+2. 修复count-down组件change事件不触发问题
+## 2.0.15(2021-12-21)
+1. 修复Cell单元格titleWidth无效
+2. 修复cheakbox组件ischecked不更新
+3. 修复keyboard是否显示"."按键默认值问题
+4. 修复number-keyboard是否显示键盘的"."符号问题
+5. 修复Input输入框 readonly无效
+6. 修复u-avatar 导致打包app、H5时候报错问题
+7. 修复Upload上传deletable无效
+8. 修复upload当设置maxSize时无效的问题
+9. 修复tabs lineWidth传入带单位的字符串的时候偏移量计算错误问题
+10. 修复rate组件在有padding的view内,显示的星星位置和可触摸区域不匹配,无法正常选中星星
+## 2.0.13(2021-12-14)
+## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY)
+1. 修复配置默认单位为rpx可能会导致自定义导航栏高度异常的问题
+## 2.0.12(2021-12-14)
+1. 修复tabs组件在vue环境下划线消失的问题
+2. 修复upload组件在安卓小程序无法选择视频的问题
+3. 添加uni.$u.config.unit配置,用于配置参数默认单位,详见:[默认单位配置](https://www.uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE)
+4. 修复textarea组件在没绑定v-model时,字符统计不生效问题
+5. 修复nvue下控制是否出现滚动条失效问题
+## 2.0.11(2021-12-13)
+1. text组件align参数无效的问题
+2. subsection组件添加keyName参数
+3. upload组件无法判断[Object file]类型的问题
+4. 处理notify层级过低问题
+5. codeInput组件添加disabledDot参数
+6. 处理actionSheet组件round参数无效的问题
+7. calendar组件添加round参数用于控制圆角值
+8. 处理swipeAction组件在vue环境下默认被打开的问题
+9. button组件的throttleTime节流参数无效的问题
+10. 解决u-notify手动关闭方法close()无效的问题
+11. input组件readonly不生效问题
+12. tag组件type参数为info不生效问题
+## 2.0.10(2021-12-08)
+1. 修复button sendMessagePath属性不生效
+2. 修复DatetimePicker选择器title无效
+3. 修复u-toast设置loading=true不生效
+4. 修复u-text金额模式传0报错
+5. 修复u-toast组件的icon属性配置不生效
+6. button的icon在特殊场景下的颜色优化
+7. IndexList优化,增加#
+## 2.0.9(2021-12-01)
+## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
+1. 优化swiper的height支持100%值(仅vue有效),修复嵌入视频时click事件无法触发的问题
+2. 优化tabs组件对list值为空的判断,或者动态变化list时重新计算相关尺寸的问题
+3. 优化datetime-picker组件逻辑,让其后续打开的默认值为上一次的选中值,需要通过v-model绑定值才有效
+4. 修复upload内嵌在其他组件中,选择图片可能不会换行的问题
+## 2.0.8(2021-12-01)
+1. 修复toast的position参数无效问题
+2. 处理input在ios nvue上无法获得焦点的问题
+3. avatar-group组件添加extraValue参数,让剩余展示数量可手动控制
+4. tabs组件添加keyName参数用于配置从对象中读取的键名
+5. 处理text组件名字脱敏默认配置无效的问题
+6. 处理picker组件item文本太长换行问题
+## 2.0.7(2021-11-30)
+1. 修复radio和checkbox动态改变v-model无效的问题。
+2. 优化form规则validator在微信小程序用法
+3. 修复backtop组件mode参数在微信小程序无效的问题
+4. 处理Album的previewFullImage属性无效的问题
+5. 处理u-datetime-picker组件mode='time'在选择改变时间时,控制台报错的问题
+## 2.0.6(2021-11-27)
+1. 处理tag组件在vue下边框无效的问题。
+2. 处理popup组件圆角参数可能无效的问题。
+3. 处理tabs组件lineColor参数可能无效的问题。
+4. propgress组件在值很小时,显示异常的问题。
+## 2.0.5(2021-11-25)
+1. calendar在vue下显示异常问题。
+2. form组件labelPosition和errorType参数无效的问题
+3. input组件inputAlign无效的问题
+4. 其他一些修复
+## 2.0.4(2021-11-23)
+0. input组件缺失@confirm事件,以及subfix和prefix无效问题
+1. component.scss文件样式在vue下干扰全局布局问题
+2. 修复subsection在vue环境下表现异常的问题
+3. tag组件的bgColor等参数无效的问题
+4. upload组件不换行的问题
+5. 其他的一些修复处理
+## 2.0.3(2021-11-16)
+## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
+1. uView2.0已实现全面兼容nvue
+2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升
+3. 目前uView2.0为公测阶段,相关细节可能会有变动
+4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
+5. 处理modal的confirm回调事件拼写错误问题
+6. 处理input组件@input事件参数错误问题
+7. 其他一些修复
+## 2.0.2(2021-11-16)
+5. 修复input组件formatter参数缺失问题
+6. 优化loading-icon组件的scss写法问题,防止不兼容新版本scss
+## 2.0.0(2020-11-15)
@@ -0,0 +1,78 @@
+ <uvForm
+ ref="uForm"
+ :model="model"
+ :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'
+ name: 'u-form',
+ // #ifndef MP-WEIXIN
+ name: 'u--form',
+ mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
+ uvForm
+ 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
+ <uvImage
+ :src="src"
+ :mode="mode"
+ :width="width"
+ :height="height"
+ :shape="shape"
+ :radius="radius"
+ :lazyLoad="lazyLoad"
+ :showMenuByLongpress="showMenuByLongpress"
+ :loadingIcon="loadingIcon"
+ :errorIcon="errorIcon"
+ :showLoading="showLoading"
+ :showError="showError"
+ :fade="fade"
+ :webp="webp"
+ :duration="duration"
+ :bgColor="bgColor"
+ @click="$emit('click')"
+ @error="$emit('error')"
+ @load="$emit('load')"
+ <template v-slot:loading>
+ <slot name="loading"></slot>
+ </template>
+ <template v-slot:error>
+ <slot name="error"></slot>
+ </uvImage>
+ * 此组件存在的理由是,在nvue下,u-image被uni-app官方占用了,u-image在nvue中相当于image组件
+ * 所以在nvue下,取名为u--image,内部其实还是u-iamge.vue,只不过做一层中转
+ import uvImage from '../u-image/u-image.vue';
+ import props from '../u-image/props.js';
+ name: 'u--image',
+ uvImage
@@ -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>
+ <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
+ <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";
+ 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
@@ -0,0 +1,54 @@
+ // 操作菜单是否展示 (默认false)
+ show: {
+ default: uni.$u.props.actionSheet.show
+ // 标题
+ title: {
+ default: uni.$u.props.actionSheet.title
+ // 选项上方的描述信息
+ description: {
+ default: uni.$u.props.actionSheet.description
+ // 数据
+ type: Array,
+ default: uni.$u.props.actionSheet.actions
+ // 取消按钮的文字,不为空时显示按钮
+ cancelText: {
+ default: uni.$u.props.actionSheet.cancelText
+ // 点击某个菜单项时是否关闭弹窗
+ closeOnClickAction: {
+ default: uni.$u.props.actionSheet.closeOnClickAction
+ // 处理底部安全区(默认true)
+ safeAreaInsetBottom: {
+ default: uni.$u.props.actionSheet.safeAreaInsetBottom
+ // 小程序的打开方式
+ openType: {
+ default: uni.$u.props.actionSheet.openType
+ // 点击遮罩是否允许关闭 (默认true)
+ closeOnClickOverlay: {
+ default: uni.$u.props.actionSheet.closeOnClickOverlay
+ // 圆角值
+ round: {
+ type: [Boolean, String, Number],
+ default: uni.$u.props.actionSheet.round
+ <u-popup
+ mode="bottom"
+ @close="closeHandler"
+ :safeAreaInsetBottom="safeAreaInsetBottom"
+ :round="round"
+ <view class="u-action-sheet">
+ 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"
+ <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],
+ // 操作项目的样式
+ 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'
+ closeHandler() {
+ // 允许点击遮罩关闭时,才发出close事件
+ if(this.closeOnClickOverlay) {
+ this.$emit('close')
+ // 点击取消按钮
+ cancel() {
+ 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 {
+ 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)) : ''"
+ :src="getSrc(item)"
+ :mode="
+ urls.length === 1
+ ? imageHeight > 0
+ ? singleMode
+ : 'widthFix'
+ : multipleMode
+ "
+ :style="[
+ width: imageWidth,
+ height: imageHeight
+ ]"
+ 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])
+ 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);
+ &__wrapper {
+ &__text {
+ 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() {
+ case 'success':
+ return 'checkmark-circle-fill';
+ case 'error':
+ return 'close-circle-fill';
+ case 'warning':
+ return 'error-circle-fill';
+ case 'info':
+ return 'info-circle-fill';
+ case 'primary':
+ return 'more-circle-fill';
+ // 点击内容
+ clickHandler() {
+ this.$emit('click')
+ // 点击关闭按钮
+ this.show = false
+ .u-alert {
+ background-color: $u-primary;
+ padding: 8px 10px;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ &--primary--dark {
+ &--primary--light {
+ background-color: #ecf5ff;
+ &--error--dark {
+ background-color: $u-error;
+ &--error--light {
+ background-color: #FEF0F0;
+ &--success--dark {
+ background-color: $u-success;
+ &--success--light {
+ background-color: #f5fff0;
+ &--warning--dark {
+ background-color: $u-warning;
+ &--warning--light {
+ background-color: #FDF6EC;
+ &--info--dark {
+ background-color: $u-info;
+ &--info--light {
+ background-color: #f4f4f5;
+ &__icon {
+ margin-right: 5px;
+ &__content {
+ font-size: 14px;
+ margin-bottom: 2px;
+ &__desc {
+ &__title--dark,
+ &__desc--dark {
+ &__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
+ 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
+ // 生成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;
+ 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;
+// 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;
+ &: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],
+ // 副标题
+ subtitle: {
+ // 是否显示标题
+ showTitle: {
+ // 是否显示副标题
+ showSubtitle: {
+ name() {
+ .u-calendar-header {
+ padding-bottom: 4px;
+ 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: {
+ // 每个日期的宽度
+ 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)
+ this.$nextTick(() => {
+ // 这里需要另一个延时,因为获取宽度后,会进行月份数据渲染,只有渲染完成之后,才有真正的高度
+ // 因为nvue下,$nextTick并不是100%可靠的
+ uni.$u.sleep(10).then(() => {
+ this.getWrapperWidth()
+ this.getMonthRect()
+ // 判断两个日期是否相等
+ dateSame(date1, date2) {
+ return dayjs(date1).isSame(dayjs(date2))
+ // 获取月份数据区域的宽度,因为nvue不支持百分比,所以无法通过css设置每个日期item的宽度
+ getWrapperWidth() {
+ dom.getComponentRect(this.$refs['u-calendar-month-wrapper'], res => {
+ this.width = res.size.width
+ this.$uGetRect('.u-calendar-month-wrapper').then(size => {
+ this.width = size.width
+ getMonthRect() {
+ // 获取每个月份数据的尺寸,用于父组件在scroll-view滚动事件中,监听当前滚动到了第几个月份
+ const promiseAllArr = this.months.map((item, index) => this.getMonthRectByPromise(
+ `u-calendar-month-${index}`))
+ // 一次性返回
+ Promise.all(promiseAllArr).then(
+ sizes => {
+ let height = 1
+ const topArr = []
+ for (let i = 0; i < this.months.length; i++) {
+ // 添加到months数组中,供scroll-view滚动事件中,判断当前滚动到哪个月份
+ topArr[i] = height
+ height += sizes[i].height
+ // 由于微信下,无法通过this.months[i].top的形式(引用类型)去修改父组件的month的top值,所以使用事件形式对外发出
+ this.$emit('updateMonthTop', topArr)
+ // 获取每个月份区域的尺寸
+ getMonthRectByPromise(el) {
+ // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
+ // 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
+ return new Promise(resolve => {
+ this.$uGetRect(`.${el}`).then(size => {
+ resolve(size)
+ // nvue下,使用dom模块查询元素高度
+ // 返回一个promise,让调用此方法的主体能使用then回调
+ dom.getComponentRect(this.$refs[el][0], res => {
+ resolve(res.size)
+ // 点击某一个日期
+ clickHandler(index1, index2, item) {
+ if (this.readonly) {
+ 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} 天`)
+ // 如果当前日期大于已有日期,将当前的添加到数组尾部
+ selected.push(date)
+ const startDate = selected[0]
+ const endDate = selected[1]
+ let i = 0
+ // 将开始和结束日期之间的日期添加到数组中
+ 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;
+ &--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
+ :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>
+ <slot name="footer" v-if="showConfirm">
+ <view class="u-calendar__confirm">
+ :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) {
+ const {
+ 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 {
+ 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;