按需引入mint-ui

本项目用了 mint-ui 作为基础ui框架,在使用中遇到不少问题。官网doc 还不断的访问不了。不过还是很感谢 mint-ui 团队。
在此推荐一个 vue移动端ui库 vant

  • 按需引入
* mint-ui
import 'mint-ui/lib/style.css'
import {
  Navbar,
  TabItem,
  TabContainer,
  TabContainerItem,
  Radio, Actionsheet,
  Switch,
  Popup,
  Button,
  DatetimePicker,
  Toast,
  Picker,
  MessageBox,
  loadmore,
  Range,
  Progress,
  Indicator,
} from 'mint-ui'

Vue.component(Navbar.name, Navbar)
Vue.component(TabItem.name, TabItem)
Vue.component(TabContainer.name, TabContainer)
Vue.component(TabContainerItem.name, TabContainerItem)
Vue.component(Radio.name, Radio)
Vue.component(Actionsheet.name, Actionsheet)
Vue.component(Popup.name, Popup)
Vue.component(Button.name, Button)
Vue.component(DatetimePicker.name, DatetimePicker)
Vue.component(Picker.name, Picker);
Vue.component(loadmore.name, loadmore);
Vue.component(Range.name, Range);
Vue.component(Progress.name, Progress);
Vue.component(Switch.name, Switch);

二次封装 mt-loadmore 组件

列表的下拉刷新和上拉加载更多是移动端必须的组件。但是 mt的 loadmore组件有点问题,因此 我自己包了一层,让它变得更加
明了好用了

二次封装特点

  • 模拟iphone 点击顶部 滚动列表到顶部。
  • 不用写死高度了,并且兼容 iphoneX
  • 对外提供了更加简明易用的 刷新,回到顶部,获得和设置滚动条位置的方法
  • 统一的UI提示,免去重复css代码。

代码

<template>
  <div class="loader-more" ref="loadBox">
    <mt-loadmore :topMethod="topMethod"
                 :bottomMethod="bottomMethod"
                 :topPullText="`下拉刷新`"
                 :bottomPullText="`上拉加载更多`"
                 :autoFill="false"
                 :bottomDistance="40"
                 :topDistance="60"
                 :bottomAllLoaded="bottomAllLoaded"
                 ref="loadmore">
      <ul class="load-more-content" v-if="rows.length>0">
          <slot v-for="(item,index) in rows" v-bind="{item,index}"></slot>
      </ul>
      <ul class="load-more-content" v-else>
        <li class="no-data">{{loadingText}}</li>
      </ul>
    </mt-loadmore>
  </div>
