Experience in the development of vue mobile terminal

Introducing Mint UI on demand

Used in this project mint-ui As a basic ui framework, it has encountered many problems in use. The official website doc still can't visit. But thanks to the mint ui team.
Here we recommend a vue mobile ui Library vant

  • Introduction on demand
* 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);

Secondary encapsulation of MT loadmore components

The pull-down refresh and pull-up load of the list are more necessary components of the mobile terminal. But there is something wrong with mt's loadmore component, so I have a layer to make it more
Clear, easy to use

Secondary packaging features

  • Simulation iphone click on the top to scroll through the list to the top.
  • No need to write dead height, and compatible with iPhone x
  • It provides a more concise and easy-to-use method for refreshing, returning to the top, obtaining and setting the scroll bar position
  • Unified UI prompt, without repeating css code.

Code

<template>
  <div class="loader-more" ref="loadBox">
    <mt-loadmore :topMethod="topMethod"
                 :bottomMethod="bottomMethod"
                 :topPullText="`Drop-down refresh`"
                 :bottomPullText="`Pull up to load more`"
                 :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:{ //List item handler
        type:Function,
        default:null
      },
      url:{
        type:String,
        default:""
      },
      param:{ //Query parameters
        type:Object,
        default:{}
      },
      type:{  //Configuring ajax method types
        type:String,
        default:"get"
      },
      dataKey:{ //key to read interface data
        type:String,
        default:"content"
      },
      clickToTop:{ //Do you want to click the top to return to the start
        type:Boolean,
        default:true,
      },
    },
    watch:{
      rows(val){
        this.$emit('change',val);
      }
    },
    mounted(){
      setTimeout( ()=>{
        var myDiv = document.getElementsByClassName('mobile-top')[0];
        //Get style through different methods by judging whether it supports currentStyle (ie or not)
        var finalStyle = myDiv.currentStyle ? myDiv.currentStyle : document.defaultView.getComputedStyle(myDiv, null);
        //More paddingTop for iPhone x
        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)  //Delayed execution, fixed can't get the bug of paddingTop
      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) {
        /*If the parameter is an object, the watch update param will update the method and then execute, resulting in inaccurate bug of parameter merging*/
        return new Promise((resolve,reject)=>{
          setTimeout(()=>{
            this.loadingText = "Loading...";
            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 = "No data"
              }
              resolve(true)
            })
          },100)
        })

      },
      //Drop-down refresh
      topMethod() {
        this.bottomAllLoaded = false;
        this.rows = [];
        this.upData({
          page: 1
        }).then(res => {
          if (res) {
            this.ToastTip("Refresh success", 'suc');
            this.$refs.loadmore.onTopLoaded();
          }
        })
      },
      //Pull up to load more
      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("No more data!")
          this.$refs.loadmore.onBottomLoaded();
        }
      },
      refresh(){
        this.bottomAllLoaded = false;
        this.rows = [];
        this.upData({
          page: 1
        }).then(res => {
          if (res) {
            this.$refs.loadmore.onTopLoaded();
          }
        })
      },
      //External control pull-up refresh
      allLoad(bool){
        this.bottomAllLoaded = bool;
      },
      //wipe data
      clearData(){
        this.rows = [];
      },
      //Handle item functions to facilitate the parent component's operation on list items
      processData(callBack){
        callBack(this.rows);
      },
      //Click the top title to scroll to the beginning of the list
      toTop(){
        var app = document.getElementsByClassName('scrolling')[0]||document.getElementsByTagName('body')[0];
        app.className ="";/*fix bug of page shaking caused by inertial sliding of mobile terminal*/
        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 When the pull-up is not completed, the list will be pulled, resulting in repeated bug s*/
        document.addEventListener('touchstart',(ev)=>{
          if(this.$refs['loadBox']&&this.$refs['loadBox'].contains(ev.changedTouches[0].target)){
            app.className ="scrolling";
            clearInterval(this.timer);
          }
        })
      },
      //Get current scroll position
      getPosition(){
        return this.$el.scrollTop;
      },
      //Set scroll position
      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>

Use

