本項目用了 mint-ui 做爲基礎ui框架,在使用中遇到很多問題。官網doc 還不斷的訪問不了。不過仍是很感謝 mint-ui 團隊。
在此推薦一個 vue移動端ui庫 vantcss
* mint-ui import 'mint-ui/lib/style.css' import { Navbar, TabItem, TabContainer, TabContainerItem, Radio, Actionsheet, Switch, Popup, Button, DatetimePicker, Toast, Picker, MessageBox, loadmore, Range, Progress, Indicator, } from 'mint-ui' Vue.component(Navbar.name, Navbar) Vue.component(TabItem.name, TabItem) Vue.component(TabContainer.name, TabContainer) Vue.component(TabContainerItem.name, TabContainerItem) Vue.component(Radio.name, Radio) Vue.component(Actionsheet.name, Actionsheet) Vue.component(Popup.name, Popup) Vue.component(Button.name, Button) Vue.component(DatetimePicker.name, DatetimePicker) Vue.component(Picker.name, Picker); Vue.component(loadmore.name, loadmore); Vue.component(Range.name, Range); Vue.component(Progress.name, Progress); Vue.component(Switch.name, Switch);
列表的下拉刷新和上拉加載更可能是移動端必須的組件。可是 mt的 loadmore組件有點問題,所以 我本身包了一層,讓它變得更加
明瞭好用了vue
<template> <div class="loader-more" ref="loadBox"> <mt-loadmore :topMethod="topMethod" :bottomMethod="bottomMethod" :topPullText="`下拉刷新`" :bottomPullText="`上拉加載更多`" :autoFill="false" :bottomDistance="40" :topDistance="60" :bottomAllLoaded="bottomAllLoaded" ref="loadmore"> <ul class="load-more-content" v-if="rows.length>0"> <slot v-for="(item,index) in rows" v-bind="{item,index}"></slot> </ul> <ul class="load-more-content" v-else> <li class="no-data">{{loadingText}}</li> </ul> </mt-loadmore> </div> </template> <script> import Bus from "../common/bus.js" export default { data: function () { return { rows: [], loadingText: '', total: 0, bottomAllLoaded:false, timer:null, search: { page: 1, size: 10, }, } }, props: { top:{ type:[Number,String], default:0 }, bottom:{ type:[Number,String], default:0 }, itemProcess:{ //列表項目處理函數 type:Function, default:null }, url:{ type:String, default:"" }, param:{ //查詢參數 type:Object, default:{} }, type:{ //配置ajax方法類型 type:String, default:"get" }, dataKey:{ //讀取接口的數據的key type:String, default:"content" }, clickToTop:{ //是否開啓點擊頂部回到開始 type:Boolean, default:true, }, }, watch:{ rows(val){ this.$emit('change',val); } }, mounted(){ setTimeout( ()=>{ var myDiv = document.getElementsByClassName('mobile-top')[0]; //利用判斷是否支持currentStyle(是否爲ie)來經過不一樣方法獲取style var finalStyle = myDiv.currentStyle ? myDiv.currentStyle : document.defaultView.getComputedStyle(myDiv, null); //iphoneX 多出來的paddingTop var iphoneXPT = parseInt(finalStyle.paddingTop)==20?0:parseInt(finalStyle.paddingTop)-20; this.$refs.loadBox.style.top = parseInt(this.top) + iphoneXPT +"px"; this.$refs.loadBox.style.bottom = parseInt(this.bottom) + iphoneXPT +"px"; },100) //延遲執行,fixed 獲取不到paddingTop的bug this.search = Object.assign(this.search,this.param); this.upData(); if(this.clickToTop){ Bus.$on('toTop', () => { this.toTop(); }) } }, watch:{ param(val){ this.search = Object.assign(this.search,val); } }, methods:{ upData(data) { /*若是參數是對象,watch更新param會update方法以後執行,致使參數合併不許確bug*/ return new Promise((resolve,reject)=>{ setTimeout(()=>{ this.loadingText = "加載中..."; var query = Object.assign(this.search, data); return this.$http({ url: this.url, data: query, type:this.type, loading:false, }).then(res => { let rows = res[this.dataKey]; this.total = res.total; if (rows.length > 0) { if(typeof this.itemProcess == 'function'){ rows = this.itemProcess(rows); } this.rows = this.rows.concat(rows); } if (this.rows.length == 0) { this.loadingText = "暫無數據" } resolve(true) }) },100) }) }, //下拉刷新 topMethod() { this.bottomAllLoaded = false; this.rows = []; this.upData({ page: 1 }).then(res => { if (res) { this.ToastTip("刷新成功", 'suc'); this.$refs.loadmore.onTopLoaded(); } }) }, //上拉加載更多 bottomMethod() { if (this.rows.length < this.total) { this.bottomAllLoaded = false; this.upData({ page: ++this.search.page }).then(()=>{ this.$refs.loadmore.onBottomLoaded(); }) } else { this.bottomAllLoaded = true; this.ToastTip("沒有更多數據了!") this.$refs.loadmore.onBottomLoaded(); } }, refresh(){ this.bottomAllLoaded = false; this.rows = []; this.upData({ page: 1 }).then(res => { if (res) { this.$refs.loadmore.onTopLoaded(); } }) }, //對外提供控制上拉刷新 allLoad(bool){ this.bottomAllLoaded = bool; }, //清空數據 clearData(){ this.rows = []; }, //處理item的函數,方便父組件對列表項目操做 processData(callBack){ callBack(this.rows); }, //點擊頂部標題滾動到列表開頭 toTop(){ var app = document.getElementsByClassName('scrolling')[0]||document.getElementsByTagName('body')[0]; app.className ="";/*fix 移動端因爲慣性滑動形成頁面顫抖的bug*/ clearInterval(this.timer); this.timer =setInterval(()=>{ var scrollTop= this.$el.scrollTop; var ispeed=Math.floor(-scrollTop/8); if(scrollTop==0){ app.className ="scrolling"; clearInterval(this.timer); } this.$el.scrollTop = scrollTop+ispeed; },10); /*fix 上拉未完成時,拉動列表,致使重複上提的bug*/ document.addEventListener('touchstart',(ev)=>{ if(this.$refs['loadBox']&&this.$refs['loadBox'].contains(ev.changedTouches[0].target)){ app.className ="scrolling"; clearInterval(this.timer); } }) }, //獲取當前滾動位置 getPosition(){ return this.$el.scrollTop; }, //設置滾動位置 setPosition(position=0){ this.$el.scrollTop = position; } } } </script> <style lang="scss" scoped> .loader-more { padding-bottom: 0.2rem; background-color: #fff; overflow-y: auto; /*position: fixed;*/ position: absolute; left: 0; right: 0; box-sizing: border-box; } </style>
<myLoadMore class="t-body" :url="ajaxApi.docSearch.draft" :param="param" top="65px" ref="myLoadMore" :itemProcess="itemProcess"> <li slot-scope="{item}" class="row-box" :key="item.id" @click="toDetail(item.id,item.serviceCode)"> <div class="row title">{{item.time}}</div> </li> </myLoadMore> //列表出來函數 itemProcess(rows) { rows.forEach(item => { item.time= new Date().getTime(); }) return rows },
移動端 select 組件 實際 等於 popup.bottom + picker 兩個組件組合出來的;ios
<template> <div> <div class="selected" @click="show"> <span style="margin-right: 10px;">{{name}}</span> <v-icon name="chevron-down"></v-icon> </div> <mt-popup class="selected-box" v-model="popupVisible" position="bottom" style="width: 100%;" :closeOnClickModal="false"> <div class="picker-toolbar flex-ar"> <span @click="cancel">取消</span> <span @click="selected">肯定</span> </div> <mt-picker v-show="popupVisible" :slots="slots" @change="onValuesChange" :value-key="keyName" ref="picker" :visibleItemCount="visibleItemCount"> </mt-picker> </mt-popup> </div> </template> <script> export default { data: function () { return { popupVisible: false, name:'', value:'', oldName:'', oldValue:'', defaultItem:null, slots: [{ values:[], defaultIndex: 0, }], } }, model:{ prop:'selectValue', event:'change' }, props: { selectValue:{ type:[Number,String] }, dataArr: { type: Array, default: function () { return [] } }, keyName:{ //顯示名 type:String, default:'name' }, keyValue:{ type:String, default:'value' }, visibleItemCount:{ type:Number, default:5 }, defaultIndex:{//默認選中項 type:Number, default:0 } }, watch:{ popupVisible(val){ var bottom = document.getElementsByClassName("mobile-bottom"); if(val){ for(var i=0;i<bottom.length;i++){ bottom[i].style.display = "none"; } } else { for(var i=0;i<bottom.length;i++){ bottom[i].style.display = "flex"; } } }, }, created() { this.slots[0].values = this.dataArr; this.slots[0].defaultIndex = this.defaultIndex; this.defaultItem = { name:this.slots[0].values[this.defaultIndex][this.keyName], value:this.slots[0].values[this.defaultIndex][this.keyValue], }; }, methods: { show(){ this.oldName = this.name; this.oldValue = this.value; this.noScrollAfter.open(this,`popupVisible`) }, cancel(){ this.name = this.oldName; this.value = this.oldValue; this.popupVisible=false; }, selected(){ this.noScrollAfter.close(this,`popupVisible`) this.oldName = this.name; this.oldValue = this.value; this.$emit('change',this.value);//把value傳到父 this.$emit('select',{name:this.name,value:this.value}) }, onValuesChange(picker, values) { this.name = values[0][this.keyName]; this.value = values[0][this.keyValue]; }, set(index){ //設置選中值index let theIndex = index || this.defaultIndex; this.name = this.slots[0].values[theIndex][this.keyName]; this.value = this.slots[0].values[theIndex][this.keyValue]; this.slots[0].defaultIndex = index; this.selected();//同步父組件數據; }, } } </script> <style lang="scss" scoped> .selected{ padding: 0.1rem; text-align: right; display: flex; align-items: center; justify-content: flex-end; } .selected-box{ user-select: none; z-index: 3000!important; position:fixed; right: 0; bottom: 0; } .picker-toolbar{ height: 40px; border-bottom: solid 1px #eaeaea; color: #26a2ff; } </style>
<my-select :dataArr="leaveTypeData" keyName="enumerationName" keyValue="enumerationCode" v-model="leaveType" ref="mySelect" @select="select"> </my-select> //設置選中 this.$refs['mySelect'].setTime(index);
popup 組件通常都是經過配置position 達到滑動進入或者底部出來或者中間彈窗的目的。惟一的害處是,若是你的頁面有不少彈窗,你要設置不少變量 true/false 來控制彈窗隱現。因此在此我封裝了一下。git
<!--封裝 mint-ui 的彈窗組件,不須要一個個定義變量和方法來控制 彈窗的顯示隱藏 * position: right 從右邊劃出彈窗 * radius:是否圓角彈窗 * 打開彈窗: this.$refs[`你定義的popup的ref`].open() * 關閉彈窗: this.$refs[`你定義的popup的ref`].close() --> <template> <mt-popup v-model="visible" :class="{radiusPopup:radius,wh100:!radius}" :modal="radius" :closeOnClickModal="false" :popup-transition="radius?`popup-fade`:``" :position="position"> <slot></slot> </mt-popup> </template> <script> export default { data: function () { return { visible: false } }, props:{ position:{ type:String, default:"" }, radius:{ type:Boolean, default:true } }, methods:{ open(){ this.noScrollAfter.open(this,`visible`) }, close(){ this.noScrollAfter.close(this,`visible`) }, state(){ return this.visible; } } } </script> <style lang="scss" scoped> </style>
<popup ref="exceptionFlow" position="right" :radius="false"> xxxx </popup> //打開 this.$refs['exceptionFlow'].open(); //關閉 this.$refs['exceptionFlow'].close();
positon的值跟mint原來是同樣的
github
mint 的時間控件使用起來也比較麻煩,也作了二次封裝,主要有如下特色ajax
<template> <div class="timer"> <div class="item-content"> <div class="item-content-div" v-show="confirmTimeStart" @click="open"> <v-icon class="item-content-icon" v-if="delTime" v-show="confirmTimeStart" name="x-circle" @click.native.stop="confirmTimeStart = false"></v-icon> {{timeStartFmt}} </div> <div class="item-content-div" v-show="!confirmTimeStart" @click="open"></div> <v-icon class="item-content-icon" name="calendar" @click.native="open"></v-icon> </div> <mt-datetime-picker ref="timePicker" :type="dateType" @cancel=" timeStart = oldTimeStart;close();" @visible-change="oldTimeStart = timeStart;$emit(`timeChange`)" @confirm="confirmTime" v-model="timeStart"> </mt-datetime-picker> </div> </template> <script> export default { data: function () { return { timeStart:new Date(), confirmTimeStart:false, } }, model:{ prop:'time', events:'change', }, props:{ dateType:{ //時間控件類型 type:String, default:"date", }, initDate:{//是否默初始化並認選中今天 type:Boolean, default:false, }, time:{ type:String, default:'' }, delTime:{ //是否顯示清空時間按鈕 type:Boolean, default:true, } }, watch:{ //確認選擇時間和取消 confirmTimeStart(val){ if(val){ this.$emit("confirm",this.timeStartFmt); }else{ this.$emit("confirm",""); } } }, computed: { //格式化時間 timeStartFmt() { let fmt = this.dateType=="date"?"yyyy-MM-dd":null; return this.tools.dateFmt(this.timeStart,fmt); }, }, mounted(){ if(this.initDate){ this.confirmTime(); } }, methods:{ //改變時間時; confirmTime(){ this.confirmTimeStart = true; this.$emit("confirm",this.timeStartFmt); this.close(); }, /** * 做者:lzh * 功能:設置時間,供父組件調用的方法,配合ref調用; * 參數:val DateObj * 返回值: */ setTime(val){ this.timeStart = val; this.confirmTimeStart =val!==""?true:false; }, open(){ var bottom = document.getElementsByClassName("mobile-bottom"); this.$refs[`timePicker`].open(); for(var i=0;i<bottom.length;i++){ bottom[i].style.display = "none"; } }, close(){ var bottom = document.getElementsByClassName("mobile-bottom"); for(var i=0;i<bottom.length;i++){ bottom[i].style.display = "flex"; } }, } } </script> <style lang="scss" scoped> .timer{ .item-content{ width: 100%; height: 30px; display: flex; justify-content: space-between; align-items: center; .item-content-div{ flex:10; border: 1px solid #eaeaea; padding: 5px 25px 5px 5px; box-sizing: border-box; height: 100%; position:relative; .item-content-icon{ position:absolute; right:5px; color: #d8d8d8; } } .icon { margin-left: 10px; width: 17px; height: 17px; } } } </style>
<timer @confirm="(val)=>{startTime = val}"></timer>
上傳圖片也是經常使用組件,在這裏本身實現了一下。axios
<!--上傳附件--> <template> <div> <form-card-item itemTitle="上傳附件:" :required="required" class="box"> <input ref="uploadInput" type="file" @change="upload" style="padding-right: 0.5rem;"> <v-icon v-show="uploading" class="stop" name="x-circle" @click.native.stop="clearFile"></v-icon> <progressDom ref="progressId"></progressDom> </form-card-item> <adjunct ref="list" @delFile="del"></adjunct> </div> </template> <script> import qs from "qs" import axios from "axios" export default { data: function () { return { all:'all', pic:["jpg","jpeg","gif","png"], gzip:["zip","rar"], uploading:false, } }, model:{ prop:'adjunct', event:'change' }, props:{ adjunct:{ //上傳附件個數 type:Number, default:0, }, data:{ type:Object, default:()=>{return {} } }, types:{ type:String, default:"all" }, required:{ type:Boolean, default:false, }, saveParam:{ type:Object, default:()=>{return {} } } }, methods: { upload() { let file = this.$refs[`uploadInput`].files[0]; if (!file){ this.$emit('change',false); return; }; let type = this[this.types]; if(type!=='all'&&type.indexOf(file.type.split(`/`)[1])==-1){ this.ToastTip("請上傳如下類型附件: "+type.join(","), "warn",5000); this.$refs[`uploadInput`].value = ""; return; } if (file.size /(1024*1024) > 50) { //size 是bt單位 1kb = 1024bt; this.ToastTip("請上傳50M之內大小的圖片", "warn"); this.$refs[`uploadInput`].value = ""; return; } let form = new FormData(); form.append("file", file); let actionUrl = process.env.proxyString + this.ajaxApi.attachment.upload + '?' + qs.stringify(this.saveParam); this.$refs[`progressId`].start(); this.uploading = true; axios.post(actionUrl, form).then((res) => { if (res.status==200&&res.data) { this.ToastTip("附件上傳成功","suc"); this.updateList(); this.$refs[`uploadInput`].value = ""; let num = this.adjunct+1; this.$emit('change',num); this.$emit("success"); } else { let msg = data.msg||data.messages||"上傳出錯"; this.$refs[`uploadInput`].value = ""; this.ToastTip(msg, "warn"); } this.$refs[`progressId`].stop(); this.uploading = false; }).catch(res=>{ console.log(res) }) }, clearFile(){ this.$refs[`uploadInput`].value = ""; this.$refs[`progressId`].stop(); this.uploading = false; }, del(length){ this.$emit('change',length);//覆蓋附件個數 }, updateList(){ if(this.saveParam&&this.saveParam.docid){ this.$refs['list'].updateList({ url:this.ajaxApi.attachment.attachmentList, type:'post', data: { docid:this.saveParam.docid, tid:this.saveParam.taskId, device:'mobile', service:this.saveParam.service } }); } } } } </script> <style lang="scss" scoped> .box.form-item{ padding-top: 16px; padding-bottom: 16px; } .box /deep/{ .form-item-value{ position: relative; } .stop { margin-left: 10px; width: 17px; height: 17px; position:absolute; right: 18px; top: 12px; color: #d8d8d8; } } </style> * adjunct.vue <!--文檔附件--> <template> <form-card :title="title" class="mb20" v-show="list.length>0"> <v-icon name="paperclip" slot="title-icon" style="color:#8a8a8a;margin-right: 0.1rem;"></v-icon> <form-card-item class="list" v-if="list.length>0" v-for="item in list" :itemTitle="item.name" :key="item.id"> <icon icon-class="icon-huixingzhen" color="#59a5ff" size="20" slot="before-title-icon"></icon> <icon v-show="icon==`download`" icon-class="icon-xiazai1" color="#306bd3" size="28" @click.native="download(item)"></icon> <v-icon v-show="icon==`del`" name="trash-2" style="color:#8a8a8a;margin-top: 10px;" @click.native="del(item)"></v-icon> </form-card-item> </form-card> </template> <script> export default { data: function () { return { list:[] } }, props:{ title:{ type:String, default:'文檔附件' }, icon:{ type:String, default:'del' } }, methods:{ updateList(param){ this.$http(param).then(res=>{ this.list = res.files; this.$emit('delFile',this.list.length); }) }, del(item){ this.MessageBox({ closeOnClickModal:false, showCancelButton:true, confirmButtonText:'肯定', title:'刪除文件', message:'肯定要刪除該文件嗎?', }).then((res)=>{ if(res=="confirm"){ this.$http({ url:this.ajaxApi.attachment.delAttachment, type:"post", data:{ docid:item.documentId, fileId:item.id } }).then(res=>{ this.ToastTip(res.result,'suc'); this.list.splice(this.list.findIndex(o=>{ return o.id == item.id }),1); this.$emit('delFile',this.list.length); }) }; }) }, download(item){ } }, } </script> <style lang="scss" scoped> .mb20 /deep/ .form-item{ .form-item-value{ width: auto; } } .list{ /deep/ .form-item-title{ word-break: break-all; max-width: 6rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; } } </style>
<!--上傳附件--> <uploadFile class="text" ref="uploadFile" :saveParam="saveParam" v-model="adjunct" :required="true"> </uploadFile>