</template>
<script>
  import Bus from "../common/bus.js"
  export default {
    data: function () {
      return {
        rows: [],
        loadingText: '',
        total: 0,
        bottomAllLoaded:false,
        timer:null,
        search: {
          page: 1,
          size: 10,
        },
      }
    },
    props: {
      top:{
        type:[Number,String],
        default:0
      },
      bottom:{
        type:[Number,String],
        default:0
      },
      itemProcess:{ //列表项目处理函数
        type:Function,
        default:null
      },
      url:{
        type:String,
        default:""
      },
      param:{ //查询参数
        type:Object,
        default:{}
      },
      type:{  //配置ajax方法类型
        type:String,
        default:"get"
      },
      dataKey:{ //读取接口的数据的key
        type:String,
        default:"content"
      },
      clickToTop:{ //是否开启点击顶部回到开始
        type:Boolean,
        default:true,
      },
    },
    watch:{
      rows(val){
        this.$emit('change',val);
      }
    },
    mounted(){
      setTimeout( ()=>{
        var myDiv = document.getElementsByClassName('mobile-top')[0];
        //利用判断是否支持currentStyle(是否为ie)来通过不同方法获取style
        var finalStyle = myDiv.currentStyle ? myDiv.currentStyle : document.defaultView.getComputedStyle(myDiv, null);
        //iphoneX 多出来的paddingTop
        var iphoneXPT = parseInt(finalStyle.paddingTop)==20?0:parseInt(finalStyle.paddingTop)-20;
        this.$refs.loadBox.style.top = parseInt(this.top) + iphoneXPT +"px";
        this.$refs.loadBox.style.bottom = parseInt(this.bottom)  + iphoneXPT +"px";
      },100)  //延迟执行,fixed 获取不到paddingTop的bug
      this.search = Object.assign(this.search,this.param);
      this.upData();
      if(this.clickToTop){
        Bus.$on('toTop', () => {
          this.toTop();
        })
      }
    },
    watch:{
      param(val){
        this.search = Object.assign(this.search,val);
      }
    },
    methods:{
      upData(data) {
        /*如果参数是对象,watch更新param会update方法之后执行,导致参数合并不准确bug*/
        return new Promise((resolve,reject)=>{
          setTimeout(()=>{
            this.loadingText = "加载中...";
            var query = Object.assign(this.search, data);
            return this.$http({
              url: this.url,
              data: query,
              type:this.type,
              loading:false,
            }).then(res => {
              let rows = res[this.dataKey];
              this.total = res.total;
              if (rows.length > 0) {
                if(typeof this.itemProcess == 'function'){
                  rows = this.itemProcess(rows);
                }
                this.rows = this.rows.concat(rows);
              }
              if (this.rows.length == 0) {
                this.loadingText = "暂无数据"
              }
              resolve(true)
            })
          },100)
        })

      },
      //下拉刷新
      topMethod() {
        this.bottomAllLoaded = false;
        this.rows = [];
        this.upData({
          page: 1
        }).then(res => {
          if (res) {
            this.ToastTip("刷新成功", 'suc');
            this.$refs.loadmore.onTopLoaded();
          }
        })
      },
      //上拉加载更多
      bottomMethod() {
        if (this.rows.length < this.total) {
          this.bottomAllLoaded = false;
          this.upData({
            page: ++this.search.page
          }).then(()=>{
            this.$refs.loadmore.onBottomLoaded();
          })
        } else {
          this.bottomAllLoaded = true;
          this.ToastTip("没有更多数据了!")
          this.$refs.loadmore.onBottomLoaded();
        }
      },
      refresh(){
        this.bottomAllLoaded = false;
        this.rows = [];
        this.upData({
          page: 1
        }).then(res => {
          if (res) {
            this.$refs.loadmore.onTopLoaded();
          }
        })
      },
      //对外提供控制上拉刷新
      allLoad(bool){
        this.bottomAllLoaded = bool;
      },
      //清空数据
      clearData(){
        this.rows = [];
      },
      //处理item的函数,方便父组件对列表项目操作
      processData(callBack){
        callBack(this.rows);
      },
      //点击顶部标题滚动到列表开头
      toTop(){
        var app = document.getElementsByClassName('scrolling')[0]||document.getElementsByTagName('body')[0];
        app.className ="";/*fix 移动端由于惯性滑动造成页面颤抖的bug*/
        clearInterval(this.timer);
        this.timer =setInterval(()=>{
          var scrollTop= this.$el.scrollTop;
          var ispeed=Math.floor(-scrollTop/8);
          if(scrollTop==0){
            app.className ="scrolling";
            clearInterval(this.timer);
          }
          this.$el.scrollTop = scrollTop+ispeed;
        },10);
        /*fix 上拉未完成时,拉动列表,导致重复上提的bug*/
        document.addEventListener('touchstart',(ev)=>{
          if(this.$refs['loadBox']&&this.$refs['loadBox'].contains(ev.changedTouches[0].target)){
            app.className ="scrolling";
            clearInterval(this.timer);
          }
        })
      },
      //获取当前滚动位置
      getPosition(){
        return this.$el.scrollTop;
      },
      //设置滚动位置
      setPosition(position=0){
        this.$el.scrollTop = position;
      }
    }
  }
</script>
<style lang="scss" scoped>
  .loader-more {
    padding-bottom: 0.2rem;
    background-color: #fff;
    overflow-y: auto;
    /*position: fixed;*/
    position: absolute;
    left: 0;
    right: 0;
    box-sizing: border-box;
  }
