記 vue 移動端開發 中的經驗

項目背景

手上的 vue移動端 項目已經開發了大幾個月了,遇到了一些頗有意思的坑,也讓本身學習了不少;寫此文主要目的是記下一些我遇到的坑,以及本身的解決方案,分享的同時也方便之後複習。css

項目的底層是上司經過 Cordova 等經常使用的 hybird app工具打包出來的。而後經過 webview 打開個人vue項目。因此嚴格意義上說,我仍是在作單頁面應用。 hybird app 的底層會提供一些api 給我調用,方便我關閉打開webview,或者跳轉到不一樣子頁面。hybird app會集成不一樣的業務。這些業務有hybird app本事的服務,也有像我這種,徹底來自其服務的頁面。這些就是項目大概的背景。html

提示:因爲是項目總結文章,可能總結點會比較混亂,部分前後,想到什麼寫什麼。

移動端resize.css

body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, button, input, textarea, th, td { margin:0; padding:0;box-sizing: border-box; }
body, button, input, select, textarea { font:12px/1.5tahoma, arial, \5b8b\4f53; }
address, cite, dfn, em, var { font-style:normal; }
code, kbd, pre, samp { font-family:couriernew, courier, monospace; }
small{ font-size:14px; }
ul, ol { list-style:none; }
a { text-decoration:none; color:#000;}
a:hover { text-decoration:none; }
sup { vertical-align:text-top; }
sub{ vertical-align:text-bottom; }
legend { color:#000; }
fieldset, img { border:0; }
button, input, select, textarea { font-size:100%; }
table { border-collapse:collapse; border-spacing:0; }
input{-webkit-appearance: none;}

//直接再main.js 中引入就能夠,common.css 也同樣

* common.css
/*
 * @Author lizhenhua
 * @version 2018/5/14
 * @description
 */




/*--------------頭中底佈局樣式*/

html {
  line-height: initial;
}

body {
  font-size: 0.32rem;
  //padding-top: constant(safe-area-inset-top);
  //padding-top: env(safe-area-inset-top);
}

html, body{
  position: relative;
  height: 100%;
  /*overflow-y: auto;*/
  /*overflow-x: hidden;*/ /*這裏不能加overflow全部屬性,在蘋果下會有上下拉蓋住頂部底部的bug */
}

.page{
  height: 100vh;
  box-sizing: border-box;
  //position: relative;/*relative 不能加載page上,會致使切換動畫失效*/
}
.page-overflow{
  height: 100%;
  overflow: hidden;
}
.mobile-top{
  background: #3275dd;
  position: absolute;
  z-index: 1000;
  top: 0;
  left: 0;
  right: 0;
  padding-top: 20px;
  padding-top: constant(safe-area-inset-top); /* 這裏須要使用 calc 動態計算 */
  padding-top: env(safe-area-inset-top);
  padding-left: constant(safe-area-inset-left);
  padding-left: env(safe-area-inset-left);
  padding-right: constant(safe-area-inset-right);
  padding-right: env(safe-area-inset-right);
}
.mobile-content {
  width: 100%;
  overflow: hidden;
  background: #f1f2f6;
  height: 100vh;
  box-sizing: border-box;
  position: relative;
  padding-top:62.5px;
  padding-top: calc(constant(safe-area-inset-top) + 42.5px);/*1.25rem 自己就預留了信號bar高度0.4rem,這裏要減去*/
  padding-top: calc(env(safe-area-inset-top) + 42.5px);
  padding-bottom:50px;
  padding-bottom: calc(constant(safe-area-inset-bottom) + 50px);
  padding-bottom: calc(env(safe-area-inset-bottom) + 50px);
  padding-left: calc(constant(safe-area-inset-left));
  padding-left: calc(env(safe-area-inset-left));
}
.mobile-content-pb0{
  padding-bottom: 0;
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}
.mobile-bottom{
  height: 1rem;
  height: calc(constant(safe-area-inset-bottom) + 50px);
  height: calc(env(safe-area-inset-bottom) + 50px);
  /*position: fixed;*/
  position:absolute;
  overflow: hidden;
  box-shadow: 0px 0 1px 1px #ccc;
  background: #fff;
  border-bottom: 1px solid #ccc;
  z-index: 1000;
  display: flex;
  left: 0;
  right: 0;
  bottom: 0;
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: constant(safe-area-inset-left);
  padding-left: env(safe-area-inset-left);
  padding-right: constant(safe-area-inset-right);
  padding-right: env(safe-area-inset-right);
}
//安卓彈窗鍵盤頂起底部的bug
@media screen and (max-height: 450px) {
  .mobile-bottom{
    display: none;
  }
}
.load-more-content{ //讓拉動屏幕底部也能夠刷新 load-more
  min-height: 77vh;
}
input[readonly]{
  background: #eee;
}
input:focus {
  outline: none;
}
.v-icon{
  width: 17px;
  height: 17px;
}
.icon{
  width: 17px;
  height: 17px;
}
/*動畫閃屏bug*/
.mint-loadmore-content{
  -webkit-transform-style: preserve-3d;
  -webkit-backface-visibility: hidden;
  transform: translate3d(0,0,0);
  transform-style: preserve-3d;
  backface-visibility: hidden;
  li{
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
  }
}
/*end 動畫閃屏bug*/

/*fix 移動端輸入板 擋住 input ,textarea 的bug*/
.input-bug{
  position: absolute;
  top: 20%;
  left: 0;
  right: 0;
  z-index: 6000;
}
 #inputBugModel{
  width: 4000px;
  height: 4000px;
  top:50%;
  left: 50%;
  transform: translate(-50%,-50%);
  position: absolute;
  background-color: #000;
  opacity: 0.5;
  z-index: 5000;
}
.input-bug-oh{
  overflow: hidden!important;
  -webkit-overflow-scrolling: inherit;
}
/*end fix移動端輸入板 擋住 input textarea 的bug*/

/*end--------------------------- 頭中底佈局樣式*/


/*-------------工具類*/
.flex-ar{
  display: flex;
  justify-content: space-around;
  align-items: center;
}
.flex-bet{
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.fl{
  float: left;
}
.fr{
  float: right;
}
.clear{
  *zoom: 1;
}
.clear:before,
.clear:after {
  display: table;
  line-height: 0;
  content: "";
}

.clear:after {
  clear: both;
}

.dian{
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap
}

.dian4{
  overflow: hidden; /*超出隱藏*/
  text-overflow: ellipsis; /*文本溢出時顯示省略標記*/
  display: -webkit-box; /*設置彈性盒模型*/
  -webkit-line-clamp: 4; /*文本佔的行數,若是要設置2行加...則設置爲2*/
  -webkit-box-orient: vertical; /*子代元素垂直顯示*/
}

.dian3 {
  overflow: hidden; /*超出隱藏*/
  text-overflow: ellipsis; /*文本溢出時顯示省略標記*/
  display: -webkit-box; /*設置彈性盒模型*/
  -webkit-line-clamp: 3; /*文本佔的行數,若是要設置2行加...則設置爲2*/
  -webkit-box-orient: vertical; /*子代元素垂直顯示*/
}
.wh100{
  width: 100%;
  height: 100%;
}
.oh{
  overflow: hidden!important;
  -webkit-overflow-scrolling: inherit;
}
.hide{
  display: none;
}
.no-scroll{
  position: fixed;
  width: 100%;
}
.pd{
  padding:0.2rem;
}
.pd20{
  padding:0.2rem;
}
pl20{
  padding-left:0.2rem;
}
pr20{
  padding-right:0.2rem;
}
.mb0{
  margin-bottom: 0;
}
.mb20{
  margin-bottom: 0.2rem;
}
.mt10{
  margin-top: 0.1rem;
}
.mt20{
  margin-top: 0.2rem;
}
.ml10{
  margin-left: 0.1rem;
}
.tr{
  text-align: right!important;
}
.nowrap{
  white-space: nowrap;
}
.ab-mid{
  position: absolute;
  top:50%;
  left: 50%;
  transform: translate(-50%,-50%);
}
.no-data{
  text-align: center;
  color: #ccc;
  padding: .5rem;
}
.clearfix:after {       //在類名爲「clearfix」的元素內最後面加入內容;
content: ".";     //內容爲「.」就是一個英文的句號而已。也能夠不寫。
display: block;   //加入的這個元素轉換爲塊級元素。
clear: both;     //清除左右兩邊浮動。
visibility: hidden;      //可見度設爲隱藏。注意它和display:none;是有區別的。仍然佔據空間,只是看不到而已;
height: 0;     //高度爲0;
font-size:0;    //字體大小爲0;
}

.no-height {
  height: auto !important;
  .mint-button {
    border-radius: 0;
  }
}
.bg0{
  background: #fff;
}
.bg1{
  background: #f8f8f8;
}

.loading{ /*css3 loading icon*/
  margin: 0;
  padding:0;
  display: inline-block;
  width: 20px;
  height: 20px;
  border: 1px solid #3275dd;
  border-radius: 50%;
  border-left: none;
  animation: rotates 0.8s infinite linear;
}
@keyframes rotates {
  0% {transform: rotate(0);}
  100% {transform: rotate(360deg);}
}


/*動畫*/
  .fade-enter-active {
    transition: all .2s ease;
  }
  .fade-leave-active {
    transition: all .3s ease;
  }
  .fade-enter, .fade-leave-to
    /* .slide-fade-leave-active for below version 2.1.8 */ {
    transform: translateX(100px);
    opacity: 0;
  }
/*end動畫*/



/*end-------------工具類*/


/*-------------默認設定*/

/*end-------------默認設定*/


/*---------------form 相關*/
.form-card-input{
  padding:10px 0.2rem;
  border: none;
  font-size: 14px;
  text-align: right;
  &:focus{
    text-align: left;
  }
}
.form-line{
  width: 100%;
  height: 15px;
  background-color: #f8f8f8;
}
/*小紙條*/
.paper-tips {
  background: #f7f7f7;
  padding: 0.3rem 0.2rem;
  font-size: 15px;
  .tips-top {
    .btn {
      color: #2f6fdd;
    }
  }
  p {
    padding: 0.1rem 0;
    color: #d9534f;
    line-height: 0.4rem;
    font-size: 13px;
    text-align: left;
  }
}
/*end 小紙條*/

/*行中提示*/
.tips {
  font-size: 14px;
  text-align: left;
  padding: 5px 15px;
  color: #a0a0a0;
  background-color: #f8f8f8;
  b {
    font-weight: normal;
  }
}
/*end行中提示*/

/*通用input框 樣式*/
.icon-input-style{
  color: #191919;
  margin-top: 0.1rem;
  border: 1px solid #cccccc;
  border-radius: 5px;
  overflow: hidden;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  input{
    border: none;
    margin: 0;
    padding:0 0.2rem;
    height: 100%;
    width: 100%;
  }
  .iconfont{
    font-size: 20px;
    padding-left: 0.1rem;
    border-left: 1px solid #a4e1fe;
  }
}
/*end通用input框 樣式*/

.no-touch.mint-button{/*禁止點擊按鈕*/
  background-color: #c8c9cc;
  color:#fff;
}


/*改 radio 控件樣式*/
.mint-radiolist /deep/ {
  display: flex;
  justify-content: space-around;
  .mint-cell-wrapper {
    font-size: 14px;
    padding: 0;
    border: none!important;
    background-image: none!important;
    background: transparent!important;
  }
  .mint-cell {
    min-height: auto;
    background: transparent!important;
    background-image: none!important;
  }
  .mint-radio-input:checked + .mint-radio-core {
    background-color: #fff;
  }
  .mint-radio-input:checked + .mint-radio-core::after {
    background-color: #26a2ff;
  }
}


/*------------end form相關*/


/*---------------副頁面相關*/
/*圓角彈窗*/
.radius-popup{
  border-radius: 10px;
  overflow: hidden;
}
.radiusPopup{
  border-radius: 5px;
  overflow: hidden;
}
/*my-popup 右劃頁面樣式*/
body{
  /deep/ .my-popup {
    width: 100%;
    height: 100%;
    .mint-button{
      height: 100%;
    }
    .mobile-content{
      height: 100%;
      box-sizing: border-box;
    }
  }
}
.mint-button{
  .mint-button-text{
    user-select: none;
  }
}
/*end my-popup*/

/*loading圈層級*/
.mint-msgbox-wrapper{
  z-index: 3000!important;
  .mint-msgbox{
    box-shadow: 0 0 10px #ccc;
  }
}
.mint-indicator-wrapper{
  z-index: 4000;
}
.mint-indicator-mask{ //loading 蓋住頁面
  z-index: 4000;
}
/*end loading圈層級*/

/*表格*/
.gf-table {
  text-align: left;
  .t-head {
    background: #f5f5f5;
    font-size: 14px;
    height: 35px;
    color: #8f8f8f;
  }
  .row {
    height: 100%;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 0 0.2rem;
    .item {
      text-align: left;
      width: 2rem;
      font-size: 13px;
      span {
        color: #8f8f8f;
      }
    }
    .item:last-child {
      width: 3rem;
    }
  }
  .t-body .row {
    min-height: 50px;
    border-bottom: 1px solid #ededed;
    margin-left: 0.2rem;
    padding: 0 0.2rem 0 0;
    &:last-child {
      border-bottom: none;
    }
  }
}
/*表格end*/

/*Toast 顏色*/
.mint-toast{
  z-index: 2010;
  word-break: break-all;
}
.mint-toast.is-placebottom{
  font-weight: bolder;
  &.err{
    //background: rgba(245,108,108,0.8);
    background: #feccd5;
    color:#f56c6c;
  }
  &.suc{
    //background: rgba(103,194,58,0.8);
    background: #cdf9c3;
    color:#67c23a;
  }
  &.warn{
    //background: rgba(230,162,60,0.8);
    background: #fde8af;
    color:#e6a23c;
  }
  &.info{
    //background: rgba(144,147,153,0.7);
    background: #eaeaeb;
    color: #686b71;
  }
}
/*end Toast 顏色*/


/*end---------------副頁面相關*/

上中下三部分的css定位問題。

這個問題我在 文章 中已經詳細說過。vue

rem 的使用;

我直接在 app.vue 中添加如下方法,運行後,你會在html 標籤中看到 fontsize 設置爲了50px; 表示 1rem = 50px;node

created() {
      this.resize(document, window);
    },
    
methods:{
    /*設置rem參照單位。width:1rem = 50px 因此設計稿寬 375px == 375/50 = 7.5rem
      * 因爲頁面中有些元素用了絕對定位。特別是top,bottom。因爲設備不一樣,計算出的rem不一樣,
      * 致使定位覆蓋。因此,建議涉及高度的 統一用 px 作單位,包括padding-top,bottom等。
      * 由於高度存在滾動條,不存在適配問題。主要針對寬度作適配。
      *
      * */
      resize(doc, win) {
        var docE1 = doc.documentElement,
          resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
          recalc = function () {
            var clientWidth = docE1.clientWidth;
            if (!clientWidth) return;
            //docE1.style.fontSize = clientWidth / 375  + 'px'; 這裏但願設置 1rem = 1px,實驗證實,這樣作 會致使 html 的 fontsize小於 12px
            docE1.style.fontSize = (clientWidth / (375*2)) * 100 + 'px'; //乘以100的意義是,1爲了避免受fontsize小於12的影響,2爲了計算方便;
          };
        if (!doc.addEventListener) return;
        win.addEventListener(resizeEvt, recalc, false);
        doc.addEventListener('DOMContentLoaded', recalc, false);
      },
}

clipboard.png

使用建議:
1,少許大小的定義儘可能使用px,由於對自適應效果影響不大。例如某個div的padding,設置爲5px 10px,影響是不大的。
2,寬度上的定義儘可能使用rem 做爲單位,由於移動設備對寬度敏感,可謂寸金寸土。設置了以上代碼後,能夠經過設計稿尺寸/50 獲得rem單位的數值。 例如 padding:10px; 能夠寫成 padding: 10px 0.2rem; 或者 padding:0.2rem;
3,高度上的定義,儘可能使用px;由於本項目能夠滾動內容頁,因此高度是不敏感的。設置爲px 的緣由是,後面定位 loadermore 組件會有幫助。固然,若是你對計算頗有把握,或者頁面內容不容許滾動,也可使用 rem;css3

刷新某個子頁面

遇到一個填寫表單點保存造成草稿模式的需求。要求在url中加入參數 id;刷新本頁面,從新經過id獲取數據回填。 vue 是單頁面應用,確定不能全局刷新。web

同事的解決方案

調用保存接口,獲取到id後, 經過ajax

this.router.push(this.$route.path + "&id=" + id);//加參數本頁並不會刷新

改變url ,而後從新申請 調用接口,拿到最新的數據,回填回去。
這樣作,理論上是行得通的。當時很危險,由於用戶操做頁面,會改變不少變量。若是回填數據後,因爲沒有經歷完整的created等生命週期,這些變量仍是原來狀態,容易出bug;
其次,若是像本項目那樣,須要支持 hybird app 經過url+id 的方式直接去到草稿的話,代碼很差維護。因此,最理想的作法,就是真實的從新
load 一次這個子頁面。vuex

正確作法

利用vue 的provide / inject apisegmentfault

* app.vue 中定義
 <router-view v-if="isRouterAlive"/>
data() {
      return {
        isRouterAlive: true,
      }
    },
 provide() {
      return {
        reload: this.reload,
      }
    },
methods: {
    reload() {
        this.isRouterAlive = false
        this.$nextTick(() => (this.isRouterAlive = true))
      },
    }


* 須要刷新的子頁面
inject: ['reload'],

//須要調用的地方
let path =  this.$route.path+"?id="+id
this.$router.replace(path);
this.reload();

keep-alive 頁面怎麼刷新

這個需求很常見,有個列表頁面,點擊某一條去到詳情頁面,點擊返回,列表頁面保持狀態不變,滾動條保持原來位置。若是,詳情對數據作了改變,點擊返回,列表頁面才刷新。api

* app.vue 中
<div id="app">
    <keep-alive>
      <router-view v-if="isRouterAlive&&$route.meta.keepAlive"/>
    </keep-alive>
    <router-view v-if="isRouterAlive&&!$route.meta.keepAlive"/>
  </div>

* route.js 中
{
      path: 'a',//個人草稿
      name: 'myDraft',
      meta:{
        keepAlive:true,
      },
      component: resolve => require(['page/myDraft'],resolve)
    },

這樣,定義了meta keepAlive 爲true 的頁面就會被 緩存。數據不變的狀況下,點擊返回, 只要把滾動條位置設置到原來離開哪裏就行了。

可是問題來了,1,從首頁進入 keepAlive 頁面,每次都要刷新,二,詳情頁若是改變了數據,返回後也要刷新 頁面。

這裏我主要經過 eventBus 來解決了組件通知 頁面 刷新的問題。
細節能夠看 個人筆記,最好的實踐應該是最後提到大神的連接文章。

topBar組件 點擊返回,回到各個出發頁面。

* topBar.vue 
組件的封裝並不難,就是預留自定cancel函數,否則就調用 app.vue 中的 backHome 函數 對返回作統一處理
inject:['backHome'],
 cancel(){
        if(this.popup){ 
          this.$emit('cancel')
        }else{
          this.backHome();
        }
      },
      
* app.vue

provide() {
      return {
        backHome:this.backHome
      }
    },
backHome(){ //返回或退出webview
        let  isOutsidePage = this.$route.params.inside;
        let  from = this.$route.params.from;
        if(isOutsidePage=='in'){ //內頁跳轉
          if(from=="CC"){ //回到a中心
            this.$router.replace('/controlCenter')
          }else if(from=="SF"){ //回到b中心
            this.$router.replace('/controlCenter2')
          }else { //回到原來的子頁面(從a頁到b頁前,必需要先保存lastFullPath)
            this.$router.replace(this.$store.getters.lastFullPath)
            this.$store.commit('setLastFullPath',"")//置空舊路徑
          }
        }else{//關閉webView
            closeWebView();
        }
      }
      
* router.js
{
      path: '/myDraft/:from/:inside',
      name: 'myDraft',
      component: resolve => require(['page/myDraft'],resolve)
    },
    {
      path: '/myDraft', 
      redirect: 'myDraft/ll/out',
    },

經過上面的定義 //hybrid app 只須要調用 ip:xxxx/myDraft 就能打開這個頁面,而且返回鍵自動關閉webview;

經過 CC CF 等標誌字符 能夠判斷來自哪一個 中心的。

最後來到重點的 子頁跳子頁返回 操做,主要就是須要藉助vuex 保存舊 路徑

a.vue 子頁
//跳轉前先把當前路徑保存到全局vuex變量lastFullPath
this.$store.commit('setLastFullPath',route.fullPath)//保存路由用於返回本頁
this.$router.replace('/ ');//清空路由,不重置會致使url 混亂。
this.$router.replace(`b/`+route.name+`/in?id=`+id);

eventBus 使用

bus.vue

import Vue from 'vue'
export default new Vue()

//監聽事件
Bus.$on('update', (param) => { //監聽數據變更
        this.updatexxx(param);
      })
      
//觸發事件
Bus.$emit('update',param)

//銷燬事件監聽
 Bus.$off('update');

用鑽層列表 代替 樹形組件

樹形選擇 組件在pc端是經常用到的。特別是一些有明確層級關係,又須要勾選的數據。
可是移動端開發不能用樹,一般就是像百度網盤那樣,類型文件夾的方式交互。

原理

我項目是選擇部門,而後選擇人員,勾選或者取消。支持快速查詢選擇。
個人思路是,設置兩個組件,一個presonInput,一個personBox;
personInput 主要用於表單中的顯示,支持輸入中文或者拼音,查找並生成選中人員。
personBox 便於選擇多我的或部門,是一個頁面大小的彈窗頁,鑽層列表,支持搜索。
input和Box 兩個組件 都經過v-model 爲父頁面 維護同一組數據。就是選擇的人員的數組。

實現

* personInput.vue 核心代碼

created(){
      document.addEventListener('touchstart',(e)=>{  //點擊其餘地方下拉框消失
        if(this.$refs['con']&&!this.$refs['con'].contains(e.target)){
          this.visible=false;
        }
      })
    },

mounted(){
      Bus.$emit('updateHasSelectPerson');//通知selectPerson 組件更新緩存;
    },
    
cancelSelect(item) {
        //用這一句會不許確,請用findIndex
        // this.hasSelectPerson.splice(this.hasSelectPerson.indexOf(item),1);
        this.hasSelectPerson.splice(this.hasSelectPerson.findIndex(k => k.id == item.id), 1);
        Bus.$emit('updateHasSelectPerson');
      },    

 selected(item) {
        this.visible = false;
        this.inputText = "";
        if (this.one) {
          this.hasSelectPerson.splice(0);//先清空數組
        }else if(this.limit&&this.hasSelectPerson.length==this.limit){
          this.sureTips("最多選擇 "+this.limit+" 我的");
          return;
        }
        //從帶部門的接口中,選擇出id與 人員接口的userCode 相同的人
        this.$http({
          url: this.ajaxApi.department.search,
          type: "post",
          data: {
            key: item.name,
          }
        }).then(res=>{
          let theGuy = res.filter(i=>{
            return i.id == item.userCode
          })
          this.hasSelectPerson.push(theGuy[0]);
        })
        Bus.$emit('updateHasSelectPerson'); //通知personBox 組件同步更新數據
      },
  • personBox 核心代碼
<template>
  <div class="my-popup">
    <topBar :back="true" :popup="true" :title="title" @cancel="cancel" :saveBtn="true" @save="save"></topBar>
    <div class="mobile-content mobile-content-pb0">
        <div class="pd20">
          <div class="icon-input-style">
            <input type="text" v-model="searchText" @keyup.enter="search" @blur="search" :placeholder="`請輸入人名或拼音搜索`">
            <icon icon-class="icon-search" @click.native="search"></icon>
          </div>
          <div class="list-btn flex-bet">
            <span v-if="dataIndex==1" class="no-more">
              <icon icon-class="icon-houtui"  size="25"></icon>
              <b>上一層</b>
            </span>
            <span v-if="dataIndex>1" @click="goBack">
               <icon icon-class="icon-houtui"  size="25"></icon>
              <b>上一層</b>
            </span>
            <span v-if="dataIndex==listData.length" class="no-more">
              <b>下一層</b>
              <icon icon-class="icon-qianjin"  size="25"></icon>
            </span>
            <span v-if="dataIndex>=1&&dataIndex<listData.length" @click="forward">
              <b>下一層</b>
              <icon icon-class="icon-qianjin"  size="25"></icon>
            </span>
          </div>
          <div class="person-list">
            <ul v-if="person&&person.length>0">
              <li v-for="(item,index) in person" :key="item.id">
                <div v-if="item.isParent==`false`" class="check-box" @click="selected(item.id,item,item.checked)">
                  <icon v-if="item.checked" color="#42bd56" icon-class="icon-checkbox-copy"></icon>
                  <icon v-else color="#000" icon-class="icon-checkbox"></icon>
                </div>
                <div class="item-icon">
                  <icon v-if="item.isParent==`true`" size="20" color="#2e6bd5" icon-class="icon-bumen"></icon>
                  <icon v-else size="20" color="#2e6bd5" icon-class="icon-iconmaijia" @click.native="selected(item.id,item,item.checked)"></icon>
                </div>
                <div class="item-title dian" v-if="item.isParent==`true`" @click="getData(item.id)">{{item.display}}</div>
                <div class="item-title dian" v-else @click="selected(item.id,item,item.checked)">{{item.display}}</div>
                <div v-if="item.isParent==`false`" class="selected-span" @click="selected(item.id,item,item.checked)"></div>
                <div v-else class="selected-span" @click="getData(item.id)"></div>
              </li>
            </ul>
            <ul v-else>
              <li style="justify-content: center;">暫無數據</li>
            </ul>
          </div>
        </div>
        <div class="pd20">
          <div class="has-select">
            <h3>已選擇的人員:</h3>
            <ul class="person-name-box">
              <li v-for="item in hasSelectPerson" class="person-name">{{item.name}}<span @click="cancelSelect(item)">×</span></li>
            </ul>
          </div>
        </div>
    </div>
  </div>
</template>
<script>
  import Bus from "../common/bus.js"
  export default {
    data: function () {
      return {
        person : [],//部門的person數組,
        searchText:"",//搜索關鍵字
        listData:[],//緩存每次查詢結果
        dataIndex:1, //當前渲染data指針
        forwardAction:false,//方便模擬 上一步動做
        oldSelected:[],
        first:true,
      }
    },
    model:{
      prop:'hasSelectPerson',
      event:'change'
    },
    props:{
      hasSelectPerson:{ //已經選擇的人員
        type:Array,
        default:()=>{
          return []
        }
      },
      title:{ //彈窗標籤
        type:String,
        default:"選擇人員"
      },
      one:{//是否單選
        type:Boolean,
        default:false
      },
      limit:{
        type:Number,
        default:100,
      }
    },
    created(){
      //緩存第一次進來的數據,方便後面取消選擇操做使用
      if(this.first){ //第一次操做,而且新舊值相同
        this.oldSelected = this.tools.cloneObj(this.hasSelectPerson);
        this.first = false;
      }
      this.$store.dispatch('getDeptList').then(res=>{
        this.upDatePerson(res)
      })
    },
    mounted(){
      Bus.$on('updateHasSelectPerson', () => { //監聽數據變更
        this.save();
      })
    },
    watch:{
      //選擇的人員若是改變,就更新person
      hasSelectPerson(val){
        //當在 personInput 改變了 hasSelectPerson 數組的時候,手動同步 oldSelected
        if(this.first&&!this.tools.eq(this.oldSelected,this.hasSelectPerson)){
          this.oldSelected = this.tools.cloneObj(this.hasSelectPerson);
        }
        this.person.forEach(k=>{
          k.checked = false;
          val.forEach(o=>{
            if(k.id == o.id){
              k.checked = true
            }
          })
        })
      },
      //若是變爲單選,就取第一個已選擇人員
      one(val){
        if(val){
          this.hasSelectPerson.splice(1);//先清空數組
        }
      }
    },
    methods: {
      upDatePerson(res,boor){ //boor 爲true時,不改變listData;
        if(!boor){
          this.listData.push(res)
        }
        this.person = res
        if(this.person){
          this.person.forEach(k=>{
            k.checked = false;
            this.hasSelectPerson.forEach(o=>{
              if(k.id == o.id){
                k.checked = true
              }
            })
          })
        }
      },
      cancel() {
        if(!this.tools.eq(this.oldSelected,this.hasSelectPerson)){
          this.MessageBox({
            showCancelButton:true,
            confirmButtonText:'保存',
            cancelButtonText:'不保存',
            title:'改變保存',
            message:'選擇人員發生改變,須要保存嗎?',
          }).then((res)=>{
            if(res=="confirm"){
              this.save();
            };
            if(res=='cancel'){
              this.hasSelectPerson.splice(0);//清空已選擇
              this.hasSelectPerson.push(...this.oldSelected);//用原來的替換
              this.$emit('cancel')
            }
          })
        }else{
          this.$emit('cancel')
        }
      },
      save(){
        this.first = true;
        this.oldSelected  =this.tools.cloneObj(this.hasSelectPerson);
        this.$emit('cancel')
      },
      selected(id,item,checked){
        this.first = false;//區別於 personInput 的select操做
        if(!checked){ //若是未選擇,就操做選中
          if(this.one){
            this.hasSelectPerson.splice(0);//先清空數組
          }else if(this.limit&&this.hasSelectPerson.length==this.limit){
            this.sureTips("最多選擇 "+this.limit+" 我的");
            return;
          }
          this.hasSelectPerson.push(item);
        }else{
          //這裏若是用filter ,會徹底替換了 this.hasSelectPerson;vue 失去了雙向綁定
          // this.hasSelectPerson = this.hasSelectPerson.filter((o)=>{return o.id!=id});
          //下面這樣只是在原數組上作修改,因此沒有破壞雙向綁定機制;
          this.hasSelectPerson.splice(this.hasSelectPerson.findIndex(k=>k.id==item.id),1);
        }
      },
      cancelSelect(item){
        this.first = false;//區別於 personInput 的select操做
        this.hasSelectPerson.splice(this.hasSelectPerson.findIndex(k=>k.id==item.id),1);
      },
      //功能:獲取數據
      getData(id){
        this.$store.dispatch('getDeptListChild',id).then(res=>{
          if(this.forwardAction){ //點擊了上一步,而後緊接着加載數據,則先把以前的下一層的緩存去掉,模擬瀏覽器行爲
            this.listData.splice(this.dataIndex);
          }
          this.dataIndex++;
          this.upDatePerson(res)
        })
      },
      search(){
        if(!this.searchText){
          this.ToastTip("請輸入名字查找",'warn')
          return
        }
        this.$http({
          url: this.ajaxApi.department.search,
          type:"post",
          data:{
            key:this.searchText
          }
        }).then(res=>{
          this.dataIndex+=1;
          this.upDatePerson(res)
        })
      },
      /**
      * 做者:lzh
      * 功能:返回上一步
      * 參數:
      * 返回值:
      */
      goBack(){
        this.dataIndex--;
        this.forwardAction = true;//點擊了上一步
        this.upDatePerson(this.listData[this.dataIndex-1],true)
      },
      /**
      * 做者:lzh
      * 功能:返回下一步
      * 參數:
      * 返回值:
      */
      forward(){
        this.dataIndex++;
        this.forwardAction = false;
        this.upDatePerson(this.listData[this.dataIndex-1],true)
      }
    },
  }
</script>
<style lang="scss" scoped>
  .icon-input-style{
    background: #fff;
    margin-bottom: 0.1rem;
    box-shadow: 0 1px 1px 1px #ccc;
    i{
      color:#2e6bd5;
    }
  }
  .person-list{
    background: #fff;
    height: 5rem;
    overflow-y: auto;
    border-radius:0 0 5px 5px;
    ul{
      padding:0.1rem 0.1rem;
      li{
        border-bottom: 1px solid #f2f6fd;
        height: .7rem;
        display: flex;
        justify-content: left;
        align-items: center;
        .check-box,.item-icon{
          width: 0.7rem;
          height: 100%;
          text-align: center;
          line-height:  0.7rem;
          i{
            margin-right: 0;
          }
        }
        .item-title{
          height: 100%;
          line-height: 0.7rem;
          font-size: 16px;
          &:active{
            opacity: 0.4;
          }
        }
        .selected-span{
          flex:2;
          height: 100%;
        }
      }
    }
  }
  .list-btn{
    padding:0.2rem 1rem;
    background: #fff;
    border-radius: 5px 5px 0 0;
    /*margin-top: 0.2rem;*/
    padding-bottom: 0.1rem;
    border-bottom: 1px solid #f2f6fd;
    span{
      font-size: 14px;
      display: flex;
      align-items: center;
      i{
        margin:0 5px;
      }
    }
    span.no-more{
      opacity: 0.4;
    }
    span:active{
      opacity: 0.4;
    }
    .iconfont{
      opacity: 0.8;
    }
  }
  .person-name-box{
    text-align: left;
    padding:0.2rem;
    line-height: 0.2rem;
    box-sizing: border-box;
    max-height: 4rem;
    overflow-y: auto;
    .person-name{
      display: inline-block;
      padding:0.15rem;
      background:#4e7ccc;
      color: #fff;
      border-radius: 3px;
      margin-right: 0.2rem;
      margin-bottom: 0.1rem;
      margin-left: 0.1rem;
      margin-top: 0.1rem;
      position: relative;
      line-height: 0.25rem;
      font-size: 14px;
      span{
        display: inline-block;
        width: 0.3rem;
        height: 0.3rem;
        background: red;
        border-radius: 50%;
        text-align: center;
        font-size: 16px;
        line-height: 0.3rem;
        position: absolute;
        top:-0.1rem;
        right: -0.2rem;
      }
    }
  }
  .has-select{
    h3{
      text-align: left;
      font-size: 16px;
      height: 0.6rem;
      line-height: 0.6rem;
    }
    .person-name-box{
      background: #fff;
      border-radius: 5px;
      height: 3.5rem;
      overflow-y: auto;
    }
  }

</style>
  • 父組件使用
<person-input title="受權人" @select="$refs[`permitMenBox`].open()" required v-model="permitMen" :one="true"/>

 <personBox @cancel="$refs[`permitMenBox`].close()" v-model="permitMen" :one="true"/>

permitMen: [],

效果

  • input效果

clipboard.png

  • personBox 效果

clipboard.png

比較兩個對象是否相等

eq(a, b, aStack, bStack) {
    var toString = Object.prototype.toString;

    function isFunction(obj) {
      return toString.call(obj) === '[object Function]'
    }

    function eq(a, b, aStack, bStack) {

      // === 結果爲 true 的區別出 +0 和 -0
      if (a === b) return a !== 0 || 1 / a === 1 / b;

      // typeof null 的結果爲 object ,這裏作判斷,是爲了讓有 null 的狀況儘早退出函數
      if (a == null || b == null) return false;

      // 判斷 NaN
      if (a !== a) return b !== b;

      // 判斷參數 a 類型,若是是基本類型,在這裏能夠直接返回 false
      var type = typeof a;
      if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

      // 更復雜的對象使用 deepEq 函數進行深度比較
      return deepEq(a, b, aStack, bStack);
    };

    function deepEq(a, b, aStack, bStack) {

      // a 和 b 的內部屬性 [[class]] 相同時 返回 true
      var className = toString.call(a);
      if (className !== toString.call(b)) return false;

      switch (className) {
        case '[object RegExp]':
        case '[object String]':
          return '' + a === '' + b;
        case '[object Number]':
          if (+a !== +a) return +b !== +b;
          return +a === 0 ? 1 / +a === 1 / b : +a === +b;
        case '[object Date]':
        case '[object Boolean]':
          return +a === +b;
      }

      var areArrays = className === '[object Array]';
      // 不是數組
      if (!areArrays) {
        // 過濾掉兩個函數的狀況
        if (typeof a != 'object' || typeof b != 'object') return false;

        var aCtor = a.constructor,
          bCtor = b.constructor;
        // aCtor 和 bCtor 必須都存在而且都不是 Object 構造函數的狀況下,aCtor 不等於 bCtor, 那這兩個對象就真的不相等啦
        if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) {
          return false;
        }
      }


      aStack = aStack || [];
      bStack = bStack || [];
      var length = aStack.length;

      // 檢查是否有循環引用的部分
      while (length--) {
        if (aStack[length] === a) {
          return bStack[length] === b;
        }
      }

      aStack.push(a);
      bStack.push(b);

      // 數組判斷
      if (areArrays) {

        length = a.length;
        if (length !== b.length) return false;

        while (length--) {
          if (!eq(a[length], b[length], aStack, bStack)) return false;
        }
      }
      // 對象判斷
      else {

        var keys = Object.keys(a),
          key;
        length = keys.length;

        if (Object.keys(b).length !== length) return false;
        while (length--) {

          key = keys[length];
          if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) return false;
        }
      }

      aStack.pop();
      bStack.pop();
      return true;

    }


    return eq(a, b, aStack, bStack)

  },

