按需引入mint-ui
本项目用了 mint-ui 作为基础ui框架,在使用中遇到不少问题。官网doc 还不断的访问不了。不过还是很感谢 mint-ui 团队。
在此推荐一个 vue移动端ui库 vant
- 按需引入
* 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 组件
列表的下拉刷新和上拉加载更多是移动端必须的组件。但是 mt的 loadmore组件有点问题,因此 我自己包了一层,让它变得更加
明了好用了
二次封装特点
- 模拟iphone 点击顶部 滚动列表到顶部。
- 不用写死高度了,并且兼容 iphoneX
- 对外提供了更加简明易用的 刷新,回到顶部,获得和设置滚动条位置的方法
- 统一的UI提示,免去重复css代码。
代码
<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
},
mySelect 组件
移动端 select 组件 实际 等于 popup.bottom + picker 两个组件组合出来的;
代码
<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 组件
popup 组件一般都是通过配置position 达到滑动进入或者底部出来或者中间弹窗的目的。唯一的害处是,如果你的页面有很多弹窗,你要设置很多变量 true/false 来控制弹窗隐现。所以在此我封装了一下。
- 减少css代码,组件配置
- 减少声明控制隐藏显示的变量
实现
<!--封装 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原来是一样的
时间控件封装
mint 的时间控件使用起来也比较麻烦,也做了二次封装,主要有以下特点
- 直接得到时间值字符串
- 自动绑定了open 和 close 方法
- 添加了取消,保存功能
- 支持初始化时间,动态设置时间值
代码
<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>
封装上传图片组件
上传图片也是常用组件,在这里自己实现了一下。
代码
<!--上传附件-->
<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>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。