</style>

使用

<myLoadMore class="t-body"
            :url="ajaxApi.docSearch.draft"
            :param="param"
            top="65px"
            ref="myLoadMore"
            :itemProcess="itemProcess">
  <li slot-scope="{item}" class="row-box" :key="item.id" @click="toDetail(item.id,item.serviceCode)">
    <div class="row title">{{item.time}}</div>
  </li>
</myLoadMore>

//列表出来函数
itemProcess(rows) {
        rows.forEach(item => {
          item.time= new Date().getTime();
        }) 
        return rows
      },

mySelect 组件

移动端 select 组件 实际 等于 popup.bottom + picker 两个组件组合出来的;

代码

<template>
  <div>
    <div class="selected" @click="show">
      <span style="margin-right: 10px;">{{name}}</span>
      <v-icon name="chevron-down"></v-icon>
    </div>
    <mt-popup class="selected-box" v-model="popupVisible" position="bottom" style="width: 100%;" :closeOnClickModal="false">
      <div class="picker-toolbar flex-ar">
        <span @click="cancel">取消</span>
        <span @click="selected">确定</span>
      </div>
      <mt-picker v-show="popupVisible"
                 :slots="slots"
                 @change="onValuesChange"
                 :value-key="keyName"
                 ref="picker"
                 :visibleItemCount="visibleItemCount">
      </mt-picker>
    </mt-popup>
  </div>
</template>
<script>
  export default {
    data: function () {
      return {
        popupVisible: false,
        name:'',
        value:'',
        oldName:'',
        oldValue:'',
        defaultItem:null,
        slots: [{
          values:[],
          defaultIndex: 0,
        }],
      }
    },
    model:{
      prop:'selectValue',
      event:'change'
    },
    props: {
      selectValue:{
        type:[Number,String]
      },
      dataArr: {
        type: Array,
        default: function () {
          return []
        }
      },
      keyName:{ //显示名
        type:String,
        default:'name'
      },
      keyValue:{
        type:String,
        default:'value'
      },
      visibleItemCount:{
        type:Number,
        default:5
      },
      defaultIndex:{//默认选中项
        type:Number,
        default:0
      }
    },
    watch:{
      popupVisible(val){
        var bottom = document.getElementsByClassName("mobile-bottom");
        if(val){
            for(var i=0;i<bottom.length;i++){
              bottom[i].style.display = "none";
            }
        } else {
            for(var i=0;i<bottom.length;i++){
              bottom[i].style.display = "flex";
            }
        }
      },
    },
    created() {
      this.slots[0].values = this.dataArr;
      this.slots[0].defaultIndex = this.defaultIndex;
      this.defaultItem = {
        name:this.slots[0].values[this.defaultIndex][this.keyName],
        value:this.slots[0].values[this.defaultIndex][this.keyValue],
      };
    },
    methods: {
      show(){
        this.oldName = this.name;
        this.oldValue = this.value;
        this.noScrollAfter.open(this,`popupVisible`)
      },
      cancel(){
        this.name =  this.oldName;
        this.value = this.oldValue;
        this.popupVisible=false;
      },
      selected(){
        this.noScrollAfter.close(this,`popupVisible`)
        this.oldName = this.name;
        this.oldValue = this.value;
        this.$emit('change',this.value);//把value传到父
        this.$emit('select',{name:this.name,value:this.value})
      },
      onValuesChange(picker, values) {
        this.name = values[0][this.keyName];
        this.value = values[0][this.keyValue];
      },
      set(index){  //设置选中值index
        let theIndex = index || this.defaultIndex;
        this.name = this.slots[0].values[theIndex][this.keyName];
        this.value = this.slots[0].values[theIndex][this.keyValue];
        this.slots[0].defaultIndex = index;
        this.selected();//同步父组件数据;
      },
    }
  }
</script>
<style lang="scss" scoped>
  .selected{
    padding: 0.1rem;
    text-align: right;
    display: flex;
    align-items: center;
    justify-content: flex-end;
  }
  .selected-box{
    user-select: none;
    z-index: 3000!important;
    position:fixed;
    right: 0;
    bottom: 0;
  }
  .picker-toolbar{
    height: 40px;
    border-bottom: solid 1px #eaeaea;
    color: #26a2ff;
  }