輸入面板 擋住 textarea 或者 input

移動端常見問題,緣由上網找找。特徵也比較明顯,就是視口高度改變了,某些手機會觸發 onresize 事件。
解決方案有不少,由於個人例子比較極端。本身搞出來一個比較極端的方案。就是把 整個 輸入區域 定位到頂部,輸入完後恢復。
雖然極端,我的以爲也算是一個通用作法,不用考慮滾動,兼容各類莫名其妙的問題。

方法實現

/**
   * 做者:lzh
   * 功能:解決移動端輸入板擋住輸入框bug
   * 參數:id,須要修復點擊bug的父元素id;
   * 參數:pullClass,須要被提起的盒子class;
   * 參數:scrollContentClass,發生滾動的盒子class,默認mobile-content;
   * 參數:top,發生滾動的盒子class,默認mobile-content;
   * 說明:fixBug,只有在原生標籤 加上fixBug="true" 自定義屬性才彈起修復;
   * 返回值:
   */
  fixInputBug(id="app",pullClass="form-item",scrollContentClass="mobile-content",top=100){
    var mobileArr = ["iPhone", "iPad", "Android", "Windows Phone", "BB10; Touch", "BB10; Touch", "PlayBook", "Nokia"];
    var ua = navigator.userAgent;
    var res = mobileArr.filter(function (arr) {
      return ua.indexOf(arr) > 0;
    });
    var nodeObj = document.getElementById(id);
    if (res.length > 0) {
      nodeObj.onclick = function (ev) {
        var ev = ev || nodeObj.event;
        var target = ev.target || ev.srcElement;
        let content = findParent(target,pullClass);
        let father = findParent(target,scrollContentClass);
        let scrollTop = father.scrollTop;
        let model = document.createElement('div');
        model.id = "inputBugModel";
        if (target.nodeName.toLowerCase() == 'input' || target.nodeName.toLowerCase() == 'textarea') {
          if(target.type!=="radio"&&target.type!=="checkbox"&&target.getAttribute('fixBug')){
            addClass(content,"input-bug")
            addClass(father,"input-bug-oh")
            if(document.getElementById("inputBugModel")){
              father.removeChild(document.getElementById("inputBugModel"));
            }
            father.appendChild(model);
            father.scrollTop = top;
            target.onblur = function () {
              removeClass(content,"input-bug")
              removeClass(father,"input-bug-oh")
              father.removeChild(model);
              father.scrollTop = scrollTop;
            }
          }
        }
      }
      function addClass(node,className) {
        if(node.className.split(" ").indexOf(className)==-1){
          node.className = node.className + ' ' + className;
        }
      }
      function removeClass(node,className) {
        node.className = node.className.replace(" "+className, '');
      }
      function  findParent(node, className){
        let target = node;
        if (target && target.parentNode&&target.parentNode.nodeName!=='HTML') {
          if(target.parentNode.className.split(" ").indexOf(className)!==-1){
              return target.parentNode;
          } else {
            return findParent(target.parentNode,className)
          }
        } else {
          return document.getElementsByTagName('body')[0];
        }
      }
    }
  },

