项目背景
手上的 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-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 中定义
<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 页面怎么刷新
这个需求很常见,有个列表页面,点击某一条去到详情页面,点击返回,列表页面保持状态不变,滚动条保持原来位置。如果,详情对数据做了改变,点击返回,列表页面才刷新。
* 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效果
- 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*/
使用
<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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。