</style>

使用

<my-select 
         :dataArr="leaveTypeData"
         keyName="enumerationName"
         keyValue="enumerationCode"
         v-model="leaveType"
         ref="mySelect"
         @select="select">
</my-select>

//设置选中
 this.$refs['mySelect'].setTime(index);

封装 popup 组件

popup 组件一般都是通过配置position 达到滑动进入或者底部出来或者中间弹窗的目的。唯一的害处是,如果你的页面有很多弹窗,你要设置很多变量 true/false 来控制弹窗隐现。所以在此我封装了一下。

  • 减少css代码,组件配置
  • 减少声明控制隐藏显示的变量

实现

<!--封装 mint-ui 的弹窗组件,不需要一个个定义变量和方法来控制 弹窗的显示隐藏
  * position: right  从右边划出弹窗
  * radius:是否圆角弹窗
  * 打开弹窗: this.$refs[`你定义的popup的ref`].open()
  * 关闭弹窗: this.$refs[`你定义的popup的ref`].close()
-->

<template>
    <mt-popup v-model="visible" :class="{radiusPopup:radius,wh100:!radius}"
              :modal="radius"   :closeOnClickModal="false" :popup-transition="radius?`popup-fade`:``" :position="position">
      <slot></slot>
    </mt-popup>
</template>
<script>
  export default {
    data: function () {
      return {
        visible: false
      }
    },
    props:{
      position:{
        type:String,
        default:""
      },
      radius:{
        type:Boolean,
        default:true
      }
    },
    methods:{
      open(){
        this.noScrollAfter.open(this,`visible`)
      },
      close(){
        this.noScrollAfter.close(this,`visible`)
      },
      state(){
        return this.visible;
      }
    }
  }
</script>
<style lang="scss" scoped>
</style>

使用

<popup ref="exceptionFlow" position="right" :radius="false">
      xxxx
</popup>

//打开 
this.$refs['exceptionFlow'].open();

//关闭
this.$refs['exceptionFlow'].close();

positon的值跟mint原来是一样的
clipboard.png

时间控件封装

mint 的时间控件使用起来也比较麻烦,也做了二次封装,主要有以下特点

  • 直接得到时间值字符串
  • 自动绑定了open 和 close 方法
  • 添加了取消,保存功能
  • 支持初始化时间,动态设置时间值

代码

<template>
    <div class="timer">
      <div class="item-content">
        <div class="item-content-div" v-show="confirmTimeStart" @click="open">
          <v-icon class="item-content-icon" v-if="delTime" v-show="confirmTimeStart" name="x-circle" @click.native.stop="confirmTimeStart = false"></v-icon>
          {{timeStartFmt}}
        </div>
        <div class="item-content-div" v-show="!confirmTimeStart" @click="open"></div>
        <v-icon class="item-content-icon" name="calendar" @click.native="open"></v-icon>
      </div>
      <mt-datetime-picker
        ref="timePicker"
        :type="dateType"
        @cancel=" timeStart = oldTimeStart;close();"
        @visible-change="oldTimeStart = timeStart;$emit(`timeChange`)"
        @confirm="confirmTime"
        v-model="timeStart">
      </mt-datetime-picker>
    </div>