* css
/*fix 移動端輸入板 擋住 input ,textarea 的bug*/
.input-bug{
  position: absolute;
  top: 20%;
  left: 0;
  right: 0;
  z-index: 6000;
}
 #inputBugModel{
  width: 4000px;
  height: 4000px;
  top:50%;
  left: 50%;
  transform: translate(-50%,-50%);
  position: absolute;
  background-color: #000;
  opacity: 0.5;
  z-index: 5000;
}
.input-bug-oh{
  overflow: hidden!important;
  -webkit-overflow-scrolling: inherit;
}
/*end fix移動端輸入板 擋住 input textarea 的bug*/

使用

<textarea v-model="item.reason" fixBug="true"></textarea>

mounted(){
      this.tools.fixInputBug("permitFlowContent");
    },

效果

clipboard.png

移動端快速點擊

因爲移動端瀏覽器存在300ms 延遲,某些組件須要快速響應點擊事件,例如 - 0 + 組件;
利用 fastclick 插件 封裝了一個組件

fastclick組件

<!--快速點擊封裝-->
<template>
  <div class="box fastClick">
    <slot></slot>
  </div>
</template>
<script>
  import fastclick from 'fastclick'


  export default {
    data: function () {
      return {}
    },
    mounted() {
      let dom = document.getElementsByClassName('fastClick')
      for (var i = 0; i < dom.length; i++) {
        fastclick.attach(dom[i]);
      }
    },
  }
