资讯专栏INFORMATION COLUMN

记 vue 移动端开发 中的经验

shuibo / 2828人阅读

摘要:中中我的草稿这样,定义了为的页面就会被缓存。但是移动端开发不能用树,通常就是像百度网盘那样,类型文件夹的方式交互。

项目背景

手上的 vue移动端 项目已经开发了大几个月了,遇到了一些很有意思的坑,也让自己学习了很多;写此文主要目的是记下一些我遇到的坑,以及自己的解决方案,分享的同时也方便以后复习。

项目的底层是上司通过 Cordova 等常用的 hybird app工具打包出来的。然后通过 webview 打开我的vue项目。所以严格意义上说,我还是在做单页面应用。 hybird app 的底层会提供一些api 给我调用,方便我关闭打开webview,或者跳转到不同子页面。hybird app会集成不同的业务。这些业务有hybird app本事的服务,也有像我这种,完全来自其服务的页面。这些就是项目大概的背景。

提示:由于是项目总结文章,可能总结点会比较混乱,部分先后,想到什么写什么。
移动端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, 5b8b4f53; }
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定位问题。

这个问题我在 文章 中已经详细说过。

rem 的使用;

我直接在 app.vue 中添加以下方法,运行后,你会在html 标签中看到 fontsize 设置为了50px; 表示 1rem = 50px;

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);
      },
}    

使用建议:
1,少量大小的定义尽量使用px,因为对自适应效果影响不大。例如某个div的padding,设置为5px 10px,影响是不大的。
2,宽度上的定义尽量使用rem 作为单位,因为移动设备对宽度敏感,可谓寸金寸土。设置了以上代码后,可以通过设计稿尺寸/50 得到rem单位的数值。 例如 padding:10px; 可以写成 padding: 10px 0.2rem; 或者 padding:0.2rem;
3,高度上的定义,尽量使用px;因为本项目可以滚动内容页,所以高度是不敏感的。设置为px 的原因是,后面定位 loadermore 组件会有帮助。当然,如果你对计算很有把握,或者页面内容不允许滚动,也可以使用 rem;

刷新某个子页面

遇到一个填写表单点保存形成草稿模式的需求。要求在url中加入参数 id;刷新本页面,重新通过id获取数据回填。 vue 是单页面应用,肯定不能全局刷新。

同事的解决方案

调用保存接口,获取到id后, 通过

this.router.push(this.$route.path + "&id=" + id);//加参数本页并不会刷新

改变url ,然后重新申请 调用接口,拿到最新的数据,回填回去。
这样做,理论上是行得通的。当时很危险,因为用户操作页面,会改变很多变量。如果回填数据后,由于没有经历完整的created等生命周期,这些变量还是原来状态,容易出bug;
其次,如果像本项目那样,需要支持 hybird app 通过url+id 的方式直接去到草稿的话,代码不好维护。所以,最理想的做法,就是真实的重新
load 一次这个子页面。

正确做法

利用vue 的provide / inject api

* app.vue 中定义
 
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 页面怎么刷新

这个需求很常见,有个列表页面,点击某一条去到详情页面,点击返回,列表页面保持状态不变,滚动条保持原来位置。如果,详情对数据做了改变,点击返回,列表页面才刷新。

* app.vue 中
* 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 核心代码




父组件使用

 

 

permitMen: [],
效果

input效果

personBox 效果

比较两个对象是否相等
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*/
使用


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

效果

移动端快速点击

由于移动端浏览器存在300ms 延迟,某些组件需要快速响应点击事件,例如 - 0 + 组件;
利用 fastclick 插件 封装了一个组件

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





* 复制阿里图标库的代码到alifont.css,并在main.js 中引入

//引入阿里图标
import "@/assets/icon/alifont.css"
使用
 

leftClass 是你在阿里icon上面拿到的name

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/102438.html

相关文章

  • vue 移动开发 中的经验(2)

    摘要:官网还不断的访问不了。在此推荐一个移动端库按需引入二次封装组件列表的下拉刷新和上拉加载更多是移动端必须的组件。不用写死高度了,并且兼容对外提供了更加简明易用的刷新,回到顶部,获得和设置滚动条位置的方法统一的提示,免去重复代码。 按需引入mint-ui 本项目用了 mint-ui 作为基础ui框架,在使用中遇到不少问题。官网doc 还不断的访问不了。不过还是很感谢 mint-ui 团队。...

    Flands 评论0 收藏0
  • 2018.11.19秋招末第二波前实习/校招小结

    摘要:背景个人背景就读于东北某普通二本院校计算机软件工程专业,现大四,北京实习前端方向,自学,技术栈时间背景大概是在月日准备好简历开始投递秋招差不多已经结束招聘岗位不多,投递对象为大一些的互联网公司事件背景第一个入职的是好未来的前端实习岗,待遇工 背景 个人背景 就读于东北某普通二本院校计算机软件工程专业,现大四,北京实习 前端方向,自学,vue技术栈 时间背景 大概是在11月9日准备...

    suxier 评论0 收藏0
  • 2018.11.19秋招末第二波前实习/校招小结

    摘要:背景个人背景就读于东北某普通二本院校计算机软件工程专业,现大四,北京实习前端方向,自学,技术栈时间背景大概是在月日准备好简历开始投递秋招差不多已经结束招聘岗位不多,投递对象为大一些的互联网公司事件背景第一个入职的是好未来的前端实习岗,待遇工 背景 个人背景 就读于东北某普通二本院校计算机软件工程专业,现大四,北京实习 前端方向,自学,vue技术栈 时间背景 大概是在11月9日准备...

    canger 评论0 收藏0

发表评论

0条评论

shuibo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<