<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>

//List out functions
itemProcess(rows) {
        rows.forEach(item => {
          item.time= new Date().getTime();
        }) 
        return rows
      },

mySelect component

The mobile select component is actually the combination of pop.bottom + picker;

Code

<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">cancel</span>
        <span @click="selected">Determine</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:{ //Display name
        type:String,
        default:'name'
      },
      keyValue:{
        type:String,
        default:'value'
      },
      visibleItemCount:{
        type:Number,
        default:5
      },
      defaultIndex:{//Default selection
        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);//Pass value to the father
        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){  //Set the selected value 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();//Synchronize parent component data;
      },
    }
  }
</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>

Use

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

//Set select
 this.$refs['mySelect'].setTime(index);

Encapsulate the pop component

The pop-up component is generally configured with position to slide in or out of the bottom or pop-up window in the middle. The only harm is that if your page has a lot of pop ups, you have to set many variables true/false to control pop ups. So here I encapsulate it.

  • Reduce css code and component configuration
  • Reduce declaration controls hidden variables

Realization

<!--encapsulation mint-ui It does not need to define variables and methods one by one to control the display and hiding of the pop-up window
  * position:  right  Draw the pop-up window from the right
  * radius: Whether to fillet the pop-up window
  * To open a pop-up window: this.$refs[`What you define popup Of ref`].open()
  * Close the pop-up window: this.$refs[`What you define popup Of 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>

Use

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

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

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

The value of post is the same as that of mint

Time control encapsulation

The time control of mint is also cumbersome to use, and it has also made a secondary encapsulation, which mainly has the following characteristics

  • Get the time value string directly
  • Automatic binding of open and close methods
  • Cancel and save function added
  • Support initialization time and dynamically set time value

Code

<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:{ //Time control type
          type:String,
          default:"date",
        },
        initDate:{//Default to initialize and confirm today
          type:Boolean,
          default:false,
        },
        time:{
          type:String,
          default:''
        },
        delTime:{ //Whether to display the clear time button
          type:Boolean,
          default:true,
        }
      },
      watch:{
        //Confirm selection time and cancel
        confirmTimeStart(val){
          if(val){
            this.$emit("confirm",this.timeStartFmt);
          }else{
            this.$emit("confirm","");
          }
        }
      },
      computed: {
        //Format time
        timeStartFmt() {
          let fmt = this.dateType=="date"?"yyyy-MM-dd":null;
          return this.tools.dateFmt(this.timeStart,fmt);
        },
      },
      mounted(){
        if(this.initDate){
          this.confirmTime();
        }
      },
      methods:{
        //When changing time;
        confirmTime(){
          this.confirmTimeStart = true;
          this.$emit("confirm",this.timeStartFmt);
          this.close();
        },
        /**
        * Author: lzh
        * Function: set the time for the method called by the parent component to cooperate with ref;
        * Parameter: val dateobj
        * Return value:
        */
        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>

Use

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

Package upload picture component

Uploading pictures is also a common component, which is implemented here.

Code

<!--Upload attachments-->
<template>
  <div>
    <form-card-item itemTitle="Upload attachment:" :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:{ //Number of uploaded attachments
        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("Please upload the following types of attachments:  "+type.join(","), "warn",5000);
          this.$refs[`uploadInput`].value = "";
          return;
        }
        if (file.size /(1024*1024) > 50) { //size is BT unit 1kb = 1024bt;
          this.ToastTip("Please upload 50 M Pictures up to", "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("Attachment uploaded successfully","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||"Upload error";
            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);//Number of overwriting attachments
      },
      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
<!--Document attachment-->
<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:'Document attachment'
      },
      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:'Determine',
          title:'Delete files',
          message:'Are you sure you want to delete this file?',
        }).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>

Use

<!--Upload attachments-->
        <uploadFile class="text" ref="uploadFile"  :saveParam="saveParam"
                    v-model="adjunct" :required="true">
        </uploadFile>

Effect

Tags: Javascript Vue Mobile axios IE

Posted on Wed, 04 Dec 2019 06:12:49 -0500 by reaper7861