</script>
<style lang="scss" scoped>
  .box{
    touch-action: none;
  }
</style>

使用

<fastClick>
    <mt-button size="small" class="number-button" @click.native="dayChange">
    </mt-button>
</fastClick>

輸入板頂起底部 button

focus 的時候,因爲底部的 mobile-bottom 部分是 absolute 的,因此被頂起來。
網上不少說法經過js判斷 onresize 事件 控制 底部顯示隱藏。能夠實現,可是存在兼容性問題。且代碼囉嗦
這裏直接經過css 媒體查詢實現了。

@media screen and (max-height: 450px) {
  .mobile-bottom{
    display: none;
  }
}

適配 iphoneX

蘋果給出了 iphone的 有效區域概念。只要給碰到邊框的大div作些css兼容寫法就能夠了。
設置高,寬,top,left,right,bottom 的都加上兼容。

  • 原來代碼
.mobile-top{
  background: #3275dd;
  position: absolute;
  z-index: 1000;
  top: 0;
  left: 0;
  right: 0;
  padding-top: 20px;
}
.mobile-content {
  width: 100%;
  overflow: hidden;
  background: #f1f2f6;
  height: 100vh;
  box-sizing: border-box;
  position: relative;
  padding-top:62.5px;
  padding-bottom:50px;
}
.mobile-bottom{
  height: 1rem;
  /*position: fixed;*/
  position:absolute;
  overflow: hidden;
  box-shadow: 0px 0 1px 1px #ccc;
  background: #fff;
  border-bottom: 1px solid #ccc;
  z-index: 1000;
  display: flex;
  left: 0;
  right: 0;
  bottom: 0;
}
  • 兼容代碼
