手上的 vue移動端 項目已經開發了大幾個月了,遇到了一些頗有意思的坑,也讓本身學習了不少;寫此文主要目的是記下一些我遇到的坑,以及本身的解決方案,分享的同時也方便之後複習。css
項目的底層是上司經過 Cordova 等經常使用的 hybird app工具打包出來的。而後經過 webview 打開個人vue項目。因此嚴格意義上說,我仍是在作單頁面應用。 hybird app 的底層會提供一些api 給我調用,方便我關閉打開webview,或者跳轉到不一樣子頁面。hybird app會集成不一樣的業務。這些業務有hybird app本事的服務,也有像我這種,徹底來自其服務的頁面。這些就是項目大概的背景。html
提示:因爲是項目總結文章,可能總結點會比較混亂,部分前後,想到什麼寫什麼。
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---------------副頁面相關*/
這個問題我在 文章 中已經詳細說過。vue
我直接在 app.vue 中添加如下方法,運行後,你會在html 標籤中看到 fontsize 設置爲了50px; 表示 1rem = 50px;node
created() { this.resize(document, window); }, methods:{ /*設置rem參照單位。width:1rem = 50px 因此設計稿寬 375px == 375/50 = 7.5rem * 因爲頁面中有些元素用了絕對定位。特別是top,bottom。因爲設備不一樣,計算出的rem不一樣, * 致使定位覆蓋。因此,建議涉及高度的 統一用 px 作單位,包括padding-top,bottom等。 * 由於高度存在滾動條,不存在適配問題。主要針對寬度作適配。 * * */ resize(doc, win) { var docE1 = doc.documentElement, resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize', recalc = function () { var clientWidth = docE1.clientWidth; if (!clientWidth) return; //docE1.style.fontSize = clientWidth / 375 + 'px'; 這裏但願設置 1rem = 1px,實驗證實,這樣作 會致使 html 的 fontsize小於 12px docE1.style.fontSize = (clientWidth / (375*2)) * 100 + 'px'; //乘以100的意義是,1爲了避免受fontsize小於12的影響,2爲了計算方便; }; if (!doc.addEventListener) return; win.addEventListener(resizeEvt, recalc, false); doc.addEventListener('DOMContentLoaded', recalc, false); }, }
使用建議:
1,少許大小的定義儘可能使用px,由於對自適應效果影響不大。例如某個div的padding,設置爲5px 10px,影響是不大的。
2,寬度上的定義儘可能使用rem 做爲單位,由於移動設備對寬度敏感,可謂寸金寸土。設置了以上代碼後,能夠經過設計稿尺寸/50 獲得rem單位的數值。 例如 padding:10px; 能夠寫成 padding: 10px 0.2rem; 或者 padding:0.2rem;
3,高度上的定義,儘可能使用px;由於本項目能夠滾動內容頁,因此高度是不敏感的。設置爲px 的緣由是,後面定位 loadermore 組件會有幫助。固然,若是你對計算頗有把握,或者頁面內容不容許滾動,也可使用 rem;css3
遇到一個填寫表單點保存造成草稿模式的需求。要求在url中加入參數 id;刷新本頁面,從新經過id獲取數據回填。 vue 是單頁面應用,確定不能全局刷新。web
調用保存接口,獲取到id後, 經過ajax
this.router.push(this.$route.path + "&id=" + id);//加參數本頁並不會刷新
改變url ,而後從新申請 調用接口,拿到最新的數據,回填回去。
這樣作,理論上是行得通的。當時很危險,由於用戶操做頁面,會改變不少變量。若是回填數據後,因爲沒有經歷完整的created等生命週期,這些變量仍是原來狀態,容易出bug;
其次,若是像本項目那樣,須要支持 hybird app 經過url+id 的方式直接去到草稿的話,代碼很差維護。因此,最理想的作法,就是真實的從新
load 一次這個子頁面。vuex
利用vue 的provide / inject apisegmentfault
* app.vue 中定義 <router-view v-if="isRouterAlive"/> data() { return { isRouterAlive: true, } }, provide() { return { reload: this.reload, } }, methods: { reload() { this.isRouterAlive = false this.$nextTick(() => (this.isRouterAlive = true)) }, } * 須要刷新的子頁面 inject: ['reload'], //須要調用的地方 let path = this.$route.path+"?id="+id this.$router.replace(path); this.reload();
這個需求很常見,有個列表頁面,點擊某一條去到詳情頁面,點擊返回,列表頁面保持狀態不變,滾動條保持原來位置。若是,詳情對數據作了改變,點擊返回,列表頁面才刷新。api
* app.vue 中 <div id="app"> <keep-alive> <router-view v-if="isRouterAlive&&$route.meta.keepAlive"/> </keep-alive> <router-view v-if="isRouterAlive&&!$route.meta.keepAlive"/> </div> * route.js 中 { path: 'a',//個人草稿 name: 'myDraft', meta:{ keepAlive:true, }, component: resolve => require(['page/myDraft'],resolve) },
這樣,定義了meta keepAlive 爲true 的頁面就會被 緩存。數據不變的狀況下,點擊返回, 只要把滾動條位置設置到原來離開哪裏就行了。
可是問題來了,1,從首頁進入 keepAlive 頁面,每次都要刷新,二,詳情頁若是改變了數據,返回後也要刷新 頁面。
這裏我主要經過 eventBus 來解決了組件通知 頁面 刷新的問題。
細節能夠看 個人筆記,最好的實踐應該是最後提到大神的連接文章。
* topBar.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);
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 組件同步更新數據 },
<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: [],
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) },
移動端常見問題,緣由上網找找。特徵也比較明顯,就是視口高度改變了,某些手機會觸發 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 插件 封裝了一個組件
<!--快速點擊封裝--> <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>
focus 的時候,因爲底部的 mobile-bottom 部分是 absolute 的,因此被頂起來。
網上不少說法經過js判斷 onresize 事件 控制 底部顯示隱藏。能夠實現,可是存在兼容性問題。且代碼囉嗦
這裏直接經過css 媒體查詢實現了。
@media screen and (max-height: 450px) { .mobile-bottom{ display: none; } }
蘋果給出了 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); }
<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