</template>
<script>
    export default {
      data: function () {
          return {
            timeStart:new Date(),
            confirmTimeStart:false,
          }
      },
      model:{
        prop:'time',
        events:'change',
      },
      props:{
        dateType:{ //时间控件类型
          type:String,
          default:"date",
        },
        initDate:{//是否默初始化并认选中今天
          type:Boolean,
          default:false,
        },
        time:{
          type:String,
          default:''
        },
        delTime:{ //是否显示清空时间按钮
          type:Boolean,
          default:true,
        }
      },
      watch:{
        //确认选择时间和取消
        confirmTimeStart(val){
          if(val){
            this.$emit("confirm",this.timeStartFmt);
          }else{
            this.$emit("confirm","");
          }
        }
      },
      computed: {
        //格式化时间
        timeStartFmt() {
          let fmt = this.dateType=="date"?"yyyy-MM-dd":null;
          return this.tools.dateFmt(this.timeStart,fmt);
        },
      },
      mounted(){
        if(this.initDate){
          this.confirmTime();
        }
      },
      methods:{
        //改变时间时;
        confirmTime(){
          this.confirmTimeStart = true;
          this.$emit("confirm",this.timeStartFmt);
          this.close();
        },
        /**
        * 作者:lzh
        * 功能:设置时间,供父组件调用的方法,配合ref调用;
        * 参数:val  DateObj
        * 返回值:
        */
        setTime(val){
          this.timeStart = val;
          this.confirmTimeStart =val!==""?true:false;
        },
        open(){
          var bottom = document.getElementsByClassName("mobile-bottom");
          this.$refs[`timePicker`].open();
          for(var i=0;i<bottom.length;i++){
            bottom[i].style.display = "none";
          }
        },
        close(){
          var bottom = document.getElementsByClassName("mobile-bottom");
          for(var i=0;i<bottom.length;i++){
            bottom[i].style.display = "flex";
          }
        },
      }
    }
</script>
<style lang="scss" scoped>
  .timer{
    .item-content{
      width: 100%;
      height: 30px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      .item-content-div{
        flex:10;
        border: 1px solid #eaeaea;
        padding: 5px 25px 5px 5px;
        box-sizing: border-box;
        height: 100%;
        position:relative;
        .item-content-icon{
          position:absolute;
          right:5px;
          color: #d8d8d8;
        }
      }
      .icon {
        margin-left: 10px;
        width: 17px;
        height: 17px;
      }
    }
  }
</style>

使用

 <timer @confirm="(val)=>{startTime = val}"></timer>

封装上传图片组件

上传图片也是常用组件,在这里自己实现了一下。

代码

<!--上传附件-->
<template>
  <div>
    <form-card-item itemTitle="上传附件:" :required="required" class="box">
      <input ref="uploadInput" type="file" @change="upload" style="padding-right: 0.5rem;">
      <v-icon v-show="uploading" class="stop" name="x-circle" @click.native.stop="clearFile"></v-icon>
      <progressDom ref="progressId"></progressDom>
    </form-card-item>
    <adjunct ref="list" @delFile="del"></adjunct>
  </div>
</template>
<script>
  import qs from "qs"
  import axios from "axios"

  export default {
    data: function () {
      return {
        all:'all',
        pic:["jpg","jpeg","gif","png"],
        gzip:["zip","rar"],
        uploading:false,
      }
    },
    model:{
      prop:'adjunct',
      event:'change'
    },
    props:{
      adjunct:{ //上传附件个数
        type:Number,
        default:0,
      },
      data:{
        type:Object,
        default:()=>{return {} }
      },
      types:{
        type:String,
        default:"all"
      },
      required:{
        type:Boolean,
        default:false,
      },
      saveParam:{
        type:Object,
        default:()=>{return {}
         }
      }
    },
    methods: {
      upload() {
        let file = this.$refs[`uploadInput`].files[0];
        if (!file){
          this.$emit('change',false);
          return;
        };
        let type = this[this.types];
        if(type!=='all'&&type.indexOf(file.type.split(`/`)[1])==-1){
          this.ToastTip("请上传以下类型附件:  "+type.join(","), "warn",5000);
          this.$refs[`uploadInput`].value = "";
          return;
        }
        if (file.size /(1024*1024) > 50) { //size 是bt单位 1kb = 1024bt;
          this.ToastTip("请上传50M以内大小的图片", "warn");
          this.$refs[`uploadInput`].value = "";
          return;
        }
        let form = new FormData();
        form.append("file", file);
        let actionUrl = process.env.proxyString + this.ajaxApi.attachment.upload + '?' + qs.stringify(this.saveParam);
        this.$refs[`progressId`].start();
        this.uploading = true;
        axios.post(actionUrl, form).then((res) => {
          if (res.status==200&&res.data) {
            this.ToastTip("附件上传成功","suc");
            this.updateList();
            this.$refs[`uploadInput`].value = "";
            let num = this.adjunct+1;
            this.$emit('change',num);
            this.$emit("success");
          } else {
            let msg = data.msg||data.messages||"上传出错";
            this.$refs[`uploadInput`].value = "";
            this.ToastTip(msg, "warn");
          }
          this.$refs[`progressId`].stop();
          this.uploading = false;
        }).catch(res=>{
          console.log(res)
        })
      },
      clearFile(){
        this.$refs[`uploadInput`].value = "";
        this.$refs[`progressId`].stop();
        this.uploading = false;
      },
      del(length){
        this.$emit('change',length);//覆盖附件个数
      },
      updateList(){
        if(this.saveParam&&this.saveParam.docid){
          this.$refs['list'].updateList({
            url:this.ajaxApi.attachment.attachmentList,
            type:'post',
            data: {
              docid:this.saveParam.docid,
              tid:this.saveParam.taskId,
              device:'mobile',
              service:this.saveParam.service
            }
          });
        }
      }
    }
  }