.mobile-top{
  background: #3275dd;
  position: absolute;
  z-index: 1000;
  top: 0;
  left: 0;
  right: 0;
  padding-top: 20px;
  padding-top: constant(safe-area-inset-top); /* 這裏須要使用 calc 動態計算 */
  padding-top: env(safe-area-inset-top);
  padding-left: constant(safe-area-inset-left);
  padding-left: env(safe-area-inset-left);
  padding-right: constant(safe-area-inset-right);
  padding-right: env(safe-area-inset-right);
}
.mobile-content {
  width: 100%;
  overflow: hidden;
  background: #f1f2f6;
  height: 100vh;
  box-sizing: border-box;
  position: relative;
  padding-top:62.5px;
  padding-top: calc(constant(safe-area-inset-top) + 42.5px);/*1.25rem 自己就預留了信號bar高度0.4rem,這裏要減去*/
  padding-top: calc(env(safe-area-inset-top) + 42.5px);
  padding-bottom:50px;
  padding-bottom: calc(constant(safe-area-inset-bottom) + 50px);
  padding-bottom: calc(env(safe-area-inset-bottom) + 50px);
  padding-left: calc(constant(safe-area-inset-left));
  padding-left: calc(env(safe-area-inset-left));
}
.mobile-bottom{
  height: 1rem;
  height: calc(constant(safe-area-inset-bottom) + 50px);
  height: calc(env(safe-area-inset-bottom) + 50px);
  /*position: fixed;*/
  position:absolute;
  overflow: hidden;
  box-shadow: 0px 0 1px 1px #ccc;
  background: #fff;
  border-bottom: 1px solid #ccc;
  z-index: 1000;
  display: flex;
  left: 0;
  right: 0;
  bottom: 0;
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: constant(safe-area-inset-left);
  padding-left: env(safe-area-inset-left);
  padding-right: constant(safe-area-inset-right);
  padding-right: env(safe-area-inset-right);
}

封裝可用的阿里icon組件

<template>
      <i class="iconfont" :class="iconClass" :style="'font-size:'+ size +'px;color:'+color+';'"></i>
</template>
<script>
    export default {
      props: {
        iconClass: {
          type: String
        },
        size:{
            type:[Number,String],
        },
        color:{
          type:String
        }
      },
      data: function () {
          return {}
      },
    }
</script>
<style scoped>
  i{
    margin-right: 5px;
  }

</style>


* 複製阿里圖標庫的代碼到alifont.css,並在main.js 中引入

//引入阿里圖標
import "@/assets/icon/alifont.css"

使用

<icon  @click.native="cancel" class="left" :icon-class="leftClass" :size="20"></icon>

leftClass 是你在阿里icon上面拿到的name
相關文章
相關標籤/搜索