記 vue 移動端開發 中的經驗(2)

按需引入mint-ui

本項目用了 mint-ui 做爲基礎ui框架,在使用中遇到很多問題。官網doc 還不斷的訪問不了。不過仍是很感謝 mint-ui 團隊。
在此推薦一個 vue移動端ui庫 vantcss

  • 按需引入
* 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組件有點問題,所以 我本身包了一層,讓它變得更加
明瞭好用了vue

二次封裝特色

  • 模擬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 兩個組件組合出來的;ios

代碼

<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 來控制彈窗隱現。因此在此我封裝了一下。git

  • 減小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.pnggithub

時間控件封裝

mint 的時間控件使用起來也比較麻煩,也作了二次封裝,主要有如下特色ajax

  • 直接獲得時間值字符串
  • 自動綁定了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>

封裝上傳圖片組件

上傳圖片也是經常使用組件,在這裏本身實現了一下。axios

代碼

<!--上傳附件-->
<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

相關文章
相關標籤/搜索