</script>
<style lang="scss" scoped>
  .box.form-item{
    padding-top: 16px;
    padding-bottom: 16px;
  }
  .box /deep/{
     .form-item-value{
      position: relative;
    }
    .stop {
      margin-left: 10px;
      width: 17px;
      height: 17px;
      position:absolute;
      right: 18px;
      top: 12px;
      color: #d8d8d8;
    }
  }
</style>


*  adjunct.vue
<!--文档附件-->
<template>
  <form-card :title="title" class="mb20" v-show="list.length>0">
    <v-icon name="paperclip" slot="title-icon" style="color:#8a8a8a;margin-right: 0.1rem;"></v-icon>
    <form-card-item class="list" v-if="list.length>0" v-for="item in list" :itemTitle="item.name" :key="item.id">
      <icon icon-class="icon-huixingzhen" color="#59a5ff" size="20" slot="before-title-icon"></icon>
      <icon v-show="icon==`download`" icon-class="icon-xiazai1" color="#306bd3" size="28"
            @click.native="download(item)"></icon>
      <v-icon v-show="icon==`del`" name="trash-2" style="color:#8a8a8a;margin-top: 10px;"
              @click.native="del(item)"></v-icon>
    </form-card-item>
  </form-card>
</template>
<script>
  export default {
    data: function () {
      return {
        list:[]
      }
    },
    props:{
      title:{
        type:String,
        default:'文档附件'
      },
      icon:{
        type:String,
        default:'del'
      }
    },
    methods:{
      updateList(param){
        this.$http(param).then(res=>{
          this.list = res.files;
          this.$emit('delFile',this.list.length);
        })
      },
      del(item){
        this.MessageBox({
          closeOnClickModal:false,
          showCancelButton:true,
          confirmButtonText:'确定',
          title:'删除文件',
          message:'确定要删除该文件吗?',
        }).then((res)=>{
          if(res=="confirm"){
            this.$http({
              url:this.ajaxApi.attachment.delAttachment,
              type:"post",
              data:{
                docid:item.documentId,
                fileId:item.id
              }
            }).then(res=>{
              this.ToastTip(res.result,'suc');
              this.list.splice(this.list.findIndex(o=>{
                return o.id == item.id
              }),1);
              this.$emit('delFile',this.list.length);
            })
          };
        })
      },
      download(item){

      }
    },
  }
</script>
<style lang="scss" scoped>
  .mb20 /deep/ .form-item{
    .form-item-value{
      width: auto;
    }
  }
  .list{
    /deep/ .form-item-title{
      word-break: break-all;
      max-width: 6rem;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-size: 14px;
    }
  }
</style>

使用

<!--上传附件-->
        <uploadFile class="text" ref="uploadFile"  :saveParam="saveParam"
                    v-model="adjunct" :required="true">
        </uploadFile>

效果

clipboard.png


clipboard.png


clipboard.png


lidog
119 声望3 粉丝