项目背景

手上的 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, \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-moremin-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小于 12pxdocE1.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 中定义<router-view v-if="isRouterAlive"/>
data() {return {isRouterAlive: true,}},provide() {return {reload: this.reload,}},
methods: {reload() {this.isRouterAlive = falsethis.$nextTick(() => (this.isRouterAlive = true))},}* 需要刷新的子页面
inject: ['reload'],//需要调用的地方
let path =  this.$route.path+"?id="+id
this.$router.replace(path);
this.reload();

keep-alive 页面怎么刷新

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

* 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.vueprovide() {return {backHome:this.backHome}},
backHome(){ //返回或退出webviewlet  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{//关闭webViewcloseWebView();}}* 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.vueimport 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:{//选择的人员如果改变,就更新personhasSelectPerson(val){//当在 personInput 改变了 hasSelectPerson 数组的时候,手动同步 oldSelectedif(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 = resif(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效果

  • 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 和 -0if (a === b) return a !== 0 || 1 / a === 1 / b;// typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数if (a == null || b == null) return false;// 判断 NaNif (a !== a) return b !== b;// 判断参数 a 类型,如果是基本类型,在这里可以直接返回 falsevar 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]] 相同时 返回 truevar 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");},

效果

移动端快速点击

由于移动端浏览器存在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

记 vue 移动端开发 中的经验相关推荐

  1. CSS3: 移动端开发中 max-device-width 与 max-width 的区别

    翻译自stackoverflow.com,源地址:http://stackoverflow.com/questions/6747242/what-is-the-difference-between-m ...

  2. Vue CLI 3开发中试用UIkit 3组件库

    一.UIkit组件库与vuikit简介 在选择好意中的前端开发基本框架后,接下来一个重要任务就是选择一款好的UI组件库.其中,UIkit组件库是一款基于Less+JS的一款轻量级.模块化.响应式的前端 ...

  3. vue移动端h5中a标签下载/预览文件

    需求:项目分PC端和移动端,PC端(react)以实现列表页附件下载,现需同步移动端(vue)h5页面在原有的列表页中增加一行查看(下载)附件. 只写结构,暂不考虑其他,增加附件行的代码如下: < ...

  4. 用Abp实现两步验证(Two-Factor Authentication,2FA)登录(二):Vue网页端开发

    文章目录 发送验证码 登录 退出登录 界面控件 获取用户信息功能 项目地址 前端代码的框架采用vue.js + elementUI 这套较为简单的方式实现,以及typescript语法更方便阅读. 首 ...

  5. Vue CLI 3开发中屏蔽烦人的EsLint错误

    问题 Vue开发中,特别是当你阅读分析别人的其中早期版本的Vue代码时往往会遭遇到满屏幕的烦人的EsLint错误.有关EsLint这个工具的作用不再赘述.查阅网上参考文档,大多是针对早起版本Vue C ...

  6. VUE学习和开发中的注意点总结(一),便于回顾(不断完善补充。)

    1.export 和export default 的区别? 在JavaScript ES6中,export与export default均可用于导出常量.函数.文件.模块等,你可以在其它文件或模块中通 ...

  7. 产品开发中的经验教训

    产品开发是一项有趣的活动. 它涉及很多挑战和很多学习. 但是随着时间的流逝,我们将获得很多重要的经验教训. 在这篇文章中,我将根据我在产品开发方面的经验来分享一些经验. 寻求MVP而非完整的产品 根据 ...

  8. vue axios在开发中遇到的问题

    第一次学vue,自己摸索了好久.用axios的时候遇到了几个问题.跨域,还有就是多个proxyTable配置的时候出现的问题.记下,方便以后查阅 var _this=this;_this.$axios ...

  9. 一次二次开发中的经验与教训(二)

    更悲催的是,他们要求我上班时间必须在他们公司.只有下班后,我才能回到办事处,进行编码工作.但这时同事已经下班了.  在这一点上,我做的不够,没有想到将情况实时的报告给经理)就这样导致我们在开发中遇到问 ...

最新文章

  1. 实现影响力的三个核心要素
  2. 新手搭ssm要多久_如何快速学习ssm 框架?
  3. JavaScript进阶2-学习笔记
  4. Python入门学习指南--内附学习框架
  5. python内存管理错误的是_解读Python内存管理机制(转载)
  6. oracle计算两个日期的时间差时分秒
  7. python gmm em算法 2维数据_AI大语音(六)——混合高斯模型(GMM)(深度解析)...
  8. getAttribute、setAttribute、removeAttribute
  9. 大数据开源舆情分析系统-数据采集技术架构浅析
  10. 佐治亚理工计算机科学,Gatech的CS「佐治亚理工学院计算机科学系」
  11. 天猫淘宝越来越难做了,为什么不考虑下跨境电商?
  12. 2022跨年烟花代码|用Python送你一场跨年烟花秀
  13. 2019年的阅读书单,夯实我的技术栈
  14. 智能交通系统功能模块
  15. jQuery 选择器 选取所有元素
  16. html的slider怎么在图片下面,javascript – HTML5滑块.如何使用’noUiSlider’
  17. 51单片机的轮胎气压监测系统_汽车轮胎压力监测系统
  18. 中兴服务器电源 ppc22,中兴ZXDU68B201技术参数报价中兴48v高频嵌入式电源
  19. 腾达AC7拆解,有一根天线是假的笑死我了
  20. 20159313网络攻击与防范第六周学习总结

热门文章

  1. python大众点评网实训报告中的参考文献_python小练习(052):爬取大众点评网美食版块+数据库储存+大数据分析(二)...
  2. 在 Kubernetes 实施混沌工程—— Chaos Mesh® 原理分析与控制面开发
  3. 最新公众号今日头条自媒体视频课程
  4. Kotlin第一弹:Kotlin详细介绍
  5. 合伙人股权设计的9点常识
  6. iOS 更新自动布局,获取子视图frame
  7. [Untiy]贪吃蛇大作战(五)——游戏主界面
  8. 交换机、路由器、网桥、集线器 作用和区别
  9. Python(详解)找出一个整数的所有因子---显示所有的最小因子--素因子
  10. 近视手术?一个医学的阴谋?